From 9b94749ede62705b0c567ec7c6d5e0b1652f24ef Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 10 Aug 2025 23:47:23 -0400 Subject: [PATCH 1/3] Track virtual debug info from suspensey CSS --- .../src/backend/fiber/renderer.js | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 41436e8a6e9c9..0f5caef419694 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -3215,6 +3215,92 @@ export function attach( } } + const hostAsyncInfoCache: WeakMap<{...}, ReactAsyncInfo> = new WeakMap(); + + function trackDebugInfoFromHostResource( + devtoolsInstance: DevToolsInstance, + fiber: Fiber, + ): void { + const resource: ?{ + type: 'stylesheet' | 'style' | 'script' | 'void', + instance?: null | HostInstance, + ... + } = fiber.memoizedState; + if (resource == null) { + return; + } + + // Use a cached entry based on the resource. This ensures that if we use the same + // resource in multiple places, it gets deduped and inner boundaries don't consider it + // as contributing to those boundaries. + const existingEntry = hostAsyncInfoCache.get(resource); + if (existingEntry !== undefined) { + insertSuspendedBy(existingEntry); + return; + } + + const props: { + href?: string, + media?: string, + ... + } = fiber.memoizedProps; + + // Stylesheet resources may suspend. We need to track that. + const mayResourceSuspendCommit = + resource.type === 'stylesheet' && + // If it doesn't match the currently debugged media, then it doesn't count. + (typeof props.media !== 'string' || + typeof matchMedia !== 'function' || + matchMedia(props.media)); + if (!mayResourceSuspendCommit) { + return; + } + + const instance = resource.instance; + if (instance == null) { + return; + } + + // Unlike props.href, this href will be fully qualified which we need for comparison below. + const href = instance.href; + if (typeof href !== 'string') { + return; + } + let start = -1; + let end = -1; + // $FlowFixMe[method-unbinding] + if (typeof performance.getEntriesByType === 'function') { + // We may be able to collect the start and end time of this resource from Performance Observer. + const resourceEntries = performance.getEntriesByType('resource'); + for (let i = 0; i < resourceEntries.length; i++) { + const resourceEntry = resourceEntries[i]; + if (resourceEntry.name === href) { + start = resourceEntry.startTime; + end = start + resourceEntry.duration; + } + } + } + const value = instance.sheet; + const promise = Promise.resolve(value); + (promise: any).status = 'fulfilled'; + (promise: any).value = value; + const ioInfo: ReactIOInfo = { + name: 'stylesheet', + start, + end, + value: promise, + }; + const asyncInfo: ReactAsyncInfo = { + awaited: ioInfo, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber._debugOwner == null ? null : fiber._debugOwner, + debugStack: fiber._debugStack == null ? null : fiber._debugStack, + debugTask: fiber._debugTask == null ? null : fiber._debugTask, + }; + hostAsyncInfoCache.set(resource, asyncInfo); + insertSuspendedBy(asyncInfo); + } + function mountVirtualChildrenRecursively( firstChild: Fiber, lastChild: null | Fiber, // non-inclusive @@ -3442,6 +3528,7 @@ export function attach( throw new Error('Did not expect a host hoistable to be the root'); } aquireHostResource(nearestInstance, fiber.memoizedState); + trackDebugInfoFromHostResource(nearestInstance, fiber); } else if ( fiber.tag === HostComponent || fiber.tag === HostText || @@ -4278,6 +4365,7 @@ export function attach( } releaseHostResource(nearestInstance, prevFiber.memoizedState); aquireHostResource(nearestInstance, nextFiber.memoizedState); + trackDebugInfoFromHostResource(nearestInstance, nextFiber); } else if ( (nextFiber.tag === HostComponent || nextFiber.tag === HostText || From 02e01dd04cd77b3c80dfab3cd2b5f8245021a619 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 11 Aug 2025 01:36:25 -0400 Subject: [PATCH 2/3] Link to the link itself if it's not filtered Always show the io owner if it's not the same as the other owner. --- .../react-devtools-shared/src/backend/fiber/renderer.js | 2 ++ .../views/Components/InspectedElementSuspendedBy.js | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 0f5caef419694..59f2ee67cfa89 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -3289,6 +3289,8 @@ export function attach( start, end, value: promise, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber, // Allow linking to the if it's not filtered. }; const asyncInfo: ReactAsyncInfo = { awaited: ioInfo, 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 3608cff85ce55..80cb8673cd96b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -164,9 +164,12 @@ function SuspendedByRow({ } /> )} - {(showIOStack || !showAwaitStack) && - ioOwner !== null && - ioOwner.id !== inspectedElement.id ? ( + {ioOwner !== null && + ioOwner.id !== inspectedElement.id && + (showIOStack || + !showAwaitStack || + asyncOwner === null || + ioOwner.id !== asyncOwner.id) ? ( Date: Mon, 11 Aug 2025 01:36:43 -0400 Subject: [PATCH 3/3] Extract the description from href properties --- packages/shared/ReactIODescription.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/shared/ReactIODescription.js b/packages/shared/ReactIODescription.js index 7767b93da4653..10c888213ddb7 100644 --- a/packages/shared/ReactIODescription.js +++ b/packages/shared/ReactIODescription.js @@ -24,6 +24,8 @@ export function getIODescription(value: any): string { return String(value.message); } else if (typeof value.url === 'string') { return value.url; + } else if (typeof value.href === 'string') { + return value.href; } else if (typeof value.command === 'string') { return value.command; } else if (