diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 5ffa5251dca..a8fe8f24283 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -59,6 +59,7 @@ import { import { extractLocationFromComponentStack, extractLocationFromOwnerStack, + parseStackTrace, } from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { cleanForBridge, @@ -4441,10 +4442,10 @@ export function attach( function getSuspendedByOfSuspenseNode( suspenseNode: SuspenseNode, - ): Array { + ): Array { // Collect all ReactAsyncInfo that was suspending this SuspenseNode but // isn't also in any parent set. - const result: Array = []; + const result: Array = []; if (!suspenseNode.hasUniqueSuspenders) { return result; } @@ -4469,7 +4470,8 @@ export function attach( ioInfo, ); if (asyncInfo !== null) { - result.push(asyncInfo); + const index = result.length; + result.push(serializeAsyncInfo(asyncInfo, index, firstInstance)); } } }); @@ -4486,10 +4488,63 @@ export function attach( parentInstance, ioInfo.owner, ); - const awaitOwnerInstance = findNearestOwnerInstance( - parentInstance, - asyncInfo.owner, - ); + let awaitStack = + asyncInfo.debugStack == null + ? null + : // While we have a ReactStackTrace on ioInfo.stack, that will point to the location on + // the server. We need a location that points to the virtual source on the client which + // we can then use to source map to the original location. + parseStackTrace(asyncInfo.debugStack, 1); + let awaitOwnerInstance: null | FiberInstance | VirtualInstance; + if ( + asyncInfo.owner == null && + (awaitStack === null || awaitStack.length === 0) + ) { + // We had no owner nor stack for the await. This can happen if you render it as a child + // or throw a Promise. Replace it with the parent as the await. + awaitStack = null; + awaitOwnerInstance = + parentInstance.kind === FILTERED_FIBER_INSTANCE ? null : parentInstance; + if ( + parentInstance.kind === FIBER_INSTANCE || + parentInstance.kind === FILTERED_FIBER_INSTANCE + ) { + const fiber = parentInstance.data; + switch (fiber.tag) { + case ClassComponent: + case FunctionComponent: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + case IndeterminateComponent: + case MemoComponent: + case SimpleMemoComponent: + // If we awaited in the child position of a component, then the best stack would be the + // return callsite but we don't have that available so instead we skip. The callsite of + // the JSX would be misleading in this case. The same thing happens with throw-a-Promise. + break; + default: + // If we awaited by passing a Promise to a built-in element, then the JSX callsite is a + // good stack trace to use for the await. + if ( + fiber._debugOwner != null && + fiber._debugStack != null && + typeof fiber._debugStack !== 'string' + ) { + awaitStack = parseStackTrace(fiber._debugStack, 1); + awaitOwnerInstance = findNearestOwnerInstance( + parentInstance, + fiber._debugOwner, + ); + } + } + } + } else { + awaitOwnerInstance = findNearestOwnerInstance( + parentInstance, + asyncInfo.owner, + ); + } + const value: any = ioInfo.value; let resolvedValue = undefined; if ( @@ -4518,14 +4573,20 @@ export function attach( ioOwnerInstance === null ? null : instanceToSerializedElement(ioOwnerInstance), - stack: ioInfo.stack == null ? null : ioInfo.stack, + stack: + ioInfo.debugStack == null + ? null + : // While we have a ReactStackTrace on ioInfo.stack, that will point to the location on + // the server. We need a location that points to the virtual source on the client which + // we can then use to source map to the original location. + parseStackTrace(ioInfo.debugStack, 1), }, env: asyncInfo.env == null ? null : asyncInfo.env, owner: awaitOwnerInstance === null ? null : instanceToSerializedElement(awaitOwnerInstance), - stack: asyncInfo.stack == null ? null : asyncInfo.stack, + stack: awaitStack, }; } @@ -4831,8 +4892,11 @@ 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; - + fiberInstance.suspendedBy === null + ? [] + : fiberInstance.suspendedBy.map((info, index) => + serializeAsyncInfo(info, index, fiberInstance), + ); return { id: fiberInstance.id, @@ -4889,12 +4953,7 @@ export function attach( ? [] : Array.from(componentLogsEntry.warnings.entries()), - suspendedBy: - suspendedBy === null - ? [] - : suspendedBy.map((info, index) => - serializeAsyncInfo(info, index, fiberInstance), - ), + suspendedBy: suspendedBy, // List of owners owners, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css index ded305bbc66..0fb5107361c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css @@ -123,3 +123,10 @@ .TimeBarSpanErrored { background-color: var(--color-timespan-background-errored); } + +.SmallHeader { + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); + padding-left: 1.25rem; + margin-top: 0.25rem; +} 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 c7d0b39df3b..79fdbd1a361 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -80,21 +80,13 @@ function SuspendedByRow({ maxTime, }: RowProps) { const [isOpen, setIsOpen] = useState(false); - const name = asyncInfo.awaited.name; - const description = asyncInfo.awaited.description; + const ioInfo = asyncInfo.awaited; + const name = ioInfo.name; + const description = ioInfo.description; const longName = description === '' ? name : name + ' (' + description + ')'; const shortDescription = getShortDescription(name, description); - let stack; - let owner; - if (asyncInfo.stack === null || asyncInfo.stack.length === 0) { - stack = asyncInfo.awaited.stack; - owner = asyncInfo.awaited.owner; - } else { - stack = asyncInfo.stack; - owner = asyncInfo.owner; - } - const start = asyncInfo.awaited.start; - const end = asyncInfo.awaited.end; + const start = ioInfo.start; + const end = ioInfo.end; const timeScale = 100 / (maxTime - minTime); let left = (start - minTime) * timeScale; let width = (end - start) * timeScale; @@ -106,7 +98,19 @@ function SuspendedByRow({ } } - const value: any = asyncInfo.awaited.value; + const ioOwner = ioInfo.owner; + const asyncOwner = asyncInfo.owner; + const showIOStack = ioInfo.stack !== null && ioInfo.stack.length !== 0; + // Only show the awaited stack if the I/O started in a different owner + // than where it was awaited. If it's started by the same component it's + // probably easy enough to infer and less noise in the common case. + const showAwaitStack = + !showIOStack || + (ioOwner === null + ? asyncOwner !== null + : asyncOwner === null || ioOwner.id !== asyncOwner.id); + + const value: any = ioInfo.value; const metaName = value !== null && typeof value === 'object' ? value[meta.name] : null; const isFulfilled = metaName === 'fulfilled Thenable'; @@ -146,20 +150,39 @@ function SuspendedByRow({ {isOpen && (
- {stack !== null && stack.length > 0 && ( - - )} - {owner !== null && owner.id !== inspectedElement.id ? ( + {showIOStack && } + {(showIOStack || !showAwaitStack) && + ioOwner !== null && + ioOwner.id !== inspectedElement.id ? ( ) : null} + {showAwaitStack ? ( + <> +
awaited at:
+ {asyncInfo.stack !== null && asyncInfo.stack.length > 0 && ( + + )} + {asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? ( + + ) : null} + + ) : null}
- {functionName} + {functionName || virtualFunctionName} {' @ '} , > = new Map(); -export async function symbolicateSourceWithCache( +export function symbolicateSourceWithCache( fetchFileWithCaching: FetchFileWithCaching, sourceURL: string, line: number, // 1-based