Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 76 additions & 17 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
import {
extractLocationFromComponentStack,
extractLocationFromOwnerStack,
parseStackTrace,
} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
import {
cleanForBridge,
Expand Down Expand Up @@ -4441,10 +4442,10 @@ export function attach(

function getSuspendedByOfSuspenseNode(
suspenseNode: SuspenseNode,
): Array<ReactAsyncInfo> {
): Array<SerializedAsyncInfo> {
// Collect all ReactAsyncInfo that was suspending this SuspenseNode but
// isn't also in any parent set.
const result: Array<ReactAsyncInfo> = [];
const result: Array<SerializedAsyncInfo> = [];
if (!suspenseNode.hasUniqueSuspenders) {
return result;
}
Expand All @@ -4469,7 +4470,8 @@ export function attach(
ioInfo,
);
if (asyncInfo !== null) {
result.push(asyncInfo);
const index = result.length;
result.push(serializeAsyncInfo(asyncInfo, index, firstInstance));
}
}
});
Expand All @@ -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 (
Expand Down Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -146,20 +150,39 @@ function SuspendedByRow({
</Button>
{isOpen && (
<div className={styles.CollapsableContent}>
{stack !== null && stack.length > 0 && (
<StackTraceView stack={stack} />
)}
{owner !== null && owner.id !== inspectedElement.id ? (
{showIOStack && <StackTraceView stack={ioInfo.stack} />}
{(showIOStack || !showAwaitStack) &&
ioOwner !== null &&
ioOwner.id !== inspectedElement.id ? (
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
key={ioOwner.id}
displayName={ioOwner.displayName || 'Anonymous'}
hocDisplayNames={ioOwner.hocDisplayNames}
compiledWithForget={ioOwner.compiledWithForget}
id={ioOwner.id}
isInStore={store.containsElement(ioOwner.id)}
type={ioOwner.type}
/>
) : null}
{showAwaitStack ? (
<>
<div className={styles.SmallHeader}>awaited at:</div>
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
<StackTraceView stack={asyncInfo.stack} />
)}
{asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
<OwnerView
key={asyncOwner.id}
displayName={asyncOwner.displayName || 'Anonymous'}
hocDisplayNames={asyncOwner.hocDisplayNames}
compiledWithForget={asyncOwner.compiledWithForget}
id={asyncOwner.id}
isInStore={store.containsElement(asyncOwner.id)}
type={asyncOwner.type}
/>
) : null}
</>
) : null}
<div className={styles.PreviewContainer}>
<KeyValue
alphaSort={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@
*/

import * as React from 'react';
import {use, useContext} from 'react';

import useOpenResource from '../useOpenResource';

import styles from './StackTraceView.css';

import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes';
import type {
ReactStackTrace,
ReactCallSite,
ReactFunctionLocation,
} from 'shared/ReactTypes';

import FetchFileWithCachingContext from './FetchFileWithCachingContext';

import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';

import formatLocationForDisplay from './formatLocationForDisplay';

Expand All @@ -22,7 +31,23 @@ type CallSiteViewProps = {
};

export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
const symbolicatedCallSite: null | ReactCallSite = null; // TODO
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);

const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
callSite;

const symbolicatedCallSite: null | ReactFunctionLocation =
fetchFileWithCaching !== null
? use(
symbolicateSourceWithCache(
fetchFileWithCaching,
virtualURL,
virtualLine,
virtualColumn,
),
)
: null;

const [linkIsEnabled, viewSource] = useOpenResource(
callSite,
symbolicatedCallSite,
Expand All @@ -31,7 +56,7 @@ export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
symbolicatedCallSite !== null ? symbolicatedCallSite : callSite;
return (
<div className={styles.CallSite}>
{functionName}
{functionName || virtualFunctionName}
{' @ '}
<span
className={linkIsEnabled ? styles.Link : null}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-devtools-shared/src/symbolicateSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const symbolicationCache: Map<
Promise<ReactFunctionLocation | null>,
> = new Map();

export async function symbolicateSourceWithCache(
export function symbolicateSourceWithCache(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
line: number, // 1-based
Expand Down
Loading