Skip to content

Commit

Permalink
feat: allow using multiple performance profilers (#76)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ElviraBurchik authored Jun 14, 2022
1 parent 19a7b51 commit a9b6a97
Show file tree
Hide file tree
Showing 18 changed files with 207 additions and 289 deletions.
3 changes: 2 additions & 1 deletion fixture/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ buildscript {
}
repositories {
google()
jcenter()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
Expand Down Expand Up @@ -45,6 +45,7 @@ allprojects {
}
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}

Expand Down
17 changes: 17 additions & 0 deletions fixture/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RootStackParamList>();

Expand All @@ -29,11 +30,27 @@ const NavigationTree = () => {
<Stack.Screen name={NavigationKeys.CONDITIONAL_RENDERING_SCREEN} component={ConditionalRenderingScreen} />
<Stack.Screen name={NavigationKeys.FLAT_LIST_SCREEN} component={FlatListScreen} />
<Stack.Screen name={NavigationKeys.NESTED_NAVIGATION_SCREEN} component={NestedNavigationScreen} />
<Stack.Screen
name={NavigationKeys.NESTED_PROFILER_CONTEXT}
component={NestedProfilerNavigationTree}
options={{headerShown: false}}
/>
</Stack.Navigator>
</NavigationContainer>
);
};

function NestedProfilerNavigationTree() {
return (
<Stack.Navigator>
<Stack.Screen name={NavigationKeys.NESTED_CONTEXT_SCREEN} component={NestedContextScreen} />
<Stack.Group screenOptions={{presentation: 'modal'}}>
<Stack.Screen name={NavigationKeys.INNER_NESTED_CONTEXT_SCREEN} component={InnerNestedContextScreen} />
</Stack.Group>
</Stack.Navigator>
);
}

const App = () => {
const apolloClient = useMemo(() => {
return new ApolloClient({
Expand Down
3 changes: 3 additions & 0 deletions fixture/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T[keyof T];
Expand Down
26 changes: 18 additions & 8 deletions fixture/src/examples/ExamplesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<StackNavigationProp<RootStackParamList, 'Examples'>>();
const navigation = useNavigation<StackNavigationProp<RootStackParamList, 'Examples'>>();

const renderTimeoutMillisOverride = (screenName: string) => {
return screenName === NavigationKeys.PERFORMANCE ? 6 * 1000 : undefined;
Expand Down Expand Up @@ -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}) => (
<TouchableOpacity
style={styles.row}
onPress={uiEvent => {
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,
);
}
}}
>
<Text style={styles.rowTitle}>{item.title}</Text>
Expand Down
54 changes: 54 additions & 0 deletions fixture/src/examples/NestedContextScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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';

/**
* NOTE: This screen shouldn't be used as an example since we don't generally recommend mixing multiple profilers.
* Nested profilers only make sense when transitioning between JS and native layers in a hybrid use case.
* For example, in brown-field apps, gradually adopting React Native.
* Please stick with only profiler per App if there is no serious matter to do otherwise.
*/

const NestedContextScreen = () => {
const {navigate} = useProfiledNavigation<StackNavigationProp<RootStackParamList, 'Examples'>>();

return (
<PerformanceProfiler logLevel={LogLevel.Debug}>
<ReactNavigationPerformanceView screenName={NavigationKeys.NESTED_CONTEXT_SCREEN} interactive>
<Button
title="Present new screen inside nested Profiler Context"
onPress={() => navigate(NavigationKeys.INNER_NESTED_CONTEXT_SCREEN)}
/>
</ReactNavigationPerformanceView>
</PerformanceProfiler>
);
};

export const InnerNestedContextScreen = () => {
const text = 'This is a screen rendered in a nested Profiler Context\n\n You should see no errors in the logs';
return (
<ReactNavigationPerformanceView screenName={NavigationKeys.INNER_NESTED_CONTEXT_SCREEN} interactive>
<View style={styles.textContainer}>
<Text style={styles.text}>{text}</Text>
</View>
</ReactNavigationPerformanceView>
);
};

export default NestedContextScreen;

const styles = StyleSheet.create({
text: {
textAlignVertical: 'center',
textAlign: 'center',
fontSize: 18,
},
textContainer: {
flex: 1,
justifyContent: 'center',
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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
Expand All @@ -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<T : Any> : ReadWriteProperty<PerformanceMarker, T?> {
Expand Down Expand Up @@ -110,5 +121,13 @@ class PerformanceMarkerManager : SimpleViewManager<PerformanceMarker>() {
view.componentInstanceId = componentInstanceId
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.builder<String, Any>().put(
"onRenderComplete",
MapBuilder.of(
"registrationName", "onRenderComplete")
).build();
}

override fun getName() = "PerformanceMarker"
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import Foundation
private var interactive: Interactive? = nil
private var destinationScreen: String? = nil
private var componentInstanceId: String? = nil
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")
Expand Down Expand Up @@ -48,21 +55,24 @@ import Foundation
let renderPassName = renderPassName,
let interactive = interactive,
let destinationScreen = destinationScreen,
let componentInstanceId = componentInstanceId
let componentInstanceId = componentInstanceId,
let onRenderComplete = onRenderComplete
else {
return
}

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)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

This file was deleted.

This file was deleted.

Loading

0 comments on commit a9b6a97

Please sign in to comment.