Skip to content

Commit

Permalink
[Flight] Stack Parallel Components in Separate Tracks (#31735)
Browse files Browse the repository at this point in the history
Stacked on #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.
  • Loading branch information
sebmarkbage authored Dec 16, 2024
1 parent f7b1273 commit 031230d
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 10 deletions.
61 changes: 53 additions & 8 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export type JSONValue =
| $ReadOnlyArray<JSONValue>;

type ProfilingResult = {
track: number,
endTime: number,
};

Expand Down Expand Up @@ -642,7 +643,7 @@ export function reportGlobalError(response: Response, error: Error): void {
}
});
if (enableProfilerTimer && enableComponentPerformanceTrack) {
flushComponentPerformance(getChunk(response, 0));
flushComponentPerformance(getChunk(response, 0), 0, -Infinity);
}
}

Expand Down Expand Up @@ -2740,9 +2741,16 @@ function resolveTypedArray(
resolveBuffer(response, id, view);
}

function flushComponentPerformance(root: SomeChunk<any>): number {
function flushComponentPerformance(
root: SomeChunk<any>,
trackIdx: number, // Next available track
trackTime: number, // The time after which it is available
): ProfilingResult {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return 0;
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'flushComponentPerformance should never be called in production mode. This is a bug in React.',
);
}
// Write performance.measure() entries for Server Components in tree order.
// This must be done at the end to collect the end time from the whole tree.
Expand All @@ -2753,7 +2761,9 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
// chunk in two places. We should extend the current end time as if it was
// rendered as part of this tree.
const previousResult: ProfilingResult = root._children;
return previousResult.endTime;
// Since we didn't bump the track this time, we just return the same track.
previousResult.track = trackIdx;
return previousResult;
}
const children = root._children;
if (root.status === RESOLVED_MODEL) {
Expand All @@ -2762,16 +2772,49 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
// the performance characteristics of the app by profiling.
initializeModelChunk(root);
}
const result: ProfilingResult = {endTime: -Infinity};

// First find the start time of the first component to know if it was running
// in parallel with the previous.
const debugInfo = root._debugInfo;
if (debugInfo) {
for (let i = 1; i < debugInfo.length; i++) {
const info = debugInfo[i];
if (typeof info.name === 'string') {
// $FlowFixMe: Refined.
const startTimeInfo = debugInfo[i - 1];
if (typeof startTimeInfo.time === 'number') {
const startTime = startTimeInfo.time;
if (startTime < trackTime) {
// The start time of this component is before the end time of the previous
// component on this track so we need to bump the next one to a parallel track.
trackIdx++;
trackTime = startTime;
}
break;
}
}
}
}

const result: ProfilingResult = {track: trackIdx, endTime: -Infinity};
root._children = result;
let childrenEndTime = -Infinity;
let childTrackIdx = trackIdx;
let childTrackTime = trackTime;
for (let i = 0; i < children.length; i++) {
const childEndTime = flushComponentPerformance(children[i]);
const childResult = flushComponentPerformance(
children[i],
childTrackIdx,
childTrackTime,
);
childTrackIdx = childResult.track;
const childEndTime = childResult.endTime;
childTrackTime = childEndTime;
if (childEndTime > childrenEndTime) {
childrenEndTime = childEndTime;
}
}
const debugInfo = root._debugInfo;

if (debugInfo) {
let endTime = 0;
for (let i = debugInfo.length - 1; i >= 0; i--) {
Expand All @@ -2790,6 +2833,7 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
const startTime = startTimeInfo.time;
logComponentRender(
componentInfo,
trackIdx,
startTime,
endTime,
childrenEndTime,
Expand All @@ -2798,7 +2842,8 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
}
}
}
return (result.endTime = childrenEndTime);
result.endTime = childrenEndTime;
return result;
}

function processFullBinaryRow(
Expand Down
20 changes: 18 additions & 2 deletions packages/react-client/src/ReactFlightPerformanceTrack.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const COMPONENTS_TRACK = 'Server Components ⚛';
// Reused to avoid thrashing the GC.
const reusableComponentDevToolDetails = {
color: 'primary',
track: COMPONENTS_TRACK,
track: '',
trackGroup: COMPONENTS_TRACK,
};
const reusableComponentOptions = {
start: -0,
Expand All @@ -32,13 +33,27 @@ const reusableComponentOptions = {
},
};

const trackNames = [
'Primary',
'Parallel',
'Parallel\u200b', // Padded with zero-width space to give each track a unique name.
'Parallel\u200b\u200b',
'Parallel\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
'Parallel\u200b\u200b\u200b\u200b\u200b\u200b\u200b\u200b',
];

export function logComponentRender(
componentInfo: ReactComponentInfo,
trackIdx: number,
startTime: number,
endTime: number,
childrenEndTime: number,
): void {
if (supportsUserTiming && childrenEndTime >= 0) {
if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
const name = componentInfo.name;
const selfTime = endTime - startTime;
reusableComponentDevToolDetails.color =
Expand All @@ -49,6 +64,7 @@ export function logComponentRender(
: selfTime < 500
? 'primary-dark'
: 'error';
reusableComponentDevToolDetails.track = trackNames[trackIdx];
reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
reusableComponentOptions.end = childrenEndTime;
performance.measure(name, reusableComponentOptions);
Expand Down

0 comments on commit 031230d

Please sign in to comment.