Measuring GPU time on a tile-based GPU without fooling yourself
· 1 min read · webgpu, performance, profiling, game-dev, three.js
The first GPU baseline for Threejs-Punk said 28 ms for a frame that took 7.5 ms. The measurement was wrong, and the reason is worth knowing if you profile WebGPU on Apple hardware.
Why per-pass times lie #
On tile-based GPUs, every Apple GPU, a pass's vertex stage starts while the previous pass is still shading. So each pass's own [begin, end] also covers waiting for the one before it. Summed, the per-pass numbers read 2 to 4 times the real frame.
What the profiler reports instead #
A console call drives the loop by hand for N frames and reports:
frameMs: wall time per frame with frames pipelined back to back, no vsync. This is the honest number, max(CPU, GPU) in steady state.gpuBusyMs: the union of all pass intervals from raw timestamp queries.- per-pass exclusive share: from where the previous pass ended to where this one ends.
- CPU per subsystem, draw calls, triangles and p99.
gpuBusyMs still reads a little high, because frames resolved one at a time let the GPU clock down. Trust frameMs.
Things that keep it honest #
- Turn adaptive DPR off first, so the resolution holds still.
- Measure at the resolution players use: a 5K display at a max pixel ratio of 1.5 is a 3840x2160 buffer.
- To find what costs time, toggle subsystems off and compare frame time.
The score #
I hold the game to a 1 to 5 rubric per metric (frame, GPU busy, CPU, draw calls, p99) with a target of 4.5 or better at 2880x1620 and 3840x2160. A 4.5 on frame time is 80 fps: a 60 Hz display with 25% headroom. Draw calls are scored on their own because they are what hurts weaker CPUs, even when the desktop CPU score is fine.
The fixes that came out of it are in the uploads post. The project is on its project page.
Related projects
- Threejs-Punk — Fully featured Racing Game