From c77b8798aeb5296f3ac41812702fd6cfe4352b8d Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Mon, 28 Sep 2026 14:47:27 +0200 Subject: [PATCH] :sparkles: Track allocation sizes and Skia cache in GPU memory harness --- frontend/playwright/perf/README.md | 1 + frontend/playwright/perf/gpu-memory.spec.js | 15 +++++++ .../playwright/perf/gpu-memory/report.mjs | 26 +++++++++-- .../perf/gpu-memory/webgl-tracker.js | 45 ++++++++++++++++--- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/frontend/playwright/perf/README.md b/frontend/playwright/perf/README.md index 17cdb28e34..d895d77f26 100644 --- a/frontend/playwright/perf/README.md +++ b/frontend/playwright/perf/README.md @@ -31,6 +31,7 @@ Projects: `gpu-memory-chromium`, `gpu-memory-firefox`, `gpu-memory-webkit`. | `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_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_PROC_MATCH` | `ms-playwright` | Substring of the browser binary path used to find its processes | diff --git a/frontend/playwright/perf/gpu-memory.spec.js b/frontend/playwright/perf/gpu-memory.spec.js index 8778f9313e..cd9d93739d 100644 --- a/frontend/playwright/perf/gpu-memory.spec.js +++ b/frontend/playwright/perf/gpu-memory.spec.js @@ -23,6 +23,7 @@ const config = { iterations: Number(env.PERF_ITERATIONS ?? 12), settleMs: Number(env.PERF_SETTLE_MS ?? 300), purgeProbe: env.PERF_PURGE_PROBE === "1", + skiaCacheMb: env.PERF_SKIA_CACHE_MB ? Number(env.PERF_SKIA_CACHE_MB) : null, outDir: env.PERF_OUT_DIR ?? "playwright/perf/results", }; @@ -121,6 +122,19 @@ for (let repeat = 0; repeat < config.repeats; repeat++) { await workspace.mockGetFile(config.fixture); await workspace.goToWorkspace(); await workspace.waitForFirstRenderWithoutUI(); + if (config.skiaCacheMb) { + // Set after the first render, so the load phase still runs on the + // build's default budget. + const applied = await page.evaluate((mb) => { + const mod = globalThis.app?.common?.render_wasm?.wasm?.internal_module; + if (typeof mod?._set_resource_cache_limit_mb !== "function") + return false; + mod._set_resource_cache_limit_mb(mb); + return true; + }, config.skiaCacheMb); + if (!applied) + throw new Error("wasm build lacks set_resource_cache_limit_mb"); + } await sample("load"); // Canvas captures from the first repeat show whether a change broke // rendering, not only how much memory it saved. @@ -186,6 +200,7 @@ for (let repeat = 0; repeat < config.repeats; repeat++) { repeat, fixture: config.fixture, iterations: config.iterations, + skiaCacheMb: config.skiaCacheMb, viewport: base, dpr: testInfo.project.use.deviceScaleFactor ?? 1, renderer, diff --git a/frontend/playwright/perf/gpu-memory/report.mjs b/frontend/playwright/perf/gpu-memory/report.mjs index 87f896d747..820cf1dcd7 100644 --- a/frontend/playwright/perf/gpu-memory/report.mjs +++ b/frontend/playwright/perf/gpu-memory/report.mjs @@ -71,6 +71,12 @@ const SERIES = [ unit: "count", get: (s) => s.web?.allocs?.total, }, + { + key: "skia", + title: "Skia resource cache", + unit: "bytes", + get: (s) => s.web?.skiaCache?.bytes, + }, { key: "gpuRss", title: "RSS of processes holding the GPU", @@ -189,6 +195,18 @@ const METRICS = [ unit: "bytes", get: (r) => perCycleGrowth(r, webgl), }, + { + key: "skiaCachePeak", + title: "Skia resource cache, peak", + unit: "bytes", + get: (r) => peak(r, (s) => s.web?.skiaCache?.bytes), + }, + { + key: "skiaPurgeableIdle", + title: "Skia purgeable bytes, idle", + unit: "bytes", + get: (r) => lastOf(r, "idle", (s) => s.web?.skiaCache?.purgeableBytes), + }, { key: "wasmHeapPeak", title: "WASM heap, peak", @@ -341,7 +359,7 @@ function fmtDelta(delta) { } function pad(s, n) { - return String(s).padEnd(n); + return `${String(s).padEnd(n - 1)} `; } // ------------------------------------------------------------------- main @@ -394,7 +412,7 @@ for (const engine of engines) { ` ${label}: ${used}/${n} runs, commit ${meta.commit}, wasm ${meta.wasm}, ${meta.renderer?.renderer ?? "unknown renderer"}`, ); } - const header = [pad("metric", 38), ...present.map((l) => pad(l, 22))].join( + const header = [pad("metric", 38), ...present.map((l) => pad(l, 26))].join( "", ); console.log(`\n${header}`); @@ -404,9 +422,9 @@ for (const engine of engines) { const cells = present.map((label) => { const st = byLabel[label].metrics[m.key]; const value = fmt(st?.median, m.unit); - if (label === baseline) return pad(value, 22); + if (label === baseline) return pad(value, 26); const v = verdict(base, st, m.informative, m.unit); - return pad(`${value} ${fmtDelta(v.delta)} ${v.verdict}`, 22); + return pad(`${value} ${fmtDelta(v.delta)} ${v.verdict}`, 26); }); console.log([pad(m.title, 38), ...cells].join("")); } diff --git a/frontend/playwright/perf/gpu-memory/webgl-tracker.js b/frontend/playwright/perf/gpu-memory/webgl-tracker.js index 70e1d4464e..65eea42782 100644 --- a/frontend/playwright/perf/gpu-memory/webgl-tracker.js +++ b/frontend/playwright/perf/gpu-memory/webgl-tracker.js @@ -100,6 +100,17 @@ export function installWebGLMemoryTracker() { }; const totals = { texture: 0, renderbuffer: 0, buffer: 0 }; let allocatedBytesTotal = 0; + // Allocations since the last snapshot, by kind and size: shows which + // surfaces a step reallocated. + const allocsBySize = new Map(); + + function logAlloc(kind, w, h, bytes) { + const key = `${kind} ${w}x${h}`; + const entry = allocsBySize.get(key) ?? { count: 0, bytes: 0 }; + entry.count++; + entry.bytes += bytes; + allocsBySize.set(key, entry); + } let contextLosses = 0; // texture -> Map("face:level" -> bytes); renderbuffer/buffer -> bytes @@ -209,10 +220,14 @@ export function installWebGLMemoryTracker() { }); wrap(proto, "texImage2D", (gl, a) => { if (a.length >= 8) { - setTextureLevel(gl, a[0], a[1], a[3] * a[4] * bpp(a[2], a[6], a[7])); + const bytes = a[3] * a[4] * bpp(a[2], a[6], a[7]); + setTextureLevel(gl, a[0], a[1], bytes); + logAlloc("texture", a[3], a[4], bytes); } else { const [w, h] = sourceSize(a[5]); - setTextureLevel(gl, a[0], a[1], w * h * bpp(a[2], a[3], a[4])); + const bytes = w * h * bpp(a[2], a[3], a[4]); + setTextureLevel(gl, a[0], a[1], bytes); + logAlloc("texture", w, h, bytes); } }); wrap(proto, "texImage3D", (gl, a) => { @@ -232,6 +247,7 @@ export function installWebGLMemoryTracker() { }); wrap(proto, "texStorage2D", (gl, a) => { setTextureStorage(gl, a[0], a[1], a[3], a[4], 1, bpp(a[2])); + logAlloc("texture", a[3], a[4], a[3] * a[4] * bpp(a[2])); }); wrap(proto, "texStorage3D", (gl, a) => { setTextureStorage(gl, a[0], a[1], a[3], a[4], a[5], bpp(a[2])); @@ -250,11 +266,9 @@ export function installWebGLMemoryTracker() { stateOf(gl).renderbuffer = rb; }); wrap(proto, "renderbufferStorage", (gl, [, fmt, w, h]) => { - setObjectBytes( - "renderbuffer", - stateOf(gl).renderbuffer, - w * h * bpp(fmt), - ); + const bytes = w * h * bpp(fmt); + setObjectBytes("renderbuffer", stateOf(gl).renderbuffer, bytes); + logAlloc("renderbuffer", w, h, bytes); }); wrap( proto, @@ -262,6 +276,7 @@ export function installWebGLMemoryTracker() { (gl, [, samples, fmt, w, h]) => { const bytes = w * h * bpp(fmt) * Math.max(1, samples); setObjectBytes("renderbuffer", stateOf(gl).renderbuffer, bytes); + logAlloc("renderbuffer", w, h, bytes); }, ); wrap(proto, "createRenderbuffer", () => counters.renderbuffersCreated++); @@ -372,6 +387,15 @@ export function installWebGLMemoryTracker() { }; } + function skiaCache() { + const mod = globalThis.app?.common?.render_wasm?.wasm?.internal_module; + if (typeof mod?._resource_cache_bytes !== "function") return null; + return { + bytes: mod._resource_cache_bytes(), + purgeableBytes: mod._resource_cache_purgeable_bytes(), + }; + } + function wasmHeapBytes() { if (wasmMemories.length === 0) return null; return wasmMemories.reduce((acc, m) => acc + m.buffer.byteLength, 0); @@ -396,6 +420,11 @@ export function installWebGLMemoryTracker() { rendererInfo, snapshot() { const drawingBuffer = drawingBuffers(); + const bySize = [...allocsBySize.entries()] + .map(([key, v]) => ({ key, ...v })) + .sort((a, b) => b.bytes - a.bytes) + .slice(0, 12); + allocsBySize.clear(); return { textures: { count: textureLevels.size, bytes: totals.texture }, renderbuffers: { @@ -413,9 +442,11 @@ export function installWebGLMemoryTracker() { counters.bufferAllocs, }, allocatedBytesTotal, + allocsBySize: bySize, contexts: contexts.length, contextLosses, wasmHeapBytes: wasmHeapBytes(), + skiaCache: skiaCache(), jsHeapBytes: performance.memory?.usedJSHeapSize ?? null, }; },