From d4ac7689f94f8ed53b779a651d62a2b9af20e6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 28 Dec 2024 02:01:49 -0500 Subject: [PATCH 1/2] Add Profiler mode to fixtures even if React DevTools is not installed (#31877) Currently you need to do one of either: 1. Install React DevTools 2. Install React Refresh 3. Add Profiler component To opt in to component level profiling. It was a bit confusing that some of the fixtures was doing 2 which made them work while other was depending on if you had DevTools. Really React Refresh shouldn't really opt you in I think. --- fixtures/flight/src/index.js | 24 +++++++++++++++--------- fixtures/ssr/src/index.js | 8 +++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index 755551047535b..f08f7a110bf61 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import {use, Suspense, useState, startTransition} from 'react'; +import {use, Suspense, useState, startTransition, Profiler} from 'react'; import ReactDOM from 'react-dom/client'; import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client'; @@ -54,14 +54,20 @@ async function hydrateApp() { } ); - ReactDOM.hydrateRoot(document, , { - // TODO: This part doesn't actually work because the server only returns - // form state during the request that submitted the form. Which means it - // the state needs to be transported as part of the HTML stream. We intend - // to add a feature to Fizz for this, but for now it's up to the - // metaframework to implement correctly. - formState: formState, - }); + ReactDOM.hydrateRoot( + document, + + + , + { + // TODO: This part doesn't actually work because the server only returns + // form state during the request that submitted the form. Which means it + // the state needs to be transported as part of the HTML stream. We intend + // to add a feature to Fizz for this, but for now it's up to the + // metaframework to implement correctly. + formState: formState, + } + ); } // Remove this line to simulate MPA behavior diff --git a/fixtures/ssr/src/index.js b/fixtures/ssr/src/index.js index f6457ce570674..bac5be6ec62e2 100644 --- a/fixtures/ssr/src/index.js +++ b/fixtures/ssr/src/index.js @@ -1,6 +1,12 @@ import React from 'react'; +import {Profiler} from 'react'; import {hydrateRoot} from 'react-dom/client'; import App from './components/App'; -hydrateRoot(document, ); +hydrateRoot( + document, + + + +); From 50f00fd876b0b92b243cd8b54a222f9577446392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 28 Dec 2024 02:02:16 -0500 Subject: [PATCH 2/2] [Flight] Mark Errored Server Components (#31879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is similar to #31876 but for Server Components. It marks them as errored and puts the error message in the Summary properties. Screenshot 2024-12-20 at 5 05 35 PM This only looks at the current chunk for rejections. That means that there might still be promises deeper that rejected but it's only the immediate return value of the Server Component that's considered a rejection of the component itself. --- .../react-client/src/ReactFlightClient.js | 38 ++++++++++++---- .../src/ReactFlightPerformanceTrack.js | 43 +++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 30d340ac56700..7c424d45b3183 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -73,6 +73,7 @@ import { markAllTracksInOrder, logComponentRender, logDedupedComponentRender, + logComponentErrored, } from './ReactFlightPerformanceTrack'; import { @@ -2876,6 +2877,7 @@ function flushComponentPerformance( if (debugInfo) { let endTime = 0; + let isLastComponent = true; for (let i = debugInfo.length - 1; i >= 0; i--) { const info = debugInfo[i]; if (typeof info.time === 'number') { @@ -2890,17 +2892,37 @@ function flushComponentPerformance( const startTimeInfo = debugInfo[i - 1]; if (typeof startTimeInfo.time === 'number') { const startTime = startTimeInfo.time; - logComponentRender( - componentInfo, - trackIdx, - startTime, - endTime, - childrenEndTime, - response._rootEnvironmentName, - ); + if ( + isLastComponent && + root.status === ERRORED && + root.reason !== response._closedReason + ) { + // If this is the last component to render before this chunk rejected, then conceptually + // this component errored. If this was a cancellation then it wasn't this component that + // errored. + logComponentErrored( + componentInfo, + trackIdx, + startTime, + endTime, + childrenEndTime, + response._rootEnvironmentName, + root.reason, + ); + } else { + logComponentRender( + componentInfo, + trackIdx, + startTime, + endTime, + childrenEndTime, + response._rootEnvironmentName, + ); + } // Track the root most component of the result for deduping logging. result.component = componentInfo; } + isLastComponent = false; } } } diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index d2860e407cc65..123878526d3eb 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -102,6 +102,49 @@ export function logComponentRender( } } +export function logComponentErrored( + componentInfo: ReactComponentInfo, + trackIdx: number, + startTime: number, + endTime: number, + childrenEndTime: number, + rootEnv: string, + error: mixed, +): void { + if (supportsUserTiming) { + const properties = []; + if (__DEV__) { + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + properties.push(['Error', message]); + } + const env = componentInfo.env; + const name = componentInfo.name; + const isPrimaryEnv = env === rootEnv; + const entryName = + isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + performance.measure(entryName, { + start: startTime < 0 ? 0 : startTime, + end: childrenEndTime, + detail: { + devtools: { + color: 'error', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + tooltipText: entryName + ' Errored', + properties, + }, + }, + }); + } +} + export function logDedupedComponentRender( componentInfo: ReactComponentInfo, trackIdx: number,