This lets us track what data each Server Component depended on. This will be used by Performance Track and React DevTools. We use Node.js `async_hooks`. This has a number of downside. It is Node.js specific so this feature is not available in other runtimes until something equivalent becomes available. It's [discouraged by Node.js docs](https://nodejs.org/api/async_hooks.html#async-hooks). It's also slow which makes this approach only really viable in development mode. At least with stack traces. However, it's really the only solution that gives us the data that we need. The [Diagnostic Channel](https://nodejs.org/api/diagnostics_channel.html) API is not sufficient. Not only is many Node.js built-in APIs missing but all libraries like databases are also missing. Were as `async_hooks` covers pretty much anything async in the Node.js ecosystem. However, even if coverage was wider it's not actually showing the information we want. It's not enough to show the low level I/O that is happening because that doesn't provide the context. We need the stack trace in user space code where it was initiated and where it was awaited. It's also not each low level socket operation that we want to surface but some higher level concept which can span a sequence of I/O operations but as far as user space is concerned. Therefore this solution is anchored on stack traces and ignore listing to determine what the interesting span is. It is somewhat Promise-centric (and in particular async/await) because it allows us to model an abstract span instead of just random I/O. Async/await points are also especially useful because this allows Async Stacks to show the full sequence which is not supported by random callbacks. However, if no Promises are involved we still to our best to show the stack causing plain I/O callbacks. Additionally, we don't want to track all possible I/O. For example, side-effects like logging that doesn't affect the rendering performance doesn't need to be included. We only want to include things that actually block the rendering output. We also need to track which data blocks each component so that we can track which data caused a particular subtree to suspend. We can do this using `async_hooks` because we can track the graph of what resolved what and then spawned what. To track what suspended what, something has to resolve. Therefore it needs to run to completion before we can show what it was suspended on. So something that never resolves, won't be tracked for example. We use the `async_hooks` in `ReactFlightServerConfigDebugNode` to build up an `ReactFlightAsyncSequence` graph that collects the stack traces for basically all I/O and Promises allocated in the whole app. This is pretty heavy, especially the stack traces, but it's because we don't know which ones we'll need until they resolve. We don't materialize the stacks until we need them though. Once they end up pinging the Flight runtime, we collect which current executing task that pinged the runtime and then log the sequence that led up until that runtime into the RSC protocol. Currently we only include things that weren't already resolved before we started rendering this task/component, so that we don't log the entire history each time. Each operation is split into two parts. First a `ReactIOInfo` which represents an I/O operation and its start/end time. Basically the start point where it was start. This is basically represents where you called `new Promise()` or when entering an `async function` which has an implied Promise. It can be started in a different component than where it's awaited and it can be awaited in multiple places. Therefore this is global information and not associated with a specific Component. The second part is `ReactAsyncInfo`. This represents where this I/O was `await`:ed or `.then()` called. This is associated with a point in the tree (usually the Promise that's a direct child of a Component). Since you can have multiple different I/O awaited in a sequence technically it forms a dependency graph but to simplify the model these awaits as flattened into the `ReactDebugInfo` list. Basically it contains each await in a sequence that affected this part from unblocking. This means that the same `ReactAsyncInfo` can appear in mutliple components if they all await the same `ReactIOInfo` but the same Promise only appears once. Promises that are only resolved by other Promises or immediately are not considered here. Only if they're resolved by an I/O operation. We pick the Promise basically on the border between user space code and ignored listed code (`node_modules`) to pick the most specific span but abstract enough to not give too much detail irrelevant to the current audience. Similarly, the deepest `await` in user space is marked as the relevant `await` point. This feature is only available in the `node` builds of React. Not if you use the `edge` builds inside of Node.js. --------- Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
151 lines
5.7 KiB
JavaScript
151 lines
5.7 KiB
JavaScript
/**
|
|
* 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 type {
|
|
AsyncSequence,
|
|
IONode,
|
|
PromiseNode,
|
|
AwaitNode,
|
|
} from './ReactFlightAsyncSequence';
|
|
|
|
import {IO_NODE, PROMISE_NODE, AWAIT_NODE} from './ReactFlightAsyncSequence';
|
|
import {createHook, executionAsyncId} from 'async_hooks';
|
|
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
|
|
|
|
const pendingOperations: Map<number, AsyncSequence> =
|
|
__DEV__ && enableAsyncDebugInfo ? new Map() : (null: any);
|
|
|
|
// 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) {
|
|
createHook({
|
|
init(asyncId: number, type: string, triggerAsyncId: number): void {
|
|
const trigger = pendingOperations.get(triggerAsyncId);
|
|
let node: AsyncSequence;
|
|
if (type === 'PROMISE') {
|
|
const currentAsyncId = executionAsyncId();
|
|
if (currentAsyncId !== triggerAsyncId) {
|
|
// When you call .then() on a native Promise, or await/Promise.all() a thenable,
|
|
// then this intermediate Promise is created. We use this as our await point
|
|
if (trigger === undefined) {
|
|
// We don't track awaits on things that started outside our tracked scope.
|
|
return;
|
|
}
|
|
const current = pendingOperations.get(currentAsyncId);
|
|
// If the thing we're waiting on is another Await we still track that sequence
|
|
// so that we can later pick the best stack trace in user space.
|
|
node = ({
|
|
tag: AWAIT_NODE,
|
|
stack: new Error(),
|
|
start: -1.1,
|
|
end: -1.1,
|
|
awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
|
|
previous: current === undefined ? null : current, // The path that led us here.
|
|
}: AwaitNode);
|
|
} else {
|
|
node = ({
|
|
tag: PROMISE_NODE,
|
|
stack: new Error(),
|
|
start: performance.now(),
|
|
end: -1.1, // Set when we resolve.
|
|
awaited:
|
|
trigger === undefined
|
|
? null // It might get overridden when we resolve.
|
|
: trigger,
|
|
previous: null,
|
|
}: PromiseNode);
|
|
}
|
|
} else if (
|
|
type !== 'Microtask' &&
|
|
type !== 'TickObject' &&
|
|
type !== 'Immediate'
|
|
) {
|
|
if (trigger === undefined) {
|
|
// We have begun a new I/O sequence.
|
|
node = ({
|
|
tag: IO_NODE,
|
|
stack: new Error(), // This is only used if no native promises are used.
|
|
start: performance.now(),
|
|
end: -1.1, // Only set when pinged.
|
|
awaited: null,
|
|
previous: null,
|
|
}: IONode);
|
|
} else if (trigger.tag === AWAIT_NODE) {
|
|
// We have begun a new I/O sequence after the await.
|
|
node = ({
|
|
tag: IO_NODE,
|
|
stack: new Error(),
|
|
start: performance.now(),
|
|
end: -1.1, // Only set when pinged.
|
|
awaited: null,
|
|
previous: trigger,
|
|
}: IONode);
|
|
} else {
|
|
// Otherwise, this is just a continuation of the same I/O sequence.
|
|
node = trigger;
|
|
}
|
|
} else {
|
|
// Ignore nextTick and microtasks as they're not considered I/O operations.
|
|
// we just treat the trigger as the node to carry along the sequence.
|
|
if (trigger === undefined) {
|
|
return;
|
|
}
|
|
node = trigger;
|
|
}
|
|
pendingOperations.set(asyncId, node);
|
|
},
|
|
promiseResolve(asyncId: number): void {
|
|
const resolvedNode = pendingOperations.get(asyncId);
|
|
if (resolvedNode !== undefined) {
|
|
if (resolvedNode.tag === IO_NODE) {
|
|
// eslint-disable-next-line react-internal/prod-error-codes
|
|
throw new Error(
|
|
'A Promise should never be an IO_NODE. This is a bug in React.',
|
|
);
|
|
}
|
|
if (resolvedNode.tag === PROMISE_NODE) {
|
|
// Log the end time when we resolved the promise.
|
|
resolvedNode.end = performance.now();
|
|
}
|
|
const currentAsyncId = executionAsyncId();
|
|
if (asyncId !== currentAsyncId) {
|
|
// If the promise was not resolved by itself, then that means that
|
|
// the trigger that we originally stored wasn't actually the dependency.
|
|
// Instead, the current execution context is what ultimately unblocked it.
|
|
const awaited = pendingOperations.get(currentAsyncId);
|
|
resolvedNode.awaited = awaited === undefined ? null : awaited;
|
|
}
|
|
}
|
|
},
|
|
|
|
destroy(asyncId: number): void {
|
|
// If we needed the meta data from this operation we should have already
|
|
// extracted it or it should be part of a chain of triggers.
|
|
pendingOperations.delete(asyncId);
|
|
},
|
|
}).enable();
|
|
}
|
|
}
|
|
|
|
export function getCurrentAsyncSequence(): null | AsyncSequence {
|
|
if (!__DEV__ || !enableAsyncDebugInfo) {
|
|
return null;
|
|
}
|
|
const currentNode = pendingOperations.get(executionAsyncId());
|
|
if (currentNode === undefined) {
|
|
// Nothing that we tracked led to the resolution of this execution context.
|
|
return null;
|
|
}
|
|
return currentNode;
|
|
}
|