Android 17 以降では、プラットフォームは、一般的なアプリケーションのメモリ制限と連携する 2 つの補完的な Linux cgroup v2 メカニズムを使用して、システム プロセスのメモリを管理します。
- システム デーモン保護の下限(
memory.minを含むSystemMemoryBudgetManager): cgroup v2memory.min予約のセルフチューニング(各デーモンの固定された常駐ワーキング セットの 90% に設定され、物理 RAM の合計の 10% を上限とする)により、メモリ不足時に重要なインタラクティブ デーモンとシェルプロセス(system_server、surfaceflinger、グラフィックcomposerHAL、audioserver、cameraserver、netd、SystemUI、ホーム ランチャー)を保護し、静的なPinnerServicemlockファイルの固定を置き換えます。 - システムアプリのメモリ上限(
memory.highを含む<memory-budget>): 永続的なバックグラウンド システムアプリ(Bluetooth、Nfc、CalendarProvider、Traceur、MediaProvider、:PhotoPicker、NetworkStack、AppSearch、TeleService)のマニフェスト ファイルで宣言された、ワークロード調整済みの cgroup v2memory.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、グラフィックcomposerHAL、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 を参照)。
- リーフ プロセスノード(
pid_<pid>/memory.min): プロセスの(スケーリングされた)90% ワーキング セットの下限に設定します。 - UID ノード(
uid_<uid>/memory.min): 管理対象のpid_*リーフフロアの正確な合計に設定されます。そのため、その UID を共有する管理対象外のプロセス(他のAID_SYSTEMデーモンやsharedUserId="android.uid.system"パッケージなど)はmemory.min = 0となり、保護を要求できません。 - 最上位スライス(
/sys/fs/cgroup/system/memory.minと/sys/fs/cgroup/apps/memory.min):uid_*子の正確な合計に設定します。管理対象ノードの古いmemory.low値は0にクリアされ、以前の更新で管理されていたuid_*ディレクトリ(ユーザーがランチャーを切り替えた後の以前のホーム ランチャーなど)は0にリセットされます。
図 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(静的ファイルリストではなく)が、どのホットコード、ヒープ、グラフィック ページを常駐させるかを選択するという利点があります。
図 2. メモリ負荷の上昇に伴うデーモンのワーキング セットの保持。
起動時のプリウォーミングと起動後のマッピングされていないファイル キャッシュの再利用
config_defaultPinnerServiceFiles の PinnerService 永続 mlock を置き換えるには、次の 2 つの起動時の動作を調整する必要があります。
- 1 回限りのバックグラウンド ブート プリウォーミング(
PinnerService): ページ キャッシュをウォームアップせずにPinnerServicemlockをスキップすると、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_servercgroup に残します。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書き込みをスキップします。
図 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など)は、ディスク上の大きなファイルをマッピングまたはストリーミングします。cgroupmemory.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(UID1001のandroid.uid.phoneや UID1000のandroid.uid.systemなど)を他のシステム パッケージと共有し、その UID を共有する他のプロセスが一緒にスロットルされないようにする場合や、1 つの APK が異なるメモリロールを持つ複数のプロセス(バックグラウンドMediaProviderデーモンと:PhotoPickerUI サブプロセスなど)を実行する場合に使用します。
図 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.currentP99 とanon + swapP99 の間に大きなギャップがある場合は、キャッシュがマッピングされておらず、コールドである場合に、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 プローブ、BluetoothA2DP と BLE の初期化など)を見逃す可能性があります。バースト サービスの場合、予算は、RAM 階層全体でライフタイム カーネルmemory.peakの高水位の 90 ~ 95 パーセンタイルをカバーするようにサイズ設定されました。- RAM 階層がデバイスの世代ではなく RAM を反映していることを確認する: 新しいデバイスほど、RAM と機能の両方が増える傾向があるため、RAM 階層とともに増加するワーキング セットは、実際には機能とともに増加している可能性があります。
maxDeviceRamMb階層を追加する前に、デバイスモデル別にデータをスライスします。たとえば、com.android.phoneは、同じ世代の 12 GB と 16 GB のスマートフォンで同じ P99anon + swap(約 85 ~ 90 MB)に達しますが、古い 12 GB の折りたたみ式スマートフォンでは 40 MB を使用します。そのため、その予算では RAM 階層のない単一の値が使用され、新しいテレフォニー機能を搭載した低 RAM デバイスの予算が不足しないようにしています。
図 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 スラッシングやスワップ アクティビティを発生させることなく、プライベート ヒープとページ キャッシュをクリーンにバインドできることが確認されました。
図 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 MB background |
アクティブなトレースの記録とエクスポート(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≫anon。データベース プロバイダ、検索インデクサ、ログデーモンなど): P99anon + 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> 60MBCUJ の前後に 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 レイテンシは変化しません。