Skip to content

Commit 9cc74fe

Browse files
authored
[Flight] Emit the time we awaited something inside a Server Component (#33402)
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.
1 parent 157ac57 commit 9cc74fe

File tree

6 files changed

+138
-19
lines changed

6 files changed

+138
-19
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
logDedupedComponentRender,
8080
logComponentErrored,
8181
logIOInfo,
82+
logComponentAwait,
8283
} from './ReactFlightPerformanceTrack';
8384

8485
import {
@@ -680,7 +681,7 @@ function getIOInfoTaskName(ioInfo: ReactIOInfo): string {
680681
}
681682

682683
function getAsyncInfoTaskName(asyncInfo: ReactAsyncInfo): string {
683-
return 'await'; // We could be smarter about this and give it a name like `then` or `Promise.all`.
684+
return 'await ' + getIOInfoTaskName(asyncInfo.awaited);
684685
}
685686

686687
function getServerComponentTaskName(componentInfo: ReactComponentInfo): string {
@@ -2971,9 +2972,12 @@ function flushComponentPerformance(
29712972
for (let i = debugInfo.length - 1; i >= 0; i--) {
29722973
const info = debugInfo[i];
29732974
if (typeof info.time === 'number') {
2974-
endTime = info.time;
2975-
if (endTime > childrenEndTime) {
2976-
childrenEndTime = endTime;
2975+
if (info.time > childrenEndTime) {
2976+
childrenEndTime = info.time;
2977+
}
2978+
if (endTime === 0) {
2979+
// Last timestamp is the end of the last component.
2980+
endTime = info.time;
29772981
}
29782982
}
29792983
if (typeof info.name === 'string' && i > 0) {
@@ -3011,8 +3015,29 @@ function flushComponentPerformance(
30113015
}
30123016
// Track the root most component of the result for deduping logging.
30133017
result.component = componentInfo;
3018+
// Set the end time of the previous component to the start of the previous.
3019+
endTime = startTime;
30143020
}
30153021
isLastComponent = false;
3022+
} else if (info.awaited && i > 0 && i < debugInfo.length - 2) {
3023+
// $FlowFixMe: Refined.
3024+
const asyncInfo: ReactAsyncInfo = info;
3025+
const startTimeInfo = debugInfo[i - 1];
3026+
const endTimeInfo = debugInfo[i + 1];
3027+
if (
3028+
typeof startTimeInfo.time === 'number' &&
3029+
typeof endTimeInfo.time === 'number'
3030+
) {
3031+
const awaitStartTime = startTimeInfo.time;
3032+
const awaitEndTime = endTimeInfo.time;
3033+
logComponentAwait(
3034+
asyncInfo,
3035+
trackIdx,
3036+
awaitStartTime,
3037+
awaitEndTime,
3038+
response._rootEnvironmentName,
3039+
);
3040+
}
30163041
}
30173042
}
30183043
}

packages/react-client/src/ReactFlightPerformanceTrack.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
/* eslint-disable react-internal/no-production-logging */
1111

12-
import type {ReactComponentInfo, ReactIOInfo} from 'shared/ReactTypes';
12+
import type {
13+
ReactComponentInfo,
14+
ReactIOInfo,
15+
ReactAsyncInfo,
16+
} from 'shared/ReactTypes';
1317

1418
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
1519

@@ -224,6 +228,48 @@ function getIOColor(
224228
}
225229
}
226230

231+
export function logComponentAwait(
232+
asyncInfo: ReactAsyncInfo,
233+
trackIdx: number,
234+
startTime: number,
235+
endTime: number,
236+
rootEnv: string,
237+
): void {
238+
if (supportsUserTiming && endTime > 0) {
239+
const env = asyncInfo.env;
240+
const name = asyncInfo.awaited.name;
241+
const isPrimaryEnv = env === rootEnv;
242+
const color = getIOColor(name);
243+
const entryName =
244+
'await ' +
245+
(isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']');
246+
const debugTask = asyncInfo.debugTask;
247+
if (__DEV__ && debugTask) {
248+
debugTask.run(
249+
// $FlowFixMe[method-unbinding]
250+
console.timeStamp.bind(
251+
console,
252+
entryName,
253+
startTime < 0 ? 0 : startTime,
254+
endTime,
255+
trackNames[trackIdx],
256+
COMPONENTS_TRACK,
257+
color,
258+
),
259+
);
260+
} else {
261+
console.timeStamp(
262+
entryName,
263+
startTime < 0 ? 0 : startTime,
264+
endTime,
265+
trackNames[trackIdx],
266+
COMPONENTS_TRACK,
267+
color,
268+
);
269+
}
270+
}
271+
}
272+
227273
export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void {
228274
const startTime = ioInfo.start;
229275
const endTime = ioInfo.end;

packages/react-server/src/ReactFlightAsyncSequence.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export type AwaitNode = {
3737
tag: 2,
3838
owner: null | ReactComponentInfo,
3939
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.
40+
start: number, // when we started blocking. This might be later than the I/O started.
41+
end: number, // when we unblocked. This might be later than the I/O resolved if there's CPU time.
4242
awaited: null | AsyncSequence, // the promise we were waiting on
4343
previous: null | AsyncSequence, // the sequence that was blocking us from awaiting in the first place
4444
};

packages/react-server/src/ReactFlightServer.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1919,6 +1919,24 @@ function visitAsyncNode(
19191919
if (awaited !== null) {
19201920
const ioNode = visitAsyncNode(request, task, awaited, cutOff, visited);
19211921
if (ioNode !== null) {
1922+
if (node.end < 0) {
1923+
// If we haven't defined an end time, use the resolve of the inner Promise.
1924+
// This can happen because the ping gets invoked before the await gets resolved.
1925+
if (ioNode.end < node.start) {
1926+
// If we're awaiting a resolved Promise it could have finished before we started.
1927+
node.end = node.start;
1928+
} else {
1929+
node.end = ioNode.end;
1930+
}
1931+
}
1932+
if (node.end < cutOff) {
1933+
// This was already resolved when we started this sequence. It must have been
1934+
// part of a different component.
1935+
// TODO: Think of some other way to exclude irrelevant data since if we awaited
1936+
// a cached promise, we should still log this component as being dependent on that data.
1937+
return null;
1938+
}
1939+
19221940
const stack = filterStackTrace(
19231941
request,
19241942
parseStackTrace(node.stack, 1),
@@ -1933,6 +1951,15 @@ function visitAsyncNode(
19331951
// We log the environment at the time when the last promise pigned ping which may
19341952
// be later than what the environment was when we actually started awaiting.
19351953
const env = (0, request.environmentName)();
1954+
if (node.start <= cutOff) {
1955+
// If this was an await that started before this sequence but finished after,
1956+
// then we clamp it to the start of this sequence. We don't need to emit a time
1957+
// TODO: Typically we'll already have a previous time stamp with the cutOff time
1958+
// so we shouldn't need to emit another one. But not always.
1959+
emitTimingChunk(request, task.id, cutOff);
1960+
} else {
1961+
emitTimingChunk(request, task.id, node.start);
1962+
}
19361963
// Then emit a reference to us awaiting it in the current task.
19371964
request.pendingChunks++;
19381965
emitDebugChunk(request, task.id, {
@@ -1941,6 +1968,7 @@ function visitAsyncNode(
19411968
owner: node.owner,
19421969
stack: stack,
19431970
});
1971+
emitTimingChunk(request, task.id, node.end);
19441972
}
19451973
}
19461974
// If we had awaited anything we would have written it now.
@@ -1976,10 +2004,14 @@ function emitAsyncSequence(
19762004
// We log the environment at the time when we ping which may be later than what the
19772005
// environment was when we actually started awaiting.
19782006
const env = (0, request.environmentName)();
2007+
// If we don't have any thing awaited, the time we started awaiting was internal
2008+
// when we yielded after rendering. The cutOff time is basically that.
2009+
emitTimingChunk(request, task.id, cutOff);
19792010
emitDebugChunk(request, task.id, {
19802011
awaited: ((awaitedNode: any): ReactIOInfo), // This is deduped by this reference.
19812012
env: env,
19822013
});
2014+
emitTimingChunk(request, task.id, awaitedNode.end);
19832015
}
19842016
}
19852017

packages/react-server/src/ReactFlightServerConfigDebugNode.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export function initAsyncDebugInfo(): void {
4949
tag: AWAIT_NODE,
5050
owner: resolveOwner(),
5151
stack: new Error(),
52-
start: -1.1,
53-
end: -1.1,
52+
start: performance.now(),
53+
end: -1.1, // set when resolved.
5454
awaited: trigger, // The thing we're awaiting on. Might get overrriden when we resolve.
5555
previous: current === undefined ? null : current, // The path that led us here.
5656
}: AwaitNode);
@@ -118,10 +118,8 @@ export function initAsyncDebugInfo(): void {
118118
'A Promise should never be an IO_NODE. This is a bug in React.',
119119
);
120120
}
121-
if (resolvedNode.tag === PROMISE_NODE) {
122-
// Log the end time when we resolved the promise.
123-
resolvedNode.end = performance.now();
124-
}
121+
// Log the end time when we resolved the promise.
122+
resolvedNode.end = performance.now();
125123
const currentAsyncId = executionAsyncId();
126124
if (asyncId !== currentAsyncId) {
127125
// If the promise was not resolved by itself, then that means that

packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
170170
],
171171
],
172172
},
173+
{
174+
"time": 0,
175+
},
173176
{
174177
"awaited": {
175178
"end": 0,
@@ -257,6 +260,12 @@ describe('ReactFlightAsyncDebugInfo', () => {
257260
],
258261
],
259262
},
263+
{
264+
"time": 0,
265+
},
266+
{
267+
"time": 0,
268+
},
260269
{
261270
"awaited": {
262271
"end": 0,
@@ -347,6 +356,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
347356
{
348357
"time": 0,
349358
},
359+
{
360+
"time": 0,
361+
},
350362
]
351363
`);
352364
}
@@ -398,13 +410,16 @@ describe('ReactFlightAsyncDebugInfo', () => {
398410
[
399411
"Object.<anonymous>",
400412
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
401-
368,
413+
380,
402414
109,
403-
355,
415+
367,
404416
67,
405417
],
406418
],
407419
},
420+
{
421+
"time": 0,
422+
},
408423
{
409424
"awaited": {
410425
"end": 0,
@@ -420,9 +435,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
420435
[
421436
"Object.<anonymous>",
422437
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
423-
368,
438+
380,
424439
109,
425-
355,
440+
367,
426441
67,
427442
],
428443
],
@@ -431,9 +446,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
431446
[
432447
"Component",
433448
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
434-
358,
449+
370,
435450
7,
436-
356,
451+
368,
437452
5,
438453
],
439454
],
@@ -444,6 +459,9 @@ describe('ReactFlightAsyncDebugInfo', () => {
444459
{
445460
"time": 0,
446461
},
462+
{
463+
"time": 0,
464+
},
447465
]
448466
`);
449467
}

0 commit comments

Comments
 (0)