Skip to content

Commit 6928bf2

Browse files
authored
[Flight] Log Server Component into Performance Track (#31729)
<img width="966" alt="Screenshot 2024-12-10 at 10 49 19 PM" src="https://github.com/user-attachments/assets/27a21bdf-86b9-4203-893b-89523e698138"> This emits a tree view visualization of the timing information for each Server Component provided in the RSC payload. The unique thing about this visualization is that the end time of each Server Component spans the end of the last child. Now what is conceptually a blocking child is kind of undefined in RSC. E.g. if you're not using a Promise on the client, or if it is wrapped in Suspense, is it really blocking the parent? Here I reconstruct parent-child relationship by which chunks reference other chunks. A child can belong to more than one parent like when we dedupe the result of a Server Component. Then I wait until the whole RSC payload has streamed in, and then I traverse the tree collecting the end time from children as I go and emit the `performance.measure()` calls on the way up. There's more work for this visualization in follow ups but this is the basics. For example, since the Server Component time span includes async work it's possible for siblings to execute their span in parallel (Foo and Bar in the screenshot are parallel siblings). To deal with this we need to spawn parallel work into separate tracks. Each one can be deep due to large trees. This can makes this type of visualization unwieldy when you have a lot of parallelism. Therefore I also plan another flatter Timeline visualization in a follow up.
1 parent ca58742 commit 6928bf2

File tree

3 files changed

+190
-8
lines changed

3 files changed

+190
-8
lines changed

fixtures/flight/src/App.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,26 @@ import {like, greet, increment} from './actions.js';
2020
import {getServerState} from './ServerState.js';
2121

2222
const promisedText = new Promise(resolve =>
23-
setTimeout(() => resolve('deferred text'), 100)
23+
setTimeout(() => resolve('deferred text'), 50)
2424
);
2525

26+
function Foo({children}) {
27+
return <div>{children}</div>;
28+
}
29+
30+
function Bar({children}) {
31+
return <div>{children}</div>;
32+
}
33+
34+
async function ServerComponent() {
35+
await new Promise(resolve => setTimeout(() => resolve('deferred text'), 50));
36+
}
37+
2638
export default async function App({prerender}) {
2739
const res = await fetch('http://localhost:3001/todos');
2840
const todos = await res.json();
41+
42+
const dedupedChild = <ServerComponent />;
2943
return (
3044
<html lang="en">
3145
<head>
@@ -66,6 +80,8 @@ export default async function App({prerender}) {
6680
</div>
6781
<Client />
6882
<Note />
83+
<Foo>{dedupedChild}</Foo>
84+
<Bar>{dedupedChild}</Bar>
6985
</Container>
7086
</body>
7187
</html>

packages/react-client/src/ReactFlightClient.js

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import {createBoundServerReference} from './ReactFlightReplyClient';
7171

7272
import {readTemporaryReference} from './ReactFlightTemporaryReferences';
7373

74+
import {logComponentRender} from './ReactFlightPerformanceTrack';
75+
7476
import {
7577
REACT_LAZY_TYPE,
7678
REACT_ELEMENT_TYPE,
@@ -124,6 +126,10 @@ export type JSONValue =
124126
| {+[key: string]: JSONValue}
125127
| $ReadOnlyArray<JSONValue>;
126128

129+
type ProfilingResult = {
130+
endTime: number,
131+
};
132+
127133
const ROW_ID = 0;
128134
const ROW_TAG = 1;
129135
const ROW_LENGTH = 2;
@@ -144,39 +150,44 @@ type PendingChunk<T> = {
144150
value: null | Array<(T) => mixed>,
145151
reason: null | Array<(mixed) => mixed>,
146152
_response: Response,
147-
_debugInfo?: null | ReactDebugInfo,
153+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
154+
_debugInfo?: null | ReactDebugInfo, // DEV-only
148155
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
149156
};
150157
type BlockedChunk<T> = {
151158
status: 'blocked',
152159
value: null | Array<(T) => mixed>,
153160
reason: null | Array<(mixed) => mixed>,
154161
_response: Response,
155-
_debugInfo?: null | ReactDebugInfo,
162+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
163+
_debugInfo?: null | ReactDebugInfo, // DEV-only
156164
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
157165
};
158166
type ResolvedModelChunk<T> = {
159167
status: 'resolved_model',
160168
value: UninitializedModel,
161169
reason: null,
162170
_response: Response,
163-
_debugInfo?: null | ReactDebugInfo,
171+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
172+
_debugInfo?: null | ReactDebugInfo, // DEV-only
164173
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
165174
};
166175
type ResolvedModuleChunk<T> = {
167176
status: 'resolved_module',
168177
value: ClientReference<T>,
169178
reason: null,
170179
_response: Response,
171-
_debugInfo?: null | ReactDebugInfo,
180+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
181+
_debugInfo?: null | ReactDebugInfo, // DEV-only
172182
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
173183
};
174184
type InitializedChunk<T> = {
175185
status: 'fulfilled',
176186
value: T,
177187
reason: null | FlightStreamController,
178188
_response: Response,
179-
_debugInfo?: null | ReactDebugInfo,
189+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
190+
_debugInfo?: null | ReactDebugInfo, // DEV-only
180191
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
181192
};
182193
type InitializedStreamChunk<
@@ -186,15 +197,17 @@ type InitializedStreamChunk<
186197
value: T,
187198
reason: FlightStreamController,
188199
_response: Response,
189-
_debugInfo?: null | ReactDebugInfo,
200+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
201+
_debugInfo?: null | ReactDebugInfo, // DEV-only
190202
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
191203
};
192204
type ErroredChunk<T> = {
193205
status: 'rejected',
194206
value: null,
195207
reason: mixed,
196208
_response: Response,
197-
_debugInfo?: null | ReactDebugInfo,
209+
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
210+
_debugInfo?: null | ReactDebugInfo, // DEV-only
198211
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
199212
};
200213
type SomeChunk<T> =
@@ -216,6 +229,9 @@ function ReactPromise(
216229
this.value = value;
217230
this.reason = reason;
218231
this._response = response;
232+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
233+
this._children = [];
234+
}
219235
if (__DEV__) {
220236
this._debugInfo = null;
221237
}
@@ -548,9 +564,11 @@ type InitializationHandler = {
548564
errored: boolean,
549565
};
550566
let initializingHandler: null | InitializationHandler = null;
567+
let initializingChunk: null | BlockedChunk<any> = null;
551568

552569
function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
553570
const prevHandler = initializingHandler;
571+
const prevChunk = initializingChunk;
554572
initializingHandler = null;
555573

556574
const resolvedModel = chunk.value;
@@ -563,6 +581,10 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
563581
cyclicChunk.value = null;
564582
cyclicChunk.reason = null;
565583

584+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
585+
initializingChunk = cyclicChunk;
586+
}
587+
566588
try {
567589
const value: T = parseModel(chunk._response, resolvedModel);
568590
// Invoke any listeners added while resolving this model. I.e. cyclic
@@ -595,6 +617,9 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
595617
erroredChunk.reason = error;
596618
} finally {
597619
initializingHandler = prevHandler;
620+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
621+
initializingChunk = prevChunk;
622+
}
598623
}
599624
}
600625

@@ -622,6 +647,9 @@ export function reportGlobalError(response: Response, error: Error): void {
622647
triggerErrorOnChunk(chunk, error);
623648
}
624649
});
650+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
651+
flushComponentPerformance(getChunk(response, 0));
652+
}
625653
}
626654

