Skip to content

Commit 031230d

Browse files
authored
[Flight] Stack Parallel Components in Separate Tracks (facebook#31735)
Stacked on facebook#31729 <img width="1436" alt="Screenshot 2024-12-11 at 3 36 41 PM" src="https://github.com/user-attachments/assets/0a201913-0076-4bbf-be18-8f1df6c58313" /> The Server Components visualization is currently a tree flame graph where parent spans the child. This makes it equivalent to the Client Components visualization. However, since Server Components can be async and therefore parallel, we need to do something when two children are executed in parallel. This PR bumps parallel children into a separate track and then within that track if that child has more children it can grow within that track. I currently just cut off more than 10 parallel tracks. Synchronous Server Components are still in sequence but it's unlikely because even a simple microtasky Async Component is still parallel. <img width="959" alt="Screenshot 2024-12-11 at 4 31 17 PM" src="https://github.com/user-attachments/assets/5ad6a7f8-7fa0-46dc-af51-78caf9849176" /> I think this is probably not a very useful visualization for Server Components but we can try it out. I'm also going to try a different visualization where parent-child relationship is horizontal and parallel vertical instead, but it might not be possible to make that line up in this tool. It makes it a little harder to see how much different components (including their children) impact the overall tree. If that's the only visualization it's also confusing why it's different dimensions than the Client Component version.
1 parent f7b1273 commit 031230d

File tree

2 files changed

+71
-10
lines changed

2 files changed

+71
-10
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export type JSONValue =
125125
| $ReadOnlyArray<JSONValue>;
126126

127127
type ProfilingResult = {
128+
track: number,
128129
endTime: number,
129130
};
130131

@@ -642,7 +643,7 @@ export function reportGlobalError(response: Response, error: Error): void {
642643
}
643644
});
644645
if (enableProfilerTimer && enableComponentPerformanceTrack) {
645-
flushComponentPerformance(getChunk(response, 0));
646+
flushComponentPerformance(getChunk(response, 0), 0, -Infinity);
646647
}
647648
}
648649

@@ -2740,9 +2741,16 @@ function resolveTypedArray(
27402741
resolveBuffer(response, id, view);
27412742
}
27422743

