mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add API to run benchmarks in Fantom (#48452)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
96205dd78e
commit
3795f0fb66
@@ -46,6 +46,8 @@ const DEFAULT_MODE: FantomTestConfigMode =
|
||||
|
||||
const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/;
|
||||
|
||||
const FANTOM_BENCHMARK_SUITE_RE = /\nbenchmark(\s*)\.suite\(/g;
|
||||
|
||||
/**
|
||||
* Extracts the Fantom configuration from the test file, specified as part of
|
||||
* the docblock comment. E.g.:
|
||||
@@ -69,7 +71,9 @@ const FANTOM_FLAG_FORMAT = /^(\w+):(\w+)$/;
|
||||
export default function getFantomTestConfig(
|
||||
testPath: string,
|
||||
): FantomTestConfig {
|
||||
const docblock = extract(fs.readFileSync(testPath, 'utf8'));
|
||||
const testContents = fs.readFileSync(testPath, 'utf8');
|
||||
|
||||
const docblock = extract(testContents);
|
||||
const pragmas = parse(docblock) as DocblockPragmas;
|
||||
|
||||
const config: FantomTestConfig = {
|
||||
@@ -102,6 +106,10 @@ export default function getFantomTestConfig(
|
||||
default:
|
||||
throw new Error(`Invalid Fantom mode: ${mode}`);
|
||||
}
|
||||
} else {
|
||||
if (FANTOM_BENCHMARK_SUITE_RE.test(testContents)) {
|
||||
config.mode = FantomTestConfigMode.Optimized;
|
||||
}
|
||||
}
|
||||
|
||||
const maybeRawFlagConfig = pragmas.fantom_flags;
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
import nullthrows from 'nullthrows';
|
||||
import NativeCPUTime from 'react-native/src/private/specs/modules/NativeCPUTime';
|
||||
import {
|
||||
Bench,
|
||||
type BenchOptions,
|
||||
type FnOptions,
|
||||
type TaskResult,
|
||||
} from 'tinybench';
|
||||
|
||||
type SuiteOptions = $ReadOnly<{
|
||||
...Pick<
|
||||
BenchOptions,
|
||||
'iterations' | 'time' | 'warmup' | 'warmupIterations' | 'warmupTime',
|
||||
>,
|
||||
disableOptimizedBuildCheck?: boolean,
|
||||
}>;
|
||||
|
||||
type SuiteResults = Array<$ReadOnly<TaskResult>>;
|
||||
|
||||
interface SuiteAPI {
|
||||
add(name: string, fn: () => void, options?: FnOptions): SuiteAPI;
|
||||
verify(fn: (results: SuiteResults) => void): SuiteAPI;
|
||||
}
|
||||
|
||||
export function suite(
|
||||
suiteName: string,
|
||||
suiteOptions?: ?SuiteOptions,
|
||||
): SuiteAPI {
|
||||
const {disableOptimizedBuildCheck, ...benchOptions} = suiteOptions ?? {};
|
||||
|
||||
const bench = new Bench({
|
||||
...benchOptions,
|
||||
name: suiteName,
|
||||
throws: true,
|
||||
now: () => NativeCPUTime.getCPUTimeNanos() / 1000000,
|
||||
});
|
||||
|
||||
const verifyFns = [];
|
||||
|
||||
global.it(suiteName, () => {
|
||||
if (bench.tasks.length === 0) {
|
||||
throw new Error('No benchmark tests defined');
|
||||
}
|
||||
|
||||
bench.runSync();
|
||||
|
||||
printBenchmarkResults(bench);
|
||||
|
||||
for (const verify of verifyFns) {
|
||||
verify(bench.results);
|
||||
}
|
||||
|
||||
if (!NativeCPUTime.hasAccurateCPUTimeNanosForBenchmarks()) {
|
||||
throw new Error(
|
||||
'`NativeCPUTime` module does not provide accurate CPU time information in this environment. Please run the benchmarks in an environment where it does.',
|
||||
);
|
||||
}
|
||||
|
||||
if (__DEV__ && disableOptimizedBuildCheck !== true) {
|
||||
throw new Error('Benchmarks should not be run in development mode');
|
||||
}
|
||||
});
|
||||
|
||||
const suiteAPI = {
|
||||
add(name: string, fn: () => void, options?: FnOptions): SuiteAPI {
|
||||
bench.add(name, fn, options);
|
||||
return suiteAPI;
|
||||
},
|
||||
verify(fn: (results: SuiteResults) => void): SuiteAPI {
|
||||
verifyFns.push(fn);
|
||||
return suiteAPI;
|
||||
},
|
||||
};
|
||||
|
||||
return suiteAPI;
|
||||
}
|
||||
|
||||
function printBenchmarkResults(bench: Bench) {
|
||||
const longestTaskNameLength = bench.tasks.reduce(
|
||||
(maxLength, task) => Math.max(maxLength, task.name.length),
|
||||
0,
|
||||
);
|
||||
const separatorWidth = 121 + longestTaskNameLength - 'Task name'.length;
|
||||
|
||||
console.log('-'.repeat(separatorWidth));
|
||||
console.log(bench.name);
|
||||
console.table(nullthrows(bench.table()));
|
||||
console.log('-'.repeat(separatorWidth) + '\n');
|
||||
}
|
||||
+68
-1
@@ -14,6 +14,7 @@ import type {
|
||||
} from './getFantomRenderedOutput';
|
||||
import type {MixedElement} from 'react';
|
||||
|
||||
import * as Benchmark from './Benchmark';
|
||||
import getFantomRenderedOutput from './getFantomRenderedOutput';
|
||||
import ReactFabric from 'react-native/Libraries/Renderer/shims/ReactFabric';
|
||||
import NativeFantom from 'react-native/src/private/specs/modules/NativeFantom';
|
||||
@@ -63,7 +64,7 @@ class Root {
|
||||
this.#hasRendered = true;
|
||||
}
|
||||
|
||||
ReactFabric.render(element, this.#surfaceId, () => {}, true);
|
||||
ReactFabric.render(element, this.#surfaceId, null, true);
|
||||
}
|
||||
|
||||
getMountingLogs(): Array<string> {
|
||||
@@ -137,3 +138,69 @@ export function runWorkLoop(): void {
|
||||
export function createRoot(rootConfig?: RootConfig): Root {
|
||||
return new Root(rootConfig);
|
||||
}
|
||||
|
||||
export const benchmark = Benchmark;
|
||||
|
||||
/**
|
||||
* Quick and dirty polyfills required by tinybench.
|
||||
*/
|
||||
|
||||
if (typeof global.Event === 'undefined') {
|
||||
global.Event = class Event {
|
||||
constructor() {}
|
||||
};
|
||||
} else {
|
||||
console.warn(
|
||||
'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.',
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof global.EventTarget === 'undefined') {
|
||||
global.EventTarget = class EventTarget {
|
||||
listeners: $FlowFixMe;
|
||||
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
addEventListener(type: string, cb: () => void) {
|
||||
if (!(type in this.listeners)) {
|
||||
this.listeners[type] = [];
|
||||
}
|
||||
this.listeners[type].push(cb);
|
||||
}
|
||||
|
||||
removeEventListener(type: string, cb: () => void): void {
|
||||
if (!(type in this.listeners)) {
|
||||
return;
|
||||
}
|
||||
let handlers = this.listeners[type];
|
||||
for (let i in handlers) {
|
||||
if (cb === handlers[i]) {
|
||||
handlers.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchEvent(type: string, event: Event) {
|
||||
if (!(type in this.listeners)) {
|
||||
return;
|
||||
}
|
||||
let handlers = this.listeners[type];
|
||||
for (let i in handlers) {
|
||||
handlers[i].call(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
clearEventListeners() {
|
||||
for (let i in this.listeners) {
|
||||
delete this.listeners[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.warn(
|
||||
'The global Event class is already defined. If this API is already defined by React Native, you might want to remove this logic.',
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user