@@ -13,8 +13,12 @@ import type {
1313 ReactDebugInfo ,
1414 ReactAsyncInfo ,
1515 ReactIOInfo ,
16+ ReactStackTrace ,
17+ ReactCallSite ,
1618} from 'shared/ReactTypes' ;
1719
20+ import type { HooksTree } from 'react-debug-tools/src/ReactDebugHooks' ;
21+
1822import {
1923 ComponentFilterDisplayName ,
2024 ComponentFilterElementType ,
@@ -5187,6 +5191,32 @@ export function attach(
51875191 return null;
51885192 }
51895193
5194+ function inspectHooks(fiber: Fiber): HooksTree {
5195+ const originalConsoleMethods: {[string]: $FlowFixMe} = {};
5196+
5197+ // Temporarily disable all console logging before re-running the hook.
5198+ for (const method in console) {
5199+ try {
5200+ // $FlowFixMe[invalid-computed-prop]
5201+ originalConsoleMethods[method] = console[method];
5202+ // $FlowFixMe[prop-missing]
5203+ console[method] = () => {};
5204+ } catch (error) {}
5205+ }
5206+
5207+ try {
5208+ return inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
5209+ } finally {
5210+ // Restore original console functionality.
5211+ for (const method in originalConsoleMethods) {
5212+ try {
5213+ // $FlowFixMe[prop-missing]
5214+ console[method] = originalConsoleMethods[method];
5215+ } catch (error) {}
5216+ }
5217+ }
5218+ }
5219+
51905220 function getSuspendedByOfSuspenseNode(
51915221 suspenseNode: SuspenseNode,
51925222 ): Array<SerializedAsyncInfo> {
@@ -5196,6 +5226,11 @@ export function attach(
51965226 if (!suspenseNode.hasUniqueSuspenders) {
51975227 return result;
51985228 }
5229+ // Cache the inspection of Hooks in case we need it for multiple entries.
5230+ // We don't need a full map here since it's likely that every ioInfo that's unique
5231+ // to a specific instance will have those appear in order of when that instance was discovered.
5232+ let hooksCacheKey: null | DevToolsInstance = null;
5233+ let hooksCache: null | HooksTree = null;
51995234 suspenseNode.suspendedBy.forEach((set, ioInfo) => {
52005235 let parentNode = suspenseNode.parent;
52015236 while (parentNode !== null) {
@@ -5217,18 +5252,100 @@ export function attach(
52175252 ioInfo,
52185253 );
52195254 if (asyncInfo !== null) {
5220- const index = result.length;
5221- result.push(serializeAsyncInfo(asyncInfo, index, firstInstance));
5255+ let hooks: null | HooksTree = null;
5256+ if (asyncInfo.stack == null && asyncInfo.owner == null) {
5257+ if (hooksCacheKey === firstInstance) {
5258+ hooks = hooksCache;
5259+ } else if (firstInstance.kind !== VIRTUAL_INSTANCE) {
5260+ const fiber = firstInstance.data;
5261+ if (
5262+ fiber.dependencies &&
5263+ fiber.dependencies._debugThenableState
5264+ ) {
5265+ // This entry had no stack nor owner but this Fiber used Hooks so we might
5266+ // be able to get the stack from the Hook.
5267+ hooksCacheKey = firstInstance;
5268+ hooksCache = hooks = inspectHooks(fiber);
5269+ }
5270+ }
5271+ }
5272+ result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
52225273 }
52235274 }
52245275 });
52255276 return result;
52265277 }
52275278
5279+ function getAwaitStackFromHooks(
5280+ hooks: HooksTree,
5281+ asyncInfo: ReactAsyncInfo,
5282+ ): null | ReactStackTrace {
5283+ // TODO: We search through the hooks tree generated by inspectHooksOfFiber so that we can
5284+ // use the information already extracted but ideally this search would be faster since we
5285+ // could know which index to extract from the debug state.
5286+ for (let i = 0; i < hooks.length; i++) {
5287+ const node = hooks[i];
5288+ const debugInfo = node.debugInfo;
5289+ if (debugInfo != null && debugInfo.indexOf(asyncInfo) !== -1) {
5290+ // Found a matching Hook. We'll now use its source location to construct a stack.
5291+ const source = node.hookSource;
5292+ if (
5293+ source != null &&
5294+ source.functionName !== null &&
5295+ source.fileName !== null &&
5296+ source.lineNumber !== null &&
5297+ source.columnNumber !== null
5298+ ) {
5299+ // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite.
5300+ const callSite: ReactCallSite = [
5301+ source.functionName,
5302+ source.fileName,
5303+ source.lineNumber,
5304+ source.columnNumber,
5305+ 0,
5306+ 0,
5307+ false,
5308+ ];
5309+ // As we return we'll add any custom hooks parent stacks to the array.
5310+ return [callSite];
5311+ } else {
5312+ return [];
5313+ }
5314+ }
5315+ // Otherwise, search the sub hooks of any custom hook.
5316+ const matchedStack = getAwaitStackFromHooks(node.subHooks, asyncInfo);
5317+ if (matchedStack !== null) {
5318+ // Append this custom hook to the stack trace since it must have been called inside of it.
5319+ const source = node.hookSource;
5320+ if (
5321+ source != null &&
5322+ source.functionName !== null &&
5323+ source.fileName !== null &&
5324+ source.lineNumber !== null &&
5325+ source.columnNumber !== null
5326+ ) {
5327+ // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite.
5328+ const callSite: ReactCallSite = [
5329+ source.functionName,
5330+ source.fileName,
5331+ source.lineNumber,
5332+ source.columnNumber,
5333+ 0,
5334+ 0,
5335+ false,
5336+ ];
5337+ matchedStack.push(callSite);
5338+ }
5339+ return matchedStack;
5340+ }
5341+ }
5342+ return null;
5343+ }
5344+
52285345 function serializeAsyncInfo(
52295346 asyncInfo: ReactAsyncInfo,
5230- index: number,
52315347 parentInstance: DevToolsInstance,
5348+ hooks: null | HooksTree,
52325349 ): SerializedAsyncInfo {
52335350 const ioInfo = asyncInfo.awaited;
52345351 const ioOwnerInstance = findNearestOwnerInstance(
@@ -5268,6 +5385,11 @@ export function attach(
52685385 // If we awaited in the child position of a component, then the best stack would be the
52695386 // return callsite but we don't have that available so instead we skip. The callsite of
52705387 // the JSX would be misleading in this case. The same thing happens with throw-a-Promise.
5388+ if (hooks !== null) {
5389+ // If this component used Hooks we might be able to instead infer the stack from the
5390+ // use() callsite if this async info came from a hook. Let's search the tree to find it.
5391+ awaitStack = getAwaitStackFromHooks(hooks, asyncInfo);
5392+ }
52715393 break;
52725394 default:
52735395 // If we awaited by passing a Promise to a built-in element, then the JSX callsite is a
@@ -5538,31 +5660,9 @@ export function attach(
55385660 const owners: null | Array<SerializedElement> =
55395661 getOwnersListFromInstance(fiberInstance);
55405662
5541- let hooks = null;
5663+ let hooks: null | HooksTree = null;
55425664 if (usesHooks) {
5543- const originalConsoleMethods: {[string]: $FlowFixMe} = {};
5544-
5545- // Temporarily disable all console logging before re-running the hook.
5546- for (const method in console) {
5547- try {
5548- // $FlowFixMe[invalid-computed-prop]
5549- originalConsoleMethods[method] = console[method];
5550- // $FlowFixMe[prop-missing]
5551- console[method] = () => {};
5552- } catch (error) {}
5553- }
5554-
5555- try {
5556- hooks = inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
5557- } finally {
5558- // Restore original console functionality.
5559- for (const method in originalConsoleMethods) {
5560- try {
5561- // $FlowFixMe[prop-missing]
5562- console[method] = originalConsoleMethods[method];
5563- } catch (error) {}
5564- }
5565- }
5665+ hooks = inspectHooks(fiber);
55665666 }
55675667
55685668 let rootType = null;
@@ -5641,8 +5741,8 @@ export function attach(
56415741 // TODO: Prepend other suspense sources like css, images and use().
56425742 fiberInstance.suspendedBy === null
56435743 ? []
5644- : fiberInstance.suspendedBy.map(( info, index) =>
5645- serializeAsyncInfo(info, index, fiberInstance ),
5744+ : fiberInstance.suspendedBy.map(info =>
5745+ serializeAsyncInfo(info, fiberInstance, hooks ),
56465746 );
56475747 return {
56485748 id: fiberInstance.id,
@@ -5813,8 +5913,8 @@ export function attach(
58135913 suspendedBy:
58145914 suspendedBy === null
58155915 ? []
5816- : suspendedBy.map(( info, index) =>
5817- serializeAsyncInfo(info, index, virtualInstance ),
5916+ : suspendedBy.map(info =>
5917+ serializeAsyncInfo(info, virtualInstance, null ),
58185918 ),
58195919
58205920 // List of owners
0 commit comments