✨ Track allocation sizes and Skia cache in GPU memory harness

This commit is contained in:
Elena Torro 2026-09-28 14:47:27 +02:00
parent 45077e6608
commit c77b8798ae
4 changed files with 76 additions and 11 deletions

View File

@ -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 |

View File

@ -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,

View File

@ -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(""));
}

View File

@ -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,
};
},