Skip to content

Commit

Permalink
Improved paint/repaint behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Mar 18, 2024
1 parent dd60e55 commit 70fd3d6
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 43 deletions.
13 changes: 13 additions & 0 deletions packages/e2e-tests/helpers/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export async function getGraphicsExecutionPoint(page: Page): Promise<string | nu
return await element.getAttribute("data-execution-point");
}

export async function getGraphicsScreenshotType(page: Page): Promise<string | null> {
const element = getVideoElement(page);
return await element.getAttribute("data-screenshot-type");
}

export async function getGraphicsStatus(page: Page): Promise<string | null> {
const element = getVideoElement(page);
return await element.getAttribute("data-status");
Expand Down Expand Up @@ -135,3 +140,11 @@ export async function waitForGraphicsToLoad(page: Page) {
await expect(await getGraphicsStatus(page)).toBe("loaded");
});
}

export async function waitForRepaintGraphicsToLoad(page: Page) {
await debugPrint(page, `Waiting for repaint graphics to load...`, "waitForRepaintGraphicsToLoad");

await waitFor(async () => {
await expect(await getGraphicsScreenshotType(page)).toBe("repaint");
});
}
132 changes: 114 additions & 18 deletions packages/e2e-tests/tests/repaint-01.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { openDevToolsTab, startTest } from "../helpers";
import { rewindToLine, stepOver, waitForPaused } from "../helpers/pause-information-panel";
import { getGraphicsDataUrl } from "../helpers/screenshot";
import { addBreakpoint } from "../helpers/source-panel";
import { waitFor } from "../helpers/utils";
import { test } from "../testFixture";
import { findConsoleMessage, seekToConsoleMessage } from "../helpers/console-panel";
import { stepOver } from "../helpers/pause-information-panel";
import { getGraphicsDataUrl, waitForRepaintGraphicsToLoad } from "../helpers/screenshot";
import { debugPrint } from "../helpers/utils";
import { expect, test } from "../testFixture";

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

Expand All @@ -14,21 +14,117 @@ test("repaint-01: repaints the screen screen when stepping over code that modifi
await startTest(page, recordingId);
await openDevToolsTab(page);

await addBreakpoint(page, { lineNumber: 50, url: exampleKey });
await rewindToLine(page, 50);
const printedStrings = [
"catch",
"afterCatch",
"finally",
"yield 1",
"generated 1",
"yield 2",
"generated 2",
"after timer 1",
"after timer 2",
"after throw timer 1",
"after throw timer 2",
"within iterator",
];

const prevSource = await getGraphicsDataUrl(page);
const paints = {};

// this steps over a (synchronous) DOM update. The recorded screenshot for the point after the step
// will not have changed (because the browser doesn't repaint in the middle of javascript execution),
// but the repainted screenshot must have changed.
await stepOver(page);
await waitForPaused(page);
{
await debugPrint(page, "Testing repaint graphics");

await waitFor(async () => {
const nextSource = await getGraphicsDataUrl(page);
if (prevSource === nextSource) {
throw `The screenshot did not change`;
let graphicsDataUrl = "";
let previousGraphicsDataUrl = "";

for (let index = 0; index < printedStrings.length; index++) {
const string = printedStrings[index];
const text = `updateText("${string}")`;

const locator = await findConsoleMessage(page, text, "console-log");
await seekToConsoleMessage(page, locator);

// Step to "Element text before..."
await stepOver(page);
await stepOver(page);

await waitForRepaintGraphicsToLoad(page);

graphicsDataUrl = await getGraphicsDataUrl(page);
expect(
graphicsDataUrl,
`Expected graphics before "${string}" not to match previous after graphics`
).not.toEqual(previousGraphicsDataUrl);
previousGraphicsDataUrl = graphicsDataUrl;

const cachedPaints = {
before: graphicsDataUrl,
after: "",
};

// Step to "Element text after..."
await stepOver(page);
await stepOver(page);

await waitForRepaintGraphicsToLoad(page);

graphicsDataUrl = await getGraphicsDataUrl(page);
expect(
graphicsDataUrl,
`Expected graphics after "${string}" not to match the graphics before`
).not.toEqual(previousGraphicsDataUrl);
previousGraphicsDataUrl = graphicsDataUrl;

cachedPaints.after = graphicsDataUrl;

paints[string] = cachedPaints;
}
}

{
await debugPrint(page, "Testing screenshot caching");

let graphicsDataUrl = "";

for (let index = 0; index < printedStrings.length; index++) {
const string = printedStrings[index];
const text = `updateText("${string}")`;

const locator = await findConsoleMessage(page, text, "console-log");
await seekToConsoleMessage(page, locator);

// Step to "Element text before..."
await stepOver(page);
await stepOver(page);

await waitForRepaintGraphicsToLoad(page);

graphicsDataUrl = await getGraphicsDataUrl(page);
expect(
graphicsDataUrl,
`Expected graphics before "${string}" to match previous cached graphics`
).toEqual(paints[string]?.before);

const cachedPaints = {
before: graphicsDataUrl,
after: "",
};

// Step to "Element text after..."
await stepOver(page);
await stepOver(page);

await waitForRepaintGraphicsToLoad(page);

graphicsDataUrl = await getGraphicsDataUrl(page);
expect(
graphicsDataUrl,
`Expected graphics after "${string}" to match previous cached graphics`
).toEqual(paints[string]?.after);

cachedPaints.after = graphicsDataUrl;

paints[string] = cachedPaints;
}
});
}
});
17 changes: 16 additions & 1 deletion packages/protocol/RepaintGraphicsCache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PauseId, repaintGraphicsResult } from "@replayio/protocol";
import { Cache, createCache } from "suspense";

