System memory budgets

In Android 17 and higher, the platform governs system process memory using two complementary Linux cgroup v2 mechanisms that work alongside the general application Memory Limiter:

  • System daemon protection floors (SystemMemoryBudgetManager with memory.min): Self-tuning cgroup v2 memory.min reservations (set to 90% of each daemon's settled resident working set and capped globally at 10% of total physical RAM) protect critical interactive daemons and shell processes (system_server, surfaceflinger, the graphics composer HAL, audioserver, cameraserver, netd, SystemUI, and the Home launcher) under memory pressure, replacing static PinnerService mlock file pinning.
  • System app memory ceilings (<memory-budget> with memory.high): Workload-calibrated cgroup v2 memory.high soft limits (typically 30 MB to 190 MB) declared in manifest files for persistent and background system apps (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch, and TeleService) proactively reclaim cold file page cache and post-burst anonymous heap before global memory pressure occurs.

The following table compares the two mechanisms:

Dimension System daemon protection floors System app memory ceilings
Platform service SystemMemoryBudgetManager MemoryLimiter
Cgroup v2 control memory.min hard protection floor memory.high soft reclaim ceiling
Target processes Latency-critical interactive daemons and shell UI (system_server, surfaceflinger, composer HAL, audioserver, cameraserver, netd, SystemUI, and the Home app) Persistent and headless system apps, content providers, and background services (such as Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch, and TeleService)
Sizing model Self-tuning at run time (90% of settled working set, globally capped at 10% of total RAM) with zero per-device tuning tables Declarative <memory-budget> manifest XML attributes calibrated by RAM tier, process state, and display pixel count
Primary benefit Prevents interactive UI jank and wakeup stalls under memory pressure while replacing static PinnerService mlock file lists Continuously evicts cold one-time file cache and post-burst heap to leave more physical RAM for foreground and cached apps

For more information about <memory-budget> syntax, XML attributes, runtime APIs, and OS memory accounting, see Set app memory budgets and Understanding and troubleshooting Android memory.

Protect critical interactive daemons with SystemMemoryBudgetManager

SystemMemoryBudgetManager keeps the processes that sit directly on the user's interactive path resident in physical RAM during memory pressure. Rather than pinning a static list of APK, ODEX, and VDEX files into memory with mlock (as PinnerService does for config_defaultPinnerServiceFiles and config_pinnerHomePinBytes), SystemMemoryBudgetManager assigns dynamic cgroup v2 memory.min floors to the following processes:

  • Core system slice (/sys/fs/cgroup/system): system_server, surfaceflinger, the graphics composer HAL, audioserver, cameraserver, and netd, plus any form-factor-specific native system or vendor daemons listed in config_systemMemoryBudgetAdditionalSystemProcesses (for example, android.hardware.perceptionhost and auracamerahal on XR devices).
  • Interactive app slice (/sys/fs/cgroup/apps): The main process of the configured SystemUI package (config_systemUi) and the main process of the current user's Home role holder.

Protecting the Home role holder alongside config_systemUi ensures coverage across all form factors. On form factors where config_systemUi is a lightweight compatibility package (such as Wear OS, where the watch face, notifications, and tiles run inside the Home app), the user-facing shell still receives full working-set protection. If the Home intent resolves to the config_systemUi package, the process is protected once. Only the main process of each package (processName == packageName) receives a protection floor, so auxiliary subprocesses (such as :screenshot) don't consume protection budget.

Self-tuning policy and three-level cgroup delegation

Because the working set of an interactive daemon varies widely across form factors and RAM capacities (for example, SystemUI uses about 45 MB on a smartwatch, 60 MB on a 6 GB phone, and 180 MB on a 16 GB phone), static per-device lookup tables are brittle. Instead, SystemMemoryBudgetManager derives floors directly from each process's own resident footprint using three constants:

Policy parameter Default value Behavior
Per-process floor 90% of settled memory.current less unmapped file cache Protects anonymous heap, slab, and hot mapped code while leaving the coldest 10% tail reclaimable so genuinely cold pages can still be evicted. Unmapped file cache (max(0, file - shmem - file_mapped) from memory.stat) is subtracted before applying the 90% ratio so boot package-scan cache or one-time asset reads don't inflate floors.
Global RAM cap 10% of total RAM (config_systemMemoryBudgetLowKb) Matches the PinnerService config_pinnerMaxPinnedMemoryPercentage guardrail. If the sum of 90% floors exceeds 10% of MemTotal, every floor is scaled down proportionally by the same factor.
Refresh cadence 1, 4, 16, and 64 s after start, then every 30 min A geometric settle series converges on each daemon's post-startup working set without hardcoding per-device warm-up delays, and a 30-minute steady-state refresh tracks gradual working-set growth and shrinkage over the device lifetime.

In the Linux cgroup v2 memory controller (mm/memcontrol.c:effective_protection), a leaf cgroup's effective memory.min protection is bounded by the memory.min configured on its parent and ancestor cgroups. Setting memory.min only on a leaf pid_<pid> directory while uid_<uid>/memory.min is 0 results in zero effective protection. Conversely, setting a flat memory.min only on the top /sys/fs/cgroup/system slice distributes unclaimed protection across all child UIDs and processes proportionally (memory_recursiveprot), allowing diagnostic or secondary processes sharing uid_1000 (android.uid.system) to siphon protection away from surfaceflinger and audioserver.

To provide exact per-daemon isolation, SystemMemoryBudgetManager writes protection across all three levels of the cgroup v2 hierarchy on every refresh, as shown in figure 1:

  1. Leaf process node (pid_<pid>/memory.min): Set to the process's (scaled) 90% working-set floor.
  2. UID node (uid_<uid>/memory.min): Set to the exact sum of its managed pid_* leaf floors, so unmanaged processes sharing that UID (such as other AID_SYSTEM daemons or sharedUserId="android.uid.system" packages) have memory.min = 0 and can't claim protection.
  3. Top-level slice (/sys/fs/cgroup/system/memory.min and /sys/fs/cgroup/apps/memory.min): Set to the exact sum of its uid_* children. Any stale memory.low values on managed nodes are cleared to 0, and any uid_* directories that were managed on a previous refresh (such as a previous Home launcher after the user switches launchers) are reset to 0.

Three-level cgroup v2 memory.min protection hierarchy across /sys/fs/cgroup/system and /sys/fs/cgroup/apps, showing leaf process floors summed into UID and slice nodes while unprotected sibling processes remain at zero

Figure 1. Three-level protection floor delegation across cgroup slices.

Why memory.min is used instead of memory.low on MGLRU kernels

Under Multi-Generational LRU (MGLRU, the default page reclaim engine on Android 6.6 and newer kernels), memory.low and memory.min behave differently in mm/vmscan.c:shrink_one:

  • memory.low (single-pass deferral): A cgroup whose usage is under memory.low is rotated to the tail of the memcg LRU (MEMCG_LRU_TAIL) and skipped for a single reclaim pass. On the next pass under sustained memory pressure, the kernel reclaims the cgroup in full and logs a MEMCG_LOW event. In physical-device stress tests with an anonymous memory hog growing to 85% of physical RAM, memory.low allowed SystemUI on a smartwatch to collapse from ~45 MB down to ~5 to 10 MB and audioserver on a 16 GB foldable to drop from 15 MB to 2 MB against a ~14 MB floor.
  • memory.min (hard protection floor): A cgroup whose usage is under memory.min is skipped by global reclaim. As shown in figure 2, under the same 85% RAM pressure workload, memory.min sheds only the coldest 10% tail of each daemon and holds SystemUI, surfaceflinger, and audioserver at their 90% floors with zero anonymous refaults and no increase in lmkd process terminations.

memory.min provides a hard physical reservation matching the protection of the PinnerService mlock pinning it replaces, with the advantage that the kernel LRU (rather than a static file list) chooses which hot code, heap, and graphics pages stay resident.

Comparison of daemon resident memory as system memory pressure increases from 40% to 95% of physical RAM, showing memory.min holding at its 90% floor while memory.low collapses after a single MGLRU deferral pass

Figure 2. Daemon working-set retention under rising memory pressure.

Boot prewarming and post-boot unmapped file cache reclaim

Replacing the PinnerService permanent mlock of config_defaultPinnerServiceFiles requires coordinating two boot-time behaviors:

  • One-shot background boot prewarming (PinnerService): Skipping PinnerService mlock without warming the page cache shifts ~95 MB of SystemUI APK, ODEX, and VDEX disk reads onto the synchronous boot path when SystemUI starts (OnBootPhase_550), adding 300 to 450 ms to boot completion. Calling madvise(MADV_WILLNEED) isn't sufficient because force_page_cache_ra in the kernel clamps a single MADV_WILLNEED call to one readahead window (typically 128 KB to 2 MB). Instead, when SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning is active, PinnerService maps each file in config_defaultPinnerServiceFiles on a background thread during onStart, synchronously populates the file into the page cache with Os.mlock, and immediately calls Os.munlock, munmap, and close. This preserves fast boot timing while leaving zero permanently locked bytes so cold startup pages can be evicted later.
  • Excluding and reclaiming system_server boot package-scan cache: During boot, PackageManagerService scans installed APKs, leaving 1.5 to 2.5 GB of cold unmapped file page cache charged to the system_server cgroup. If included in the memory.min calculation of system_server, that cold boot cache saturates the 10% global RAM cap and proportionally scales down the protection floors of surfaceflinger, SystemUI, and audioserver. To prevent cap starvation, SystemMemoryBudgetManager reads memory.stat for system_server, computes unmapped file cache as max(0, file - shmem - file_mapped), and subtracts that unmapped cache from memory.current before computing the 90% floor, as shown in figure 3. In addition, 10 seconds after PHASE_BOOT_COMPLETED, the manager performs a one-shot write of <unmapped_file> swappiness=0 to the memory.reclaim node of system_server on Linux 6.2 and newer kernels to proactively evict cold package-scan cache without swapping anonymous heap or evicting mapped DEX and code pages. On older kernels (such as android14-6.1) that reject swappiness=0 with EINVAL, the manager skips the memory.reclaim write rather than falling back to an unqualified reclaim write that would swap out anonymous heap during boot.

Timeline of system_server memory after boot showing the geometric settle refreshes at 1, 4, 16, and 64 seconds, exclusion of unmapped boot package-scan cache from the 90% memory.min floor, and one-shot memory.reclaim with swappiness=0

Figure 3. Protection floor convergence and cache reclaim after boot.

Kernel requirements and device configuration overlays

SystemMemoryBudgetManager relies on mem_cgroup_calculate_protection in the Linux kernel to propagate memory.min across nested cgroups. Android common kernels android15-6.6 and android16-6.12 include full MGLRU nested protection support out of the box. Devices running android14-6.1 with MGLRU enabled require three upstream backports in mm/vmscan.c and mm/memcontrol.c so empty intermediate slice and UID cgroups propagate protection to leaf process cgroups:

  • 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)

