Repository: facebook/react
Pull request: [Flight] Stack Parallel Components in Separate Tracks

Stacked on https://github.com/facebook/react/pull/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.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -125,6 +125,7 @@ export type JSONValue =
   | $ReadOnlyArray<JSONValue>;
 
 type ProfilingResult = {
+  track: number,
   endTime: number,
 };
 
@@ -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);
   }
 }
 
@@ -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.
@@ -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) {
@@ -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--) {
@@ -2790,6 +2833,7 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
           const startTime = startTimeInfo.time;
           logComponentRender(
             componentInfo,
+            trackIdx,
             startTime,
             endTime,
             childrenEndTime,
@@ -2798,7 +2842,8 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
       }
     }
   }
-  return (result.endTime = childrenEndTime);
+  result.endTime = childrenEndTime;
+  return result;
 }
 
 function processFullBinaryRow(

filename: packages/react-client/src/ReactFlightPerformanceTrack.js
patch: @@ -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,
@@ -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 =
@@ -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);