Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 71ecaf8

Browse filesBrowse files
eps1lonclaude
andauthored
[test] Add Flight regression test for async debug info surviving Promise GC (#37037)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b740af2 commit 71ecaf8
Copy full SHA for 71ecaf8

1 file changed

+228Lines changed: 228 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js‎

Copy file name to clipboardExpand all lines: packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
+228Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3932,4 +3932,232 @@ describe('ReactFlightAsyncDebugInfo', () => {
39323932
`);
39333933
}
39343934
});
3935+
3936+
it('does not lose I/O debug info when intermediate promises are garbage collected', async () => {
3937+
// Get a handle on the garbage collector without running the test with --expose-gc.
3938+
const v8 = require('v8');
3939+
const vm = require('vm');
3940+
v8.setFlagsFromString('--expose-gc');
3941+
const gc = vm.runInNewContext('gc');
3942+
v8.setFlagsFromString('--no-expose-gc');
3943+
3944+
// The global setImmediate is patched to setTimeout in this test which
3945+
// registers as new I/O in async_hooks. Use the real setImmediate for
3946+
// yielding to the event loop while waiting for GC so that it doesn't add
3947+
// I/O entries to the debug info of the component below.
3948+
const {setImmediate: realSetImmediate} = require('timers');
3949+
function tick() {
3950+
return new Promise(resolve => realSetImmediate(resolve));
3951+
}
3952+
3953+
let ioPromiseRef = null;
3954+
let collectedIntermediatePromises = false;
3955+
3956+
async function getData(text) {
3957+
const promise = delay(1);
3958+
ioPromiseRef = new WeakRef(promise);
3959+
await promise;
3960+
return text.toUpperCase();
3961+
}
3962+
3963+
async function waitForGarbageCollection(weakRef) {
3964+
// deref() keeps the target alive until the end of the current task, so
3965+
// check it on a later tick than the gc() call.
3966+
let collected = false;
3967+
for (let i = 0; !collected && i < 100; i++) {
3968+
gc();
3969+
await tick();
3970+
collected = weakRef.deref() === undefined;
3971+
await tick();
3972+
}
3973+
// The destroy() hooks of collected promises fire asynchronously. Yield
3974+
// a few more times to ensure they have all run.
3975+
for (let i = 0; i < 5; i++) {
3976+
gc();
3977+
await tick();
3978+
}
3979+
return collected;
3980+
}
3981+
3982+
async function Component() {
3983+
const result = await getData('hi');
3984+
// At this point the intermediate promises (the delay() promise and
3985+
// getData's own async function promise) are unreachable. The async
3986+
// debug info graph holds them only weakly through WeakRefs. Force them
3987+
// to be garbage collected, which fires their async_hooks destroy()
3988+
// hooks, before this component resolves, which is when React walks the
3989+
// async graph to emit the component's debug info.
3990+
collectedIntermediatePromises =
3991+
await waitForGarbageCollection(ioPromiseRef);
3992+
return result;
3993+
}
3994+
3995+
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
3996+
3997+
const readable = new Stream.PassThrough(streamOptions);
3998+
3999+
const result = ReactServerDOMClient.createFromNodeStream(readable, {
4000+
moduleMap: {},
4001+
moduleLoading: {},
4002+
});
4003+
stream.pipe(readable);
4004+
4005+
expect(await result).toBe('HI');
4006+
// If this fails, the GC helper above no longer actually collects the
4007+
// promises and this test is not testing anything.
4008+
expect(collectedIntermediatePromises).toBe(true);
4009+
4010+
await finishLoadingStream(readable);
4011+
if (
4012+
__DEV__ &&
4013+
gate(
4014+
flags =>
4015+
flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo,
4016+
)
4017+
) {
4018+
const debugInfo = getDebugInfo(result);
4019+
// The I/O entry for delay() must survive the garbage collection of the
4020+
// intermediate promises. The graph nodes are intentionally held
4021+
// strongly (only the promises themselves are held weakly through
4022+
// WeakRefs) so that the originating I/O is still reachable when the
4023+
// debug info is emitted after the promises are gone.
4024+
expect(debugInfo).toContainEqual(
4025+
expect.objectContaining({
4026+
awaited: expect.objectContaining({name: 'delay'}),
4027+
}),
4028+
);
4029+
expect(debugInfo).toMatchInlineSnapshot(`
4030+
[
4031+
{
4032+
"time": 0,
4033+
},
4034+
{
4035+
"env": "Server",
4036+
"key": null,
4037+
"name": "Component",
4038+
"props": {},
4039+
"stack": [
4040+
[
4041+
"Object.<anonymous>",
4042+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4043+
3995,
4044+
109,
4045+
3936,
4046+
87,
4047+
],
4048+
],
4049+
},
4050+
{
4051+
"time": 0,
4052+
},
4053+
{
4054+
"awaited": {
4055+
"end": 0,
4056+
"env": "Server",
4057+
"name": "delay",
4058+
"owner": {
4059+
"env": "Server",
4060+
"key": null,
4061+
"name": "Component",
4062+
"props": {},
4063+
"stack": [
4064+
[
4065+
"Object.<anonymous>",
4066+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4067+
3995,
4068+
109,
4069+
3936,
4070+
87,
4071+
],
4072+
],
4073+
},
4074+
"stack": [
4075+
[
4076+
"delay",
4077+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4078+
87,
4079+
12,
4080+
86,
4081+
3,
4082+
],
4083+
[
4084+
"getData",
4085+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4086+
3957,
4087+
21,
4088+
3956,
4089+
5,
4090+
],
4091+
[
4092+
"Component",
4093+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4094+
3983,
4095+
26,
4096+
3982,
4097+
5,
4098+
],
4099+
],
4100+
"start": 0,
4101+
"value": {
4102+
"value": undefined,
4103+
},
4104+
},
4105+
"env": "Server",
4106+
"owner": {
4107+
"env": "Server",
4108+
"key": null,
4109+
"name": "Component",
4110+
"props": {},
4111+
"stack": [
4112+
[
4113+
"Object.<anonymous>",
4114+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4115+
3995,
4116+
109,
4117+
3936,
4118+
87,
4119+
],
4120+
],
4121+
},
4122+
"stack": [
4123+
[
4124+
"getData",
4125+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4126+
3959,
4127+
7,
4128+
3956,
4129+
5,
4130+
],
4131+
[
4132+
"Component",
4133+
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
4134+
3983,
4135+
26,
4136+
3982,
4137+
5,
4138+
],
4139+
],
4140+
},
4141+
{
4142+
"time": 0,
4143+
},
4144+
{
4145+
"time": 0,
4146+
},
4147+
{
4148+
"awaited": {
4149+
"byteSize": 0,
4150+
"end": 0,
4151+
"name": "rsc stream",
4152+
"owner": null,
4153+
"start": 0,
4154+
"value": {
4155+
"value": "stream",
4156+
},
4157+
},
4158+
},
4159+
]
4160+
`);
4161+
}
4162+
});
39354163
});

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.