@samitouri / QOS-React-2 / commits / 031230d2e0

[Flight] Stack Parallel Components in Separate Tracks (#31735)

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.

Sebastian Markbåge committed Dec 16, 2024 at 11:58 UTC 031230d2e047c1a354037795a5790ed421d51c55
2 files changed +71 -10
packages/react-client/src/ReactFlightClient.js
+53 -8
@@ -125,6 +125,7 @@ export type JSONValue =
125 | $ReadOnlyArray<JSONValue>;
126
127 type ProfilingResult = {
128 + track: number,
129 endTime: number,
130 };
131
@@ -642,7 +643,7 @@ export function reportGlobalError(response: Response, error: Error): void {
643 }
644 });
645 if (enableProfilerTimer && enableComponentPerformanceTrack) {
645 - flushComponentPerformance(getChunk(response, 0));
646 + flushComponentPerformance(getChunk(response, 0), 0, -Infinity);
647 }
648 }
649
@@ -2740,9 +2741,16 @@ function resolveTypedArray(
2741 resolveBuffer(response, id, view);
2742 }
2743
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 {
2749 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 + );
2754 }
2755 // Write performance.measure() entries for Server Components in tree order.
2756 // 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 {
2761 // chunk in two places. We should extend the current end time as if it was
2762 // rendered as part of this tree.
2763 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;
2767 }
2768 const children = root._children;
2769 if (root.status === RESOLVED_MODEL) {
@@ -2762,16 +2772,49 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
2772 // the performance characteristics of the app by profiling.
2773 initializeModelChunk(root);
2774 }
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};
2800 root._children = result;
2801 let childrenEndTime = -Infinity;
2802 + let childTrackIdx = trackIdx;
2803 + let childTrackTime = trackTime;
2804 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;
2813 if (childEndTime > childrenEndTime) {
2814 childrenEndTime = childEndTime;
2815 }
2816 }
2774 - const debugInfo = root._debugInfo;
2817 +
2818 if (debugInfo) {
2819 let endTime = 0;
2820 for (let i = debugInfo.length - 1; i >= 0; i--) {
@@ -2790,6 +2833,7 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
2833 const startTime = startTimeInfo.time;
2834 logComponentRender(
2835 componentInfo,
2836 + trackIdx,
2837 startTime,
2838 endTime,
2839 childrenEndTime,
@@ -2798,7 +2842,8 @@ function flushComponentPerformance(root: SomeChunk<any>): number {
2842 }
2843 }
2844 }
2801 - return (result.endTime = childrenEndTime);
2845 + result.endTime = childrenEndTime;
2846 + return result;
2847 }
2848
2849 function processFullBinaryRow(
packages/react-client/src/ReactFlightPerformanceTrack.js
+18 -2
@@ -22,7 +22,8 @@ const COMPONENTS_TRACK = 'Server Components ⚛';
22 // Reused to avoid thrashing the GC.
23 const reusableComponentDevToolDetails = {
24 color: 'primary',
25 - track: COMPONENTS_TRACK,
25 + track: '',
26 + trackGroup: COMPONENTS_TRACK,
27 };
28 const reusableComponentOptions = {
29 start: -0,
@@ -32,13 +33,27 @@ const reusableComponentOptions = {
33 },
34 };
35
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 +
49 export function logComponentRender(
50 componentInfo: ReactComponentInfo,
51 + trackIdx: number,
52 startTime: number,
53 endTime: number,
54 childrenEndTime: number,
55 ): void {
41 - if (supportsUserTiming && childrenEndTime >= 0) {
56 + if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
57 const name = componentInfo.name;
58 const selfTime = endTime - startTime;
59 reusableComponentDevToolDetails.color =
@@ -49,6 +64,7 @@ export function logComponentRender(
64 : selfTime < 500
65 ? 'primary-dark'
66 : 'error';
67 + reusableComponentDevToolDetails.track = trackNames[trackIdx];
68 reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
69 reusableComponentOptions.end = childrenEndTime;
70 performance.measure(name, reusableComponentOptions);