OEMs can customize or disable SystemMemoryBudgetManager using the following framework resource overlays:

  • config_systemMemoryBudgetLowKb (integer, KB): 0 (default) caps the sum of all memory.min floors at 10% of total physical RAM. A positive value sets an explicit cap in KB, and a negative value disables SystemMemoryBudgetManager on the device (retaining legacy PinnerService static mlock pinning).
  • config_systemMemoryBudgetHighKb (integer, KB): 0 (default) leaves /sys/fs/cgroup/system/memory.high at max. A positive value sets a top slice memory.high ceiling (must be greater than or equal to the floor cap).
  • config_systemMemoryBudgetAdditionalSystemProcesses (string-array): Lists additional latency-critical native system or vendor daemon process names in /sys/fs/cgroup/system (matched against /proc/<pid>/comm or the basename of argv[0] in /proc/<pid>/cmdline for system and vendor UIDs under 10000) to protect with 90% memory.min floors alongside the default six system daemons. Use this overlay rather than setting memory.min in init.rc because SystemMemoryBudgetManager recomputes uid_<uid>/memory.min as the sum of its managed leaf processes and clears unmanaged uid_* nodes on every refresh. For example, an XR device can protect its spatial tracking and camera HAL daemons with the following overlay:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (string): Package name of the platform SystemUI app, resolved with MATCH_SYSTEM_ONLY.

