Skip to content

Commit a9b6a97

Browse files
feat: allow using multiple performance profilers (#76)
* feat: use instance level onRenderComplete callbacks instead of one global listener * update Android native to send instance-events instead of global * update onRenderComplete callback's usage * clean up * add an example with nested Profiler to fixture app * update after PR comments: - set LogLevel Debug for a global profiler - add a note about not using NestedContextScreen as an example
1 parent 19a7b51 commit a9b6a97

18 files changed

+207
-289
lines changed

fixture/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ buildscript {
1313
}
1414
repositories {
1515
google()
16-
jcenter()
16+
mavenCentral()
1717
maven {
1818
url "https://plugins.gradle.org/m2/"
1919
}
@@ -45,6 +45,7 @@ allprojects {
4545
}
4646
}
4747
google()
48+
mavenCentral()
4849
maven { url 'https://www.jitpack.io' }
4950
}
5051

fixture/src/App.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import FastRenderPassesScreen from './examples/FastRenderPassesScreen';
1414
import ConditionalRenderingScreen from './examples/ConditionalRenderingScreen';
1515
import DrawerNavigator from './examples/DrawerNavigator';
1616
import NestedNavigationScreen from './examples/NestedNavigationScreen';
17+
import NestedContextScreen, {InnerNestedContextScreen} from './examples/NestedContextScreen';
1718

1819
const Stack = createStackNavigator<RootStackParamList>();
1920

@@ -29,11 +30,27 @@ const NavigationTree = () => {
2930
<Stack.Screen name={NavigationKeys.CONDITIONAL_RENDERING_SCREEN} component={ConditionalRenderingScreen} />
3031
<Stack.Screen name={NavigationKeys.FLAT_LIST_SCREEN} component={FlatListScreen} />
3132
<Stack.Screen name={NavigationKeys.NESTED_NAVIGATION_SCREEN} component={NestedNavigationScreen} />
33+
<Stack.Screen
34+
name={NavigationKeys.NESTED_PROFILER_CONTEXT}
35+
component={NestedProfilerNavigationTree}
36+
options={{headerShown: false}}
37+
/>
3238
</Stack.Navigator>
3339
</NavigationContainer>
3440
);
3541
};
3642

