メモリ使用量をモニタリングする

ゲームのメモリ使用量を効果的に最適化するには、まず、Android プラットフォームがメモリを測定する方法と、システム テレメトリ、診断 API、プロファイリング ツールの使用方法を理解する必要があります。このガイドでは、新しいプラットフォーム ガイドラインに沿ってゲームのメモリ割り当てをモニタリング、キャプチャ、分析する方法について詳しく説明します。

RSS 指標とスワップ指標について

ゲームのメモリ動作を効果的に分析してデバッグするには、Android プラットフォームがメモリ管理に使用する正確な技術指標を理解する必要があります。このテレメトリー パラメータが実際にどのように処理され、モニタリングされるかについて詳しくは、Android Vitals - メモリ使用量(匿名 RSS + スワップ)のドキュメントをご覧ください。

1. 匿名 RSS(RssAnon)

Resident Set Size(RSS)は、デバイスの物理 RAM に保持されているプロセスが占有するメモリの部分を測定します。RSS は、ファイルバック メモリと匿名メモリに分割されます。Android のメモリ指標は、匿名 RSS に厳密に焦点を当てています。

  • 含まれるもの: ゲームプロセスによって直接割り当てられたメモリページ。ストレージ上の物理ファイルにリンクされていません。これらのページには、Java または Kotlin のヒープ、スレッド実行スタック、そして重要なネイティブ メモリ割り当て(カスタム C++ エンジン アロケータや、ネイティブ malloc または new を使用してリクエストされ、ゲームロジックによってダーティ化されたメモリブロックなど)が含まれます。この指標の詳細については、プロセスメモリ(RSS)の辞書をご覧ください。
  • 重要性: ゲームエンジンは、物理、レンダリング、ロジックを処理するために、大規模なネイティブ メモリプールを使用します。これらのプールはファイルにバックアップされないため、完全に匿名 RSS に存在し、ゲームの物理フットプリントの大部分を形成します。

2. 非圧縮スワップ(VmSwap)

Android は、フラッシュ ストレージの摩耗とレイテンシの制約により、従来のディスクベースのスワップ領域をサポートしていません。代わりに、zRAM(非圧縮スワップ)を使用します。

  • 内容: 物理 RAM の負荷が増加すると、カーネルのメモリ管理デーモンが、アクティブでない匿名ページを圧縮し、物理 RAM(zRAM)の専用の圧縮されていない部分に移動します。
  • 指標の計算: システムは、非圧縮サイズ(VmSwap)に基づいてこれを追跡し、ゲームの実際の物理メモリ需要を評価します。ゲームがメモリを割り当て、システムがそれを zRAM にスワップした場合でも、そのメモリはゲームの合計メモリ使用量にカウントされます。

3. プロセスの状態

メモリ使用量は、Android Vitals のプロセス状態別に分類されます。ゲーム デベロッパーの場合、サードパーティの SDK やゲームが、ユーザーが認識できるサービスやバックグラウンド サービスを予期せずトリガーすることもあります。

  • 含まれるもの: フォアグラウンド、知覚可能なサービス、バックグラウンド、キャッシュ。
  • 重要性: プロセスの状態によって、Android OS のメモリ管理に与える影響が異なります。サードパーティの SDK が意図せずバックグラウンド タスクをトリガーした場合、ゲームが機密性の高いプロセス状態で実行されていることに気づかない可能性があります。RunningAppProcessInfo を使用して、ゲームがバックグラウンドで実行されているかどうかをモニタリングします。

アプリケーション プログラミング インターフェース(API)

Android には、ゲームがメモリ不足に動的に対応し、実行時に詳細なメモリ診断をキャプチャできるようにするシステム API が用意されています。

メモリトリム イベントに応答する

システムは onTrimMemory を使用して、アプリがメモリ使用量を自発的に削減し、ローメモリ キラー(LMK)によって強制終了されるのを回避して、他のアプリが使用できるようにメモリを解放するのに適したタイミングで、ライフサイクル イベントをアプリに通知します。