To inspect active SystemMemoryBudgetManager floors and cgroup nodes on a device, run:

# 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

Bound persistent and background system apps with memory budgets

While SystemMemoryBudgetManager protects interactive daemons from reclaim using memory.min floors, many other system apps and privileged platform services run in the background for long periods as PERSISTENT processes, bound foreground services, or frequently queried content providers. Without explicit memory ceilings, these processes aren't constrained until they reach multi-gigabyte Memory Limiter limits (such as 4 GB to 10 GB for visible apps and 2 GB to 5 GB for background apps) or global memory pressure triggers system-wide kswapd and low-memory killer (lmkd) activity.

Left unconstrained, background system processes often accumulate large amounts of cold memory that isn't part of their active working set:

  • Unbounded file page cache: Services that index media, search databases, or sync calendar and provider tables (such as AppSearch, MediaProvider, and CalendarProvider) map or stream through large files on disk. Without a cgroup memory.high ceiling, clean file-backed pages from one-time reads remain charged to the process cgroup indefinitely, consuming hundreds of megabytes of physical RAM.
  • Post-burst anonymous heap retention: Services that experience transient activity spikes, such as Bluetooth audio and BLE discovery bursts, NetworkStack captive-portal TLS validation, or UI helpers transitioning to the background, can retain cold anonymous heap pages long after the burst completes.

