Skip to content

Commit 8b8d265

Browse files
authored
[Flight] Wire up async_hooks in Node.js DEV for inspecting Promises (#27840)
This wires up the use of `async_hooks` in the Node build (as well as the Edge build when a global is available) in DEV mode only. This will be used to track debug info about what suspended during an RSC pass. Enabled behind a flag for now.
1 parent 63310df commit 8b8d265

31 files changed

+158
-7
lines changed

.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ module.exports = {
532532
trustedTypes: 'readonly',
533533
IS_REACT_ACT_ENVIRONMENT: 'readonly',
534534
AsyncLocalStorage: 'readonly',
535+
async_hooks: 'readonly',
535536
globalThis: 'readonly',
536537
},
537538
};

packages/react-server/src/ReactFlightServer.js

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
requestStorage,
7171
prepareHostDispatcher,
7272
createHints,
73+
initAsyncDebugInfo,
7374
} from './ReactFlightServerConfig';
7475

7576
import {
@@ -117,6 +118,8 @@ import binaryToComparableString from 'shared/binaryToComparableString';
117118

118119
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
119120

121+
initAsyncDebugInfo();
122+
120123
const ObjectPrototype = Object.prototype;
121124

122125
type JSONValue =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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
8+
*/
9+
10+
import {createAsyncHook, executionAsyncId} from './ReactFlightServerConfig';
11+
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
12+
13+
// Initialize the tracing of async operations.
14+
// We do this globally since the async work can potentially eagerly
15+
// start before the first request and once requests start they can interleave.
16+
// In theory we could enable and disable using a ref count of active requests
17+
// but given that typically this is just a live server, it doesn't really matter.
18+
export function initAsyncDebugInfo(): void {
19+
if (__DEV__ && enableAsyncDebugInfo) {
20+
createAsyncHook({
21+
init(asyncId: number, type: string, triggerAsyncId: number): void {
22+
// TODO
23+
},
24+
promiseResolve(asyncId: number): void {
25+
// TODO
26+
executionAsyncId();
27+
},
28+
destroy(asyncId: number): void {
29+
// TODO
30+
},
31+
}).enable();
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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
8+
*/
9+
10+
// Exported for runtimes that don't support Promise instrumentation for async debugging.
11+
export function initAsyncDebugInfo(): void {}

packages/react-server/src/forks/ReactFlightServerConfig.custom.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {Request} from 'react-server/src/ReactFlightServer';
1111

1212
export * from '../ReactFlightServerConfigBundlerCustom';
1313

14+
export * from '../ReactFlightServerConfigDebugNoop';
15+
1416
export type Hints = any;
1517
export type HintCode = any;
1618
// eslint-disable-next-line no-unused-vars

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js

+2
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1616
export const supportsRequestStorage = true;
1717
export const requestStorage: AsyncLocalStorage<Request> =
1818
new AsyncLocalStorage();
19+
20+
export * from '../ReactFlightServerConfigDebugNoop';

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1414

1515
export const supportsRequestStorage = false;
1616
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17+
18+
export * from '../ReactFlightServerConfigDebugNoop';

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1414

1515
export const supportsRequestStorage = false;
1616
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17+
18+
export * from '../ReactFlightServerConfigDebugNoop';

packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1414

1515
export const supportsRequestStorage = false;
1616
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17+
18+
export * from '../ReactFlightServerConfigDebugNoop';

packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js

+15
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,18 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
1616
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
1717
? new AsyncLocalStorage()
1818
: (null: any);
19+
20+
// We use the Node version but get access to async_hooks from a global.
21+
import type {HookCallbacks, AsyncHook} from 'async_hooks';
22+
export const createAsyncHook: HookCallbacks => AsyncHook =
23+
typeof async_hooks === 'object'
24+
? async_hooks.createHook
25+
: function () {
26+
return ({
27+
enable() {},
28+
disable() {},
29+
}: any);
30+
};
31+
export const executionAsyncId: () => number =
32+
typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
33+
export * from '../ReactFlightServerConfigDebugNode';

packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js

+15
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,18 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
1616
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
1717
? new AsyncLocalStorage()
1818
: (null: any);
19+
20+
// We use the Node version but get access to async_hooks from a global.
21+
import type {HookCallbacks, AsyncHook} from 'async_hooks';
22+
export const createAsyncHook: HookCallbacks => AsyncHook =
23+
typeof async_hooks === 'object'
24+
? async_hooks.createHook
25+
: function () {
26+
return ({
27+
enable() {},
28+
disable() {},
29+
}: any);
30+
};
31+
export const executionAsyncId: () => number =
32+
typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
33+
export * from '../ReactFlightServerConfigDebugNode';

packages/react-server/src/forks/ReactFlightServerConfig.dom-fb-experimental.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1414

1515
export const supportsRequestStorage = false;
1616
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17+
18+
export * from '../ReactFlightServerConfigDebugNoop';

packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1414

1515
export const supportsRequestStorage = false;
1616
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17+
18+
export * from '../ReactFlightServerConfigDebugNoop';

packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js

+3
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1616
export const supportsRequestStorage = true;
1717
export const requestStorage: AsyncLocalStorage<Request> =
1818
new AsyncLocalStorage();
19+
20+
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
21+
export * from '../ReactFlightServerConfigDebugNode';

packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1717
export const supportsRequestStorage = true;
1818
export const requestStorage: AsyncLocalStorage<Request> =
1919
new AsyncLocalStorage();
20+
21+
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
22+
export * from '../ReactFlightServerConfigDebugNode';

packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
1717
export const supportsRequestStorage = true;
1818
export const requestStorage: AsyncLocalStorage<Request> =
1919
new AsyncLocalStorage();
20+
21+
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
22+
export * from '../ReactFlightServerConfigDebugNode';

packages/shared/ReactFeatureFlags.js

+2
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ export const enableProfilerNestedUpdatePhase = __PROFILE__;
229229
// issues in DEV builds.
230230
export const enableDebugTracing = false;
231231

232+
export const enableAsyncDebugInfo = __EXPERIMENTAL__;
233+
232234
// Track which Fiber(s) schedule render work.
233235
export const enableUpdaterTracking = __PROFILE__;
234236

packages/shared/forks/ReactFeatureFlags.native-fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const {
2929
// The rest of the flags are static for better dead code elimination.
3030
export const disableModulePatternComponents = true;
3131
export const enableDebugTracing = false;
32+
export const enableAsyncDebugInfo = false;
3233
export const enableSchedulingProfiler = __PROFILE__;
3334
export const enableProfilerTimer = __PROFILE__;
3435
export const enableProfilerCommitHooks = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.native-oss.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.native-oss';
1212

1313
export const debugRenderPhaseSideEffectsForStrictMode = false;
1414
export const enableDebugTracing = false;
15+
export const enableAsyncDebugInfo = false;
1516
export const enableSchedulingProfiler = false;
1617
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
1718
export const enableProfilerTimer = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
1212

1313
export const debugRenderPhaseSideEffectsForStrictMode = false;
1414
export const enableDebugTracing = false;
15+
export const enableAsyncDebugInfo = false;
1516
export const enableSchedulingProfiler = false;
1617
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
1718
export const enableProfilerTimer = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.test-renderer.native.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
1212

1313
export const debugRenderPhaseSideEffectsForStrictMode = false;
1414
export const enableDebugTracing = false;
15+
export const enableAsyncDebugInfo = false;
1516
export const enableSchedulingProfiler = false;
1617
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
1718
export const enableProfilerTimer = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer.www';
1212

1313
export const debugRenderPhaseSideEffectsForStrictMode = false;
1414
export const enableDebugTracing = false;
15+
export const enableAsyncDebugInfo = false;
1516
export const enableSchedulingProfiler = false;
1617
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
1718
export const enableProfilerTimer = __PROFILE__;

packages/shared/forks/ReactFeatureFlags.www.js

+2
Original file line numberDiff line numberDiff line change
@@ -116,5 +116,7 @@ export const forceConcurrentByDefaultForTesting = false;
116116
export const useMicrotasksForSchedulingInFabric = false;
117117
export const passChildrenWhenCloningPersistedNodes = false;
118118

119+
export const enableAsyncDebugInfo = false;
120+
119121
// Flow magic to verify the exports of this file match the original version.
120122
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

scripts/flow/environment.js

+35-7
Original file line numberDiff line numberDiff line change
@@ -281,22 +281,50 @@ declare module 'pg/lib/utils' {
281281
};
282282
}
283283

284-
declare class AsyncLocalStorage<T> {
285-
disable(): void;
286-
getStore(): T | void;
287-
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
288-
enterWith(store: T): void;
289-
}
290-
284+
// Node
291285
declare module 'async_hooks' {
292286
declare class AsyncLocalStorage<T> {
293287
disable(): void;
294288
getStore(): T | void;
295289
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
296290
enterWith(store: T): void;
297291
}
292+
declare interface AsyncResource {}
293+
declare function executionAsyncId(): number;
294+
declare function executionAsyncResource(): AsyncResource;
295+
declare function triggerAsyncId(): number;
296+
declare type HookCallbacks = {
297+
init?: (
298+
asyncId: number,
299+
type: string,
300+
triggerAsyncId: number,
301+
resource: AsyncResource,
302+
) => void,
303+
before?: (asyncId: number) => void,
304+
after?: (asyncId: number) => void,
305+
promiseResolve?: (asyncId: number) => void,
306+
destroy?: (asyncId: number) => void,
307+
};
308+
declare class AsyncHook {
309+
enable(): this;
310+
disable(): this;
311+
}
312+
declare function createHook(callbacks: HookCallbacks): AsyncHook;
298313
}
299314

315+
// Edge
316+
declare class AsyncLocalStorage<T> {
317+
disable(): void;
318+
getStore(): T | void;
319+
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
320+
enterWith(store: T): void;
321+
}
322+
323+
declare var async_hooks: {
324+
createHook(callbacks: any): any,
325+
executionAsyncId(): number,
326+
};
327+
300328
declare module 'node:worker_threads' {
301329
declare class MessageChannel {
302330
port1: MessagePort;

scripts/rollup/validate/eslintrc.cjs.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454

5555
// Temp
5656
AsyncLocalStorage: 'readonly',
57+
async_hooks: 'readonly',
5758

5859
// Flight Webpack
5960
__webpack_chunk_load__: 'readonly',

scripts/rollup/validate/eslintrc.cjs2015.js

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module.exports = {
5252

5353
// Temp
5454
AsyncLocalStorage: 'readonly',
55+
async_hooks: 'readonly',
5556

5657
// Flight Webpack
5758
__webpack_chunk_load__: 'readonly',

scripts/rollup/validate/eslintrc.esm.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454

5555
// Temp
5656
AsyncLocalStorage: 'readonly',
57+
async_hooks: 'readonly',
5758

5859
// Flight Webpack
5960
__webpack_chunk_load__: 'readonly',

scripts/rollup/validate/eslintrc.fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ module.exports = {
5555

5656
// Temp
5757
AsyncLocalStorage: 'readonly',
58+
async_hooks: 'readonly',
5859

5960
// jest
6061
jest: 'readonly',

scripts/rollup/validate/eslintrc.rn.js

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ module.exports = {
5353

5454
// Temp
5555
AsyncLocalStorage: 'readonly',
56+
async_hooks: 'readonly',
5657

5758
// jest
5859
jest: 'readonly',

scripts/rollup/validate/eslintrc.umd.js

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ module.exports = {
5959

6060
// Temp
6161
AsyncLocalStorage: 'readonly',
62+
async_hooks: 'readonly',
6263

6364
// Flight Webpack
6465
__webpack_chunk_load__: 'readonly',

scripts/shared/inlinedHostConfigs.js

+7
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ module.exports = [
4545
'react-devtools-shared',
4646
'react-interactions',
4747
'shared/ReactDOMSharedInternals',
48+
'react-server/src/ReactFlightServerConfigDebugNode.js',
4849
],
4950
isFlowTyped: true,
5051
isServerSupported: true,
@@ -81,6 +82,7 @@ module.exports = [
8182
'react-devtools-shared',
8283
'react-interactions',
8384
'shared/ReactDOMSharedInternals',
85+
'react-server/src/ReactFlightServerConfigDebugNode.js',
8486
],
8587
isFlowTyped: true,
8688
isServerSupported: true,
@@ -117,6 +119,7 @@ module.exports = [
117119
'react-devtools-shared',
118120
'react-interactions',
119121
'shared/ReactDOMSharedInternals',
122+
'react-server/src/ReactFlightServerConfigDebugNode.js',
120123
],
121124
isFlowTyped: true,
122125
isServerSupported: true,
@@ -154,6 +157,7 @@ module.exports = [
154157
'react-devtools-shared',
155158
'react-interactions',
156159
'shared/ReactDOMSharedInternals',
160+
'react-server/src/ReactFlightServerConfigDebugNode.js',
157161
],
158162
isFlowTyped: true,
159163
isServerSupported: true,
@@ -297,6 +301,7 @@ module.exports = [
297301
'react-devtools-shell',
298302
'react-devtools-shared',
299303
'shared/ReactDOMSharedInternals',
304+
'react-server/src/ReactFlightServerConfigDebugNode.js',
300305
],
301306
isFlowTyped: true,
302307
isServerSupported: true,
@@ -330,6 +335,7 @@ module.exports = [
330335
'react-devtools-shell',
331336
'react-devtools-shared',
332337
'shared/ReactDOMSharedInternals',
338+
'react-server/src/ReactFlightServerConfigDebugNode.js',
333339
],
334340
isFlowTyped: true,
335341
isServerSupported: true,
@@ -364,6 +370,7 @@ module.exports = [
364370
'react-devtools-shared',
365371
'react-interactions',
366372
'shared/ReactDOMSharedInternals',
373+
'react-server/src/ReactFlightServerConfigDebugNode.js',
367374
],
368375
isFlowTyped: true,
369376
isServerSupported: true,

0 commit comments

Comments
 (0)