Skip to content

Commit 3795f0f

Browse files
rubennortefacebook-github-bot
authored andcommitted
Add API to run benchmarks in Fantom (#48452)
Summary: Pull Request resolved: #48452 Changelog: [internal] Implements a basic API to run benchmarks with Fantom (using `tinybench` under the hood): ``` import {benchmark} from 'react-native/fantom'; benchmark .suite('Suite name', { // options }) .add( 'Test name', () => { // code to benchmark }, { beforeAll: () => {}, beforeEach: () => {}, afterEach: () => {}, afterAll: () => {}, }, ) .verify(results => { // check results and throw an error if the expectations fail }); ``` Features: * Print benchmark results in the console as a table. * It opts into optimized builds automatically * Verifies that optimized build is used (unless manually opting out of the check via `disableOptimizedBuildCheck`). * Supports verification of results (making expectations and making the test fail if the benchmark doesn't meet some expectations). Reviewed By: rshest Differential Revision: D66926183 fbshipit-source-id: 61cfa7689ea7684eb870fbbc815b8d236a1871e6
1 parent 96205dd commit 3795f0f

3 files changed

Lines changed: 176 additions & 2 deletions

File tree

packages/react-native-fantom/runner/getFantomTestConfig.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const DEFAULT_MODE: FantomTestConfigMode =
4646

4747
const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/;
4848

49+
const FANTOM_BENCHMARK_SUITE_RE = /\nbenchmark(\s*)\.suite\(/g;
50+
4951
/**
5052
* Extracts the Fantom configuration from the test file, specified as part of
5153
* the docblock comment. E.g.:
@@ -69,7 +71,9 @@ const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/;
6971
export default function getFantomTestConfig(
7072
testPath: string,
7173
): FantomTestConfig {
72-
const docblock = extract(fs.readFileSync(testPath, 'utf8'));
74+
const testContents = fs.readFileSync(testPath, 'utf8');
75+
76+
const docblock = extract(testContents);
7377
const pragmas = parse(docblock) as DocblockPragmas;
7478

7579
const config: FantomTestConfig = {
@@ -102,6 +106,10 @@ export default function getFantomTestConfig(
102106
default:
103107
throw new Error(`Invalid Fantom mode: ${mode}`);
104108
}
109+
} else {
110+
if (FANTOM_BENCHMARK_SUITE_RE.test(testContents)) {
111+
config.mode = FantomTestConfigMode.Optimized;
112+
}
105113
}
106114

107115
const maybeRawFlagConfig = pragmas.fantom_flags;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import nullthrows from 'nullthrows';
12+
import NativeCPUTime from 'react-native/src/private/specs/modules/NativeCPUTime';
13+
import {
14+
Bench,
15+
type BenchOptions,
16+
type FnOptions,
17+
type TaskResult,
18+
} from 'tinybench';
19+
20+
type SuiteOptions = $ReadOnly<{
21+
...Pick<
22+
BenchOptions,
23+
'iterations' | 'time' | 'warmup' | 'warmupIterations' | 'warmupTime',
24+
>,
25+
disableOptimizedBuildCheck?: boolean,
26+
}>;
27+
28+
type SuiteResults = Array<$ReadOnly<TaskResult>>;
29+
30+
interface SuiteAPI {
31+
add(name: string, fn: () => void, options?: FnOptions): SuiteAPI;
32+
verify(fn: (results: SuiteResults) => void): SuiteAPI;
33+
}
34+
35+
export function suite(
36+
suiteName: string,
37+
suiteOptions?: ?SuiteOptions,
38+
): SuiteAPI {
39+
const {disableOptimizedBuildCheck, ...benchOptions} = suiteOptions ?? {};
40+
41+
const bench = new Bench({
42+
...benchOptions,
43+
name: suiteName,
44+
throws: true,
45+
now: () => NativeCPUTime.getCPUTimeNanos() / 1000000,
46+
});
47+
48+
const verifyFns = [];
49+
50+
global.it(suiteName, () => {
51+
if (bench.tasks.length === 0) {
52+
throw new Error('No benchmark tests defined');
53+
}
54+
55+
bench.runSync();
56+
57+
printBenchmarkResults(bench);
58+
59+
for (const verify of verifyFns) {
60+
verify(bench.results);
61+
}
62+
63+
if (!NativeCPUTime.hasAccurateCPUTimeNanosForBenchmarks()) {
64+
throw new Error(
65+
'`NativeCPUTime` module does not provide accurate CPU time information in this environment. Please run the benchmarks in an environment where it does.',
66+
);
67+
}
68+
69+
if (__DEV__ && disableOptimizedBuildCheck !== true) {
70+
throw new Error('Benchmarks should not be run in development mode');
71+
}
72+
});
73+
74+
const suiteAPI = {
75+
add(name: string, fn: () => void, options?: FnOptions): SuiteAPI {
76+
bench.add(name, fn, options);
77+
return suiteAPI;
78+
},
79+
verify(fn: (results: SuiteResults) => void): SuiteAPI {
80+
verifyFns.push(fn);
81+
return suiteAPI;
82+
},
83+
};
84+
85+
return suiteAPI;
86+
}
87+
88+
function printBenchmarkResults(bench: Bench) {
89+
const longestTaskNameLength = bench.tasks.reduce(
90+
(maxLength, task) => Math.max(maxLength, task.name.length),
91+
0,
92+
);
93+
const separatorWidth = 121 + longestTaskNameLength - 'Task name'.length;
94+
95+
console.log('-'.repeat(separatorWidth));
96+
console.log(bench.name);
97+
console.table(nullthrows(bench.table()));
98+
console.log('-'.repeat(separatorWidth) + '\n');
99+
}

packages/react-native-fantom/src/index.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
} from './getFantomRenderedOutput';
1515
import type {MixedElement} from 'react';
1616

17+
import * as Benchmark from './Benchmark';
1718
import getFantomRenderedOutput from './getFantomRenderedOutput';
1819
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
1920
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';
@@ -63,7 +64,7 @@ class Root {
6364
this.#hasRendered = true;
6465
}
6566

66-
ReactFabric.render(element, this.#surfaceId, () => {}, true);
67+
ReactFabric.render(element, this.#surfaceId, null, true);
6768
}
6869

6970
getMountingLogs(): Array<string> {
@@ -137,3 +138,69 @@ export function runWorkLoop(): void {
137138
export function createRoot(rootConfig?: RootConfig): Root {
138139
return new Root(rootConfig);
139140
}
141+
142+
export const benchmark = Benchmark;
143+
144+
/**
145+
* Quick and dirty polyfills required by tinybench.
146+
*/
147+
148+
if (typeof global.Event === 'undefined') {
149+
global.Event = class Event {
150+
constructor() {}
151+
};
152+
} else {
153+
console.warn(
154+
'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.',
155+
);
156+
}
157+
158+
if (typeof global.EventTarget === 'undefined') {
159+
global.EventTarget = class EventTarget {
160+
listeners: $FlowFixMe;
161+
162+
constructor() {
163+
this.listeners = {};
164+
}
165+
166+
addEventListener(type: string, cb: () => void) {
167+
if (!(type in this.listeners)) {
168+
this.listeners[type] = [];
169+
}
170+
this.listeners[type].push(cb);
171+
}
172+
173+
removeEventListener(type: string, cb: () => void): void {
174+
if (!(type in this.listeners)) {
175+
return;
176+
}
177+
let handlers = this.listeners[type];
178+
for (let i in handlers) {
179+
if (cb === handlers[i]) {
180+
handlers.splice(i, 1);
181+
return;
182+
}
183+
}
184+
}
185+
186+
dispatchEvent(type: string, event: Event) {
187+
if (!(type in this.listeners)) {
188+
return;
189+
}
190+
let handlers = this.listeners[type];
191+
for (let i in handlers) {
192+
handlers[i].call(this, event);
193+
}
194+
}
195+
196+
clearEventListeners() {
197+
for (let i in this.listeners) {
198+
delete this.listeners[i];
199+
}
200+
}
201+
};
202+
} else {
203+
console.warn(
204+
'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.',
205+
);
206+
}

0 commit comments

Comments
 (0)