Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix detection of async parent pauses outside of the focus window #10573

Merged
merged 2 commits into from
Jun 19, 2024
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
4 changes: 4 additions & 0 deletions packages/e2e-tests/examples.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions packages/e2e-tests/helpers/pause-information-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,21 @@ export async function stepOver(page: Page): Promise<void> {
await clickCommandBarButton(page, "Step Over");
}

export function waitForAllFramesToLoad(page: Page) {
return waitFor(async () => {
expect(await page.locator('[data-test-name="FramesLoading"]').count()).toBe(0);
});
}

export function getAsyncParentCount(page: Page) {
return page.locator('[data-test-name="AsyncParentLabel"]').count();
}

export async function isAsyncParentUnavailable(page: Page) {
const asyncParentUnavailable = page.locator('[data-test-name="AsyncParentUnavailable"]');
return (await asyncParentUnavailable.count()) > 0;
}

export async function verifyFramesCount(page: Page, expectedCount: number) {
const framesPanel = getFramesPanel(page);
return waitFor(async () => {
Expand Down
34 changes: 34 additions & 0 deletions packages/e2e-tests/tests/async-stack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { openDevToolsTab, startTest } from "../helpers";
import { warpToMessage } from "../helpers/console-panel";
import {
getAsyncParentCount,
isAsyncParentUnavailable,
waitForAllFramesToLoad,
} from "../helpers/pause-information-panel";
import { setFocusRange } from "../helpers/timeline";
import test, { expect } from "../testFixture";

test.use({ exampleKey: "doc_async_stack.html" });

test(`async-stack: should detect async stacks outside the focus window`, async ({
pageWithMeta: { page, recordingId, testScope },
exampleKey,
}) => {
await startTest(page, recordingId, testScope);
await openDevToolsTab(page);

await warpToMessage(page, "Starting", 7);
await waitForAllFramesToLoad(page);
expect(await getAsyncParentCount(page)).toBe(0);
expect(await isAsyncParentUnavailable(page)).toBe(false);

await warpToMessage(page, "ExampleFinished", 9);
await waitForAllFramesToLoad(page);
expect(await getAsyncParentCount(page)).toBe(1);
expect(await isAsyncParentUnavailable(page)).toBe(false);

await setFocusRange(page, { startTimeString: "00:01" });
await waitForAllFramesToLoad(page);
expect(await getAsyncParentCount(page)).toBe(1);
expect(await isAsyncParentUnavailable(page)).toBe(true);
});
13 changes: 13 additions & 0 deletions public/test/examples/doc_async_stack.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html lang="en" dir="ltr">
<body>
<div id="maindiv" style="padding-top:50px">Hello World!</div>
</body>
<script>
async function foo() {
console.log("Starting");
await new Promise(resolve => setTimeout(resolve, 2000));
console.log("ExampleFinished");
}
foo();
</script>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ import {
getSelectedFrameId,
getThreadContext,
} from "devtools/client/debugger/src/selectors";
import { isFocusWindowApplied } from "devtools/client/debugger/src/utils/focus";
import { InlineErrorBoundary } from "replay-next/components/errors/InlineErrorBoundary";
import { copyToClipboard } from "replay-next/components/sources/utils/clipboard";
import { FocusContext } from "replay-next/src/contexts/FocusContext";
import { SessionContext } from "replay-next/src/contexts/SessionContext";
import { useCurrentFocusWindow } from "replay-next/src/hooks/useCurrentFocusWindow";
import { useIsPointWithinFocusWindow } from "replay-next/src/hooks/useIsPointWithinFocusWindow";
import { getPointAndTimeForPauseId, pauseIdCache } from "replay-next/src/suspense/PauseCache";
Expand Down Expand Up @@ -46,13 +44,16 @@ function FramesRenderer({
const replayClient = useContext(ReplayClientContext);
const sourcesState = useAppSelector(state => state.sources);
const { rangeForSuspense: focusWindow } = useContext(FocusContext);
const { endpoint } = useContext(SessionContext);
const dispatch = useAppDispatch();

if (focusWindow === null) {
return null;
}
Comment on lines +49 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

Returning null here still feels odd. I guess this will only ever happy briefly during initialization so maybe it doesn't matter.


const asyncSeparator =
asyncIndex > 0 ? (
<div role="listitem">
<span className="location-async-cause">
<span className="location-async-cause" data-test-name="AsyncParentLabel">
<span className="async-label">async</span>
</span>
</div>
Expand All @@ -64,30 +65,25 @@ function FramesRenderer({
asyncIndex,
focusWindow
);
if (asyncParentPauseId === null) {
if (asyncParentPauseId === true) {
return (
<>
{asyncSeparator}
<div className="pane-info empty">
This part of the call stack is unavailable.
{isFocusWindowApplied(focusWindow, endpoint) && (
<>
{" "}
Perhaps it is outside of{" "}
<span className="cursor-pointer underline" onClick={() => dispatch(enterFocusMode())}>
your debugging window
</span>
.
</>
)}
<div className="pane-info empty" data-test-name="AsyncParentUnavailable">
This part of the call stack is unavailable because it is outside{" "}
<span className="cursor-pointer underline" onClick={() => dispatch(enterFocusMode())}>
your debugging window
</span>
.
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
</div>
</>
);
}

let frames = asyncParentPauseId
? getPauseFramesSuspense(replayClient, asyncParentPauseId, sourcesState)
: undefined;
let frames =
typeof asyncParentPauseId === "string"
? getPauseFramesSuspense(replayClient, asyncParentPauseId, sourcesState)
: undefined;
if (asyncIndex > 0) {
frames = frames?.slice(1);
}
Expand All @@ -108,7 +104,13 @@ function FramesRenderer({
name="NewFrames"
fallback={<div className="pane-info empty">Error loading frames :(</div>}
>
<Suspense fallback={<div className="pane-info empty">Loading async frames…</div>}>
<Suspense
fallback={
<div className="pane-info empty" data-test-name="FramesLoading">
Loading async frames…
</div>
}
>
<FramesRenderer panel={panel} pauseId={pauseId} asyncIndex={asyncIndex + 1} />
</Suspense>
</InlineErrorBoundary>
Expand Down Expand Up @@ -228,7 +230,13 @@ function Frames({ panel, point, time }: FramesProps) {
name="Frames"
fallback={<div className="pane-info empty">Error loading frames :((</div>}
>
<Suspense fallback={<div className="pane-info empty">Loading...</div>}>
<Suspense
fallback={
<div className="pane-info empty" data-test-name="FramesLoading">
Loading...
</div>
}
>
<div role="list">
<FramesRenderer pauseId={pauseId} panel={panel} />
</div>
Expand All @@ -243,7 +251,9 @@ export default function NewFrames(props: FramesProps) {
<Suspense
fallback={
<div className="pane">
<div className="pane-info empty">Loading...</div>
<div className="pane-info empty" data-test-name="FramesLoading">
Loading...
</div>
</div>
}
>
Expand Down
10 changes: 0 additions & 10 deletions src/devtools/client/debugger/src/utils/focus.ts

This file was deleted.

16 changes: 8 additions & 8 deletions src/ui/suspense/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,36 @@ import { pauseIdCache } from "replay-next/src/suspense/PauseCache";
import { isExecutionPointsWithinRange } from "replay-next/src/utils/time";
import { ReplayClientInterface } from "shared/client/types";

// returns undefined if the async parent pause doesn't exist
// or null if it is not in a loaded region
// returns false if the async parent pause doesn't exist
// or true if it is not in a loaded region
export function getAsyncParentPauseIdSuspense(
replayClient: ReplayClientInterface,
pauseId: PauseId,
asyncIndex: number,
focusWindow: TimeStampedPointRange | null
): PauseId | null {
): PauseId | boolean {
while (asyncIndex > 0) {
const frames = framesCache.read(replayClient, pauseId)!;
if (!frames?.length) {
return null;
return false;
}

const steps = frameStepsCache.read(replayClient, pauseId, frames[frames.length - 1].frameId);
if (!steps || steps.length === 0) {
return null;
return false;
}

const executionPoint = steps[0].point;
if (
focusWindow === null ||
focusWindow &&
!isExecutionPointsWithinRange(executionPoint, focusWindow.begin.point, focusWindow.end.point)
) {
return null;
return true;
}

const parentPauseId = pauseIdCache.read(replayClient, steps[0].point, steps[0].time);
if (parentPauseId === pauseId) {
return null;
return false;
}

pauseId = parentPauseId;
Expand Down
Loading