Skip to content

Commit 9074f9d

Browse files
authored
Native perf benchmarking infra for Fabric components (#15772)
* add native perf benchmarking infrastructure for Fabric components measures rendering pipeline — JS reconciliation → Fabric → Yoga layout → Composition commit → frame Missing Components: Button, Modal, Pressable, TouchableHighlight, TouchableOpacity, SectionList. * Added all 6 missing components * Change files * updates baselines in release mode * add winAppsdk and dev mode * Change files * use winget as installer * use direct download from download.microsoft.com * relax TouchableOpacity bulk threshold in JS test * update snapshots * update snapshots * review comments and segregate native tests * poll for valid perf JSON instead of single read to handle transient UIA failures * nit * lint:fix and format * fix: use --msbuildprops instead of -- /p: for SDK version override
1 parent 40b2805 commit 9074f9d

41 files changed

Lines changed: 1759 additions & 14 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/perf-tests.yml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
perf-tests:
2828
name: Component Performance Tests
2929
runs-on: windows-latest
30-
timeout-minutes: 30
30+
timeout-minutes: 60
3131

3232
permissions:
3333
contents: read
@@ -49,9 +49,29 @@ jobs:
4949
- name: Install dependencies
5050
run: yarn install --frozen-lockfile
5151

52+
- name: Detect preinstalled Windows SDK
53+
id: winsdk
54+
shell: pwsh
55+
run: |
56+
# Find the latest SDK version already installed on the runner
57+
$sdkRoot = "${env:ProgramFiles(x86)}\Windows Kits\10\Include"
58+
$versions = Get-ChildItem $sdkRoot -Directory | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | Sort-Object { [version]$_.Name } -Descending
59+
if ($versions.Count -eq 0) {
60+
echo "::error::No Windows SDK found on runner"
61+
exit 1
62+
}
63+
$sdk = $versions[0].Name
64+
echo "version=$sdk" >> $env:GITHUB_OUTPUT
65+
echo "::notice::Using preinstalled Windows SDK $sdk"
66+
5267
- name: Build perf-testing package
5368
run: yarn workspace @react-native-windows/perf-testing build
5469

70+
# ── Build & Deploy RNTesterApp-Fabric (for native perf tests) ──
71+
- name: Build and deploy RNTesterApp-Fabric
72+
working-directory: packages/e2e-test-app-fabric
73+
run: yarn windows --release --no-launch --logging --msbuildprops WindowsTargetPlatformVersion=${{ steps.winsdk.outputs.version }}
74+
5575
# ── Run Tests ──────────────────────────────────────────
5676
- name: Run perf tests
5777
id: perf-run
@@ -61,7 +81,14 @@ jobs:
6181
RN_TARGET_PLATFORM: windows
6282
run: yarn perf:ci
6383
continue-on-error: true # Don't fail here — let comparison decide
64-
84+
- name: Run native perf tests
85+
id: native-perf-run
86+
working-directory: packages/e2e-test-app-fabric
87+
env:
88+
CI: 'true'
89+
RN_TARGET_PLATFORM: windows
90+
run: yarn perf:native:ci
91+
continue-on-error: true
6592
# ── Compare & Report ───────────────────────────────────
6693
- name: Compare against baselines
6794
id: compare
@@ -80,7 +107,9 @@ jobs:
80107
name: perf-results
81108
path: |
82109
packages/e2e-test-app-fabric/.perf-results/
110+
packages/e2e-test-app-fabric/.native-perf-results/
83111
packages/e2e-test-app-fabric/test/__perf__/**/__perf_snapshots__/
112+
packages/e2e-test-app-fabric/test/__native_perf__/**/__perf_snapshots__/
84113
retention-days: 30
85114

86115
# ── Status Gate ────────────────────────────────────────
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "add native perf benchmarking infrastructure for Fabric components",
4+
"packageName": "@react-native-windows/automation",
5+
"email": "74712637+iamAbhi-916@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "add native perf benchmarking infrastructure for Fabric components",
4+
"packageName": "@react-native-windows/perf-testing",
5+
"email": "74712637+iamAbhi-916@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/@react-native-windows/automation/src/AutomationEnvironment.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,24 @@ export default class AutomationEnvironment extends NodeEnvironment {
209209
// Set up the "Desktop" or Root session
210210
const rootBrowser = await webdriverio.remote(this.rootWebDriverOptions);
211211

212-
// Get the list of windows
213-
const allWindows = await rootBrowser.$$('//Window');
214-
215-
// Find our target window
212+
// Poll for the app window with timeout (cold starts can be slow)
213+
const windowTimeout = 300000; // 5 minutes
214+
const pollInterval = 2000;
215+
const deadline = Date.now() + windowTimeout;
216216
let appWindow: webdriverio.Element | undefined;
217-
for (const window of allWindows) {
218-
if ((await window.getAttribute('Name')) === appName) {
219-
appWindow = window;
217+
218+
while (Date.now() < deadline) {
219+
const allWindows = await rootBrowser.$$('//Window');
220+
for (const window of allWindows) {
221+
if ((await window.getAttribute('Name')) === appName) {
222+
appWindow = window;
223+
break;
224+
}
225+
}
226+
if (appWindow) {
220227
break;
221228
}
229+
await new Promise(resolve => setTimeout(resolve, pollInterval));
222230
}
223231

224232
if (!appWindow) {

packages/@react-native-windows/perf-testing/src/config/thresholdPresets.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,14 @@ export const ThresholdPresets: Readonly<
5656
maxCV: 0.6,
5757
mode: 'track',
5858
},
59+
60+
native: {
61+
maxDurationIncrease: 15,
62+
maxDuration: Infinity,
63+
minAbsoluteDelta: 5,
64+
maxRenderCount: 1,
65+
minRuns: 10,
66+
maxCV: 0.5,
67+
mode: 'gate',
68+
},
5969
};
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT License.
4+
* @format
5+
*/
6+
7+
'use strict';
8+
9+
const React = require('react');
10+
const {
11+
View,
12+
Text,
13+
TextInput,
14+
Image,
15+
ScrollView,
16+
FlatList,
17+
SectionList,
18+
Switch,
19+
ActivityIndicator,
20+
Button,
21+
Modal,
22+
Pressable,
23+
TouchableHighlight,
24+
TouchableOpacity,
25+
StyleSheet,
26+
} = require('react-native');
27+
28+
const {useState, useRef, useCallback, useEffect} = React;
29+
30+
const PHASE_IDLE = 'idle';
31+
const PHASE_CLEARING = 'clearing';
32+
const PHASE_MOUNTING = 'mounting';
33+
const PHASE_DONE = 'done';
34+
35+
const COMPONENT_REGISTRY = {
36+
View: () => <View style={styles.target} />,
37+
Text: () => <Text style={styles.target}>Benchmark Text</Text>,
38+
TextInput: () => (
39+
<TextInput style={styles.targetInput} placeholder="Benchmark" />
40+
),
41+
Image: () => (
42+
<Image
43+
style={styles.targetImage}
44+
source={require('../../assets/uie_thumb_normal.png')}
45+
/>
46+
),
47+
ScrollView: () => (
48+
<ScrollView style={styles.target}>
49+
{Array.from({length: 20}, (_, i) => (
50+
<View key={i} style={styles.scrollItem} />
51+
))}
52+
</ScrollView>
53+
),
54+
FlatList: () => (
55+
<FlatList
56+
style={styles.target}
57+
data={Array.from({length: 50}, (_, i) => ({key: String(i)}))}
58+
renderItem={({item}) => <Text>{item.key}</Text>}
59+
/>
60+
),
61+
SectionList: () => (
62+
<SectionList
63+
style={styles.target}
64+
sections={[
65+
{title: 'A', data: ['A1', 'A2', 'A3']},
66+
{title: 'B', data: ['B1', 'B2', 'B3']},
67+
]}
68+
renderItem={({item}) => <Text>{item}</Text>}
69+
renderSectionHeader={({section}) => <Text>{section.title}</Text>}
70+
/>
71+
),
72+
Switch: () => <Switch value={false} />,
73+
ActivityIndicator: () => <ActivityIndicator size="large" />,
74+
Button: () => <Button title="Benchmark" onPress={() => {}} />,
75+
Modal: () => (
76+
<Modal visible={false} transparent>
77+
<View style={styles.target} />
78+
</Modal>
79+
),
80+
Pressable: () => (
81+
<Pressable style={styles.target}>
82+
<Text>Press</Text>
83+
</Pressable>
84+
),
85+
TouchableHighlight: () => (
86+
<TouchableHighlight style={styles.target} onPress={() => {}}>
87+
<Text>Highlight</Text>
88+
</TouchableHighlight>
89+
),
90+
TouchableOpacity: () => (
91+
<TouchableOpacity style={styles.target} onPress={() => {}}>
92+
<Text>Opacity</Text>
93+
</TouchableOpacity>
94+
),
95+
};
96+
97+
function BenchmarkRunner() {
98+
const [componentName, setComponentName] = useState('View');
99+
const [runsInput, setRunsInput] = useState('15');
100+
const [phase, setPhase] = useState(PHASE_IDLE);
101+
const [showTarget, setShowTarget] = useState(false);
102+
const [resultsJson, setResultsJson] = useState('');
103+
104+
const durationsRef = useRef([]);
105+
const runIndexRef = useRef(0);
106+
const totalRunsRef = useRef(15);
107+
const markNameRef = useRef('');
108+
109+
const finishRun = useCallback(() => {
110+
const markEnd = `perf-end-${runIndexRef.current}`;
111+
performance.mark(markEnd);
112+
try {
113+
const measure = performance.measure(
114+
`perf-run-${runIndexRef.current}`,
115+
markNameRef.current,
116+
markEnd,
117+
);
118+
durationsRef.current.push(measure.duration);
119+
} catch (_) {}
120+
performance.clearMarks(markNameRef.current);
121+
performance.clearMarks(markEnd);
122+
performance.clearMeasures(`perf-run-${runIndexRef.current}`);
123+
124+
runIndexRef.current++;
125+
if (runIndexRef.current < totalRunsRef.current) {
126+
setPhase(PHASE_CLEARING);
127+
} else {
128+
setShowTarget(false);
129+
setResultsJson(
130+
JSON.stringify({
131+
componentName,
132+
runs: durationsRef.current.length,
133+
durations: durationsRef.current,
134+
}),
135+
);
136+
setPhase(PHASE_DONE);
137+
}
138+
}, [componentName]);
139+
140+
useEffect(() => {
141+
if (phase === PHASE_CLEARING) {
142+
setShowTarget(false);
143+
requestAnimationFrame(() => {
144+
setPhase(PHASE_MOUNTING);
145+
});
146+
}
147+
}, [phase]);
148+
149+
useEffect(() => {
150+
if (phase === PHASE_MOUNTING) {
151+
const markStart = `perf-start-${runIndexRef.current}`;
152+
markNameRef.current = markStart;
153+
performance.mark(markStart);
154+
setShowTarget(true);
155+
}
156+
}, [phase]);
157+
158+
useEffect(() => {
159+
if (phase === PHASE_MOUNTING && showTarget) {
160+
requestAnimationFrame(() => {
161+
finishRun();
162+
});
163+
}
164+
}, [phase, showTarget, finishRun]);
165+
166+
const handleRun = useCallback(() => {
167+
const runs = parseInt(runsInput, 10) || 15;
168+
totalRunsRef.current = runs;
169+
runIndexRef.current = 0;
170+
durationsRef.current = [];
171+
setResultsJson('');
172+
setPhase(PHASE_CLEARING);
173+
}, [runsInput]);
174+
175+
const ComponentFactory = COMPONENT_REGISTRY[componentName];
176+
177+
return (
178+
<View style={styles.container}>
179+
<View style={styles.controls}>
180+
<TextInput
181+
testID="perf-component-input"
182+
style={styles.input}
183+
value={componentName}
184+
onChangeText={setComponentName}
185+
placeholder="Component name"
186+
/>
187+
<TextInput
188+
testID="perf-runs-input"
189+
style={styles.input}
190+
value={runsInput}
191+
onChangeText={setRunsInput}
192+
keyboardType="numeric"
193+
placeholder="Runs"
194+
/>
195+
<Pressable
196+
testID="perf-run-btn"
197+
style={styles.button}
198+
onPress={handleRun}
199+
disabled={phase !== PHASE_IDLE && phase !== PHASE_DONE}>
200+
<Text style={styles.buttonText}>Run Benchmark</Text>
201+
</Pressable>
202+
</View>
203+
204+
<Text testID="perf-status" style={styles.status}>
205+
{phase}
206+
</Text>
207+
208+
<View style={styles.targetContainer}>
209+
{showTarget && ComponentFactory ? <ComponentFactory /> : null}
210+
</View>
211+
212+
<Text testID="perf-results" style={styles.results}>
213+
{resultsJson}
214+
</Text>
215+
</View>
216+
);
217+
}
218+
219+
const styles = StyleSheet.create({
220+
container: {flex: 1, padding: 8},
221+
controls: {flexDirection: 'row', gap: 8, marginBottom: 8},
222+
input: {
223+
borderWidth: 1,
224+
borderColor: '#ccc',
225+
padding: 6,
226+
minWidth: 100,
227+
fontSize: 14,
228+
},
229+
button: {
230+
backgroundColor: '#0078D4',
231+
paddingHorizontal: 16,
232+
paddingVertical: 8,
233+
borderRadius: 4,
234+
justifyContent: 'center',
235+
},
236+
buttonText: {color: 'white', fontWeight: 'bold'},
237+
status: {fontSize: 12, color: '#666', marginBottom: 4},
238+
targetContainer: {
239+
minHeight: 100,
240+
borderWidth: 1,
241+
borderColor: '#eee',
242+
marginBottom: 8,
243+
},
244+
target: {width: 80, height: 80, backgroundColor: '#f0f0f0'},
245+
targetInput: {width: 200, height: 40, borderWidth: 1, borderColor: '#999'},
246+
targetImage: {width: 80, height: 80},
247+
scrollItem: {height: 20, backgroundColor: '#ddd', marginBottom: 2},
248+
results: {fontSize: 10, fontFamily: 'monospace', color: '#333'},
249+
});
250+
251+
exports.displayName = 'NativePerfBenchmarkExample';
252+
exports.framework = 'React';
253+
exports.category = 'Basic';
254+
exports.title = 'Native Perf Benchmark';
255+
exports.description =
256+
'Measures native rendering pipeline via performance.mark/measure.';
257+
258+
exports.examples = [
259+
{
260+
title: 'Benchmark Runner',
261+
render: function () {
262+
return <BenchmarkRunner />;
263+
},
264+
},
265+
];

0 commit comments

Comments
 (0)