Applying calibrated <memory-budget> declarations (memory.high soft limits, typically 30 MB to 190 MB) to system apps provides two primary benefits:

  • Proactive, localized page reclaim: When a system process crosses its configured memory.high budget, the Linux kernel directs page reclaim specifically at that cgroup, evicting cold file-backed pages first and compressing inactive anonymous pages to ZRAM without waiting for global device memory pressure.
  • More available RAM for foreground apps and the cached app pool: Bounding cold file caches across always-on system services reclaims tens to hundreds of megabytes of physical RAM across the device, reducing UI jank, lowering camera launch pressure, and keeping more user apps alive in the cached state.

Configure system app memory budgets

System apps use the same <memory-budget> manifest syntax, process-state cascading, display-density scaling, and last-matching-clause resolution rule as general apps. For the full list of attributes, see the XML attribute reference.

OEMs can declare <memory-budget> elements directly inside a system app's AndroidManifest.xml or merge them from a separate XML file using the additional_manifests property in Android.bp. By convention, AOSP stores reference system app budgets in standalone *-budget.xml files (either in frameworks/base/core/memory_budgets/ for core platform packages or alongside individual modules such as packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/, and packages/services/Telephony/) and attaches them with additional_manifests:

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

Package-level (UID) versus process-level (PID) scoping

Whether you declare a package-level baseline budget (<application>) or a multi-process budget (<processes><process>) determines whether the kernel enforces memory.high at the UID cgroup level (/sys/fs/cgroup/uid_<uid>/memory.high) or at the per-PID cgroup level (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), as shown in figure 4:

  • Package-level (<application>) budget: Use when the APK runs a single process under its own dedicated UID (for example, Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur, or AppSearch).
  • Process-level (<processes><process>) budget: Use when the APK shares android:sharedUserId (such as android.uid.phone with UID 1001 or android.uid.system with UID 1000) with other system packages so other processes sharing that UID aren't throttled together, or when a single APK runs multiple processes with distinct memory roles (for example, the background MediaProvider daemon and the :PhotoPicker UI subprocess).

