記事一覧へ戻る

マージ前にゲームの性能を CI で比較する

リファクタリング後の機能テストが通っても、毎フレームの処理が増えていることはあります。 開発者が書いたコードでも、Claude Code や Codex が書いたコードでも同じです。 動作の正しさを確かめるテストだけでは、対象ハードウェアでの実行コストは分かりません。

性能の確認には、繰り返し実行できるシナリオと、実測した比較基準が必要です。 Framedash の perf-diff は二つのビルドのテレメトリを比較し、run-profile-test はテスト用コマンドの起動から比較までを実行します。 ゲームを動かすのは、開発者側の PC や CI ランナーです。 マージを止める判定に使う前に、何を測る比較なのか、変更していないビルドでどの程度ばらつくのかを確認します。

同じベースラインを繰り返し測る

まず、レベル内の決まった経路を移動するなど、毎回同じ手順で実行できるシナリオを一つ選びます。 ベースラインとなるビルドを測定し、コードを変えずにもう一度測定してから、変更後の候補ビルドを測ります。 次の条件は揃え、結果と一緒に記録してください。

  • ハードウェア、OS、グラフィックスドライバー、電源設定
  • エンジンと SDK のバージョン、ビルド構成、グラフィックス API
  • 解像度、品質設定、VSync、フレームレート上限
  • シナリオ、カメラの移動経路、ウォームアップ、測定時間、テレメトリ設定

変更前のビルドを繰り返すと、1 回の測定では分からないばらつきが見えます。 同じビルド同士でも予定したしきい値を超えるなら、CI を失敗させる前に実行環境や測定手順を見直します。 例にあるパーセンテージをそのまま使っても、実行ごとのばらつきを見積もったことにはなりません。

集計に混ざるデータにも注意が必要です。 perf-diff は対象期間内のテレメトリを build_id ごとに集計します(既定は過去 30 日)。 同じ ID を再利用した実行はまとめて集計され、ci.scenario を付けても、このコマンドが自動でシナリオを絞り込むわけではありません。 実行や構成を分けて比べる必要がある場合は、測定用のビルド ID を分け、元のコミットを ci.commit に残します。 ベースラインと候補の両方に、対象期間内の保持済みデータが必要です。

P50 による合否判定を理解する

perf-diff は、収集された指標の P50 と P95 を表示します。 P50 は中央値で、性能回帰の判定に使うのは P50 の変化率です。 P95 はサンプル値の上位側を見るための値であり、終了コードの判定には使いません。

CLI のインストールと設定が済んでいれば、次のコマンドでフレームタイムを判定できます。

framedash perf-diff \
  --baseline "$BASELINE_BUILD_ID" --candidate "$CANDIDATE_BUILD_ID" \
  --api-key-file ci-read.key --metric frame_time \
  --threshold 5 --fail-on-regression

二つの ID には SDK が実際に送った値を指定し、FRAMEDASH_PROJECT_ID には測定対象のプロジェクト ID を設定します。 非公開の ci-read.key ファイルには、analytics:read 権限のキーを保存します。 ここでの 5% は許容幅の例であり、推奨値ではありません。

正のベースラインに対して、フレームタイムの P50 が 10 ms から 10.5 ms になれば、増加率はちょうど 5% なので通過します。 10.6 ms なら 6% の増加で失敗です。 これらは判定の説明用の数値です。 レポートの isRegression は許容幅内の増加でも true になることがあり、CLI は別途 --threshold を適用します。 P95 だけが悪化しても通るため、一時的な引っかかりが問題になるシナリオでは上位側の値も確認します。

フラグと失敗理由は、次のように読み分けます。

  • --fail-on-regression がなければ、性能回帰があっても比較結果を表示するだけです。付けた場合は、増加率がしきい値を超えると終了コード 1 を返します。しきい値の既定値は 0% です。
  • --metric は合否判定に使う指標を選びます。レポートには他の指標も表示されます。省略すると比較可能な全指標を判定しますが、データのない指標はスキップします。フレームタイムで通過しても、GPU 時間まで確認できたとは限りません。
  • 選んだ範囲に比較可能な指標が一つもなければ、終了コード 1 を返します。これは測定された性能低下ではなく、比較に必要なデータの不足です。認証エラーや無効な ID などでも失敗するため、コードを修正する前に診断メッセージを読みます。

ほかに対応する指標は、memory、gpu_time、io.read_bytes、io.read_time_ms、io.read_ops、load_time_ms、mem.vram です。 各指標に対応するデータの収集が必要で、IO やマップロードの測定には SDK 側の設定も要ります。 判定上はいずれも増加を悪化として扱います。 IO とロード時間では、有効なベースライン値が 0 から正の値に増えると、しきい値にかかわらず失敗します。 0 を基準にした変化率は定義できないためです。

--map と --platform でデータを絞れますが、二つのマシンやシナリオの条件が揃っているかまでは確認しません。 また、--metric load_time_ms と --map は併用できません。 マップロードのイベントには、このフィルターで使う map_id がないためです。

CI からシナリオを実行する

繰り返し測定した結果を判断に使えるようになったら、ゲームの起動と比較を同じジョブに組み込みます。 次は Bash の例です。 ./ci/profile-game.sh は開発者が用意するスクリプトであり、Framedash の同梱ファイルではありません。 対象ビルドを起動し、シナリオを実行し、テレメトリの送信を終えてから、テスト結果に応じた終了コードを返すようにします。

