From 06f9d7a1dcb89df759df8c3bc01e288fa75c9969 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 8 Jul 2024 16:12:36 -0400 Subject: [PATCH] Show the current component's pause location in the component stack --- .../SecondaryPanes/ReactComponentStack.tsx | 34 ++- src/ui/suspense/depGraphCache.ts | 236 +++++++++++------- 2 files changed, 172 insertions(+), 98 deletions(-) diff --git a/src/devtools/client/debugger/src/components/SecondaryPanes/ReactComponentStack.tsx b/src/devtools/client/debugger/src/components/SecondaryPanes/ReactComponentStack.tsx index c505da17b2e..d46b41526c8 100644 --- a/src/devtools/client/debugger/src/components/SecondaryPanes/ReactComponentStack.tsx +++ b/src/devtools/client/debugger/src/components/SecondaryPanes/ReactComponentStack.tsx @@ -43,6 +43,10 @@ function ReactComponentStack({ point }: { point?: TimeStampedPoint }) { point ?? null ); + if (!point) { + return null; + } + let reactStackContent: React.ReactNode = undefined; if (reactStackStatus === "rejected") { @@ -50,15 +54,27 @@ function ReactComponentStack({ point }: { point?: TimeStampedPoint }) { } else if (reactStackStatus === "pending") { reactStackContent = ; } else { + const firstItem = reactStackValue?.[0]; reactStackContent = (

React Component Stack

