Skip to content

Commit e69e35e

Browse files
yungstersfacebook-github-bot
authored andcommitted
Fantom: Create Shadow Node Reference Counter (#51088)
Summary: Pull Request resolved: #51088 Creates `ShadowNodeReferenceCounter`, a module with utilities for writing Fantom tests that make assertions about the reference count for a `ShadowNode` object. Changelog: [Internal] Reviewed By: lunaleaps Differential Revision: D74131710 fbshipit-source-id: a949a402ee52f40445ce99c712540e80c8a05065
1 parent f85b30b commit e69e35e

4 files changed

Lines changed: 288 additions & 0 deletions

File tree

packages/react-native-fantom/src/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,20 @@ if (typeof global.EventTarget === 'undefined') {
566566
);
567567
}
568568

569+
/**
570+
* Returns a function that returns the current reference count for the supplied
571+
* element's shadow node. If the reference count is zero, that means the shadow
572+
* node has been deallocated.
573+
*
574+
* @param node The node for which to create a reference counting function.
575+
*/
576+
export function createShadowNodeReferenceCounter(
577+
node: ReactNativeElement,
578+
): () => number {
579+
let shadowNode = getNativeNodeReference(node);
580+
return NativeFantom.createShadowNodeReferenceCounter(shadowNode);
581+
}
582+
569583
/**
570584
* Saves a heap snapshot after forcing garbage collection.
571585
*
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import ReactNativeElement from '../../webapis/dom/nodes/ReadOnlyNode';
13+
import ensureInstance from './ensureInstance';
14+
import * as Fantom from '@react-native/fantom';
15+
16+
export function createShadowNodeReferenceCounter(
17+
element: ReactNativeElement,
18+
): () => number {
19+
const getReferenceCount = Fantom.createShadowNodeReferenceCounter(element);
20+
// Create the reference counting function in a helper instead of creating a
21+
// closure here, which would unintentionally retain a reference to `element`.
22+
return createExpirationChecker(getReferenceCount);
23+
}
24+
25+
function createExpirationChecker(
26+
getReferenceCount: () => number,
27+
): () => number {
28+
return () => {
29+
Fantom.runTask(() => {
30+
global.gc();
31+
});
32+
return getReferenceCount();
33+
};
34+
}
35+
36+
export function createShadowNodeReferenceCountingRef(): [
37+
() => number,
38+
React.RefSetter<mixed>,
39+
] {
40+
let getReferenceCount: ?() => number;
41+
42+
function getShadowNodeReferenceCount() {
43+
if (getReferenceCount == null) {
44+
throw new Error('ShadowNode reference counter was not initialized.');
45+
}
46+
return getReferenceCount();
47+
}
48+
49+
function ref(instance: mixed | null) {
50+
if (instance == null) {
51+
return;
52+
}
53+
const element = ensureInstance(instance, ReactNativeElement);
54+
if (getReferenceCount != null) {
55+
throw new Error('ShadowNode reference counter was already initialized.');
56+
}
57+
getReferenceCount = createShadowNodeReferenceCounter(element);
58+
}
59+
60+
return [getShadowNodeReferenceCount, ref];
61+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import 'react-native/Libraries/Core/InitializeCore';
13+
14+
import type {Node} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes';
15+
16+
import {getNodeFromPublicInstance} from '../../../../../Libraries/ReactPrivate/ReactNativePrivateInterface';
17+
import ReactNativeElement from '../../../webapis/dom/nodes/ReactNativeElement';
18+
import ensureInstance from '../ensureInstance';
19+
import isUnreachable from '../isUnreachable';
20+
import {
21+
createShadowNodeReferenceCounter,
22+
createShadowNodeReferenceCountingRef,
23+
} from '../ShadowNodeReferenceCounter';
24+
import * as Fantom from '@react-native/fantom';
25+
import nullthrows from 'nullthrows';
26+
import * as React from 'react';
27+
import {View} from 'react-native';
28+
29+
test('shadow node expires when root is destroyed', () => {
30+
const root = Fantom.createRoot();
31+
32+
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
33+
34+
Fantom.runTask(() => {
35+
root.render(<View ref={ref} />);
36+
});
37+
38+
expect(getReferenceCount()).toBeGreaterThan(0);
39+
40+
Fantom.runTask(() => {
41+
root.destroy();
42+
});
43+
44+
expect(getReferenceCount()).toBe(0);
45+
});
46+
47+
test('element is not retained by `createShadowNodeReferenceCounter`', () => {
48+
const root = Fantom.createRoot();
49+
50+
let elementWeakRef: ?WeakRef<ReactNativeElement>;
51+
let getReferenceCount: ?() => number;
52+
53+
function ref(instance: React.ElementRef<typeof View> | null) {
54+
if (instance == null) {
55+
return;
56+
}
57+
const element = ensureInstance(instance, ReactNativeElement);
58+
elementWeakRef = new WeakRef(element);
59+
getReferenceCount = createShadowNodeReferenceCounter(element);
60+
}
61+
62+
Fantom.runTask(() => {
63+
root.render(
64+
<View>
65+
<View ref={ref} />
66+
</View>,
67+
);
68+
});
69+
70+
expect(isUnreachable(nullthrows(elementWeakRef))).toBe(false);
71+
expect(getReferenceCount?.()).toBeGreaterThan(0);
72+
73+
Fantom.runTask(() => {
74+
root.destroy();
75+
});
76+
77+
expect(isUnreachable(nullthrows(elementWeakRef))).toBe(true);
78+
expect(getReferenceCount?.()).toBe(0);
79+
});
80+
81+
test('shadow node expires when JavaScript element is reachable', () => {
82+
const root = Fantom.createRoot();
83+
84+
let element: ?ReactNativeElement;
85+
let getReferenceCount: ?() => number;
86+
87+
function ref(instance: React.ElementRef<typeof View> | null) {
88+
if (instance == null) {
89+
return;
90+
}
91+
element = ensureInstance(instance, ReactNativeElement);
92+
getReferenceCount = createShadowNodeReferenceCounter(element);
93+
}
94+
95+
Fantom.runTask(() => {
96+
root.render(
97+
<View>
98+
<View ref={ref} />
99+
</View>,
100+
);
101+
});
102+
103+
expect(element).not.toBe(undefined);
104+
expect(getNodeFromPublicInstance(nullthrows(element))).not.toBe(null);
105+
expect(getReferenceCount?.()).toBeGreaterThan(0);
106+
107+
Fantom.runTask(() => {
108+
root.destroy();
109+
});
110+
111+
expect(element).not.toBe(undefined);
112+
expect(getNodeFromPublicInstance(nullthrows(element))).toBe(null);
113+
expect(getReferenceCount?.()).toBe(0);
114+
});
115+
116+
test('shadow node is retained when JavaScript node is reachable', () => {
117+
const root = Fantom.createRoot();
118+
119+
let node: ?Node;
120+
let getReferenceCount: ?() => number;
121+
122+
function ref(instance: React.ElementRef<typeof View> | null) {
123+
if (instance == null) {
124+
return;
125+
}
126+
const element = ensureInstance(instance, ReactNativeElement);
127+
node = getNodeFromPublicInstance(element);
128+
getReferenceCount = createShadowNodeReferenceCounter(element);
129+
}
130+
131+
Fantom.runTask(() => {
132+
root.render(
133+
<View>
134+
<View ref={ref} />
135+
</View>,
136+
);
137+
});
138+
139+
expect(node).not.toBe(undefined);
140+
expect(getReferenceCount?.()).toBeGreaterThan(0);
141+
142+
Fantom.runTask(() => {
143+
root.destroy();
144+
});
145+
146+
expect(node).not.toBe(undefined);
147+
expect(getReferenceCount?.()).toBeGreaterThan(0);
148+
});
149+
150+
test('shadow node expires when replaced by null', () => {
151+
const root = Fantom.createRoot();
152+
153+
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
154+
155+
Fantom.runTask(() => {
156+
root.render(
157+
<View>
158+
<View ref={ref} />
159+
</View>,
160+
);
161+
});
162+
163+
expect(getReferenceCount()).toBeGreaterThan(0);
164+
165+
Fantom.runTask(() => {
166+
root.render(<View>{null}</View>);
167+
});
168+
169+
// TODO (T223254666): Delete this and figure out why test fails.
170+
Fantom.runTask(() => {
171+
root.render(<View>{null}</View>);
172+
});
173+
174+
expect(getReferenceCount()).toBe(0);
175+
});
176+
177+
test('shadow node expires when replaced by another view', () => {
178+
const root = Fantom.createRoot();
179+
180+
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
181+
182+
Fantom.runTask(() => {
183+
root.render(
184+
<View>
185+
<View key="a" ref={ref} />
186+
</View>,
187+
);
188+
});
189+
190+
expect(getReferenceCount()).toBeGreaterThan(0);
191+
192+
Fantom.runTask(() => {
193+
root.render(
194+
<View>
195+
<View key="b" />
196+
</View>,
197+
);
198+
});
199+
200+
// TODO (T223254666): Delete this and figure out why test fails.
201+
Fantom.runTask(() => {
202+
root.render(
203+
<View>
204+
<View key="b" />
205+
</View>,
206+
);
207+
});
208+
209+
expect(getReferenceCount()).toBe(0);
210+
});

packages/react-native/src/private/testing/fantom/specs/NativeFantom.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ interface Spec extends TurboModule {
9393
validateEmptyMessageQueue: () => void;
9494
getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string;
9595
reportTestSuiteResultsJSON: (results: string) => void;
96+
createShadowNodeReferenceCounter(
97+
shadowNode: mixed /* ShadowNode */,
98+
): () => number;
9699
saveJSMemoryHeapSnapshot: (filePath: string) => void;
97100
}
98101

0 commit comments

Comments
 (0)