Skip to content

Instantly share code, notes, and snippets.

@AndreiCalazans
Created November 17, 2022 17:19
Show Gist options
  • Save AndreiCalazans/42d377bac1a5de47300135bd1a0602b6 to your computer and use it in GitHub Desktop.
Save AndreiCalazans/42d377bac1a5de47300135bd1a0602b6 to your computer and use it in GitHub Desktop.
Debug Deterministic Performance screen
/* 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