// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ 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 { 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 { 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((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([]); }); });