[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.
This commit is contained in:
Sebastian Markbåge 2023-12-15 21:38:01 -05:00 committed by GitHub
parent 63310df2b2
commit 8b8d265bd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 158 additions and 7 deletions

View File

@ -532,6 +532,7 @@ module.exports = {
trustedTypes: 'readonly',
IS_REACT_ACT_ENVIRONMENT: 'readonly',
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
},
};

View File

@ -70,6 +70,7 @@ import {
requestStorage,
prepareHostDispatcher,
createHints,
initAsyncDebugInfo,
} from './ReactFlightServerConfig';
import {
@ -117,6 +118,8 @@ import binaryToComparableString from 'shared/binaryToComparableString';
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
initAsyncDebugInfo();
const ObjectPrototype = Object.prototype;
type JSONValue =

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {createAsyncHook, executionAsyncId} from './ReactFlightServerConfig';
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
// Initialize the tracing of async operations.
// We do this globally since the async work can potentially eagerly
// start before the first request and once requests start they can interleave.
// In theory we could enable and disable using a ref count of active requests
// but given that typically this is just a live server, it doesn't really matter.
export function initAsyncDebugInfo(): void {
if (__DEV__ && enableAsyncDebugInfo) {
createAsyncHook({
init(asyncId: number, type: string, triggerAsyncId: number): void {
// TODO
},
promiseResolve(asyncId: number): void {
// TODO
executionAsyncId();
},
destroy(asyncId: number): void {
// TODO
},
}).enable();
}
}

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// Exported for runtimes that don't support Promise instrumentation for async debugging.
export function initAsyncDebugInfo(): void {}

View File

@ -11,6 +11,8 @@ import type {Request} from 'react-server/src/ReactFlightServer';
export * from '../ReactFlightServerConfigBundlerCustom';
export * from '../ReactFlightServerConfigDebugNoop';
export type Hints = any;
export type HintCode = any;
// eslint-disable-next-line no-unused-vars

View File

@ -16,3 +16,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage<Request> =
new AsyncLocalStorage();
export * from '../ReactFlightServerConfigDebugNoop';

View File

@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
export * from '../ReactFlightServerConfigDebugNoop';

View File

@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
export * from '../ReactFlightServerConfigDebugNoop';

View File

@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
export * from '../ReactFlightServerConfigDebugNoop';

View File

@ -16,3 +16,18 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
? new AsyncLocalStorage()
: (null: any);
// We use the Node version but get access to async_hooks from a global.
import type {HookCallbacks, AsyncHook} from 'async_hooks';
export const createAsyncHook: HookCallbacks => AsyncHook =
typeof async_hooks === 'object'
? async_hooks.createHook
: function () {
return ({
enable() {},
disable() {},
}: any);
};
export const executionAsyncId: () => number =
typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
export * from '../ReactFlightServerConfigDebugNode';

View File

@ -16,3 +16,18 @@ export const supportsRequestStorage = typeof AsyncLocalStorage === 'function';
export const requestStorage: AsyncLocalStorage<Request> = supportsRequestStorage
? new AsyncLocalStorage()
: (null: any);
// We use the Node version but get access to async_hooks from a global.
import type {HookCallbacks, AsyncHook} from 'async_hooks';
export const createAsyncHook: HookCallbacks => AsyncHook =
typeof async_hooks === 'object'
? async_hooks.createHook
: function () {
return ({
enable() {},
disable() {},
}: any);
};
export const executionAsyncId: () => number =
typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
export * from '../ReactFlightServerConfigDebugNode';

View File

@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
export * from '../ReactFlightServerConfigDebugNoop';

View File

@ -14,3 +14,5 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
export * from '../ReactFlightServerConfigDebugNoop';

View File

@ -16,3 +16,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage<Request> =
new AsyncLocalStorage();
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
export * from '../ReactFlightServerConfigDebugNode';

View File

@ -17,3 +17,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage<Request> =
new AsyncLocalStorage();
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
export * from '../ReactFlightServerConfigDebugNode';

View File

@ -17,3 +17,6 @@ export * from 'react-dom-bindings/src/server/ReactFlightServerConfigDOM';
export const supportsRequestStorage = true;
export const requestStorage: AsyncLocalStorage<Request> =
new AsyncLocalStorage();
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
export * from '../ReactFlightServerConfigDebugNode';

View File

@ -229,6 +229,8 @@ export const enableProfilerNestedUpdatePhase = __PROFILE__;
// issues in DEV builds.
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = __EXPERIMENTAL__;
// Track which Fiber(s) schedule render work.
export const enableUpdaterTracking = __PROFILE__;

View File

@ -29,6 +29,7 @@ export const {
// The rest of the flags are static for better dead code elimination.
export const disableModulePatternComponents = true;
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = false;
export const enableSchedulingProfiler = __PROFILE__;
export const enableProfilerTimer = __PROFILE__;
export const enableProfilerCommitHooks = __PROFILE__;

View File

@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.native-oss';
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = false;
export const enableSchedulingProfiler = false;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
export const enableProfilerTimer = __PROFILE__;

View File

@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = false;
export const enableSchedulingProfiler = false;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;

View File

@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = false;
export const enableSchedulingProfiler = false;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;

View File

@ -12,6 +12,7 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer.www';
export const debugRenderPhaseSideEffectsForStrictMode = false;
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = false;
export const enableSchedulingProfiler = false;
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
export const enableProfilerTimer = __PROFILE__;

View File

@ -116,5 +116,7 @@ export const forceConcurrentByDefaultForTesting = false;
export const useMicrotasksForSchedulingInFabric = false;
export const passChildrenWhenCloningPersistedNodes = false;
export const enableAsyncDebugInfo = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

View File

@ -281,13 +281,7 @@ declare module 'pg/lib/utils' {
};
}
declare class AsyncLocalStorage<T> {
disable(): void;
getStore(): T | void;
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
enterWith(store: T): void;
}
// Node
declare module 'async_hooks' {
declare class AsyncLocalStorage<T> {
disable(): void;
@ -295,8 +289,42 @@ declare module 'async_hooks' {
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
enterWith(store: T): void;
}
declare interface AsyncResource {}
declare function executionAsyncId(): number;
declare function executionAsyncResource(): AsyncResource;
declare function triggerAsyncId(): number;
declare type HookCallbacks = {
init?: (
asyncId: number,
type: string,
triggerAsyncId: number,
resource: AsyncResource,
) => void,
before?: (asyncId: number) => void,
after?: (asyncId: number) => void,
promiseResolve?: (asyncId: number) => void,
destroy?: (asyncId: number) => void,
};
declare class AsyncHook {
enable(): this;
disable(): this;
}
declare function createHook(callbacks: HookCallbacks): AsyncHook;
}
// Edge
declare class AsyncLocalStorage<T> {
disable(): void;
getStore(): T | void;
run(store: T, callback: (...args: any[]) => void, ...args: any[]): void;
enterWith(store: T): void;
}
declare var async_hooks: {
createHook(callbacks: any): any,
executionAsyncId(): number,
};
declare module 'node:worker_threads' {
declare class MessageChannel {
port1: MessagePort;

View File

@ -54,6 +54,7 @@ module.exports = {
// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
// Flight Webpack
__webpack_chunk_load__: 'readonly',

View File

@ -52,6 +52,7 @@ module.exports = {
// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
// Flight Webpack
__webpack_chunk_load__: 'readonly',

View File

@ -54,6 +54,7 @@ module.exports = {
// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
// Flight Webpack
__webpack_chunk_load__: 'readonly',

View File

@ -55,6 +55,7 @@ module.exports = {
// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
// jest
jest: 'readonly',

View File

@ -53,6 +53,7 @@ module.exports = {
// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
// jest
jest: 'readonly',

View File

@ -59,6 +59,7 @@ module.exports = {
// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
// Flight Webpack
__webpack_chunk_load__: 'readonly',

View File

@ -45,6 +45,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
@ -81,6 +82,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
@ -117,6 +119,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
@ -154,6 +157,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
@ -297,6 +301,7 @@ module.exports = [
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
@ -330,6 +335,7 @@ module.exports = [
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
@ -364,6 +370,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,