+ {/* Show the _current_ location, by itself */} + {firstItem && ( +
+
+ {firstItem.componentName} ({point.time.toFixed(2)}){" "} + +
+
+ )} {reactStackValue?.map((entry, index) => { const jumpButton = entry.point ? : null; return (
-
- <{entry.componentName}> {jumpButton} +
+ {/* Show the parent->child rendering relationship */} + {entry.parentComponentName} → <{entry.componentName}> ( + {entry.time?.toFixed(2)}) {jumpButton}
); @@ -75,7 +91,7 @@ function DepGraphDisplay({ mode, title, }: { - point: ExecutionPoint; + point?: ExecutionPoint; mode?: DependencyGraphMode; title: string; }) { @@ -84,7 +100,7 @@ function DepGraphDisplay({ const { status: depGraphStatus, value: depGraphValue } = useImperativeCacheValue( depGraphCache, replayClient, - point, + point ?? null, mode ?? null ); @@ -142,21 +158,19 @@ function DepGraphDisplay({ export function DepGraphPrototypePanel() { const { point, time, pauseId } = useMostRecentLoadedPause() ?? {}; const replayClient = useContext(ReplayClientContext); - const [currentPoint, setCurrentPoint] = useState(null); + const [currentPoint, setCurrentPoint] = useState(null); if (!pauseId || !point || !time) { return
Not paused at a point
; } - let timeStampedPoint: TimeStampedPoint = { point, time }; - return (
- - - + + REACT_DOM_SOURCE_URLS.some(partial => url?.includes(partial)); +const pairwise = (arr: T[]): [T, T][] => { + const pairs: [T, T][] = []; + for (let i = 0; i < arr.length - 1; i++) { + pairs.push([arr[i], arr[i + 1]]); + } + return pairs; +}; + +interface RenderCreateElementPair { + render: { + // React has rendered a component. + code: "ReactRender"; + calleeLocation?: URLLocation; + } & TimeStampedPoint; + createElement: { + // An application render function created an element object for converting + // into a component. + code: "ReactCreateElement"; + } & TimeStampedPoint; +} + +async function getComponentDetails( + replayClient: ReplayClientInterface, + location: URLLocation | undefined, + sourcesById: Map +): Promise<{ componentName: string; location: LocationWithUrl | null }> { + let componentName = "Unknown"; + let finalLocation: LocationWithUrl | null = null; + + if (location) { + const sourcesByUrl = await sourcesByUrlCache.readAsync(replayClient); + + const bestSource = getSourceToDisplayForUrl(sourcesById, sourcesByUrl, location.url); + + if (bestSource) { + const locationInFunction: Location = { + sourceId: bestSource.sourceId, + line: location.line, + column: location.column, + }; + finalLocation = { + ...locationInFunction, + url: location.url, + }; + + const formattedFunctionDescription = await formatFunctionDetailsFromLocation( + replayClient, + "component", + locationInFunction, + undefined, + true + ); + + componentName = + formattedFunctionDescription?.classComponentName ?? + formattedFunctionDescription?.functionName ?? + "Unknown"; + } + } + + return { + componentName, + location: finalLocation, + }; +} + +async function formatComponentStackEntry( + replayClient: ReplayClientInterface, + sourcesById: Map, + currentComponent: RenderCreateElementPair, + parentComponent: RenderCreateElementPair +): Promise { + const elementCreationPoint = currentComponent.createElement; + const componentLocation = currentComponent.render.calleeLocation; + const parentLocation = parentComponent.render.calleeLocation; + + if (!componentLocation || !parentLocation) { + return null; + } + + const [componentDetails, parentComponentDetails] = await Promise.all([ + getComponentDetails(replayClient, componentLocation, sourcesById), + getComponentDetails(replayClient, parentLocation, sourcesById), + ]); + + const pointStack = await formattedPointStackCache.readAsync(replayClient, { + ...elementCreationPoint, + frameDepth: 2, + }); + + let finalJumpPoint: TimeStampedPoint = elementCreationPoint; + + if (pointStack.allFrames.length > 1) { + // Element creation happens up one frame + const elementCreationFrame = pointStack.allFrames[1]; + finalJumpPoint = elementCreationFrame.point!; + } + + const stackEntry: ReactComponentStackEntry = { + ...finalJumpPoint, + parentLocation: parentComponentDetails.location, + componentLocation: componentDetails.location, + componentName: componentDetails.componentName, + parentComponentName: parentComponentDetails.componentName, + }; + + return stackEntry; +} + export const reactComponentStackCache: Cache< [replayClient: ReplayClientInterface, point: TimeStampedPoint | null], ReactComponentStackEntry[] | null @@ -85,13 +182,17 @@ export const reactComponentStackCache: Cache< ...point, frameDepth: 2, }, + // don't ignore any files, we _want_ `node_modules` here [] ); - console.log("Point stack for point: ", point, currentPointStack); - const precedingFrame = currentPointStack.allFrames[1]; + // We expect that if we're currently rendering a React component, + // the parent frame is either `renderWithHooks()` or + // `finishClassComponent()`. For now we'll just check if the + // preceding frame is at least in a React build artifact. + // If not, there's no point in trying to build a component stack. if (!isReactUrl(precedingFrame?.url)) { return null; } @@ -103,8 +204,6 @@ export const reactComponentStackCache: Cache< DependencyGraphMode.ReactParentRenders ); - console.log("Deps: ", { originalDependencies, reactDependencies }); - if (!originalDependencies || !reactDependencies) { return null; } @@ -114,6 +213,9 @@ export const reactComponentStackCache: Cache< const sourcesById = await sourcesByIdCache.readAsync(replayClient); const remainingDepEntries = reactDependencies.slice().reverse(); + + const renderPairs: RenderCreateElementPair[] = []; + while (remainingDepEntries.length) { const depEntry = remainingDepEntries.shift()!; @@ -134,71 +236,29 @@ export const reactComponentStackCache: Cache< console.error("Expected point in previous entry: ", previousEntry); } - const elementCreationPoint: TimeStampedPoint = { - point: previousEntry.point!, - time: previousEntry.time!, - }; - const parentLocation = depEntry.calleeLocation; - - let componentName = "Unknown"; + const renderPair = { + render: depEntry, + createElement: previousEntry as { + code: "ReactCreateElement"; + }, + } as RenderCreateElementPair; - if (parentLocation) { - const sourcesByUrl = await sourcesByUrlCache.readAsync(replayClient); - const sourcesForUrl = sourcesByUrl.get(parentLocation.url); + renderPairs.push(renderPair); + } + } - const bestSource = getSourceToDisplayForUrl( - sourcesById, - sourcesByUrl, - parentLocation.url - ); + const renderPairsWithParents = pairwise(renderPairs); - if (!bestSource) { - continue; - } - - const locationInFunction: Location = { - sourceId: bestSource.sourceId, - line: parentLocation.line, - column: parentLocation.column, - }; - - const formattedFunctionDescription = await formatFunctionDetailsFromLocation( - replayClient, - "component", - locationInFunction, - undefined, - true - ); + for (const [current, parent] of renderPairsWithParents) { + const stackEntry = await formatComponentStackEntry( + replayClient, + sourcesById, + current, + parent + ); - componentName = - formattedFunctionDescription?.classComponentName ?? - formattedFunctionDescription?.functionName ?? - "Unknown"; - - const pointStack = await formattedPointStackCache.readAsync(replayClient, { - ...elementCreationPoint, - frameDepth: 2, - }); - - let finalJumpPoint = elementCreationPoint; - - if (pointStack.allFrames.length > 1) { - // Element creation happens up one frame - const elementCreationFrame = pointStack.allFrames[1]; - finalJumpPoint = elementCreationFrame.point!; - } - - const stackEntry: ReactComponentStackEntry = { - ...finalJumpPoint, - parentLocation: { - ...locationInFunction, - url: parentLocation.url, - }, - componentName, - }; - - componentStack.push(stackEntry); - } + if (stackEntry) { + componentStack.push(stackEntry); } }