Skip to content

Commit 56408a5

Browse files
[Flight] Emit timestamps only in forwards advancing time in debug info (#33482)
Previously you weren't guaranteed to have only advancing time entries, you could jump back in time, but now it omits unnecessary duplicates and clamps automatically if you emit a previous time entry to enforce forwards order only. The reason I didn't do this originally is because `await` can jump in the order because we're trying to encode a graph into a flat timeline for simplicity of the protocol and consumers. ```js async function a() { await fetch1(); await fetch2(); } async function b() { await fetch3(); } async function foo() { const p = a(); await b(); return p; } ``` This can effectively create two parallel sequences: ``` --1.................----2.......-- ------3......--------------------- ``` This can now be flattened to either: ``` --1.................3---2.......-- ``` Or: ``` ------3......1......----2.......-- ``` Depending on which one we visit first. Regardless, information is lost. I'd say that the second one is worse encoding of this scenario because it pretends that we weren't waiting for part of the timespan that we were. To solve this I think we should probably make `emitAsyncSequence` create a temporary flat list and then sort it by start time before emitting. Although we weren't actually blocked since there was some CPU time that was able to proceed to get to 3. So maybe the second one is actually better. If we wanted that consistently we'd have to figure out what the intersection was. --------- Co-authored-by: Hendrik Liebau <[email protected]>
1 parent c38e268 commit 56408a5

File tree

6 files changed

+670
-252
lines changed

6 files changed

+670
-252
lines changed

fixtures/flight/src/App.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,19 @@ async function delay(text, ms) {
3737
return new Promise(resolve => setTimeout(() => resolve(text), ms));
3838
}
3939

40+
async function delayTwice() {
41+
await delay('', 20);
42+
await delay('', 10);
43+
}
44+
45+
async function delayTrice() {
46+
const p = delayTwice();
47+
await delay('', 40);
48+
return p;
49+
}
50+
4051
async function Bar({children}) {
41-
await delay('deferred text', 10);
52+
await delayTrice();
4253
return <div>{children}</div>;
4354
}
4455

packages/react-client/src/ReactFlightClient.js

Lines changed: 97 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,6 +2902,46 @@ function resolveTypedArray(
29022902
resolveBuffer(response, id, view);
29032903
}
29042904

2905+
function logComponentInfo(
2906+
response: Response,
2907+
root: SomeChunk<any>,
2908+
componentInfo: ReactComponentInfo,
2909+
trackIdx: number,
2910+
startTime: number,
2911+
componentEndTime: number,
2912+
childrenEndTime: number,
2913+
isLastComponent: boolean,
2914+
): void {
2915+
// $FlowFixMe: Refined.
2916+
if (
2917+
isLastComponent &&
2918+
root.status === ERRORED &&
2919+
root.reason !== response._closedReason
2920+
) {
2921+
// If this is the last component to render before this chunk rejected, then conceptually
2922+
// this component errored. If this was a cancellation then it wasn't this component that
2923+
// errored.
2924+
logComponentErrored(
2925+
componentInfo,
2926+
trackIdx,
2927+
startTime,
2928+
componentEndTime,
2929+
childrenEndTime,
2930+
response._rootEnvironmentName,
2931+
root.reason,
2932+
);
2933+
} else {
2934+
logComponentRender(
2935+
componentInfo,
2936+
trackIdx,
2937+
startTime,
2938+
componentEndTime,
2939+
childrenEndTime,
2940+
response._rootEnvironmentName,
2941+
);
2942+
}
2943+
}
2944+
29052945
function flushComponentPerformance(
29062946
response: Response,
29072947
root: SomeChunk<any>,
@@ -2957,28 +2997,28 @@ function flushComponentPerformance(
29572997
// in parallel with the previous.
29582998
const debugInfo = __DEV__ && root._debugInfo;
29592999
if (debugInfo) {
2960-
for (let i = 1; i < debugInfo.length; i++) {
3000+
let startTime = 0;
3001+
for (let i = 0; i < debugInfo.length; i++) {
29613002
const info = debugInfo[i];
3003+
if (typeof info.time === 'number') {
3004+
startTime = info.time;
3005+
}
29623006
if (typeof info.name === 'string') {
2963-
// $FlowFixMe: Refined.
2964-
const startTimeInfo = debugInfo[i - 1];
2965-
if (typeof startTimeInfo.time === 'number') {
2966-
const startTime = startTimeInfo.time;
2967-
if (startTime < trackTime) {
2968-
// The start time of this component is before the end time of the previous
2969-
// component on this track so we need to bump the next one to a parallel track.
2970-
trackIdx++;
2971-
}
2972-
trackTime = startTime;
2973-
break;
3007+
if (startTime < trackTime) {
3008+
// The start time of this component is before the end time of the previous
3009+
// component on this track so we need to bump the next one to a parallel track.
3010+
trackIdx++;
29743011
}
3012+
trackTime = startTime;
3013+
break;
29753014
}
29763015
}
29773016
for (let i = debugInfo.length - 1; i >= 0; i--) {
29783017
const info = debugInfo[i];
29793018
if (typeof info.time === 'number') {
29803019
if (info.time > parentEndTime) {
29813020
parentEndTime = info.time;
3021+
break; // We assume the highest number is at the end.
29823022
}
29833023
}
29843024
}
@@ -3006,85 +3046,72 @@ function flushComponentPerformance(
30063046
}
30073047
childTrackIdx = childResult.track;
30083048
const childEndTime = childResult.endTime;
3009-
childTrackTime = childEndTime;
3049+
if (childEndTime > childTrackTime) {
3050+
childTrackTime = childEndTime;
3051+
}
30103052
if (childEndTime > childrenEndTime) {
30113053
childrenEndTime = childEndTime;
30123054
}
30133055
}
30143056

30153057
if (debugInfo) {
3016-
let endTime = 0;
3058+
// Write debug info in reverse order (just like stack traces).
3059+
let componentEndTime = 0;
30173060
let isLastComponent = true;
3061+
let endTime = -1;
3062+
let endTimeIdx = -1;
30183063
for (let i = debugInfo.length - 1; i >= 0; i--) {
30193064
const info = debugInfo[i];
3020-
if (typeof info.time === 'number') {
3021-
if (info.time > childrenEndTime) {
3022-
childrenEndTime = info.time;
3023-
}
3024-
if (endTime === 0) {
3025-
// Last timestamp is the end of the last component.
3026-
endTime = info.time;
3027-
}
3065+
if (typeof info.time !== 'number') {
3066+
continue;
30283067
}
3029-
if (typeof info.name === 'string' && i > 0) {
3030-
// $FlowFixMe: Refined.
3031-
const componentInfo: ReactComponentInfo = info;
3032-
const startTimeInfo = debugInfo[i - 1];
3033-
if (typeof startTimeInfo.time === 'number') {
3034-
const startTime = startTimeInfo.time;
3035-
if (
3036-
isLastComponent &&
3037-
root.status === ERRORED &&
3038-
root.reason !== response._closedReason
3039-
) {
3040-
// If this is the last component to render before this chunk rejected, then conceptually
3041-
// this component errored. If this was a cancellation then it wasn't this component that
3042-
// errored.
3043-
logComponentErrored(
3068+
if (componentEndTime === 0) {
3069+
// Last timestamp is the end of the last component.
3070+
componentEndTime = info.time;
3071+
}
3072+
const time = info.time;
3073+
if (endTimeIdx > -1) {
3074+
// Now that we know the start and end time, we can emit the entries between.
3075+
for (let j = endTimeIdx - 1; j > i; j--) {
3076+
const candidateInfo = debugInfo[j];
3077+
if (typeof candidateInfo.name === 'string') {
3078+
if (componentEndTime > childrenEndTime) {
3079+
childrenEndTime = componentEndTime;
3080+
}
3081+
// $FlowFixMe: Refined.
3082+
const componentInfo: ReactComponentInfo = candidateInfo;
3083+
logComponentInfo(
3084+
response,
3085+
root,
30443086
componentInfo,
30453087
trackIdx,
3046-
startTime,
3047-
endTime,
3088+
time,
3089+
componentEndTime,
30483090
childrenEndTime,
3049-
response._rootEnvironmentName,
3050-
root.reason,
3091+
isLastComponent,
30513092
);
3052-
} else {
3053-
logComponentRender(
3054-
componentInfo,
3093+
componentEndTime = time; // The end time of previous component is the start time of the next.
3094+
// Track the root most component of the result for deduping logging.
3095+
result.component = componentInfo;
3096+
isLastComponent = false;
3097+
} else if (candidateInfo.awaited) {
3098+
if (endTime > childrenEndTime) {
3099+
childrenEndTime = endTime;
3100+
}
3101+
// $FlowFixMe: Refined.
3102+
const asyncInfo: ReactAsyncInfo = candidateInfo;
3103+
logComponentAwait(
3104+
asyncInfo,
30553105
trackIdx,
3056-
startTime,
3106+
time,
30573107
endTime,
3058-
childrenEndTime,
30593108
response._rootEnvironmentName,
30603109
);
30613110
}
3062-
// Track the root most component of the result for deduping logging.
3063-
result.component = componentInfo;
3064-
// Set the end time of the previous component to the start of the previous.
3065-
endTime = startTime;
3066-
}
3067-
isLastComponent = false;
3068-
} else if (info.awaited && i > 0 && i < debugInfo.length - 2) {
3069-
// $FlowFixMe: Refined.
3070-
const asyncInfo: ReactAsyncInfo = info;
3071-
const startTimeInfo = debugInfo[i - 1];
3072-
const endTimeInfo = debugInfo[i + 1];
3073-
if (
3074-
typeof startTimeInfo.time === 'number' &&
3075-
typeof endTimeInfo.time === 'number'
3076-
) {
3077-
const awaitStartTime = startTimeInfo.time;
3078-
const awaitEndTime = endTimeInfo.time;
3079-
logComponentAwait(
3080-
asyncInfo,
3081-
trackIdx,
3082-
awaitStartTime,
3083-
awaitEndTime,
3084-
response._rootEnvironmentName,
3085-
);
30863111
}
30873112
}
3113+
endTime = time; // The end time of the next entry is this time.
3114+
endTimeIdx = i;
30883115
}
30893116
}
30903117
result.endTime = childrenEndTime;

packages/react-server/src/ReactFlightAsyncSequence.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export type PromiseNode = {
3838
start: number, // start time when the Promise was created
3939
end: number, // end time when the Promise was resolved.
4040
awaited: null | AsyncSequence, // the thing that ended up resolving this promise
41-
previous: null, // where we created the promise is not interesting since creating it doesn't mean waiting.
41+
previous: null | AsyncSequence, // represents what the last return of an async function depended on before returning
4242
};
4343

4444
export type AwaitNode = {

0 commit comments

Comments
 (0)