about:tracing フラグを使用した WebGL ゲームのプロファイリング

Lilli Thompson 氏
Lilli Thompson

測定できなければ、改善もできません。

ロード ケルビン

HTML5 ゲームを高速化するには、まずパフォーマンスのボトルネックを特定する必要がありますが、これは難しい場合があります。フレーム/秒(FPS)データの評価は出発点ですが、全体像を把握するには、Chrome のアクティビティにおける微妙な違いを把握する必要があります。

about:tracing ツールを使用すると、パフォーマンスの改善を目的とした急いで回避できるが、本質的には善意による推測による回避策を回避できます。時間とエネルギーを大幅に節約し、各フレームで Chrome の動作をより明確に把握できるようになります。また、この情報を使ってゲームを最適化できます。

紹介:トレース

Chrome の about:tracing ツールを使用すると、一定期間にわたって Chrome のすべてのアクティビティを表示するウィンドウが細かく表示されるため、最初は多大な粒度で表示されます。Chrome の関数の多くはトレース用に実装されているため、手動で計測しなくても、about:tracing を使用してパフォーマンスを追跡できます。(JS を手動で計測する方法については、後述のセクションをご覧ください)。

トレースビューを表示するには、Chrome のアドレスバーに「about:tracing」と入力します。

Chrome アドレスバー
Chrome のアドレスバーに「about:tracing」と入力する

トレースツールで記録を開始し、ゲームを数秒間実行した後、トレースデータを確認できます。以下はデータの例です。

シンプルなトレース結果
シンプルなトレース結果

紛らわしいですね。読み方についてお話ししましょう。

各行はプロファイリングされるプロセスを表し、左右の軸は時間、色付きのボックスはそれぞれインストゥルメント化された関数呼び出しを表します。さまざまな種類のリソースに対応する行があります。ゲームのプロファイリングで特に興味深いのは、グラフィック プロセッシング ユニット(GPU)の動作を示す CrGpuMain と CrRendererMain です。各トレースには、トレース期間中に開いた各タブの CrRendererMain 行が含まれています([about:tracing] タブ自体を含む)。

トレースデータを読み取る際の最初のタスクは、ゲームに対応する CrRendererMain 行を特定することです。

シンプルなトレース結果がハイライト表示されている
シンプルなトレース結果がハイライト表示されている

この例では、2 つの候補は 2216 と 6516 です。残念ながら現時点では、定期的に更新を頻繁に行っている行(またはトレース ポイントを使用してコードを手動で計測した場合はトレースデータを含む行を探す)以外は、アプリケーションを見つける洗練された方法はありません。この例では、更新の頻度から 6516 がメインループを実行しているようです。トレースを開始する前に他のすべてのタブを閉じると、正しい CrRendererMain を簡単に見つけることができます。ただし、ゲーム以外のプロセス用の CrRendererMain 行が残っている場合があります。

フレームを探しています

ゲームのトレースツールで正しい行を見つけたら、次のステップはメインループを見つけることです。メインループは、トレースデータの繰り返しパターンのように見えます。W、A、S、D キーを使用してトレースデータを移動できます。A と D で左右(時間で前後)に移動でき、W と S でデータを拡大および縮小できます。ゲームが 60 Hz で実行されている場合、メインループは 16 ミリ秒ごとに繰り返されるパターンであることが想定されます。

3 つの実行フレームのように見える
3 つの実行フレームのように見える

ゲームのハートビートを特定したら、各フレームでコードの動作を詳しく調べることができます。ファンクション ボックスのテキストが読み取れるまで、W、A、S、D キーを使用してズームインします。

実行フレームを深く掘り下げる
実行フレームの詳細

この箱のコレクションは、一連の関数呼び出しを示しており、各呼び出しは色付きのボックスで表されています。各関数はその上のボックスによって呼び出されています。この例では、MessageLoop::RunTask が RenderWidget::OnSwapBuffersComplete と呼び出し、RenderWidget::DoDeferredUpdate などを呼び出します。このデータを読み取ると、各実行が何の呼び出しで、何にかかったかについて、全体像を把握できます。

ただし、ここで問題は少々頭が痛いところです。about:tracing によって公開される情報は、Chrome のソースコードからの未加工の関数呼び出しです。名前から各関数が何を行うかについて知識に基づいて推測することはできますが、情報は正確にはユーザー フレンドリーではありません。フレームの全体的な流れを見るのは便利ですが、何が起きているのかを実際に理解するには、もう少し人間が読み取れる内容が必要です。

トレースタグの追加

幸い、console.timeconsole.timeEnd という簡単な方法で、コードに手動インストルメンテーションを追加してトレースデータを作成できます。

console.time("update");
update();
console.timeEnd("update");
console.time("render");
update();
console.timeEnd("render");

上記のコードでは、指定されたタグを持つ新しいボックスがトレースビュー名に作成されるため、アプリを再実行すると、各タグの開始呼び出しと終了呼び出し間の経過時間を示す「update」ボックスと「render」ボックスが表示されます。

手動で追加されたタグ
手動で追加したタグ

これを使用して、人が読める形式のトレースデータを作成し、コード内のホットスポットを追跡できます。

GPU か CPU か

