mirror of
https://github.com/penpot/penpot.git
synced 2026-10-31 15:00:32 +00:00
✨ Add fixture server and alloc traces to GPU harness
This commit is contained in:
parent
f726f5943d
commit
71cae946ab
2
.gitignore
vendored
2
.gitignore
vendored
@ -113,4 +113,4 @@ opencode.json
|
||||
/.codex/
|
||||
/tools/__pycache__
|
||||
/frontend/playwright/perf/results*/
|
||||
/frontend/playwright/perf/report.html
|
||||
/frontend/playwright/perf/report*.html
|
||||
|
||||
@ -27,12 +27,15 @@ Projects: `gpu-memory-chromium`, `gpu-memory-firefox`, `gpu-memory-webkit`.
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `PERF_LABEL` | `git describe --dirty` | Name of the build under test; results go to `results/<label>/` |
|
||||
| `PERF_FIXTURE` | `render-wasm/get-file-shadows.json` | `get-file` fixture under `playwright/data/` |
|
||||
| `PERF_FIXTURE` | `render-wasm/get-file-shadows.json` | `get-file` fixture under `playwright/data/`; served over local HTTP, so size is no limit |
|
||||
| `PERF_REPEATS` | `3` | Runs per engine; repeat 0 is dropped as warm-up |
|
||||
| `PERF_ITERATIONS` | `12` | Steps per phase |
|
||||
| `PERF_DPR` | `2` | Device pixel ratio |
|
||||
| `PERF_EDIT_TARGET` | select all | `x,y` in CSS px: the edit phase selects the element there instead of everything |
|
||||
| `PERF_SKIA_CACHE_MB` | build default | Skia resource cache budget, set after the first render (needs `set_resource_cache_limit_mb` in the wasm build) |
|
||||
| `PERF_PURGE_PROBE` | off | `1` calls `free_gpu_resources` at the end and samples again |
|
||||
| `PERF_TIMEOUT_MIN` | `15` | Time limit per run; heavy fixtures in Firefox need more |
|
||||
| `PERF_TRACE_SIZE` | off | `WxH`: record the call stacks of allocations of that size (in each sample's `web.traces`) |
|
||||
| `PERF_PROC_MATCH` | `ms-playwright` | Substring of the browser binary path used to find its processes |
|
||||
|
||||
Report flags: `--baseline <label>` (default `develop`, else the oldest
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { WasmWorkspacePage } from "../ui/pages/WasmWorkspacePage";
|
||||
import { installWebGLMemoryTracker } from "./gpu-memory/webgl-tracker.js";
|
||||
@ -23,7 +25,11 @@ const config = {
|
||||
iterations: Number(env.PERF_ITERATIONS ?? 12),
|
||||
settleMs: Number(env.PERF_SETTLE_MS ?? 300),
|
||||
purgeProbe: env.PERF_PURGE_PROBE === "1",
|
||||
// "x,y" in CSS px: select the element there instead of everything. On a
|
||||
// big page, moving every shape at once takes minutes per step.
|
||||
editTarget: env.PERF_EDIT_TARGET?.split(",").map(Number) ?? null,
|
||||
skiaCacheMb: env.PERF_SKIA_CACHE_MB ? Number(env.PERF_SKIA_CACHE_MB) : null,
|
||||
timeoutMin: Number(env.PERF_TIMEOUT_MIN ?? 15),
|
||||
outDir: env.PERF_OUT_DIR ?? "playwright/perf/results",
|
||||
};
|
||||
|
||||
@ -52,6 +58,36 @@ async function setPageSize(page, size) {
|
||||
}, size);
|
||||
}
|
||||
|
||||
// Serves the fixture over plain HTTP. `route.fulfill` sends the body through
|
||||
// the DevTools protocol, and a large fixture (tens of MB) drops the browser
|
||||
// session.
|
||||
let fixtureServer;
|
||||
|
||||
async function fixtureUrl() {
|
||||
if (!fixtureServer) {
|
||||
fixtureServer = createServer((_req, res) => {
|
||||
res.writeHead(200, {
|
||||
"content-type": "application/transit+json",
|
||||
"access-control-allow-origin": "*",
|
||||
});
|
||||
createReadStream(`playwright/data/${config.fixture}`).pipe(res);
|
||||
});
|
||||
await new Promise((resolve) =>
|
||||
fixtureServer.listen(0, "127.0.0.1", resolve),
|
||||
);
|
||||
}
|
||||
return `http://127.0.0.1:${fixtureServer.address().port}/get-file`;
|
||||
}
|
||||
|
||||
test.afterAll(() => fixtureServer?.close());
|
||||
|
||||
// goToWorkspace waits for the page name, which differs per fixture.
|
||||
async function fixturePageName(fixture) {
|
||||
const data = JSON.parse(await readFile(`playwright/data/${fixture}`, "utf8"));
|
||||
const pages = data["~:data"]?.["~:pages-index"] ?? {};
|
||||
return Object.values(pages)[0]?.["~:name"] ?? "Page 1";
|
||||
}
|
||||
|
||||
async function wasmHash() {
|
||||
const bytes = await readFile("resources/public/js/render-wasm.wasm").catch(
|
||||
() => null,
|
||||
@ -83,10 +119,12 @@ async function settle(page, quietMs = config.settleMs, maxMs = 10000) {
|
||||
);
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(installWebGLMemoryTracker);
|
||||
page.on("crash", () => console.error("[gpu-memory] page crashed"));
|
||||
page.on("pageerror", (e) => console.error(`[gpu-memory] ${e.message}`));
|
||||
await page.addInitScript(installWebGLMemoryTracker, {
|
||||
traceSize: env.PERF_TRACE_SIZE ?? null,
|
||||
});
|
||||
await WasmWorkspacePage.init(page);
|
||||
await WasmWorkspacePage.mockRPC(
|
||||
page,
|
||||
@ -101,7 +139,7 @@ for (let repeat = 0; repeat < config.repeats; repeat++) {
|
||||
browser,
|
||||
browserName,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(15 * 60 * 1000);
|
||||
test.setTimeout(config.timeoutMin * 60 * 1000);
|
||||
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
const dir = `${config.outDir}/${config.label}`;
|
||||
@ -120,7 +158,12 @@ for (let repeat = 0; repeat < config.repeats; repeat++) {
|
||||
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(config.fixture);
|
||||
await workspace.goToWorkspace();
|
||||
const url = await fixtureUrl();
|
||||
// Registered last, so it wins over the fulfill route mockGetFile added.
|
||||
await page.route(/get-file\?/, (route) => route.continue({ url }));
|
||||
await workspace.goToWorkspace({
|
||||
pageName: await fixturePageName(config.fixture),
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
if (config.skiaCacheMb) {
|
||||
// Set after the first render, so the load phase still runs on the
|
||||
@ -168,7 +211,12 @@ for (let repeat = 0; repeat < config.repeats; repeat++) {
|
||||
await sample("pan", i);
|
||||
}
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+a");
|
||||
if (config.editTarget) {
|
||||
const [ex, ey] = config.editTarget;
|
||||
await page.mouse.click(ex, ey);
|
||||
} else {
|
||||
await page.keyboard.press("ControlOrMeta+a");
|
||||
}
|
||||
for (let i = 0; i < config.iterations; i++) {
|
||||
await page.keyboard.press(i % 2 ? "Shift+ArrowLeft" : "Shift+ArrowRight");
|
||||
await sample("edit", i);
|
||||
@ -201,6 +249,7 @@ for (let repeat = 0; repeat < config.repeats; repeat++) {
|
||||
fixture: config.fixture,
|
||||
iterations: config.iterations,
|
||||
skiaCacheMb: config.skiaCacheMb,
|
||||
editTarget: config.editTarget,
|
||||
viewport: base,
|
||||
dpr: testInfo.project.use.deviceScaleFactor ?? 1,
|
||||
renderer,
|
||||
|
||||
@ -5,8 +5,12 @@
|
||||
// running totals of the bytes backing textures, renderbuffers and buffers,
|
||||
// plus counters for every storage (re)allocation. Sizes are what WebGL was
|
||||
// asked for; drivers add padding, alignment and their own copies on top.
|
||||
export function installWebGLMemoryTracker() {
|
||||
export function installWebGLMemoryTracker(options = {}) {
|
||||
if (globalThis.__gpuMem) return;
|
||||
// Allocation size ("WxH") whose call stacks are recorded, for finding
|
||||
// which render path creates it.
|
||||
const traceSize = options.traceSize ?? null;
|
||||
const traces = new Map();
|
||||
|
||||
const protos = [
|
||||
globalThis.WebGL2RenderingContext?.prototype,
|
||||
@ -105,6 +109,14 @@ export function installWebGLMemoryTracker() {
|
||||
const allocsBySize = new Map();
|
||||
|
||||
function logAlloc(kind, w, h, bytes) {
|
||||
if (traceSize === `${w}x${h}`) {
|
||||
const stack = (new Error().stack ?? "")
|
||||
.split("\n")
|
||||
.slice(3, 30)
|
||||
.map((l) => l.trim())
|
||||
.join("\n");
|
||||
traces.set(stack, (traces.get(stack) ?? 0) + 1);
|
||||
}
|
||||
const key = `${kind} ${w}x${h}`;
|
||||
const entry = allocsBySize.get(key) ?? { count: 0, bytes: 0 };
|
||||
entry.count++;
|
||||
@ -443,6 +455,12 @@ export function installWebGLMemoryTracker() {
|
||||
},
|
||||
allocatedBytesTotal,
|
||||
allocsBySize: bySize,
|
||||
traces: traceSize
|
||||
? [...traces.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8)
|
||||
.map(([stack, count]) => ({ count, stack }))
|
||||
: undefined,
|
||||
contexts: contexts.length,
|
||||
contextLosses,
|
||||
wasmHeapBytes: wasmHeapBytes(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user