mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Fantom: Create Shadow Node Reference Counter (#51088)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
f85b30b24b
commit
e69e35e370
+14
@@ -566,6 +566,20 @@ if (typeof global.EventTarget === 'undefined') {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function that returns the current reference count for the supplied
|
||||
* element's shadow node. If the reference count is zero, that means the shadow
|
||||
* node has been deallocated.
|
||||
*
|
||||
* @param node The node for which to create a reference counting function.
|
||||
*/
|
||||
export function createShadowNodeReferenceCounter(
|
||||
node: ReactNativeElement,
|
||||
): () => number {
|
||||
let shadowNode = getNativeNodeReference(node);
|
||||
return NativeFantom.createShadowNodeReferenceCounter(shadowNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a heap snapshot after forcing garbage collection.
|
||||
*
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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 ReactNativeElement from '../../webapis/dom/nodes/ReadOnlyNode';
|
||||
import ensureInstance from './ensureInstance';
|
||||
import * as Fantom from '@react-native/fantom';
|
||||
|
||||
export function createShadowNodeReferenceCounter(
|
||||
element: ReactNativeElement,
|
||||
): () => number {
|
||||
const getReferenceCount = Fantom.createShadowNodeReferenceCounter(element);
|
||||
// Create the reference counting function in a helper instead of creating a
|
||||
// closure here, which would unintentionally retain a reference to `element`.
|
||||
return createExpirationChecker(getReferenceCount);
|
||||
}
|
||||
|
||||
function createExpirationChecker(
|
||||
getReferenceCount: () => number,
|
||||
): () => number {
|
||||
return () => {
|
||||
Fantom.runTask(() => {
|
||||
global.gc();
|
||||
});
|
||||
return getReferenceCount();
|
||||
};
|
||||
}
|
||||
|
||||
export function createShadowNodeReferenceCountingRef(): [
|
||||
() => number,
|
||||
React.RefSetter<mixed>,
|
||||
] {
|
||||
let getReferenceCount: ?() => number;
|
||||
|
||||
function getShadowNodeReferenceCount() {
|
||||
if (getReferenceCount == null) {
|
||||
throw new Error('ShadowNode reference counter was not initialized.');
|
||||
}
|
||||
return getReferenceCount();
|
||||
}
|
||||
|
||||
function ref(instance: mixed | null) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
const element = ensureInstance(instance, ReactNativeElement);
|
||||
if (getReferenceCount != null) {
|
||||
throw new Error('ShadowNode reference counter was already initialized.');
|
||||
}
|
||||
getReferenceCount = createShadowNodeReferenceCounter(element);
|
||||
}
|
||||
|
||||
return [getShadowNodeReferenceCount, ref];
|
||||
}
|
||||
Vendored
+210
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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 'react-native/Libraries/Core/InitializeCore';
|
||||
|
||||
import type {Node} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes';
|
||||
|
||||
import {getNodeFromPublicInstance} from '../../../../../Libraries/ReactPrivate/ReactNativePrivateInterface';
|
||||
import ReactNativeElement from '../../../webapis/dom/nodes/ReactNativeElement';
|
||||
import ensureInstance from '../ensureInstance';
|
||||
import isUnreachable from '../isUnreachable';
|
||||
import {
|
||||
createShadowNodeReferenceCounter,
|
||||
createShadowNodeReferenceCountingRef,
|
||||
} from '../ShadowNodeReferenceCounter';
|
||||
import * as Fantom from '@react-native/fantom';
|
||||
import nullthrows from 'nullthrows';
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
test('shadow node expires when root is destroyed', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<View ref={ref} />);
|
||||
});
|
||||
|
||||
expect(getReferenceCount()).toBeGreaterThan(0);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.destroy();
|
||||
});
|
||||
|
||||
expect(getReferenceCount()).toBe(0);
|
||||
});
|
||||
|
||||
test('element is not retained by `createShadowNodeReferenceCounter`', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
let elementWeakRef: ?WeakRef<ReactNativeElement>;
|
||||
let getReferenceCount: ?() => number;
|
||||
|
||||
function ref(instance: React.ElementRef<typeof View> | null) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
const element = ensureInstance(instance, ReactNativeElement);
|
||||
elementWeakRef = new WeakRef(element);
|
||||
getReferenceCount = createShadowNodeReferenceCounter(element);
|
||||
}
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View ref={ref} />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(isUnreachable(nullthrows(elementWeakRef))).toBe(false);
|
||||
expect(getReferenceCount?.()).toBeGreaterThan(0);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.destroy();
|
||||
});
|
||||
|
||||
expect(isUnreachable(nullthrows(elementWeakRef))).toBe(true);
|
||||
expect(getReferenceCount?.()).toBe(0);
|
||||
});
|
||||
|
||||
test('shadow node expires when JavaScript element is reachable', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
let element: ?ReactNativeElement;
|
||||
let getReferenceCount: ?() => number;
|
||||
|
||||
function ref(instance: React.ElementRef<typeof View> | null) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
element = ensureInstance(instance, ReactNativeElement);
|
||||
getReferenceCount = createShadowNodeReferenceCounter(element);
|
||||
}
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View ref={ref} />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(element).not.toBe(undefined);
|
||||
expect(getNodeFromPublicInstance(nullthrows(element))).not.toBe(null);
|
||||
expect(getReferenceCount?.()).toBeGreaterThan(0);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.destroy();
|
||||
});
|
||||
|
||||
expect(element).not.toBe(undefined);
|
||||
expect(getNodeFromPublicInstance(nullthrows(element))).toBe(null);
|
||||
expect(getReferenceCount?.()).toBe(0);
|
||||
});
|
||||
|
||||
test('shadow node is retained when JavaScript node is reachable', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
let node: ?Node;
|
||||
let getReferenceCount: ?() => number;
|
||||
|
||||
function ref(instance: React.ElementRef<typeof View> | null) {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
const element = ensureInstance(instance, ReactNativeElement);
|
||||
node = getNodeFromPublicInstance(element);
|
||||
getReferenceCount = createShadowNodeReferenceCounter(element);
|
||||
}
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View ref={ref} />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(node).not.toBe(undefined);
|
||||
expect(getReferenceCount?.()).toBeGreaterThan(0);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.destroy();
|
||||
});
|
||||
|
||||
expect(node).not.toBe(undefined);
|
||||
expect(getReferenceCount?.()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('shadow node expires when replaced by null', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View ref={ref} />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(getReferenceCount()).toBeGreaterThan(0);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(<View>{null}</View>);
|
||||
});
|
||||
|
||||
// TODO (T223254666): Delete this and figure out why test fails.
|
||||
Fantom.runTask(() => {
|
||||
root.render(<View>{null}</View>);
|
||||
});
|
||||
|
||||
expect(getReferenceCount()).toBe(0);
|
||||
});
|
||||
|
||||
test('shadow node expires when replaced by another view', () => {
|
||||
const root = Fantom.createRoot();
|
||||
|
||||
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View key="a" ref={ref} />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(getReferenceCount()).toBeGreaterThan(0);
|
||||
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View key="b" />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
// TODO (T223254666): Delete this and figure out why test fails.
|
||||
Fantom.runTask(() => {
|
||||
root.render(
|
||||
<View>
|
||||
<View key="b" />
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(getReferenceCount()).toBe(0);
|
||||
});
|
||||
@@ -93,6 +93,9 @@ interface Spec extends TurboModule {
|
||||
validateEmptyMessageQueue: () => void;
|
||||
getRenderedOutput: (surfaceId: number, config: RenderFormatOptions) => string;
|
||||
reportTestSuiteResultsJSON: (results: string) => void;
|
||||
createShadowNodeReferenceCounter(
|
||||
shadowNode: mixed /* ShadowNode */,
|
||||
): () => number;
|
||||
saveJSMemoryHeapSnapshot: (filePath: string) => void;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user