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
162 changes: 131 additions & 31 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ import type {
ReactDebugInfo,
ReactAsyncInfo,
ReactIOInfo,
ReactStackTrace,
ReactCallSite,
} from 'shared/ReactTypes';

import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';

import {
ComponentFilterDisplayName,
ComponentFilterElementType,
Expand Down Expand Up @@ -5187,6 +5191,32 @@ export function attach(
return null;
}

function inspectHooks(fiber: Fiber): HooksTree {
const originalConsoleMethods: {[string]: $FlowFixMe} = {};

// Temporarily disable all console logging before re-running the hook.
for (const method in console) {
try {
// $FlowFixMe[invalid-computed-prop]
originalConsoleMethods[method] = console[method];
// $FlowFixMe[prop-missing]
console[method] = () => {};
} catch (error) {}
}

try {
return inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
} finally {
// Restore original console functionality.
for (const method in originalConsoleMethods) {
try {
// $FlowFixMe[prop-missing]
console[method] = originalConsoleMethods[method];
} catch (error) {}
}
}
}

function getSuspendedByOfSuspenseNode(
suspenseNode: SuspenseNode,
): Array<SerializedAsyncInfo> {
Expand All @@ -5196,6 +5226,11 @@ export function attach(
if (!suspenseNode.hasUniqueSuspenders) {
return result;
}
// Cache the inspection of Hooks in case we need it for multiple entries.
// We don't need a full map here since it's likely that every ioInfo that's unique
// to a specific instance will have those appear in order of when that instance was discovered.
let hooksCacheKey: null | DevToolsInstance = null;
let hooksCache: null | HooksTree = null;
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
let parentNode = suspenseNode.parent;
while (parentNode !== null) {
Expand All @@ -5217,18 +5252,100 @@ export function attach(
ioInfo,
);
if (asyncInfo !== null) {
const index = result.length;
result.push(serializeAsyncInfo(asyncInfo, index, firstInstance));
let hooks: null | HooksTree = null;
if (asyncInfo.stack == null && asyncInfo.owner == null) {
if (hooksCacheKey === firstInstance) {
hooks = hooksCache;
} else if (firstInstance.kind !== VIRTUAL_INSTANCE) {
const fiber = firstInstance.data;
if (
fiber.dependencies &&
fiber.dependencies._debugThenableState
) {
// This entry had no stack nor owner but this Fiber used Hooks so we might
// be able to get the stack from the Hook.
hooksCacheKey = firstInstance;
hooksCache = hooks = inspectHooks(fiber);
}
}
}
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
}
}
});
return result;
}

function getAwaitStackFromHooks(
hooks: HooksTree,
asyncInfo: ReactAsyncInfo,
): null | ReactStackTrace {
// TODO: We search through the hooks tree generated by inspectHooksOfFiber so that we can
// use the information already extracted but ideally this search would be faster since we
// could know which index to extract from the debug state.
for (let i = 0; i < hooks.length; i++) {
const node = hooks[i];
const debugInfo = node.debugInfo;
if (debugInfo != null && debugInfo.indexOf(asyncInfo) !== -1) {
// Found a matching Hook. We'll now use its source location to construct a stack.
const source = node.hookSource;
if (
source != null &&
source.functionName !== null &&
source.fileName !== null &&
source.lineNumber !== null &&
source.columnNumber !== null
) {
// Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite.
const callSite: ReactCallSite = [
source.functionName,
source.fileName,
source.lineNumber,
source.columnNumber,
0,
0,
false,
];
// As we return we'll add any custom hooks parent stacks to the array.
return [callSite];
} else {
return [];
}
}
// Otherwise, search the sub hooks of any custom hook.
const matchedStack = getAwaitStackFromHooks(node.subHooks, asyncInfo);
if (matchedStack !== null) {
// Append this custom hook to the stack trace since it must have been called inside of it.
const source = node.hookSource;
if (
source != null &&
source.functionName !== null &&
source.fileName !== null &&
source.lineNumber !== null &&
source.columnNumber !== null
) {
// Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite.
const callSite: ReactCallSite = [
source.functionName,
source.fileName,
source.lineNumber,
source.columnNumber,
0,
0,
false,
];
matchedStack.push(callSite);
}
return matchedStack;
}
}
return null;
}

