Skip to content

Commit

Permalink
Show the current component's pause location in the component stack
Browse files Browse the repository at this point in the history
  • Loading branch information
markerikson committed Jul 8, 2024
1 parent cf3f739 commit 06f9d7a
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,38 @@ function ReactComponentStack({ point }: { point?: TimeStampedPoint }) {
point ?? null
);

if (!point) {
return null;
}

let reactStackContent: React.ReactNode = undefined;

if (reactStackStatus === "rejected") {
reactStackContent = <div>Error loading dependencies</div>;
} else if (reactStackStatus === "pending") {
reactStackContent = <Loader />;
} else {
const firstItem = reactStackValue?.[0];
reactStackContent = (
<div className="m-1 flex grow flex-col border">
<h3 className="text-base font-bold">React Component Stack</h3>
{/* Show the _current_ location, by itself */}
{firstItem && (
<div className="m-1 flex flex-col">
<div title={firstItem.componentLocation?.url}>
{firstItem.componentName} ({point.time.toFixed(2)}){" "}
<JumpToDefinitionButton point={point} />
</div>
</div>
)}
{reactStackValue?.map((entry, index) => {
const jumpButton = entry.point ? <JumpToDefinitionButton point={entry} /> : null;
return (
<div key={index} className="m-1 flex flex-col">
<div title={entry.parentLocation.url}>
&lt;{entry.componentName}&gt; {jumpButton}
<div title={entry.parentLocation?.url}>
{/* Show the parent->child rendering relationship */}
{entry.parentComponentName} &#8594; &lt;{entry.componentName}&gt; (
{entry.time?.toFixed(2)}) {jumpButton}
</div>
</div>
);
Expand All @@ -75,7 +91,7 @@ function DepGraphDisplay({
mode,
title,
}: {
point: ExecutionPoint;
point?: ExecutionPoint;
mode?: DependencyGraphMode;
title: string;
}) {
Expand All @@ -84,7 +100,7 @@ function DepGraphDisplay({
const { status: depGraphStatus, value: depGraphValue } = useImperativeCacheValue(
depGraphCache,
replayClient,
point,
point ?? null,
mode ?? null
);

Expand Down Expand Up @@ -142,21 +158,19 @@ function DepGraphDisplay({
export function DepGraphPrototypePanel() {
const { point, time, pauseId } = useMostRecentLoadedPause() ?? {};
const replayClient = useContext(ReplayClientContext);
const [currentPoint, setCurrentPoint] = useState<ExecutionPoint | null>(null);
const [currentPoint, setCurrentPoint] = useState<TimeStampedPoint | null>(null);

if (!pauseId || !point || !time) {
return <div>Not paused at a point</div>;
}

let timeStampedPoint: TimeStampedPoint = { point, time };

return (
<div className="react-component-stack flex flex-col">
<Button className="self-start" onClick={() => setCurrentPoint(point)}>
<Button className="self-start" onClick={() => setCurrentPoint({ point, time })}>
Load dependencies
</Button>
<ReactComponentStack point={timeStampedPoint} />
<DepGraphDisplay point={point} title="Dep Graph (Regular)" />
<ReactComponentStack point={currentPoint ?? undefined} />
<DepGraphDisplay point={currentPoint?.point} title="Dep Graph (Regular)" />
<DepGraphDisplay
point={point}
mode={DependencyGraphMode.ReactParentRenders}
Expand Down
236 changes: 148 additions & 88 deletions src/ui/suspense/depGraphCache.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import {
ExecutionPoint,
Frame,
FunctionMatch,
FunctionOutline,
Location,
PauseDescription,
PauseId,
PointDescription,
PointStackFrame,
RunEvaluationResult,
TimeStampedPoint,
TimeStampedPointRange,
} from "@replayio/protocol";
import { ExecutionPoint, Location, TimeStampedPoint } from "@replayio/protocol";
import { Cache, createCache } from "suspense";

import { sourceOutlineCache } from "replay-next/src/suspense/SourceOutlineCache";
import { sourcesByIdCache, sourcesByUrlCache } from "replay-next/src/suspense/SourcesCache";
import {
getSourceIdToDisplayForUrl,
getSourceToDisplayForUrl,
} from "replay-next/src/utils/sources";
import { Source, sourcesByIdCache, sourcesByUrlCache } from "replay-next/src/suspense/SourcesCache";
import { getSourceToDisplayForUrl } from "replay-next/src/utils/sources";
import {
DependencyChainStep,
DependencyGraphMode,
ReplayClientInterface,
URLLocation,
} from "shared/client/types";
import { formatFunctionDetailsFromLocation } from "ui/actions/eventListeners/eventListenerUtils";
import { findFunctionOutlineForLocation } from "ui/actions/eventListeners/jumpToCode";

import { formattedPointStackCache } from "./frameCache";

Expand All @@ -47,14 +30,19 @@ export const depGraphCache: Cache<
}
const dependencies = await replayClient.getDependencies(point, mode ?? undefined);

console.log("Deps for point: ", point, dependencies);
return dependencies;
},
});

interface LocationWithUrl extends Location {
url: string;
}

interface ReactComponentStackEntry extends TimeStampedPoint {
parentLocation: Location & { url: string };
parentLocation: LocationWithUrl | null;
componentLocation: LocationWithUrl | null;
componentName: string;
parentComponentName: string;
}

export const REACT_DOM_SOURCE_URLS = [
Expand All @@ -67,6 +55,115 @@ export const REACT_DOM_SOURCE_URLS = [
export const isReactUrl = (url?: string) =>
REACT_DOM_SOURCE_URLS.some(partial => url?.includes(partial));

const pairwise = <T>(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<string, Source>
): 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<string, Source>,
currentComponent: RenderCreateElementPair,
parentComponent: RenderCreateElementPair
): Promise<ReactComponentStackEntry | null> {
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
Expand All @@ -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;
}
Expand All @@ -103,8 +204,6 @@ export const reactComponentStackCache: Cache<
DependencyGraphMode.ReactParentRenders
);

console.log("Deps: ", { originalDependencies, reactDependencies });

if (!originalDependencies || !reactDependencies) {
return null;
}
Expand All @@ -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()!;

Expand All @@ -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);
}
}

Expand Down

0 comments on commit 06f9d7a

Please sign in to comment.