From a8ce82f1d8883df347747fb335bd136bc0b0dd38 Mon Sep 17 00:00:00 2001 From: Elvira Burchik Date: Wed, 1 Jun 2022 16:03:56 +0200 Subject: [PATCH 1/6] fix: use instance level onRenderComplete callbacks instead of one global listener --- .../PerformanceMarker.swift | 21 ++-- .../PerformanceMarkerManager.m | 1 + .../RenderCompletionEventEmitter.m | 5 - .../RenderCompletionEventEmitter.swift | 49 -------- .../src/PerformanceMarker.tsx | 13 ++ .../src/PerformanceMeasureView.tsx | 20 ++- .../__tests__/PerformanceMeasureView.test.tsx | 35 ++++++ .../context/PerformanceProfiler.test.tsx | 18 --- .../useNativeRenderCompletionEvents.test.tsx | 117 ------------------ .../src/context/PerformanceProfiler.tsx | 3 - .../useNativeRenderCompletionEvents.ts | 42 ------- 11 files changed, 80 insertions(+), 244 deletions(-) delete mode 100644 packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.m delete mode 100644 packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.swift delete mode 100644 packages/react-native-performance/src/__tests__/context/useNativeRenderCompletionEvents.test.tsx delete mode 100644 packages/react-native-performance/src/context/useNativeRenderCompletionEvents.ts diff --git a/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift b/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift index 1be410b..cd3879f 100644 --- a/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift +++ b/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift @@ -6,6 +6,9 @@ import Foundation private var interactive: Interactive? = nil private var destinationScreen: String? = nil private var componentInstanceId: String? = nil + + @objc(onRenderComplete) + var onRenderComplete: RCTDirectEventBlock? @objc func setComponentInstanceId(_ componentInstanceId: String) { assertSetOnlyOnce(currentVal: self.componentInstanceId, newVal: componentInstanceId, propertyName: "componentInstanceId") @@ -55,14 +58,16 @@ import Foundation reportedOnce = true - RenderCompletionEventEmitter.INSTANCE?.onRenderComplete( - destinationScreen: destinationScreen, - renderPassName: renderPassName, - interactive: interactive, - componentInstanceId: componentInstanceId - ) ?? assertionFailure( - "RenderCompletionEventEmitter.INSTANCE was not initialized by the time PerformanceMarker got rendered for screen " + - "'\(destinationScreen)', renderPassName '\(renderPassName)'.") + let timestamp = Timestamp.nowMillis() + let onRenderCompleteEvent = [ + "timestamp": String(timestamp), + "renderPassName": renderPassName, + "interactive": interactive.description, + "destinationScreen": destinationScreen, + "componentInstanceId": componentInstanceId + ] + + onRenderComplete?(onRenderCompleteEvent) } } diff --git a/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarkerManager.m b/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarkerManager.m index 655607f..6d34016 100644 --- a/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarkerManager.m +++ b/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarkerManager.m @@ -7,5 +7,6 @@ @interface RCT_EXTERN_MODULE(PerformanceMarkerManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(interactive, NSString) RCT_EXPORT_VIEW_PROPERTY(destinationScreen, NSString) RCT_EXPORT_VIEW_PROPERTY(componentInstanceId, NSString) +RCT_EXPORT_VIEW_PROPERTY(onRenderComplete, RCTDirectEventBlock) @end diff --git a/packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.m b/packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.m deleted file mode 100644 index 74423d9..0000000 --- a/packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.m +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface RCT_EXTERN_MODULE(RenderCompletionEventEmitter, RCTEventEmitter) -RCT_EXTERN_METHOD(supportedEvents) -@end diff --git a/packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.swift b/packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.swift deleted file mode 100644 index 83ecce4..0000000 --- a/packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation - -@objc(RenderCompletionEventEmitter) -class RenderCompletionEventEmitter: RCTEventEmitter { - private static let RENDER_COMPLETION_EVENT_NAME = "@shopify/react-native-performance/onRenderComplete" - private var hasListeners = false - private(set) static var INSTANCE: RenderCompletionEventEmitter? = nil - - override init() { - super.init() - RenderCompletionEventEmitter.INSTANCE = self - } - - @objc override func supportedEvents() -> [String]! { - return [RenderCompletionEventEmitter.RENDER_COMPLETION_EVENT_NAME] - } - - func onRenderComplete( - destinationScreen: String, - renderPassName: String, - interactive: Interactive, - componentInstanceId: String - ) { - if (hasListeners) { - let timestamp = Timestamp.nowMillis() - sendEvent(withName: RenderCompletionEventEmitter.RENDER_COMPLETION_EVENT_NAME, body: [ - "timestamp": String(timestamp), - "renderPassName": renderPassName, - "interactive": interactive.description, - "destinationScreen": destinationScreen, - "componentInstanceId": componentInstanceId - ]) - } - } - - @objc override static func requiresMainQueueSetup() -> Bool { - return true - } - - @objc override func startObserving() { - super.startObserving() - hasListeners = true - } - - @objc override func stopObserving() { - super.stopObserving() - hasListeners = false - } -} diff --git a/packages/react-native-performance/src/PerformanceMarker.tsx b/packages/react-native-performance/src/PerformanceMarker.tsx index 6d28410..b366709 100644 --- a/packages/react-native-performance/src/PerformanceMarker.tsx +++ b/packages/react-native-performance/src/PerformanceMarker.tsx @@ -4,12 +4,25 @@ import {ViewStyle, requireNativeComponent, HostComponent} from 'react-native'; // So we are using strings. Legal values: `TRUE` and `FALSE`. export type Interactive = 'TRUE' | 'FALSE'; +export interface RenderCompletionEvent { + nativeEvent: { + timestamp: string; + renderPassName: string; + interactive: Interactive; + destinationScreen: string; + componentInstanceId: string; + }; +} + +type OnRenderCompletionEventHandler = (event: RenderCompletionEvent) => void; + export interface PerformanceMarkerProps { componentInstanceId: string; renderPassName: string; interactive: Interactive; destinationScreen: string; style: ViewStyle; + onRenderComplete: OnRenderCompletionEventHandler; } /** diff --git a/packages/react-native-performance/src/PerformanceMeasureView.tsx b/packages/react-native-performance/src/PerformanceMeasureView.tsx index a84d556..004b9cd 100644 --- a/packages/react-native-performance/src/PerformanceMeasureView.tsx +++ b/packages/react-native-performance/src/PerformanceMeasureView.tsx @@ -1,8 +1,8 @@ -import React, {ReactNode, useEffect, useState, useRef} from 'react'; +import React, {ReactNode, useEffect, useState, useRef, useCallback} from 'react'; import {InteractionManager, StyleSheet} from 'react-native'; import {inMemoryCounter} from './utils'; -import {getPerformanceMarker} from './PerformanceMarker'; +import {getPerformanceMarker, RenderCompletionEvent} from './PerformanceMarker'; import {StateController, useStateController} from './state-machine'; export const DEFAULT_NON_INTERACTIVE_RENDER_PASS_NAME = 'loading'; @@ -70,6 +70,21 @@ const PerformanceMeasureView = ({ useTrackComponentMounts({stateController, screenName, componentInstanceId}); + const onRenderComplete = useCallback( + (event: RenderCompletionEvent) => { + const {timestamp, renderPassName, interactive, destinationScreen, componentInstanceId} = event.nativeEvent; + + stateController.onRenderPassCompleted({ + timestamp: Number.parseFloat(timestamp), + destinationScreen, + renderPassName, + interactive: interactive === 'TRUE', + componentInstanceId, + }); + }, + [stateController], + ); + if (stateController.isEnabled) { if (show) { const PerformanceMarker = getPerformanceMarker(); @@ -82,6 +97,7 @@ const PerformanceMeasureView = ({ interactive={interactive ? 'TRUE' : 'FALSE'} renderPassName={renderPassName} style={styles.invisible} + onRenderComplete={onRenderComplete} /> {children} diff --git a/packages/react-native-performance/src/__tests__/PerformanceMeasureView.test.tsx b/packages/react-native-performance/src/__tests__/PerformanceMeasureView.test.tsx index 1addcc5..f755b27 100644 --- a/packages/react-native-performance/src/__tests__/PerformanceMeasureView.test.tsx +++ b/packages/react-native-performance/src/__tests__/PerformanceMeasureView.test.tsx @@ -324,4 +324,39 @@ describe('PerformanceMeasureView', () => { expect(stateController.onScreenUnmounted).not.toHaveBeenCalledBefore(stateController.onScreenMounted); }); + + it('notifies the state controller when the screen is rendered', () => { + inMemoryCounterMock.mockReturnValueOnce('mock-mount-id'); + + expect(stateController.onRenderPassCompleted).not.toHaveBeenCalled(); + + const screen = render( + + + + + , + ); + + const view = screen.UNSAFE_getByType(PerformanceMarker); + + view.props.onRenderComplete({ + nativeEvent: { + timestamp: 2000, + renderPassName: 'renderPass1', + interactive: 'TRUE', + destinationScreen: 'SomeScreen', + componentInstanceId: 'mock-mount-id', + }, + }); + + expect(stateController.onRenderPassCompleted).toHaveBeenCalledTimes(1); + expect(stateController.onRenderPassCompleted).toHaveBeenCalledWith({ + timestamp: 2000, + renderPassName: 'renderPass1', + interactive: true, + destinationScreen: 'SomeScreen', + componentInstanceId: 'mock-mount-id', + }); + }); }); diff --git a/packages/react-native-performance/src/__tests__/context/PerformanceProfiler.test.tsx b/packages/react-native-performance/src/__tests__/context/PerformanceProfiler.test.tsx index 9cfa7a1..763df72 100644 --- a/packages/react-native-performance/src/__tests__/context/PerformanceProfiler.test.tsx +++ b/packages/react-native-performance/src/__tests__/context/PerformanceProfiler.test.tsx @@ -5,7 +5,6 @@ import {renderHook} from '@testing-library/react-hooks'; import {PerformanceProfiler} from '../../context'; import useReportEmitter from '../../context/useReportEmitter'; import {OnStateChangedListener, useStateController, useStateControllerInitializer} from '../../state-machine'; -import useNativeRenderCompletionEvents from '../../context/useNativeRenderCompletionEvents'; jest.mock('../../state-machine/controller/useStateControllerInitializer', () => { return jest.fn(); @@ -15,10 +14,6 @@ jest.mock('../../context/useReportEmitter', () => { return jest.fn(); }); -jest.mock('../../context/useNativeRenderCompletionEvents', () => { - return jest.fn(); -}); - describe('context/PerformanceProfiler', () => { const mockStateController = {key: 'value'}; let mockReportEmitter: OnStateChangedListener; @@ -29,9 +24,6 @@ describe('context/PerformanceProfiler', () => { useStateControllerInitializer.mockReturnValue(mockStateController); // @ts-ignore useReportEmitter.mockReturnValue(mockReportEmitter); - - // @ts-ignore - useNativeRenderCompletionEvents.mockImplementation(() => {}); }); afterEach(() => { @@ -64,16 +56,6 @@ describe('context/PerformanceProfiler', () => { ); }); - it('sets up the native render completion event listener', () => { - const wrapper = ({children}: {children: React.ReactElement}) => ( - {children} - ); - - expect(useNativeRenderCompletionEvents).not.toHaveBeenCalled(); - renderHook(() => useStateController(), {wrapper}); - expect(useNativeRenderCompletionEvents).toHaveBeenCalledTimes(1); - }); - it('uses render timeouts by default', () => { const wrapper = ({children}: {children: React.ReactElement}) => ( {children} diff --git a/packages/react-native-performance/src/__tests__/context/useNativeRenderCompletionEvents.test.tsx b/packages/react-native-performance/src/__tests__/context/useNativeRenderCompletionEvents.test.tsx deleted file mode 100644 index b6f3632..0000000 --- a/packages/react-native-performance/src/__tests__/context/useNativeRenderCompletionEvents.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -import {renderHook} from '@testing-library/react-hooks'; -import {NativeEventEmitter, NativeModules, Platform} from 'react-native'; - -import MockStateController from '../MockStateController'; -import useNativeRenderCompletionEvents, { - RENDER_COMPLETION_EVENT_NAME, - RenderCompletionEvent, -} from '../../context/useNativeRenderCompletionEvents'; - -jest.mock('react-native', () => { - return { - NativeEventEmitter: jest.fn(), - NativeModules: {}, - Platform: { - OS: 'ios', - }, - }; -}); - -describe('context/useNativeRenderCompletionEvents', () => { - let stateController: MockStateController; - let addListener: NativeEventEmitter['addListener']; - let eventEmitterNativeModuleGetter: () => any; - - beforeEach(() => { - eventEmitterNativeModuleGetter = jest.fn().mockReturnValue('some-mock-module-value'); - Object.defineProperty(NativeModules, 'RenderCompletionEventEmitter', { - get: eventEmitterNativeModuleGetter, - configurable: true, - }); - - stateController = new MockStateController(); - stateController.isEnabled = true; - - addListener = jest.fn(); - // @ts-ignore - NativeEventEmitter.mockImplementation(() => { - return { - addListener, - }; - }); - Platform.OS = 'ios'; - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('adds an event listener on render', () => { - renderHook(() => useNativeRenderCompletionEvents({stateController})); - - expect(addListener).toHaveBeenCalledTimes(1); - expect(addListener).toHaveBeenCalledWith(RENDER_COMPLETION_EVENT_NAME, expect.any(Function)); - }); - - it('cleans up the event listener on unmount', () => { - const mockRemove = jest.fn(); - // @ts-ignore - addListener.mockReturnValue({ - remove: mockRemove, - }); - const renderAPI = renderHook(() => useNativeRenderCompletionEvents({stateController})); - - expect(mockRemove).not.toHaveBeenCalled(); - renderAPI.unmount(); - expect(mockRemove).toHaveBeenCalledTimes(1); - }); - - it('notifies the stateController of the render pass completion events correctly', () => { - renderHook(() => useNativeRenderCompletionEvents({stateController})); - - // @ts-ignore - const onRenderComplete = addListener.mock.calls[0][1] as (_: RenderCompletionEvent) => void; - expect(stateController.onRenderPassCompleted).not.toHaveBeenCalled(); - - onRenderComplete({ - timestamp: '123', - destinationScreen: 'some_screen', - componentInstanceId: 'id', - renderPassName: 'some_pass', - interactive: 'FALSE', - }); - - expect(stateController.onRenderPassCompleted).toHaveBeenCalledTimes(1); - - expect(stateController.onRenderPassCompleted).toHaveBeenCalledWith({ - timestamp: 123, - destinationScreen: 'some_screen', - componentInstanceId: 'id', - renderPassName: 'some_pass', - interactive: false, - }); - }); - - it('passes the correct native module as the subscriber on ios', () => { - renderHook(() => useNativeRenderCompletionEvents({stateController})); - expect(NativeEventEmitter).toHaveBeenCalledTimes(1); - expect(eventEmitterNativeModuleGetter).toHaveBeenCalledTimes(1); - expect(NativeEventEmitter).toHaveBeenCalledWith(NativeModules.RenderCompletionEventEmitter); - }); - - it('passes the correct native module as the subscriber on android', () => { - Platform.OS = 'android'; - renderHook(() => useNativeRenderCompletionEvents({stateController})); - expect(NativeEventEmitter).toHaveBeenCalledTimes(1); - expect(NativeEventEmitter).toHaveBeenCalledWith(undefined); - expect(eventEmitterNativeModuleGetter).not.toHaveBeenCalled(); - }); - - it('does not need native RenderCompletionEventEmitter module if state controller is disabled', () => { - stateController.isEnabled = false; - renderHook(() => useNativeRenderCompletionEvents({stateController})); - expect(NativeEventEmitter).not.toHaveBeenCalled(); - expect(eventEmitterNativeModuleGetter).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/react-native-performance/src/context/PerformanceProfiler.tsx b/packages/react-native-performance/src/context/PerformanceProfiler.tsx index af8c755..57dc721 100644 --- a/packages/react-native-performance/src/context/PerformanceProfiler.tsx +++ b/packages/react-native-performance/src/context/PerformanceProfiler.tsx @@ -11,7 +11,6 @@ import logger from '../utils/Logger'; import {PerformanceProfilerError} from '../exceptions'; import ReportObserver from './ReportObserver'; -import useNativeRenderCompletionEvents from './useNativeRenderCompletionEvents'; import useReportEmitter from './useReportEmitter'; const DEFAULT_RENDER_TIMEOUT_MILLIS = 5 * 1000; @@ -73,8 +72,6 @@ const PerformanceProfiler = ({ renderTimeoutMillis, }); - useNativeRenderCompletionEvents({stateController}); - return ( {children} diff --git a/packages/react-native-performance/src/context/useNativeRenderCompletionEvents.ts b/packages/react-native-performance/src/context/useNativeRenderCompletionEvents.ts deleted file mode 100644 index 0233ec2..0000000 --- a/packages/react-native-performance/src/context/useNativeRenderCompletionEvents.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {useCallback, useEffect} from 'react'; -import {NativeEventEmitter, NativeModules, Platform} from 'react-native'; - -import {Interactive} from '../PerformanceMarker'; -import {StateController} from '../state-machine'; - -export interface RenderCompletionEvent { - timestamp: string; - renderPassName: string; - interactive: Interactive; - destinationScreen: string; - componentInstanceId: string; -} - -export const RENDER_COMPLETION_EVENT_NAME = '@shopify/react-native-performance/onRenderComplete'; - -const useNativeRenderCompletionEvents = ({stateController}: {stateController: StateController}) => { - const onRenderComplete = useCallback( - ({timestamp, renderPassName, interactive, destinationScreen, componentInstanceId}: RenderCompletionEvent) => { - stateController.onRenderPassCompleted({ - timestamp: Number.parseFloat(timestamp), - destinationScreen, - renderPassName, - interactive: interactive === 'TRUE', - componentInstanceId, - }); - }, - [stateController], - ); - - useEffect(() => { - if (stateController.isEnabled) { - const eventEmitter = new NativeEventEmitter( - Platform.OS === 'ios' ? NativeModules.RenderCompletionEventEmitter : undefined, - ); - const subscription = eventEmitter.addListener(RENDER_COMPLETION_EVENT_NAME, onRenderComplete); - return () => subscription.remove(); - } - }, [stateController.isEnabled, onRenderComplete]); -}; - -export default useNativeRenderCompletionEvents; From e1c53171388bad1efb2951cb82b0e1ead5b9c17c Mon Sep 17 00:00:00 2001 From: Elvira Burchik Date: Thu, 2 Jun 2022 17:02:31 +0200 Subject: [PATCH 2/6] update Android native to send instance-events instead of global --- fixture/build.gradle | 3 +- fixture/src/constants.ts | 1 + .../PerformanceMarker.kt | 35 ++++++++++++++----- .../RenderCompletionEventEmitter.kt | 27 -------------- 4 files changed, 30 insertions(+), 36 deletions(-) delete mode 100644 packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/RenderCompletionEventEmitter.kt diff --git a/fixture/build.gradle b/fixture/build.gradle index c5672b5..f554338 100644 --- a/fixture/build.gradle +++ b/fixture/build.gradle @@ -13,7 +13,7 @@ buildscript { } repositories { google() - jcenter() + mavenCentral() maven { url "https://plugins.gradle.org/m2/" } @@ -45,6 +45,7 @@ allprojects { } } google() + mavenCentral() maven { url 'https://www.jitpack.io' } } diff --git a/fixture/src/constants.ts b/fixture/src/constants.ts index 17becb0..c96b838 100644 --- a/fixture/src/constants.ts +++ b/fixture/src/constants.ts @@ -11,6 +11,7 @@ export const NavigationKeys = { DRAWER_NAVIGATOR_SCREEN_2: 'DrawerNavigatorScreen2' as const, FLAT_LIST_SCREEN: 'FlatListScreen' as const, NESTED_NAVIGATION_SCREEN: 'NestedNavigationScreen' as const, + NESTED_PERFORMANCE_PROFILER_SCREEN: 'NestedPerformanceProfilerScreen' as const, }; type ValueOf = T[keyof T]; diff --git a/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/PerformanceMarker.kt b/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/PerformanceMarker.kt index 69f0c9b..19b8fb4 100644 --- a/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/PerformanceMarker.kt +++ b/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/PerformanceMarker.kt @@ -2,12 +2,17 @@ package com.shopify.reactnativeperformance import android.content.Context import android.view.View +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.events.RCTEventEmitter import kotlin.reflect.KProperty import kotlin.properties.ReadWriteProperty +import com.facebook.react.common.MapBuilder + +private const val RENDER_COMPLETION_EVENT_NAME = "@shopify/react-native-performance/onRenderComplete" class PerformanceMarker(context: Context?) : View(context) { @@ -35,7 +40,7 @@ class PerformanceMarker(context: Context?) : View(context) { But that should be relatively negligible in the bigger scheme of things. Also note that we're using the moment when `PerformanceMarker` is rendered as a proxy for when the rest of its siblings (the actual screen content) is rendered. So we're already using these kinds of approximations at the native layer. - Adding this 1 additional approximation shouldn't affect the final render times signficantly. + Adding this 1 additional approximation shouldn't affect the final render times significantly. */ private fun sendRenderCompletionEventIfNeeded() { val _destinationScreen = this.destinationScreen @@ -52,13 +57,19 @@ class PerformanceMarker(context: Context?) : View(context) { } reportedOnce = true - RenderCompletionEventEmitter.onRenderComplete( - context as ReactContext, - destinationScreen = _destinationScreen, - renderPassName = _renderPassName, - interactive = _interactive, - componentInstanceId = _componentInstanceId, - ) + + val event = Arguments.createMap().apply { + putString("timestamp", System.currentTimeMillis().toString()) + putString("renderPassName", _renderPassName) + putString("interactive", _interactive.toString()) + putString("destinationScreen", _destinationScreen) + putString("componentInstanceId", _componentInstanceId) + } + + val reactContext = context as ReactContext + reactContext + .getJSModule(RCTEventEmitter::class.java) + .receiveEvent(id, "onRenderComplete", event) } private class PerformanceMarkerProp : ReadWriteProperty { @@ -110,5 +121,13 @@ class PerformanceMarkerManager : SimpleViewManager() { view.componentInstanceId = componentInstanceId } + override fun getExportedCustomDirectEventTypeConstants(): MutableMap { + return MapBuilder.builder().put( + "onRenderComplete", + MapBuilder.of( + "registrationName", "onRenderComplete") + ).build(); + } + override fun getName() = "PerformanceMarker" } diff --git a/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/RenderCompletionEventEmitter.kt b/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/RenderCompletionEventEmitter.kt deleted file mode 100644 index ccfc5b6..0000000 --- a/packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/RenderCompletionEventEmitter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.shopify.reactnativeperformance - -import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.ReactContext -import com.facebook.react.modules.core.DeviceEventManagerModule - -object RenderCompletionEventEmitter { - private const val RENDER_COMPLETION_EVENT_NAME = "@shopify/react-native-performance/onRenderComplete" - - fun onRenderComplete( - context: ReactContext, - destinationScreen: String, - renderPassName: String, - interactive: Interactive, - componentInstanceId: String - ) { - val event = Arguments.createMap().apply { - putString("timestamp", System.currentTimeMillis().toString()) - putString("renderPassName", renderPassName) - putString("interactive", interactive.toString()) - putString("destinationScreen", destinationScreen) - putString("componentInstanceId", componentInstanceId) - } - context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit(RENDER_COMPLETION_EVENT_NAME, event) - } -} From 62cd61c2581128dff69290fe2d227e1c241c643c Mon Sep 17 00:00:00 2001 From: Elvira Burchik Date: Wed, 8 Jun 2022 13:22:18 +0200 Subject: [PATCH 3/6] update onRenderComplete callback's usage --- .../PerformanceMarker.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift b/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift index cd3879f..8b92667 100644 --- a/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift +++ b/packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift @@ -6,9 +6,13 @@ import Foundation private var interactive: Interactive? = nil private var destinationScreen: String? = nil private var componentInstanceId: String? = nil - - @objc(onRenderComplete) - var onRenderComplete: RCTDirectEventBlock? + private var onRenderComplete: RCTDirectEventBlock? = nil + + @objc func setOnRenderComplete(_ onRenderComplete: @escaping RCTDirectEventBlock) { + assertSetOnlyOnce(currentVal: self.onRenderComplete, newVal: onRenderComplete, propertyName: "onRenderComplete") + self.onRenderComplete = onRenderComplete + self.sendRenderCompletionEventIfNeeded() + } @objc func setComponentInstanceId(_ componentInstanceId: String) { assertSetOnlyOnce(currentVal: self.componentInstanceId, newVal: componentInstanceId, propertyName: "componentInstanceId") @@ -51,7 +55,8 @@ import Foundation let renderPassName = renderPassName, let interactive = interactive, let destinationScreen = destinationScreen, - let componentInstanceId = componentInstanceId + let componentInstanceId = componentInstanceId, + let onRenderComplete = onRenderComplete else { return } @@ -67,7 +72,7 @@ import Foundation "componentInstanceId": componentInstanceId ] - onRenderComplete?(onRenderCompleteEvent) + onRenderComplete(onRenderCompleteEvent) } } From 0d7baadece50fb241f599a41aba15cefe9db0665 Mon Sep 17 00:00:00 2001 From: Elvira Burchik Date: Wed, 8 Jun 2022 13:45:12 +0200 Subject: [PATCH 4/6] clean up --- fixture/src/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/fixture/src/constants.ts b/fixture/src/constants.ts index c96b838..17becb0 100644 --- a/fixture/src/constants.ts +++ b/fixture/src/constants.ts @@ -11,7 +11,6 @@ export const NavigationKeys = { DRAWER_NAVIGATOR_SCREEN_2: 'DrawerNavigatorScreen2' as const, FLAT_LIST_SCREEN: 'FlatListScreen' as const, NESTED_NAVIGATION_SCREEN: 'NestedNavigationScreen' as const, - NESTED_PERFORMANCE_PROFILER_SCREEN: 'NestedPerformanceProfilerScreen' as const, }; type ValueOf = T[keyof T]; From ba58653f5512e65383342d177dcf4bae790d740b Mon Sep 17 00:00:00 2001 From: Elvira Burchik Date: Mon, 13 Jun 2022 13:03:06 +0200 Subject: [PATCH 5/6] add an example with nested Profiler to fixture app --- fixture/src/App.tsx | 19 +++++++- fixture/src/constants.ts | 3 ++ fixture/src/examples/ExamplesScreen.tsx | 26 +++++++---- fixture/src/examples/NestedContextScreen.tsx | 47 ++++++++++++++++++++ 4 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 fixture/src/examples/NestedContextScreen.tsx diff --git a/fixture/src/App.tsx b/fixture/src/App.tsx index 987f269..a6e9e15 100644 --- a/fixture/src/App.tsx +++ b/fixture/src/App.tsx @@ -14,6 +14,7 @@ import FastRenderPassesScreen from './examples/FastRenderPassesScreen'; import ConditionalRenderingScreen from './examples/ConditionalRenderingScreen'; import DrawerNavigator from './examples/DrawerNavigator'; import NestedNavigationScreen from './examples/NestedNavigationScreen'; +import NestedContextScreen, {InnerNestedContextScreen} from './examples/NestedContextScreen'; const Stack = createStackNavigator(); @@ -29,11 +30,27 @@ const NavigationTree = () => { + ); }; +function NestedProfilerNavigationTree() { + return ( + + + + + + + ); +} + const App = () => { const apolloClient = useMemo(() => { return new ApolloClient({ @@ -53,7 +70,7 @@ const App = () => { return ( <> - + diff --git a/fixture/src/constants.ts b/fixture/src/constants.ts index 17becb0..79fdb02 100644 --- a/fixture/src/constants.ts +++ b/fixture/src/constants.ts @@ -11,6 +11,9 @@ export const NavigationKeys = { DRAWER_NAVIGATOR_SCREEN_2: 'DrawerNavigatorScreen2' as const, FLAT_LIST_SCREEN: 'FlatListScreen' as const, NESTED_NAVIGATION_SCREEN: 'NestedNavigationScreen' as const, + NESTED_PROFILER_CONTEXT: 'NestedProfilerContext' as const, + NESTED_CONTEXT_SCREEN: 'NestedContextScreen' as const, + INNER_NESTED_CONTEXT_SCREEN: 'InnerNestedContextScreen' as const, }; type ValueOf = T[keyof T]; diff --git a/fixture/src/examples/ExamplesScreen.tsx b/fixture/src/examples/ExamplesScreen.tsx index 0898883..b06448f 100644 --- a/fixture/src/examples/ExamplesScreen.tsx +++ b/fixture/src/examples/ExamplesScreen.tsx @@ -2,11 +2,13 @@ import React from 'react'; import {StatusBar, StyleSheet, FlatList, Text, TouchableOpacity, Image} from 'react-native'; import {ReactNavigationPerformanceView, useProfiledNavigation} from '@shopify/react-native-performance-navigation'; import {StackNavigationProp} from '@react-navigation/stack'; +import {useNavigation} from '@react-navigation/native'; import {NavigationKeys, RootStackParamList} from '../constants'; export const ExamplesScreen = () => { const {navigate} = useProfiledNavigation>(); + const navigation = useNavigation>(); const renderTimeoutMillisOverride = (screenName: string) => { return screenName === NavigationKeys.PERFORMANCE ? 6 * 1000 : undefined; @@ -40,19 +42,27 @@ export const ExamplesScreen = () => { title: 'FlatList Screen', destination: NavigationKeys.FLAT_LIST_SCREEN, }, + { + title: 'Nested Context Screen', + destination: NavigationKeys.NESTED_PROFILER_CONTEXT, + }, ]} renderItem={({item}) => ( { - navigate( - { - source: NavigationKeys.EXAMPLES, - uiEvent, - renderTimeoutMillisOverride: renderTimeoutMillisOverride(item.destination), - }, - item.destination, - ); + if (item.destination === NavigationKeys.NESTED_PROFILER_CONTEXT) { + navigation.navigate(item.destination); + } else { + navigate( + { + source: NavigationKeys.EXAMPLES, + uiEvent, + renderTimeoutMillisOverride: renderTimeoutMillisOverride(item.destination), + }, + item.destination, + ); + } }} > {item.title} diff --git a/fixture/src/examples/NestedContextScreen.tsx b/fixture/src/examples/NestedContextScreen.tsx new file mode 100644 index 0000000..da4c003 --- /dev/null +++ b/fixture/src/examples/NestedContextScreen.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {ReactNavigationPerformanceView, useProfiledNavigation} from '@shopify/react-native-performance-navigation'; +import {Button, Text, View, StyleSheet} from 'react-native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {PerformanceProfiler, LogLevel} from '@shopify/react-native-performance'; + +import {NavigationKeys, RootStackParamList} from '../constants'; + +const NestedContextScreen = () => { + const {navigate} = useProfiledNavigation>(); + + return ( + + +