Skip to content

Commit

Permalink
[Flight] Wire up async_hooks in Node.js DEV for inspecting Promises (f…
Browse files Browse the repository at this point in the history
…acebook#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.
  • Loading branch information
sebmarkbage authored and AndyPengc12 committed Apr 15, 2024
1 parent 7f2e28a commit a52d192
Show file tree
Hide file tree
Showing 31 changed files with 158 additions and 7 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ module.exports = {
trustedTypes: 'readonly',
IS_REACT_ACT_ENVIRONMENT: 'readonly',
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',
globalThis: 'readonly',
},
};
3 changes: 3 additions & 0 deletions packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
requestStorage,
prepareHostDispatcher,
createHints,
initAsyncDebugInfo,
} from './ReactFlightServerConfig';

import {
Expand Down Expand Up @@ -117,6 +118,8 @@ import binaryToComparableString from 'shared/binaryToComparableString';

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

initAsyncDebugInfo();

const ObjectPrototype = Object.prototype;

type JSONValue =
Expand Down
33 changes: 33 additions & 0 deletions packages/react-server/src/ReactFlightServerConfigDebugNode.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
11 changes: 11 additions & 0 deletions packages/react-server/src/ReactFlightServerConfigDebugNoop.js
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 2 additions & 0 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__;

Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/forks/ReactFeatureFlags.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
42 changes: 35 additions & 7 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,22 +281,50 @@ 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;
getStore(): T | void;
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;
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {

// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',

// Flight Webpack
__webpack_chunk_load__: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.cjs2015.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ module.exports = {

// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',

// Flight Webpack
__webpack_chunk_load__: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {

// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',

// Flight Webpack
__webpack_chunk_load__: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ module.exports = {

// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',

// jest
jest: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.rn.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ module.exports = {

// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',

// jest
jest: 'readonly',
Expand Down
1 change: 1 addition & 0 deletions scripts/rollup/validate/eslintrc.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ module.exports = {

// Temp
AsyncLocalStorage: 'readonly',
async_hooks: 'readonly',

// Flight Webpack
__webpack_chunk_load__: 'readonly',
Expand Down
7 changes: 7 additions & 0 deletions scripts/shared/inlinedHostConfigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down Expand Up @@ -81,6 +82,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down Expand Up @@ -117,6 +119,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down Expand Up @@ -154,6 +157,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down Expand Up @@ -297,6 +301,7 @@ module.exports = [
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down Expand Up @@ -330,6 +335,7 @@ module.exports = [
'react-devtools-shell',
'react-devtools-shared',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down Expand Up @@ -364,6 +370,7 @@ module.exports = [
'react-devtools-shared',
'react-interactions',
'shared/ReactDOMSharedInternals',
'react-server/src/ReactFlightServerConfigDebugNode.js',
],
isFlowTyped: true,
isServerSupported: true,
Expand Down

0 comments on commit a52d192

Please sign in to comment.