vgpu2 symbolsView source ↗

Symbols in this topic

Timer

GPU pass timer created by timer(gpu); requires the "timestamp-query" device feature. Use it to find the expensive pass before optimizing: encoders only record, so CPU timing says nothing about GPU cost. Mark a pass with FramePassOptions.timer and the GPU brackets it with a begin/end timestamp pair. Decoded durations land in timer.onResults, in milliseconds, 1–2 frames after submit.

Import

TypeScript
1
import type { Timer, TimerSpan } from "vgpu";

Signature

TypeScript
1
2
3
4
5
6
7
8
9
interface TimerSpan {
  readonly name: string;
}
 
interface Timer {
  span(name: string): TimerSpan;
  onResults(cb: (spans: Readonly<Record<string, number>>) => void): () => void;
  dispose(): void;
}

Parameters

ParamTypeRequiredDefaultNotes
timer()No parameters.
timer.span.namestringNon-empty result key. Spans are memoized per name, so timer.span("shadows") is allocation-free in hot loops.
timer.onResults.cb(spans: Readonly<Record<string, number>>) => voidReceives one frozen name → milliseconds record per timed frame.

Returns: timer(gpu) returns Timer; span() returns a TimerSpan to pass as FramePassOptions.timer; onResults() returns an unsubscribe function; dispose() returns void.

Throws:

  • VGPU-TIMER-INVALID when timer(gpu) runs on a device without "timestamp-query" — request it: init({ requiredFeatures: ["timestamp-query"] }).
  • VGPU-TIMER-INVALID for an empty or non-string span name — name each timed pass, e.g. timer.span("shadows").
  • VGPU-TIMER-INVALID for a span name reused within one frame (each name holds one begin/end pair per frame) — give the second pass its own span.
  • VGPU-TIMER-INVALID for a non-TimerSpan FramePassOptions.timer value, or a span used with another gpu's frames — pass only timer.span(name) results, one timer per gpu.
  • VGPU-TIMER-INVALID for any use of a disposed timer or its spans — create a new timer with timer(gpu).
  • VGPU-TIMER-CAPACITY when one frame times more than 2048 spans; a timer owns one timestamp query set and WebGPU createQuerySet caps count at 4096 (2 queries per span) — time fewer passes, or spread timing across frames.

Examples

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { init, createMockAdapter, effect, frameLoop, target, timer } from "vgpu/mock";
 
const gpu = await init({ adapter: createMockAdapter({ features: ["timestamp-query"] }), requiredFeatures: ["timestamp-query"] });
const shadowMap = target(gpu, { size: [512, 512], depth: true });
const scene = target(gpu, { size: [256, 256], depth: true });
const casters = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(0); }`);
const world = effect(gpu, `@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }`);
 
const gpuTimer = timer(gpu);
gpuTimer.onResults((spans) => {
  console.log(`shadows ${spans.shadows}ms, main ${spans.main}ms`);
});
 
const loop = frameLoop(gpu, (f) => {
  f.pass({ target: shadowMap, timer: gpuTimer.span("shadows") }, (p) => p.draw(casters));
  f.pass({ target: scene, timer: gpuTimer.span("main") }, (p) => p.draw(world));
});
loop.stop();
TypeScript
1
2
3
4
5
6
import { init, timer } from "vgpu";
 
// Optional timing: only request the feature when the adapter has it.
const gpu = await init({ requiredFeatures: ["timestamp-query"] });
const gpuTimer = gpu.device.features.has("timestamp-query") ? timer(gpu) : undefined;
gpuTimer?.onResults((spans) => console.table(spans));

Notes

  • Start with a Timer before optimizing a suspected bottleneck: CPU wall-clock timings measure submission/encoding, not GPU execution. Results are asynchronous and normally arrive one to two frames after submit, so use them as a rolling signal rather than an immediate branch condition.
  • Results are GPU durations decoded from timestamp pairs: (end - begin) nanosecond ticks converted to milliseconds. Timestamp values are implementation-defined, and WebGPU notes the counter "may reset ... which can result in unexpected values such as negative deltas"; vgpu clamps negative deltas to 0 instead of reporting garbage.
  • Readback never blocks a frame: results resolve through rotated staging buffers, and when readbacks lag more frames than the ring holds, that frame's results are dropped rather than awaited. A dropped frame is dropped whole — no resolve is encoded at all, onResults simply does not fire for it, and nothing is merged or partially applied; the next frame that finds a free staging buffer reports again. await gpu.settled() covers pending readbacks for deterministic tests and teardown.
  • One timer can be used from several frames that are open at the same time, but results stay scoped to the newest one: opening a frame retargets the timer's per-frame bookkeeping, so an older frame submitted afterwards encodes no resolve and reports nothing (its spans are dropped, never merged into the newer frame's results). Submitting it is always safe, however long it stayed open — the query set it referenced is kept alive for it.
  • A manual frame(gpu) that attached a span holds the timer's query set until you submit() or cancel() it (a failed frame releases it too). Dropping such a frame without either leaks those resources for the lifetime of the gpu — the same leak as a native GPUCommandEncoder you never finish() — because a frame is never assumed abandoned: it could still be submitted. Always close the frames you open — submit() them, or frame.cancel() the ones you decided not to submit, which releases the query set without encoding a resolve or reporting a result — or let frame(gpu, cb) do it for you; gpu.dispose() (or device loss) is the backstop.
  • Results apply in submission order; a stale readback that lands after a newer one is discarded entirely — the whole set of spans from that frame is thrown away, never merged into the newer results. A readback that fails outright (device lost while mapping) is discarded the same way and reported on gpu.onError as VGPU-QUERY-READBACK; it never rejects a frame or gpu.settled().
  • Capacity starts at 32 spans per frame and grows only at frame boundaries — a pass that overflows the current query set goes untimed for that frame, and the next frame's larger set covers it. For the hard per-frame limit, see VGPU-TIMER-CAPACITY above.
  • dispose() releases the timer's query set and resolve/staging buffers after in-flight readbacks settle. Calling it mid-frame is safe: every frame that attached a span still references the query set from its pass descriptors, so destruction is deferred until each of those frames reports back — submitted, failed or abandoned — including when several manual frame(gpu)s are open at once, each of which holds its own reference. In-flight readbacks still apply, so results already submitted are not lost. gpu.dispose() disposes the timers that gpu created. Create the timer once and reuse its spans.
  • Timing granularity is the render pass: the pair lands in the pass descriptor's timestampWrites (beginningOfPassWriteIndex/endOfPassWriteIndex), so a span measures the whole pass, not individual draws.
  • See also: timer, Frame, FramePassOptions.timer, init.