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 (
SystemMemoryBudgetManagerwithmemory.min): Self-tuning cgroup v2memory.minreservations (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 graphicscomposerHAL,audioserver,cameraserver,netd,SystemUI, and the Home launcher) under memory pressure, replacing staticPinnerServicemlockfile pinning. - System app memory ceilings (
<memory-budget>withmemory.high): Workload-calibrated cgroup v2memory.highsoft 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, andTeleService) 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 graphicscomposerHAL,audioserver,cameraserver, andnetd, plus any form-factor-specific native system or vendor daemons listed inconfig_systemMemoryBudgetAdditionalSystemProcesses(for example,android.hardware.perceptionhostandauracamerahalon 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:
- Leaf process node (
pid_<pid>/memory.min): Set to the process's (scaled) 90% working-set floor. - UID node (
uid_<uid>/memory.min): Set to the exact sum of its managedpid_*leaf floors, so unmanaged processes sharing that UID (such as otherAID_SYSTEMdaemons orsharedUserId="android.uid.system"packages) havememory.min = 0and can't claim protection. - Top-level slice (
/sys/fs/cgroup/system/memory.minand/sys/fs/cgroup/apps/memory.min): Set to the exact sum of itsuid_*children. Any stalememory.lowvalues on managed nodes are cleared to0, and anyuid_*directories that were managed on a previous refresh (such as a previous Home launcher after the user switches launchers) are reset to0.
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 undermemory.lowis 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 aMEMCG_LOWevent. In physical-device stress tests with an anonymous memory hog growing to 85% of physical RAM,memory.lowallowedSystemUIon a smartwatch to collapse from ~45 MB down to ~5 to 10 MB andaudioserveron 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 undermemory.minis skipped by global reclaim. As shown in figure 2, under the same 85% RAM pressure workload,memory.minsheds only the coldest 10% tail of each daemon and holdsSystemUI,surfaceflinger, andaudioserverat their 90% floors with zero anonymous refaults and no increase inlmkdprocess 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.
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): SkippingPinnerServicemlockwithout warming the page cache shifts ~95 MB ofSystemUIAPK, ODEX, and VDEX disk reads onto the synchronous boot path whenSystemUIstarts (OnBootPhase_550), adding 300 to 450 ms to boot completion. Callingmadvise(MADV_WILLNEED)isn't sufficient becauseforce_page_cache_rain the kernel clamps a singleMADV_WILLNEEDcall to one readahead window (typically 128 KB to 2 MB). Instead, whenSystemMemoryBudgetManager.shouldSkipDefaultSystemPinningis active,PinnerServicemaps each file inconfig_defaultPinnerServiceFileson a background thread duringonStart, synchronously populates the file into the page cache withOs.mlock, and immediately callsOs.munlock,munmap, andclose. This preserves fast boot timing while leaving zero permanently locked bytes so cold startup pages can be evicted later. - Excluding and reclaiming
system_serverboot package-scan cache: During boot,PackageManagerServicescans installed APKs, leaving 1.5 to 2.5 GB of cold unmapped file page cache charged to thesystem_servercgroup. If included in thememory.mincalculation ofsystem_server, that cold boot cache saturates the 10% global RAM cap and proportionally scales down the protection floors ofsurfaceflinger,SystemUI, andaudioserver. To prevent cap starvation,SystemMemoryBudgetManagerreadsmemory.statforsystem_server, computes unmapped file cache asmax(0, file - shmem - file_mapped), and subtracts that unmapped cache frommemory.currentbefore computing the 90% floor, as shown in figure 3. In addition, 10 seconds afterPHASE_BOOT_COMPLETED, the manager performs a one-shot write of<unmapped_file> swappiness=0to thememory.reclaimnode ofsystem_serveron 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 asandroid14-6.1) that rejectswappiness=0withEINVAL, the manager skips thememory.reclaimwrite rather than falling back to an unqualified reclaim write that would swap out anonymous heap during boot.
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 allmemory.minfloors at 10% of total physical RAM. A positive value sets an explicit cap in KB, and a negative value disablesSystemMemoryBudgetManageron the device (retaining legacyPinnerServicestaticmlockpinning).config_systemMemoryBudgetHighKb(integer, KB):0(default) leaves/sys/fs/cgroup/system/memory.highatmax. A positive value sets a top slicememory.highceiling (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>/commor the basename ofargv[0]in/proc/<pid>/cmdlinefor system and vendor UIDs under10000) to protect with 90%memory.minfloors alongside the default six system daemons. Use this overlay rather than settingmemory.minininit.rcbecauseSystemMemoryBudgetManagerrecomputesuid_<uid>/memory.minas the sum of its managed leaf processes and clears unmanageduid_*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 withMATCH_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, andCalendarProvider) map or stream through large files on disk. Without a cgroupmemory.highceiling, 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
Bluetoothaudio and BLE discovery bursts,NetworkStackcaptive-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.highbudget, 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, orAppSearch). - Process-level (
<processes><process>) budget: Use when the APK sharesandroid:sharedUserId(such asandroid.uid.phonewith UID1001orandroid.uid.systemwith UID1000) 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 backgroundMediaProviderdaemon and the:PhotoPickerUI subprocess).
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.currentinto working set versus reclaimable file cache: Becausememory.highgoverns resident cgroup charge (memory.current), each snapshot was decomposed usingmemory.statinto anonymous heap (anon), swapped anonymous pages (swap), and file-backed page cache (file), as shown in figure 5. Total incompressible anonymous demand (anon + swapat P99) establishes the minimum working-set floor for the budget so the process doesn't risk ZRAM exhaustion, while a large gap betweenmemory.currentP99 andanon + swapP99 indicates file page cache thatmemory.highcan reclaim, provided that cache is unmapped and cold. - Account for memory that
memory.highcan't reclaim: Not all file and shared memory is cold cache. Pages that are locked (unevictableinmemory.stat, such as shared memory locked withmlockor 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. Checkunevictable,shmem, andfile_mappedbefore treating file memory as reclaimable. For example, about 100 MB ofAppSearchmemory 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 outCACHEDsnapshots 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_devas the effective sample size). - Account for transient bursts with
memory.peak: Periodic sampling can miss short-lived, high-allocation bursts (such asNetworkStackcaptive-portal TLS validation and concurrent DNS probes, orBluetoothA2DP and BLE initialization). For bursty services, budgets were sized to cover the 90th to 95th percentile of lifetime kernelmemory.peakhigh-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
maxDeviceRamMbtier, slice the data by device model. For example,com.android.phonereaches the same P99anon + 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.
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.highwas stepped downward while measuring cgroupmemory.statcounters (workingset_refault_anon,workingset_refault_file,pgscan, andpgsteal) andmemory.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
:PhotoPickeracross 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"andandroid: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.artand 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 tobackground, tighteningmemory.high(for example, to 30 MB onTraceur) cleanly bounds private heap and page cache without causing GC thrashing or swap activity.
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 peakmemory.currenthigh-water mark since process startmemory.swap.current: Current compressed swap usage in ZRAMmemory.stat: Breakdown ofanon,file,pgscan,pgsteal,workingset_refault_anon, andworkingset_refault_filememory.events: Cumulative count ofhighbreaches (memory.highthrottling 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): Setandroid:maxMbto provide comfortable headroom (typically 1.5x to 4x) over P99anon + swapplus a working window for hot file pages. This configuration lets the kernel continuously evict cold unmapped file pages without touching anonymous heap. Ifunevictableorfile_mappedis large (as with locked memory or memory-mapped databases), count that footprint as working set too. - Anonymous-heap-dominated or bursty services (
anonmakes up most ofmemory.current, such as networking or connectivity stacks): Sizeandroid:maxMbto cover P90 to P95 ofmemory.peakso 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:maxMbplusandroid: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> 60MBMonitor memory.stat and memory.events before and after the CUJ:
- Budget is too low:
workingset_refault_anonorworkingset_refault_filespikes sharply during the CUJ,memory.swap.currentgrows 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:highremains0even after heavy file-streaming or indexing workloads, andfileinmemory.statgrows unboundedly with cold pages. Tighten the budget toward the active working set. - Budget is well-calibrated: Steady-state
memory.currentstays undermemory.highduring normal operation. During heavy file or cache bursts,pgstealreclaims cold file pages whileworkingset_refault_anonremains near zero and CUJ latency is unchanged.