@@ -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