Set overlap to 0 to make frame duration match the sum of pass times. The simulated passes total 13.7 ms.
Timestamp queries measure GPU pass time. A pass records the GPU clock at its beginning and end. The timestamps reach the CPU a frame or two later. A profiler holds the query set and its readback, and makes a timer for each pass you time. It reads back only the passes that ran.
Records a frame into one encoder and submits once, with a clock. Borrows the root. The
synchronous recorder borrows each frame's encoder.
@submits ― always once a frame after your recorder returns, and again at each frame.submit()
@param ― record your frame, recorded into each frame's encoder
@returns ― a loop you run with each frame's time and destroy
create(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
const
constpass:TgpuComputePass
pass=
frame:frames.Frame
frame.
Frame.encoder: TgpuCommandEncoder
Records commands for submission after the recorder returns. submit() replaces this encoder.
Read it at each call instead of keeping it, and let the frame submit it.
Marks the pass for timing in this frame and returns timestamp writes, or undefined without
timestamp-query. Only marked passes are read back, because skipped passes keep stale
timestamps. Call in every frame that records the pass, as you record it: a timing starts no
earlier than the one marked before it. Throws after destroy, even without timestamp support.
writes(),
});
// ..
constpass:TgpuComputePass
pass.
TgpuComputePass.end(): void
Completes the recording of this compute pass
end();
constgpu:profiler.Profiler
gpu.
Profiler.capture(frame: frames.Frame): void
Resolves the frame's timestamps and maps them after the submit. Call it after the timed passes.
A second call in the same frame records nothing.
capture(
frame:frames.Frame
frame);// after the timed passes
});
constshadow:profiler.Timer
shadow.ms;
Timing.ms: number |undefined
Reports the median GPU duration in milliseconds from the last samples samples, or undefined
before any samples. A duration runs from the pass's first timestamp to its last, so it includes
time spent waiting on earlier passes and time shared with passes running beside it. It starts
no earlier than the pass timed before it in the frame, since some drivers write a first
timestamp before the pass can run. Passes overlap on the GPU, so their durations do not sum to
the frame duration.
A timer is a handle you keep and pass around; its name is only a label. Make timers at setup, up to capacity, 16 by default. timers lists them in order, for a readout.
A CPU clock read around queue.onSubmittedWorkDone() times when the queue drains, which includes waiting for earlier frames and for other programs using the GPU, and can’t split a frame into passes. To time every pass of a page without changing its code, use the GPU debug panel.
A timing spans a pass’s first and last timestamps. Passes can overlap. Tile-based GPUs, including those in Apple devices and most phones, can start the next pass’s vertex work while the previous pass’s fragments finish. Independent passes can run side by side. Compare passes individually, but don’t add their times to measure a frame.
A pass’s timing also counts time it spends waiting. The canvas pass often starts early and then waits for the passes whose results it reads, so a cheap tone map can read as the most expensive pass of the frame. Some drivers write a pass’s first timestamp before it can run at all. The profiler starts each timing no earlier than the pass timed before it, so call writes() as you record each pass.
A timer’s share splits the frame instead. Each stretch of GPU time goes to the earliest recorded pass running in it, since later passes usually wait on it. The canvas pass’s share is its tail after the rest finish, the time it adds to the frame. A pass that runs entirely beside an earlier one, such as a light pass beside a trace, gets none of it, and the earlier pass’s share includes both. Shares add up to the time any timed pass was running. To measure what one pass costs, leave it out and see how much shorter the frame gets.
gpu.frame.ms measures the interval from the earliest timed pass’s beginning to the latest timed pass’s end. It reports the median of those frame intervals.
Time the frame’s first and last passes, and frame is the whole frame’s GPU time, gaps between passes included. Passes between needn’t be timed.
The last pass is usually the canvas’s, which reads what the frame drew, so it ends last. A last pass that shares nothing with the rest, such as a compute pass after the canvas’s, can run beside them and end early.
A GPU that finishes a frame early lowers its clock until the next one, and the next frame’s passes run slower. Timings show how long passes took at the clock the GPU chose. To compare passes or measure a change, also measure without vsync, where the GPU runs at full clock. The GPU debug panel’s Measure runs frames back to back at full clock.
writes() returns what a pass descriptor’s timestampWrites takes, or pipeline.withTimestampWrites, which shares the compiled pipeline.
writes() also marks the pass, and only marked passes are read back, since a skipped pass leaves old timestamps in the query set. Call it in every frame that records the pass.
Call capture(frame) after the timed passes. It maps timestamps after submit through staging buffers, 3 by default, as a readback does. If all buffers are mapping, it skips timing that frame. Passes marked after the frame’s first capture also go untimed.
ms and share are medians of the last samples valid timings, or undefined before any arrive. The profiler drops the first skip frames it reads back, for warm-up, and any sample whose end isn’t after its beginning. A pass that runs only now and then, such as a level load, reports once it runs after warm-up. samples and skip both default to 30.
onSample(timing, ms) gets every ms sample after the first skip frames, a timer’s or gpu.frame’s, as it comes back. Keep a maximum or a percentile there, or plot it.
Keep a pass descriptor and assign scenePass.timestampWrites = sceneTime.writes() before each begin, so no frame allocates one.
A kit module that opens passes takes the writes in its last argument, a profiler.Timed:
Without timestamp-query, available is false. writes() returns undefined, which timestampWrites accepts. capture does nothing and every ms stays undefined, so the same code still runs. Optional features missing from the adapter are omitted without an error. Check available and fall back to frame times.
Sets the milliseconds the timing should stay under, such as 6 for a frame's GPU time, or a function of the display's frame interval in milliseconds, called at every judgement: (interval) => 0.75 * interval.
A frame over its GPU budget misses the display’s refresh, and one well under it could draw more pixels. dynamicResolution.create(timing, { budget }) turns a timing into a resolution scale. It lowers the scale as soon as the timing runs over budget and raises it when there is room. A target set’s resolution reads the scale and changes without allocating:
Picks a resolution scale that keeps timing under budget. It assumes time grows with pixels,
the square of the scale, to step close to the budget in one move: down as soon as a median of
recent samples runs over, up after a second at one scale with room to spare, at least one step
while the median is under 85% of the budget. It judges only samples of frames recorded at the
current scale. Without samples, as without timestamp-query, the scale stays where it is: set
it after create to start lower. Throws unless budget is finite and above 0, when created or
computed, and range runs from above 0 up to at most 1.
@param ― timing what to keep under budget, such as a profiler's frame or a pass's timer
@returns ― a resolution controller you update every frame and read the scale of
create(
constgpu:profiler.Profiler
gpu.
Profiler.frame: profiler.Timing
Reports the duration from the earliest timed pass start to the latest timed pass end in each
frame. Covers the frame's GPU work when the first and last passes are timed, including gaps
between passes. The final pass, such as the canvas pass, waits for work it draws.
frame,{
Options.budget: number |((interval:number)=> number)
Sets the milliseconds the timing should stay under, such as 6 for a frame's GPU time, or a
function of the display's frame interval in milliseconds, called at every judgement:
(interval) => 0.75 * interval.
Creates render targets that follow a size. Replaces the whole set when the size changes. Put
targets needed only sometimes in their own set and create that set when needed. Throws for
transient targets with sampling, storage or mip levels, for multisampled targets with storage or
mip levels, and for layered targets that are multisampled, mipped or have more layers than the
device allows.
@param ― source the size it follows, such as a surface
@param ― specs a format and options per target, by name
@param ― options.resolution the fraction of each side drawn, which changes without allocating
@returns ― a set of targets you fit every frame and destroy
create(
constroot:TgpuRoot
root,
constsurface:web.Surface
surface,{
hdr:{
readonly format:"rgba16float";
readonly sampled:true;
}
hdr},{
Options.resolution?: number |(()=> number)|undefined
Sets the fraction of each side to draw, capped at 1 and read at each fit(). Textures keep
the source size, so changing resolution allocates nothing. Passes use viewports for the drawn
regions, and sampling reads only those regions. Default 1.
resolution:()=>
constdetail:dynamicResolution.DynamicResolution
detail.
DynamicResolution.scale: number
Reports the scale for a target set's resolution, the top of range until a move or a set.
Moves keep it on steps of 0.05 within range. Setting it clamps to range, ignores samples
of earlier frames and throws a RangeError unless the value is finite.
Reads the timing's newest sample, if one arrived, and moves scale when the samples call for
it. Call once a frame, before the targets that read the scale fit.
Rebuilds all textures if the source size changed, destroying the old textures. Updates
regions for the current resolution. Returns true after a rebuild so callers can update bind
groups that hold old textures. Call before recording passes that use the targets. Throws after
destroy.
fit();
// the scene's pass with sceneTime.writes(), the output's with outputTime.writes()
constgpu:profiler.Profiler
gpu.
Profiler.capture(frame: frames.Frame): void
Resolves the frame's timestamps and maps them after the submit. Call it after the timed passes.
A second call in the same frame records nothing.
capture(
frame:frames.Frame
frame);
});
The budget applies to the timing you pass: gpu.frame for the frame’s GPU time, or one timer, such as a ray march’s.
A function as budget takes the display’s frame interval in milliseconds. Three quarters of it allows 12.5 ms at 60 Hz and 6.25 ms at 120 Hz.
Time that doesn’t scale with pixels, such as a final pass at full size, makes a step down too small, and the next median steps again.
A scale that ran over budget stays out of reach of steps up for 10 seconds. Running over again within twice that doubles it, up to 40 seconds.
Setting scale overrides it, as a detail slider does, and the next update measures from there. Skip update to hold the scale.
Without timestamp-query, no samples arrive and the scale stays at the top of range. Set scale after create to a scale a slow device can draw.
The GPU debug panel times every pass and dispatch without changing the program, but skips passes that write their own timestamps, like the profiler’s. TypeGPU’s withPerformanceCallback times one pipeline and reports only its last run in an encoder.
A pass recorded without its timer’s writes() that frame isn’t read back, and nothing warns.
Omitting the frame’s first or last pass from timing understates frame, without a warning.
Shares count only timed passes. An untimed pass is shared out to the timed passes running beside it, or left in the gaps.
Dawn on Metal writes no timestamps for an empty pass, so an empty marker pass can’t open the frame’s timing.
A median of 30 frames trails by half a second at 60 Hz and hides spikes; onSample sees them.
A GPU that slows its clock under light load, as phones and Apple GPUs do, reports longer times at a low scale, so a resolution controller can settle below the scale the device could draw.