From 6928bf2f7c7375ed9038a7b30d5b4760bb57055a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 12 Dec 2024 14:03:18 -0500 Subject: [PATCH 1/5] [Flight] Log Server Component into Performance Track (#31729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2024-12-10 at 10 49 19 PM 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. --- fixtures/flight/src/App.js | 18 ++- .../react-client/src/ReactFlightClient.js | 124 +++++++++++++++++- .../src/ReactFlightPerformanceTrack.js | 56 ++++++++ 3 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 packages/react-client/src/ReactFlightPerformanceTrack.js diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 08987750eb210..49bfc9e05135c 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -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
{children}
; +} + +function Bar({children}) { + return
{children}
; +} + +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 = ; return ( @@ -66,6 +80,8 @@ export default async function App({prerender}) { + {dedupedChild} + {dedupedChild} diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 19b1285ad9732..4d5204ab00220 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -71,6 +71,8 @@ import {createBoundServerReference} from './ReactFlightReplyClient'; import {readTemporaryReference} from './ReactFlightTemporaryReferences'; +import {logComponentRender} from './ReactFlightPerformanceTrack'; + import { REACT_LAZY_TYPE, REACT_ELEMENT_TYPE, @@ -124,6 +126,10 @@ export type JSONValue = | {+[key: string]: JSONValue} | $ReadOnlyArray; +type ProfilingResult = { + endTime: number, +}; + const ROW_ID = 0; const ROW_TAG = 1; const ROW_LENGTH = 2; @@ -144,7 +150,8 @@ type PendingChunk = { value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type BlockedChunk = { @@ -152,7 +159,8 @@ type BlockedChunk = { value: null | Array<(T) => mixed>, reason: null | Array<(mixed) => mixed>, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModelChunk = { @@ -160,7 +168,8 @@ type ResolvedModelChunk = { value: UninitializedModel, reason: null, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModuleChunk = { @@ -168,7 +177,8 @@ type ResolvedModuleChunk = { value: ClientReference, reason: null, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedChunk = { @@ -176,7 +186,8 @@ type InitializedChunk = { value: T, reason: null | FlightStreamController, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedStreamChunk< @@ -186,7 +197,8 @@ type InitializedStreamChunk< value: T, reason: FlightStreamController, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, }; type ErroredChunk = { @@ -194,7 +206,8 @@ type ErroredChunk = { value: null, reason: mixed, _response: Response, - _debugInfo?: null | ReactDebugInfo, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type SomeChunk = @@ -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; } @@ -548,9 +564,11 @@ type InitializationHandler = { errored: boolean, }; let initializingHandler: null | InitializationHandler = null; +let initializingChunk: null | BlockedChunk = null; function initializeModelChunk(chunk: ResolvedModelChunk): void { const prevHandler = initializingHandler; + const prevChunk = initializingChunk; initializingHandler = null; const resolvedModel = chunk.value; @@ -563,6 +581,10 @@ function initializeModelChunk(chunk: ResolvedModelChunk): 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 @@ -595,6 +617,9 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { erroredChunk.reason = error; } finally { initializingHandler = prevHandler; + if (enableProfilerTimer && enableComponentPerformanceTrack) { + initializingChunk = prevChunk; + } } } @@ -622,6 +647,9 @@ export function reportGlobalError(response: Response, error: Error): void { triggerErrorOnChunk(chunk, error); } }); + if (enableProfilerTimer && enableComponentPerformanceTrack) { + flushComponentPerformance(getChunk(response, 0)); + } } function nullRefGetter() { @@ -1210,6 +1238,11 @@ function getOutlinedModel( 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); @@ -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); @@ -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': { @@ -2704,6 +2753,67 @@ function resolveTypedArray( resolveBuffer(response, id, view); } +function flushComponentPerformance(root: SomeChunk): 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, diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js new file mode 100644 index 0000000000000..f1e7b30280722 --- /dev/null +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -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); + } +} From c86542b240375f95dfd14af5b8ec39e29881e74d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 12 Dec 2024 14:10:46 -0500 Subject: [PATCH 2/5] Bump next prerelease version numbers (#31676) Updates the version numbers in the prerelease (canary and experimental) channels. --------- Co-authored-by: Jack Pope --- ReactVersions.js | 20 +++++++++---------- .../eslint-plugin-react-hooks/package.json | 2 +- packages/jest-react/package.json | 2 +- packages/react-art/package.json | 6 +++--- packages/react-dom-bindings/package.json | 2 +- packages/react-dom/package.json | 6 +++--- packages/react-is/package.json | 2 +- packages/react-markup/package.json | 2 +- packages/react-native-renderer/package.json | 8 ++++---- packages/react-reconciler/package.json | 6 +++--- packages/react-refresh/package.json | 2 +- packages/react-server-dom-esm/package.json | 2 +- packages/react-server-dom-fb/package.json | 8 ++++---- .../react-server-dom-turbopack/package.json | 6 +++--- .../react-server-dom-webpack/package.json | 6 +++--- packages/react-test-renderer/package.json | 8 ++++---- packages/react/package.json | 2 +- packages/scheduler/package.json | 4 ++-- packages/shared/ReactVersion.js | 2 +- packages/use-subscription/package.json | 4 ++-- packages/use-sync-external-store/package.json | 2 +- yarn.lock | 3 ++- 22 files changed, 53 insertions(+), 52 deletions(-) diff --git a/ReactVersions.js b/ReactVersions.js index 306ef03dc934d..e4ce043171f79 100644 --- a/ReactVersions.js +++ b/ReactVersions.js @@ -7,18 +7,18 @@ // // The @latest channel uses the version as-is, e.g.: // -// 19.0.0 +// 19.1.0 // // The @canary channel appends additional information, with the scheme // -