From 372ec00c0384cd2089651154ea7c67693ee3f2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 9 Dec 2024 19:47:43 -0500 Subject: [PATCH] Update ReactDebugInfo types to declare timing info separately (#31714) This clarifies a few things by ensuring that there is always at least one required field. This can be used to refine the object to one of the specific types. However, it's probably just a matter of time until we make this tagged unions instead. E.g. it would be nice to rename the `name` field `ReactComponentInfo` to `type` and tag it with the React Element symbol because then it's just the same as a React Element. I also extract a time field. The idea is that this will advance (or rewind) the time to the new timestamp and then anything below would be defined as happening within that time stamp. E.g. to model the start and end for a server component you'd do something like: ``` [ {time: 123}, {name: 'Component', ... }, {time: 124}, ] ``` The reason this needs to be in the `ReactDebugInfo` is so that timing information from one environment gets transferred into the next environment. It lets you take a Promise from one world and transfer it into another world and its timing information is preserved without everything else being preserved. I've gone back and forth on if this should be part of each other Info object like `ReactComponentInfo` but since those can be deduped and can change formats (e.g. this should really just be a React Element) it's better to store this separately. The time format is relative to a `timeOrigin` which is the current environment's `timeOrigin`. When it's serialized between environments this needs to be considered. Emitting these timings is not yet implemented in this PR. --------- Co-authored-by: eps1lon --- .../react-client/src/ReactFlightClient.js | 49 ++++++++++++++----- .../react-server/src/ReactFlightServer.js | 8 ++- packages/shared/ReactTypes.js | 20 ++++++-- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 9a3d3e0198671..b1479a076a1d9 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -11,7 +11,9 @@ import type { Thenable, ReactDebugInfo, ReactComponentInfo, + ReactEnvironmentInfo, ReactAsyncInfo, + ReactTimeInfo, ReactStackTrace, ReactCallSite, } from 'shared/ReactTypes'; @@ -2460,7 +2462,6 @@ function initializeFakeStack( const stack = debugInfo.stack; const env = debugInfo.env == null ? '' : debugInfo.env; // $FlowFixMe[cannot-write] - // $FlowFixMe[prop-missing] debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env); } if (debugInfo.owner != null) { @@ -2472,7 +2473,11 @@ function initializeFakeStack( function resolveDebugInfo( response: Response, id: number, - debugInfo: ReactComponentInfo | ReactAsyncInfo, + debugInfo: + | ReactComponentInfo + | ReactEnvironmentInfo + | ReactAsyncInfo + | ReactTimeInfo, ): void { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json @@ -2486,16 +2491,26 @@ function resolveDebugInfo( // to initialize it when we need it, we might be inside user code. const env = debugInfo.env === undefined ? response._rootEnvironmentName : debugInfo.env; - initializeFakeTask(response, debugInfo, env); + if (debugInfo.stack !== undefined) { + const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = + // $FlowFixMe[incompatible-type] + debugInfo; + initializeFakeTask(response, componentInfoOrAsyncInfo, env); + } if (debugInfo.owner === null && response._debugRootOwner != null) { - // $FlowFixMe - debugInfo.owner = response._debugRootOwner; + // $FlowFixMe[prop-missing] By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo` + const componentInfo: ReactComponentInfo = debugInfo; + // $FlowFixMe[cannot-write] + componentInfo.owner = response._debugRootOwner; // We override the stack if we override the owner since the stack where the root JSX // was created on the server isn't very useful but where the request was made is. - // $FlowFixMe - debugInfo.debugStack = response._debugRootStack; - } else { - initializeFakeStack(response, debugInfo); + // $FlowFixMe[cannot-write] + componentInfo.debugStack = response._debugRootStack; + } else if (debugInfo.stack !== undefined) { + const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = + // $FlowFixMe[incompatible-type] + debugInfo; + initializeFakeStack(response, componentInfoOrAsyncInfo); } const chunk = getChunk(response, id); @@ -2779,11 +2794,19 @@ function processFullStringRow( } case 68 /* "D" */: { if (__DEV__) { - const chunk: ResolvedModelChunk = - createResolvedModelChunk(response, row); + const chunk: ResolvedModelChunk< + | ReactComponentInfo + | ReactEnvironmentInfo + | ReactAsyncInfo + | ReactTimeInfo, + > = createResolvedModelChunk(response, row); initializeModelChunk(chunk); - const initializedChunk: SomeChunk = - chunk; + const initializedChunk: SomeChunk< + | ReactComponentInfo + | ReactEnvironmentInfo + | ReactAsyncInfo + | ReactTimeInfo, + > = chunk; if (initializedChunk.status === INITIALIZED) { resolveDebugInfo(response, id, initializedChunk.value); } else { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 94eaf1e20e6e8..0c330d0b8ce3e 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -61,7 +61,9 @@ import type { RejectedThenable, ReactDebugInfo, ReactComponentInfo, + ReactEnvironmentInfo, ReactAsyncInfo, + ReactTimeInfo, ReactStackTrace, ReactCallSite, } from 'shared/ReactTypes'; @@ -3267,7 +3269,11 @@ function emitModelChunk(request: Request, id: number, json: string): void { function emitDebugChunk( request: Request, id: number, - debugInfo: ReactComponentInfo | ReactAsyncInfo, + debugInfo: + | ReactComponentInfo + | ReactAsyncInfo + | ReactEnvironmentInfo + | ReactTimeInfo, ): void { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 54eccd5538dd8..26cd57fc96f01 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -188,7 +188,7 @@ export type ReactCallSite = [ export type ReactStackTrace = Array; export type ReactComponentInfo = { - +name?: string, + +name: string, +env?: string, +key?: null | string, +owner?: null | ReactComponentInfo, @@ -199,10 +199,22 @@ export type ReactComponentInfo = { +debugTask?: null | ConsoleTask, }; +export type ReactEnvironmentInfo = { + +env: string, +}; + export type ReactAsyncInfo = { - +started?: number, - +completed?: number, + +type: string, + // Stashed Data for the Specific Execution Environment. Not part of the transport protocol + +debugStack?: null | Error, + +debugTask?: null | ConsoleTask, +stack?: null | ReactStackTrace, }; -export type ReactDebugInfo = Array; +export type ReactTimeInfo = { + +time: number, // performance.now +}; + +export type ReactDebugInfo = Array< + ReactComponentInfo | ReactEnvironmentInfo | ReactAsyncInfo | ReactTimeInfo, +>;