システム トレースを設定すると、アプリの CPU とスレッドのプロファイルを短時間でキャプチャできます。設定後にシステム トレースの出力レポートを使用すれば、ゲームのパフォーマンスを改善できます。
ゲームベースのシステム トレースをセットアップする
Systrace ツールは次の 2 つの方法で使用できます。
Systrace はローレベルのツールで、次のようなメリットがあります。
- グラウンド トゥルースを入手できます。Systrace は直接カーネルから出力をキャプチャするため、キャプチャされる指標は、各種システム呼び出しがレポートする指標とほぼ同じになります。
- リソースをほとんど消費しません。Systrace は、データをインメモリ バッファにストリーミングするため、デバイスに対するオーバーヘッドを大幅に抑えることができます(通常は 1% 未満)。
最適な設定
ツールに対して適切な引数セットを指定することが重要です。
- カテゴリ: ゲームベースのシステム トレースを有効にするための最良のカテゴリセット: {
sched
、freq
、idle
、am
、wm
、gfx
、view
、sync
、binder_driver
、hal
、dalvik
} バッファサイズ: 一般的に、CPU コアあたり 10 MB のバッファサイズを設定すると、約 20 秒のトレースが可能になります。たとえば、デバイスに 2 つのクアッドコア CPU(合計 8 コア)がある場合、
systrace
プログラムに渡す適切な値は 80,000 KB(80 MB)です。ただし、大量のコンテキスト切り替えを実行するゲームの場合は、バッファサイズを CPU コアあたり 15 MB に増やします。
カスタム イベント: ゲーム内でキャプチャするカスタム イベントを定義する場合、
-a
フラグを有効にします。これにより、Systrace はカスタム イベントを出力レポートに含めることができます。
systrace
コマンドライン プログラムを使用する場合は、次のコマンドを使用して、カテゴリセット、バッファサイズ、カスタム イベントのベスト プラクティスを適用するシステム トレースをキャプチャします。
python systrace.py -a com.example.myapp -b 80000 -o my_systrace_report.html \ sched freq idle am wm gfx view sync binder_driver hal dalvik
オンデバイスで Systrace システムアプリを使用する場合は、次の手順を行って、カテゴリセット、バッファサイズ、カスタム イベントのベスト プラクティスを適用するシステム トレースをキャプチャします。
[Trace debuggable applications] オプションを有効にします。
ただし、この設定を使用するには、デバイスの CPU が 4 コアの場合は 256 MB、8 コアの場合は 512 MB のメモリが必要であり、かつ、64 MB の各メモリ部分は隣接したチャンクとして使用できる必要があります。
[Categories] を選択して、以下のリストのカテゴリを有効にします。
am
: アクティビティ マネージャーbinder_driver
: バインダ カーネル ドライバdalvik
: Dalvik VMfreq
: CPU 周波数gfx
: グラフィックhal
: ハードウェア モジュールidle
: CPU アイドルsched
: CPU スケジューリングsync
: 同期view
: ビューシステムwm
: ウィンドウ マネージャー
[Record tracing] を有効にします。
ゲームを読み込みます。
デバイス パフォーマンスの測定対象となるゲームプレイに相当するインタラクションをゲーム内で実行します。
望ましくない動作がゲーム内で発生した直後に、システム トレースをオフにします。
これで、問題を詳細に分析するうえで必要となるパフォーマンス統計情報をキャプチャできます。
ディスク領域を節約するため、オンデバイスのシステム トレースでは、圧縮トレース形式(*.ctrace
)でファイルを保存します。レポートを生成する際にこのファイルを解凍するには、コマンドライン プログラムを使用して、--from-file
オプションを指定します。
python systrace.py --from-file=/data/local/traces/my_game_trace.ctrace \ -o my_systrace_report.html
特定のパフォーマンス領域を改善する
このセクションでは、モバイルゲームのパフォーマンスに関してよくあるいくつかの問題に焦点を当てて、問題の原因となるゲームの側面を検出し、改善する方法について説明します。
読み込み速度
プレーヤーはできる限り迅速にゲームのアクションを開始したいと思っているため、ゲームの読み込み時間を可能な限り改善する必要があります。一般的に、読み込み時間を改善するうえで次のような対策が役立ちます。
- 遅延読み込みを行う。ゲーム内の連続したシーンやレベルで同じアセットを使用する場合、アセットを一度だけ読み込むようにします。
- アセットのサイズを縮小する。これにより、非圧縮バージョンのアセットをゲームの APK にバンドルできるようになります。
- ディスク効率に優れた圧縮方法を使用する。たとえば、zlib などが該当します。
- IL2CPP を使用してください。 します。(Unity を使用している場合)。IL2CPP は、C# スクリプトの実行パフォーマンスを高めます。
- ゲームをマルチスレッド化する。詳細については、フレームレートの一貫性をご覧ください。
フレームレートの一貫性
ゲームプレイ エクスペリエンスの重要な要素として、一貫したフレームレートを実現する必要があります。このセクションで説明している最適化手法に沿って設定すると、この目標を容易に実現できます。
マルチスレッド処理
複数のプラットフォーム用に開発する場合、通常は、ゲーム内のすべてのアクティビティを単一のスレッド内に配置します。この実行方法はさまざまなゲームエンジンで簡単に実装できますが、Android デバイス上で実行する方法としては適切ではありません。シングルスレッド ゲームでは読み込みが遅くなり、フレームレートの一貫性を確保できないことがよくあります。
一度に 1 つの CPU 上だけで稼働するゲームの典型的な動作を示す Systrace レポートを図 1 に示します。
ゲームのパフォーマンスを改善するには、ゲームをマルチスレッド処理にします。通常は、スレッドを 2 つにすると最適なモデルになります。
- ゲームスレッド - ゲームのメイン モジュールを格納し、レンダリング コマンドを送信します。
- レンダリング スレッド - レンダリング コマンドを受信し、デバイスの GPU がシーンを表示する際に使用するグラフィック コマンドに変換します。
Vulkan API は、2 つの共通バッファを並列プッシュする機能を備えているため、このモデルを拡張できます。この機能を使用すると、複数のレンダリング スレッドを複数の CPU に分散できるため、シーンのレンダリング時間をさらに改善できます。
また、エンジン固有の変更を加えることで、ゲームのマルチスレッド パフォーマンスを強化することもできます。
- Unity ゲームエンジンを使用してゲームを開発している場合は、[Multithreaded Rendering] オプションと [GPU Skinning] オプションを有効にします。
- カスタム レンダリング エンジンを使用している場合は、レンダリング コマンド パイプラインとグラフィック コマンド パイプラインが正しく配置されているか確認してください。正しく配置されていないと、ゲームのシーンの表示に遅延が生じる可能性があります。
この変更を適用すると、1 つのゲームが少なくとも 2 つの CPU を同時に占有するようになります(図 2 を参照)。
UI 要素の読み込み
機能が豊富なゲームを作成する場合、プレーヤーに向けてさまざまなオプションやアクションを同時に表示する方が魅力が増します。ただし、一貫したフレームレートを維持するには、比較的小さなサイズのモバイル ディスプレイを考慮し、UI をできる限りシンプルにする必要があります。
モバイル デバイスの機能を基準とした場合にあまりに多くの要素をレンダリングしようとしている UI フレームの Systrace レポートの例を図 3 に示します。
適切な目標として、UI の更新時間を 2~3 ミリ秒にまで短縮することをおすすめします。このような迅速な更新を実現するには、以下のような最適化を実施します。
- 画面上で移動した要素だけを更新します。
- UI のテクスチャとレイヤの数を制限します。1 つのマテリアルを複数のグラフィック呼び出し(シェーダーやテクスチャなど)が使用する場合、グラフィック呼び出しを結合できないか検討してください。
- 要素のアニメーション処理を GPU に任せます。
- フラスタム カリングとオクルージョン カリングを積極的に行います。
- 可能であれば、Vulkan API を使用して描画処理を行います。Vulkan により、描画呼び出しのオーバーヘッドを低減できます。
電力消費
前のセクションで説明した最適化を行ったとしても、ゲームプレイの最初の 45~50 分以内にゲームのフレームレートが低下する場合があります。また、時間の経過とともにデバイスが熱くなり、バッテリーの消費量が増大することがあります。
この高温と電力消費という望ましくない組み合わせは、多くの場合、ゲームのワークロードをデバイスの CPU 間に分散する方法に起因しています。ゲームの電力消費効率を高めるには、以下のセクションに示すベスト プラクティスを適用してください。
メモリを大量に使用するスレッドを 1 つの CPU 上で保持する
多くのモバイル デバイスでは、L1 キャッシュは特定の CPU 上に配置され、L2 キャッシュはクロックを共有する CPU のセット上に配置されます。L1 キャッシュ ヒットを最大化するには、一般的に、ゲームのメインスレッドと、メモリを大量に使用する他のスレッドを、一緒に単一の CPU 上で実行し続けることをおすすめします。
短時間で終わる処理を低消費電力の CPU に任せる
Unity を含め、ほとんどのゲームエンジンが、ワーカー スレッド処理をゲームのメインスレッドとは異なる CPU に任せるようになっています。ただし、エンジンは、デバイスの具体的なアーキテクチャを認識しないため、デベロッパーと同じレベルでゲームのワークロードを予測することはできません。
ほとんどのシステム オン チップ デバイスは、少なくとも 2 つの共有クロックを備えています。1 つはデバイスの高速 CPU 用で、もう 1 つはデバイスの低速 CPU 用です。このようなアーキテクチャの結果、高速 CPU のうちの 1 つが最大速度で動作する必要がある場合に、他の高速 CPU もすべて最大速度で動作するようになります。
高速 CPU を活用しているゲームのレポート例を図 4 に示します。このようにアクティビティ レベルが高いと、すぐに大量の電力が消費され、高温になります。
全体的な電力使用量を削減するには、オーディオの読み込みや、ワーカー スレッドの実行、コリオグラファの実行といった短時間の処理を、デバイスの低速 CPU のセットに任せるよう、スケジューラに指示することをおすすめします。必要なフレームレートを維持しつつ、短時間で終わる処理をできる限り低速 CPU に移行してください。
ほとんどのデバイスは、高速 CPU の前に低速 CPU をリストアップします。ただし、「デバイスの SOC は常にこの順序を使用する」と想定することはできません。これを確認するには、次のようなコマンドを実行します。 こちらの CPU トポロジ調査 コード ご覧ください。
どの CPU がデバイスの低速 CPU であるのか判明したら、短時間スレッドのアフィニティを宣言できます。デバイスのスケジューラは、このアフィニティに従います。アフィニティを宣言するには、各スレッド内に次のコードを追加します。
#include <sched.h> #include <sys/types.h> #include <unistd.h> pid_t my_pid; // PID of the process containing your thread. // Assumes that cpu0, cpu1, cpu2, and cpu3 are the "slow CPUs". cpu_set_t my_cpu_set; CPU_ZERO(&my_cpu_set); CPU_SET(0, &my_cpu_set); CPU_SET(1, &my_cpu_set); CPU_SET(2, &my_cpu_set); CPU_SET(3, &my_cpu_set); sched_setaffinity(my_pid, sizeof(cpu_set_t), &my_cpu_set);
熱応力
デバイスが熱くなると、CPU や GPU が正常に動作しなくなり、ゲームも予想外の影響を受けることがあります。複雑なグラフィック、大量の計算、持続的なネットワーク アクティビティが組み込まれたゲームは、問題が発生する可能性が高くなります。
Thermal API を使用してデバイスの温度変化を監視し、消費電力を抑えてデバイスの温度を下げる対策を講じることができます。デバイスから熱応力が報告されると、継続的なアクティビティを停止して消費電力を抑えます。たとえば、フレームレートやポリゴン テッセレーションを制限します。
まず、PowerManager
オブジェクトを宣言し、onCreate()
メソッドで初期化します。温度ステータス リスナーをオブジェクトに追加します。
Kotlin
class MainActivity : AppCompatActivity() { lateinit var powerManager: PowerManager override fun onCreate(savedInstanceState: Bundle?) { powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager powerManager.addThermalStatusListener(thermalListener) } }
Java
public class MainActivity extends AppCompatActivity { PowerManager powerManager; @Override protected void onCreate(Bundle savedInstanceState) { ... powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); powerManager.addThermalStatusListener(thermalListener); } }
リスナーがステータスの変化を検出したときに実行するアクションを定義します。ゲームで C / C++ を使用している場合は、onThermalStatusChanged()
の温度ステータス レベルにコードを追加し、JNI を使用してネイティブ ゲームコードを呼び出すか、ネイティブ Thermal API を使用します。
Kotlin
val thermalListener = object : PowerManager.OnThermalStatusChangedListener() { override fun onThermalStatusChanged(status: Int) { when (status) { PowerManager.THERMAL_STATUS_NONE -> { // No thermal status, so no action necessary } PowerManager.THERMAL_STATUS_LIGHT -> { // Add code to handle light thermal increase } PowerManager.THERMAL_STATUS_MODERATE -> { // Add code to handle moderate thermal increase } PowerManager.THERMAL_STATUS_SEVERE -> { // Add code to handle severe thermal increase } PowerManager.THERMAL_STATUS_CRITICAL -> { // Add code to handle critical thermal increase } PowerManager.THERMAL_STATUS_EMERGENCY -> { // Add code to handle emergency thermal increase } PowerManager.THERMAL_STATUS_SHUTDOWN -> { // Add code to handle immediate shutdown } } } }
Java
PowerManager.OnThermalStatusChangedListener thermalListener = new PowerManager.OnThermalStatusChangedListener () { @Override public void onThermalStatusChanged(int status) { switch (status) { case PowerManager.THERMAL_STATUS_NONE: // No thermal status, so no action necessary break; case PowerManager.THERMAL_STATUS_LIGHT: // Add code to handle light thermal increase break; case PowerManager.THERMAL_STATUS_MODERATE: // Add code to handle moderate thermal increase break; case PowerManager.THERMAL_STATUS_SEVERE: // Add code to handle severe thermal increase break; case PowerManager.THERMAL_STATUS_CRITICAL: // Add code to handle critical thermal increase break; case PowerManager.THERMAL_STATUS_EMERGENCY: // Add code to handle emergency thermal increase break; case PowerManager.THERMAL_STATUS_SHUTDOWN: // Add code to handle immediate shutdown break; } } };
タップとディスプレイ間のレイテンシ
可能な限り高速にフレームをレンダリングするゲームの場合、フレーム バッファに対して過剰な詰め込みが行われ、GPU バウンドの状況を引き起こします。この場合、CPU は GPU を待つ必要があります。そのため、プレーヤーが入力を行うタイミングと、その入力が画面上で効果を発揮するタイミングとの間で、明確に認識できる遅延が発生します。
ゲームのフレーム ペーシングを改善できるか判断するには、以下の手順を行います。
gfx
カテゴリとinput
カテゴリを含む Systrace レポートを生成します。このカテゴリには、タップとディスプレイ間のレイテンシを判断するうえで特に有用な測定データが含まれます。Systrace レポートの
SurfaceView
セクションをチェックします。バッファが詰め込みすぎの状態になっていると、保留中のバッファの描画回数が 1~2 の間で周期変動するようになります(図 5 を参照)。図 5. 周期的に容量を超えて描画コマンドを受け入れることができない過剰な詰め込み状態のバッファを示す Systrace レポート
このようにフレーム ペーシングが一貫していない状況を緩和するには、以下のセクションで説明する対策を行います。
Android Frame Pacing API をゲームに統合する
Android Frame Pacing API を使用すると、フレーム スワップを行い、スワップ間隔を定義することで、ゲーム内のフレームレートの一貫性を維持できるようになります。
ゲーム内の非 UI アセットの解像度を下げる
最新のモバイル デバイスのディスプレイには、プレーヤーが処理できるピクセルよりも多くのピクセルが含まれているため、ダウンサンプリングして、5 ピクセル単位や 10 ピクセル単位で単色化して実行しても大きな問題にはなりません。大半のディスプレイ キャッシュの構造を踏まえ、1 次元に限り解像度を下げることをおすすめします。
ただし、ゲームの UI 要素の解像度は下げないようにしてください。すべてのプレーヤーに対して十分な大きさのタップ ターゲット サイズを維持するため、UI 要素の線幅を保持する必要があります。
スムーズなレンダリング
SurfaceFlinger がディスプレイ バッファをラッチしてゲーム内のシーンを表示すると、CPU アクティビティが瞬間的に増加します。このような CPU アクティビティの急増が不均一に発生した場合、ゲーム内でスタッタリングが発生することがあります。この状況が発生する理由を図 6 に示します。
フレームの描画開始が数ミリ秒遅いだけで、次の表示ウィンドウを逃すことがあります。フレームを表示するには次の Vsync まで待つ必要があり(30 FPS でゲームを実行している場合は 33 ミリ秒)、これにより、プレーヤーにとって明確に認識できる遅延が発生します。
この状況に対処するには、VSync ウェーブフロントに対して新しいフレームを常に提示する Android Frame Pacing API を使用します。
メモリの状態
ゲームを長時間実行すると、デバイスのメモリ不足エラーが発生する可能性があります。
この状況が発生する場合、Systrace レポートで CPU アクティビティをチェックして、kswapd
デーモンの呼び出しがどれくらいの頻度で行われているのか確認します。ゲームの実行中にこの呼び出しが多数行われている場合は、ゲームがどのようにメモリを管理、クリーンアップしているのか詳細に調べることをおすすめします。
詳細については、ゲームでメモリを効率的に管理するをご覧ください。
スレッドの状態
Systrace レポートで通常の要素を表示しているときに、レポート内のスレッドを選択すると、そのスレッドを対象に各スレッド状態の経過時間を表示できます(図 7 を参照)。
図 7 を見ると、ゲームのスレッドが「実行中(Running)」状態や「実行可能(Runnable)」状態になっている時間が短いことがわかります。一般的に、スレッドが定期的に異常な状態に遷移する主な理由は次のとおりです。
- スレッドが長時間スリープしている場合、ロックの競合か GPU アクティビティの待機のいずれかが発生している可能性があります。
- 頻繁にスレッドの I/O がブロックされている場合、ディスクから一度に大量のデータを読み取っているか、ゲームにスラッシングが発生しています。
参考情報
ゲームのパフォーマンスを改善する方法については、以下のリソースをご覧ください。
動画
- Android Game Developer Summit 2018 のプレゼンテーション「Systrace for Games」