Skip to content

Commit

Permalink
[skip ci] Add API to run benchmarks in Fantom (facebook#48452)
Browse files Browse the repository at this point in the history
Summary:

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
  • Loading branch information
rubennorte authored and facebook-github-bot committed Jan 4, 2025
1 parent d05443c commit 2280ae9
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 2 deletions.
10 changes: 9 additions & 1 deletion packages/react-native-fantom/runner/getFantomTestConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.:
Expand All @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down
99 changes: 99 additions & 0 deletions packages/react-native-fantom/src/Benchmark.js
Original file line number Diff line number Diff line change
@@ -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');
}
69 changes: 68 additions & 1 deletion packages/react-native-fantom/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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.',
);
}

0 comments on commit 2280ae9

Please sign in to comment.