How to use WebGPU Timestamp Query

This tutorial covers how to use WebGPU’s timestamp query to measure how much time your GPU commands take to execute.

Timestamp queries are an optional WebGPU feature and may not be supported in all implementations. At the time of writing it is currently disabled in browsers for security reasons (see discussion here).

See also: GitHub version of this tutorial.

Summary

Below is a summary of the workflow for using timestamp queries:

  1. Request access to timestamp-query when initializing the device
  2. Create a query set with capacity N (number of timestamps you want to store in a frame)
  3. Create a storage buffer with size N * 8. This is where the timestamp results are stored in nanoseconds, 64 bit numbers.
  4. Record timestamps by calling commandEncoder.writeTimestamp. This will record a timestamp after all previous commands have finished.
  5. Call commandEncoder.resolveQuerySet to write the recorded timestamps to the storage buffer
  6. Copy the results from the storage buffer to the CPU and decode them as a BigInt64Array (see BigInt)

Example: you want to measure how long it takes to render all transparent objects in your scene. You would write 1 timestamp before any transparent draw calls, then a 2nd timestamp after all the transparent draw calls, then compute the difference.

Step by step guide

A full example implementation can be found in this PR: https://github.com/OmarShehata/webgpu-compute-rasterizer/pull/5/files

Launch Chrome with the following command line flag:

--disable-dawn-features=disallow_unsafe_apis

First we add timestamp-query to the list of required features when requesting the device:

const device = await adapter.requestDevice({
requiredFeatures: ["timestamp-query"],
});

This will throw with the following error if it’s not enabled/supported:

Uncaught (in promise) TypeError: Failed to execute 'requestDevice' on 'GPUAdapter': Unsupported feature: timestamp-query

Then we create a query set and a query buffer:

const capacity = 3;//Max number of timestamps we can store
const querySet = device.createQuerySet({
type: "timestamp",
count: capacity,
});
const queryBuffer = device.createBuffer({
size: 8 * capacity,
usage: GPUBufferUsage.QUERY_RESOLVE
| GPUBufferUsage.STORAGE
| GPUBufferUsage.COPY_SRC
| GPUBufferUsage.COPY_DST,
});

We call commandEncoder.writeTimestamp(querySet, index) at any point in the pipeline where we want to record a timestamp:

// Add timestamps in between GPU commands
commandEncoder.writeTimestamp(querySet, 0);// Initial timestamp
// commandEncoder.draw(...)
commandEncoder.writeTimestamp(querySet, 1);

The index must be less than or equal to capacity - 1 that we defined earlier.

At the end of your frame, call commandEncoder.resolveQuerySet to actually write the timestamps to the storage buffer:

commandEncoder.resolveQuerySet(
querySet,
0,// index of first query to resolve
capacity,//number of queries to resolve
queryBuffer,
0);// destination offset

To get the timestamp results we need to copy the queryBuffer data to the CPU. Reading from a WebGPU buffer is explained in more detail here: https://web.dev/gpu-compute/#commands-submission.

Once you have the buffer data, you decode it as a BigInt typed array. The timestamps are recorded in nanoseconds.

// === After `commandEncoder.finish()` is called ===
// Read the storage buffer data
const arrayBuffer = await readBuffer(device, queryBuffer);
// Decode it into an array of timestamps in nanoseconds
const timingsNanoseconds = new BigInt64Array(arrayBuffer);
// ....async function readBuffer(device, buffer) {
const size = buffer.size;
const gpuReadBuffer = device.createBuffer({size, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(buffer, 0, gpuReadBuffer, 0, size);
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
return gpuReadBuffer.getMappedRange();
}

To make the output more useful, we can define labels for each timestamp we collect, and then print out the diff so we can see the time it took for each step in our pipeline. It may also be useful to convert the result to milliseconds.

This is implemented in the reference example here: https://github.com/OmarShehata/webgpu-compute-rasterizer/pull/5/files.

Hope you found this helpful! Sign up to my newsletter to follow WebGL/WebGPU writing & stay in touch.

Big thanks to Markus Schütz whose Potree implementation WebGPU implementation provided a great example of how to use timestamp queries. Thanks to Yang Gu for explaining how to enable it in Chrome.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Omar Shehata

Graphics programmer working on maps. I love telling stories and it's why I do what I do, from making games, to teaching & writing. https://omarshehata.me/