import { paintHashCache } from "replay-next/src/suspense/ScreenshotCache";
import { ReplayClientInterface } from "shared/client/types";

export const RepaintGraphicsCache: Cache<
Expand All @@ -11,6 +12,20 @@ export const RepaintGraphicsCache: Cache<
debugLabel: "RepaintGraphicsCache",
getKey: ([replayClient, pauseId]) => pauseId,
load: async ([replayClient, pauseId]) => {
return replayClient.repaintGraphics(pauseId);
const result = await replayClient.repaintGraphics(pauseId);

let { description, screenShot } = result;

// The backend won't return a screenshot for a given hash more than once; see FE-2357
if (screenShot) {
paintHashCache.cacheValue(screenShot, description.hash);
} else {
screenShot = paintHashCache.getValueIfCached(description.hash);
}

return {
description,
screenShot,
};
},
});
17 changes: 15 additions & 2 deletions packages/replay-next/src/suspense/ScreenshotCache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ExecutionPoint, ScreenShot } from "@replayio/protocol";
import { createCache } from "suspense";
import { createCache, createExternallyManagedCache } from "suspense";

import { ReplayClientInterface } from "shared/client/types";

Expand All @@ -10,5 +10,18 @@ export const screenshotCache = createCache<
config: { immutable: true },
debugLabel: "ScreenshotCache",
getKey: ([client, point, paintHash]) => paintHash,
load: ([client, point]) => client.getScreenshot(point),
load: async ([client, point]) => {
const screenShot = await client.getScreenshot(point);

paintHashCache.cacheValue(screenShot, screenShot.hash);

return screenShot;
},
});

// The backend won't return a screenshot for a given hash more than once; see FE-2357
export const paintHashCache = createExternallyManagedCache<[paintHash: string], ScreenShot>({
config: { immutable: true },
debugLabel: "paintHashCache",
getKey: ([paintHash]) => paintHash,
});
1 change: 1 addition & 0 deletions src/ui/components/Video/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default function Video() {

// The data attributes are sed for e2e tests
containerElement.setAttribute("data-execution-point", nextState.currentExecutionPoint ?? "");
containerElement.setAttribute("data-screenshot-type", "" + nextState.screenShotType);
containerElement.setAttribute("data-status", "" + nextState.status);
containerElement.setAttribute("data-time", "" + nextState.currentTime);
graphicsElement.setAttribute("data-scale", nextState.localScale.toString());
Expand Down
19 changes: 19 additions & 0 deletions src/ui/components/Video/imperative/MutableGraphicsState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface Rect {
}

export type Status = "failed" | "loaded" | "loading";
export type ScreenShotType = "cached-paint" | "repaint";

export interface State {
currentExecutionPoint: ExecutionPoint | null;
Expand All @@ -33,6 +34,7 @@ export interface State {
localScale: number;
recordingScale: number;
screenShot: ScreenShot | undefined;
screenShotType: ScreenShotType | undefined;
status: Status;
}

Expand All @@ -48,25 +50,33 @@ export const state = createState<State>({
localScale: 1,
recordingScale: 1,
screenShot: undefined,
screenShotType: undefined,
status: "loading",
});

let lock = new Object();

export async function updateState(
containerElement: HTMLElement,
options: Partial<{
didResize?: boolean;
executionPoint: ExecutionPoint | null;
screenShot: ScreenShot | null;
screenShotType: ScreenShotType | null;
status: Status;
time: number;
}> = {}
) {
const localLock = new Object();
lock = localLock;

const prevState = state.read();

let {
didResize,
executionPoint = prevState.currentExecutionPoint,
screenShot = prevState.screenShot,
screenShotType = prevState.screenShotType,
status = prevState.status,
time = prevState.currentTime,
} = options;
Expand All @@ -76,6 +86,10 @@ export async function updateState(
let recordingScale = prevState.recordingScale;
if (screenShot && (screenShot != prevState.screenShot || didResize)) {
const naturalDimensions = await getDimensions(screenShot.data, screenShot.mimeType);
if (lock !== localLock) {
return;
}

const naturalHeight = naturalDimensions.height;
const naturalWidth = naturalDimensions.width;

Expand All @@ -86,6 +100,10 @@ export async function updateState(
imageHeight: naturalHeight,
imageWidth: naturalWidth,
});
if (lock !== localLock) {
return;
}

const clientHeight = scaledDimensions.height;
const clientWidth = scaledDimensions.width;

Expand All @@ -107,6 +125,7 @@ export async function updateState(
localScale,
recordingScale,
screenShot: screenShot || undefined,
screenShotType: screenShotType || undefined,
status,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,9 @@ export function subscribeToMutableSources({
});
}
} else {
if (didStopPlaying) {
if (abortController != null) {
abortController.abort();
abortController = null;
}
if (abortController != null) {
abortController.abort();
abortController = null;
}

abortController = new AbortController();
Expand Down
Loading

0 comments on commit 70fd3d6

Please sign in to comment.