diff --git a/flow-typed/npm/tinybench_v3.1.x.js b/flow-typed/npm/tinybench_v4.1.x.js similarity index 93% rename from flow-typed/npm/tinybench_v3.1.x.js rename to flow-typed/npm/tinybench_v4.1.x.js index 69526caf1465..a40214d96733 100644 --- a/flow-typed/npm/tinybench_v3.1.x.js +++ b/flow-typed/npm/tinybench_v4.1.x.js @@ -91,7 +91,14 @@ declare module 'tinybench' { beforeEach?: (this: Task) => void | Promise, }; - export type Fn = () => Promise | mixed; + export interface FnReturnedObject { + overriddenDuration?: number; + } + + export type Fn = () => + | Promise + | void + | FnReturnedObject; declare export class Bench extends EventTarget { concurrency: null | 'task' | 'bench'; diff --git a/package.json b/package.json index 03d3261c8ad2..37721cb79951 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "signedsource": "^1.0.0", "supports-color": "^7.1.0", "temp-dir": "^2.0.0", - "tinybench": "^3.1.0", + "tinybench": "^4.1.0", "typescript": "5.8.3", "ws": "^6.2.3" }, diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js new file mode 100644 index 000000000000..1d9370309d9a --- /dev/null +++ b/packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js @@ -0,0 +1,60 @@ +/** + * 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 '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import measureRenderTime from '../../../../src/private/__tests__/utilities/measureRenderTime'; +import ViewNativeComponent from '../ViewNativeComponent'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {View} from 'react-native'; + +let root; +let testViews: React.MixedElement; + +const NUMBER_OF_VIEWS = 100; +const NUMBER_OF_ITERATIONS = 1000; + +component Noop(children: React.Node, style?: mixed) { + return children; +} + +Noop.displayName = 'Noop'; + +Fantom.unstable_benchmark + .suite('View vs. ViewNativeComponent', {minIterations: NUMBER_OF_ITERATIONS}) + .test.each( + [Noop, ViewNativeComponent, View], + Component => + `render ${NUMBER_OF_VIEWS} views (${Component.displayName ?? Component.name ?? 'ViewNativeComponent'})`, + () => { + return { + overriddenDuration: measureRenderTime(root, testViews), + }; + }, + Component => ({ + beforeAll: () => { + let views: React.Node = null; + for (let i = 0; i < NUMBER_OF_VIEWS; i++) { + views = ( + {views} + ); + } + // $FlowExpectedError[incompatible-type] + testViews = views; + }, + beforeEach: () => { + root = Fantom.createRoot(); + }, + afterEach: () => { + root.destroy(); + }, + }), + ); diff --git a/packages/react-native/src/private/__tests__/utilities/measureRenderTime.js b/packages/react-native/src/private/__tests__/utilities/measureRenderTime.js new file mode 100644 index 000000000000..938a85d3bf15 --- /dev/null +++ b/packages/react-native/src/private/__tests__/utilities/measureRenderTime.js @@ -0,0 +1,46 @@ +/** + * 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 + * @oncall react_native + */ + +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; + +const now = Fantom.unstable_benchmark.now; + +export default function measureRenderTime( + root: Fantom.Root, + elements: React.MixedElement, +): number { + let startTime: ?number; + let endTime: ?number; + + function RecordStart() { + startTime ??= now(); + return null; + } + + function RecordEnd() { + endTime = now(); + return null; + } + + Fantom.runTask(() => { + // React renders nodes using a depth-first algorithm. + root.render( + <> + + {elements} + + , + ); + }); + + return nullthrows(endTime) - nullthrows(startTime); +} diff --git a/private/react-native-fantom/src/Benchmark.js b/private/react-native-fantom/src/Benchmark.js index afeb18ba5ee5..f5a78dca345c 100644 --- a/private/react-native-fantom/src/Benchmark.js +++ b/private/react-native-fantom/src/Benchmark.js @@ -15,6 +15,7 @@ import NativeCPUTime from 'react-native/src/private/testing/fantom/specs/NativeC import { Bench, type BenchOptions, + type Fn, type FnOptions, type TaskResult, } from 'tinybench'; @@ -65,20 +66,20 @@ interface ParameterizedTestFunction { ( testArgs: $ReadOnlyArray, name: TestWithArgName, - fn: (testArg: TestArgType) => void, + fn: (testArg: TestArgType) => ReturnType, options?: TestWithArgOptions, ): SuiteAPI; only: ( testArgs: $ReadOnlyArray, name: TestWithArgName, - fn: (testArg: TestArgType) => void, + fn: (testArg: TestArgType) => ReturnType, options?: TestWithArgOptions, ) => SuiteAPI; } interface TestFunction { - (name: string, fn: () => void, options?: FnOptions): SuiteAPI; - only: (name: string, fn: () => void, options?: FnOptions) => SuiteAPI; + (name: string, fn: Fn, options?: FnOptions): SuiteAPI; + only: (name: string, fn: Fn, options?: FnOptions) => SuiteAPI; // `each` allows to run the same test multiple times with different arguments, provided as an array of values: each: ParameterizedTestFunction; } @@ -90,10 +91,14 @@ interface SuiteAPI { interface TestTask { name: string; - fn: () => void; + fn: Fn; options: InternalTestOptions | void; } +export function now(): number { + return NativeCPUTime.getCPUTimeNanos() / 1000000; +} + export function suite( suiteName: string, suiteOptions?: SuiteOptions = {}, @@ -127,7 +132,7 @@ export function suite( benchOptions.name = suiteName; benchOptions.throws = true; - benchOptions.now = () => NativeCPUTime.getCPUTimeNanos() / 1000000; + benchOptions.now = now; if (!isTestOnly) { if (suiteOptions.minIterations != null) { @@ -197,16 +202,12 @@ export function suite( reportBenchmarkResult(createBenchmarkResultsObject(bench, tasks)); }); - const test = ( - name: string, - fn: () => void, - options?: FnOptions, - ): SuiteAPI => { + const test = (name: string, fn: Fn, options?: FnOptions): SuiteAPI => { tasks.push({name, fn, options}); return suiteAPI; }; - test.only = (name: string, fn: () => void, options?: FnOptions): SuiteAPI => { + test.only = (name: string, fn: Fn, options?: FnOptions): SuiteAPI => { tasks.push({name, fn, options: {...options, only: true}}); return suiteAPI; }; @@ -214,7 +215,7 @@ export function suite( const testWithArg = ( testArg: TestArgType, name: TestWithArgName, - fn: (testArg: TestArgType) => void, + fn: (testArg: TestArgType) => ReturnType, options?: TestWithArgOptions, only?: boolean = false, ): TestTask => { @@ -232,7 +233,7 @@ export function suite( const testEach: ParameterizedTestFunction = ( testArgs: $ReadOnlyArray, name: TestWithArgName, - fn: (testArg: TestArgType) => void, + fn: (testArg: TestArgType) => ReturnType, options?: TestWithArgOptions, ): SuiteAPI => { for (const testArg of testArgs) { diff --git a/yarn.lock b/yarn.lock index 6ecd59616224..6aa329922d90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9121,10 +9121,10 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -tinybench@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-3.1.0.tgz#ec68451ff05233cf3de12c46f39f06011897109a" - integrity sha512-Km+oMh2xqNCxuyoUsqbRmHgFSd8sATh7v7xreP+kHN6x67w28Pawr83WmBxcaORvxkc0Ex6zgqK951yBnTFaaQ== +tinybench@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-4.1.0.tgz#090118e51159eb105f3cc2ef5cf371f3f8adc7bf" + integrity sha512-8JZoQRJgWWEIIeAmpiNmMHIREmUY3oGX8GRmlmNapLr/qtgMe+K76vM2qabh85hNScnE2lqTVTajVETjuD9Ixg== tmp@^0.0.33: version "0.0.33"