framedash run-profile-test \
  --command "./ci/profile-game.sh" \
  --build-id "$CANDIDATE_BUILD_ID" --commit "$GITHUB_SHA" \
  --scenario plaza-loop --api-key-file ci-read.key \
  --baseline "$BASELINE_BUILD_ID" --metric frame_time \
  --threshold 5 --fail-on-regression

GITHUB_SHA は GitHub Actions が設定するコミットの値です。 別の CI では相当する値に置き換えてください。 ベースラインは事前に測定しておきます。 判定基準を調整する間は --fail-on-regression を外し、結果の確認だけに使えます。

run-profile-test は、値がある場合に FRAMEDASH_BUILD_ID、FRAMEDASH_GIT_BRANCH、FRAMEDASH_GIT_COMMIT、FRAMEDASH_TEST_SCENARIO を子プロセスへ渡します。 Unity と Godot C# では SDK を初期化した後、テストの入口で次のように読み取れます。

Framedash.TelemetrySDK.Instance.BeginAutomatedSessionFromEnvironment();
// Run the scenario, then flush and wait using your SDK's CI procedure.
Framedash.TelemetrySDK.Instance.EndAutomatedSession();

この抜粋はセッションへのタグ付けを示すもので、テスト実装全体ではありません。 UE5 では UFramedashSubsystem に対応するメソッドがあります。 セッションにはトップレベルの build_id と、ci.branch、ci.commit、ci.scenario の属性が設定されます。 セッションとランナーの設定は CI 統合プロファイリングの手順にあります。 セッションを終了しても、バッファ内のイベントがサーバーに届いたことにはなりません。 SDK ごとのフラッシュと待機の手順に従い、送信が終わるまでゲームを動かしておきます。

二つのプロセスでは、別々のキーを使います。 CLI は ci-read.key の analytics:read キーで読み取り、ゲームは events:write キーで送信します。 読み取り用キーを --api-key-file で渡せば、FRAMEDASH_API_KEY はゲームの初期化処理が読み取る送信用キーに使えます。 どちらも CI のシークレットや非公開ファイルで管理し、ソース管理やログに残さないでください。

描画性能を測るときは描画を有効にする

UE5 の -nullrhi は通常の描画を無効にします。 ヘッドレスでのロジックテストには使えても、描画時の負荷は再現できず、GPU の性能比較にも使えません。 描画性能を測るなら、対象 GPU で描画を有効にし、両ビルドの設定を揃えます。 ほかのエンジンで描画を無効にするモードも、同じように扱います。

測定が完了しているかを確認する

run-profile-test は、候補ビルドの性能イベント数が実行前より増えるのを待ちます。 再実行時に古いデータだけで待機を終えることは防げますが、新しい実行の全イベントが届いた保証にはなりません。 ゲームのプロセスが失敗したりタイムアウトしたりした場合は、性能回帰の判定に進まず失敗します。

既存の perf-diff ゲートには、統計上十分な最低サンプル数の判定はなく、先に挙げた測定条件のすべても検証するわけではありません。 テスト手順の中で、サンプル数とセッション数、シナリオの完了、SDK のログを確認します。 比較結果が出ても、サンプルが少なければリリース判断の根拠として十分とは限りません。

ここでのパーセンタイルは、取り込まれたテレメトリのサンプルを集計した値です。 描画された全フレームを表すとは限らず、定期的なハートビートやイベント時点のスナップショットでは、短い停止を取り逃すことがあります。 イベント頻度の変更によって集計対象の分布も変わります。 Unity で区間を限定したフレームごとの測定が必要なら、別機能の Unity 性能ラン比較パイロットで、ベースライン、変更なしの再実行、候補を run-diff で比較できます。 フレーム間隔の分布と測定の完了状態を確認する機能であり、ここで扱う P50 の性能回帰判定は行いません。 perf-diff に P95 の失敗条件が追加されるわけでもありません。

差分を調べ、修正後に同じ条件で測る

比較可能な結果がしきい値を超えたら、まず同じ条件で再実行します。 差が再現するなら対象の処理を絞り、エンジンのプロファイラーでキャプチャします。 フレームタイムやメモリの増加は症状を示しますが、それだけでは原因となった関数やアロケーションまでは分かりません。

登録済みのマップ ID と位置を付けたイベントを送っていれば、パフォーマンスヒートマップで負荷の高い場所を探せます。 候補ビルドで悪化した場所だと判断する前に、ビルドなどのフィルターを確認してください。 ビルド比較そのものにマップは必須ではありません。

Framedash の読み取り専用 MCP ツールを通じて、エージェントにデータの確認を手伝わせることもできます。 get_heatmap などの集計ツールには analytics:read が必要で、query による SQL の直接実行には追加で data:admin が必要です。 広い権限は、調査に直接クエリが必要な場合だけ付与します。 テレメトリを読み取れることと、原因の説明や修正が正しいことは別です。 プロファイラーで仮説を確かめ、変更内容をレビューし、修正後のビルドを同じシナリオで測定します。

コマンドのオプションは CLI リファレンスにあります。 Claude Code 向けプラグインには、MCP の設定と SDK 連携の手順がまとまっています。

claude plugin marketplace add crane-valley/framedash-claude-plugin
claude plugin install framedash@framedash

最初は、変更なしの実行でどの程度ばらつくかを把握できるシナリオを一つ用意します。 ベースライン、候補のレポート、測定条件、修正後の検証結果をプルリクエストにまとめて残せば、レビュアーがその性能変化を許容できるか判断する材料になります。

手元の実行環境で、変更前後の性能を比べる Unity、UE5、Godot (C#) 向けの SDK を利用できます。
無料で始める UnityUE5Godot (C#)