Skip to content

Commit 288d428

Browse files
authored
[DevTools] Only show the highest end/byteSize I/O of RSC streams (#34435)
Stacked on #34425. RSC stream info is split into one I/O entry per chunk. This means that when a single instance or boundary depends on multiple chunks, it'll show the same stream multiple times. This makes it so just the last one is shown. This is a special case for the name "RSC stream" but ideally we'd more explicitly model the concept of awaiting only part of a stream. <img width="667" height="427" alt="Screenshot 2025-09-09 at 2 09 43 PM" src="https://github.com/user-attachments/assets/890f6f61-4657-4ca9-82fd-df55a696bacc" /> Another remaining issue is that it's possible for an intermediate chunk to be depended on by just a child boundary. In that case that can be considered a "unique suspender" even though the parent depends on a later one. Ideally it would dedupe on everything below. Could also model it as every Promise depends on its chunk and every previous chunk.
1 parent a34c5df commit 288d428

File tree

1 file changed

+96
-13
lines changed
  • packages/react-devtools-shared/src/backend/fiber

1 file changed

+96
-13
lines changed

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5733,6 +5733,15 @@ export function attach(
57335733
// to a specific instance will have those appear in order of when that instance was discovered.
57345734
let hooksCacheKey: null | DevToolsInstance = null;
57355735
let hooksCache: null | HooksTree = null;
5736+
// Collect the stream entries with the highest byte offset and end time.
5737+
const streamEntries: Map<
5738+
Promise<mixed>,
5739+
{
5740+
asyncInfo: ReactAsyncInfo,
5741+
instance: DevToolsInstance,
5742+
hooks: null | HooksTree,
5743+
},
5744+
> = new Map();
57365745
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
57375746
let parentNode = suspenseNode.parent;
57385747
while (parentNode !== null) {
@@ -5771,9 +5780,92 @@ export function attach(
57715780
}
57725781
}
57735782
}
5774-
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
5783+
const newIO = asyncInfo.awaited;
5784+
if (newIO.name === 'RSC stream' && newIO.value != null) {
5785+
const streamPromise = newIO.value;
5786+
// Special case RSC stream entries to pick the last entry keyed by the stream.
5787+
const existingEntry = streamEntries.get(streamPromise);
5788+
if (existingEntry === undefined) {
5789+
streamEntries.set(streamPromise, {
5790+
asyncInfo,
5791+
instance: firstInstance,
5792+
hooks,
5793+
});
5794+
} else {
5795+
const existingIO = existingEntry.asyncInfo.awaited;
5796+
if (
5797+
newIO !== existingIO &&
5798+
((newIO.byteSize !== undefined &&
5799+
existingIO.byteSize !== undefined &&
5800+
newIO.byteSize > existingIO.byteSize) ||
5801+
newIO.end > existingIO.end)
5802+
) {
5803+
// The new entry is later in the stream that the old entry. Replace it.
5804+
existingEntry.asyncInfo = asyncInfo;
5805+
existingEntry.instance = firstInstance;
5806+
existingEntry.hooks = hooks;
5807+
}
5808+
}
5809+
} else {
5810+
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
5811+
}
5812+
}
5813+
}
5814+
});
5815+
// Add any deduped stream entries.
5816+
streamEntries.forEach(({asyncInfo, instance, hooks}) => {
5817+
result.push(serializeAsyncInfo(asyncInfo, instance, hooks));
5818+
});
5819+
return result;
5820+
}
5821+
5822+
function getSuspendedByOfInstance(
5823+
devtoolsInstance: DevToolsInstance,
5824+
hooks: null | HooksTree,
5825+
): Array<SerializedAsyncInfo> {
5826+
const suspendedBy = devtoolsInstance.suspendedBy;
5827+
if (suspendedBy === null) {
5828+
return [];
5829+
}
5830+
5831+
const foundIOEntries: Set<ReactIOInfo> = new Set();
5832+
const streamEntries: Map<Promise<mixed>, ReactAsyncInfo> = new Map();
5833+
const result: Array<SerializedAsyncInfo> = [];
5834+
for (let i = 0; i < suspendedBy.length; i++) {
5835+
const asyncInfo = suspendedBy[i];
5836+
const ioInfo = asyncInfo.awaited;
5837+
if (foundIOEntries.has(ioInfo)) {
5838+
// We have already added this I/O entry to the result. We can dedupe it.
5839+
// This can happen when an instance depends on the same data in mutliple places.
5840+
continue;
5841+
}
5842+
foundIOEntries.add(ioInfo);
5843+
if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
5844+
const streamPromise = ioInfo.value;
5845+
// Special case RSC stream entries to pick the last entry keyed by the stream.
5846+
const existingEntry = streamEntries.get(streamPromise);
5847+
if (existingEntry === undefined) {
5848+
streamEntries.set(streamPromise, asyncInfo);
5849+
} else {
5850+
const existingIO = existingEntry.awaited;
5851+
if (
5852+
ioInfo !== existingIO &&
5853+
((ioInfo.byteSize !== undefined &&
5854+
existingIO.byteSize !== undefined &&
5855+
ioInfo.byteSize > existingIO.byteSize) ||
5856+
ioInfo.end > existingIO.end)
5857+
) {
5858+
// The new entry is later in the stream that the old entry. Replace it.
5859+
streamEntries.set(streamPromise, asyncInfo);
5860+
}
57755861
}
5862+
} else {
5863+
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
57765864
}
5865+
}
5866+
// Add any deduped stream entries.
5867+
streamEntries.forEach(asyncInfo => {
5868+
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
57775869
});
57785870
return result;
57795871
}
@@ -6297,11 +6389,7 @@ export function attach(
62976389
// In this case, this becomes associated with the Client/Host Component where as normally
62986390
// you'd expect these to be associated with the Server Component that awaited the data.
62996391
// TODO: Prepend other suspense sources like css, images and use().
6300-
fiberInstance.suspendedBy === null
6301-
? []
6302-
: fiberInstance.suspendedBy.map(info =>
6303-
serializeAsyncInfo(info, fiberInstance, hooks),
6304-
);
6392+
getSuspendedByOfInstance(fiberInstance, hooks);
63056393
const suspendedByRange = getSuspendedByRange(
63066394
getNearestSuspenseNode(fiberInstance),
63076395
);
@@ -6446,7 +6534,7 @@ export function attach(
64466534
64476535
const isSuspended = null;
64486536
// Things that Suspended this Server Component (use(), awaits and direct child promises)
6449-
const suspendedBy = virtualInstance.suspendedBy;
6537+
const suspendedBy = getSuspendedByOfInstance(virtualInstance, null);
64506538
const suspendedByRange = getSuspendedByRange(
64516539
getNearestSuspenseNode(virtualInstance),
64526540
);
@@ -6497,12 +6585,7 @@ export function attach(
64976585
? []
64986586
: Array.from(componentLogsEntry.warnings.entries()),
64996587
6500-
suspendedBy:
6501-
suspendedBy === null
6502-
? []
6503-
: suspendedBy.map(info =>
6504-
serializeAsyncInfo(info, virtualInstance, null),
6505-
),
6588+
suspendedBy: suspendedBy,
65066589
suspendedByRange: suspendedByRange,
65076590
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
65086591

0 commit comments

Comments
 (0)