627655
function nullRefGetter() {
@@ -1210,6 +1238,11 @@ function getOutlinedModel<T>(
12101238
const path = reference.split(':');
12111239
const id = parseInt(path[0], 16);
12121240
const chunk = getChunk(response, id);
1241+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1242+
if (initializingChunk !== null && isArray(initializingChunk._children)) {
1243+
initializingChunk._children.push(chunk);
1244+
}
1245+
}
12131246
switch (chunk.status) {
12141247
case RESOLVED_MODEL:
12151248
initializeModelChunk(chunk);
@@ -1359,6 +1392,14 @@ function parseModelString(
13591392
// Lazy node
13601393
const id = parseInt(value.slice(2), 16);
13611394
const chunk = getChunk(response, id);
1395+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1396+
if (
1397+
initializingChunk !== null &&
1398+
isArray(initializingChunk._children)
1399+
) {
1400+
initializingChunk._children.push(chunk);
1401+
}
1402+
}
13621403
// We create a React.lazy wrapper around any lazy values.
13631404
// When passed into React, we'll know how to suspend on this.
13641405
return createLazyChunkWrapper(chunk);
@@ -1371,6 +1412,14 @@ function parseModelString(
13711412
}
13721413
const id = parseInt(value.slice(2), 16);
13731414
const chunk = getChunk(response, id);
1415+
if (enableProfilerTimer && enableComponentPerformanceTrack) {
1416+
if (
1417+
initializingChunk !== null &&
1418+
isArray(initializingChunk._children)
1419+
) {
1420+
initializingChunk._children.push(chunk);
1421+
}
1422+
}
13741423
return chunk;
13751424
}
13761425
case 'S': {
@@ -2704,6 +2753,67 @@ function resolveTypedArray(
27042753
resolveBuffer(response, id, view);
27052754
}
27062755

2756+
function flushComponentPerformance(root: SomeChunk<any>): number {
2757+
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
2758+
return 0;
2759+
}
2760+
// Write performance.measure() entries for Server Components in tree order.
2761+
// This must be done at the end to collect the end time from the whole tree.
2762+
if (!isArray(root._children)) {
2763+
// We have already written this chunk. If this was a cycle, then this will
2764+
// be -Infinity and it won't contribute to the parent end time.
2765+
// If this was already emitted by another sibling then we reused the same
2766+
// chunk in two places. We should extend the current end time as if it was
2767+
// rendered as part of this tree.
2768+
const previousResult: ProfilingResult = root._children;
2769+
return previousResult.endTime;
2770+
}
2771+
const children = root._children;
2772+
if (root.status === RESOLVED_MODEL) {
2773+
// If the model is not initialized by now, do that now so we can find its
2774+
// children. This part is a little sketchy since it significantly changes
2775+
// the performance characteristics of the app by profiling.
2776+
initializeModelChunk(root);
2777+
}
2778+
const result: ProfilingResult = {endTime: -Infinity};
2779+
root._children = result;
2780+
let childrenEndTime = -Infinity;
2781+
for (let i = 0; i < children.length; i++) {
2782+
const childEndTime = flushComponentPerformance(children[i]);
2783+
if (childEndTime > childrenEndTime) {
2784+
childrenEndTime = childEndTime;
2785+
}
2786+
}
2787+
const debugInfo = root._debugInfo;
2788+
if (debugInfo) {
2789+
let endTime = 0;
2790+
for (let i = debugInfo.length - 1; i >= 0; i--) {
2791+
const info = debugInfo[i];
2792+
if (typeof info.time === 'number') {
2793+
endTime = info.time;
2794+
if (endTime > childrenEndTime) {
2795+
childrenEndTime = endTime;
2796+
}
2797+
}
2798+
if (typeof info.name === 'string' && i > 0) {
2799+
// $FlowFixMe: Refined.
2800+
const componentInfo: ReactComponentInfo = info;
2801+
const startTimeInfo = debugInfo[i - 1];
2802+
if (typeof startTimeInfo.time === 'number') {
2803+
const startTime = startTimeInfo.time;
2804+
logComponentRender(
2805+
componentInfo,
2806+
startTime,
2807+
endTime,
2808+
childrenEndTime,
2809+
);
2810+
}
2811+
}
2812+
}
2813+
}
2814+
return (result.endTime = childrenEndTime);
2815+
}
2816+
27072817
function processFullBinaryRow(
27082818
response: Response,
27092819
id: number,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ReactComponentInfo} from 'shared/ReactTypes';
11+
12+
import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
13+
14+
const supportsUserTiming =
15+
enableProfilerTimer &&
16+
typeof performance !== 'undefined' &&
17+
// $FlowFixMe[method-unbinding]
18+
typeof performance.measure === 'function';
19+
20+
const COMPONENTS_TRACK = 'Server Components ⚛';
21+
22+
// Reused to avoid thrashing the GC.
23+
const reusableComponentDevToolDetails = {
24+
color: 'primary',
25+
track: COMPONENTS_TRACK,
26+
};
27+
const reusableComponentOptions = {
28+
start: -0,
29+
end: -0,
30+
detail: {
31+
devtools: reusableComponentDevToolDetails,
32+
},
33+
};
34+
35+
export function logComponentRender(
36+
componentInfo: ReactComponentInfo,
37+
startTime: number,
38+
endTime: number,
39+
childrenEndTime: number,
40+
): void {
41+
if (supportsUserTiming && childrenEndTime >= 0) {
42+
const name = componentInfo.name;
43+
const selfTime = endTime - startTime;
44+
reusableComponentDevToolDetails.color =
45+
selfTime < 0.5
46+
? 'primary-light'
47+
: selfTime < 50
48+
? 'primary'
49+
: selfTime < 500
50+
? 'primary-dark'
51+
: 'error';
52+
reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
53+
reusableComponentOptions.end = childrenEndTime;
54+
performance.measure(name, reusableComponentOptions);
55+
}
56+
}

0 commit comments

Comments
 (0)