Myaku のしくみ
フレーム
Myaku は requestAnimationFrame をラップします。新しいタイムスタンプを持つ最初のコールバックがフレームの始まりです。フレームのインターバルは、その開始から次のフレームの開始までで、プレイヤーが目にする間隔そのものです。CPU 時間は、そのフレームのコールバックが動いていた時間です。
インターバルがフレームの CPU 時間より短くなることはありません。コールバックが 30 ms かかったあとでも、Chrome は 16.7 ms の格子にぴったり戻ったタイムスタンプで次のフレームを渡すことがあり、タイムスタンプだけ見ると間に合ったことになってしまいます。Long Animation Frames は 50 ms を超えるフレームしか報告しないので、これも見逃します。でも、プレイヤーがいちばんよく感じるのはこういうヒッチなのです。
GPU の仕事
Myaku は canvas と OffscreenCanvas の getContext をラップします。WebGL コンテキストでは、draw* の呼び出し、テクスチャとバッファのアップロード(バイト数つき)、シェーダーのコンパイルとプログラムのリンクを数えます。2D コンテキストでは描画呼び出しを数えます。Myaku の開始前に作られたコンテキストは数えられないので、Myaku は最初に読み込む必要があります。
原因の特定
Chromium は Long Animation Frames を報告します。50 ms を超えるフレームについて、どのスクリプトがどのファイルのどの関数から動いたか、スタイルとレイアウトにどれだけかかったかがわかります。Myaku はこのエントリーを保持し、時刻でフレームと突き合わせます。
フレームコールバックを呼ぶのは Myaku のラッパーなので、ブラウザはそれを Myaku のせいにします。そこで Myaku は、コールバックの初回登録時に登録元を記録し(コールバックごとにスタックを 1 回取得してキャッシュ)、判定ではそちらを指します。
エンジンのスパン
エンジンは自分から名乗り出ている場所で見つけるので、ゲームに手を入れる必要はありません。PixiJS と three.js の devtools フック、Excalibur の devtools グローバル、Phaser と KAPLAY のグローバルです。PixiJS はティッカーができる前にフックを呼ぶことがあるので、Myaku は init() が終わってから接続を完了します。ブラウザは performance.now() を丸めるため、軽いスパンは 0 ms と測られることがよくあります。それでもレポートには出るので、接続されなかったエンジンと速いエンジンは見分けられます。スパンも参照してください。
ガベージコレクション
Chromium では performance.memory でヒープサイズがわかります。ゲームのコードが暇だったのにフレーム間でヒープが 1 MB 以上減っていたら、GC が走った強いサインです。
コスト
フレームは固定サイズの型付き配列に保存されるので、フレームの記録でメモリ確保は起きず、Myaku 自身が探している GC を引き起こすことはありません。Myaku 自身のフレームあたりの時間は計測され、overheadMs として報告されます。判定は求められたときにだけ計算されます。
最初の 10 フレーム(スクリプトのコンパイル、アセットのデコード)はウォームアップとして扱い、プレイには数えません。
Myaku に見えないもの
- GPU 時間。 GPU が律速のフレームは
unknownになり、ドローコールが多いときはヒントが出ます。 - エンジンのループの内側。 スパンがなければ見えません。
- Chromium 以外でのスクリプトの位置。 ほかのブラウザでもフレーム時間、カウント、スパンは取れます。
- Worker。 プロファイルするのはメインスレッドだけです。
設計は ADR 0011 に記録されています。