- カテゴリー
- WebGPU・性能計測
- 公開日
- 2026.09.07
Contents
はじめに
performance.now()でqueue.submit()の前後を囲むだけでは、GPUが実際に処理していた時間は分かりません。WebGPUはJavaScriptからGPUへコマンドを非同期に送るため、「JavaScriptがコマンドを投入した時間」と「GPUがコマンドを実行した時間」は別の値です。
この記事は、WebGPUを初めて計測する人を対象にしています。timestamp-queryへの対応を確認し、空のcompute passを計測するHTMLを作ります。読み終えると、GPU時間とCPU側の完了待ち時間を区別し、非対応環境でも誤った値を表示しない実装ができます。
作るもの
1つのindex.htmlを作り、ボタンを押すと次の結果を表示します。
timestamp-query対応環境:GPU区間とCPU側の完了待ち時間- 非対応環境:CPU側の完了待ち時間だけ
- WebGPU自体が使えない環境:理由を含むエラーメッセージ
空の処理は実行時間が短く、タイマーの精度調整によってGPU区間が0 msになる場合があります。これは直ちに失敗を意味しません。
仕組み
timestamp-queryでは、compute passの開始時と終了時の2か所へタイムスタンプを書き込みます。query setはJavaScriptから直接読めないため、次の順番で結果を取り出します。
- passの開始時と終了時にタイムスタンプを書き込む
- query setの結果をresolve用バッファへ解決する
- MAP_READを付けた読取用バッファへコピーする
- 読取用バッファをmapし、2つの64ビット値の差を求める
仕様上、タイムスタンプの単位はナノ秒です。ただし値の決め方や精度は実装依存で、セキュリティとプライバシーへの対策として精度が下げられる場合があります。
必要環境
- WebGPUを利用できるブラウザとGPU環境
- HTTPSで配信したページ、または
http://localhostなどの安全なローカル開発環境 - テキストエディタ
- ローカルHTTPサーバー(この記事ではPythonの例を使用)
timestamp-queryはWebGPUのオプション機能です。WebGPUが使えても、この機能が使えるとは限りません。そのため、必ずadapter.features.has("timestamp-query")で確認します。
手順1:HTMLを作る
作業用フォルダにindex.htmlを作り、次の内容を保存します。コードは、機能検出、GPUコマンドの作成、GPU時間とCPU時間の計測、エラー表示までを1つにまとめています。
<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<title>WebGPU timestamp-query</title>
<body>
<button id="measure">計測する</button>
<pre id="result">未計測</pre>
<script type="module">
const button = document.querySelector("#measure");
const result = document.querySelector("#result");
button.addEventListener("click", async () => {
button.disabled = true;
result.textContent = "計測中...";
let device;
let querySet;
let resolveBuffer;
let readBuffer;
try {
if (!navigator.gpu) {
throw new Error("この環境ではWebGPUを利用できません");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("GPUAdapterを取得できません");
const canTimestamp = adapter.features.has("timestamp-query");
device = await adapter.requestDevice({
requiredFeatures: canTimestamp ? ["timestamp-query"] : [],
});
const shaderModule = device.createShaderModule({
code: `@compute @workgroup_size(1) fn main() {}`,
});
const pipeline = device.createComputePipeline({
layout: "auto",
compute: { module: shaderModule, entryPoint: "main" },
});
if (canTimestamp) {
querySet = device.createQuerySet({ type: "timestamp", count: 2 });
resolveBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
});
readBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
}
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass(
canTimestamp
? {
timestampWrites: {
querySet,
beginningOfPassWriteIndex: 0,
endOfPassWriteIndex: 1,
},
}
: {},
);
pass.setPipeline(pipeline);
pass.dispatchWorkgroups(1);
pass.end();
if (canTimestamp) {
encoder.resolveQuerySet(querySet, 0, 2, resolveBuffer, 0);
encoder.copyBufferToBuffer(resolveBuffer, 0, readBuffer, 0, 16);
}
const commandBuffer = encoder.finish();
const started = performance.now();
device.queue.submit([commandBuffer]);
await device.queue.onSubmittedWorkDone();
const cpuWaitMs = performance.now() - started;
if (!canTimestamp) {
result.textContent = [
"timestamp-query: 非対応",
`CPU側の完了待ち: ${cpuWaitMs.toFixed(3)} ms`,
"GPU時間としては表示しません",
].join("\n");
return;
}
await readBuffer.mapAsync(GPUMapMode.READ);
const values = new BigUint64Array(
readBuffer.getMappedRange().slice(0),
);
readBuffer.unmap();
if (values[1] < values[0]) {
throw new Error("タイムスタンプの順序が不正です");
}
const elapsedNs = values[1] - values[0];
result.textContent = [
"timestamp-query: 対応",
`GPU区間: ${Number(elapsedNs) / 1_000_000} ms`,
`CPU側の完了待ち: ${cpuWaitMs.toFixed(3)} ms`,
].join("\n");
} catch (error) {
result.textContent = `エラー: ${error.message}`;
} finally {
querySet?.destroy();
resolveBuffer?.destroy();
readBuffer?.destroy();
device?.destroy();
button.disabled = false;
}
});
</script>
</body>
</html>
手順2:ローカルサーバーで開く
index.htmlを保存したフォルダで、次のコマンドを実行します。
python -m http.server 8000
ブラウザでhttp://localhost:8000を開き、「計測する」を押します。file://で直接開くのではなく、ローカルHTTPサーバーを使ってください。
コードの解説
機能を確認してからデバイスを作る
timestamp-queryが使えるときだけrequiredFeaturesへ指定します。非対応環境で無条件に要求すると、requestDevice()が失敗します。
const canTimestamp = adapter.features.has("timestamp-query");
const device = await adapter.requestDevice({
requiredFeatures: canTimestamp ? ["timestamp-query"] : [],
});
16バイトのバッファを2つ使う
タイムスタンプは64ビット値が2個なので、必要なサイズは16バイトです。resolve用バッファへ結果を解決し、CPUからmapできる読取用バッファへコピーします。
const resolveBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
});
const readBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
bigintのまま差を求める
BigUint64Arrayから得る値はbigintです。絶対値を先にNumberへ変換すると精度を失う可能性があるため、差を求めてからミリ秒へ変換します。
const elapsedNs = values[1] - values[0];
const elapsedMs = Number(elapsedNs) / 1_000_000;
GPU時間とCPU側の待ち時間を分ける
timestamp-queryは指定したpassのGPU実行区間を測ります。一方、performance.now()とonSubmittedWorkDone()の組み合わせは、JavaScriptがsubmitしてから先に投入済みのキューを含む作業が完了するまでの待ち時間です。転送やキュー待ちなども関係するため、2つの値が一致する必要はありません。
動作確認
対応環境では、次のような結果が表示されます。数値は端末や負荷によって変わります。
timestamp-query: 対応
GPU区間: 0 ms
CPU側の完了待ち: 1.234 ms
確認するポイントは、エラーが出ないこと、2つのタイムスタンプが取得できること、終了値が開始値以上であることです。空のcompute passでは、精度調整によりGPU区間: 0 msとなる場合があります。
非対応分岐では次の形式になります。この値をGPU時間と呼ばないことが重要です。
timestamp-query: 非対応
CPU側の完了待ち: 1.234 ms
GPU時間としては表示しません
実用コードでの注意
- 1回目はパイプライン準備などの影響を受けやすいため、ウォームアップとして分ける
- 1回だけで判断せず、複数回測って中央値や分布を見る
- query setとバッファは毎回作らず、必要に応じて再利用する
- map中の読取バッファを次のコピー先にしない
- ページの表示状態、電源状態、GPU負荷、端末情報を検証メモに残す
- デバイス喪失が起きた試行や、終了値が開始値より小さい試行を結果から除外する
長時間の重い処理を繰り返すと、ブラウザやOSの応答性を落とす可能性があります。負荷を段階的に増やし、開発中のデータを保存してから試してください。
トラブル対処
navigator.gpuがない:対応ブラウザか、安全なコンテキストで開いているか確認します。timestamp-queryがない:GPU時間とは表示せず、CPU側の完了待ち時間だけを使います。- mapに失敗する:読取バッファがmap中でないか、usageとコピーサイズが一致するか確認します。
- GPU区間が0になる:空の処理では起こり得ます。実際の処理を測り、ウォームアップ後に複数回確認します。
- 値が不自然:ページの表示状態やGPU負荷をそろえ、デバイス喪失がなかったか確認します。
実運用では、次のようにデバイス喪失も監視します。
device.lost.then((info) => {
console.error("GPUDevice lost", info.reason, info.message);
});
まとめ
GPUの処理時間とJavaScriptから見た完了待ち時間は別物です。timestamp-queryを機能検出し、passの開始と終了を測ることでGPU区間を取得できます。非対応時はCPU側の値へフォールバックし、その値を「GPU時間」と表示しないようにすると、WebGPUの性能を誤解しにくくなります。