システムがバックグラウンドでアプリを強制終了すると、ユーザーは再開時に遅いコールド スタートを経験することになります。バックグラウンドでのメモリ使用量を削減すると、こうしたバックグラウンドでの終了を防ぐことができます。

トリムイベントに応答する際は、すぐに必要とされない、再構築可能な大規模なメモリ割り当てを解放します。

  • 例: TRIM_MEMORY_UI_HIDDEN に応じて、キャッシュに保存されたビットマップ(ローカル ストレージからデコードされたもの)をトリミングまたはパージします。

Kotlin

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {
    override fun onTrimMemory(level: Int) {
        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }
        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

public class MainActivity extends AppCompatActivity implements ComponentCallbacks2 {
    @Override
    public void onTrimMemory(int level) {
        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }
        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

ProfilingManager

Android 15(API レベル 35)で導入された ProfilingManager API を使用すると、アプリは実行時にプログラムで定義されたスナップショット(ヒープ プロファイル、システム トレース、Java ヒープダンプなど)を直接キャプチャできます。

デベロッパーは、特定のシーンで手動でキャプチャをトリガーすることも、TRIGGER_TYPE_ANOMALY などの自動トリガーを登録して、ゲームプロセスがメモリリミッターのしきい値に違反したときに自動的にキャプチャをトリガーすることもできます。ただし、ゲーム デベロッパーは、最新のゲームエンジンの重要な制限を考慮する必要があります。

注: 最新のゲームエンジン(Unity や Unreal など)は、MAP_ANONYMOUS フラグを指定した mmap を使用して、カーネルから大規模な仮想メモリブロックを事前に割り当てることで、実行パフォーマンスを管理します。エンジンは、カスタムのサブアロケータ(Unity のネイティブ メモリ マネージャーや Unreal の BinnedAllocators など)を使用して、メモリブロックを内部で細分化して割り当てます。

ApplicationExitInfo

ゲームがバックグラウンドで終了した場合や、個々のプロセスのメモリ上限に違反したために強制終了された場合、標準の Java またはネイティブ コードでのクラッシュ ダンプ メカニズム(Firebase Crashlytics など)はイベントを登録しません。これらの終了をプログラムでクエリしてログに記録するには、ゲームの起動時に ApplicationExitInfo API を利用する必要があります。

  • 実装: 開始時に ActivityManager.getHistoricalProcessExitReasons() を呼び出して、最近のセッションの終了理由を取得します。
  • 主なメモリ終了理由:
    • REASON_LOW_MEMORY: プロセスがシステムのローメモリ キラー(LMK)によって終了されたことを示します。この終了は、デバイス全体のメモリ負荷が高く、OS が RAM を再利用する必要がある場合に発生します。この終了理由は、ゲームのバックグラウンド フットプリントが大きすぎて、他のアプリケーションと共存できないことを示しています。
    • REASON_MEMORY_LIMITER(Android 17 QPR2、API レベル 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2 以降): プラットフォームのメモリリミッターによって割り当てられた cgroup メモリ上限(RssAnon + VmSwap)を超えたため、プロセスが強制終了されたことを示します。この終了は、デバイスに十分な物理メモリが残っている場合でも発生することがあります。以前の Android 17 ビルドでは、getReason() は getDescription() の "MemoryLimiter:AnonSwap" で REASON_OTHER を返します。

利用可能なツールを使用する

開発時と QA 時に次のプラットフォーム ツールを使用して、ゲームのメモリ使用量を正確に測定します。

meminfo

このツールは、メモリの統計情報を収集して、割り当てられた PSS メモリの容量とメモリが使用されたカテゴリを表示します。

次のいずれかの方法で meminfo 統計情報を出力します。

  • adb shell dumpsys meminfo package-name コマンドを使用する。
  • Android Debug API から MemoryInfo 呼び出しを使用する。

PrivateDirty 統計情報は、ディスクにページングできず他のプロセスと共有されていない、プロセス内の RAM の容量を示します。プロセスが強制終了されると、この容量の大部分をシステムで利用できるようになります。

メモリ トレースポイント

メモリ トレースポイントは、ゲームが使用している RSS メモリの量を追跡します。RSS メモリ使用量の計算は、PSS 使用量の計算よりはるかに高速です。計算が高速であるため、RSS はメモリサイズの変化をより詳細に示し、ピーク時のメモリ使用量をより正確に測定します。そのため、ゲームのメモリ不足を引き起こす可能性のあるピークに気づきやすくなります。

perfetto

Perfetto は、デバイスのパフォーマンスとメモリの情報を収集し、ウェブベースの UI に表示するためのツールのスイートです。任意の長いトレースをサポートしているため、RSS の経時的な変化を確認できます。オフライン処理用に生成したデータに対して SQL クエリを発行することもできます。System Tracing アプリから長いトレースを有効にします。トレースに対して memory:Memory カテゴリが有効になっていることを確認します。開発とテストでカスタムメモリ計測を行う場合は、heapprofd API(ベータ版)を使用することもできます。

Perfetto で RssAnon とスワップを検査する

ゲームの匿名メモリと zRAM スワップの影響を確認するには、ui.perfetto.dev のウェブベースの UI にトレースファイルを読み込み、メモリの詳細なケーススタディ用に設計された分析手法に従います(詳細については、Perfetto メモリ分析のケーススタディをご覧ください)。

1. タイムラインでのメモリカウンタの可視化

  • プロセスを見つける: ナビゲーション リストで、ゲームのパッケージ名またはプロセス名を検索します。
  • トラック グループを展開する: プロセス行をクリックしてスレッド トラックを展開し、Memory という名前のサブグループを見つけます。
  • トラックを分析します。
    • mem.rss.anon(匿名 RSS): この折れ線グラフは、ゲームの管理対象外のメモリプールが占有している物理 RAM をリアルタイムで示します。シーンの読み込み、UI のポップアップ、ゲームプレイの遷移中にこのタイムラインをモニタリングして、割り当てのピークが高いかどうかを確認します。
    • mem.swap(圧縮スワップまたは VmSwap): このグラフは、zRAM に移動されたメモリブロックの圧縮前のサイズをプロットします。ゲームプレイと同時にスワップ アクティビティが頻繁に発生している場合は、メモリが制限されたデバイスでゲームが実行されており、システムがバックグラウンド アセットを積極的に圧縮していることを示します。

2. SQL クエリの実行(Trace Processor): 詳細なオフライン分析を行うには、Perfetto UI コンソール内で SQL クエリを直接実行するか、スタンドアロンの Trace Processor Python ライブラリを使用して統計的なピークを計算します。

  • ピーク時の匿名 RSS 割り当てを確認します。

    SELECT
      max(value) / 1024 / 1024 AS max_rss_anon_mb
    FROM counter
    JOIN counter_track ON counter.track_id = counter_track.id
    WHERE counter_track.name = 'mem.rss.anon'
      AND counter_track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      );
    
  • 任意のタイムスタンプで RssAnon と VmSwap を関連付けます。

    SELECT
      ts,
      track.name AS metric_type,
      value / 1024 / 1024 AS size_mb
    FROM counter
    JOIN counter_track track ON counter.track_id = track.id
    WHERE (track.name = 'mem.rss.anon' OR track.name = 'mem.swap')
      AND track.upid IN (
        SELECT upid FROM process WHERE name = 'your.game.package.name'
      )
    ORDER BY ts ASC;
    

Android Studio を使用してトレース ファイルを検証する方法について詳しくは、システム トレースを検証する: プロセスメモリ(RSS)をご覧ください。メモリ プロファイルのスクリプト作成の詳細については、ネイティブ割り当てを記録するをご覧ください。

heapprofd

heapprofd は、Perfetto の一部であるメモリ トラッキング ツールです。このツールは、malloc を使用してメモリが割り当てられた場所を示すことで、メモリリークの発見に役立ちます。heapprofd は Python スクリプトを使用して起動できます。このツールはオーバーヘッドが少ないため、他のツール(Malloc Debug など)のようにパフォーマンスに影響することはありません。

bugreport

bugreport は、ゲームがメモリ不足でクラッシュしたのかどうかを調べるロギングツールです。ツールから出力されるレポートは、logcat を使用するよりもはるかに詳細です。ゲームがメモリ不足でクラッシュしたのか、LMK によって強制終了されたのかが示されるため、メモリのデバッグに役立ちます。

詳細については、バグレポートのキャプチャと確認をご覧ください。

ゲームエンジン ツール

プラットフォーム レベルのログとシステム テレメトリーは、OS のしきい値とコンプライアンスを追跡するために不可欠ですが、ゲームエンジン固有のツールを使用すると、割り当てをゲーム オブジェクト、スクリプトの動作、アクティブなシーン階層に直接関連付けることができます。

Unity

Unity Engine 環境では、Unity のネイティブ プロファイリング ツールとクラスを使用して、実行時の Android 匿名 RSS + スワップ メモリ使用量を高い信頼性で正確に推定できます(通常、実際の OS レベルの値と比較して 10% 未満の差異が表示されます)。

構成ルールやランタイム スクリプトなど、詳細なチュートリアルについては、Unity ツールでメモリを確認する方法をご覧ください。

  • Unity Profiler API: コアエンジンの指標をクエリすることで、実行時にゲームのアンマネージド メモリ使用量をプログラムで概算できます。
    • Profiler クラスを使用する: Profiler.GetTotalReservedMemoryLong() と Profiler.GetMonoHeapSizeLong() の値を合計して、メモリ割り当ての合計を追跡します。
    • ProfilerRecorder クラスの使用: メモリのカテゴリを動的にモニタリングします。信頼できるベースライン近似を確立するには、Total Reserved Memory(リリースビルドの場合)を取得するか、そこから Gfx Reserved Memory(開発ビルドの場合)を差し引いて、ファイルベースのグラフィック メモリ コンポーネントを取り除きます。
  • Unity Memory Profiler: メモリリークをオフラインで特定してデバッグするには、メモリ スナップショットをキャプチャし、[All of Memory] セクションにある [Resident Memory on Device] グラフを調べます。おおよそのフットプリントを計算するには、Untracked、Android Runtime、Native、Managed の各カテゴリの合計を足します。
    • zRAM の制限: メモリが逼迫している場合、Android カーネルはアクティブでないメモリページをスワップ領域(zRAM)に圧縮できます。Unity Memory Profiler は OS レベルのスワップ パラメータを検出できないため、メモリ使用量の多いシーンでは、フットプリントにわずかな不一致が見られることがあります。Perfetto で推定値を相互参照して、正確な値を確認します。

Unreal

Unreal Engine 環境では、エンジンの診断とプラットフォームのテレメトリーを組み合わせることで、ゲームのメモリ使用量を評価できます。詳細な手順とプロファイリング ワークフローについては、Unreal Engine でメモリ使用量を確認するをご覧ください。

主な診断ツールとインターフェースは次のとおりです。

  • C++ Diagnostics API: 軽量のメモリクエリには GetMemoryUsedFast を使用し、ハードウェア レベルのメモリ統計情報には GetStats インターフェースを使用します。
  • コンソール コマンド: stat unit エンジン コマンドと stat unitmax エンジン コマンドを使用して、デバイス ハードウェアのリアルタイムのメモリ割り当て傾向をモニタリングします。
  • Unreal Insights: フレーム単位で正確なタイムライン キャプチャを検査して、プラットフォーム レベルの指標とカスタムメモリ カウンタを分析します。