Cgroup v2 hierarchy comparing UID-level memory.high enforcement for dedicated UIDs with per-PID memory.high enforcement for shared system UIDs and multi-process APKs

Figure 4. UID-level versus PID-level memory ceilings.

The following example shows MediaProvider-budget.xml, which scopes RAM-tiered budgets to the default MediaProvider process (<process>) and a display-scaled budget to the :PhotoPicker UI process. Each android:maxDeviceRamMb attribute matches devices whose total physical RAM (MemTotal) is at or below the specified MB threshold (or applies to any RAM capacity when omitted). Clauses are ordered from the unconstrained default down to the smallest maxDeviceRamMb threshold so the last-matching-clause resolution rule selects the tightest applicable tier:

<?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>

How AOSP reference budgets were calibrated on Google Pixel devices

The reference system app memory budgets in AOSP were calibrated across Google Pixel devices sweeping 6 GB to 16 GB of physical RAM (including 6 GB, 8 GB, 12 GB, and 16 GB phones, foldables, and tablets) and display resolutions ranging from ~1.2 MP (720×1600) to ~4.5 MP (2076×2152).

Calibration combined two complementary data sources: fleet-wide cgroup v2 field telemetry and controlled physical-device memory.high sweeps across critical user journeys (CUJs).

Fleet-wide cgroup v2 telemetry analysis

Periodic cgroup v2 snapshots were collected across the device fleet and sliced by physical RAM tier (≤8 GB, 12 GB, and ≥16 GB) and process state bucket (foreground, perceptible, background, and cached). Six principles guided the statistical analysis:

  • Decompose memory.current into working set versus reclaimable file cache: Because memory.high governs resident cgroup charge (memory.current), each snapshot was decomposed using memory.stat into anonymous heap (anon), swapped anonymous pages (swap), and file-backed page cache (file), as shown in figure 5. Total incompressible anonymous demand (anon + swap at P99) establishes the minimum working-set floor for the budget so the process doesn't risk ZRAM exhaustion, while a large gap between memory.current P99 and anon + swap P99 indicates file page cache that memory.high can reclaim, provided that cache is unmapped and cold.
  • Account for memory that memory.high can't reclaim: Not all file and shared memory is cold cache. Pages that are locked (unevictable in memory.stat, such as shared memory locked with mlock or the locked guest RAM of a virtual machine whose host processes are charged to the app's cgroup) set a hard floor under the budget, and mapped pages (file_mapped) are often part of the active working set. Check unevictable, shmem, and file_mapped before treating file memory as reclaimable. For example, about 100 MB of AppSearch memory is locked and unevictable, so its budget covers that floor plus its hot host working set, with headroom for the locked region to grow.
  • Filter out idle-state dilution: Content providers (CalendarProvider) and on-demand developer tools (Traceur) spend 70% to 99.9% of field snapshots idle at ~1 MB to 3 MB. Computing raw percentiles across all snapshots severely dilutes P95 and P99 estimates. Instead, percentiles and confidence intervals were computed over undiluted active snapshots (filtering out CACHED snapshots and idle baseline snapshots).
  • Use device-clustered confidence intervals: Because a single device contributes many periodic snapshots over time, treating snapshots as independent samples understates tail uncertainty. All P95 and P99 working set estimates were evaluated using conservative device-clustered 95% confidence intervals (using the distinct device count N_dev as the effective sample size).
  • Account for transient bursts with memory.peak: Periodic sampling can miss short-lived, high-allocation bursts (such as NetworkStack captive-portal TLS validation and concurrent DNS probes, or Bluetooth A2DP and BLE initialization). For bursty services, budgets were sized to cover the 90th to 95th percentile of lifetime kernel memory.peak high-water marks across RAM tiers.
  • Confirm that a RAM tier reflects RAM, not device generation: Newer devices tend to have both more RAM and more features, so a working set that grows with RAM tier might actually be growing with features. Before adding a maxDeviceRamMb tier, slice the data by device model. For example, com.android.phone reaches the same P99 anon + swap (~85 to 90 MB) on 12 GB and 16 GB phones of the same generation, while an older 12 GB foldable uses 40 MB. Its budget therefore uses a single value with no RAM tier, so that a lower-RAM device that ships the newer telephony features isn't under-budgeted.

