[Flight] Track Owner on AsyncInfo and IOInfo (#33395)

Stacked on #33394.

This lets us create async stack traces to the owner that was in context
when the I/O was started or awaited.

<img width="615" alt="Screenshot 2025-06-01 at 12 31 52 AM"
src="https://github.com/user-attachments/assets/6ff5a146-33d6-4a4b-84af-1b57e73047d4"
/>

This owner might not be the immediate closest parent where the I/O was
awaited.
This commit is contained in:
Sebastian Markbåge
2025-06-03 16:12:26 -04:00
committed by GitHub
parent d8919a0a68
commit 45da4e055d
6 changed files with 154 additions and 33 deletions
+5 -5
View File
@@ -2616,14 +2616,15 @@ function resolveDebugInfo(
initializeFakeTask(response, componentInfoOrAsyncInfo, env);
}
if (debugInfo.owner === null && response._debugRootOwner != null) {
// $FlowFixMe[prop-missing] By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
const componentInfo: ReactComponentInfo = debugInfo;
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
// $FlowFixMe: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
debugInfo;
// $FlowFixMe[cannot-write]
componentInfo.owner = response._debugRootOwner;
componentInfoOrAsyncInfo.owner = response._debugRootOwner;
// We override the stack if we override the owner since the stack where the root JSX
// was created on the server isn't very useful but where the request was made is.
// $FlowFixMe[cannot-write]
componentInfo.debugStack = response._debugRootStack;
componentInfoOrAsyncInfo.debugStack = response._debugRootStack;
} else if (debugInfo.stack !== undefined) {
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
// $FlowFixMe[incompatible-type]
@@ -2764,7 +2765,6 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
initializeFakeTask(response, ioInfo, env);
initializeFakeStack(response, ioInfo);
}
// TODO: Initialize owner.
// Adjust the time to the current environment's time space.
// $FlowFixMe[cannot-write]
ioInfo.start += response._timeOrigin;
+5
View File
@@ -7,12 +7,15 @@
* @flow
*/
import type {ReactComponentInfo} from 'shared/ReactTypes';
export const IO_NODE = 0;
export const PROMISE_NODE = 1;
export const AWAIT_NODE = 2;
export type IONode = {
tag: 0,
owner: null | ReactComponentInfo,
stack: Error, // callsite that spawned the I/O
start: number, // start time when the first part of the I/O sequence started
end: number, // we typically don't use this. only when there's no promise intermediate.
@@ -22,6 +25,7 @@ export type IONode = {
export type PromiseNode = {
tag: 1,
owner: null | ReactComponentInfo,
stack: Error, // callsite that created the Promise
start: number, // start time when the Promise was created
end: number, // end time when the Promise was resolved.
@@ -31,6 +35,7 @@ export type PromiseNode = {
export type AwaitNode = {
tag: 2,
owner: null | ReactComponentInfo,
stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
start: -1.1, // not used. We use the timing of the awaited promise.
end: -1.1, // not used.
+23 -2
View File
@@ -1934,6 +1934,7 @@ function visitAsyncNode(
request.pendingChunks++;
emitDebugChunk(request, task.id, {
awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
owner: node.owner,
stack: stack,
});
}
@@ -3523,6 +3524,7 @@ function emitIOInfoChunk(
name: string,
start: number,
end: number,
owner: ?ReactComponentInfo,
stack: ?ReactStackTrace,
): void {
if (!__DEV__) {
@@ -3560,8 +3562,15 @@ function emitIOInfoChunk(
name: name,
start: relativeStartTimestamp,
end: relativeEndTimestamp,
stack: stack,
};
if (stack != null) {
// $FlowFixMe[cannot-write]
debugIOInfo.stack = stack;
}
if (owner != null) {
// $FlowFixMe[cannot-write]
debugIOInfo.owner = owner;
}
// $FlowFixMe[incompatible-type] stringify can return null
const json: string = stringify(debugIOInfo, replacer);
const row = id.toString(16) + ':J' + json + '\n';
@@ -3577,12 +3586,18 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
request.pendingChunks++;
const id = request.nextChunkId++;
const owner = ioInfo.owner;
// Ensure the owner is already outlined.
if (owner != null) {
outlineComponentInfo(request, owner);
}
emitIOInfoChunk(
request,
id,
ioInfo.name,
ioInfo.start,
ioInfo.end,
owner,
ioInfo.stack,
);
request.writtenObjects.set(ioInfo, serializeByValueID(id));
@@ -3612,10 +3627,15 @@ function serializeIONode(
name = name.slice(7);
}
}
const owner = ioNode.owner;
// Ensure the owner is already outlined.
if (owner != null) {
outlineComponentInfo(request, owner);
}
request.pendingChunks++;
const id = request.nextChunkId++;
emitIOInfoChunk(request, id, name, ioNode.start, ioNode.end, stack);
emitIOInfoChunk(request, id, name, ioNode.start, ioNode.end, owner, stack);
const ref = serializeByValueID(id);
request.writtenObjects.set(ioNode, ref);
return ref;
@@ -4141,6 +4161,7 @@ function forwardDebugInfo(
const debugAsyncInfo: Omit<ReactAsyncInfo, 'debugTask' | 'debugStack'> =
{
awaited: ioInfo,
owner: debugInfo[i].owner,
stack: debugInfo[i].stack,
};
emitDebugChunk(request, id, debugAsyncInfo);
@@ -15,6 +15,7 @@ import type {
} 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';
@@ -46,6 +47,7 @@ export function initAsyncDebugInfo(): void {
// so that we can later pick the best stack trace in user space.
node = ({
tag: AWAIT_NODE,
owner: resolveOwner(),
stack: new Error(),
start: -1.1,
end: -1.1,
@@ -55,6 +57,7 @@ export function initAsyncDebugInfo(): void {
} else {
node = ({
tag: PROMISE_NODE,
owner: resolveOwner(),
stack: new Error(),
start: performance.now(),
end: -1.1, // Set when we resolve.
@@ -74,6 +77,7 @@ export function initAsyncDebugInfo(): void {
// 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.
@@ -84,6 +88,7 @@ export function initAsyncDebugInfo(): void {
// 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.
@@ -39,6 +39,9 @@ function normalizeIOInfo(ioInfo) {
if (ioInfo.stack) {
copy.stack = normalizeStack(ioInfo.stack);
}
if (ioInfo.owner) {
copy.owner = normalizeDebugInfo(ioInfo.owner);
}
if (typeof ioInfo.start === 'number') {
copy.start = 0;
}
@@ -160,9 +163,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
133,
109,
117,
120,
50,
],
],
@@ -171,49 +174,83 @@ describe('ReactFlightAsyncDebugInfo', () => {
"awaited": {
"end": 0,
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"owner": null,
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
133,
109,
120,
50,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
112,
115,
12,
111,
114,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
119,
122,
13,
118,
121,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
126,
129,
26,
125,
128,
5,
],
],
"start": 0,
},
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"owner": null,
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
133,
109,
120,
50,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
119,
122,
13,
118,
121,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
126,
129,
26,
125,
128,
5,
],
],
@@ -222,49 +259,83 @@ describe('ReactFlightAsyncDebugInfo', () => {
"awaited": {
"end": 0,
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"owner": null,
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
133,
109,
120,
50,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
112,
115,
12,
111,
114,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
120,
123,
21,
118,
121,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
126,
129,
20,
125,
128,
5,
],
],
"start": 0,
},
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"owner": null,
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
133,
109,
120,
50,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
121,
124,
21,
118,
121,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
126,
129,
20,
125,
128,
5,
],
],
@@ -323,9 +394,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
293,
364,
109,
280,
351,
67,
],
],
@@ -334,13 +405,30 @@ describe('ReactFlightAsyncDebugInfo', () => {
"awaited": {
"end": 0,
"name": "setTimeout",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"owner": null,
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
364,
109,
351,
67,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
283,
354,
7,
281,
352,
5,
],
],
+2
View File
@@ -234,6 +234,7 @@ export type ReactIOInfo = {
+name: string, // the name of the async function being called (e.g. "fetch")
+start: number, // the start time
+end: number, // the end time (this might be different from the time the await was unblocked)
+owner?: null | ReactComponentInfo,
+stack?: null | ReactStackTrace,
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
+debugStack?: null | Error,
@@ -242,6 +243,7 @@ export type ReactIOInfo = {
export type ReactAsyncInfo = {
+awaited: ReactIOInfo,
+owner?: null | ReactComponentInfo,
+stack?: null | ReactStackTrace,
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
+debugStack?: null | Error,