refactor(app): implement canvas-based diff rendering

This commit is contained in:
Mohamed Boudra
2026-08-17 21:14:37 +02:00
parent 75adf53893
commit d03cf14703
66 changed files with 6147 additions and 3210 deletions
+466 -128
View File
@@ -1,41 +1,38 @@
import { writeFile } from "node:fs/promises";
import path from "node:path";
import type { CDPSession } from "@playwright/test";
import type { CDPSession, Locator, Page, TestInfo } from "@playwright/test";
import { buildHostWorkspaceRoute } from "../../src/utils/host-routes";
import { test, expect } from "../support/fixtures";
import { getServerId } from "../support/helpers/server-id";
import { connectSeedClient } from "../support/helpers/seed-client";
import { createTempGitRepo } from "../support/helpers/workspace";
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
import { openChangesPanel, waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
import { DIFF_POC_BASELINE } from "./diff-performance-baseline";
const RUN_DIFF_PERF = process.env.PASEO_DIFF_PERF_E2E === "1";
const diffPerfDescribe = RUN_DIFF_PERF ? test.describe : test.describe.skip;
const LINE_COUNT = 2_400;
const LINE_COUNT_PER_FILE = 1_200;
const FILE_PATHS = ["src/large-a.ts", "src/large-b.ts"] as const;
const CPU_SLOWDOWN = Number(process.env.PASEO_DIFF_PERF_CPU_SLOWDOWN ?? 6);
const MAX_MOUNTED_CODE_ROWS = 220;
const MAX_EXPANSION_MS = 6_000;
const CHANGES_PREFERENCES_KEY = "@paseo:changes-preferences";
const POC_MEDIAN_FRAME_MS = Number(
process.env.PASEO_DIFF_POC_MEDIAN_FRAME_MS ?? DIFF_POC_BASELINE.medianFrameMs,
);
const POC_P95_FRAME_MS = Number(
process.env.PASEO_DIFF_POC_P95_FRAME_MS ?? DIFF_POC_BASELINE.p95FrameMs,
);
const MAX_MEDIAN_FRAME_MS = POC_MEDIAN_FRAME_MS * 1.1;
const MAX_P95_FRAME_MS = POC_P95_FRAME_MS * 1.1;
diffPerfDescribe("Diff rendering performance", () => {
test("large unwrapped diffs keep mounting bounded under CPU slowdown", async ({
page,
}, testInfo) => {
test.setTimeout(90_000);
const repo = await createTempGitRepo("diff-perf-", {
files: [{ path: "src/large.ts", content: "export const baseline = 0;\n" }],
});
diffPerfDescribe("Diff canvas performance", () => {
test("one canvas stays filled through continuous fast scrolling", async ({ page }, testInfo) => {
test.setTimeout(180_000);
const repo = await createLargeDiffRepository();
const client = await connectSeedClient();
let cdp: CDPSession | null = null;
let traceStarted = false;
try {
const generatedLines = Array.from(
{ length: LINE_COUNT },
(_, index) => `export const generated_${index} = "${index}-${"wide".repeat(16)}";`,
).join("\n");
await writeFile(
path.join(repo.path, "src/large.ts"),
`export const baseline = 1;\n${generatedLines}\n`,
);
const created = await client.createWorkspace({
source: { kind: "directory", path: repo.path },
});
@@ -43,100 +40,89 @@ diffPerfDescribe("Diff rendering performance", () => {
throw new Error(created.error ?? "Failed to create diff performance workspace");
}
await page.addInitScript(
({ preferencesKey }) => {
localStorage.setItem(
preferencesKey,
JSON.stringify({
layout: "unified",
viewMode: "flat",
wrapLines: false,
hideWhitespace: false,
}),
);
},
{ preferencesKey: CHANGES_PREFERENCES_KEY },
);
await configureUnwrappedUnifiedDiff(page);
await page.setViewportSize({ width: 1400, height: 900 });
await page.goto(buildHostWorkspaceRoute(getServerId(), created.workspace.id));
await waitForWorkspaceTabsVisible(page);
await page.getByRole("button", { name: "Open explorer" }).click();
const fileHeader = page.getByTestId("diff-file-0");
await expect(fileHeader).toContainText("large.ts", { timeout: 30_000 });
await expect(page.getByTestId("git-diff-canvas")).toBeVisible({ timeout: 30_000 });
await expectSingleCanvasSurface(page);
cdp = await page.context().newCDPSession(page);
await cdp.send("Emulation.setCPUThrottlingRate", { rate: CPU_SLOWDOWN });
const expansionStart = await page.evaluate(() => performance.now());
await fileHeader.click();
await expect(page.getByTestId("diff-file-0-body")).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId("diff-code-row-0")).toBeVisible({ timeout: 30_000 });
const expansionMs = (await page.evaluate(() => performance.now())) - expansionStart;
await startChromiumTrace(cdp);
traceStarted = true;
await startRendererInstrumentation(page);
const scroll = page.getByTestId("git-diff-scroll");
const initial = await readDiffMountReport(page);
expect(initial.mountedCodeRows).toBeLessThanOrEqual(MAX_MOUNTED_CODE_ROWS);
expect(initial.mountedGutterRows).toBe(initial.mountedCodeRows);
const scrollReports = [];
const heapUsage = [];
for (let cycle = 0; cycle < 3; cycle += 1) {
scrollReports.push(await continuouslyScrollCanvas(page));
heapUsage.push(await collectHeapUsage(cdp));
}
await expectContinuousFramesMatchStableRender(
page,
scrollReports.flatMap((report) => report.frameSignatures),
);
const frameImages = await captureCanvasAtScrollIntervals(page);
await expectFramesMatchStableRender(page, frameImages);
for (const [frameIndex, frame] of frameImages.entries()) {
await writeFile(testInfo.outputPath(`diff-canvas-frame-${frameIndex}.png`), frame.image);
await testInfo.attach(`diff-canvas-frame-${frameIndex}`, {
body: frame.image,
contentType: "image/png",
});
expect(frame.image.byteLength).toBeGreaterThan(4_000);
}
await page.getByTestId("diff-gutter-row-1").hover();
await page.getByTestId("diff-gutter-action-1").click();
await expect(page.getByTestId("inline-review-editor")).toBeVisible();
const withEditor = await readDiffMountReport(page);
expect(withEditor.mountedCodeRows).toBeLessThanOrEqual(MAX_MOUNTED_CODE_ROWS);
expect(withEditor.mountedGutterRows).toBe(withEditor.mountedCodeRows);
expect(await readDiffRowAlignment(page, 2)).toBeCloseTo(0, 0);
await expect(page.getByTestId("inline-review-editor-input")).toBeFocused();
await page.getByTestId("inline-review-editor-cancel").click();
await expect(page.getByTestId("inline-review-editor")).toHaveCount(0);
await expectIndependentHorizontalOffsetsSurviveVerticalScrolling(page);
const renderer = await readRendererInstrumentation(page);
const measuredCycles = scrollReports.slice(1);
expect(Math.max(...measuredCycles.map((cycle) => cycle.medianFrameMs))).toBeLessThanOrEqual(
MAX_MEDIAN_FRAME_MS,
);
expect(Math.max(...measuredCycles.map((cycle) => cycle.p95FrameMs))).toBeLessThanOrEqual(
MAX_P95_FRAME_MS,
);
for (const cycle of scrollReports) {
expect(Math.abs(cycle.maximumScrollTop - cycle.endScrollTop)).toBeLessThanOrEqual(1);
}
expect(heapUsage.at(-1)!.usedSize).toBeLessThanOrEqual(
heapUsage[0]!.usedSize * 1.1 + 1_000_000,
);
expect(renderer.addedElements).toBe(0);
expect(renderer.removedElements).toBe(0);
expect(renderer.reactCommits).toBeLessThanOrEqual(2);
await scroll.evaluate((element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
});
await expect
.poll(async () => (await readDiffMountReport(page)).highestMountedRow)
.toBeGreaterThan(LINE_COUNT - 100);
const afterScroll = await readDiffMountReport(page);
expect(afterScroll.mountedCodeRows).toBeLessThanOrEqual(MAX_MOUNTED_CODE_ROWS);
expect(afterScroll.mountedGutterRows).toBe(afterScroll.mountedCodeRows);
await reloadDiffPresentation(page, { layout: "unified", wrapLines: true });
const wrapped = await measureAdditionalPerformanceScenario(page, cdp, testInfo, "wrapped");
await reloadDiffPresentation(page, { layout: "split", wrapLines: false });
const split = await measureAdditionalPerformanceScenario(page, cdp, testInfo, "split");
const report = {
cpuSlowdown: CPU_SLOWDOWN,
sourceLines: LINE_COUNT,
expansionMs: Math.round(expansionMs),
initial,
withEditor,
afterScroll,
pocBaseline: {
medianFrameMs: POC_MEDIAN_FRAME_MS,
p95FrameMs: POC_P95_FRAME_MS,
},
sourceFiles: FILE_PATHS.length,
sourceLines: FILE_PATHS.length * LINE_COUNT_PER_FILE,
scrollReports,
heapUsage,
renderer,
wrapped,
split,
canvasPngBytes: frameImages.map((frame) => frame.image.byteLength),
};
await testInfo.attach("diff-performance", {
body: JSON.stringify(report, null, 2),
contentType: "application/json",
});
console.log(`[perf] Diff rendering: ${JSON.stringify(report)}`);
expect(expansionMs).toBeLessThan(MAX_EXPANSION_MS);
await attachPerformanceReport(testInfo, report);
await stopChromiumTrace(cdp, testInfo);
traceStarted = false;
await cdp.send("Emulation.setCPUThrottlingRate", { rate: 1 });
await scroll.evaluate((element) => {
element.scrollTop = 0;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
});
await page.getByTestId("changes-toggle-layout").click();
const oldLine = page.getByText("export const baseline = 0;", { exact: true });
const newLine = page.getByText("export const baseline = 1;", { exact: true });
await expect(oldLine).toBeVisible();
await expect(newLine).toBeVisible();
const [bodyBox, oldBox, newBox] = await Promise.all([
page.getByTestId("diff-file-0-body").boundingBox(),
oldLine.boundingBox(),
newLine.boundingBox(),
]);
expect(bodyBox).not.toBeNull();
expect(oldBox).not.toBeNull();
expect(newBox).not.toBeNull();
expect(oldBox!.x).toBeLessThan(newBox!.x);
expect(oldBox!.x).toBeGreaterThanOrEqual(bodyBox!.x);
expect(newBox!.x).toBeGreaterThan(bodyBox!.x + bodyBox!.width / 2);
expect(newBox!.x).toBeLessThan(bodyBox!.x + bodyBox!.width);
} finally {
if (cdp && traceStarted) {
await stopChromiumTrace(cdp, testInfo).catch(() => undefined);
}
await cdp?.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(() => undefined);
await cdp?.detach().catch(() => undefined);
await client.close().catch(() => undefined);
@@ -145,40 +131,392 @@ diffPerfDescribe("Diff rendering performance", () => {
});
});
async function readDiffRowAlignment(
page: import("@playwright/test").Page,
rowIndex: number,
): Promise<number> {
const gutterTop = await page
.getByTestId(`diff-gutter-row-${rowIndex}`)
.evaluate((row) => row.getBoundingClientRect().top);
const codeTop = await page
.getByTestId(`diff-code-row-${rowIndex}`)
.evaluate((row) => row.getBoundingClientRect().top);
return gutterTop - codeTop;
async function measureAdditionalPerformanceScenario(
page: Page,
cdp: CDPSession,
testInfo: TestInfo,
name: "wrapped" | "split",
) {
await expectSingleCanvasSurface(page);
await startRendererInstrumentation(page);
const scrollReports = [];
const heapUsage = [];
for (let cycle = 0; cycle < 3; cycle += 1) {
scrollReports.push(await continuouslyScrollCanvas(page));
heapUsage.push(await collectHeapUsage(cdp));
}
await expectContinuousFramesMatchStableRender(
page,
scrollReports.flatMap((report) => report.frameSignatures),
);
const frames = await captureCanvasAtScrollIntervals(page);
await expectFramesMatchStableRender(page, frames);
for (const [index, frame] of frames.entries()) {
await writeFile(testInfo.outputPath(`diff-canvas-${name}-frame-${index}.png`), frame.image);
await testInfo.attach(`diff-canvas-${name}-frame-${index}`, {
body: frame.image,
contentType: "image/png",
});
expect(frame.image.byteLength).toBeGreaterThan(4_000);
}
const measuredCycles = scrollReports.slice(1);
expect(Math.max(...measuredCycles.map((cycle) => cycle.medianFrameMs))).toBeLessThanOrEqual(
MAX_MEDIAN_FRAME_MS,
);
expect(Math.max(...measuredCycles.map((cycle) => cycle.p95FrameMs))).toBeLessThanOrEqual(
MAX_P95_FRAME_MS,
);
for (const cycle of scrollReports) {
expect(Math.abs(cycle.maximumScrollTop - cycle.endScrollTop)).toBeLessThanOrEqual(1);
}
expect(heapUsage.at(-1)!.usedSize).toBeLessThanOrEqual(heapUsage[0]!.usedSize * 1.1 + 1_000_000);
const renderer = await readRendererInstrumentation(page);
expect(renderer.addedElements).toBe(0);
expect(renderer.removedElements).toBe(0);
expect(renderer.reactCommits).toBeLessThanOrEqual(2);
return {
scrollReports,
heapUsage,
renderer,
canvasPngBytes: frames.map((frame) => frame.image.byteLength),
};
}
async function readDiffMountReport(page: import("@playwright/test").Page): Promise<{
mountedCodeRows: number;
mountedGutterRows: number;
highestMountedRow: number;
bodyDomElements: number;
}> {
return page.getByTestId("diff-file-0-body").evaluate((body) => {
const codeRows = Array.from(
body.querySelectorAll<HTMLElement>('[data-testid^="diff-code-row-"]'),
async function reloadDiffPresentation(
page: Page,
requestedPresentation: { layout: "unified" | "split"; wrapLines: boolean },
): Promise<void> {
await page.evaluate(
({ preferencesKey, presentation }) => {
const current = JSON.parse(localStorage.getItem(preferencesKey) ?? "{}") as object;
localStorage.setItem(preferencesKey, JSON.stringify({ ...current, ...presentation }));
},
{ preferencesKey: CHANGES_PREFERENCES_KEY, presentation: requestedPresentation },
);
await page.reload();
await waitForWorkspaceTabsVisible(page);
await openChangesPanel(page);
await expect(page.getByTestId("git-diff-canvas")).toBeVisible({ timeout: 30_000 });
}
async function createLargeDiffRepository() {
const repo = await createTempGitRepo("diff-perf-", {
files: FILE_PATHS.map((filePath, fileIndex) => ({
path: filePath,
content: `export const baseline_${fileIndex} = 0;\n`,
})),
});
for (const [fileIndex, filePath] of FILE_PATHS.entries()) {
const generatedLines = Array.from(
{ length: LINE_COUNT_PER_FILE },
(_, lineIndex) =>
`export const generated_${fileIndex}_${lineIndex} = "${lineIndex}-${"wide".repeat(16)}";`,
).join("\n");
await writeFile(
path.join(repo.path, filePath),
`export const baseline_${fileIndex} = 1;\n${generatedLines}\n`,
);
const gutterRows = body.querySelectorAll('[data-testid^="diff-gutter-row-"]');
}
return repo;
}
async function configureUnwrappedUnifiedDiff(page: Page): Promise<void> {
await page.addInitScript(
({ preferencesKey }) => {
const scope = globalThis as typeof globalThis & {
__PASEO_DIFF_REACT_STATS__?: { commits: number };
};
scope.__PASEO_DIFF_REACT_STATS__ = { commits: 0 };
if (!localStorage.getItem(preferencesKey)) {
localStorage.setItem(
preferencesKey,
JSON.stringify({
layout: "unified",
viewMode: "flat",
wrapLines: false,
hideWhitespace: false,
}),
);
}
},
{ preferencesKey: CHANGES_PREFERENCES_KEY },
);
}
async function expectSingleCanvasSurface(page: Page): Promise<void> {
await expect(page.locator("canvas")).toHaveCount(1);
await expect(page.locator('[data-testid^="diff-code-row-"]')).toHaveCount(0);
const elementCount = await page
.getByTestId("git-diff-canvas-root")
.evaluate((root) => root.querySelectorAll("*").length);
expect(elementCount).toBeLessThan(150);
}
async function continuouslyScrollCanvas(page: Page): Promise<{
medianFrameMs: number;
p95FrameMs: number;
maximumFrameMs: number;
slowFrames: number;
endScrollTop: number;
maximumScrollTop: number;
frameSignatures: Array<{ scrollTop: number; signature: number }>;
}> {
return page.getByTestId("git-diff-scroll").evaluate(async (element) => {
const scroll = element as HTMLElement;
const maximumScrollTop = scroll.scrollHeight - scroll.clientHeight;
scroll.scrollTop = 0;
const frameDurations: number[] = [];
const frameSignatures: Array<{ scrollTop: number; signature: number }> = [];
let previousFrame = performance.now();
for (let step = 1; step <= 90; step += 1) {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
const now = performance.now();
frameDurations.push(now - previousFrame);
previousFrame = now;
scroll.scrollTop = (maximumScrollTop * step) / 90;
scroll.dispatchEvent(new Event("scroll", { bubbles: false }));
if (step % 10 === 0) {
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
const canvas = document.querySelector<HTMLCanvasElement>(
'[data-testid="git-diff-canvas"]',
)!;
frameSignatures.push({
scrollTop: scroll.scrollTop,
signature: canvasSignature(canvas),
});
previousFrame = performance.now();
}
}
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
const ordered = [...frameDurations].sort((left, right) => left - right);
const percentile = (fraction: number) =>
ordered[Math.min(ordered.length - 1, Math.floor(ordered.length * fraction))] ?? 0;
return {
mountedCodeRows: codeRows.length,
mountedGutterRows: gutterRows.length,
highestMountedRow: Math.max(
-1,
...codeRows.map((row) =>
Number((row.getAttribute("data-testid") ?? "").slice("diff-code-row-".length)),
),
),
bodyDomElements: body.querySelectorAll("*").length,
medianFrameMs: percentile(0.5),
p95FrameMs: percentile(0.95),
maximumFrameMs: Math.max(...frameDurations),
slowFrames: frameDurations.filter((duration) => duration > 50).length,
endScrollTop: scroll.scrollTop,
maximumScrollTop,
frameSignatures,
};
function canvasSignature(canvas: HTMLCanvasElement): number {
const sample = document.createElement("canvas");
sample.width = 64;
sample.height = 36;
const context = sample.getContext("2d")!;
context.drawImage(canvas, 0, 0, sample.width, sample.height);
const pixels = context.getImageData(0, 0, sample.width, sample.height).data;
let hash = 2166136261;
for (const value of pixels) hash = Math.imul(hash ^ value, 16777619);
return hash >>> 0;
}
});
}
interface CapturedCanvasFrame {
scrollTop: number;
image: Buffer;
}
async function captureCanvasAtScrollIntervals(page: Page): Promise<CapturedCanvasFrame[]> {
const scroll = page.getByTestId("git-diff-scroll");
const canvas = page.getByTestId("git-diff-canvas");
const frames: CapturedCanvasFrame[] = [];
for (let step = 0; step <= 8; step += 1) {
const scrollTop = await scroll.evaluate(async (element, progress) => {
const scrollElement = element as HTMLElement;
scrollElement.scrollTop =
(scrollElement.scrollHeight - scrollElement.clientHeight) * Number(progress);
scrollElement.dispatchEvent(new Event("scroll", { bubbles: false }));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
return scrollElement.scrollTop;
}, step / 8);
frames.push({ scrollTop, image: await canvas.screenshot() });
}
return frames;
}
async function expectFramesMatchStableRender(
page: Page,
frames: CapturedCanvasFrame[],
): Promise<void> {
const scroll = page.getByTestId("git-diff-scroll");
const canvas = page.getByTestId("git-diff-canvas");
for (const frame of frames) {
await scroll.evaluate(async (element, scrollTop) => {
element.scrollTop = scrollTop;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
await new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
);
}, frame.scrollTop);
expect(await canvas.screenshot()).toEqual(frame.image);
}
}
async function expectContinuousFramesMatchStableRender(
page: Page,
samples: Array<{ scrollTop: number; signature: number }>,
): Promise<void> {
const stable = await page.getByTestId("git-diff-scroll").evaluate(async (element, frames) => {
const canvas = document.querySelector<HTMLCanvasElement>('[data-testid="git-diff-canvas"]')!;
const signatures: number[] = [];
for (const frame of frames) {
element.scrollTop = frame.scrollTop;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
await new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
);
const sample = document.createElement("canvas");
sample.width = 64;
sample.height = 36;
const context = sample.getContext("2d")!;
context.drawImage(canvas, 0, 0, sample.width, sample.height);
const pixels = context.getImageData(0, 0, sample.width, sample.height).data;
let hash = 2166136261;
for (const value of pixels) hash = Math.imul(hash ^ value, 16777619);
signatures.push(hash >>> 0);
}
return signatures;
}, samples);
expect(stable).toEqual(samples.map((sample) => sample.signature));
}
async function expectIndependentHorizontalOffsetsSurviveVerticalScrolling(
page: Page,
): Promise<void> {
const first = page.getByTestId("diff-file-0-horizontal-scroll");
const second = page.getByTestId("diff-file-1-horizontal-scroll");
const firstOffset = await setHorizontalOffset(first, 240);
const secondOffset = await setHorizontalOffset(second, 120);
expect(firstOffset).not.toBe(secondOffset);
await page.getByTestId("git-diff-scroll").evaluate(async (element) => {
element.scrollTop = element.scrollHeight - element.clientHeight;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
element.scrollTop = 0;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
});
await expect.poll(() => first.evaluate((element) => element.scrollLeft)).toBe(firstOffset);
await expect.poll(() => second.evaluate((element) => element.scrollLeft)).toBe(secondOffset);
}
async function setHorizontalOffset(locator: Locator, offset: number) {
return locator.evaluate(async (element, nextOffset) => {
element.scrollLeft = nextOffset;
element.dispatchEvent(new Event("scroll", { bubbles: false }));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
return element.scrollLeft;
}, offset);
}
async function startRendererInstrumentation(page: Page): Promise<void> {
await page.getByTestId("git-diff-canvas-root").evaluate((root) => {
const scope = globalThis as typeof globalThis & {
__PASEO_DIFF_REACT_STATS__?: { commits: number };
__PASEO_DIFF_RENDERER_STATS__?: {
initialCommits: number;
addedElements: number;
removedElements: number;
};
};
const reactStats = scope.__PASEO_DIFF_REACT_STATS__ ?? { commits: 0, unmounts: 0 };
const stats = {
initialCommits: reactStats.commits,
addedElements: 0,
removedElements: 0,
};
scope.__PASEO_DIFF_RENDERER_STATS__ = stats;
new MutationObserver((records) => {
for (const record of records) {
stats.addedElements += Array.from(record.addedNodes).filter(
(node) => node.nodeType === Node.ELEMENT_NODE,
).length;
stats.removedElements += Array.from(record.removedNodes).filter(
(node) => node.nodeType === Node.ELEMENT_NODE,
).length;
}
}).observe(root, { childList: true, subtree: true });
});
}
async function readRendererInstrumentation(page: Page): Promise<{
reactCommits: number;
addedElements: number;
removedElements: number;
}> {
return page.evaluate(() => {
const scope = globalThis as typeof globalThis & {
__PASEO_DIFF_REACT_STATS__?: { commits: number };
__PASEO_DIFF_RENDERER_STATS__?: {
initialCommits: number;
addedElements: number;
removedElements: number;
};
};
const react = scope.__PASEO_DIFF_REACT_STATS__!;
const renderer = scope.__PASEO_DIFF_RENDERER_STATS__!;
return {
reactCommits: react.commits - renderer.initialCommits,
addedElements: renderer.addedElements,
removedElements: renderer.removedElements,
};
});
}
async function collectHeapUsage(cdp: CDPSession): Promise<{ usedSize: number; totalSize: number }> {
await cdp.send("HeapProfiler.collectGarbage");
return cdp.send("Runtime.getHeapUsage");
}
async function startChromiumTrace(cdp: CDPSession): Promise<void> {
await cdp.send("Tracing.start", {
categories: [
"devtools.timeline",
"v8.execute",
"blink.user_timing",
"disabled-by-default-v8.cpu_profiler",
].join(","),
transferMode: "ReturnAsStream",
});
}
async function stopChromiumTrace(cdp: CDPSession, testInfo: TestInfo): Promise<void> {
const completed = new Promise<string>((resolve, reject) => {
cdp.once("Tracing.tracingComplete", ({ stream }) => {
if (stream) resolve(stream);
else reject(new Error("Chromium trace completed without a stream"));
});
});
await cdp.send("Tracing.end");
const stream = await completed;
const chunks: string[] = [];
let eof = false;
while (!eof) {
const chunk = await cdp.send("IO.read", { handle: stream });
chunks.push(chunk.data);
eof = chunk.eof;
}
await cdp.send("IO.close", { handle: stream });
const trace = chunks.join("");
await writeFile(testInfo.outputPath("diff-canvas-chromium-trace.json"), trace);
await testInfo.attach("diff-canvas-chromium-trace", {
body: trace,
contentType: "application/json",
});
}
async function attachPerformanceReport(
testInfo: TestInfo,
report: Record<string, unknown>,
): Promise<void> {
await testInfo.attach("diff-canvas-performance", {
body: JSON.stringify(report, null, 2),
contentType: "application/json",
});
}