From 7060a2c9fd007f2300cae85533049735769568c6 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 9 Sep 2025 11:23:12 -0400 Subject: [PATCH 1/3] Dedupe I/O info for an Instance We already do this for SuspenseNode. --- .../src/backend/fiber/renderer.js | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 2102dc1926bc..ca33b0880bcd 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -5778,6 +5778,31 @@ export function attach( return result; } + function getSuspendedByOfInstance( + devtoolsInstance: DevToolsInstance, + hooks: null | HooksTree, + ): Array { + const suspendedBy = devtoolsInstance.suspendedBy; + if (suspendedBy === null) { + return []; + } + + const foundIOEntries: Set = new Set(); + const result: Array = []; + for (let i = 0; i < suspendedBy.length; i++) { + const asyncInfo = suspendedBy[i]; + const ioInfo = asyncInfo.awaited; + if (foundIOEntries.has(ioInfo)) { + // We have already added this I/O entry to the result. We can dedupe it. + // This can happen when an instance depends on the same data in mutliple places. + continue; + } + foundIOEntries.add(ioInfo); + result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks)); + } + return result; + } + const FALLBACK_THROTTLE_MS: number = 300; function getSuspendedByRange( @@ -6297,11 +6322,7 @@ export function attach( // In this case, this becomes associated with the Client/Host Component where as normally // you'd expect these to be associated with the Server Component that awaited the data. // TODO: Prepend other suspense sources like css, images and use(). - fiberInstance.suspendedBy === null - ? [] - : fiberInstance.suspendedBy.map(info => - serializeAsyncInfo(info, fiberInstance, hooks), - ); + getSuspendedByOfInstance(fiberInstance, hooks); const suspendedByRange = getSuspendedByRange( getNearestSuspenseNode(fiberInstance), ); @@ -6446,7 +6467,7 @@ export function attach( const isSuspended = null; // Things that Suspended this Server Component (use(), awaits and direct child promises) - const suspendedBy = virtualInstance.suspendedBy; + const suspendedBy = getSuspendedByOfInstance(virtualInstance, null); const suspendedByRange = getSuspendedByRange( getNearestSuspenseNode(virtualInstance), ); @@ -6497,12 +6518,7 @@ export function attach( ? [] : Array.from(componentLogsEntry.warnings.entries()), - suspendedBy: - suspendedBy === null - ? [] - : suspendedBy.map(info => - serializeAsyncInfo(info, virtualInstance, null), - ), + suspendedBy: suspendedBy, suspendedByRange: suspendedByRange, unknownSuspenders: UNKNOWN_SUSPENDERS_NONE, From e13f58345d9a86338a73f07c6f8a2c4df790ca2c Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 9 Sep 2025 13:56:41 -0400 Subject: [PATCH 2/3] Pick the I/O entry with the highest end/bytesize for the same stream This is a hack since it special cases the RSC stream. Ideally this concept would be modelled explicitly. --- .../src/backend/fiber/renderer.js | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index ca33b0880bcd..7c5684570e6a 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -5733,6 +5733,15 @@ export function attach( // to a specific instance will have those appear in order of when that instance was discovered. let hooksCacheKey: null | DevToolsInstance = null; let hooksCache: null | HooksTree = null; + // Collect the stream entries with the highest byte offset and end time. + const streamEntries: Map< + Promise, + { + asyncInfo: ReactAsyncInfo, + instance: DevToolsInstance, + hooks: null | HooksTree, + }, + > = new Map(); suspenseNode.suspendedBy.forEach((set, ioInfo) => { let parentNode = suspenseNode.parent; while (parentNode !== null) { @@ -5771,10 +5780,42 @@ export function attach( } } } - result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks)); + const newIO = asyncInfo.awaited; + if (newIO.name === 'RSC stream' && newIO.value != null) { + const streamPromise = newIO.value; + // Special case RSC stream entries to pick the last entry keyed by the stream. + const existingEntry = streamEntries.get(streamPromise); + if (existingEntry === undefined) { + streamEntries.set(streamPromise, { + asyncInfo, + instance: firstInstance, + hooks, + }); + } else { + const existingIO = existingEntry.asyncInfo.awaited; + if ( + newIO !== existingIO && + ((newIO.byteSize !== undefined && + existingIO.byteSize !== undefined && + newIO.byteSize > existingIO.byteSize) || + newIO.end > existingIO.end) + ) { + // The new entry is later in the stream that the old entry. Replace it. + existingEntry.asyncInfo = asyncInfo; + existingEntry.instance = firstInstance; + existingEntry.hooks = hooks; + } + } + } else { + result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks)); + } } } }); + // Add any deduped stream entries. + streamEntries.forEach(({asyncInfo, instance, hooks}) => { + result.push(serializeAsyncInfo(asyncInfo, instance, hooks)); + }); return result; } From c19065e1c67b24d81b80a96a63ac44aa46b6f0de Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 9 Sep 2025 14:05:00 -0400 Subject: [PATCH 3/3] Same thing for instances --- .../src/backend/fiber/renderer.js | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 7c5684570e6a..536df1f6b452 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -5829,6 +5829,7 @@ export function attach( } const foundIOEntries: Set = new Set(); + const streamEntries: Map, ReactAsyncInfo> = new Map(); const result: Array = []; for (let i = 0; i < suspendedBy.length; i++) { const asyncInfo = suspendedBy[i]; @@ -5839,8 +5840,33 @@ export function attach( continue; } foundIOEntries.add(ioInfo); - result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks)); + if (ioInfo.name === 'RSC stream' && ioInfo.value != null) { + const streamPromise = ioInfo.value; + // Special case RSC stream entries to pick the last entry keyed by the stream. + const existingEntry = streamEntries.get(streamPromise); + if (existingEntry === undefined) { + streamEntries.set(streamPromise, asyncInfo); + } else { + const existingIO = existingEntry.awaited; + if ( + ioInfo !== existingIO && + ((ioInfo.byteSize !== undefined && + existingIO.byteSize !== undefined && + ioInfo.byteSize > existingIO.byteSize) || + ioInfo.end > existingIO.end) + ) { + // The new entry is later in the stream that the old entry. Replace it. + streamEntries.set(streamPromise, asyncInfo); + } + } + } else { + result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks)); + } } + // Add any deduped stream entries. + streamEntries.forEach(asyncInfo => { + result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks)); + }); return result; }