Decomposition of cgroup memory over time into incompressible anonymous working set, active file cache window, and cold one-time file cache reclaimed beyond memory.high

Figure 5. Working set versus reclaimable file cache.

Physical-device CUJ and display-scaling sweeps

To validate the telemetry-derived budgets and calibrate display-proportional budgets (such as :PhotoPicker) and state-specific background budgets (such as Traceur), live memory.high sweeps were executed on physical devices spanning 6 GB to 16 GB RAM:

  • Finding the zero-refault knee: While driving end-to-end CUJs (such as fast photo grid flinging, album browsing, full-screen high-resolution photo and video preview swiping, and trace recording and saving), memory.high was stepped downward while measuring cgroup memory.stat counters (workingset_refault_anon, workingset_refault_file, pgscan, and pgsteal) and memory.events (high). As illustrated in figure 6, the optimal budget sits just over the zero-refault knee. The zero-refault knee is the threshold where the kernel reclaims cold one-time file cache (pgsteal > 0) without triggering active working-set thrashing (workingset_refault == 0).
  • Validating display-pixel scaling across resolutions: Testing :PhotoPicker across four physical and emulated display resolutions from ~1.2 MP (720×1600) to ~4.5 MP (2076×2152 unfolded inner display) confirmed that its active Glide and ViewPager2 bitmap working set scales linearly with display pixel count. The non-display baseline also varies by device: at nearly the same pixel count, the zero-refault knee was about 140 MB on a 16 GB foldable's outer display versus 110 to 125 MB on a 6 GB phone. An 85 MB baseline plus 25 bytes per display pixel (android:maxMb="85" and android:additionalBytesPerDisplayPixel="25") stays above the measured knee on every tested display (~110 MB on HD+, ~145 MB on FHD+, and ~190 MB on unfolded displays).
  • Verifying ART garbage collection in background states: In cgroup v2, Zygote-shared boot.art and framework image pages remain charged to Zygote's cgroup and are treated as an immune space during ART concurrent mark-compact (CMC) garbage collection. Stress testing confirmed that when a system UI app transitions to background, tightening memory.high (for example, to 30 MB on Traceur) cleanly bounds private heap and page cache without causing GC thrashing or swap activity.

Plot of cold cache reclaim (pgsteal) and working-set thrashing (workingset_refault) as memory.high is stepped downward during a CUJ, highlighting the zero-refault knee and target budget

Figure 6. Finding the zero-refault knee during a CUJ.

Summary of AOSP reference system app budgets

The following table summarizes the calibrated reference budgets in AOSP across RAM tiers and process states:

System package or process Scope ≤8 GB RAM 12 GB RAM ≥16 GB RAM Primary calibration driver
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB Covers P90 lifetime memory.peak bursts (~75 to 110 MB) and >2x P99 active memory.current.
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB Covers active Host Card Emulation P99 (up to ~50 MB) and P90 to P95 memory.peak while trimming cold file cache.
com.android.providers.calendar UID (<application>) 70 MB default (foreground)
50 MB perceptible & background
Active working set is invariant across RAM tiers. The background budget keeps anonymous headroom for reminder alarms while trimming SQLite page cache during syncs.
com.android.traceur UID (<application>) 85 MB default (foreground & perceptible)
30 MB background
Preserves full 85 MB headroom during active trace recording and export (TraceService), while capping idle and broadcast state at 30 MB.
com.android.providers.media.module (main) PID (<process>) 60 MB 75 MB 85 MB Provides ~10 to 30 MB headroom over P99 anon + swap while reclaiming cold SQLite and thumbnail file cache.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 MB + 25 bytes per pixel
(~110 MB on HD+, ~145 MB on FHD+, ~190 MB on ~4.5 MP fold)
Scales proportionally with display pixel count (additionalBytesPerDisplayPixel="25") to match the Glide bitmap working-set knee.
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB Predominantly anonymous memory, sized to P95 lifetime memory.peak to cover captive-portal TLS, DNS, and DHCP bursts.
com.android.appsearch.apk UID (<application>) 130 MB Covers ~100 MB of locked, unevictable memory plus a ~5 MB hot host working set, with headroom for the locked region to grow. Zero refaults measured at ≥125 MB while cold page cache (up to ~330 MB at P99) is reclaimed.
com.android.phone (TeleService) PID (<process>) 120 MB Scoped per-PID on shared UID 1001. Working set tracks device generation rather than RAM, so a single budget sits ~30 MB above the highest per-model P99 anon + swap.

