From 4d7e946d46e685327df1fe5d28b438805e999701 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 12 Oct 2025 23:00:53 -0400 Subject: [PATCH 1/2] Group suspended by rows by the same name --- .../Components/InspectedElementSuspendedBy.js | 187 ++++++++++++++++-- 1 file changed, 172 insertions(+), 15 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..463b3a52deb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -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'} /> - {name} - {shortDescription === '' ? null : ( + + {skipName ? shortDescription : name} + + {skipName || shortDescription === '' ? null : ( <> {' ('} @@ -331,6 +335,109 @@ 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; + } + } + return ( +
+ + {isOpen && + suspendedBy.map(({value, index}) => ( + + ))} +
+ ); +} + export default function InspectedElementSuspendedBy({ bridge, element, @@ -390,6 +497,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 +558,48 @@ export default function InspectedElementSuspendedBy({ - {sortedSuspendedBy.map(({value, index}) => ( - - ))} + {groups.length === 1 + ? // If it's only one type of suspender we can flatten it. + groups[0].map(entry => ( + + )) + : groups.map((entries, index) => + entries.length === 1 ? ( + + ) : ( + + ), + )} {unknownSuspenders} ); From 33bc070aae25ebb69c0354ffcd76e3a5a566c3e7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 13 Oct 2025 00:09:38 -0400 Subject: [PATCH 2/2] Pluralize the name when used as the group title E.g. script -> scripts fetch -> fetches --- .../Components/InspectedElementSuspendedBy.js | 7 ++-- .../src/devtools/views/utils.js | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 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 463b3a52deb..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'; @@ -391,6 +391,7 @@ function SuspendedByGroup({ left = 95; } } + const pluralizedName = pluralize(name); return (