メインコンテンツまでスキップ

AppStatsによるプロファイリング

AppStatsは、app.statsを通じて読み取り専用のエンジン測定値と、書き込み可能なユーザーカウンターを公開します。オーバーレイを描画せずに、プロファイリングスクリプト、カスタムダッシュボード、自動比較に使用できます。MiniStatsは、アプリケーションの統計と独自のCPUタイマーを視覚的に表示します。

ここで説明する公開getterには、Engine 2.23以降が必要です。ApplicationとAppBaseの両方で使用できます。アプリケーションがstatsインスタンスを作成し、アクセスするたびに同じオブジェクトを返します。自分で作成したり置き換えたりする必要はありません。

統計の読み取り​

測定値が必要なときにgetterを読み取ります。

const stats = app.stats;
const updateMs = stats.cpuUpdateTime;
const renderMs = stats.cpuRenderTime;
const textureMiB = stats.vramTextureBytes / (1024 * 1024);

時間の単位はミリ秒、メモリサイズの単位はバイトです。getterは取得可能な最新の値を返し、MiniStatsの0.5秒平均ではありません。例外はfpsで、直近のおよそ1秒間の報告区間におけるフレーム数を返します。

statsを読み取るだけでは、GPUプロファイリングは有効にならず、スナップショットオブジェクトの確保や描画も行われません。CPU時間とカウンターの初期値はゼロ、GPU時間の初期値はundefinedです。

ユーザーカウンター​

app.stats.userは、アプリケーションが管理するMap<string, number>を返します。エンジンの測定値とともに独自の値を公開できます。

app.stats.user.set('activeEnemies', 12);
const activeEnemies = app.stats.user.get('activeEnemies');
app.stats.user.delete('activeEnemies');
// 不要になったユーザーカウンターをすべて削除します。
app.stats.user.clear();

Mapへの参照は読み取り専用ですが、エントリーは変更できます。アプリケーションごとに独立したMapがあり、すべてのビルドで利用できます。エンジンは値をリセットしません。値を加算する前にエントリーを初期化し、フレームごとの合計はframeupdateでリセットしてください。単位はアプリケーション側で定義します。オーバーレイに表示するには、MiniStatsにuser.activeEnemiesなどのパスを設定します。ユーザーカウンターを参照してください。

指標と対応ビルド​

測定対象getterビルド
フレーム周期frameTime、fpsすべて
ドローコールdrawCallCountすべて
CPU更新と描画cpuUpdateTime、cpuRenderTimeすべて
CPUシステムフェーズcpuSystemUpdateTime、cpuSystemPostUpdateTimeすべて
アニメーションと物理cpuAnimationTime、cpuPhysicsTimeすべて
GPUフレームgpuFrameTimeサポートされ、有効になっている場合はすべて
GPUリソースメモリGPUメモリのgetterすべて
プリミティブprimitiveCountdebugとprofilerのみ。releaseとminifiedではundefined

npmアプリケーションでプリミティブ数が必要な場合は、playcanvas/debugまたはplaycanvas/profilerを使用します。アプリケーションとその他のエンジンクラスは、選択したビルドから一貫してインポートしてください。CPU時間、ドローコール、メモリ、サポートされているGPU時間の取得には、releaseビルドから切り替える必要はありません。

drawCallCountは、前のフレームで送信された描画コマンドを数えます。primitiveCountは、すべてのパスで送信された三角形、線、点を数えます。プリミティブ数には、GPUのクリッピングとカリングより前の、インスタンスとCPUで作成されたmulti-drawコマンドが含まれます。これは描画パラメーターからの推定値です。GPUが生成する間接描画は対象外であり、インデックス付きストリップのprimitive-restartインデックスは調べません。GPUからのリードバックも行いません。各getterの正確な対象範囲はAPIリファレンスを参照してください。

時間測定の理解​

frameTimeは、ブラウザーのスケジューリングやエンジン外での時間を含む、アプリケーションのtick間隔です。タイムスケールやデルタ時間のクランプの影響を受けません。CPU時間とGPU時間の合計ではなく、16.67 ms付近の値は単に60 Hzの表示周期を反映している場合もあります。

