Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Log Server Component into Performance Track #31729

Merged
merged 4 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion fixtures/flight/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,26 @@ import {like, greet, increment} from './actions.js';
import {getServerState} from './ServerState.js';

const promisedText = new Promise(resolve =>
setTimeout(() => resolve('deferred text'), 100)
setTimeout(() => resolve('deferred text'), 50)
);

function Foo({children}) {
return <div>{children}</div>;
}

function Bar({children}) {
return <div>{children}</div>;
}

async function ServerComponent() {
await new Promise(resolve => setTimeout(() => resolve('deferred text'), 50));
}

export default async function App({prerender}) {
const res = await fetch('http://localhost:3001/todos');
const todos = await res.json();

const dedupedChild = <ServerComponent />;
return (
<html lang="en">
<head>
Expand Down Expand Up @@ -66,6 +80,8 @@ export default async function App({prerender}) {
</div>
<Client />
<Note />
<Foo>{dedupedChild}</Foo>
<Bar>{dedupedChild}</Bar>
</Container>
</body>
</html>
Expand Down
124 changes: 117 additions & 7 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ import {createBoundServerReference} from './ReactFlightReplyClient';

import {readTemporaryReference} from './ReactFlightTemporaryReferences';

import {logComponentRender} from './ReactFlightPerformanceTrack';

import {
REACT_LAZY_TYPE,
REACT_ELEMENT_TYPE,
Expand Down Expand Up @@ -124,6 +126,10 @@ export type JSONValue =
| {+[key: string]: JSONValue}
| $ReadOnlyArray<JSONValue>;

type ProfilingResult = {
endTime: number,
};

const ROW_ID = 0;
const ROW_TAG = 1;
const ROW_LENGTH = 2;
Expand All @@ -144,39 +150,44 @@ type PendingChunk<T> = {
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type BlockedChunk<T> = {
status: 'blocked',
value: null | Array<(T) => mixed>,
reason: null | Array<(mixed) => mixed>,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type ResolvedModelChunk<T> = {
status: 'resolved_model',
value: UninitializedModel,
reason: null,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type ResolvedModuleChunk<T> = {
status: 'resolved_module',
value: ClientReference<T>,
reason: null,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type InitializedChunk<T> = {
status: 'fulfilled',
value: T,
reason: null | FlightStreamController,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type InitializedStreamChunk<
Expand All @@ -186,15 +197,17 @@ type InitializedStreamChunk<
value: T,
reason: FlightStreamController,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void,
};
type ErroredChunk<T> = {
status: 'rejected',
value: null,
reason: mixed,
_response: Response,
_debugInfo?: null | ReactDebugInfo,
_children: Array<SomeChunk<any>> | ProfilingResult, // Profiling-only
_debugInfo?: null | ReactDebugInfo, // DEV-only
then(resolve: (T) => mixed, reject?: (mixed) => mixed): void,
};
type SomeChunk<T> =
Expand All @@ -216,6 +229,9 @@ function ReactPromise(
this.value = value;
this.reason = reason;
this._response = response;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
this._children = [];
}
if (__DEV__) {
this._debugInfo = null;
}
Expand Down Expand Up @@ -548,9 +564,11 @@ type InitializationHandler = {
errored: boolean,
};
let initializingHandler: null | InitializationHandler = null;
let initializingChunk: null | BlockedChunk<any> = null;

function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
const prevHandler = initializingHandler;
const prevChunk = initializingChunk;
initializingHandler = null;

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

if (enableProfilerTimer && enableComponentPerformanceTrack) {
initializingChunk = cyclicChunk;
}

try {
const value: T = parseModel(chunk._response, resolvedModel);
// Invoke any listeners added while resolving this model. I.e. cyclic
Expand Down Expand Up @@ -595,6 +617,9 @@ function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
erroredChunk.reason = error;
} finally {
initializingHandler = prevHandler;
if (enableProfilerTimer && enableComponentPerformanceTrack) {
initializingChunk = prevChunk;
}
}
}

Expand Down Expand Up @@ -622,6 +647,9 @@ export function reportGlobalError(response: Response, error: Error): void {
triggerErrorOnChunk(chunk, error);
}
});
if (enableProfilerTimer && enableComponentPerformanceTrack) {
flushComponentPerformance(getChunk(response, 0));
}
}

function nullRefGetter() {
Expand Down Expand Up @@ -1210,6 +1238,11 @@ function getOutlinedModel<T>(
const path = reference.split(':');
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (initializingChunk !== null && isArray(initializingChunk._children)) {
initializingChunk._children.push(chunk);
}
}
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk);
Expand Down Expand Up @@ -1359,6 +1392,14 @@ function parseModelString(
// Lazy node
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (
initializingChunk !== null &&
isArray(initializingChunk._children)
) {
initializingChunk._children.push(chunk);
}
}
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
Expand All @@ -1371,6 +1412,14 @@ function parseModelString(
}
const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
if (enableProfilerTimer && enableComponentPerformanceTrack) {
if (
initializingChunk !== null &&
isArray(initializingChunk._children)
) {
initializingChunk._children.push(chunk);
}
}
return chunk;
}
case 'S': {
Expand Down Expand Up @@ -2704,6 +2753,67 @@ function resolveTypedArray(
resolveBuffer(response, id, view);
}

function flushComponentPerformance(root: SomeChunk<any>): number {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
return 0;
}
// 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.
if (!isArray(root._children)) {
// We have already written this chunk. If this was a cycle, then this will
// be -Infinity and it won't contribute to the parent end time.
// If this was already emitted by another sibling then we reused the same
// 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;
}
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);
}
const result: ProfilingResult = {endTime: -Infinity};
root._children = result;
let childrenEndTime = -Infinity;
for (let i = 0; i < children.length; i++) {
const childEndTime = flushComponentPerformance(children[i]);
if (childEndTime > childrenEndTime) {
childrenEndTime = childEndTime;
}
}
const debugInfo = root._debugInfo;
if (debugInfo) {
let endTime = 0;
for (let i = debugInfo.length - 1; i >= 0; i--) {
const info = debugInfo[i];
if (typeof info.time === 'number') {
endTime = info.time;
if (endTime > childrenEndTime) {
childrenEndTime = endTime;
}
}
if (typeof info.name === 'string' && i > 0) {
// $FlowFixMe: Refined.
const componentInfo: ReactComponentInfo = info;
const startTimeInfo = debugInfo[i - 1];
if (typeof startTimeInfo.time === 'number') {
const startTime = startTimeInfo.time;
logComponentRender(
componentInfo,
startTime,
endTime,
childrenEndTime,
);
}
}
}
}
return (result.endTime = childrenEndTime);
}

function processFullBinaryRow(
response: Response,
id: number,
Expand Down
56 changes: 56 additions & 0 deletions packages/react-client/src/ReactFlightPerformanceTrack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {ReactComponentInfo} from 'shared/ReactTypes';

import {enableProfilerTimer} from 'shared/ReactFeatureFlags';

const supportsUserTiming =
enableProfilerTimer &&
typeof performance !== 'undefined' &&
// $FlowFixMe[method-unbinding]
typeof performance.measure === 'function';

const COMPONENTS_TRACK = 'Server Components ⚛';

// Reused to avoid thrashing the GC.
const reusableComponentDevToolDetails = {
color: 'primary',
track: COMPONENTS_TRACK,
};
const reusableComponentOptions = {
start: -0,
end: -0,
detail: {
devtools: reusableComponentDevToolDetails,
},
};

export function logComponentRender(
componentInfo: ReactComponentInfo,
startTime: number,
endTime: number,
childrenEndTime: number,
): void {
if (supportsUserTiming && childrenEndTime >= 0) {
const name = componentInfo.name;
const selfTime = endTime - startTime;
reusableComponentDevToolDetails.color =
selfTime < 0.5
? 'primary-light'
: selfTime < 50
? 'primary'
: selfTime < 500
? 'primary-dark'
: 'error';
reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
reusableComponentOptions.end = childrenEndTime;
performance.measure(name, reusableComponentOptions);
}
}
Loading