From 93d4458fdc054929e54fb25017d237ed85415533 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 13 Oct 2025 17:58:43 +0200 Subject: [PATCH 01/10] [Fiber] Ensure `useEffectEvent` reads latest values in `forwardRef` and `memo()` Components (#34831) --- .../src/ReactFiberCommitWork.js | 8 +- .../src/__tests__/useEffectEvent-test.js | 78 +++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 825b814db51..adff6647f6d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -496,7 +496,9 @@ function commitBeforeMutationEffectsOnFiber( } switch (finishedWork.tag) { - case FunctionComponent: { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { if (enableUseEffectEventHook) { if ((flags & Update) !== NoFlags) { const updateQueue: FunctionComponentUpdateQueue | null = @@ -513,10 +515,6 @@ function commitBeforeMutationEffectsOnFiber( } break; } - case ForwardRef: - case SimpleMemoComponent: { - break; - } case ClassComponent: { if ((flags & Snapshot) !== NoFlags) { if (current !== null) { diff --git a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js index 17b8d6d421f..f263c9af269 100644 --- a/packages/react-reconciler/src/__tests__/useEffectEvent-test.js +++ b/packages/react-reconciler/src/__tests__/useEffectEvent-test.js @@ -850,4 +850,82 @@ describe('useEffectEvent', () => { ); assertLog(['Add to cart', 'url: /shop/2, numberOfItems: 1']); }); + + it('reads the latest context value in memo Components', async () => { + const MyContext = createContext('default'); + + let logContextValue; + const ContextReader = React.memo(function ContextReader() { + const value = useContext(MyContext); + Scheduler.log('ContextReader: ' + value); + const fireLogContextValue = useEffectEvent(() => { + Scheduler.log('ContextReader (Effect event): ' + value); + }); + useEffect(() => { + logContextValue = fireLogContextValue; + }, []); + return null; + }); + + function App({value}) { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog(['ContextReader: first']); + + logContextValue(); + + assertLog(['ContextReader (Effect event): first']); + + await act(() => root.render()); + assertLog(['ContextReader: second']); + + logContextValue(); + assertLog(['ContextReader (Effect event): second']); + }); + + it('reads the latest context value in forwardRef Components', async () => { + const MyContext = createContext('default'); + + let logContextValue; + const ContextReader = React.forwardRef(function ContextReader(props, ref) { + const value = useContext(MyContext); + Scheduler.log('ContextReader: ' + value); + const fireLogContextValue = useEffectEvent(() => { + Scheduler.log('ContextReader (Effect event): ' + value); + }); + useEffect(() => { + logContextValue = fireLogContextValue; + }, []); + return null; + }); + + function App({value}) { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog(['ContextReader: first']); + + logContextValue(); + + assertLog(['ContextReader (Effect event): first']); + + await act(() => root.render()); + assertLog(['ContextReader: second']); + + logContextValue(); + assertLog(['ContextReader (Effect event): second']); + }); }); From b467c6e9493a484d89b4c4c09d61add968a0b3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 13 Oct 2025 12:09:45 -0400 Subject: [PATCH 02/10] [DevTools] Explicitly say which id to scroll to and only once (#34823) This ensures that we don't scroll on changes to the timeline such as when loading a new page or while the timeline is still loading. We only auto scroll to a boundary when we perform an explicit operation from the user. --- .../views/SuspenseTab/SuspenseTimeline.js | 27 +++++++++---------- .../views/SuspenseTab/SuspenseTreeContext.js | 7 +++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 30ca21476f2..8ebb06899d6 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {useContext, useEffect, useRef} from 'react'; +import {useContext, useEffect} from 'react'; import {BridgeContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks'; @@ -29,9 +29,8 @@ function SuspenseTimelineInput() { useHighlightHostInstance(); const scrollToHostInstance = useScrollToHostInstance(); - const {timeline, timelineIndex, hoveredTimelineIndex, playing} = useContext( - SuspenseTreeStateContext, - ); + const {timeline, timelineIndex, hoveredTimelineIndex, playing, autoScroll} = + useContext(SuspenseTreeStateContext); const min = 0; const max = timeline.length > 0 ? timeline.length - 1 : 0; @@ -102,7 +101,6 @@ function SuspenseTimelineInput() { }); } - const isInitialMount = useRef(true); // TODO: useEffectEvent here once it's supported in all versions DevTools supports. // For now we just exclude it from deps since we don't lint those anyway. function changeTimelineIndex(newIndex: number) { @@ -115,22 +113,21 @@ function SuspenseTimelineInput() { bridge.send('overrideSuspenseMilestone', { suspendedSet, }); - if (isInitialMount.current) { - // Skip scrolling on initial mount. Only when we're changing the timeline. - isInitialMount.current = false; - } else { - // When we're scrubbing through the timeline, scroll the current boundary - // into view as it was just revealed. This is after we override the milestone - // to reveal it. - const selectedSuspenseID = timeline[timelineIndex]; - scrollToHostInstance(selectedSuspenseID); - } } useEffect(() => { changeTimelineIndex(timelineIndex); }, [timelineIndex]); + useEffect(() => { + if (autoScroll.id > 0) { + const scrollToId = autoScroll.id; + // Consume the scroll ref so that we only trigger this scroll once. + autoScroll.id = 0; + scrollToHostInstance(scrollToId); + } + }, [autoScroll]); + useEffect(() => { if (!playing) { return undefined; diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js index 60235e09f39..484a336c349 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeContext.js @@ -31,6 +31,7 @@ export type SuspenseTreeState = { uniqueSuspendersOnly: boolean, playing: boolean, autoSelect: boolean, + autoScroll: {id: number}, // Ref that's set to 0 after scrolling once. }; type ACTION_SUSPENSE_TREE_MUTATION = { @@ -125,6 +126,7 @@ function getInitialState(store: Store): SuspenseTreeState { uniqueSuspendersOnly, playing: false, autoSelect: true, + autoScroll: {id: 0}, // Don't auto-scroll initially }; return initialState; @@ -218,6 +220,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { selectedSuspenseID, playing: false, // pause autoSelect: false, + autoScroll: {id: selectedSuspenseID}, // scroll }; } case 'SET_SUSPENSE_LINEAGE': { @@ -285,6 +288,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { timelineIndex: nextTimelineIndex, playing: false, // pause autoSelect: false, + autoScroll: {id: nextSelectedSuspenseID}, // scroll }; } case 'SUSPENSE_SKIP_TIMELINE_INDEX': { @@ -308,6 +312,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { timelineIndex: nextTimelineIndex, playing: false, // pause autoSelect: false, + autoScroll: {id: nextSelectedSuspenseID}, // scroll }; } case 'SUSPENSE_PLAY_PAUSE': { @@ -359,6 +364,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { selectedSuspenseID: nextSelectedSuspenseID, timelineIndex: nextTimelineIndex, playing: nextPlaying, + autoScroll: {id: nextSelectedSuspenseID}, // scroll }; } case 'TOGGLE_TIMELINE_FOR_ID': { @@ -392,6 +398,7 @@ function SuspenseTreeContextController({children}: Props): React.Node { timelineIndex: nextTimelineIndex, playing: false, // pause autoSelect: false, + autoScroll: {id: nextSelectedSuspenseID}, }; } case 'HOVER_TIMELINE_FOR_ID': { From 34b15674272b153f34d3bf535bcdb7a36b4b391e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 13 Oct 2025 12:10:54 -0400 Subject: [PATCH 03/10] [DevTools] Ignore suspense boundaries, without visual representation, in the timeline (#34824) This ignore a Suspense boundary from the timeline when it has no visual representation. No rect. In effect, this is not blocking the user experience. Technically it could be an effect that mounts which can have a side-effect which is visible. It could also be a meta-data tag like `` which is visible. We could hoistables a virtual representation by giving them a virtual rect. E.g. at the top of the page. This could be added after the fact. --- .../react-devtools-shared/src/devtools/store.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 99971f6a1a6..eeb6da60f8a 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -51,6 +51,7 @@ import type { ComponentFilter, ElementType, SuspenseNode, + Rect, } from 'react-devtools-shared/src/frontend/types'; import type { FrontendBridge, @@ -99,6 +100,10 @@ export type Capabilities = { supportsAdvancedProfiling: AdvancedProfiling, }; +function isNonZeroRect(rect: Rect) { + return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0; +} + /** * The store is the single source of truth for updates from the backend. * ContextProviders can subscribe to the Store for specific things they want to provide. @@ -918,7 +923,15 @@ export default class Store extends EventEmitter<{ if (current === undefined) { continue; } + // Ignore any suspense boundaries that has no visual representation as this is not + // part of the visible loading sequence. + // TODO: Consider making visible meta data and other side-effects get virtual rects. + const hasRects = + current.rects !== null && + current.rects.length > 0 && + current.rects.some(isNonZeroRect); if ( + hasRects && (!uniqueSuspendersOnly || current.hasUniqueSuspenders) && // Roots are already included as part of the Screen current.id !== rootID From e2ce64acb94968d8ca464f37fab2a7ed77848fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= <sebastian@calyptus.eu> Date: Mon, 13 Oct 2025 12:11:52 -0400 Subject: [PATCH 04/10] [DevTools] Don't show the root as being non-compliant (#34827) `isStrictModeNonCompliant` on the root just means that it supports strict mode. It's inherited by other nodes. It's not possible to opt-in to strict mode on the root itself but rather right below it. So we should not mark the root as being non-compliant. This lets you select the root in the suspense tab and it shouldn't show as red with a warning. --- .../src/devtools/views/Components/InspectedElement.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index 88aeab7bfd4..633a4d382eb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -194,7 +194,7 @@ export default function InspectedElementWrapper(_: Props): React.Node { } let strictModeBadge = null; - if (element.isStrictModeNonCompliant) { + if (element.isStrictModeNonCompliant && element.parentID !== 0) { strictModeBadge = ( <Tooltip label="This component is not running in StrictMode. Click to learn more."> <a @@ -237,7 +237,7 @@ export default function InspectedElementWrapper(_: Props): React.Node { <div className={styles.SelectedComponentName}> <div className={ - element.isStrictModeNonCompliant + element.isStrictModeNonCompliant && element.parentID !== 0 ? `${styles.ComponentName} ${styles.StrictModeNonCompliantComponentName}` : styles.ComponentName } From d7215b4970858ea45db924e1ec435a9a3e5cff40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= <sebastian@calyptus.eu> Date: Mon, 13 Oct 2025 12:12:12 -0400 Subject: [PATCH 05/10] [DevTools] Preserve the original index when sorting suspended by (#34829) The index is both used as the key and for hydration purposes. Previously we didn't preserve the index when sorting so the index didn't line up which caused hydration to be the wrong slot when sorted. --- .../Components/InspectedElementSuspendedBy.js | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index d1da51a0cf1..19cc47f982c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -300,9 +300,31 @@ type Props = { store: Store, }; -function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number { - const ioA = a.awaited; - const ioB = b.awaited; +function withIndex( + value: SerializedAsyncInfo, + index: number, +): { + index: number, + value: SerializedAsyncInfo, +} { + return { + index, + value, + }; +} + +function compareTime( + a: { + index: number, + value: SerializedAsyncInfo, + }, + b: { + index: number, + value: SerializedAsyncInfo, + }, +): number { + const ioA = a.value.awaited; + const ioB = b.value.awaited; if (ioA.start === ioB.start) { return ioA.end - ioB.end; } @@ -364,7 +386,8 @@ export default function InspectedElementSuspendedBy({ minTime = maxTime - 25; } - const sortedSuspendedBy = suspendedBy === null ? [] : suspendedBy.slice(0); + const sortedSuspendedBy = + suspendedBy === null ? [] : suspendedBy.map(withIndex); sortedSuspendedBy.sort(compareTime); let unknownSuspenders = null; @@ -407,11 +430,11 @@ export default function InspectedElementSuspendedBy({ <ButtonIcon type="copy" /> </Button> </div> - {sortedSuspendedBy.map((asyncInfo, index) => ( + {sortedSuspendedBy.map(({value, index}) => ( <SuspendedByRow key={index} index={index} - asyncInfo={asyncInfo} + asyncInfo={value} bridge={bridge} element={element} inspectedElement={inspectedElement} From 026abeaa5f3fdb5f5fdef7ebd699e3ed8e64a506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= <sebastian@calyptus.eu> Date: Mon, 13 Oct 2025 12:29:00 -0400 Subject: [PATCH 06/10] [Flight] Respect displayName of Promise instances on the server (#34825) This lets you assign a name to a Promise that's passed into first party code from third party since it otherwise would have no other stack frame to indicate its name since the whole creation stack would be in third party. We already respect the `displayName` on the client but it's more complicated on the server because we don't only consider the exact instance passed to `use()` but the whole await sequence and we can pick any Promise along the way for consideration. Therefore this also adds a change where we pick the Promise node for consideration if it has a name but no stack. Where we otherwise would've picked the I/O node. Another thing that this PR does is treat anonymous stack frames (empty url) as third party for purposes of heuristics like "hasUnfilteredFrame" and the name assignment. This lets you include these in the actual generated stacks (by overriding `filterStackFrame`) but we don't actually want them to be considered first party code in the heuristics since it ends up favoring those stacks and using internals like `Function.all` in name assignment. --- .../react-server/src/ReactFlightServer.js | 73 ++- .../ReactFlightAsyncDebugInfo-test.js | 590 +++++++++++------- 2 files changed, 415 insertions(+), 248 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 152be2eecca..1c769c93ee7 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -252,7 +252,11 @@ function findCalledFunctionNameFromStackTrace( const url = devirtualizeURL(callsite[1]); const lineNumber = callsite[2]; const columnNumber = callsite[3]; - if (filterStackFrame(url, functionName, lineNumber, columnNumber)) { + if ( + filterStackFrame(url, functionName, lineNumber, columnNumber) && + // Don't consider anonymous code first party even if the filter wants to include them in the stack. + url !== '' + ) { if (bestMatch === '') { // If we had no good stack frames for internal calls, just use the last // first party function name. @@ -308,7 +312,10 @@ function hasUnfilteredFrame(request: Request, stack: ReactStackTrace): boolean { const isAsync = callsite[6]; if ( !isAsync && - filterStackFrame(url, functionName, lineNumber, columnNumber) + filterStackFrame(url, functionName, lineNumber, columnNumber) && + // Ignore anonymous stack frames like internals. They are also not in first party + // code even though it might be useful to include them in the final stack. + url !== '' ) { return true; } @@ -367,7 +374,10 @@ export function isAwaitInUserspace( const url = devirtualizeURL(callsite[1]); const lineNumber = callsite[2]; const columnNumber = callsite[3]; - return filterStackFrame(url, functionName, lineNumber, columnNumber); + return ( + filterStackFrame(url, functionName, lineNumber, columnNumber) && + url !== '' + ); } return false; } @@ -2347,6 +2357,7 @@ function visitAsyncNode( } const awaited = node.awaited; let match: void | null | PromiseNode | IONode = previousIONode; + const promise = node.promise.deref(); if (awaited !== null) { const ioNode = visitAsyncNode(request, task, awaited, visited, cutOff); if (ioNode === undefined) { @@ -2361,17 +2372,27 @@ function visitAsyncNode( if (ioNode.tag === PROMISE_NODE) { // If the ioNode was a Promise, then that means we found one in user space since otherwise // we would've returned an IO node. We assume this has the best stack. + // Note: This might also be a Promise with a displayName but potentially a worse stack. + // We could potentially favor the outer Promise if it has a stack but not the inner. match = ioNode; } else if ( - node.stack === null || - !hasUnfilteredFrame(request, node.stack) + (node.stack !== null && hasUnfilteredFrame(request, node.stack)) || + (promise !== undefined && + // $FlowFixMe[prop-missing] + typeof promise.displayName === 'string' && + (ioNode.stack === null || + !hasUnfilteredFrame(request, ioNode.stack))) ) { + // If this Promise has a stack trace then we favor that over the I/O node since we're + // mainly dealing with Promises as the abstraction. + // If it has no stack but at least has a displayName and the io doesn't have a better + // stack anyway, then also use this Promise instead since at least it has a name. + match = node; + } else { // If this Promise was created inside only third party code, then try to use // the inner I/O node instead. This could happen if third party calls into first // party to perform some I/O. match = ioNode; - } else { - match = node; } } else if (request.status === ABORTING) { if (node.start < request.abortTime && node.end > request.abortTime) { @@ -2379,8 +2400,11 @@ function visitAsyncNode( // Promise that was aborted. This won't necessarily have I/O associated with it but // it's a point of interest. if ( - node.stack !== null && - hasUnfilteredFrame(request, node.stack) + (node.stack !== null && + hasUnfilteredFrame(request, node.stack)) || + (promise !== undefined && + // $FlowFixMe[prop-missing] + typeof promise.displayName === 'string') ) { match = node; } @@ -2389,7 +2413,6 @@ function visitAsyncNode( } // We need to forward after we visit awaited nodes because what ever I/O we requested that's // the thing that generated this node and its virtual children. - const promise = node.promise.deref(); if (promise !== undefined) { const debugInfo = promise._debugInfo; if (debugInfo != null && !visited.has(debugInfo)) { @@ -4497,17 +4520,33 @@ function serializeIONode( let stack = null; let name = ''; + if (ioNode.promise !== null) { + // Pick an explicit name from the Promise itself if it exists. + // Note that we don't use the promiseRef passed in since that's sometimes the awaiting Promise + // which is the value observed but it's likely not the one with the name on it. + const promise = ioNode.promise.deref(); + if ( + promise !== undefined && + // $FlowFixMe[prop-missing] + typeof promise.displayName === 'string' + ) { + name = promise.displayName; + } + } if (ioNode.stack !== null) { // The stack can contain some leading internal frames for the construction of the promise that we skip. const fullStack = stripLeadingPromiseCreationFrames(ioNode.stack); stack = filterStackTrace(request, fullStack); - name = findCalledFunctionNameFromStackTrace(request, fullStack); - // The name can include the object that this was called on but sometimes that's - // just unnecessary context. - if (name.startsWith('Window.')) { - name = name.slice(7); - } else if (name.startsWith('<anonymous>.')) { - name = name.slice(7); + if (name === '') { + // If we didn't have an explicit name, try finding one from the stack. + name = findCalledFunctionNameFromStackTrace(request, fullStack); + // The name can include the object that this was called on but sometimes that's + // just unnecessary context. + if (name.startsWith('Window.')) { + name = name.slice(7); + } else if (name.startsWith('<anonymous>.')) { + name = name.slice(7); + } } } const owner = ioNode.owner; diff --git a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js index 9253371f54a..992b58d3880 100644 --- a/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js +++ b/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js @@ -109,6 +109,7 @@ describe('ReactFlightAsyncDebugInfo', () => { async function getData(text) { await delay(1); const promise = delay(2); + promise.displayName = 'hello'; await Promise.all([promise]); return text.toUpperCase(); } @@ -159,7 +160,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -183,7 +184,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -210,9 +211,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 26, - 116, + 117, 5, ], ], @@ -231,7 +232,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -250,9 +251,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 26, - 116, + 117, 5, ], ], @@ -267,7 +268,7 @@ describe('ReactFlightAsyncDebugInfo', () => { "awaited": { "end": 0, "env": "Server", - "name": "delay", + "name": "hello", "owner": { "env": "Server", "key": null, @@ -277,7 +278,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -304,9 +305,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 20, - 116, + 117, 5, ], ], @@ -325,7 +326,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -336,7 +337,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 112, + 113, 21, 109, 5, @@ -344,9 +345,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 117, + 118, 20, - 116, + 117, 5, ], ], @@ -366,9 +367,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 119, + 120, 60, - 116, + 117, 5, ], ], @@ -380,7 +381,7 @@ describe('ReactFlightAsyncDebugInfo', () => { "awaited": { "end": 0, "env": "Server", - "name": "delay", + "name": "hello", "owner": { "env": "Server", "key": null, @@ -390,7 +391,7 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 128, + 129, 109, 108, 50, @@ -430,9 +431,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 119, + 120, 60, - 116, + 117, 5, ], ], @@ -441,9 +442,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 125, + 126, 35, - 122, + 123, 5, ], ], @@ -624,9 +625,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -656,9 +657,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -683,17 +684,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 571, + 572, 13, - 570, + 571, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 576, + 577, 36, - 575, + 576, 5, ], ], @@ -712,9 +713,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -731,17 +732,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 571, + 572, 13, - 570, + 571, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 576, + 577, 36, - 575, + 576, 5, ], ], @@ -761,9 +762,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 578, + 579, 60, - 575, + 576, 5, ], ], @@ -782,9 +783,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 588, + 589, 40, - 569, + 570, 49, ], [ @@ -809,17 +810,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 571, + 572, 13, - 570, + 571, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 577, + 578, 22, - 575, + 576, 5, ], ], @@ -838,9 +839,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 578, + 579, 60, - 575, + 576, 5, ], ], @@ -849,9 +850,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 584, + 585, 40, - 581, + 582, 5, ], ], @@ -926,9 +927,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 895, + 896, 109, - 882, + 883, 80, ], ], @@ -947,9 +948,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 895, + 896, 109, - 882, + 883, 80, ], ], @@ -966,9 +967,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 895, + 896, 109, - 882, + 883, 80, ], ], @@ -1040,9 +1041,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1009, + 1010, 109, - 1000, + 1001, 94, ], ], @@ -1125,9 +1126,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1094, + 1095, 109, - 1070, + 1071, 50, ], ], @@ -1221,9 +1222,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1190, + 1191, 109, - 1173, + 1174, 63, ], ], @@ -1248,9 +1249,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1280,9 +1281,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1299,17 +1300,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1175, + 1176, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 24, - 1180, + 1181, 5, ], ], @@ -1336,9 +1337,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1347,17 +1348,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1175, + 1176, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 24, - 1180, + 1181, 5, ], ], @@ -1390,9 +1391,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1409,17 +1410,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1176, + 1177, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 18, - 1180, + 1181, 5, ], ], @@ -1446,9 +1447,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1186, + 1187, 24, - 1185, + 1186, 5, ], ], @@ -1457,17 +1458,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1176, + 1177, 13, - 1174, + 1175, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1181, + 1182, 18, - 1180, + 1181, 5, ], ], @@ -1565,9 +1566,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1597,9 +1598,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1624,17 +1625,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1514, + 1515, 13, - 1513, + 1514, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1524, + 1525, 13, - 1523, + 1524, 5, ], ], @@ -1653,9 +1654,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1672,17 +1673,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1514, + 1515, 13, - 1513, + 1514, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1524, + 1525, 13, - 1523, + 1524, 5, ], ], @@ -1702,9 +1703,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1525, + 1526, 60, - 1523, + 1524, 5, ], ], @@ -1726,9 +1727,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1529, + 1530, 40, - 1512, + 1513, 62, ], [ @@ -1753,17 +1754,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1514, + 1515, 13, - 1513, + 1514, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1524, + 1525, 13, - 1523, + 1524, 5, ], ], @@ -1782,9 +1783,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1525, + 1526, 60, - 1523, + 1524, 5, ], ], @@ -1793,9 +1794,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1519, + 1520, 28, - 1518, + 1519, 5, ], ], @@ -1878,9 +1879,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -1910,9 +1911,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -1937,17 +1938,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1828, + 1829, 13, - 1827, + 1828, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1837, + 1838, 23, - 1836, + 1837, 5, ], ], @@ -1966,9 +1967,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -1985,17 +1986,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1828, + 1829, 13, - 1827, + 1828, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1837, + 1838, 23, - 1836, + 1837, 5, ], ], @@ -2015,9 +2016,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1838, + 1839, 60, - 1836, + 1837, 5, ], ], @@ -2036,9 +2037,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1842, + 1843, 40, - 1826, + 1827, 57, ], [ @@ -2063,17 +2064,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1828, + 1829, 13, - 1827, + 1828, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1837, + 1838, 23, - 1836, + 1837, 5, ], ], @@ -2087,9 +2088,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 1838, + 1839, 60, - 1836, + 1837, 5, ], ], @@ -2174,9 +2175,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2206,9 +2207,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2233,17 +2234,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2128, + 2129, 13, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2262,9 +2263,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2281,17 +2282,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2128, + 2129, 13, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2313,9 +2314,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2340,25 +2341,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2122, + 2123, 13, - 2121, + 2122, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2127, + 2128, 15, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2377,9 +2378,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2396,25 +2397,25 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2122, + 2123, 13, - 2121, + 2122, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2127, + 2128, 15, - 2126, + 2127, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2133, + 2134, 13, - 2132, + 2133, 5, ], ], @@ -2436,9 +2437,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2463,9 +2464,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2123, + 2124, 13, - 2121, + 2122, 5, ], ], @@ -2484,9 +2485,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2138, + 2139, 40, - 2120, + 2121, 80, ], [ @@ -2503,9 +2504,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2123, + 2124, 13, - 2121, + 2122, 5, ], ], @@ -2578,9 +2579,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2547, + 2548, 109, - 2536, + 2537, 58, ], ], @@ -2602,9 +2603,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2547, + 2548, 109, - 2536, + 2537, 58, ], ], @@ -2621,17 +2622,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2538, + 2539, 14, - 2537, + 2538, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2544, + 2545, 20, - 2543, + 2544, 5, ], ], @@ -2650,9 +2651,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2547, + 2548, 109, - 2536, + 2537, 58, ], ], @@ -2661,17 +2662,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2538, + 2539, 23, - 2537, + 2538, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2544, + 2545, 20, - 2543, + 2544, 5, ], ], @@ -2750,9 +2751,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2714, + 2715, 40, - 2702, + 2703, 56, ], [ @@ -2782,9 +2783,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2714, + 2715, 40, - 2702, + 2703, 56, ], [ @@ -2809,9 +2810,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2710, + 2711, 20, - 2709, + 2710, 5, ], ], @@ -2830,9 +2831,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2714, + 2715, 40, - 2702, + 2703, 56, ], [ @@ -2849,9 +2850,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2710, + 2711, 20, - 2709, + 2710, 5, ], ], @@ -2944,9 +2945,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -2976,9 +2977,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -2995,17 +2996,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2889, + 2890, 15, - 2888, + 2889, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 19, - 2897, + 2898, 5, ], ], @@ -3024,9 +3025,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -3043,17 +3044,17 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2889, + 2890, 15, - 2888, + 2889, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 19, - 2897, + 2898, 5, ], ], @@ -3075,9 +3076,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -3094,9 +3095,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 25, - 2897, + 2898, 5, ], ], @@ -3115,9 +3116,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2903, + 2904, 40, - 2882, + 2883, 42, ], [ @@ -3134,9 +3135,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 2898, + 2899, 25, - 2897, + 2898, 5, ], ], @@ -3212,9 +3213,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3179, + 3180, 40, - 3167, + 3168, 36, ], ], @@ -3236,9 +3237,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3179, + 3180, 40, - 3167, + 3168, 36, ], ], @@ -3247,9 +3248,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3171, + 3172, 7, - 3169, + 3170, 5, ], ], @@ -3268,9 +3269,9 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Object.<anonymous>", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3179, + 3180, 40, - 3167, + 3168, 36, ], ], @@ -3279,9 +3280,136 @@ describe('ReactFlightAsyncDebugInfo', () => { [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", - 3173, + 3174, 7, - 3169, + 3170, + 5, + ], + ], + }, + { + "time": 0, + }, + { + "time": 0, + }, + { + "awaited": { + "byteSize": 0, + "end": 0, + "name": "RSC stream", + "owner": null, + "start": 0, + "value": { + "value": "stream", + }, + }, + }, + ] + `); + } + }); + + it('can track a promise created fully outside first party code', async function internal_test() { + async function internal_API(text, timeout) { + let resolve; + const promise = new Promise(r => { + resolve = r; + }); + promise.displayName = 'greeting'; + setTimeout(() => resolve(text), timeout); + return promise; + } + + async function Component({promise}) { + const result = await promise; + return result; + } + + const stream = ReactServerDOMServer.renderToPipeableStream( + <Component promise={internal_API('hello', 1)} />, + {}, + { + filterStackFrame, + }, + ); + + const readable = new Stream.PassThrough(streamOptions); + + const result = ReactServerDOMClient.createFromNodeStream(readable, { + moduleMap: {}, + moduleLoading: {}, + }); + stream.pipe(readable); + + expect(await result).toBe('hello'); + + await finishLoadingStream(readable); + if ( + __DEV__ && + gate( + flags => + flags.enableComponentPerformanceTrack && flags.enableAsyncDebugInfo, + ) + ) { + expect(getDebugInfo(result)).toMatchInlineSnapshot(` + [ + { + "time": 0, + }, + { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + { + "time": 0, + }, + { + "awaited": { + "end": 0, + "env": "Server", + "name": "greeting", + "start": 0, + "value": { + "status": "halted", + }, + }, + "env": "Server", + "owner": { + "env": "Server", + "key": null, + "name": "Component", + "props": {}, + "stack": [ + [ + "new Promise", + "", + 0, + 0, + 0, + 0, + ], + ], + }, + "stack": [ + [ + "Component", + "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", + 3325, + 20, + 3324, 5, ], ], From 83ea655a0ba1de44c933368cd7f56c8f0418f07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= <sebastian@calyptus.eu> Date: Mon, 13 Oct 2025 13:07:39 -0400 Subject: [PATCH 07/10] [DevTools] Group consecutive suspended by rows by the same name (#34830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #34829. This lets you get an overview more easily when there's lots of things like scripts downloading. Pluralized the name. E.g. `script` -> `scripts` or `fetch` -> `fetches`. This only groups them consecutively when they'd have the same place in the list anyway because otherwise it might cover up some kind of waterfall effects. <img width="404" height="225" alt="Screenshot 2025-10-13 at 12 06 51 AM" src="https://github.com/user-attachments/assets/da204a8e-d5f7-4eb0-8c51-4cc5bfd184c4" /> Expanded: <img width="407" height="360" alt="Screenshot 2025-10-13 at 12 07 00 AM" src="https://github.com/user-attachments/assets/de3c3de9-f314-4c87-b606-31bc49eb4aba" /> --- .../Components/InspectedElementSuspendedBy.js | 190 ++++++++++++++++-- .../src/devtools/views/utils.js | 36 ++++ 2 files changed, 210 insertions(+), 16 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index 19cc47f982c..fa2b6a95bed 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -13,7 +13,7 @@ import {useState, useTransition} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import KeyValue from './KeyValue'; -import {serializeDataForCopy} from '../utils'; +import {serializeDataForCopy, pluralize} from '../utils'; import Store from '../../store'; import styles from './InspectedElementSharedStyles.css'; import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck'; @@ -44,6 +44,7 @@ type RowProps = { index: number, minTime: number, maxTime: number, + skipName?: boolean, }; function getShortDescription(name: string, description: string): string { @@ -99,6 +100,7 @@ function SuspendedByRow({ index, minTime, maxTime, + skipName, }: RowProps) { const [isOpen, setIsOpen] = useState(false); const [openIsPending, startOpenTransition] = useTransition(); @@ -166,8 +168,10 @@ function SuspendedByRow({ className={styles.CollapsableHeaderIcon} type={isOpen ? 'expanded' : 'collapsed'} /> - <span className={styles.CollapsableHeaderTitle}>{name}</span> - {shortDescription === '' ? null : ( + <span className={styles.CollapsableHeaderTitle}> + {skipName ? shortDescription : name} + </span> + {skipName || shortDescription === '' ? null : ( <> <span className={styles.CollapsableHeaderSeparator}>{' ('}</span> <span className={styles.CollapsableHeaderTitle}> @@ -331,6 +335,110 @@ function compareTime( return ioA.start - ioB.start; } +type GroupProps = { + bridge: FrontendBridge, + element: Element, + inspectedElement: InspectedElement, + store: Store, + name: string, + suspendedBy: Array<{ + index: number, + value: SerializedAsyncInfo, + }>, + minTime: number, + maxTime: number, +}; + +function SuspendedByGroup({ + bridge, + element, + inspectedElement, + store, + name, + suspendedBy, + minTime, + maxTime, +}: GroupProps) { + const [isOpen, setIsOpen] = useState(false); + let start = Infinity; + let end = -Infinity; + let isRejected = false; + for (let i = 0; i < suspendedBy.length; i++) { + const asyncInfo: SerializedAsyncInfo = suspendedBy[i].value; + const ioInfo = asyncInfo.awaited; + if (ioInfo.start < start) { + start = ioInfo.start; + } + if (ioInfo.end > end) { + end = ioInfo.end; + } + const value: any = ioInfo.value; + if ( + value !== null && + typeof value === 'object' && + value[meta.name] === 'rejected Thenable' + ) { + isRejected = true; + } + } + const timeScale = 100 / (maxTime - minTime); + let left = (start - minTime) * timeScale; + let width = (end - start) * timeScale; + if (width < 5) { + // Use at least a 5% width to avoid showing too small indicators. + width = 5; + if (left > 95) { + left = 95; + } + } + const pluralizedName = pluralize(name); + return ( + <div className={styles.CollapsableRow}> + <Button + className={styles.CollapsableHeader} + onClick={() => { + setIsOpen(prevIsOpen => !prevIsOpen); + }} + title={pluralizedName}> + <ButtonIcon + className={styles.CollapsableHeaderIcon} + type={isOpen ? 'expanded' : 'collapsed'} + /> + <span className={styles.CollapsableHeaderTitle}>{pluralizedName}</span> + <div className={styles.CollapsableHeaderFiller} /> + {isOpen ? null : ( + <div className={styles.TimeBarContainer}> + <div + className={ + !isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored + } + style={{ + left: left.toFixed(2) + '%', + width: width.toFixed(2) + '%', + }} + /> + </div> + )} + </Button> + {isOpen && + suspendedBy.map(({value, index}) => ( + <SuspendedByRow + key={index} + index={index} + asyncInfo={value} + bridge={bridge} + element={element} + inspectedElement={inspectedElement} + store={store} + minTime={minTime} + maxTime={maxTime} + skipName={true} + /> + ))} + </div> + ); +} + export default function InspectedElementSuspendedBy({ bridge, element, @@ -390,6 +498,27 @@ export default function InspectedElementSuspendedBy({ suspendedBy === null ? [] : suspendedBy.map(withIndex); sortedSuspendedBy.sort(compareTime); + // Organize into groups of consecutive entries with the same name. + const groups = []; + let currentGroup = null; + let currentGroupName = null; + for (let i = 0; i < sortedSuspendedBy.length; i++) { + const entry = sortedSuspendedBy[i]; + const name = entry.value.awaited.name; + if ( + currentGroupName !== name || + !name || + name === 'Promise' || + currentGroup === null + ) { + // Create a new group. + currentGroupName = name; + currentGroup = []; + groups.push(currentGroup); + } + currentGroup.push(entry); + } + let unknownSuspenders = null; switch (inspectedElement.unknownSuspenders) { case UNKNOWN_SUSPENDERS_REASON_PRODUCTION: @@ -430,19 +559,48 @@ export default function InspectedElementSuspendedBy({ <ButtonIcon type="copy" /> </Button> </div> - {sortedSuspendedBy.map(({value, index}) => ( - <SuspendedByRow - key={index} - index={index} - asyncInfo={value} - bridge={bridge} - element={element} - inspectedElement={inspectedElement} - store={store} - minTime={minTime} - maxTime={maxTime} - /> - ))} + {groups.length === 1 + ? // If it's only one type of suspender we can flatten it. + groups[0].map(entry => ( + <SuspendedByRow + key={entry.index} + index={entry.index} + asyncInfo={entry.value} + bridge={bridge} + element={element} + inspectedElement={inspectedElement} + store={store} + minTime={minTime} + maxTime={maxTime} + /> + )) + : groups.map((entries, index) => + entries.length === 1 ? ( + <SuspendedByRow + key={entries[0].index} + index={entries[0].index} + asyncInfo={entries[0].value} + bridge={bridge} + element={element} + inspectedElement={inspectedElement} + store={store} + minTime={minTime} + maxTime={maxTime} + /> + ) : ( + <SuspendedByGroup + key={entries[0].index} + name={entries[0].value.awaited.name} + suspendedBy={entries} + bridge={bridge} + element={element} + inspectedElement={inspectedElement} + store={store} + minTime={minTime} + maxTime={maxTime} + /> + ), + )} {unknownSuspenders} </div> ); diff --git a/packages/react-devtools-shared/src/devtools/views/utils.js b/packages/react-devtools-shared/src/devtools/views/utils.js index ed14b2c236b..3b0de4118a2 100644 --- a/packages/react-devtools-shared/src/devtools/views/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/utils.js @@ -198,3 +198,39 @@ export function truncateText(text: string, maxLength: number): string { return text; } } + +export function pluralize(word: string): string { + if (!/^[a-z]+$/i.test(word)) { + // If it's not a single a-z word, give up. + return word; + } + + switch (word) { + case 'man': + return 'men'; + case 'woman': + return 'women'; + case 'child': + return 'children'; + case 'foot': + return 'feet'; + case 'tooth': + return 'teeth'; + case 'mouse': + return 'mice'; + case 'person': + return 'people'; + } + + // Words ending in s, x, z, ch, sh → add "es" + if (/(s|x|z|ch|sh)$/i.test(word)) return word + 'es'; + + // Words ending in consonant + y → replace y with "ies" + if (/[bcdfghjklmnpqrstvwxz]y$/i.test(word)) return word.slice(0, -1) + 'ies'; + + // Words ending in f or fe → replace with "ves" + if (/(?:f|fe)$/i.test(word)) return word.replace(/(?:f|fe)$/i, 'ves'); + + // Default: just add "s" + return word + 's'; +} From 7b971c0a5536f7cd4573ff574921463acf947b14 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" <sebastian.silbermann@vercel.com> Date: Mon, 13 Oct 2025 23:14:51 +0200 Subject: [PATCH 08/10] Current behavior for excluding Component render with unchanged props from Components track (#34822) If we rerender with the same props, the render time will not be accounted for in the Components track. The attached test reproduces the behavior observed in https://codesandbox.io/p/sandbox/patient-fast-j94f2g: <img width="1118" height="354" alt="CleanShot 2025-10-13 at 00 13 41@2x" src="https://github.com/user-attachments/assets/4be10ee9-d529-4d98-9035-4f26f9587f52" /> --- .../__tests__/ReactPerformanceTrack-test.js | 135 ++++++++++++++++-- 1 file changed, 125 insertions(+), 10 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js index 72310812482..827592bb80d 100644 --- a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js +++ b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js @@ -15,9 +15,20 @@ let act; let useEffect; describe('ReactPerformanceTracks', () => { + const performanceMeasureCalls = []; + beforeEach(() => { + performanceMeasureCalls.length = 0; Object.defineProperty(performance, 'measure', { - value: jest.fn(), + value: jest.fn((measureName, reusableOptions) => { + performanceMeasureCalls.push([ + measureName, + { + // React will mutate the options it passes to performance.measure. + ...reusableOptions, + }, + ]); + }), configurable: true, }); console.timeStamp = () => {}; @@ -32,6 +43,19 @@ describe('ReactPerformanceTracks', () => { useEffect = React.useEffect; }); + function getConsoleTimestampEntries() { + try { + return console.timeStamp.mock.calls.filter(call => { + const [, startTime, endTime] = call; + + const isRegisterTrackCall = startTime !== 0.003 && endTime !== 0.003; + return isRegisterTrackCall; + }); + } finally { + console.timeStamp.mockClear(); + } + } + // @gate __DEV__ && enableComponentPerformanceTrack it('shows a hint if an update is triggered by a deeply equal object', async () => { const App = function App({items}) { @@ -45,7 +69,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(<App items={items} />); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ 'Mount', { @@ -62,14 +86,14 @@ describe('ReactPerformanceTracks', () => { }, ], ]); - performance.measure.mockClear(); + performanceMeasureCalls.length = 0; Scheduler.unstable_advanceTime(10); await act(() => { ReactNoop.render(<App items={items.concat('4')} />); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ '​App', { @@ -105,7 +129,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(<App items={items} />); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ 'Mount', { @@ -122,14 +146,14 @@ describe('ReactPerformanceTracks', () => { }, ], ]); - performance.measure.mockClear(); + performanceMeasureCalls.length = 0; Scheduler.unstable_advanceTime(10); await act(() => { ReactNoop.render(<App items={items.concat('-1')} />); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ '​App', { @@ -171,7 +195,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(<App data={{buffer: null}} />); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ 'Mount', { @@ -188,7 +212,7 @@ describe('ReactPerformanceTracks', () => { }, ], ]); - performance.measure.mockClear(); + performanceMeasureCalls.length = 0; Scheduler.unstable_advanceTime(10); @@ -197,7 +221,7 @@ describe('ReactPerformanceTracks', () => { ReactNoop.render(<App data={{buffer: bigData}} />); }); - expect(performance.measure.mock.calls).toEqual([ + expect(performanceMeasureCalls).toEqual([ [ '​App', { @@ -324,4 +348,95 @@ describe('ReactPerformanceTracks', () => { ], ]); }); + + // @gate __DEV__ && enableComponentPerformanceTrack + it('includes spans for Components with no prop changes', async () => { + function Left({value}) { + Scheduler.unstable_advanceTime(5000); + } + function Right() { + Scheduler.unstable_advanceTime(10000); + } + + await act(() => { + ReactNoop.render( + <> + <Left value={1} /> + <Right /> + </>, + ); + }); + + expect(performanceMeasureCalls).toEqual([ + [ + 'Mount', + { + detail: { + devtools: { + color: 'warning', + properties: null, + tooltipText: 'Mount', + track: 'Components ⚛', + }, + }, + end: 5000, + start: 0, + }, + ], + [ + 'Mount', + { + detail: { + devtools: { + color: 'warning', + properties: null, + tooltipText: 'Mount', + track: 'Components ⚛', + }, + }, + end: 15000, + start: 5000, + }, + ], + ]); + performanceMeasureCalls.length = 0; + getConsoleTimestampEntries(); + + Scheduler.unstable_advanceTime(1000); + + await act(() => { + ReactNoop.render( + <> + <Left value={2} /> + <Right /> + </>, + ); + }); + + expect(performanceMeasureCalls).toEqual([ + [ + '​Left', + { + detail: { + devtools: { + color: 'error', + properties: [ + ['Changed Props', ''], + ['– value', '1'], + ['+ value', '2'], + ], + tooltipText: 'Left', + track: 'Components ⚛', + }, + }, + end: 21000, + start: 16000, + }, + ], + ]); + expect(getConsoleTimestampEntries()).toEqual([ + ['Render', 16000, 31000, 'Blocking', 'Scheduler ⚛', 'primary-dark'], + ]); + performanceMeasureCalls.length = 0; + }); }); From 47905a79507f9ae5fc1bf633f7cbbd1894b9523b Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:23:04 -0400 Subject: [PATCH 09/10] Fix/add missing else branch for renders with no props change (#34837) Stacked on https://github.com/facebook/react/pull/34822. Fixes a bug introduced in https://github.com/facebook/react/pull/34370. Just copying the lower else branch to the `properties.length` else branch at the top. --- .../src/ReactFiberPerformanceTrack.js | 24 +++++++++++++++++++ .../__tests__/ReactPerformanceTrack-test.js | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index 65cc7f04066..b2d165343a6 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -305,6 +305,30 @@ export function logComponentRender( } else { performance.measure('\u200b' + name, reusableComponentOptions); } + } else { + if (debugTask != null) { + debugTask.run( + // $FlowFixMe[method-unbinding] + console.timeStamp.bind( + console, + name, + startTime, + endTime, + COMPONENTS_TRACK, + undefined, + color, + ), + ); + } else { + console.timeStamp( + name, + startTime, + endTime, + COMPONENTS_TRACK, + undefined, + color, + ); + } } } else { if (debugTask != null) { diff --git a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js index 827592bb80d..0f5152e8d47 100644 --- a/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js +++ b/packages/react-reconciler/src/__tests__/ReactPerformanceTrack-test.js @@ -350,7 +350,7 @@ describe('ReactPerformanceTracks', () => { }); // @gate __DEV__ && enableComponentPerformanceTrack - it('includes spans for Components with no prop changes', async () => { + it('includes console.timeStamp spans for Components with no prop changes', async () => { function Left({value}) { Scheduler.unstable_advanceTime(5000); } @@ -436,6 +436,7 @@ describe('ReactPerformanceTracks', () => { ]); expect(getConsoleTimestampEntries()).toEqual([ ['Render', 16000, 31000, 'Blocking', 'Scheduler ⚛', 'primary-dark'], + ['Right', 21000, 31000, 'Components ⚛', undefined, 'error'], ]); performanceMeasureCalls.length = 0; }); From b9ec735de248f46da181afbc12aa906422be0dba Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:42:13 -0400 Subject: [PATCH 10/10] [Perf Tracks]: Clear potentially large measures (#34803) Fixes https://github.com/facebook/react/issues/34770. We need to clear measures at some point, otherwise all these copies of props that we end up recording will allocate too much memory in Chromium. This adds `performance.clearMeasures(...)` calls to such cases in DEV. Validated that entries are still shown on Performance panel timeline. --- .../src/ReactFlightPerformanceTrack.js | 37 ++++++++++++++----- .../src/ReactFiberPerformanceTrack.js | 29 +++++++++++---- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 81474767363..fb29fb45d0b 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -102,6 +102,7 @@ export function logComponentRender( const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; const debugTask = componentInfo.debugTask; + const measureName = '\u200b' + entryName; if (__DEV__ && debugTask) { const properties: Array<[string, string]> = []; if (componentInfo.key != null) { @@ -110,9 +111,10 @@ export function logComponentRender( if (componentInfo.props != null) { addObjectToProperties(componentInfo.props, properties, 0, ''); } + debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + entryName, { + performance.measure.bind(performance, measureName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -125,9 +127,10 @@ export function logComponentRender( }, }), ); + performance.clearMeasures(measureName); } else { console.timeStamp( - '\u200b' + entryName, + measureName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -152,6 +155,7 @@ export function logComponentAborted( const isPrimaryEnv = env === rootEnv; const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + const measureName = '\u200b' + entryName; if (__DEV__) { const properties: Array<[string, string]> = [ [ @@ -165,7 +169,8 @@ export function logComponentAborted( if (componentInfo.props != null) { addObjectToProperties(componentInfo.props, properties, 0, ''); } - performance.measure('\u200b' + entryName, { + + performance.measure(measureName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -178,9 +183,10 @@ export function logComponentAborted( }, }, }); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -206,6 +212,7 @@ export function logComponentErrored( const isPrimaryEnv = env === rootEnv; const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + const measureName = '\u200b' + entryName; if (__DEV__) { const message = typeof error === 'object' && @@ -222,7 +229,8 @@ export function logComponentErrored( if (componentInfo.props != null) { addObjectToProperties(componentInfo.props, properties, 0, ''); } - performance.measure('\u200b' + entryName, { + + performance.measure(measureName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, detail: { @@ -235,9 +243,10 @@ export function logComponentErrored( }, }, }); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, childrenEndTime, trackNames[trackIdx], @@ -397,6 +406,7 @@ export function logComponentAwaitAborted( }, }), ); + performance.clearMeasures(entryName); } else { console.timeStamp( entryName, @@ -453,6 +463,7 @@ export function logComponentAwaitErrored( }, }), ); + performance.clearMeasures(entryName); } else { console.timeStamp( entryName, @@ -514,6 +525,7 @@ export function logComponentAwait( }, }), ); + performance.clearMeasures(entryName); } else { console.timeStamp( entryName, @@ -538,6 +550,7 @@ export function logIOInfoErrored( const description = getIODescription(error); const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); const debugTask = ioInfo.debugTask; + const measureName = '\u200b' + entryName; if (__DEV__ && debugTask) { const message = typeof error === 'object' && @@ -550,9 +563,10 @@ export function logIOInfoErrored( const properties = [['rejected with', message]]; const tooltipText = getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected'; + debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + entryName, { + performance.measure.bind(performance, measureName, { start: startTime < 0 ? 0 : startTime, end: endTime, detail: { @@ -565,9 +579,10 @@ export function logIOInfoErrored( }, }), ); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, endTime, IO_TRACK, @@ -590,6 +605,7 @@ export function logIOInfo( const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); const color = getIOColor(entryName); const debugTask = ioInfo.debugTask; + const measureName = '\u200b' + entryName; if (__DEV__ && debugTask) { const properties: Array<[string, string]> = []; if (typeof value === 'object' && value !== null) { @@ -605,7 +621,7 @@ export function logIOInfo( ); debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + entryName, { + performance.measure.bind(performance, measureName, { start: startTime < 0 ? 0 : startTime, end: endTime, detail: { @@ -618,9 +634,10 @@ export function logIOInfo( }, }), ); + performance.clearMeasures(measureName); } else { console.timeStamp( - entryName, + measureName, startTime < 0 ? 0 : startTime, endTime, IO_TRACK, diff --git a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js index b2d165343a6..5f94bd35e02 100644 --- a/packages/react-reconciler/src/ReactFiberPerformanceTrack.js +++ b/packages/react-reconciler/src/ReactFiberPerformanceTrack.js @@ -133,6 +133,7 @@ function logComponentTrigger( } else { performance.measure(trigger, reusableComponentOptions); } + performance.clearMeasures(trigger); } } @@ -200,7 +201,7 @@ const reusableComponentOptions: PerformanceMeasureOptions = { }, }; -const resuableChangedPropsEntry = ['Changed Props', '']; +const reusableChangedPropsEntry = ['Changed Props', '']; const DEEP_EQUALITY_WARNING = 'This component received deeply equal props. It might benefit from useMemo or the React Compiler in its owner.'; @@ -261,7 +262,7 @@ export function logComponentRender( alternate.memoizedProps !== props ) { // If this is an update, we'll diff the props and emit which ones changed. - const properties: Array<[string, string]> = [resuableChangedPropsEntry]; + const properties: Array<[string, string]> = [reusableChangedPropsEntry]; const isDeeplyEqual = addObjectDiffToProperties( alternate.memoizedProps, props, @@ -293,18 +294,20 @@ export function logComponentRender( reusableComponentOptions.start = startTime; reusableComponentOptions.end = endTime; + const measureName = '\u200b' + name; if (debugTask != null) { debugTask.run( // $FlowFixMe[method-unbinding] performance.measure.bind( performance, - '\u200b' + name, + measureName, reusableComponentOptions, ), ); } else { - performance.measure('\u200b' + name, reusableComponentOptions); + performance.measure(measureName, reusableComponentOptions); } + performance.clearMeasures(measureName); } else { if (debugTask != null) { debugTask.run( @@ -421,14 +424,17 @@ export function logComponentErrored( }, }, }; + + const measureName = '\u200b' + name; if (__DEV__ && debugTask) { debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + name, options), + performance.measure.bind(performance, measureName, options), ); } else { - performance.measure('\u200b' + name, options); + performance.measure(measureName, options); } + performance.clearMeasures(measureName); } else { console.timeStamp( name, @@ -488,14 +494,16 @@ function logComponentEffectErrored( }, }; const debugTask = fiber._debugTask; + const measureName = '\u200b' + name; if (debugTask) { debugTask.run( // $FlowFixMe[method-unbinding] - performance.measure.bind(performance, '\u200b' + name, options), + performance.measure.bind(performance, measureName, options), ); } else { - performance.measure('\u200b' + name, options); + performance.measure(measureName, options); } + performance.clearMeasures(measureName); } else { console.timeStamp( name, @@ -762,6 +770,7 @@ export function logBlockingStart( } else { performance.measure(label, measureOptions); } + performance.clearMeasures(label); } else { console.timeStamp( label, @@ -867,6 +876,7 @@ export function logGestureStart( } else { performance.measure(label, measureOptions); } + performance.clearMeasures(label); } else { console.timeStamp( label, @@ -1007,6 +1017,7 @@ export function logTransitionStart( } else { performance.measure(label, measureOptions); } + performance.clearMeasures(label); } else { console.timeStamp( label, @@ -1238,6 +1249,7 @@ export function logRecoveredRenderPhase( } else { performance.measure('Recovered', options); } + performance.clearMeasures('Recovered'); } else { console.timeStamp( 'Recovered', @@ -1449,6 +1461,7 @@ export function logCommitErrored( } else { performance.measure('Errored', options); } + performance.clearMeasures('Errored'); } else { console.timeStamp( 'Errored',