From 031230d2e047c1a354037795a5790ed421d51c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 16 Dec 2024 11:58:25 -0500 Subject: [PATCH] [Flight] Stack Parallel Components in Separate Tracks (#31735) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on https://github.com/facebook/react/pull/31729 Screenshot 2024-12-11 at 3 36 41 PM 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. Screenshot 2024-12-11 at 4 31 17 PM 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. --- .../react-client/src/ReactFlightClient.js | 61 ++++++++++++++++--- .../src/ReactFlightPerformanceTrack.js | 20 +++++- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 0e2bb5b9f4606..0456136a7e6cf 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -125,6 +125,7 @@ export type JSONValue = | $ReadOnlyArray; 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): number { +function flushComponentPerformance( + root: SomeChunk, + 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): 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): 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): number { const startTime = startTimeInfo.time; logComponentRender( componentInfo, + trackIdx, startTime, endTime, childrenEndTime, @@ -2798,7 +2842,8 @@ function flushComponentPerformance(root: SomeChunk): number { } } } - return (result.endTime = childrenEndTime); + result.endTime = childrenEndTime; + return result; } function processFullBinaryRow( diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index f1e7b30280722..b8bd9ca2be935 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -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);