function serializeAsyncInfo(
asyncInfo: ReactAsyncInfo,
index: number,
parentInstance: DevToolsInstance,
hooks: null | HooksTree,
): SerializedAsyncInfo {
const ioInfo = asyncInfo.awaited;
const ioOwnerInstance = findNearestOwnerInstance(
Expand Down Expand Up @@ -5268,6 +5385,11 @@ export function attach(
// 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.
if (hooks !== null) {
// If this component used Hooks we might be able to instead infer the stack from the
// use() callsite if this async info came from a hook. Let's search the tree to find it.
awaitStack = getAwaitStackFromHooks(hooks, asyncInfo);
}
break;
default:
// If we awaited by passing a Promise to a built-in element, then the JSX callsite is a
Expand Down Expand Up @@ -5538,31 +5660,9 @@ export function attach(
const owners: null | Array<SerializedElement> =
getOwnersListFromInstance(fiberInstance);

let hooks = null;
let hooks: null | HooksTree = null;
if (usesHooks) {
const originalConsoleMethods: {[string]: $FlowFixMe} = {};

// Temporarily disable all console logging before re-running the hook.
for (const method in console) {
try {
// $FlowFixMe[invalid-computed-prop]
originalConsoleMethods[method] = console[method];
// $FlowFixMe[prop-missing]
console[method] = () => {};
} catch (error) {}
}

try {
hooks = inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
} finally {
// Restore original console functionality.
for (const method in originalConsoleMethods) {
try {
// $FlowFixMe[prop-missing]
console[method] = originalConsoleMethods[method];
} catch (error) {}
}
}
hooks = inspectHooks(fiber);
}

let rootType = null;
Expand Down Expand Up @@ -5641,8 +5741,8 @@ export function attach(
// TODO: Prepend other suspense sources like css, images and use().
fiberInstance.suspendedBy === null
? []
: fiberInstance.suspendedBy.map((info, index) =>
serializeAsyncInfo(info, index, fiberInstance),
: fiberInstance.suspendedBy.map(info =>
serializeAsyncInfo(info, fiberInstance, hooks),
);
return {
id: fiberInstance.id,
Expand Down Expand Up @@ -5813,8 +5913,8 @@ export function attach(
suspendedBy:
suspendedBy === null
? []
: suspendedBy.map((info, index) =>
serializeAsyncInfo(info, index, virtualInstance),
: suspendedBy.map(info =>
serializeAsyncInfo(info, virtualInstance, null),
),

// List of owners
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,21 @@ function SuspendedByRow({
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const ioInfo = asyncInfo.awaited;
const name = ioInfo.name;
let name = ioInfo.name;
if (name === '' || name === 'Promise') {
// If all we have is a generic name, we can try to infer a better name from
// the stack. We only do this if the stack has more than one frame since
// otherwise it's likely to just be the name of the component which isn't better.
const bestStack = ioInfo.stack || asyncInfo.stack;
if (bestStack !== null && bestStack.length > 1) {
// TODO: Ideally we'd get the name from the last ignore listed frame before the
// first visible frame since this is the same algorithm as the Flight server uses.
// Ideally, we'd also get the name from the source mapped entry instead of the
// original entry. However, that would require suspending the immediate display
// of these rows to first do source mapping before we can show the name.
Comment on lines +94 to +95
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or put the sourcemapped asyncInfo into useDeferredValue in the parent

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would still need to show something faster for each keystroke so a placeholder with same height is probably best.

name = bestStack[0][0];
}
}
const description = ioInfo.description;
const longName = description === '' ? name : name + ' (' + description + ')';
const shortDescription = getShortDescription(name, description);
Expand Down
Loading