CPU測定値の対象範囲はそれぞれ異なります。

  • cpuUpdateTimeには、コンポーネントシステム、アプリケーションのupdateリスナー、入力の更新が含まれます。グラフィックスデバイスの更新は含みません。
  • cpuRenderTimeには、prerender/postrenderリスナー、階層の同期、バッチ処理、描画コマンドの送信が含まれます。グラフィックスデバイスのframeStart/frameEnd処理やGPUの実行は含みません。描画をスキップした場合は最新の値を保持します。
  • cpuSystemUpdateTimeには、スクリプトのupdateコールバック、物理、そのフェーズに登録された他のシステムが含まれます。cpuSystemPostUpdateTimeにはスクリプトのpostUpdateコールバックが含まれます。どちらもcpuUpdateTimeの一部です。
  • cpuAnimationTimeは、アプリケーション更新内のAnimComponentSystem専用フェーズを測定します。従来のAnimationComponentSystemの処理はシステム更新フェーズに含まれます。
  • cpuPhysicsTimeは、通常システム更新内で実行される直近の物理ステップを測定し、同期と接触処理を含みます。手動で複数回ステップを実行しても累積されません。最初のステップの前、または物理のtimeScaleで一時停止している場合はゼロです。

これらのフェーズは重なったり、互いを内包したりします。合計を得るためにすべてのCPU getterを足し合わせないでください。MiniStatsのCPU全体タイマーは、より広いフレームイベントの範囲を使用するため、cpuUpdateTime + cpuRenderTimeと一致するとは限りません。

フレームカウンターは、次のアプリケーションtickの開始時に公開されます。CPU時間は各フェーズが終了した時点で更新されます。frameendリスナーで読み取ると、完了したCPUフェーズの測定値を取得できますが、公開されているドローコール数は前のフレームの値です。GPU結果はさらに遅れて到着します。statsオブジェクトは最新の測定値を保持するライブデータであり、単一フレームのアトミックなスナップショットではありません。

GPU時間測定の有効化​

GPUプロファイリングは既定で無効です。GPUサンプルを収集する前に有効にします。

const profiler = app.graphicsDevice.gpuProfiler;
if (profiler) {
profiler.enabled = true;
}

// Later, once frames have rendered and query results have arrived:
const gpuMs = app.stats.gpuFrameTime;
if (gpuMs !== undefined) {
// Use gpuMs as the most recently resolved GPU duration.
}
バックエンド必要な機能測定範囲
WebGL 2EXT_disjoint_timer_query_webgl2フレーム全体のタイマークエリによる経過時間。
WebGPUtimestamp-query最初のプロファイル対象パスの開始から最後のパスの終了まで。パス間の空白時間も含みます。

アダプターがWebGPUの機能を公開している場合、エンジンが自動的に要求します。時間測定が未対応でもプロファイラーオブジェクトは存在する場合があります。有効化できたことは、結果を取得できる証拠にはなりません。

gpuFrameTimeは、プロファイリングが無効または未対応の場合、有効な結果が届く前、コンテキストの消失などでタイミングが無効化された後にundefinedを返します。取得できない結果を解釈するときに、ゼロで置き換えないでください。有効な結果も数フレーム前の値である可能性があり、新しいクエリ結果が届くまで複数のCPUフレームで同じ値が返ることがあります。

これはGPUの経過時間であり、GPU使用率ではありません。パス区間は重なることがあるため、各パスの時間を加算するとフレーム全体の時間を過大評価する場合があります。パスの内訳にはWebGPUのMiniStatsを、より詳しい調査にはネイティブGPUプロファイラーを使用してください。

MiniStatsもGPUタイマーの作成時にプロファイリングを有効にします。GPU時間を必要とするツールがなくなったら、profiler.enabled = falseに設定してください。statsの読み取り、MiniStatsの非表示、オーバーレイの破棄では、デバイスのGPUプロファイラーは無効になりません。

GPUリソースメモリ​

vramTotalBytesは、次の推定値の合計です。

getterリソース
vramTextureBytesテクスチャ
vramVertexBufferBytes頂点バッファ
vramIndexBufferBytesインデックスバッファ
vramUniformBufferBytesユニフォームバッファ
vramStorageBufferBytesストレージバッファ

追跡対象のバッファが確保されていない場合、ユニフォームバッファやストレージバッファの値はゼロになることがあります。ストレージバッファを持たないバックエンドでは、そのメモリ量はゼロです。

