From 32d92b3b869cbf99c6eb03fb34cdbda39ed46d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 13 Aug 2025 07:06:01 -0700 Subject: [PATCH 1/3] Upgrade tinybench to v4.1.0 Differential Revision: D80169516 --- .../npm/{tinybench_v3.1.x.js => tinybench_v4.1.x.js} | 9 ++++++++- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) rename flow-typed/npm/{tinybench_v3.1.x.js => tinybench_v4.1.x.js} (93%) 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/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" From 1b05c69a7052c8b571a64914817bbb4cadc176e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 13 Aug 2025 07:06:01 -0700 Subject: [PATCH 2/3] Expose timestamps for benchmarks via Fantom.unstable_benchmark.now Differential Revision: D80169515 --- private/react-native-fantom/src/Benchmark.js | 29 ++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) 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) { From 8a0f8b7bd3d243ab591ff89b3cc994922155d170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Wed, 13 Aug 2025 07:47:49 -0700 Subject: [PATCH 3/3] Add benchmark to compare rendering times for View and ViewNativeComponent (#53248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53248 Changelog: [internal] This adds a new benchmark that compares the rendering time (only rendering, not committing, mounting, effects, etc.) of `` and ``. Baseline: | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | ---------------------------------------- | ----------------- | ------------------ | ---------------------- | ---------------------- | ------- | | 0 | 'render 100 views (Noop)' | '333036 ± 0.39%' | '328452 ± 2393.0' | '3019 ± 0.18%' | '3045 ± 22' | 3003 | | 1 | 'render 100 views (ViewNativeComponent)' | '1335974 ± 3.45%' | '1228468 ± 7541.5' | '797 ± 0.71%' | '814 ± 5' | 1000 | | 2 | 'render 100 views (View)' | '2296988 ± 1.60%' | '2170821 ± 12374' | '449 ± 0.74%' | '461 ± 3' | 1000 | This shows that **`` currently has an overhead of 75% in rendering time**. I've also tested a modification of `View` such as: ``` component View(...props: ViewProps) { return { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type: ViewNativeComponent, key: undefined, // $FlowExpectedError[prop-missing] ref: props.ref, props, }; } ``` This makes `View` basically a no-op component, and the benchmark after this looks like: | (index) | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples | | ------- | ---------------------------------------- | ----------------- | ----------------- | ---------------------- | ---------------------- | ------- | | 0 | 'render 100 views (View)' | '1743010 ± 2.25%' | '1630816 ± 10616' | '600 ± 0.74%' | '613 ± 4' | 1000 | | 1 | 'render 100 views (ViewNativeComponent)' | '1370699 ± 4.04%' | '1242284 ± 14172' | '789 ± 0.74%' | '805 ± 9' | 1000 | This shows that `View`, just for existing as a wrapper component, has an overhead of 31% in rendering time, which means that **the opportunities to reduce the overhead beyond what we already did are limited**. Reviewed By: rshest Differential Revision: D80169514 --- ...-vs-ViewNativeComponent-benchmark-itest.js | 60 +++++++++++++++++++ .../__tests__/utilities/measureRenderTime.js | 46 ++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/react-native/Libraries/Components/View/__tests__/View-vs-ViewNativeComponent-benchmark-itest.js create mode 100644 packages/react-native/src/private/__tests__/utilities/measureRenderTime.js 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); +}