mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[test] Add Flight regression test for async debug info surviving Promise GC (#37037)
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Hanzo Dev
parent
c49275ba14
commit
7623c8a446
@@ -3932,4 +3932,232 @@ describe('ReactFlightAsyncDebugInfo', () => {
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not lose I/O debug info when intermediate promises are garbage collected', async () => {
|
||||
// Get a handle on the garbage collector without running the test with --expose-gc.
|
||||
const v8 = require('v8');
|
||||
const vm = require('vm');
|
||||
v8.setFlagsFromString('--expose-gc');
|
||||
const gc = vm.runInNewContext('gc');
|
||||
v8.setFlagsFromString('--no-expose-gc');
|
||||
|
||||
// The global setImmediate is patched to setTimeout in this test which
|
||||
// registers as new I/O in async_hooks. Use the real setImmediate for
|
||||
// yielding to the event loop while waiting for GC so that it doesn't add
|
||||
// I/O entries to the debug info of the component below.
|
||||
const {setImmediate: realSetImmediate} = require('timers');
|
||||
function tick() {
|
||||
return new Promise(resolve => realSetImmediate(resolve));
|
||||
}
|
||||
|
||||
let ioPromiseRef = null;
|
||||
let collectedIntermediatePromises = false;
|
||||
|
||||
async function getData(text) {
|
||||
const promise = delay(1);
|
||||
ioPromiseRef = new WeakRef(promise);
|
||||
await promise;
|
||||
return text.toUpperCase();
|
||||
}
|
||||
|
||||
async function waitForGarbageCollection(weakRef) {
|
||||
// deref() keeps the target alive until the end of the current task, so
|
||||
// check it on a later tick than the gc() call.
|
||||
let collected = false;
|
||||
for (let i = 0; !collected && i < 100; i++) {
|
||||
gc();
|
||||
await tick();
|
||||
collected = weakRef.deref() === undefined;
|
||||
await tick();
|
||||
}
|
||||
// The destroy() hooks of collected promises fire asynchronously. Yield
|
||||
// a few more times to ensure they have all run.
|
||||
for (let i = 0; i < 5; i++) {
|
||||
gc();
|
||||
await tick();
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
async function Component() {
|
||||
const result = await getData('hi');
|
||||
// At this point the intermediate promises (the delay() promise and
|
||||
// getData's own async function promise) are unreachable. The async
|
||||
// debug info graph holds them only weakly through WeakRefs. Force them
|
||||
// to be garbage collected, which fires their async_hooks destroy()
|
||||
// hooks, before this component resolves, which is when React walks the
|
||||
// async graph to emit the component's debug info.
|
||||
collectedIntermediatePromises =
|
||||
await waitForGarbageCollection(ioPromiseRef);
|
||||
return result;
|
||||
}
|
||||
|
||||
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
|
||||
|
||||
const readable = new Stream.PassThrough(streamOptions);
|
||||
|
||||
const result = ReactServerDOMClient.createFromNodeStream(readable, {
|
||||
moduleMap: {},
|
||||
moduleLoading: {},
|
||||
});
|
||||
stream.pipe(readable);
|
||||
|
||||
expect(await result).toBe('HI');
|
||||
// If this fails, the GC helper above no longer actually collects the
|
||||
// promises and this test is not testing anything.
|
||||
expect(collectedIntermediatePromises).toBe(true);
|
||||
|
||||
await finishLoadingStream(readable);
|
||||
if (
|
||||
__DEV__ &&
|
||||
gate(
|
||||
flags =>
|
||||
flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
|
||||
)
|
||||
) {
|
||||
const debugInfo = getDebugInfo(result);
|
||||
// The I/O entry for delay() must survive the garbage collection of the
|
||||
// intermediate promises. The graph nodes are intentionally held
|
||||
// strongly (only the promises themselves are held weakly through
|
||||
// WeakRefs) so that the originating I/O is still reachable when the
|
||||
// debug info is emitted after the promises are gone.
|
||||
expect(debugInfo).toContainEqual(
|
||||
expect.objectContaining({
|
||||
awaited: expect.objectContaining({name: 'delay'}),
|
||||
}),
|
||||
);
|
||||
expect(debugInfo).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3995,
|
||||
109,
|
||||
3936,
|
||||
87,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"awaited": {
|
||||
"end": 0,
|
||||
"env": "Server",
|
||||
"name": "delay",
|
||||
"owner": {
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3995,
|
||||
109,
|
||||
3936,
|
||||
87,
|
||||
],
|
||||
],
|
||||
},
|
||||
"stack": [
|
||||
[
|
||||
"delay",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
87,
|
||||
12,
|
||||
86,
|
||||
3,
|
||||
],
|
||||
[
|
||||
"getData",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3957,
|
||||
21,
|
||||
3956,
|
||||
5,
|
||||
],
|
||||
[
|
||||
"Component",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3983,
|
||||
26,
|
||||
3982,
|
||||
5,
|
||||
],
|
||||
],
|
||||
"start": 0,
|
||||
"value": {
|
||||
"value": undefined,
|
||||
},
|
||||
},
|
||||
"env": "Server",
|
||||
"owner": {
|
||||
"env": "Server",
|
||||
"key": null,
|
||||
"name": "Component",
|
||||
"props": {},
|
||||
"stack": [
|
||||
[
|
||||
"Object.<anonymous>",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3995,
|
||||
109,
|
||||
3936,
|
||||
87,
|
||||
],
|
||||
],
|
||||
},
|
||||
"stack": [
|
||||
[
|
||||
"getData",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3959,
|
||||
7,
|
||||
3956,
|
||||
5,
|
||||
],
|
||||
[
|
||||
"Component",
|
||||
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
|
||||
3983,
|
||||
26,
|
||||
3982,
|
||||
5,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"time": 0,
|
||||
},
|
||||
{
|
||||
"awaited": {
|
||||
"byteSize": 0,
|
||||
"end": 0,
|
||||
"name": "rsc stream",
|
||||
"owner": null,
|
||||
"start": 0,
|
||||
"value": {
|
||||
"value": "stream",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user