Repository: facebook/react
Pull request: [Flight] Flush performance track once we have no more pending chunks

Stacked on #33718. Alternative to #33716.

The issue with flushing the Server Components track in its current form is that we need to decide how long to wait before flushing whatever we have. That's because the root's end time will be determined by the end time of that last child.

However, if a child isn't actually used then we don't necessarily need to include it in the Server Components track since it wasn't blocking the initial render.

This waits for 100ms after the last pending chunk is resolved and if nothing is invoking any more lazy initializers after that then we log the Server Components track with the information we have at that point. We also don't eagerly initialize any chunks that wasn't already initialized so if nothing was rendered, then nothing will be logged.

This is somewhat an artifact of the current visualization. If we did another transposed form we wouldn't necessarily need to wait until the end and can log things as they're discovered.

Changed files:
filename: packages/react-client/src/ReactFlightClient.js
patch: @@ -351,6 +351,7 @@ type Response = {
   _closedReason: mixed,
   _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
   _timeOrigin: number, // Profiling-only
+  _pendingInitialRender: null | TimeoutID, // Profiling-only,
   _pendingChunks: number, // DEV-only
   _weakResponse: WeakResponse, // DEV-only
   _debugRootOwner?: null | ReactComponentInfo, // DEV-only
@@ -444,8 +445,13 @@ export function getRoot<T>(weakResponse: WeakResponse): Thenable<T> {
 function createPendingChunk<T>(response: Response): PendingChunk<T> {
   if (__DEV__) {
     // Retain a strong reference to the Response while we wait for the result.
-    response._pendingChunks++;
-    response._weakResponse.response = response;
+    if (response._pendingChunks++ === 0) {
+      response._weakResponse.response = response;
+      if (response._pendingInitialRender !== null) {
+        clearTimeout(response._pendingInitialRender);
+        response._pendingInitialRender = null;
+      }
+    }
   }
   // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors
   return new ReactPromise(PENDING, null, null);
@@ -457,6 +463,14 @@ function releasePendingChunk(response: Response, chunk: SomeChunk<any>): void {
       // We're no longer waiting for any more chunks. We can release the strong reference
       // to the response. We'll regain it if we ask for any more data later on.
       response._weakResponse.response = null;
+      // Wait a short period to see if any more chunks get asked for. E.g. by a React render.
+      // These chunks might discover more pending chunks.
+      // If we don't ask for more then we assume that those chunks weren't blocking initial
+      // render and are excluded from the performance track.
+      response._pendingInitialRender = setTimeout(
+        flushInitialRenderPerformance.bind(null, response),
+        100,
+      );
     }
   }
 }
@@ -868,18 +882,6 @@ export function reportGlobalError(
       response._debugChannel = undefined;
     }
   }
-  if (enableProfilerTimer && enableComponentPerformanceTrack) {
-    if (response._replayConsole) {
-      markAllTracksInOrder();
-      flushComponentPerformance(
-        response,
-        getChunk(response, 0),
-        0,
-        -Infinity,
-        -Infinity,
-      );
-    }
-  }
 }
 
 function nullRefGetter() {
@@ -2105,6 +2107,7 @@ function ResponseInstance(
   this._tempRefs = temporaryReferences;
   if (enableProfilerTimer && enableComponentPerformanceTrack) {
     this._timeOrigin = 0;
+    this._pendingInitialRender = null;
   }
   if (__DEV__) {
     this._pendingChunks = 0;
@@ -3491,12 +3494,6 @@ function flushComponentPerformance(
     return previousResult;
   }
   const children = root._children;
-  if (root.status === RESOLVED_MODEL) {
-    // If the model is not initialized by now, do that now so we can find its
-    // children. This part is a little sketchy since it significantly changes
-    // the performance characteristics of the app by profiling.
-    initializeModelChunk(root);
-  }
 
   // First find the start time of the first component to know if it was running
   // in parallel with the previous.
@@ -3703,6 +3700,20 @@ function flushComponentPerformance(
   return result;
 }
 
+function flushInitialRenderPerformance(response: Response): void {
+  if (
+    enableProfilerTimer &&
+    enableComponentPerformanceTrack &&
+    response._replayConsole
+  ) {
+    const rootChunk = getChunk(response, 0);
+    if (isArray(rootChunk._children)) {
+      markAllTracksInOrder();
+      flushComponentPerformance(response, rootChunk, 0, -Infinity, -Infinity);
+    }
+  }
+}
+
 function processFullBinaryRow(
   response: Response,
   id: number,