Files
2026-07-13 12:25:07 +08:00

143 lines
5.8 KiB
TypeScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import type { ConsoleMessage, Page } from "@playwright/test";
import { expect, test } from "@perspective-dev/test";
import {
assertPlotNeverBlank,
calibratePlotBaseline,
captureFrames,
gotoBasic,
restoreChart,
waitOneFrame,
} from "./helpers";
const SCATTER = {
plugin: "X/Y Scatter",
columns: ["Quantity", "Postal Code"],
};
const TABLE_NAME = "load-viewer-csv";
const TOGGLE_COUNT = 12;
function collectLeakErrors(page: Page): string[] {
const hits: string[] = [];
page.on("console", (m: ConsoleMessage) => {
hits.push(m.text());
});
page.on("pageerror", (e: Error) => {
hits.push(String(e));
});
return hits;
}
async function toggleSecondViewer(page: Page, cfg: object): Promise<void> {
await page.evaluate(
async ({ cfg, tableName }) => {
const worker = (window as any).__TEST_WORKER__;
const table = await worker.open_table(tableName);
const v = document.createElement("perspective-viewer");
document.body.appendChild(v);
await v.load(table);
await v.restore(cfg);
},
{ cfg, tableName: TABLE_NAME },
);
await waitOneFrame(page);
await waitOneFrame(page);
await page.evaluate(() => {
document.querySelector("perspective-viewer[data-toggle]")?.remove();
});
}
async function toggleSecondViewerRacingInit(
page: Page,
cfg: object,
): Promise<void> {
await page.evaluate(
async ({ cfg, tableName }) => {
const worker = (window as any).__TEST_WORKER__;
const table = await worker.open_table(tableName);
const v = document.createElement("perspective-viewer");
document.body.appendChild(v);
await v.load(table);
v.restore(cfg).catch(() => {});
await new Promise<void>((resolve) =>
setTimeout(() => {
v.remove();
resolve();
}, 0),
);
},
{ cfg, tableName: TABLE_NAME },
);
await waitOneFrame(page);
}
test.describe("WebGL context leak", () => {
test.beforeEach(async ({ page }) => {
await gotoBasic(page);
await restoreChart(page, SCATTER);
});
test("repeated full toggles never evict the first chart's context", async ({
page,
}) => {
// test.setTimeout(180_000);
const leakErrors = collectLeakErrors(page);
const baseline = await calibratePlotBaseline(page);
expect(baseline).toBeGreaterThan(0);
const threshold = Math.max(1, Math.floor(baseline * 0.5));
for (let i = 0; i < TOGGLE_COUNT; i++) {
await toggleSecondViewer(page, SCATTER);
}
// Force the first viewer to re-render through its own renderer.
// If its context had been evicted by a leak, the worker can no
// longer paint it and the plot region comes back blank.
await restoreChart(page, SCATTER);
const frames = await captureFrames(page, async () => {
await waitOneFrame(page);
await waitOneFrame(page);
});
assertPlotNeverBlank(frames, threshold);
expect(leakErrors).toEqual([]);
});
test("toggling during renderer init never leaks a context", async ({
page,
}) => {
// test.setTimeout(180_000);
const leakErrors = collectLeakErrors(page);
const baseline = await calibratePlotBaseline(page);
expect(baseline).toBeGreaterThan(0);
const threshold = Math.max(1, Math.floor(baseline * 0.5));
for (let i = 0; i < TOGGLE_COUNT; i++) {
await toggleSecondViewerRacingInit(page, SCATTER);
}
await restoreChart(page, SCATTER);
const frames = await captureFrames(page, async () => {
await waitOneFrame(page);
await waitOneFrame(page);
});
assertPlotNeverBlank(frames, threshold);
expect(leakErrors).toEqual([]);
});
});