From afb543683caba0d912d914aa8f7bc05bb7f9eb35 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Sat, 24 May 2025 15:44:11 -0700 Subject: [PATCH] introduce a way to inspect shadow node revision in Fantom (#51566) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51566 changelog: [internal] Expose [revision](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h#L229) of shadow node in Fantom tests. This makes it possible to write tests verifying that shadow nodes are only cloned when they should. Even though excessive cloning does not usually lead to bugs, it may lead to performance problems. Also introduce a test showing a performance problem where changing height of "Sibling" view from 1 to 2 will lead to component `D` being cloned by Yoga. Component D is not affected by the size change of Sibling and the clone is unnecessary. ```jsx ``` Reviewed By: rshest Differential Revision: D75287261 fbshipit-source-id: ea5acb2f5d7ba6e1e5bf895d8f82a16471122ec5 --- packages/react-native-fantom/src/index.js | 13 ++++ .../utilities/ShadowNodeRevisionGetter.js | 49 ++++++++++++ .../ShadowNodeRevisionGetter-itest.js | 74 +++++++++++++++++++ .../testing/fantom/specs/NativeFantom.js | 3 + 4 files changed, 139 insertions(+) create mode 100644 packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js create mode 100644 packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 85a3e56fe1d..18a122bf850 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -616,6 +616,19 @@ export function createShadowNodeReferenceCounter( return NativeFantom.createShadowNodeReferenceCounter(shadowNode); } +/** + * Returns a function that returns the current revision number for the supplied + * element's shadow node. + * + * @param node The node for which to create a revision getter. + */ +export function createShadowNodeRevisionGetter( + node: ReactNativeElement, +): () => ?number { + let shadowNode = getNativeNodeReference(node); + return NativeFantom.createShadowNodeRevisionGetter(shadowNode); +} + /** * Saves a heap snapshot after forcing garbage collection. * diff --git a/packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js b/packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js new file mode 100644 index 00000000000..8d251da8af1 --- /dev/null +++ b/packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js @@ -0,0 +1,49 @@ +/** + * 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 ReactNativeElement from '../../webapis/dom/nodes/ReadOnlyNode'; +import ensureInstance from './ensureInstance'; +import * as Fantom from '@react-native/fantom'; + +function createShadowNodeRevisionGetter( + element: ReactNativeElement, +): () => ?number { + const getRevision = Fantom.createShadowNodeRevisionGetter(element); + return () => { + return getRevision(); + }; +} + +export function createShadowNodeReferenceGetterRef(): [ + () => ?number, + React.RefSetter, +] { + let getRevision: ?() => ?number; + + function getShadowNodeReferenceCount() { + if (getRevision == null) { + throw new Error('ShadowNode revision getter was not initialized.'); + } + return getRevision(); + } + + function ref(instance: mixed | null) { + if (instance == null) { + return; + } + const element = ensureInstance(instance, ReactNativeElement); + if (getRevision != null) { + throw new Error('ShadowNode revision getter was already initialized.'); + } + getRevision = createShadowNodeRevisionGetter(element); + } + + return [getShadowNodeReferenceCount, ref]; +} diff --git a/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js new file mode 100644 index 00000000000..3475f0cb385 --- /dev/null +++ b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js @@ -0,0 +1,74 @@ +/** + * 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 {createShadowNodeReferenceGetterRef} from '../ShadowNodeRevisionGetter'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {ScrollView, View} from 'react-native'; + +test('base case when cloning results in revision +1', () => { + const root = Fantom.createRoot(); + + const [getRevision, ref] = createShadowNodeReferenceGetterRef(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(getRevision()).toBe(1); + + Fantom.runTask(() => { + root.render(); + }); + + expect(getRevision()).toBe(2); +}); + +test('changing height of the top item in ScrollView results in excessive cloning', () => { + const root = Fantom.createRoot(); + const [getRevision, ref] = createShadowNodeReferenceGetterRef(); + + Fantom.runTask(() => { + root.render( + + + + + + + + + + , + ); + }); + + expect(getRevision()).toBe(1); + + Fantom.runTask(() => { + root.render( + + + + + + + + + + , + ); + }); + + // TODO(T225268793): the below assertion should be: `expect(getRevision()).toBe(1);` + expect(getRevision()).toBe(2); +}); diff --git a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js index c3c5a500aa0..10b0d6d77f9 100644 --- a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js +++ b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js @@ -96,6 +96,9 @@ interface Spec extends TurboModule { createShadowNodeReferenceCounter( shadowNode: mixed /* ShadowNode */, ): () => number; + createShadowNodeRevisionGetter( + shadowNode: mixed /* ShadowNode */, + ): () => ?number; saveJSMemoryHeapSnapshot: (filePath: string) => void; }