[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.
Sebastian Markbåge committed
Dec 15, 2023 at 21:38 UTC
8b8d265bd9a4cab7bbd04a9a13950fdc946ea51c
31 files changed
+158
-7
.eslintrc.js
+1
@@ -532,6 +532,7 @@ module.exports = {
532
trustedTypes: 'readonly',
533
IS_REACT_ACT_ENVIRONMENT: 'readonly',
534
AsyncLocalStorage: 'readonly',
535
+ async_hooks: 'readonly',
536
globalThis: 'readonly',
537
},
538
};
packages/react-server/src/ReactFlightServer.js
+3
@@ -70,6 +70,7 @@ import {
70
requestStorage,
71
prepareHostDispatcher,
72
createHints,
73
+ initAsyncDebugInfo,
74
} from './ReactFlightServerConfig';
75
76
import {
@@ -117,6 +118,8 @@ import binaryToComparableString from 'shared/binaryToComparableString';
118
119
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
120
121
+initAsyncDebugInfo();
122
+
123
const ObjectPrototype = Object.prototype;
124
125
type JSONValue =
packages/react-server/src/ReactFlightServerConfigDebugNode.js
new
+33
@@ -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
+}
packages/react-server/src/ReactFlightServerConfigDebugNoop.js
new
+11
@@ -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
@@ -11,6 +11,8 @@ import type {Request} from 'react-server/src/ReactFlightServer';
11
12
export * from '../ReactFlightServerConfigBundlerCustom';
13
14
+export * from '../ReactFlightServerConfigDebugNoop';
15
+
16
export type Hints = any;
17
export type HintCode = any;
18
// eslint-disable-next-line no-unused-vars
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js
+2
@@ -16,3 +16,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
16
export const supportsRequestStorage = true;
17
export const requestStorage: AsyncLocalStorage<Request> =
18
new AsyncLocalStorage();
19
+
20
+export * from '../ReactFlightServerConfigDebugNoop';
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js
+2
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14
15
export const supportsRequestStorage = false;
16
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17
+
18
+export * from '../ReactFlightServerConfigDebugNoop';
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js
+2
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14
15
export const supportsRequestStorage = false;
16
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17
+
18
+export * from '../ReactFlightServerConfigDebugNoop';
packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js
+2
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14
15
export const supportsRequestStorage = false;
16
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17
+
18
+export * from '../ReactFlightServerConfigDebugNoop';
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js
+15
@@ -16,3 +16,18 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
16
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
17
? new AsyncLocalStorage()
18
: (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
@@ -16,3 +16,18 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
16
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
17
? new AsyncLocalStorage()
18
: (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
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14
15
export const supportsRequestStorage = false;
16
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17
+
18
+export * from '../ReactFlightServerConfigDebugNoop';
packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
+2
@@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
14
15
export const supportsRequestStorage = false;
16
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
17
+
18
+export * from '../ReactFlightServerConfigDebugNoop';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js
+3
@@ -16,3 +16,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
16
export const supportsRequestStorage = true;
17
export const requestStorage: AsyncLocalStorage<Request> =
18
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
@@ -17,3 +17,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
17
export const supportsRequestStorage = true;
18
export const requestStorage: AsyncLocalStorage<Request> =
19
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
@@ -17,3 +17,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
17
export const supportsRequestStorage = true;
18
export const requestStorage: AsyncLocalStorage<Request> =
19
new AsyncLocalStorage();
20
+
21
+export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
22
+export * from '../ReactFlightServerConfigDebugNode';
packages/shared/ReactFeatureFlags.js
+2
@@ -229,6 +229,8 @@ export const enableProfilerNestedUpdatePhase = __PROFILE__;
229
// issues in DEV builds.
230
export const enableDebugTracing = false;
231
232
+export const enableAsyncDebugInfo = __EXPERIMENTAL__;
233
+
234
// Track which Fiber(s) schedule render work.
235
export const enableUpdaterTracking = __PROFILE__;
236
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -29,6 +29,7 @@ export const {
29
// The rest of the flags are static for better dead code elimination.
30
export const disableModulePatternComponents = true;
31
export const enableDebugTracing = false;
32
+export const enableAsyncDebugInfo = false;
33
export const enableSchedulingProfiler = __PROFILE__;
34
export const enableProfilerTimer = __PROFILE__;
35
export const enableProfilerCommitHooks = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.native-oss';
12
13
export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
+export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
18
export const enableProfilerTimer = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
12
13
export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
+export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18
export const enableProfilerTimer = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
12
13
export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
+export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18
export const enableProfilerTimer = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer.www';
12
13
export const debugRenderPhaseSideEffectsForStrictMode = false;
14
export const enableDebugTracing = false;
15
+export const enableAsyncDebugInfo = false;
16
export const enableSchedulingProfiler = false;
17
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18
export const enableProfilerTimer = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -116,5 +116,7 @@ export const forceConcurrentByDefaultForTesting = false;
116
export const useMicrotasksForSchedulingInFabric = false;
117
export const passChildrenWhenCloningPersistedNodes = false;
118
119
+export const enableAsyncDebugInfo = false;
120
+
121
// Flow magic to verify the exports of this file match the original version.
122
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
scripts/flow/environment.js
+35
-7
@@ -281,13 +281,7 @@ declare module 'pg/lib/utils' {
281
};
282
}
283
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
285
declare module 'async_hooks' {
286
declare class AsyncLocalStorage<T> {
287
disable(): void;
@@ -295,8 +289,42 @@ declare module 'async_hooks' {
289
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
290
enterWith(store: T): void;
291
}
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;
313
}
314
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
+
328
declare module 'node:worker_threads' {
329
declare class MessageChannel {
330
port1: MessagePort;
scripts/rollup/validate/eslintrc.cjs.js
+1
@@ -54,6 +54,7 @@ module.exports = {
54
55
// Temp
56
AsyncLocalStorage: 'readonly',
57
+ async_hooks: 'readonly',
58
59
// Flight Webpack
60
__webpack_chunk_load__: 'readonly',
scripts/rollup/validate/eslintrc.cjs2015.js
+1
@@ -52,6 +52,7 @@ module.exports = {
52
53
// Temp
54
AsyncLocalStorage: 'readonly',
55
+ async_hooks: 'readonly',
56
57
// Flight Webpack
58
__webpack_chunk_load__: 'readonly',
scripts/rollup/validate/eslintrc.esm.js
+1
@@ -54,6 +54,7 @@ module.exports = {
54
55
// Temp
56
AsyncLocalStorage: 'readonly',
57
+ async_hooks: 'readonly',
58
59
// Flight Webpack
60
__webpack_chunk_load__: 'readonly',
scripts/rollup/validate/eslintrc.fb.js
+1
@@ -55,6 +55,7 @@ module.exports = {
55
56
// Temp
57
AsyncLocalStorage: 'readonly',
58
+ async_hooks: 'readonly',
59
60
// jest
61
jest: 'readonly',
scripts/rollup/validate/eslintrc.rn.js
+1
@@ -53,6 +53,7 @@ module.exports = {
53
54
// Temp
55
AsyncLocalStorage: 'readonly',
56
+ async_hooks: 'readonly',
57
58
// jest
59
jest: 'readonly',
scripts/rollup/validate/eslintrc.umd.js
+1
@@ -59,6 +59,7 @@ module.exports = {
59
60
// Temp
61
AsyncLocalStorage: 'readonly',
62
+ async_hooks: 'readonly',
63
64
// Flight Webpack
65
__webpack_chunk_load__: 'readonly',
scripts/shared/inlinedHostConfigs.js
+7
@@ -45,6 +45,7 @@ module.exports = [
45
'react-devtools-shared',
46
'react-interactions',
47
'shared/ReactDOMSharedInternals',
48
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
49
],
50
isFlowTyped: true,
51
isServerSupported: true,
@@ -81,6 +82,7 @@ module.exports = [
82
'react-devtools-shared',
83
'react-interactions',
84
'shared/ReactDOMSharedInternals',
85
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
86
],
87
isFlowTyped: true,
88
isServerSupported: true,
@@ -117,6 +119,7 @@ module.exports = [
119
'react-devtools-shared',
120
'react-interactions',
121
'shared/ReactDOMSharedInternals',
122
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
123
],
124
isFlowTyped: true,
125
isServerSupported: true,
@@ -154,6 +157,7 @@ module.exports = [
157
'react-devtools-shared',
158
'react-interactions',
159
'shared/ReactDOMSharedInternals',
160
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
161
],
162
isFlowTyped: true,
163
isServerSupported: true,
@@ -297,6 +301,7 @@ module.exports = [
301
'react-devtools-shell',
302
'react-devtools-shared',
303
'shared/ReactDOMSharedInternals',
304
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
305
],
306
isFlowTyped: true,
307
isServerSupported: true,
@@ -330,6 +335,7 @@ module.exports = [
335
'react-devtools-shell',
336
'react-devtools-shared',
337
'shared/ReactDOMSharedInternals',
338
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
339
],
340
isFlowTyped: true,
341
isServerSupported: true,
@@ -364,6 +370,7 @@ module.exports = [
370
'react-devtools-shared',
371
'react-interactions',
372
'shared/ReactDOMSharedInternals',
373
+ 'react-server/src/ReactFlightServerConfigDebugNode.js',
374
],
375
isFlowTyped: true,
376
isServerSupported: true,