43+
function NestedProfilerNavigationTree() {
44+
return (
45+
<Stack.Navigator>
46+
<Stack.Screen name={NavigationKeys.NESTED_CONTEXT_SCREEN} component={NestedContextScreen} />
47+
<Stack.Group screenOptions={{presentation: 'modal'}}>
48+
<Stack.Screen name={NavigationKeys.INNER_NESTED_CONTEXT_SCREEN} component={InnerNestedContextScreen} />
49+
</Stack.Group>
50+
</Stack.Navigator>
51+
);
52+
}
53+
3754
const App = () => {
3855
const apolloClient = useMemo(() => {
3956
return new ApolloClient({

fixture/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export const NavigationKeys = {
1111
DRAWER_NAVIGATOR_SCREEN_2: 'DrawerNavigatorScreen2' as const,
1212
FLAT_LIST_SCREEN: 'FlatListScreen' as const,
1313
NESTED_NAVIGATION_SCREEN: 'NestedNavigationScreen' as const,
14+
NESTED_PROFILER_CONTEXT: 'NestedProfilerContext' as const,
15+
NESTED_CONTEXT_SCREEN: 'NestedContextScreen' as const,
16+
INNER_NESTED_CONTEXT_SCREEN: 'InnerNestedContextScreen' as const,
1417
};
1518

1619
type ValueOf<T> = T[keyof T];

fixture/src/examples/ExamplesScreen.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import React from 'react';
22
import {StatusBar, StyleSheet, FlatList, Text, TouchableOpacity, Image} from 'react-native';
33
import {ReactNavigationPerformanceView, useProfiledNavigation} from '@shopify/react-native-performance-navigation';
44
import {StackNavigationProp} from '@react-navigation/stack';
5+
import {useNavigation} from '@react-navigation/native';
56

67
import {NavigationKeys, RootStackParamList} from '../constants';
78

89
export const ExamplesScreen = () => {
910
const {navigate} = useProfiledNavigation<StackNavigationProp<RootStackParamList, 'Examples'>>();
11+
const navigation = useNavigation<StackNavigationProp<RootStackParamList, 'Examples'>>();
1012

1113
const renderTimeoutMillisOverride = (screenName: string) => {
1214
return screenName === NavigationKeys.PERFORMANCE ? 6 * 1000 : undefined;
@@ -40,19 +42,27 @@ export const ExamplesScreen = () => {
4042
title: 'FlatList Screen',
4143
destination: NavigationKeys.FLAT_LIST_SCREEN,
4244
},
45+
{
46+
title: 'Nested Context Screen',
47+
destination: NavigationKeys.NESTED_PROFILER_CONTEXT,
48+
},
4349
]}
4450
renderItem={({item}) => (
4551
<TouchableOpacity
4652
style={styles.row}
4753
onPress={uiEvent => {
48-
navigate(
49-
{
50-
source: NavigationKeys.EXAMPLES,
51-
uiEvent,
52-
renderTimeoutMillisOverride: renderTimeoutMillisOverride(item.destination),
53-
},
54-
item.destination,
55-
);
54+
if (item.destination === NavigationKeys.NESTED_PROFILER_CONTEXT) {
55+
navigation.navigate(item.destination);
56+
} else {
57+
navigate(
58+
{
59+
source: NavigationKeys.EXAMPLES,
60+
uiEvent,
61+
renderTimeoutMillisOverride: renderTimeoutMillisOverride(item.destination),
62+
},
63+
item.destination,
64+
);
65+
}
5666
}}
5767
>
5868
<Text style={styles.rowTitle}>{item.title}</Text>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import {ReactNavigationPerformanceView, useProfiledNavigation} from '@shopify/react-native-performance-navigation';
3+
import {Button, Text, View, StyleSheet} from 'react-native';
4+
import {StackNavigationProp} from '@react-navigation/stack';
5+
import {PerformanceProfiler, LogLevel} from '@shopify/react-native-performance';
6+
7+
import {NavigationKeys, RootStackParamList} from '../constants';
8+
9+
/**
10+
* NOTE: This screen shouldn't be used as an example since we don't generally recommend mixing multiple profilers.
11+
* Nested profilers only make sense when transitioning between JS and native layers in a hybrid use case.
12+
* For example, in brown-field apps, gradually adopting React Native.
13+
* Please stick with only profiler per App if there is no serious matter to do otherwise.
14+
*/
15+
16+
const NestedContextScreen = () => {
17+
const {navigate} = useProfiledNavigation<StackNavigationProp<RootStackParamList, 'Examples'>>();
18+
19+
return (
20+
<PerformanceProfiler logLevel={LogLevel.Debug}>
21+
<ReactNavigationPerformanceView screenName={NavigationKeys.NESTED_CONTEXT_SCREEN} interactive>
22+
<Button
23+
title="Present new screen inside nested Profiler Context"
24+
onPress={() => navigate(NavigationKeys.INNER_NESTED_CONTEXT_SCREEN)}
25+
/>
26+
</ReactNavigationPerformanceView>
27+
</PerformanceProfiler>
28+
);
29+
};
30+
31+
export const InnerNestedContextScreen = () => {
32+
const text = 'This is a screen rendered in a nested Profiler Context\n\n You should see no errors in the logs';
33+
return (
34+
<ReactNavigationPerformanceView screenName={NavigationKeys.INNER_NESTED_CONTEXT_SCREEN} interactive>
35+
<View style={styles.textContainer}>
36+
<Text style={styles.text}>{text}</Text>
37+
</View>
38+
</ReactNavigationPerformanceView>
39+
);
40+
};
41+
42+
export default NestedContextScreen;
43+
44+
const styles = StyleSheet.create({
45+
text: {
46+
textAlignVertical: 'center',
47+
textAlign: 'center',
48+
fontSize: 18,
49+
},
50+
textContainer: {
51+
flex: 1,
52+
justifyContent: 'center',
53+
},
54+
});

packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/PerformanceMarker.kt

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package com.shopify.reactnativeperformance
22

33
import android.content.Context
44
import android.view.View
5+
import com.facebook.react.bridge.Arguments
56
import com.facebook.react.bridge.ReactContext
67
import com.facebook.react.uimanager.SimpleViewManager
78
import com.facebook.react.uimanager.ThemedReactContext
89
import com.facebook.react.uimanager.annotations.ReactProp
10+
import com.facebook.react.uimanager.events.RCTEventEmitter
911
import kotlin.reflect.KProperty
1012
import kotlin.properties.ReadWriteProperty
13+
import com.facebook.react.common.MapBuilder
14+
15+
private const val RENDER_COMPLETION_EVENT_NAME = "@shopify/react-native-performance/onRenderComplete"
1116

1217
class PerformanceMarker(context: Context?) : View(context) {
1318

@@ -35,7 +40,7 @@ class PerformanceMarker(context: Context?) : View(context) {
3540
But that should be relatively negligible in the bigger scheme of things. Also note that we're using the
3641
moment when `PerformanceMarker` is rendered as a proxy for when the rest of its siblings (the actual
3742
screen content) is rendered. So we're already using these kinds of approximations at the native layer.
38-
Adding this 1 additional approximation shouldn't affect the final render times signficantly.
43+
Adding this 1 additional approximation shouldn't affect the final render times significantly.
3944
*/
4045
private fun sendRenderCompletionEventIfNeeded() {
4146
val _destinationScreen = this.destinationScreen
@@ -52,13 +57,19 @@ class PerformanceMarker(context: Context?) : View(context) {
5257
}
5358

5459
reportedOnce = true
55-
RenderCompletionEventEmitter.onRenderComplete(
56-
context as ReactContext,
57-
destinationScreen = _destinationScreen,
58-
renderPassName = _renderPassName,
59-
interactive = _interactive,
60-
componentInstanceId = _componentInstanceId,
61-
)
60+
61+
val event = Arguments.createMap().apply {
62+
putString("timestamp", System.currentTimeMillis().toString())
63+
putString("renderPassName", _renderPassName)
64+
putString("interactive", _interactive.toString())
65+
putString("destinationScreen", _destinationScreen)
66+
putString("componentInstanceId", _componentInstanceId)
67+
}
68+
69+
val reactContext = context as ReactContext
70+
reactContext
71+
.getJSModule(RCTEventEmitter::class.java)
72+
.receiveEvent(id, "onRenderComplete", event)
6273
}
6374

6475
private class PerformanceMarkerProp<T : Any> : ReadWriteProperty<PerformanceMarker, T?> {
@@ -110,5 +121,13 @@ class PerformanceMarkerManager : SimpleViewManager<PerformanceMarker>() {
110121
view.componentInstanceId = componentInstanceId
111122
}
112123

124+
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
125+
return MapBuilder.builder<String, Any>().put(
126+
"onRenderComplete",
127+
MapBuilder.of(
128+
"registrationName", "onRenderComplete")
129+
).build();
130+
}
131+
113132
override fun getName() = "PerformanceMarker"
114133
}

packages/react-native-performance/android/src/main/kotlin/com/shopify/reactnativeperformance/RenderCompletionEventEmitter.kt

Lines changed: 0 additions & 27 deletions
This file was deleted.

packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarker.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import Foundation
66
private var interactive: Interactive? = nil
77
private var destinationScreen: String? = nil
88
private var componentInstanceId: String? = nil
9+
private var onRenderComplete: RCTDirectEventBlock? = nil
10+
11+
@objc func setOnRenderComplete(_ onRenderComplete: @escaping RCTDirectEventBlock) {
12+
assertSetOnlyOnce(currentVal: self.onRenderComplete, newVal: onRenderComplete, propertyName: "onRenderComplete")
13+
self.onRenderComplete = onRenderComplete
14+
self.sendRenderCompletionEventIfNeeded()
15+
}
916

1017
@objc func setComponentInstanceId(_ componentInstanceId: String) {
1118
assertSetOnlyOnce(currentVal: self.componentInstanceId, newVal: componentInstanceId, propertyName: "componentInstanceId")
@@ -48,21 +55,24 @@ import Foundation
4855
let renderPassName = renderPassName,
4956
let interactive = interactive,
5057
let destinationScreen = destinationScreen,
51-
let componentInstanceId = componentInstanceId
58+
let componentInstanceId = componentInstanceId,
59+
let onRenderComplete = onRenderComplete
5260
else {
5361
return
5462
}
5563

5664
reportedOnce = true
5765

58-
RenderCompletionEventEmitter.INSTANCE?.onRenderComplete(
59-
destinationScreen: destinationScreen,
60-
renderPassName: renderPassName,
61-
interactive: interactive,
62-
componentInstanceId: componentInstanceId
63-
) ?? assertionFailure(
64-
"RenderCompletionEventEmitter.INSTANCE was not initialized by the time PerformanceMarker got rendered for screen " +
65-
"'\(destinationScreen)', renderPassName '\(renderPassName)'.")
66+
let timestamp = Timestamp.nowMillis()
67+
let onRenderCompleteEvent = [
68+
"timestamp": String(timestamp),
69+
"renderPassName": renderPassName,
70+
"interactive": interactive.description,
71+
"destinationScreen": destinationScreen,
72+
"componentInstanceId": componentInstanceId
73+
]
74+
75+
onRenderComplete(onRenderCompleteEvent)
6676

6777
}
6878
}

packages/react-native-performance/ios/ReactNativePerformance/PerformanceMarkerManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ @interface RCT_EXTERN_MODULE(PerformanceMarkerManager, RCTViewManager)
77
RCT_EXPORT_VIEW_PROPERTY(interactive, NSString)
88
RCT_EXPORT_VIEW_PROPERTY(destinationScreen, NSString)
99
RCT_EXPORT_VIEW_PROPERTY(componentInstanceId, NSString)
10+
RCT_EXPORT_VIEW_PROPERTY(onRenderComplete, RCTDirectEventBlock)
1011

1112
@end

packages/react-native-performance/ios/ReactNativePerformance/RenderCompletionEventEmitter.m

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)