gpudump
v0.1.0
Published
Embeddable WebGL instrumentation: always-on frame metrics + on-demand frame captures, as one versioned data model.
Maintainers
Readme
gpudump
Backbone library to gather metrics about WebGL 1/2 (WebGPU will be added later) apps.
I was using stuff like Spector.js / gamestats.js for the longest time, but for serious projects I want more insight into how my game is being rendered. Lately I started making web games with my own rendering solutions, and I wanted a unified way to gather insight about different parts of my render pipeline.
But instead of making "Spector.js 2", I opted for this library — gpudump — that lets you
easily gather enough information to make your own domain-specific gamestats / Spector.js /
MCP for debugging rendering.
So: no UI, no transport, no opinions about your engine. It patches getContext, watches
the GL calls that go through it, and hands you plain data.
Two tiers
Metrics — always on, basically free. Every frame you get draw calls, triangles, state
switches, upload bytes, CPU/GPU time and a memory snapshot, split by your own markers.
This is the "rendering inspector" case, without hand-maintaining drawCalls++ all over
your renderer.
Captures — armed on demand. One frame (or one callback) recorded as a full command stream, plus the GL state, a resource table with shader sources, and optional images of what the framebuffer looked like along the way. This is the "Spector.js" case.
Both tiers are the same versioned data model — see src/core/types.ts, that file is the
contract. Everything downstream (your HUD, your capture viewer, your agent bridge) is
written against it and nothing else.
Quick start
import gpudump from "gpudump";
gpudump.install(); // before you create any contextThen per frame, if you want the numbers attributed to something meaningful:
gpudump.pushMarker("shadows");
drawShadows();
gpudump.popMarker();
gpudump.pushMarker("world");
drawWorld();
gpudump.popMarker();And read them wherever your HUD lives:
const [frame] = gpudump.fetchMetrics();
frame.counters.drawCalls; // frame totals
frame.markers.children; // same counters, per marker, as a tree
frame.gpuMs; // null if timer queries aren't availableFrames are detected from rAF automatically. If your loop isn't rAF-driven, call
frameStart() / frameEnd() yourself — the first explicit call turns auto-detection off.
Capturing a frame:
const capture = await gpudump.snapshotNextFrame({ visuals: { passes: true } });
// capture.commands, capture.resources, capture.frames, capture.blobsor a specific piece of code, wherever it runs:
const { capture, result } = await gpudump.snapshot(() => rebuildAtlas());Name your objects so captures are readable — label() is the only reason an inspector can
say "atlas" instead of "texture #7":
gpudump.label(texture, "atlas");API
| | |
|---|---|
| install(options?) / uninstall() | patch / restore this realm |
| frameStart() / frameEnd() | manual frame boundaries (optional) |
| pushMarker(name) / popMarker(name?) | nestable scopes; name on pop is just mismatch checking |
| fetchMetrics(amount?, realm?) | newest-first frames, deep-copied |
| changeMetricsBufferSize(frames) | ring depth, default 240 (~4s at 60fps) |
| label(obj, name) | name a texture/buffer/program/framebuffer |
| fetchResources(realm?) | per-resource byte accounting, on demand |
| snapshotNextFrame(options?) | capture the next frame boundary |
| snapshot(fn, options?) | capture whatever fn does; returns its value too |
| getRealms() | main thread + connected workers |
| serializeCapture / deserializeCapture | captures over the wire, binary blobs intact |
SnapshotOptions is where the cost lives: maxCommands (default 15k), stacks,
visuals (images after draws / at pass switches / on markers / at frame end, with a byte
budget), and resources (shader source on by default, buffer/texture contents off).
resources.texturePreviews is the cheap one — a thumbnail per 2D texture, downscaled on
the GPU once each, and it stands on its own without visuals.
Workers
Call install() in the worker too. It finds the main realm on its own, and from the main
thread getRealms() lists it — pass one to fetchMetrics, fetchResources or
snapshotNextFrame to reach across. Worker metrics land about one frame behind, and
fetchResources(realm) returns a promise for a remote realm, so await it either way.
Caveats
install()has to run before your contexts are created. It patchesgetContext; a context that already exists was never seen being made.- GPU timings need
EXT_disjoint_timer_query_webgl2and resolve a few frames late —gpuMsisnullwhere that isn't available, andgpuMsFrameLagtells you how late. - Byte totals are estimates: mips and formats are counted, driver padding isn't. Counts are exact.
- Not for production. There's no fast path that makes patched GL free.
MIT.