推定値はアプリケーションのグラフィックスデバイスに属し、そのデバイスは複数のアプリケーションで共有される場合があります。物理GPUメモリ全体の使用量や容量を表すものではなく、追跡されないドライバーのオーバーヘッドやJavaScriptメモリは含みません。MiBで報告するには、1024 * 1024で割ります。

定期レポートの収集​

有用なベースラインを得るには、フレームサンプルを蓄積し、低い頻度でレポートを出力します。この例は、およそ500 msにわたるフレーム間隔、CPU時間、ドローコール数を平均し、フレーム間隔のピーク値も記録します。GPU時間、プリミティブ数、メモリには、最新の取得値であることが明示された名前を付けています。

const stats = app.stats;
const report = {
windowMs: 0,
samples: 0,
frameAvgMs: 0,
framePeakMs: 0,
cpuUpdateAvgMs: 0,
cpuRenderAvgMs: 0,
drawCallsAvg: 0,
gpuLatestMs: null,
primitivesLatest: null,
vramLatestMiB: 0
};
let elapsed = 0;
let samples = 0;
let framePeak = 0;
let updateTotal = 0;
let renderTotal = 0;
let drawCallsTotal = 0;

function sampleStats() {
elapsed += stats.frameTime;
samples++;
framePeak = Math.max(framePeak, stats.frameTime);
updateTotal += stats.cpuUpdateTime;
renderTotal += stats.cpuRenderTime;
drawCallsTotal += stats.drawCallCount;

if (elapsed < 500) return;

report.windowMs = elapsed;
report.samples = samples;
report.frameAvgMs = elapsed / samples;
report.framePeakMs = framePeak;
report.cpuUpdateAvgMs = updateTotal / samples;
report.cpuRenderAvgMs = renderTotal / samples;
report.drawCallsAvg = drawCallsTotal / samples;
report.gpuLatestMs = stats.gpuFrameTime ?? null;
report.primitivesLatest = stats.primitiveCount ?? null;
report.vramLatestMiB = stats.vramTotalBytes / (1024 * 1024);

// Serialize only when publishing; console output is optional instrumentation.
console.log(JSON.stringify(report));

elapsed = 0;
samples = 0;
framePeak = 0;
updateTotal = 0;
renderTotal = 0;
drawCallsTotal = 0;
}

app.on('frameend', sampleStats);

// When profiling is finished:
// app.off('frameend', sampleStats);

サンプリング処理は状態を再利用し、フレームごとに配列やオブジェクトを作成しません。シリアライズは出力時のみ行います。取得できないGPUまたはプリミティブの測定値はnullで報告されます。同じ取得済みGPUフレームを繰り返し読み取る場合があるため、この例ではGPU値を平均しません。

ダッシュボードやエージェントに統合する場合は、コンソール出力を独自のレポート処理に置き換えてください。レポートオブジェクトは再利用されるため、受け手が保持する場合は出力時にコピーまたはシリアライズします。ログ、オーバーレイの描画、GPUクエリは測定値に影響するため、各実行で同じ計測条件を維持してください。

変更前後の比較​

  1. 再現可能なカメラ経路や操作と、目標フレーム予算を決めます。両方の実行で同じデバイス、バックエンド、解像度、品質設定、エンジンビルドを使用します。
  2. 定常状態のサンプルを収集する前に、アセットの読み込みとシェーダーのコンパイルを完了させます。起動時の処理は、Editor Profilerやブラウザーのパフォーマンスツールで別途測定します。
  3. アプリケーションを表示したまま、同じシナリオを一定時間繰り返します。バックグラウンドでのスロットリングはフレーム間隔を変化させます。
  4. ベースラインのレポートを保存し、1つ変更してから再実行します。単一フレームやFPSだけではなく、平均値、ピーク値、メモリの傾向を比較します。
  5. CPUフェーズの時間が長い場合はブラウザーのパフォーマンスツールを、GPUコストにはMiniStatsのパス時間やネイティブプロファイラーを使用します。メモリが増え続ける場合はリソースのライフサイクルを確認します。
AI

シナリオ、環境、測定区間、ベースラインのレポートをエージェントに渡します。ボトルネックの候補を特定し、1つの変更を提案し、同じ計測を再実行して差を報告するよう依頼してください。内部のstatsフィールドではなく公開getterを使用し、取得できない値をゼロとして扱わず、そのまま保持します。プロジェクトのセットアップとブラウザーでの検証については、AIを活用した開発を参照してください。