[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.
Sebastian Markbåge committed
Jun 3, 2025 at 16:12 UTC
45da4e055dc7a2b9de6abdae0709e242f8091636
6 files changed
+154
-33
packages/react-client/src/ReactFlightClient.js
+5
-5
@@ -2616,14 +2616,15 @@ function resolveDebugInfo(
2616
initializeFakeTask(response, componentInfoOrAsyncInfo, env);
2617
}
2618
if (debugInfo.owner === null && response._debugRootOwner != null) {
2619
- // $FlowFixMe[prop-missing] By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
2620
- const componentInfo: ReactComponentInfo = debugInfo;
2619
+ const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
2620
+ // $FlowFixMe: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo`
2621
+ debugInfo;
2622
// $FlowFixMe[cannot-write]
2622
- componentInfo.owner = response._debugRootOwner;
2623
+ componentInfoOrAsyncInfo.owner = response._debugRootOwner;
2624
// We override the stack if we override the owner since the stack where the root JSX
2625
// was created on the server isn't very useful but where the request was made is.
2626
// $FlowFixMe[cannot-write]
2626
- componentInfo.debugStack = response._debugRootStack;
2627
+ componentInfoOrAsyncInfo.debugStack = response._debugRootStack;
2628
} else if (debugInfo.stack !== undefined) {
2629
const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo =
2630
// $FlowFixMe[incompatible-type]
@@ -2764,7 +2765,6 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
2765
initializeFakeTask(response, ioInfo, env);
2766
initializeFakeStack(response, ioInfo);
2767
}
2767
- // TODO: Initialize owner.
2768
// Adjust the time to the current environment's time space.
2769
// $FlowFixMe[cannot-write]
2770
ioInfo.start += response._timeOrigin;
packages/react-server/src/ReactFlightAsyncSequence.js
+5
@@ -7,12 +7,15 @@
7
* @flow
8
*/
9
10
+import type {ReactComponentInfo} from 'shared/ReactTypes';
11
+
12
export const IO_NODE = 0;
13
export const PROMISE_NODE = 1;
14
export const AWAIT_NODE = 2;
15
16
export type IONode = {
17
tag: 0,
18
+ owner: null | ReactComponentInfo,
19
stack: Error, // callsite that spawned the I/O
20
start: number, // start time when the first part of the I/O sequence started
21
end: number, // we typically don't use this. only when there's no promise intermediate.
@@ -22,6 +25,7 @@ export type IONode = {
25
26
export type PromiseNode = {
27
tag: 1,
28
+ owner: null | ReactComponentInfo,
29
stack: Error, // callsite that created the Promise
30
start: number, // start time when the Promise was created
31
end: number, // end time when the Promise was resolved.
@@ -31,6 +35,7 @@ export type PromiseNode = {
35
36
export type AwaitNode = {
37
tag: 2,
38
+ owner: null | ReactComponentInfo,
39
stack: Error, // callsite that awaited (using await, .then(), Promise.all(), ...)
40
start: -1.1, // not used. We use the timing of the awaited promise.
41
end: -1.1, // not used.
packages/react-server/src/ReactFlightServer.js
+23
-2
@@ -1934,6 +1934,7 @@ function visitAsyncNode(
1934
request.pendingChunks++;
1935
emitDebugChunk(request, task.id, {
1936
awaited: ((ioNode: any): ReactIOInfo), // This is deduped by this reference.
1937
+ owner: node.owner,
1938
stack: stack,
1939
});
1940
}
@@ -3523,6 +3524,7 @@ function emitIOInfoChunk(
3524
name: string,
3525
start: number,
3526
end: number,
3527
+ owner: ?ReactComponentInfo,
3528
stack: ?ReactStackTrace,
3529
): void {
3530
if (!__DEV__) {
@@ -3560,8 +3562,15 @@ function emitIOInfoChunk(
3562
name: name,
3563
start: relativeStartTimestamp,
3564
end: relativeEndTimestamp,
3563
- stack: stack,
3565
};
3566
+ if (stack != null) {
3567
+ // $FlowFixMe[cannot-write]
3568
+ debugIOInfo.stack = stack;
3569
+ }
3570
+ if (owner != null) {
3571
+ // $FlowFixMe[cannot-write]
3572
+ debugIOInfo.owner = owner;
3573
+ }
3574
// $FlowFixMe[incompatible-type] stringify can return null
3575
const json: string = stringify(debugIOInfo, replacer);
3576
const row = id.toString(16) + ':J' + json + '\n';
@@ -3577,12 +3586,18 @@ function outlineIOInfo(request: Request, ioInfo: ReactIOInfo): void {
3586
// We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing.
3587
request.pendingChunks++;
3588
const id = request.nextChunkId++;
3589
+ const owner = ioInfo.owner;
3590
+ // Ensure the owner is already outlined.
3591
+ if (owner != null) {
3592
+ outlineComponentInfo(request, owner);
3593
+ }
3594
emitIOInfoChunk(
3595
request,
3596
id,
3597
ioInfo.name,
3598
ioInfo.start,
3599
ioInfo.end,
3600
+ owner,
3601
ioInfo.stack,
3602
);
3603
request.writtenObjects.set(ioInfo, serializeByValueID(id));
@@ -3612,10 +3627,15 @@ function serializeIONode(
3627
name = name.slice(7);
3628
}
3629
}
3630
+ const owner = ioNode.owner;
3631
+ // Ensure the owner is already outlined.
3632
+ if (owner != null) {
3633
+ outlineComponentInfo(request, owner);
3634
+ }
3635
3636
request.pendingChunks++;
3637
const id = request.nextChunkId++;
3618
- emitIOInfoChunk(request, id, name, ioNode.start, ioNode.end, stack);
3638
+ emitIOInfoChunk(request, id, name, ioNode.start, ioNode.end, owner, stack);
3639
const ref = serializeByValueID(id);
3640
request.writtenObjects.set(ioNode, ref);
3641
return ref;
@@ -4141,6 +4161,7 @@ function forwardDebugInfo(
4161
const debugAsyncInfo: Omit<ReactAsyncInfo, 'debugTask' | 'debugStack'> =
4162
{
4163
awaited: ioInfo,
4164
+ owner: debugInfo[i].owner,
4165
stack: debugInfo[i].stack,
4166
};
4167
emitDebugChunk(request, id, debugAsyncInfo);
packages/react-server/src/ReactFlightServerConfigDebugNode.js
+5
@@ -15,6 +15,7 @@ import type {
15
} from './ReactFlightAsyncSequence';
16
17
import {IO_NODE, PROMISE_NODE, AWAIT_NODE} from './ReactFlightAsyncSequence';
18
+import {resolveOwner} from './flight/ReactFlightCurrentOwner';
19
import {createHook, executionAsyncId} from 'async_hooks';
20
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
21
@@ -46,6 +47,7 @@ export function initAsyncDebugInfo(): void {
47
// so that we can later pick the best stack trace in user space.
48
node = ({
49
tag: AWAIT_NODE,
50
+ owner: resolveOwner(),
51
stack: new Error(),
52
start: -1.1,
53
end: -1.1,
@@ -55,6 +57,7 @@ export function initAsyncDebugInfo(): void {
57
} else {
58
node = ({
59
tag: PROMISE_NODE,
60
+ owner: resolveOwner(),
61
stack: new Error(),
62
start: performance.now(),
63
end: -1.1, // Set when we resolve.
@@ -74,6 +77,7 @@ export function initAsyncDebugInfo(): void {
77
// We have begun a new I/O sequence.
78
node = ({
79
tag: IO_NODE,
80
+ owner: resolveOwner(),
81
stack: new Error(), // This is only used if no native promises are used.
82
start: performance.now(),
83
end: -1.1, // Only set when pinged.
@@ -84,6 +88,7 @@ export function initAsyncDebugInfo(): void {
88
// We have begun a new I/O sequence after the await.
89
node = ({
90
tag: IO_NODE,
91
+ owner: resolveOwner(),
92
stack: new Error(),
93
start: performance.now(),
94
end: -1.1, // Only set when pinged.
packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js
+114
-26
@@ -39,6 +39,9 @@ function normalizeIOInfo(ioInfo) {
39
if (ioInfo.stack) {
40
copy.stack = normalizeStack(ioInfo.stack);
41
}
42
+ if (ioInfo.owner) {
43
+ copy.owner = normalizeDebugInfo(ioInfo.owner);
44
+ }
45
if (typeof ioInfo.start === 'number') {
46
copy.start = 0;
47
}
@@ -160,9 +163,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
163
[
164
"Object.<anonymous>",
165
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
163
- 130,
166
+ 133,
167
109,
165
- 117,
168
+ 120,
169
50,
170
],
171
],
@@ -171,49 +174,83 @@ describe('ReactFlightAsyncDebugInfo', () => {
174
"awaited": {
175
"end": 0,
176
"name": "delay",
177
+ "owner": {
178
+ "env": "Server",
179
+ "key": null,
180
+ "name": "Component",
181
+ "owner": null,
182
+ "props": {},
183
+ "stack": [
184
+ [
185
+ "Object.<anonymous>",
186
+ "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
187
+ 133,
188
+ 109,
189
+ 120,
190
+ 50,
191
+ ],
192
+ ],
193
+ },
194
"stack": [
195
[
196
"delay",
197
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
178
- 112,
198
+ 115,
199
12,
180
- 111,
200
+ 114,
201
3,
202
],
203
[
204
"getData",
205
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
186
- 119,
206
+ 122,
207
13,
188
- 118,
208
+ 121,
209
5,
210
],
211
[
212
"Component",
213
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
194
- 126,
214
+ 129,
215
26,
196
- 125,
216
+ 128,
217
5,
218
],
219
],
220
"start": 0,
221
},
222
+ "owner": {
223
+ "env": "Server",
224
+ "key": null,
225
+ "name": "Component",
226
+ "owner": null,
227
+ "props": {},
228
+ "stack": [
229
+ [
230
+ "Object.<anonymous>",
231
+ "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
232
+ 133,
233
+ 109,
234
+ 120,
235
+ 50,
236
+ ],
237
+ ],
238
+ },
239
"stack": [
240
[
241
"getData",
242
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
206
- 119,
243
+ 122,
244
13,
208
- 118,
245
+ 121,
246
5,
247
],
248
[
249
"Component",
250
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
214
- 126,
251
+ 129,
252
26,
216
- 125,
253
+ 128,
254
5,
255
],
256
],
@@ -222,49 +259,83 @@ describe('ReactFlightAsyncDebugInfo', () => {
259
"awaited": {
260
"end": 0,
261
"name": "delay",
262
+ "owner": {
263
+ "env": "Server",
264
+ "key": null,
265
+ "name": "Component",
266
+ "owner": null,
267
+ "props": {},
268
+ "stack": [
269
+ [
270
+ "Object.<anonymous>",
271
+ "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
272
+ 133,
273
+ 109,
274
+ 120,
275
+ 50,
276
+ ],
277
+ ],
278
+ },
279
"stack": [
280
[
281
"delay",
282
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
229
- 112,
283
+ 115,
284
12,
231
- 111,
285
+ 114,
286
3,
287
],
288
[
289
"getData",
290
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
237
- 120,
291
+ 123,
292
21,
239
- 118,
293
+ 121,
294
5,
295
],
296
[
297
"Component",
298
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
245
- 126,
299
+ 129,
300
20,
247
- 125,
301
+ 128,
302
5,
303
],
304
],
305
"start": 0,
306
},
307
+ "owner": {
308
+ "env": "Server",
309
+ "key": null,
310
+ "name": "Component",
311
+ "owner": null,
312
+ "props": {},
313
+ "stack": [
314
+ [
315
+ "Object.<anonymous>",
316
+ "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
317
+ 133,
318
+ 109,
319
+ 120,
320
+ 50,
321
+ ],
322
+ ],
323
+ },
324
"stack": [
325
[
326
"getData",
327
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
257
- 121,
328
+ 124,
329
21,
259
- 118,
330
+ 121,
331
5,
332
],
333
[
334
"Component",
335
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
265
- 126,
336
+ 129,
337
20,
267
- 125,
338
+ 128,
339
5,
340
],
341
],
@@ -323,9 +394,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
394
[
395
"Object.<anonymous>",
396
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
326
- 293,
397
+ 364,
398
109,
328
- 280,
399
+ 351,
400
67,
401
],
402
],
@@ -334,13 +405,30 @@ describe('ReactFlightAsyncDebugInfo', () => {
405
"awaited": {
406
"end": 0,
407
"name": "setTimeout",
408
+ "owner": {
409
+ "env": "Server",
410
+ "key": null,
411
+ "name": "Component",
412
+ "owner": null,
413
+ "props": {},
414
+ "stack": [
415
+ [
416
+ "Object.<anonymous>",
417
+ "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
418
+ 364,
419
+ 109,
420
+ 351,
421
+ 67,
422
+ ],
423
+ ],
424
+ },
425
"stack": [
426
[
427
"Component",
428
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
341
- 283,
429
+ 354,
430
7,
343
- 281,
431
+ 352,
432
5,
433
],
434
],
packages/shared/ReactTypes.js
+2
@@ -234,6 +234,7 @@ export type ReactIOInfo = {
234
+name: string, // the name of the async function being called (e.g. "fetch")
235
+start: number, // the start time
236
+end: number, // the end time (this might be different from the time the await was unblocked)
237
+ +owner?: null | ReactComponentInfo,
238
+stack?: null | ReactStackTrace,
239
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
240
+debugStack?: null | Error,
@@ -242,6 +243,7 @@ export type ReactIOInfo = {
243
244
export type ReactAsyncInfo = {
245
+awaited: ReactIOInfo,
246
+ +owner?: null | ReactComponentInfo,
247
+stack?: null | ReactStackTrace,
248
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
249
+debugStack?: null | Error,