Skip to content

@typegpu/kit/debug, @typegpu/kit/debug/recording, @typegpu/kit/debug/gpuDebugPanelgpuDebug · recording · gpuDebugPanel

GPU debug panel

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
const debug: gpuDebug.GpuDebug
debug
=
import gpuDebug
gpuDebug
.
function install(): 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

install
();
const button: HTMLButtonElement
button
.
HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)

The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.

MDN Reference

addEventListener
('click', () => {
if (
const debug: gpuDebug.GpuDebug
debug
.
GpuDebug.running: boolean
running
)
const debug: gpuDebug.GpuDebug
debug
.
GpuDebug.stop(): void

Removes the panel and stops recording. The page keeps running.

stop
();
else
const debug: 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.

To profile a page you host with web.host, install the panel before the host requests its device, and only when the address asks for it:

import * as
import web
web
from '@typegpu/kit/web';
if (new
var URLSearchParams: new (init?: string[][] | Record<string, string> | string | URLSearchParams) => URLSearchParams

The URLSearchParams interface defines utility methods to work with the query string of a URL.

MDN Reference

URLSearchParams
(
var location: Location

The read-only location property of the Window interface returns a Location object with information about the current location of the document.

MDN Reference

location
.
Location.search: string

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, "".

MDN Reference

search
).
URLSearchParams.has(name: string, value?: string): boolean

The has() method of the URLSearchParams interface returns a boolean value that indicates whether the specified parameter is in the search parameters.

MDN Reference

has
('profile')) {
const {
const gpuDebug: typeof import("node_modules/@typegpu/kit/src/debug/gpuDebug")
gpuDebug
} = await import('@typegpu/kit/debug');
const gpuDebug: typeof import("node_modules/@typegpu/kit/src/debug/gpuDebug")
gpuDebug
.
function install(): 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

install
().
GpuDebug.start(): void

Mounts the panel and starts timing frames.

start
();
}
import web
web
.
function host<{
frame(frame: Frame): void;
destroy(): void;
}>(createScene: web.CreateScene<Record<never, never>, {
frame(frame: Frame): void;
destroy(): void;
}>, hosting: web.HostOptions<{
frame(frame: Frame): void;
destroy(): void;
}>): web.Hosted<{
frame(frame: Frame): void;
destroy(): void;
}> (+1 overload)

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

host
(
function createScene(root: TgpuRoot, surface: Surface): {
frame(frame: Frame): void;
destroy(): void;
}
createScene
, {
HostOptions<S extends Scene>.canvas: HTMLCanvasElement

Supplies the canvas for the scene.

canvas
});
  • 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.
  1. 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.

  2. Hover a column to freeze the graphs on it.

  3. 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.

  4. Save trace downloads recent frames as a Chrome trace for Perfetto.

  5. 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.

  6. 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.

kit.recording

tool

Exports the recorder, the WebGPU patches and the frame hook for reading frames without mounting the panel.

watchFrames(next)

instrument(options?) → Instrumentation

createRecorder() → Recorder

nexttakes
the watch, or undefined to stop watching
Instrumentationreturns
patches you start with hooks, and dispose of to remove
Recorderreturns
hooks for instrument's start, which the panel reads
types
FrameWatchGpuCommandInstrumentHooksInstrumentationAccessGpuResourceGpuResourceUseBarCapturedCommandDependencyFrameCaptureRecorderSegmentTimings
InstrumentOptionsinstrument · 1 field
frames?

'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:

const debug: gpuDebug.GpuDebug
debug
.
GpuDebug.start(): void

Mounts the panel and starts timing frames.

start
();
const
const frame: FrameCapture
frame
= await
const debug: 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
if (
const frame: FrameCapture
frame
.
FrameCapture.timings: Timings
timings
=== 'complete') {
for (const
const command: CapturedCommand
command
of
const frame: FrameCapture
frame
.
FrameCapture.commands: CapturedCommand[]
commands
) {
const
const after: string[]
after
=
const command: CapturedCommand
command
.
CapturedCommand.after: Dependency[]
after
.
Array<Dependency>.map<string>(callbackfn: (value: Dependency, index: number, array: Dependency[]) => string, thisArg?: any): string[]

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
}`));
function report(line: string): void
report
(`${
const command: CapturedCommand
command
.
CapturedCommand.displayName: string
displayName
} ${
const command: 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 ${
const after: 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
(' ')}`);
}
}
function report(line: string): void
report
(
const debug: 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 trace preserves each pass’s start and end.

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:

const encoder = root.createCommandEncoder();
const raw = 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.

const debug: gpuDebug.GpuDebug
debug
.
GpuDebug.start(): void

Mounts the panel and starts timing frames.

start
();
const
const result: gpuDebug.Measurement
result
= await
const debug: gpuDebug.GpuDebug
debug
.
GpuDebug.measure(options?: gpuDebug.MeasureOptions): Promise<gpuDebug.Measurement>

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.

MDN Reference

log
(
import gpuDebug
gpuDebug
.
function measureText(measurement: gpuDebug.Measurement): string
export measureText

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
(
const result: 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.

kit.gpuDebugPanel

tool

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.

import { GpuDebugPanel } from '@typegpu/kit/debug/gpuDebugPanel';
import { useScene } from '@typegpu/kit/scenes/react';
import { Share, View } from 'react-native';
import { Canvas } from 'react-native-webgpu';
import { blockfall } from './blockfall.ts';
export function BlockfallScreen() {
const { canvas, scene } = useScene(blockfall, {}, { optionalFeatures: ['timestamp-query'] });
return (
<View style={{ flex: 1 }}>
<Canvas {...canvas} style={{ flex: 1 }} />
<GpuDebugPanel
scene={scene}
onLog={(message) => Share.share({ message })}
onTrace={(message) => Share.share({ message })}
style={{ position: 'absolute', left: 8, right: 8, bottom: 8 }}
/>
</View>
);
}
  • 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:

import {
function watchFrames(next: FrameWatch | undefined): void

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
} from '@typegpu/kit/debug/recording';
function watchFrames(next: FrameWatch | undefined): void

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
((
milliseconds: number
milliseconds
,
run: (milliseconds: number) => void
run
) => {
const
const start: number
start
=
var performance: Performance
performance
.
Performance.now(): DOMHighResTimeStamp

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).

MDN Reference

now
();
run: (milliseconds: number) => void
run
(
milliseconds: number
milliseconds
);
function report(ms: number): void
report
(
var performance: Performance
performance
.
Performance.now(): DOMHighResTimeStamp

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).

MDN Reference

now
() -
const start: number
start
);
});
// Stop watching
function watchFrames(next: FrameWatch | undefined): void

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.