2743-
function flushComponentPerformance(root: SomeChunk<any>): number {
2744+
function flushComponentPerformance(
2745+
root: SomeChunk<any>,
2746+
trackIdx: number, // Next available track
2747+
trackTime: number, // The time after which it is available
2748+
): ProfilingResult {
27442749
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
2745-
return 0;
2750+
// eslint-disable-next-line react-internal/prod-error-codes
2751+
throw new Error(
2752+
'flushComponentPerformance should never be called in production mode. This is a bug in React.',
2753+
);
27462754
}
27472755
// Write performance.measure() entries for Server Components in tree order.
27482756
// This must be done at the end to collect the end time from the whole tree.
@@ -2753,7 +2761,9 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
27532761
// chunk in two places. We should extend the current end time as if it was
27542762
// rendered as part of this tree.
27552763
const previousResult: ProfilingResult = root._children;
2756-
return previousResult.endTime;
2764+
// Since we didn't bump the track this time, we just return the same track.
2765+
previousResult.track = trackIdx;
2766+
return previousResult;
27572767
}
27582768
const children = root._children;
27592769
if (root.status === RESOLVED_MODEL) {
@@ -2762,16 +2772,49 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
27622772
// the performance characteristics of the app by profiling.
27632773
initializeModelChunk(root);
27642774
}
2765-
const result: ProfilingResult = {endTime: -Infinity};
2775+
2776+
// First find the start time of the first component to know if it was running
2777+
// in parallel with the previous.
2778+
const debugInfo = root._debugInfo;
2779+
if (debugInfo) {
2780+
for (let i = 1; i < debugInfo.length; i++) {
2781+
const info = debugInfo[i];
2782+
if (typeof info.name === 'string') {
2783+
// $FlowFixMe: Refined.
2784+
const startTimeInfo = debugInfo[i - 1];
2785+
if (typeof startTimeInfo.time === 'number') {
2786+
const startTime = startTimeInfo.time;
2787+
if (startTime < trackTime) {
2788+
// The start time of this component is before the end time of the previous
2789+
// component on this track so we need to bump the next one to a parallel track.
2790+
trackIdx++;
2791+
trackTime = startTime;
2792+
}
2793+
break;
2794+
}
2795+
}
2796+
}
2797+
}
2798+
2799+
const result: ProfilingResult = {track: trackIdx, endTime: -Infinity};
27662800
root._children = result;
27672801
let childrenEndTime = -Infinity;
2802+
let childTrackIdx = trackIdx;
2803+
let childTrackTime = trackTime;
27682804
for (let i = 0; i < children.length; i++) {
2769-
const childEndTime = flushComponentPerformance(children[i]);
2805+
const childResult = flushComponentPerformance(
2806+
children[i],
2807+
childTrackIdx,
2808+
childTrackTime,
2809+
);
2810+
childTrackIdx = childResult.track;
2811+
const childEndTime = childResult.endTime;
2812+
childTrackTime = childEndTime;
27702813
if (childEndTime > childrenEndTime) {
27712814
childrenEndTime = childEndTime;
27722815
}
27732816
}
2774-
const debugInfo = root._debugInfo;
2817+
27752818
if (debugInfo) {
27762819
let endTime = 0;
27772820
for (let i = debugInfo.length - 1; i >= 0; i--) {
@@ -2790,6 +2833,7 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
27902833
const startTime = startTimeInfo.time;
27912834
logComponentRender(
27922835
componentInfo,
2836+
trackIdx,
27932837
startTime,
27942838
endTime,
27952839
childrenEndTime,
@@ -2798,7 +2842,8 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
27982842
}
27992843
}
28002844
}
2801-
return (result.endTime = childrenEndTime);
2845+
result.endTime = childrenEndTime;
2846+
return result;
28022847
}
28032848

28042849
function processFullBinaryRow(

packages/react-client/src/ReactFlightPerformanceTrack.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const COMPONENTS_TRACK = 'Server Components ⚛';
2222
// Reused to avoid thrashing the GC.
2323
const reusableComponentDevToolDetails = {
2424
color: 'primary',
25-
track: COMPONENTS_TRACK,
25+
track: '',
26+
trackGroup: COMPONENTS_TRACK,
2627
};
2728
const reusableComponentOptions = {
2829
start: -0,
@@ -32,13 +33,27 @@ const reusableComponentOptions = {
3233
},
3334
};
3435

36+
const trackNames = [
37+
'Primary',
38+
'Parallel',
39+
'Parallel\u200b', // Padded with zero-width space to give each track a unique name.
40+
'Parallel\u200b\u200b',
41+
'Parallel\u200b\u200b\u200b',
42+
'Parallel\u200b\u200b\u200b\u200b',
43+
'Parallel\u200b\u200b\u200b\u200b\u200b',
44+
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b',
45+
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
46+
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
47+
];
48+
3549
export function logComponentRender(
3650
componentInfo: ReactComponentInfo,
51+
trackIdx: number,
3752
startTime: number,
3853
endTime: number,
3954
childrenEndTime: number,
4055
): void {
41-
if (supportsUserTiming && childrenEndTime >= 0) {
56+
if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
4257
const name = componentInfo.name;
4358
const selfTime = endTime - startTime;
4459
reusableComponentDevToolDetails.color =
@@ -49,6 +64,7 @@ export function logComponentRender(
4964
: selfTime < 500
5065
? 'primary-dark'
5166
: 'error';
67+
reusableComponentDevToolDetails.track = trackNames[trackIdx];
5268
reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
5369
reusableComponentOptions.end = childrenEndTime;
5470
performance.measure(name, reusableComponentOptions);

0 commit comments

Comments
 (0)