Created
November 17, 2022 17:19
-
-
Save AndreiCalazans/42d377bac1a5de47300135bd1a0602b6 to your computer and use it in GitHub Desktop.
Debug Deterministic Performance screen
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable @cb/react-no-untranslated-string */ | |
import React, { | |
Suspense, | |
useCallback, | |
useEffect, | |
useMemo, | |
useRef, | |
useState, | |
} from 'react'; | |
import { Dimensions, Pressable, View } from 'react-native'; | |
import performance from 'react-native-performance'; | |
import { useNavigation } from '@react-navigation/core'; | |
import { createNativeStackNavigator } from '@react-navigation/native-stack'; | |
import { createStackNavigator } from '@react-navigation/stack'; | |
import last from 'lodash/last'; | |
import { Box, Spacer, VStack } from '@cbhq/cds-mobile/layout'; | |
import { | |
TextBody, | |
TextLabel1, | |
TextLegal, | |
TextTitle2, | |
} from '@cbhq/cds-mobile/typography'; | |
import { Button } from '@components/interactables/Button'; | |
import { Screen } from '@app/components/Screen'; | |
const stringifiedPersistedQueries = JSON.stringify( | |
require('./../../persisted_queries.json'), | |
); | |
const useRunOnFirstFunctionCall = (cb: () => void) => { | |
const isCalledRef = useRef(false); | |
if (!isCalledRef.current) { | |
cb(); | |
isCalledRef.current = true; | |
} | |
}; | |
const NavigateButton = ({ navigateTo }: { navigateTo: string }) => { | |
const { navigate } = useNavigation(); | |
// @ts-expect-error we are ignoring navigator's screen names on purpose | |
return <Button onPress={() => navigate(navigateTo)}>Navigate</Button>; | |
}; | |
const screenDimensions = Dimensions.get('screen'); | |
const fakeNavigatorWrapperStyle = { | |
padding: 20, | |
width: screenDimensions.width, | |
height: screenDimensions.height, | |
}; | |
const FakeNativeStack = createNativeStackNavigator(); | |
const FakeStack = createStackNavigator(); | |
type StackDefinition = ReturnType< | |
typeof createStackNavigator | typeof createNativeStackNavigator | |
>; | |
const FakeNavigator = ({ | |
Stack = FakeStack, | |
stackTitle, | |
}: { | |
Stack?: StackDefinition; | |
stackTitle: string; | |
}) => { | |
const screenOne = useCallback( | |
() => ( | |
<FakeScreen | |
title={`${stackTitle} Screen One`} | |
numberOfNodes={3} | |
button={<NavigateButton navigateTo="Two" />} | |
/> | |
), | |
[stackTitle], | |
); | |
const screenTwo = useCallback( | |
() => <FakeScreen numberOfNodes={3} title={`${stackTitle} Screen Two`} />, | |
[stackTitle], | |
); | |
return ( | |
<View style={fakeNavigatorWrapperStyle}> | |
<Stack.Navigator | |
screenOptions={{ | |
header: undefined, | |
headerShown: false, | |
}} | |
> | |
<Stack.Screen name="one" component={screenOne} /> | |
<Stack.Screen name="Two" component={screenTwo} /> | |
</Stack.Navigator> | |
</View> | |
); | |
}; | |
const FakeScreen = ({ | |
title, | |
numberOfNodes = 500, | |
button, | |
}: { | |
title: string; | |
numberOfNodes?: number; | |
button?: React.ReactNode; | |
}) => { | |
const [markerDuration, setMarkerDuration] = useState<string | null>(null); | |
const markerName = useMemo( | |
() => title.replace(/\s/g, '_').toLowerCase(), | |
[title], | |
); | |
useRunOnFirstFunctionCall(() => { | |
performance.mark(markerName + '_start'); | |
}); | |
const markEndOfRender = useCallback(() => { | |
performance.measure(markerName, markerName + '_start'); | |
const entries = performance.getEntriesByName(markerName); | |
const entry = last(entries) || { duration: 0, name: '' }; | |
const duration = `marker: ${entry.name} took ${entry.duration.toFixed( | |
2, | |
)} ms`; | |
// 2022-11-16 andrei-calazans: this is for debug purposes only | |
// eslint-disable-next-line no-console | |
// console.log(duration); | |
setMarkerDuration(duration); | |
}, [markerName]); | |
return ( | |
<Box flexGrow={1}> | |
<TextTitle2>{title}</TextTitle2> | |
<Spacer vertical={1} /> | |
<TextLegal color="negative">{markerDuration}</TextLegal> | |
<Spacer vertical={2} /> | |
{button} | |
<Box flexDirection="row" flexWrap="wrap"> | |
{Array.from({ length: numberOfNodes }).map((_, index) => ( | |
<Box key={index}> | |
<TextLabel1 | |
spacing={1} | |
onLayout={ | |
index + 1 === numberOfNodes ? markEndOfRender : undefined | |
} | |
> | |
node {index} | |
</TextLabel1> | |
</Box> | |
))} | |
</Box> | |
</Box> | |
); | |
}; | |
let promise: Promise<unknown> | null = null; | |
const useForceSuspend = () => { | |
useEffect(() => { | |
return () => { | |
promise = null; | |
}; | |
}, []); | |
const didRun = useRef(false); | |
if (!didRun.current) { | |
didRun.current = true; | |
if (promise) { | |
return; | |
} | |
promise = new Promise((res) => setTimeout(res, 1000)); | |
throw promise; | |
} | |
}; | |
const SuspendingButton = () => { | |
useForceSuspend(); | |
return null; | |
}; | |
const JsonParse = () => { | |
const didRun = useRef<string | null>(null); | |
if (!didRun.current) { | |
performance.mark('json_parse_start'); | |
// This is a blocking call and thus should impact how long it takes to render. | |
JSON.parse(stringifiedPersistedQueries); | |
performance.measure('json_parse', 'json_parse_start'); | |
const entries = performance.getEntriesByName('json_parse'); | |
const entry = last(entries) || { duration: 0, name: '' }; | |
didRun.current = `marker: ${entry.name} took ${entry.duration.toFixed( | |
2, | |
)} ms`; | |
} | |
return ( | |
<Box flexGrow={1}> | |
<TextTitle2>JSON parse</TextTitle2> | |
<Spacer vertical={1} /> | |
<TextLegal color="negative">{didRun.current}</TextLegal> | |
</Box> | |
); | |
}; | |
const TestCaseEntry = ({ | |
title, | |
description, | |
onPress, | |
}: { | |
title: string; | |
description: string; | |
onPress: () => void; | |
}) => ( | |
<Pressable onPress={onPress}> | |
<Box bordered spacing={2}> | |
<TextTitle2>{title}</TextTitle2> | |
<TextBody>{description}</TextBody> | |
</Box> | |
</Pressable> | |
); | |
const Back = ({ goBack }: { goBack: () => void }) => { | |
return ( | |
<Pressable onPress={goBack}> | |
<TextBody>Back</TextBody> | |
</Pressable> | |
); | |
}; | |
export const DebugDeterministicPerformance = () => { | |
const [selectedTest, setTest] = useState< | |
| 'navigation' | |
| 'navigation_native' | |
| 'suspend' | |
| 'json_parse' | |
| '500_nodes' | |
| '1000_nodes' | |
| '1500_nodes' | |
| null | |
>(null); | |
const testOptions = useMemo( | |
() => ( | |
<VStack gap={2}> | |
<TestCaseEntry | |
onPress={() => setTest('navigation')} | |
title="Navigation" | |
description="The following test has the user navigate through 3 different screens and back to try to measure the time to mount different screens. " | |
/> | |
<TestCaseEntry | |
onPress={() => setTest('navigation_native')} | |
title="Native Navigation" | |
description="The following test has the user navigate through 3 different screens in a native stack navigator. " | |
/> | |
<TestCaseEntry | |
onPress={() => setTest('suspend')} | |
title="1000ms Suspend" | |
description="Tests suspending a component for 1000ms and seeing how long it takes to render" | |
/> | |
<TestCaseEntry | |
onPress={() => setTest('json_parse')} | |
title="JSON Parse " | |
description="Tests parsing the persisted queries JSON file with JSON.parse" | |
/> | |
<TestCaseEntry | |
onPress={() => setTest('500_nodes')} | |
title="Render 500 nodes" | |
description="A rendering nodes tests - renders 500 nodes" | |
/> | |
<TestCaseEntry | |
onPress={() => setTest('1000_nodes')} | |
title="Render 1000 nodes" | |
description="A rendering nodes tests - renders 1000 nodes" | |
/> | |
<TestCaseEntry | |
onPress={() => setTest('1500_nodes')} | |
title="Render 1500 nodes" | |
description="A rendering nodes tests - renders 1500 nodes" | |
/> | |
</VStack> | |
), | |
[setTest], | |
); | |
const caseToRender = useMemo(() => { | |
switch (selectedTest) { | |
case 'navigation': | |
return <FakeNavigator stackTitle="JS Navigator" />; | |
case 'navigation_native': | |
return ( | |
<FakeNavigator | |
stackTitle="Native Navigator" | |
Stack={FakeNativeStack} | |
/> | |
); | |
case 'suspend': | |
return ( | |
<Suspense fallback={<TextLabel1>Loading...</TextLabel1>}> | |
<FakeScreen | |
button={<SuspendingButton />} | |
title="Suspend 1000ms" | |
numberOfNodes={3} | |
/> | |
</Suspense> | |
); | |
case 'json_parse': | |
return <JsonParse />; | |
case '500_nodes': | |
return <FakeScreen title="500 nodes" numberOfNodes={500} />; | |
case '1000_nodes': | |
return <FakeScreen title="1000 nodes" numberOfNodes={1000} />; | |
case '1500_nodes': | |
return <FakeScreen title="1500 nodes" numberOfNodes={1500} />; | |
default: | |
return testOptions; | |
} | |
}, [testOptions, selectedTest]); | |
return ( | |
<Screen | |
alignItems="center" | |
navHeader={{ | |
title: 'Deterministic Performance tests', | |
leading: selectedTest !== null && <Back goBack={() => setTest(null)} />, | |
}} | |
> | |
{caseToRender} | |
</Screen> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment