システムメモリの予算

Android 17 以降では、プラットフォームは、一般的なアプリケーションのメモリ制限と連携する 2 つの補完的な Linux cgroup v2 メカニズムを使用して、システム プロセスのメモリを管理します。

  • システム デーモン保護の下限(memory.min を含む SystemMemoryBudgetManager): cgroup v2 memory.min 予約のセルフチューニング(各デーモンの固定された常駐ワーキング セットの 90% に設定され、物理 RAM の合計の 10% を上限とする)により、メモリ不足時に重要なインタラクティブ デーモンとシェルプロセス(system_server、surfaceflinger、グラフィック composer HAL、audioserver、cameraserver、netd、SystemUI、ホーム ランチャー)を保護し、静的な PinnerService mlock ファイルの固定を置き換えます。
  • システムアプリのメモリ上限(memory.high を含む <memory-budget>): 永続的なバックグラウンド システムアプリ(Bluetooth、Nfc、CalendarProvider、Traceur、MediaProvider、:PhotoPicker、NetworkStack、AppSearch、TeleService)のマニフェスト ファイルで宣言された、ワークロード調整済みの cgroup v2 memory.high ソフトリミット(通常は 30 MB ~ 190 MB)は、グローバルなメモリ不足が発生する前に、コールド ファイル ページ キャッシュとバースト後の匿名ヒープを事前に再利用します。

次の表は、2 つのメカニズムを比較したものです。

ディメンション システム デーモンの保護の下限 システムアプリのメモリ上限
プラットフォーム サービス SystemMemoryBudgetManager MemoryLimiter
Cgroup v2 の制御 memory.min ハード プロテクション フロア memory.high のソフト再利用の上限
ターゲット プロセス レイテンシが重要なインタラクティブ デーモンとシェル UI(system_server、surfaceflinger、composer HAL、audioserver、cameraserver、netd、SystemUI、Google Home アプリ) 永続的なヘッドレス システムアプリ、コンテンツ プロバイダ、バックグラウンド サービス(Bluetooth、Nfc、MediaProvider、NetworkStack、AppSearch、TeleService など)
サイズ設定モデル 実行時のセルフチューニング(安定したワーキング セットの 90%、グローバルに合計 RAM の 10% を上限とする)、デバイスごとのチューニング テーブルはゼロ RAM 階層、プロセス状態、ディスプレイのピクセル数によって調整される宣言型 <memory-budget> マニフェスト XML 属性
主なメリット 静的な PinnerService mlock ファイルリストを置き換えることで、メモリ不足時のインタラクティブ UI のジャンクとウェイクアップの停止を防ぎます。 コールドな 1 回限りのファイル キャッシュとバースト後のヒープを継続的に削除して、フォアグラウンド アプリとキャッシュに保存されたアプリにより多くの物理 RAM を残します。

<memory-budget> 構文、XML 属性、ランタイム API、OS メモリ アカウンティングの詳細については、アプリのメモリ予算を設定すると Android メモリの理解とトラブルシューティングをご覧ください。

SystemMemoryBudgetManager で重要なインタラクティブ デーモンを保護する

SystemMemoryBudgetManager は、メモリが不足しているときに、ユーザーのインタラクティブ パスに直接存在するプロセスを物理 RAM に常駐させます。mlock を使用して APK、ODEX、VDEX ファイルの静的リストをメモリに固定するのではなく(PinnerService が config_defaultPinnerServiceFiles と config_pinnerHomePinBytes に対して行うように)、SystemMemoryBudgetManager は動的 cgroup v2 memory.min フロアを次のプロセスに割り当てます。

  • コアシステム スライス(/sys/fs/cgroup/system): system_server、surfaceflinger、グラフィック composer HAL、audioserver、cameraserver、netd。さらに、config_systemMemoryBudgetAdditionalSystemProcesses に記載されているフォーム ファクタ固有のネイティブ システムまたはベンダー デーモン(XR デバイスの android.hardware.perceptionhost や auracamerahal など)。
  • インタラクティブなアプリスライス(/sys/fs/cgroup/apps): 構成済みの SystemUI パッケージ(config_systemUi)のメインプロセスと、現在のユーザーのホームロール ホルダーのメインプロセス。

config_systemUi とともにホームロール ホルダーを保護することで、すべてのフォーム ファクタを確実にカバーできます。config_systemUi が軽量の互換性パッケージであるフォーム ファクタ(Wear OS など、ウォッチフェイス、通知、タイルが Google Home アプリ内で実行される場合)では、ユーザー向けのシェルは引き続き完全なワーキング セット保護を受けます。Home インテントが config_systemUi パッケージに解決されると、プロセスは 1 回保護されます。保護フロアを受け取るのは各パッケージのメインプロセス(processName == packageName)のみであるため、補助サブプロセス(:screenshot など)が保護予算を消費することはありません。

セルフチューニング ポリシーと 3 レベルの cgroup 委任

インタラクティブ デーモンのワーキング セットは、フォーム ファクタと RAM 容量によって大きく異なるため(たとえば、SystemUI はスマートウォッチで約 45 MB、6 GB のスマートフォンで 60 MB、16 GB のスマートフォンで 180 MB を使用します)、デバイスごとの静的なルックアップ テーブルは脆弱です。代わりに、SystemMemoryBudgetManager は、次の 3 つの定数を使用して、各プロセスの独自の常駐フットプリントから直接フロアを導出します。

ポリシー パラメータ デフォルト値 動作
プロセスごとの下限 決済済み memory.current の 90% から未マッピングのファイル キャッシュを差し引いた金額 匿名のヒープ、スラブ、ホットマップされたコードを保護しながら、最もコールドな 10% のテールを再利用可能にして、真にコールドなページを削除できるようにします。90% の比率を適用する前に、マッピングされていないファイル キャッシュ(memory.stat の max(0, file - shmem - file_mapped))が差し引かれます。これにより、起動パッケージ スキャン キャッシュや 1 回限りのアセット読み取りによってフロアが膨らむことはありません。
グローバル RAM 上限 合計 RAM の 10%(config_systemMemoryBudgetLowKb) PinnerService config_pinnerMaxPinnedMemoryPercentage ガードレールと一致します。90% の最小価格の合計が MemTotal の 10% を超える場合、すべての最小価格が同じ係数で比例的にスケールダウンされます。
更新頻度 開始から 1、4、16、64 秒後、その後は 30 分ごと 幾何級数的な収束により、デバイスごとのウォームアップ遅延をハードコードすることなく、各デーモンの起動後のワーキング セットに収束します。また、30 分間の定常状態の更新により、デバイスのライフサイクル全体にわたってワーキング セットの段階的な増減が追跡されます。

Linux cgroup v2 メモリ コントローラ(mm/memcontrol.c:effective_protection)では、リーフ cgroup の有効な memory.min 保護は、親 cgroup と祖先 cgroup で構成された memory.min によって制限されます。uid_<uid>/memory.min が 0 のときに、リーフ pid_<pid> ディレクトリにのみ memory.min を設定すると、有効な保護はゼロになります。逆に、最上位の /sys/fs/cgroup/system スライスにのみフラットな memory.min を設定すると、未要求の保護がすべての子 UID とプロセスに比例して分散されます(memory_recursiveprot)。これにより、uid_1000(android.uid.system)を共有する診断プロセスまたはセカンダリ プロセスが surfaceflinger と audioserver から保護を奪うことができます。

デーモンごとに正確な分離を実現するため、SystemMemoryBudgetManager は更新のたびに cgroup v2 階層の 3 つのレベルすべてに保護を書き込みます(図 1 を参照)。

  1. リーフ プロセスノード(pid_<pid>/memory.min): プロセスの(スケーリングされた)90% ワーキング セットの下限に設定します。
  2. UID ノード(uid_<uid>/memory.min): 管理対象の pid_* リーフフロアの正確な合計に設定されます。そのため、その UID を共有する管理対象外のプロセス(他の AID_SYSTEM デーモンや sharedUserId="android.uid.system" パッケージなど)は memory.min = 0 となり、保護を要求できません。
  3. 最上位スライス(/sys/fs/cgroup/system/memory.min と /sys/fs/cgroup/apps/memory.min): uid_* 子の正確な合計に設定します。管理対象ノードの古い memory.low 値は 0 にクリアされ、以前の更新で管理されていた uid_* ディレクトリ(ユーザーがランチャーを切り替えた後の以前のホーム ランチャーなど)は 0 にリセットされます。

/sys/fs/cgroup/system と /sys/fs/cgroup/apps にまたがる 3 レベルの cgroup v2 memory.min 保護階層。リーフ プロセスのフロアが UID ノードとスライスノードに合計され、保護されていない兄弟プロセスはゼロのまま

図 1. cgroup スライスにわたる 3 レベルの保護フロア委任。

MGLRU カーネルで memory.low ではなく memory.min が使用される理由

Multi-Generational LRU(MGLRU、Android 6.6 以降のカーネルのデフォルトのページ再利用エンジン)では、memory.low と memory.min の動作が mm/vmscan.c:shrink_one で異なります。

  • memory.low(シングルパス遅延): 使用量が memory.low を下回る cgroup は、memcg LRU(MEMCG_LRU_TAIL)の末尾にローテーションされ、1 回の再利用パスでスキップされます。メモリ不足が続く次のパスで、カーネルは cgroup を完全に再利用し、MEMCG_LOW イベントをログに記録します。匿名のメモリホグが物理 RAM の 85% に増加する実機ストレステストでは、memory.low により、スマートウォッチの SystemUI が約 45 MB から約 5 ~ 10 MB に減少し、16 GB の折りたたみ式デバイスの audioserver が約 14 MB の下限に対して 15 MB から 2 MB に減少しました。
  • memory.min(ハード保護フロア): 使用量が memory.min を下回る cgroup は、グローバル再利用によってスキップされます。図 2 に示すように、同じ 85% の RAM 負荷ワークロードで、memory.min は各デーモンの最もコールドな 10% のテールのみを破棄し、SystemUI、surfaceflinger、audioserver を 90% のフロアに保持します。匿名のリフォルトはゼロで、lmkd プロセスの終了は増加しません。

memory.min は、置き換え対象の PinnerService mlock の固定化による保護に一致するハードな物理予約を提供します。この場合、カーネル LRU(静的ファイルリストではなく)が、どのホットコード、ヒープ、グラフィック ページを常駐させるかを選択するという利点があります。

システムメモリの負荷が物理 RAM の 40% から 95% に増加したときのデーモンの常駐メモリの比較。memory.min は 90% の下限を維持しているが、memory.low は 1 回の MGLRU 遅延パスの後に崩壊している

図 2. メモリ負荷の上昇に伴うデーモンのワーキング セットの保持。

起動時のプリウォーミングと起動後のマッピングされていないファイル キャッシュの再利用

config_defaultPinnerServiceFiles の PinnerService 永続 mlock を置き換えるには、次の 2 つの起動時の動作を調整する必要があります。

  • 1 回限りのバックグラウンド ブート プリウォーミング(PinnerService): ページ キャッシュをウォームアップせずに PinnerService mlock をスキップすると、SystemUI の起動時に SystemUI の APK、ODEX、VDEX のディスク読み取り(約 95 MB)が同期ブートパスにシフトされ(OnBootPhase_550)、起動完了までに 300 ~ 450 ミリ秒が追加されます。madvise(MADV_WILLNEED) を呼び出すだけでは不十分です。カーネルの force_page_cache_ra が、1 回の MADV_WILLNEED 呼び出しを 1 つの先読みウィンドウ(通常は 128 KB ~ 2 MB)にクランプするためです。代わりに、SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning がアクティブな場合、PinnerService は onStart 中にバックグラウンド スレッドで config_defaultPinnerServiceFiles 内の各ファイルをマッピングし、Os.mlock を使用してファイルをページ キャッシュに同期的に入力し、Os.munlock、munmap、close をすぐに呼び出します。これにより、高速起動のタイミングを維持しながら、永続的にロックされたバイトをゼロにできるため、コールド スタートアップ ページを後で強制排除できます。
  • system_server ブート パッケージ スキャン キャッシュの除外と再利用: ブート中に、PackageManagerService はインストールされた APK をスキャンし、1.5 ~ 2.5 GB のコールド マッピングされていないファイル ページ キャッシュを system_server cgroup に残します。system_server の memory.min 計算に含まれている場合、そのコールドブート キャッシュは 10% のグローバル RAM 上限を飽和させ、surfaceflinger、SystemUI、audioserver の保護フロアを比例的にスケールダウンします。上限の枯渇を防ぐため、SystemMemoryBudgetManager は system_server の memory.stat を読み取り、マッピングされていないファイル キャッシュを max(0, file - shmem - file_mapped) として計算し、図 3 に示すように、90% の下限を計算する前に、マッピングされていないキャッシュを memory.current から差し引きます。さらに、PHASE_BOOT_COMPLETED の 10 秒後に、マネージャーは Linux 6.2 以降のカーネルで system_server の memory.reclaim ノードに <unmapped_file> swappiness=0 のワンショット書き込みを実行し、匿名ヒープをスワップしたり、マッピングされた DEX ページとコードページを強制排除したりすることなく、コールド パッケージ スキャン キャッシュを事前に強制排除します。EINVAL で swappiness=0 を拒否する古いカーネル(android14-6.1 など)では、マネージャーは、起動中に匿名のヒープをスワップアウトする、不適格な再利用書き込みにフォールバックするのではなく、memory.reclaim 書き込みをスキップします。

起動後の system_server メモリのタイムライン。1 秒、4 秒、16 秒、64 秒での幾何学的安定化リフレッシュ、90% の memory.min フロアからのマッピングされていない起動パッケージ スキャン キャッシュの除外、swappiness=0 での 1 回限りの memory.reclaim を示しています。

図 3. 起動後の保護フロアの収束とキャッシュの再利用。

カーネルの要件とデバイス設定のオーバーレイ

SystemMemoryBudgetManager は、Linux カーネルの mem_cgroup_calculate_protection を利用して、ネストされた cgroup 全体に memory.min を伝播します。Android 共通カーネル android15-6.6 と android16-6.12 には、MGLRU のネストされた保護の完全なサポートが最初から含まれています。MGLRU が有効な android14-6.1 を搭載したデバイスでは、空の中間スライスと UID cgroup がリーフ プロセス cgroup に保護を伝播するように、mm/vmscan.c と mm/memcontrol.c に 3 つのアップストリーム バックポートが必要です。

  • 30d77b7eef01(mm/mglru: fix ineffective protection calculation)
  • 8aa420617918(mm/mglru: respect min_ttl_ms with memcgs)
  • adb8213014b2(mm: memcg: fix stale protection of reclaim target memcg)

OEM は、次のフレームワーク リソース オーバーレイを使用して SystemMemoryBudgetManager をカスタマイズまたは無効化できます。

  • config_systemMemoryBudgetLowKb(整数、KB): 0(デフォルト)は、すべての memory.min フロアの合計を合計物理 RAM の 10% に制限します。正の値を指定すると、KB 単位で明示的な上限が設定されます。負の値を指定すると、デバイスで SystemMemoryBudgetManager が無効になります(以前の PinnerService 静的 mlock 固定は保持されます)。
  • config_systemMemoryBudgetHighKb(整数、KB): 0(デフォルト)は /sys/fs/cgroup/system/memory.high を max のままにします。正の値を指定すると、上位スライスの memory.high の上限が設定されます(下限キャップ以上にする必要があります)。
  • config_systemMemoryBudgetAdditionalSystemProcesses(文字列配列): /sys/fs/cgroup/system 内の追加のレイテンシ クリティカルなネイティブ システムまたはベンダー デーモン プロセス名を一覧表示します(10000 のシステム UID とベンダー UID の /proc/<pid>/cmdline の /proc/<pid>/comm または argv[0] のベース名と照合されます)。デフォルトの 6 つのシステム デーモンとともに 90% の memory.min フロアで保護します。init.rc で memory.min を設定するのではなく、このオーバーレイを使用します。SystemMemoryBudgetManager は、管理対象のリーフプロセスの合計として uid_<uid>/memory.min を再計算し、更新のたびに管理対象外の uid_* ノードをクリアするためです。たとえば、XR デバイスは、次のオーバーレイを使用して空間トラッキングとカメラ HAL デーモンを保護できます。

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi(文字列): プラットフォーム SystemUI アプリのパッケージ名。MATCH_SYSTEM_ONLY で解決されます。

デバイス上のアクティブな SystemMemoryBudgetManager フロアと cgroup ノードを調べるには、次のコマンドを実行します。

# Inspect policy state, resolved PIDs, and applied floors in dumpsys meminfo
adb shell dumpsys meminfo | sed -n '/System Memory Budgets/,$p'
# Inspect raw cgroup v2 memory.min nodes across the system and apps slices
adb shell head -n 1 \
  /sys/fs/cgroup/system/memory.min \
  /sys/fs/cgroup/system/uid_*/memory.min \
  /sys/fs/cgroup/system/uid_*/pid_*/memory.min \
  /sys/fs/cgroup/apps/memory.min \
  /sys/fs/cgroup/apps/uid_*/memory.min \
  /sys/fs/cgroup/apps/uid_*/pid_*/memory.min

永続的なバックグラウンド システムアプリをメモリ バジェットにバインドする

SystemMemoryBudgetManager は memory.min フロアを使用してインタラクティブ デーモンを再利用から保護しますが、他の多くのシステムアプリや特権プラットフォーム サービスは、PERSISTENT プロセス、バインドされたフォアグラウンド サービス、または頻繁にクエリされるコンテンツ プロバイダとして、バックグラウンドで長時間実行されます。明示的なメモリ上限がない場合、これらのプロセスは、数ギガバイトのメモリ制限の上限(表示されているアプリの場合は 4 GB ~ 10 GB、バックグラウンド アプリの場合は 2 GB ~ 5 GB)に達するか、グローバルなメモリ不足によってシステム全体の kswapd とローメモリ キラー(lmkd)のアクティビティがトリガーされるまで、制約されません。

制限を設けない場合、バックグラウンド システム プロセスは、アクティブなワーキング セットの一部ではない大量のコールドメモリを蓄積することがよくあります。

  • 無制限のファイル ページ キャッシュ: メディアのインデックス登録、データベースの検索、カレンダーとプロバイダ テーブルの同期を行うサービス(AppSearch、MediaProvider、CalendarProvider など)は、ディスク上の大きなファイルをマッピングまたはストリーミングします。cgroup memory.high の上限がない場合、1 回限りの読み取りでクリーンなファイルベースのページがプロセス cgroup に無期限に課金され、数百メガバイトの物理 RAM が消費されます。
  • バースト後の匿名ヒープの保持: Bluetooth 音声や BLE の検出バースト、NetworkStack キャプティブ ポータルの TLS 検証、バックグラウンドに移行する UI ヘルパーなど、一時的なアクティビティの急増が発生するサービスでは、バーストが完了した後も、コールド匿名ヒープページが保持されることがあります。

調整済みの <memory-budget> 宣言(memory.high ソフトリミット、通常は 30 MB ~ 190 MB)をシステムアプリに適用すると、主に次の 2 つのメリットがあります。

  • プロアクティブなローカライズされたページ再利用: システム プロセスが構成済みの memory.high バジェットを超えると、Linux カーネルはページ再利用をその cgroup に特化して指示し、コールド ファイルにバックアップされたページを最初に強制排除し、非アクティブな匿名ページを ZRAM に圧縮します。このとき、グローバル デバイスのメモリ負荷を待つことはありません。
  • フォアグラウンド アプリとキャッシュに保存されたアプリプールで使用可能な RAM の増加: 常時稼働のシステム サービス全体でコールド ファイル キャッシュを制限すると、デバイス全体で数十から数百メガバイトの物理 RAM が再利用され、UI のジャンクが減少し、カメラ起動時の負荷が軽減され、キャッシュに保存された状態でより多くのユーザーアプリが維持されます。

システムアプリのメモリ予算を構成する

システムアプリは、一般アプリと同じ <memory-budget> マニフェスト構文、プロセス状態のカスケード、ディスプレイ密度のスケーリング、最後のマッチング句の解決ルールを使用します。属性の完全なリストについては、XML 属性のリファレンスをご覧ください。

OEM は、システムアプリの AndroidManifest.xml 内で <memory-budget> 要素を直接宣言するか、Android.bp の additional_manifests プロパティを使用して別の XML ファイルからマージできます。慣例として、AOSP はリファレンス システムアプリの予算をスタンドアロンの *-budget.xml ファイル(コア プラットフォーム パッケージの場合は frameworks/base/core/memory_budgets/、packages/modules/NetworkStack/memory_budgets/、packages/modules/AppSearch/apk/、packages/services/Telephony/ などの個々のモジュールの場合はその横)に保存し、additional_manifests で添付します。

android_app {
    name: "TeleService",
    manifest: "AndroidManifest.xml",
    additional_manifests: ["Telephony-budget.xml"],
    // ...
}

パッケージ レベル(UID)とプロセスレベル(PID)のスコープ

パッケージ レベルのベースライン予算(<application>)を宣言するか、マルチプロセス予算(<processes><process>)を宣言するかによって、カーネルが UID cgroup レベル(/sys/fs/cgroup/uid_<uid>/memory.high)で memory.high を適用するか、PID ごとの cgroup レベル(/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high)で適用するかが決まります。図 4 をご覧ください。

  • パッケージ レベル(<application>)の予算: APK が独自の専用 UID(Bluetooth、Nfc、NetworkStack、CalendarProvider、Traceur、AppSearch など)で単一のプロセスを実行する場合に使用します。
  • プロセスレベル(<processes><process>)の予算: APK が android:sharedUserId(UID 1001 の android.uid.phone や UID 1000 の android.uid.system など)を他のシステム パッケージと共有し、その UID を共有する他のプロセスが一緒にスロットルされないようにする場合や、1 つの APK が異なるメモリロールを持つ複数のプロセス(バックグラウンド MediaProvider デーモンと :PhotoPicker UI サブプロセスなど)を実行する場合に使用します。

専用 UID の UID レベルの memory.high 適用と、共有システム UID およびマルチプロセス APK の PID ごとの memory.high 適用を比較する Cgroup v2 階層

図 4. UID レベルと PID レベルのメモリ上限。

次の例は、RAM 階層型予算をデフォルトの MediaProvider プロセス(<process>)に、ディスプレイ スケール予算を :PhotoPicker UI プロセスにスコープする MediaProvider-budget.xml を示しています。各 android:maxDeviceRamMb 属性は、合計物理 RAM(MemTotal)が指定された MB しきい値以下のデバイスに一致します(省略した場合は、任意の RAM 容量に適用されます)。句は、制約のないデフォルトから最小の maxDeviceRamMb しきい値まで順に並べられます。これにより、最後に一致した句の解決ルールによって、適用可能な最も厳しい階層が選択されます。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.providers.media.module">
    <uses-sdk android:targetSdkVersion="10000" />
    <application>
        <processes>
            <process>
                <memory-budget
                    android:maxMb="85" />
                <memory-budget
                    android:maxMb="75"
                    android:maxDeviceRamMb="12288" />
                <memory-budget
                    android:maxMb="60"
                    android:maxDeviceRamMb="8192" />
            </process>
            <process android:process=":PhotoPicker">
                <memory-budget
                    android:maxMb="85"
                    android:additionalBytesPerDisplayPixel="25" />
            </process>
        </processes>
    </application>
</manifest>

Google Pixel デバイスで AOSP 参照バジェットを調整する方法

AOSP のリファレンス システムアプリのメモリ予算は、6 GB ~ 16 GB の物理 RAM(6 GB、8 GB、12 GB、16 GB のスマートフォン、折りたたみ式デバイス、タブレットを含む)と、約 1.2 MP(720×1600)~約 4.5 MP(2076×2152)のディスプレイ解像度をカバーする Google Pixel デバイス全体で調整されています。

キャリブレーションでは、2 つの補完的なデータソースを組み合わせました。1 つは、フリート全体の cgroup v2 フィールド テレメトリー、もう 1 つは、クリティカル ユーザー ジャーニー(CUJ)全体にわたる制御された実機 memory.high スイープです。

フリート全体の cgroup v2 テレメトリー分析

定期的な cgroup v2 スナップショットがデバイス フリート全体で収集され、物理 RAM 階層(8 GB 以下、12 GB、16 GB 以上)とプロセス状態バケット(foreground、perceptible、background、cached)でスライスされました。統計分析は、次の 6 つの原則に基づいて行われました。

  • memory.current をワーキング セットと再利用可能なファイル キャッシュに分解する: memory.high は常駐 cgroup チャージ(memory.current)を管理するため、図 5 に示すように、各スナップショットは memory.stat を使用して、匿名ヒープ(anon)、スワップされた匿名ページ(swap)、ファイルベースのページ キャッシュ(file)に分解されました。圧縮不可能な匿名メモリの需要の合計(P99 での anon + swap)は、予算の最小ワーキング セットの下限を確立するため、プロセスが ZRAM の枯渇のリスクにさらされることはありません。一方、memory.current P99 と anon + swap P99 の間に大きなギャップがある場合は、キャッシュがマッピングされておらず、コールドである場合に、memory.high が再利用できるファイル ページ キャッシュを示します。
  • memory.high が再利用できないメモリを考慮する: すべてのファイルと共有メモリがコールド キャッシュであるとは限りません。ロックされたページ(memory.stat の unevictable。たとえば、mlock でロックされた共有メモリや、ホストプロセスがアプリの cgroup に課金される仮想マシンのロックされたゲスト RAM)は、予算の下限を設定します。また、マッピングされたページ(file_mapped)は、多くの場合、アクティブなワーキング セットの一部です。ファイルメモリを再利用可能として扱う前に、unevictable、shmem、file_mapped を確認します。たとえば、約 100 MB の AppSearch メモリがロックされ、強制排除できないため、その予算は、その下限とホットホスト ワーキング セットをカバーし、ロックされたリージョンが増大する余地があります。
  • アイドル状態の希釈をフィルタする: コンテンツ プロバイダ(CalendarProvider)とオンデマンド デベロッパー ツール(Traceur)は、フィールド スナップショットの 70% ~ 99.9% をアイドル状態で 1 ~ 3 MB 程度消費します。すべてのスナップショットで未加工のパーセンタイルを計算すると、P95 と P99 の推定値が大幅に希薄化されます。代わりに、希釈されていないアクティブなスナップショット(CACHED スナップショットとアイドル状態のベースライン スナップショットを除外)で、パーセンタイルと信頼区間が計算されました。
  • デバイス クラスタ化された信頼区間を使用する: 1 台のデバイスが時間の経過とともに多くの定期的なスナップショットに寄与するため、スナップショットを独立したサンプルとして扱うと、テール不確実性が過小評価されます。すべての P95 と P99 のワーキング セットの推定値は、保守的なデバイス クラスタ化された 95% の信頼区間(有効なサンプルサイズとして個別のデバイス数 N_dev を使用)を使用して評価されました。
  • memory.peak を使用して一時的なバーストを考慮する: 定期的なサンプリングでは、短時間で大量の割り当てが発生するバースト(NetworkStack キャプティブ ポータル TLS 検証と同時 DNS プローブ、Bluetooth A2DP と BLE の初期化など)を見逃す可能性があります。バースト サービスの場合、予算は、RAM 階層全体でライフタイム カーネル memory.peak の高水位の 90 ~ 95 パーセンタイルをカバーするようにサイズ設定されました。
  • RAM 階層がデバイスの世代ではなく RAM を反映していることを確認する: 新しいデバイスほど、RAM と機能の両方が増える傾向があるため、RAM 階層とともに増加するワーキング セットは、実際には機能とともに増加している可能性があります。maxDeviceRamMb 階層を追加する前に、デバイスモデル別にデータをスライスします。たとえば、com.android.phone は、同じ世代の 12 GB と 16 GB のスマートフォンで同じ P99 anon + swap(約 85 ~ 90 MB)に達しますが、古い 12 GB の折りたたみ式スマートフォンでは 40 MB を使用します。そのため、その予算では RAM 階層のない単一の値が使用され、新しいテレフォニー機能を搭載した低 RAM デバイスの予算が不足しないようにしています。

cgroup メモリの経時的な分解。圧縮不可能な匿名ワーキング セット、アクティブなファイル キャッシュ ウィンドウ、memory.high を超えて再利用されるコールドな 1 回限りのファイル キャッシュに分解されます。

図 5. ワーキング セットと再利用可能なファイル キャッシュ。

実機 CUJ とディスプレイ スケーリング スイープ

テレメトリーから導出された予算を検証し、ディスプレイに比例する予算(:PhotoPicker など)と状態固有のバックグラウンド予算(Traceur など)を調整するために、6 GB から 16 GB の RAM を搭載した実機でライブ memory.high スイープが実行されました。

  • ゼロリフォルト ニーの検出: エンドツーエンドの CUJ(高速なフォトグリッドのフリック、アルバムの閲覧、全画面表示の高解像度写真と動画のプレビューのスワイプ、トレースの記録と保存など)を実行しながら、cgroup memory.stat カウンタ(workingset_refault_anon、workingset_refault_file、pgscan、pgsteal)と memory.events(high)を測定しつつ、memory.high を段階的に下げました。図 6 に示すように、最適なバジェットはゼロリフォルト ニーのすぐ上にあります。ゼロリフォルト ニーは、カーネルがアクティブなワーキング セットのスラッシング(workingset_refault == 0)をトリガーせずに、コールドな 1 回限りのファイル キャッシュ(pgsteal > 0)を再利用するしきい値です。
  • 解像度全体でのディスプレイ ピクセル スケーリングの検証: 約 1.2 MP(720×1600)から約 4.5 MP(2076×2152 の展開された内部ディスプレイ)までの 4 つの物理ディスプレイ解像度とエミュレートされたディスプレイ解像度で :PhotoPicker をテストしたところ、アクティブな Glide と ViewPager2 のビットマップ ワーキング セットがディスプレイ ピクセル数に比例してスケーリングされることが確認されました。ディスプレイ以外のベースラインもデバイスによって異なります。ほぼ同じピクセル数で、ゼロリフォルトのニーは、16 GB の折りたたみ式デバイスの外側ディスプレイでは約 140 MB でしたが、6 GB のスマートフォンでは 110 ~ 125 MB でした。85 MB のベースラインにディスプレイ ピクセルあたり 25 バイト(android:maxMb="85" と android:additionalBytesPerDisplayPixel="25")を加えた値は、テストしたすべてのディスプレイで測定されたニーを上回っています(HD+ で約 110 MB、FHD+ で約 145 MB、展開されたディスプレイで約 190 MB)。
  • バックグラウンド状態での ART ガベージ コレクションの検証: cgroup v2 では、Zygote 共有の boot.art とフレームワーク イメージ ページは Zygote の cgroup に課金されたままで、ART の同時マークコンパクト(CMC)ガベージ コレクション中は免疫空間として扱われます。ストレステストにより、システム UI アプリが background に移行すると、memory.high を引き締める(たとえば、Traceur で 30 MB にする)ことで、GC スラッシングやスワップ アクティビティを発生させることなく、プライベート ヒープとページ キャッシュをクリーンにバインドできることが確認されました。

CUJ 中に memory.high が段階的に減少する際のコールド キャッシュの再利用(pgsteal)とワーキング セットのスラッシング(workingset_refault)のプロット。ゼロリフォルトのニーとターゲット予算が強調表示されています。

図 6. CUJ 中にゼロリフォルト ニーを見つける。

AOSP リファレンス システムアプリの予算の概要

次の表は、RAM 階層とプロセス状態にわたる AOSP の調整済み参照バジェットをまとめたものです。

システム パッケージまたはプロセス スコープ 8 GB 以下の RAM 12 GB RAM 16 GB 以上の RAM 主なキャリブレーション要因
com.android.bluetooth UID(<application>) 80 MB 95 MB 110 MB P90 のライフタイム memory.peak バースト(約 75 ~ 110 MB)と 2 倍以上の P99 アクティブ memory.current をカバーします。
com.android.nfc UID(<application>) 50 MB 60 MB 60 MB コールド ファイル キャッシュをトリミングしながら、アクティブなホストカード エミュレーション P99(最大約 50 MB)と P90 ~ P95 memory.peak をカバーします。
com.android.providers.calendar UID(<application>) 70 MB(デフォルト、フォアグラウンド)
50 MB(perceptible、background)
アクティブなワーキング セットは RAM 階層間で不変です。バックグラウンドの予算は、同期中に SQLite ページ キャッシュをトリミングしながら、リマインダー アラーム用の匿名のヘッドルームを維持します。
com.android.traceur UID(<application>) 85 MB(デフォルト、フォアグラウンド、知覚可能)
30 MBbackground
アクティブなトレースの記録とエクスポート(TraceService)中は 85 MB のヘッドルームを完全に保持し、アイドル状態とブロードキャスト状態では 30 MB に制限します。
com.android.providers.media.module(メイン) PID(<process>) 60 MB 75 MB 85 MB コールド SQLite とサムネイル ファイル キャッシュを再利用しながら、P99 anon + swap を 10 ~ 30 MB 程度上回るヘッドルームを提供します。
com.android.providers.media.module:PhotoPicker PID(<process>) 85 MB + 25 バイト/ピクセル
(HD+ で約 110 MB、FHD+ で約 145 MB、約 4.5 MP 折りたたみで約 190 MB)
Glide ビットマップ ワーキング セットのニーに一致するように、ディスプレイのピクセル数(additionalBytesPerDisplayPixel="25")に比例してスケーリングされます。
com.android.networkstack UID(<application>) 65 MB 70 MB 85 MB 主に匿名メモリ。キャプティブ ポータルの TLS、DNS、DHCP のバーストをカバーするために、P95 の有効期間 memory.peak に合わせてサイズが調整されます。
com.android.appsearch.apk UID(<application>) 130 MB 約 100 MB のロックされた、強制排除できないメモリと、約 5 MB のホットホスト ワーキング セットをカバーし、ロックされたリージョンが増大する余地があります。コールド ページ キャッシュ(P99 で最大約 330 MB)が再利用されている間、125 MB 以上で測定されたリフォルトはゼロ。
com.android.phone(TeleService) PID(<process>) 120 MB 共有 UID 1001 で PID ごとにスコープが設定されています。ワーキング セットは RAM ではなくデバイスの世代を追跡するため、1 つの予算はモデルごとの P99 anon + swap の最大値を約 30 MB 上回ります。

OEM が独自のデバイスに合わせてシステムアプリの予算を再調整する方法

システムアプリをカスタマイズする(ベンダーの Bluetooth プロファイル、カスタム テレフォニー拡張機能、OEM メディア インデクサを追加するなど)か、追加のプリインストール システム サービスを搭載する OEM は、既存の予算を再調整するか、ハードウェア ポートフォリオ用に新しい *-budget.xml ファイルを作成できます。

アクティブな cgroup v2 メモリ プロファイルを収集する

ターゲット システム プロセスごとに、代表的なワークロードと RAM ティア全体で /sys/fs/cgroup/uid_<uid>/pid_<pid>/ の cgroup v2 メモリノードを読み取ります。

  • memory.current: cgroup に課金される現在の常駐メモリ(Zygote 共有ページを除く)
  • memory.peak: プロセスの開始以降の memory.current の全期間のピーク時の最高値
  • memory.swap.current: ZRAM での現在の圧縮スワップ使用量
  • memory.stat: anon、file、pgscan、pgsteal、workingset_refault_anon、workingset_refault_file の内訳
  • memory.events: high 違反(memory.high スロットリング イベントと再利用イベント)の累積数

ドッグフード デバイスまたはフィールド デバイスからテレメトリーを集計する場合は、アクティブなプロセス状態(TOP、FOREGROUND_SERVICE、アクティブなバインディング、アクティブな同期)をアイドル状態の CACHED またはアイドル状態の PERSISTENT 状態から分離して、アイドル状態のスナップショットが P95 および P99 のワーキング セットの計算を下方修正しないようにします。

圧縮不可能なヒープを再利用可能なファイル キャッシュから分離する

anon + swap の 99 パーセンタイルと memory.current の 99 パーセンタイルを比較します。

  • ファイル キャッシュが支配的なサービス(file &gg; anon。データベース プロバイダ、検索インデクサ、ログデーモンなど): P99 anon + swap に加えて、ホットファイル ページの作業ウィンドウに十分なヘッドルーム(通常は 1.5 ~ 4 倍)を提供するように android:maxMb を設定します。この構成により、カーネルは、匿名ヒープに触れることなく、コールドなマッピングされていないファイルページを継続的に強制排除できます。unevictable または file_mapped が大きい場合(ロックされたメモリやメモリマップド データベースなど)、そのフットプリントもワーキング セットとしてカウントします。
  • 匿名ヒープが支配的なサービスまたはバースト サービス(anon が memory.current の大部分を占める、ネットワーク スタックや接続スタックなど): memory.peak の P90 ~ P95 をカバーするように android:maxMb のサイズを設定し、一時的なバーストによってレイテンシに敏感なスレッドで同期 ZRAM スワップアウトがトリガーされないようにします。
  • ディスプレイを多用する UI プロセス(選択ツール、ギャラリー、システム UI ヘルパーなど): 最低解像度と最高解像度のディスプレイで、UI 以外のベースライン フットプリントと、メガピクセルあたりのビットマップ フットプリントの増分を測定し、android:maxMb と android:additionalBytesPerDisplayPixel を使用してバジェットを表現します。

実機で memory.high をスイープする

ビルドに新しい予算をコミットする前に、am memory-limiter manual を使用して(または、userdebug ビルドのプロセスの memory.high cgroup ノードに直接書き込んで)、自動 CUJ テストの実行中に候補の上限をテストします。

# Override memory.high for a specific PID to 60 MB during a CUJ test
adb shell am memory-limiter manual <PID> 60MB

CUJ の前後に memory.stat と memory.events をモニタリングします。

  • 予算が少なすぎる: CUJ 中に workingset_refault_anon または workingset_refault_file が急増する、アクティブなフォアグラウンド作業中に memory.swap.current が継続的に増加する、CUJ フレームまたは Binder のレイテンシが低下する。リフォールト ニーを超えて予算を増やします。
  • 予算が高すぎる: memory.events:high は、大量のファイル ストリーミングやインデックス作成ワークロードの後でも 0 のままで、memory.stat の file はコールド ページで無制限に増加します。アクティブなワーキング セットに合わせて予算を削減します。
  • 予算が適切に調整されている: 通常のオペレーション中、定常状態の memory.current は memory.high を下回ります。ファイルやキャッシュのバーストが激しい場合、pgsteal はコールド ファイル ページを再利用しますが、workingset_refault_anon はほぼゼロのままで、CUJ レイテンシは変化しません。