@samitouri / QOS-React-2 / commits / 45da4e055d

[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,