ハードウェア アクセラレーションによるグラフィックの場合、プロファイリング中に確認すべき最も重要な質問の 1 つは、「このコードは GPU を使用するのか、CPU の制約を受けるのか」ということです。フレームごとに、GPU でレンダリング処理を行い、CPU でロジックを実行します。ゲームが遅くなる原因を把握するためには、2 つのリソース間で処理がどのようにバランスされているかを確認する必要があります。

まず、トレースビューで CrGPUMain という行を見つけます。これは、特定の時間に GPU がビジー状態かどうかを示しています。

GPU と CPU のトレース

ゲームのすべてのフレームが CrRendererMain と GPU で CPU 処理を発生させていることがわかります。上記のトレースは、16 ms のフレームのほとんどで CPU と GPU の両方がアイドル状態である非常にシンプルなユースケースを示しています。

トレースビューは、実行速度が遅いゲームがあり、どのリソースを使い果たしているのか不明な場合に役立ちます。GPU と CPU の関係性を確認することが、デバッグの鍵となります。前と同じ例を使用しますが、更新のループに作業を追加します。

console.time("update");
doExtraWork();
update(Math.min(50, now - time));
console.timeEnd("update");

console.time("render");
render();
console.timeEnd("render");

次のようなトレースが表示されます。

GPU と CPU のトレース

このトレースから何がわかりますか?写真のフレームは約 2270 ms から 2320 ms になることがわかります。これは、各フレームに約 50 ms(フレームレート 20 Hz)かかることを意味します。更新ボックスの隣には、レンダリング関数を表す色付きのボックスのスライバーがありますが、フレームは更新自体が占めています。

CPU で行われていることとは対照的に、ほとんどのフレームで GPU がアイドル状態であることがわかります。このコードを最適化するには、シェーダー コードで実行できる処理を探して GPU に移動し、リソースを最大限に活用するようにします。

シェーダーのコード自体が遅く、GPU が過負荷になっている場合はどうでしょうか。CPU から不要な処理を削除し、代わりにフラグメント シェーダーのコードに処理を追加したらどうなるでしょうか。必要以上に高価なフラグメント シェーダーを次に示します。

#ifdef GL_ES
precision highp float;
#endif
void main(void) {
  for(int i=0; i<9999; i++) {
    gl_FragColor = vec4(1.0, 0, 0, 1.0);
  }
}

このシェーダーを使用しているコードのトレースはどのようなものでしょうか。

遅い GPU コードを使用した場合の GPU と CPU のトレース
遅い GPU コードを使用した場合の GPU と CPU のトレース

ここでも、フレームの長さに注意してください。ここで、繰り返しパターンは約 2750ms から 2950ms であり、継続時間は 200ms(約 5Hz のフレームレート)である。CrRendererMain 行はほぼ完全に空です。つまり、GPU が過負荷であるのに対し、CPU はほとんどの時間アイドル状態です。これは明らかに、シェーダーが重すぎることを示しています。

フレームレートの低下の原因を正確に把握できていない場合は、5 Hz のアップデートを確認して、ゲームコードを調べてゲームロジックの最適化や削除を試みたくなるかもしれません。この場合、ゲームループのロジックが時間を浪費しているわけではないため、まったく意味がありません。実際、このトレースは、各フレームでより多くの CPU 処理を実行することは、CPU がアイドル状態のままになるため、基本的に「無料」であることを示しています。処理を増やしてもフレームの所要時間に影響しません。

実例

では、実際のゲームのトレースデータがどのようなものかを見てみましょう。オープンウェブ テクノロジーで構築されたゲームの優れた点の 1 つは、お気に入りの製品で何が起こっているのかを確認できることです。プロファイリング ツールをテストする場合は、Chrome ウェブストアからお好みの WebGL タイトルを選択し、about:tracing を使用してプロファイリングします。これは、優れた WebGL ゲーム Skid Racer から取得したトレースの例です。

実際のゲームをトレース
実際のゲームのトレース

各フレームにかかる時間は約 20 ミリ秒で、フレームレートは約 50 FPS になっています。CPU と GPU の間で作業のバランスがとれていますが、GPU が最も需要の高いリソースであることがわかります。WebGL ゲームの実際の例をプロファイリングするには、次のような WebGL で構築された Chrome ウェブストアのタイトルをいくつか試してみてください。

まとめ

ゲームを 60 Hz で実行する場合、すべての処理をフレームごとに 16 ミリ秒の CPU と 16 ミリ秒の GPU 時間に収まる必要があります。並行して使用できる 2 つのリソースがあり、それらの間で作業をシフトしてパフォーマンスを最大化できます。Chrome の about:tracing ビューは、コードの実際の動作に関する分析情報を得るのに不可欠なツールです。適切な問題に対処して開発時間を最大限に活用するのに役立ちます。

次のステップ

GPU だけでなく、Chrome ランタイムの他の部分もトレースできます。Chrome の初期段階バージョンである Chrome Canary は、IO、IndexedDB、その他のいくつかのアクティビティをトレースする機能を備えています。トレース イベントの現状について詳しくは、こちらの Chromium 記事をご覧ください。

ウェブゲーム デベロッパーの方は、以下の動画をご覧ください。GDC 2012 で Google のゲーム デベロッパー アドボケイト チームが行った、Chrome ゲームのパフォーマンス最適化についてのプレゼンテーションです。