A panel that profiles any WebGPU page without changing its program.
kit.gpuDebug
tool
Installs WebGPU profiling patches without starting recording.
gpuDebug.install()→ GpuDebug
gpuDebug.measureText(measurement)
GpuDebugreturns
a profiler you start and stop, and destroy to remove the patches
measurementtakes
what debug.measure() resolved with
types
MeasurementMeasureOptionsSpreadDetailGpuDebug
owns
up to 16 query sets ·32 buffers ·a texture per canvas
Per device, while it times passes: timestamps, a buffer they resolve into and one to map; while it measures, a texture each canvas's frames draw into.
lifecycle
✓destroy()✓[Symbol.dispose]
requires
timestamp-queryoptional
It asks for it on devices requested after it installs; without it, no timings.
platform
webonly
Hover a column to freeze the graphs, then press capture to record the next frame. The lattice example has its panel open at the bottom left.
A missed frame budget does not identify the slow pass. Install the panel before requesting the device. It patches WebGPU to add timestamp queries without changing module code. Start profiling to time and name each pass and dispatch, with results overlaid on the page.
import{
import gpuDebug
gpuDebug}from'@typegpu/kit/debug';
// Install before requesting the device, including before tgpu.init()
const
constdebug:gpuDebug.GpuDebug
debug=
import gpuDebug
gpuDebug.
functioninstall():gpuDebug.GpuDebug
Installs WebGPU profiling patches without starting recording. Call this before requesting a
device. It requests timestamp-query and records pipeline names, bind groups, texture views
and shader code so recording can start later without reloading.
When stopped, finish and submit still look up command buffers for passes timed before the
stop. Other patched calls check the recording flag and run the original method. Animation and
video frame callbacks define frames. Installing again destroys the preceding installation.
Without WebGPU, it installs no patches and the panel remains empty.
@kind ― tool
@platform ― web
@requires ― timestamp-query optional it asks for it on devices requested after it installs;
without it, no timings
@owns ― up to 16 query sets, 32 buffers, a texture per canvas: per device, while it times passes:
timestamps, a buffer they resolve into and one to map; while it measures, a texture each canvas's
frames draw into
@returns ― a profiler you start and stop, and destroy to remove the patches
The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.
Removes the panel and stops recording. The page keeps running.
stop();
else
constdebug:gpuDebug.GpuDebug
debug.
GpuDebug.start(): void
Mounts the panel and starts timing frames.
start();
});
start() mounts the panel. stop() removes it while the page keeps running. destroy() also removes the WebGPU patches. Installing again destroys the previous installation.
To profile from the first frame, call start() right after install(). The examples on this site do so when the address ends in ?debug, such as the fish tank.
The search property of the Location interface is a search string, also called a query string, that is a string containing a "?" followed by the parameters of the location's URL. If the URL does not have a search query, this property contains an empty string, "".
Installs WebGPU profiling patches without starting recording. Call this before requesting a
device. It requests timestamp-query and records pipeline names, bind groups, texture views
and shader code so recording can start later without reloading.
When stopped, finish and submit still look up command buffers for passes timed before the
stop. Other patched calls check the recording flag and run the original method. Animation and
video frame callbacks define frames. Installing again destroys the preceding installation.
Without WebGPU, it installs no patches and the panel remains empty.
@kind ― tool
@platform ― web
@requires ― timestamp-query optional it asks for it on devices requested after it installs;
without it, no timings
@owns ― up to 16 query sets, 32 buffers, a texture per canvas: per device, while it times passes:
timestamps, a buffer they resolve into and one to map; while it measures, a texture each canvas's
frames draw into
@returns ― a profiler you start and stop, and destroy to remove the patches
Runs a scene on a canvas with a root, surface, frame loop and animation. destroy tears them
down in order. Use send to control the scene or onFrame to read it. If scene options are
optional, call host(createScene, { canvas }). Throws if both device and root are supplied.
@param ― createScene the scene file's createScene
@param ― options the scene's third argument
@param ― hosting the canvas, the device or root, and what the page runs beside the scene
@returns ― a hosted scene you send closures to and destroy
install() adds timestamp-query to every device requested after it when the adapter has the feature, so the host’s device times each pass. A root or device created before install() times nothing.
The dynamic import loads the panel only on ?profile, so pages opened without it don’t download it.
The graphs show GPU and CPU milliseconds per frame. Each column covers 100 ms, with reference lines at 8.33 and 16.67 ms. Legends list the largest contributors over the last five columns. GPU timings can arrive half a second after the frame.
The GPU graph’s title reads gpu 4.00ms at vsync. Timings under vsync include clock scaling; compare passes within a frame, and use Measure for what a frame costs.
Hover a column to freeze the graphs on it.
Capture records the next frame’s passes, copies and uploads, including timings and resource reads and writes. Each read identifies the last command that wrote that resource in the frame, such as #2, or earlier frame. The console receives both a line per command and structured data.
Save trace downloads recent frames as a Chrome trace for Perfetto.
Measure runs the scene’s frames back to back for a second at full clock and prints the result under the graph and to the console. See measuring.
Log prints mean frame times over the last second to the console. It lists totals, then GPU and CPU contributors from largest to smallest.
[gpu debug]
60 fps · gpu 10.00ms over 60 frames · cpu 1.00ms over 60 frames
gpu 8.00ms scene
gpu 2.00ms post
Drag the title bar to move the panel or drag its corner to resize it. The panel remembers its position.
'animation-frame' | 'animate'= 'animation-frame' where window exists, otherwise 'animate'
Selects what starts a frame.
owns
up to 16 query sets ·32 buffers ·a texture per canvas
Per device, while it times passes: timestamps, a buffer they resolve into and one to map; while it drives frames, a texture each canvas's frames draw into.
lifecycle
–destroy()(none)✓[Symbol.dispose]
requires
timestamp-queryoptional
It asks for it on devices requested after it patches; without it, no timings.
platform
webonly
Call debug.capture(), debug.trace(), debug.summary() or debug.measure() from scripts such as smoke tests. They perform the same actions as the Capture, Save trace, Log and Measure buttons:
constdebug:gpuDebug.GpuDebug
debug.
GpuDebug.start(): void
Mounts the panel and starts timing frames.
start();
const
constframe:FrameCapture
frame=await
constdebug:gpuDebug.GpuDebug
debug.
GpuDebug.capture(): Promise<FrameCapture>
Captures commands, resource reads and writes, and timings for the next frame. The panel
capture button does the same. Throws synchronously if recording has not started, and rejects
if it stops first.
capture();// the next frame, once its timestamps are back
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map(({
from: number |undefined
Identifies the last command that wrote the resource in this frame. Undefined means the write
occurred in an earlier frame.
from})=>(
from: number |undefined
Identifies the last command that wrote the resource in this frame. Undefined means the write
occurred in an earlier frame.
from===
var undefined
undefined?'prev':`#${
from: number
Identifies the last command that wrote the resource in this frame. Undefined means the write
occurred in an earlier frame.
from}`));
functionreport(line:string):void
report(`${
constcommand:CapturedCommand
command.
CapturedCommand.displayName: string
displayName}${
constcommand:CapturedCommand
command.
CapturedCommand.ms: number |undefined
Gives command time in milliseconds excluding overlap with earlier commands in the frame.
ms?.
Number.toFixed(fractionDigits?: number): string
Returns a string representing a number in fixed-point notation.
@param ― fractionDigits Number of digits after the decimal point. Must be in the range 0 - 20, inclusive.
toFixed(2)} ms, after ${
constafter:string[]
after.
Array<string>.join(separator?: string): string
Adds all the elements of an array into a string, separated by the specified separator string.
@param ― separator A string used to separate one element of the array from the next in the resulting string. If omitted, the array elements are separated with a comma.
join(' ')}`);
}
}
functionreport(line:string):void
report(
constdebug:gpuDebug.GpuDebug
debug.
GpuDebug.summary(): string
Returns mean frame times over the last second as text, with GPU then CPU sections sorted by
time. The panel log button prints the same text. Times are in milliseconds per frame. Throws
while stopped.
summary());
These methods throw synchronously while the panel is idle with Call gpuDebug.start() before capture(), trace(), summary() or measure().. Calls to capture share the pending capture until it resolves. stop() rejects it.
A capture is a FrameCapture that you can serialize with JSON.stringify. It has a frame, commands in submission order and timings. The timing status is 'complete' or one of:
'dropped' when every query set was still waiting for its readback;
'late' when some timestamps weren’t back after two seconds;
'none' when no command was timed, because timestamp-query is unavailable, passes write their own timestamps or the frame only copies.
Each command has index, kind, displayName, label and pipelines. Its kind is 'render', 'compute', 'copy' or 'upload'. It also has:
ms, the time charged to it in the graph;
start and end, in milliseconds from the start of the frame’s first timed command;
uses, the resources and their access modes;
after, the last writer for each read in from, or undefined for an earlier frame.
debug.detail reads or sets the detail, as the detail button does.
trace() returns the last two seconds of frames as a Chrome trace object. Its clock starts at the first timestamp after start(), so traces from one profiling session align. Stopping and starting resets that clock.
GPU time. The panel adds timestamp-query to the device and timestamps each pass.
CPU time. The panel times each animation frame callback and uses its name.
Names. A pass uses its label or the names of its pipelines. A pipeline uses its label, which TypeGPU derives from its variable, or its entry point.
Pass timestamps can overlap. Tile-based GPUs, including Apple GPUs and most phone GPUs, can begin vertex work while an earlier pass finishes fragment work. See what a timing means. The panel charges each pass only for time not covered by an earlier pass, so each column totals the GPU’s busy time.
Switch between own time and full timestamp spans to see overlapping passes. The panel charges only time not covered by earlier passes; bloom spans can begin with the scene while waiting for its output.
The detail button in the title bar sets what GPU time is split into:
Per pass, each pass by its label or pipelines.
Per group, each pass by the debug groups around it, so many passes read as one step such as sim.
Per dispatch, each compute dispatch on its own, under its groups. This is the default.
A compute pass has one pair of timestamps, which can hide a slow dispatch among others. To see the dispatches, the panel splits it into one pass per dispatch. A stand-in pass remembers setPipeline and setBindGroup calls. Each dispatch opens a real pass, restores that state, dispatches and ends.
Turn off Per dispatch to combine five dispatches into one compute-pass timing.
Splitting drains the GPU between dispatches, so a split frame takes longer than the frame it splits. The panel splits only every eighth frame and the next capture. It times the other frames whole, and divides each pass’s time between its dispatches by their shares in the latest split frames. The graph’s total comes from whole frames only.
Debug groups name a step that spans several passes or dispatches. Push them on the encoder or on a pass, through root.unwrap for TypeGPU objects:
constencoder=root.createCommandEncoder();
constraw=root.unwrap(encoder);
raw.pushDebugGroup('sim');
// the water step's passes
raw.popDebugGroup();
A name joins its groups with ›, such as sim › advect. Repeated dispatches with one name add up into one entry, counted as ×5.
Under vsync the GPU idles between frames and lowers its clock, so live timings depend on the clock the GPU chose. Measure, or debug.measure(), runs the scene’s frames back to back, unpaced by the display, so the GPU stays at full clock.
Runs the page's frames back to back for about a second, unpaced by the display, so the GPU
keeps its full clock, and times them. The panel measure button does the same and prints
measureText of the result. Throws synchronously while stopped.
measure({
MeasureOptions.seconds?: number |undefined
Sets how long to measure, in seconds, after a quarter second of warm-up. Default 1.
seconds:1,
MeasureOptions.dt?: number |undefined
Sets the scene time between measured frames, in seconds, so each does a displayed frame's
work. Default the live frame interval, or 1/60 before two frames.
dt:1/120,
MeasureOptions.detail?:gpuDebug.Detail|undefined
Sets what GPU time is split into in parts. Default the panel's.
detail:'group'});
var console:Console
console.
Console.log(...data: any[]): void
The console.log() static method outputs a message to the console.
Formats a measurement as the panel shows it: totals, then GPU parts with calls per frame.
@param ― measurement what debug.measure() resolved with
@returns ― lines of plain text
measureText(
constresult:gpuDebug.Measurement
result));
It prints, for example:
measured 500 frames back to back, 8.33ms of scene time apart
gpu 1.50ms (1.40 to 2.00) · a frame every 2.00ms · cpu 0.20ms
gpu 1.20ms scene ×9
gpu 0.30ms post
It warms up for a quarter second, then measures for seconds, 1 by default.
Each frame advances scene time by dt seconds, so it does a displayed frame’s work. It defaults to the live frame interval. A scene with a fixed step, such as 1/120 s, does one step per frame at dt: 1 / 120.
gpu is the median busy time per frame with the 10th and 90th percentiles, from the first timed pass’s start to the last one’s end, without the gaps between frames. wall is the interval between frames finishing, the frame rate’s interval with no display to wait for. cpu is the mean of the frame callbacks.
parts splits gpu at detail, the panel’s by default. With per group or per dispatch detail, it then runs a quarter second of split frames to learn the shares.
The page’s animation frame callbacks run in the measured frames with one timestamp each, dt apart. animate loops run there too. Live frames wait and resume with time continuing from the last measured frame.
While it measures, each canvas returns a stand-in texture of its size and format, so frames don’t wait for the display. The page shows the last live frame.
It rejects if no frame runs within two seconds, a frame throws or the panel stops.
While idle, the panel adds timestamp-query to the device and records WebGPU object metadata as objects are created. This includes pipeline names, bind-group resources, view textures, canvas textures and shader code. finish and submit look up command buffers so timings begun before a stop can still resolve. Other patched calls check a flag and call the original without query sets, timestamp writes or readbacks.
Profiling starts with the next command encoder, so the first frame after start() is missing from the graphs.
Passes begun before a stop keep their timestamps, and their query sets come back once their encoders are submitted.
Shows the GPU debug panel over a React Native scene hosted with useScene or scenes.host.
GpuDebugPanel(props)
propstakes
the hosted scene, the graph's device and the panel's callbacks
types
ProfileGpuDebugPanelProps
owns
a device unless given or provided one ·a root ·2 buffers ·a bind group ·a pipeline ·a kit/frames and the graph canvas's configuration; on the scene's runtime ·up to 16 query sets and 32 buffers per device while it records ·and a texture per canvas while it measures
requires
timestamp-queryoptional
The scene's device needs it for GPU times; without it, CPU times only.
platform
nativeonly
<GpuDebugPanel> shows the same graphs over a scene hosted with useScene. It records while it is mounted and times frames on the scene’s runtime, including a worker. Request timestamp-query with the scene’s device. Without it, the panel shows CPU times only.
The title bar has the detail button, Measure, Capture and Log. Their text appears under the graph. onLog receives the log instead, such as to share it. onTrace adds Trace, which passes the Chrome trace as JSON.
Tap the arrow to collapse the panel to its title bar. On a narrow screen, the title bar drops its title.
The panel draws its graph with device, the nearest <GPUDeviceProvider> device or a device it requests. It does not record its own draws.
Mounting the panel patches WebGPU on the scene’s runtime, and the patches stay until the runtime ends. A pipeline created before that is named by its label.
@typegpu/kit/debug/recording provides the instrumentation the panel uses. instrument() patches WebGPU in an idle state and allows one installation at a time. It returns start(hooks), stop() and [Symbol.dispose](). createRecorder() creates the hooks used by the panel. Its stop() rejects a pending capture; call it when you stop recording. Without window, frames are those that the kit’s animate runs. Pass frames to choose.
watchFrames wraps every frame that animate runs on the current runtime, such as a React Native worker, which has no requestAnimationFrame to patch. The watch receives the frame’s timestamp and run. Call run once to run the frame, or several times with later timestamps to run frames back to back:
Wraps every frame that animate runs on this runtime, so a profiler can time frames where
there is no requestAnimationFrame to patch, such as on a React Native worker. Holds one watch
per runtime; a later call replaces it.
@param ― next the watch, or undefined to stop watching
@returns ― nothing; the watch wraps frames from the next one animate runs
Wraps every frame that animate runs on this runtime, so a profiler can time frames where
there is no requestAnimationFrame to patch, such as on a React Native worker. Holds one watch
per runtime; a later call replaces it.
@param ― next the watch, or undefined to stop watching
@returns ― nothing; the watch wraps frames from the next one animate runs
The performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and ServiceWorker contexts).
The performance.now() method returns a high resolution timestamp in milliseconds. It represents the time elapsed since Performance.timeOrigin (the time when navigation has started in window contexts, or the time when the worker is run in Worker and ServiceWorker contexts).
Wraps every frame that animate runs on this runtime, so a profiler can time frames where
there is no requestAnimationFrame to patch, such as on a React Native worker. Holds one watch
per runtime; a later call replaces it.
@param ― next the watch, or undefined to stop watching
@returns ― nothing; the watch wraps frames from the next one animate runs
watchFrames(
var undefined
undefined);
A runtime holds one watch, and a later call replaces it. instrument() with frames: 'animate' holds the watch until it is disposed.
Installing after device creation cannot add timestamp-query. Pipelines and bind groups created before installation lack recorded names or resource metadata.
On the web, a frame is an animation or video frame callback. A loop driven by its own timer produces empty graphs and captures that never resolve.
Overlapping time goes to the pass submitted first. This undercounts passes with heavy vertex work and independent passes that run together. The trace shows their full intervals.
Passes with their own timestamps, such as the kit profiler, keep them and appear untimed in the panel.
Encoders submitted in one task share a query set with room for 256 timed passes. A frame uses one set per 256 passes, counting each split dispatch as a pass. An encoder that begins a timed pass but is never submitted retains its set. The rest of that frame goes untimed.
Chrome rounds timestamps to 100 µs unless chrome://flags/#enable-webgpu-developer-features is enabled. Shorter passes can measure zero and do not appear.
Measuring runs the page’s own frames, so a scene that reads the wall clock rather than its frame time does more or less work per frame than live.
Without vsync, frames can outrun timestamp readbacks. With 16 query sets, the panel times only some frames. A timed frame can include overlapping work from untimed frames. Count frames to measure throughput.