mirror of
https://github.com/facebook/react.git
synced 2026-02-26 07:55:55 +00:00
Stacked on #33400. <img width="1261" alt="Screenshot 2025-06-01 at 10 27 47 PM" src="https://github.com/user-attachments/assets/a5a73ee2-49e0-4851-84ac-e0df6032efb5" /> This is emitted with the start/end time and stack of the "await". Which may be different than the thing that started the I/O. These awaits aren't quite as simple as just every await since you can start a sequence in parallel there can actually be multiple overlapping awaits and there can be CPU work interleaved with the await on the same component. ```js function getData() { await fetch(...); await fetch(...); } const promise = getData(); doWork(); await promise; ``` This has two "I/O" awaits but those are actually happening in parallel with `doWork()`. Since these also could have started before we started rendering this sequence (e.g. a component) we have to clamp it so that we don't consider awaits that start before the component. What we're conceptually trying to convey is the time this component was blocked due to that I/O resource. Whether it's blocked from completing the last result or if it's blocked from issuing a waterfall request.
154 lines
5.9 KiB
JavaScript
154 lines
5.9 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 {resolveOwner} from './flight/ReactFlightCurrentOwner';
|
|
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,
|
|
owner: resolveOwner(),
|
|
stack: new Error(),
|
|
start: performance.now(),
|
|
end: -1.1, // set when resolved.
|
|
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,
|
|
owner: resolveOwner(),
|
|
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,
|
|
owner: resolveOwner(),
|
|
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,
|
|
owner: resolveOwner(),
|
|
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.',
|
|
);
|
|
}
|
|
// 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;
|
|
}
|