@samitouri / QOS-React-2 / commits / 8b8d265bd9

[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,