How OEMs can recalibrate system app budgets for their own devices

OEMs that customize system apps (for example, adding vendor Bluetooth profiles, custom telephony extensions, or OEM media indexers) or ship additional preinstalled system services can recalibrate existing budgets or author new *-budget.xml files for their hardware portfolio.

Collect active cgroup v2 memory profiles

For each target system process, read its cgroup v2 memory nodes under /sys/fs/cgroup/uid_<uid>/pid_<pid>/ across representative workloads and RAM tiers:

  • memory.current: Current resident memory charged to the cgroup (excluding Zygote-shared pages)
  • memory.peak: Lifetime peak memory.current high-water mark since process start
  • memory.swap.current: Current compressed swap usage in ZRAM
  • memory.stat: Breakdown of anon, file, pgscan, pgsteal, workingset_refault_anon, and workingset_refault_file
  • memory.events: Cumulative count of high breaches (memory.high throttling and reclaim events)

When aggregating telemetry from dogfood or field devices, separate active process states (TOP, FOREGROUND_SERVICE, active bindings, and active syncs) from idle CACHED or idle PERSISTENT states so idle snapshots don't skew your P95 and P99 working-set calculations downward.

Separate incompressible heap from reclaimable file cache

Compare the 99th percentile of anon + swap against the 99th percentile of memory.current:

  • File-cache-dominated services (file ≫ anon, such as database providers, search indexers, and log daemons): Set android:maxMb to provide comfortable headroom (typically 1.5x to 4x) over P99 anon + swap plus a working window for hot file pages. This configuration lets the kernel continuously evict cold unmapped file pages without touching anonymous heap. If unevictable or file_mapped is large (as with locked memory or memory-mapped databases), count that footprint as working set too.
  • Anonymous-heap-dominated or bursty services (anon makes up most of memory.current, such as networking or connectivity stacks): Size android:maxMb to cover P90 to P95 of memory.peak so transient bursts don't trigger synchronous ZRAM swap-out on latency-sensitive threads.
  • Display-intensive UI processes (such as pickers, galleries, or system UI helpers): Measure the baseline non-UI footprint and the incremental bitmap footprint per megapixel across your lowest- and highest-resolution displays, and express the budget using android:maxMb plus android:additionalBytesPerDisplayPixel.

Sweep memory.high on physical devices

Before committing a new budget to the build, use am memory-limiter manual (or write directly to the process's memory.high cgroup node on a userdebug build) to test candidate limits while running your automated CUJ tests:

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

Monitor memory.stat and memory.events before and after the CUJ:

  • Budget is too low: workingset_refault_anon or workingset_refault_file spikes sharply during the CUJ, memory.swap.current grows continuously during active foreground work, or CUJ frame or Binder latency regresses. Increase the budget past the refault knee.
  • Budget is too high: memory.events:high remains 0 even after heavy file-streaming or indexing workloads, and file in memory.stat grows unboundedly with cold pages. Tighten the budget toward the active working set.
  • Budget is well-calibrated: Steady-state memory.current stays under memory.high during normal operation. During heavy file or cache bursts, pgsteal reclaims cold file pages while workingset_refault_anon remains near zero and CUJ latency is unchanged.