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); + } +}