Budget di memoria di sistema

In Android 17 e versioni successive, la piattaforma gestisce la memoria dei processi di sistema utilizzando due meccanismi cgroup v2 di Linux complementari che funzionano insieme al limitatore di memoria generale dell'applicazione:

  • Soglie di protezione dei daemon di sistema (SystemMemoryBudgetManager con memory.min): le prenotazioni cgroup v2 memory.min con ottimizzazione automatica (impostate al 90% del working set residente stabilito di ogni daemon e limitate a livello globale al 10% della RAM fisica totale) proteggono i daemon interattivi critici e i processi della shell (system_server, surfaceflinger, l'HAL grafico composer, audioserver, cameraserver, netd, SystemUI e l'Avvio app Home) in caso di pressione della memoria, sostituendo il blocco statico dei file PinnerService mlock.
  • Limiti di memoria delle app di sistema (<memory-budget> con memory.high): i limiti flessibili di cgroup v2 memory.high calibrati in base al workload (in genere da 30 MB a 190 MB) dichiarati nei file manifest per le app di sistema persistenti e in background (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch e TeleService) recuperano in modo proattivo la cache delle pagine dei file cold e l'heap anonimo post-burst prima che si verifichi una pressione della memoria globale.

La tabella seguente mette a confronto i due meccanismi:

Dimensione Livelli minimi di protezione dei daemon di sistema Limiti di memoria delle app di sistema
Servizio di piattaforma SystemMemoryBudgetManager MemoryLimiter
Controllo cgroup v2 memory.min hard protection floor Soglia massima di recupero soft di memory.high
Processi target Daemon interattivi con latenza critica e UI della shell (system_server, surfaceflinger, HAL composer, audioserver, cameraserver, netd, SystemUI e l'app Home) App di sistema persistenti e headless, provider di contenuti e servizi in background (come Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch e TeleService)
Modello di dimensionamento Ottimizzazione automatica in fase di esecuzione (90% del working set stabilito, con un limite globale del 10% della RAM totale) senza tabelle di ottimizzazione per dispositivo Attributi XML del file manifest <memory-budget> dichiarativo calibrati in base al livello di RAM, allo stato del processo e al numero di pixel del display
Vantaggio principale Impedisce i rallentamenti della UI interattiva e i blocchi di riattivazione in caso di pressione della memoria durante la sostituzione degli elenchi di file PinnerService mlock statici Rimuove continuamente la cache dei file monouso inattivi e l'heap post-burst per lasciare più RAM fisica alle app in primo piano e memorizzate nella cache

Per ulteriori informazioni sulla sintassi <memory-budget>, sugli attributi XML, sulle API di runtime e sul conteggio della memoria del sistema operativo, consulta Imposta i budget di memoria delle app e Informazioni e risoluzione dei problemi relativi alla memoria Android.

Proteggi i daemon interattivi critici con SystemMemoryBudgetManager

SystemMemoryBudgetManager mantiene i processi che si trovano direttamente nel percorso interattivo dell'utente residenti nella RAM fisica durante la pressione della memoria. Anziché bloccare un elenco statico di file APK, ODEX e VDEX in memoria con mlock (come fa PinnerService per config_defaultPinnerServiceFiles e config_pinnerHomePinBytes), SystemMemoryBudgetManager assegna valori minimi dinamici di cgroup v2 memory.min ai seguenti processi:

  • Sezione del sistema di base (/sys/fs/cgroup/system): system_server, surfaceflinger, l'HAL composer grafico, audioserver, cameraserver e netd, oltre a eventuali daemon di sistema o fornitore nativi specifici del fattore di forma elencati in config_systemMemoryBudgetAdditionalSystemProcesses (ad esempio, android.hardware.perceptionhost e auracamerahal sui dispositivi XR).
  • Sezione dell'app interattiva (/sys/fs/cgroup/apps): il processo principale del pacchetto SystemUI configurato (config_systemUi) e il processo principale del titolare del ruolo Home dell'utente corrente.

La protezione del titolare del ruolo Home insieme a config_systemUi garantisce la copertura su tutti i fattori di forma. Sui fattori di forma in cui config_systemUi è un pacchetto di compatibilità leggero (ad esempio Wear OS, in cui il quadrante, le notifiche e i riquadri vengono eseguiti all'interno dell'app Home), la shell rivolta agli utenti riceve comunque una protezione completa del working set. Se l'intent Home viene risolto nel pacchetto config_systemUi, il processo viene protetto una volta. Solo il processo principale di ogni pacchetto (processName == packageName) riceve un livello di protezione, quindi i sottoprocessi ausiliari (come :screenshot) non consumano il budget di protezione.

Policy di ottimizzazione automatica e delega dei cgroup a tre livelli

Poiché il set di lavoro di un daemon interattivo varia notevolmente in base ai fattori di forma e alle capacità della RAM (ad esempio, SystemUI utilizza circa 45 MB su uno smartwatch, 60 MB su uno smartphone da 6 GB e 180 MB su uno smartphone da 16 GB), le tabelle di ricerca statiche per dispositivo sono fragili. Invece, SystemMemoryBudgetManager ricava i floor direttamente dal footprint residente di ogni processo utilizzando tre costanti:

Parametro della policy Valore predefinito Comportamento
Prezzo minimo per processo 90% di memory.current liquidato meno la cache dei file non mappati Protegge heap anonimo, slab e codice mappato caldo, lasciando recuperabile la coda del 10% più fredda, in modo che le pagine veramente fredde possano comunque essere rimosse. La cache dei file non mappati (max(0, file - shmem - file_mapped) da memory.stat) viene sottratta prima di applicare il rapporto del 90%, in modo che la cache di scansione del pacchetto di avvio o le letture degli asset una tantum non aumentino i valori minimi.
Limite di RAM globale 10% della RAM totale (config_systemMemoryBudgetLowKb) Corrisponde alla regola di protezione config_pinnerMaxPinnedMemoryPercentage PinnerService. Se la somma dei prezzi minimi del 90% supera il 10% di MemTotal, ogni prezzo minimo viene ridotto proporzionalmente dello stesso fattore.
Cadenza di aggiornamento 1, 4, 16 e 64 secondi dopo l'avvio, poi ogni 30 minuti Una serie di assestamento geometrico converge sul working set post-avvio di ogni daemon senza impostare come hardcoded i ritardi di riscaldamento per dispositivo, mentre un aggiornamento dello stato stabile di 30 minuti monitora la crescita e la riduzione graduali del working set nel corso del ciclo di vita del dispositivo.

Nel controller di memoria cgroup v2 di Linux (mm/memcontrol.c:effective_protection), la protezione memory.min effettiva di un cgroup foglia è limitata dal valore memory.min configurato nei cgroup padre e predecessore. L'impostazione di memory.min solo su una directory pid_<pid> foglia mentre uid_<uid>/memory.min è 0 non offre alcuna protezione effettiva. Al contrario, l'impostazione di un valore memory.min fisso solo sulla sezione /sys/fs/cgroup/system superiore distribuisce la protezione non rivendicata tra tutti gli UID secondari e i processi in modo proporzionale (memory_recursiveprot), consentendo ai processi diagnostici o secondari che condividono uid_1000 (android.uid.system) di sottrarre la protezione da surfaceflinger e audioserver.

Per fornire un isolamento esatto per daemon, SystemMemoryBudgetManager scrive la protezione su tutti e tre i livelli della gerarchia cgroup v2 a ogni aggiornamento, come mostrato nella figura 1:

  1. Nodo del processo foglia (pid_<pid>/memory.min): impostato sul limite minimo del working set del 90% (scalato) del processo.
  2. Nodo UID (uid_<uid>/memory.min): impostato sulla somma esatta dei suoi floor foglia gestiti pid_*, in modo che i processi non gestiti che condividono l'UID (come altri daemon AID_SYSTEM o pacchetti sharedUserId="android.uid.system") abbiano memory.min = 0 e non possano richiedere la protezione.
  3. Sezione di primo livello (/sys/fs/cgroup/system/memory.min e /sys/fs/cgroup/apps/memory.min): impostata sulla somma esatta delle sezioni secondarie uid_*. Tutti i valori memory.low obsoleti sui nodi gestiti vengono cancellati e impostati su 0, e tutte le directory uid_* gestite in un aggiornamento precedente (ad esempio un Avvio app Home precedente dopo che l'utente cambia Avvio app) vengono reimpostate su 0.

Gerarchia di protezione memory.min di cgroup v2 a tre livelli in /sys/fs/cgroup/system e /sys/fs/cgroup/apps, che mostra i livelli minimi dei processi foglia sommati nei nodi UID e slice, mentre i processi di pari livello non protetti rimangono a zero

Figura 1. Delega del livello di protezione a tre livelli tra le sezioni cgroup.

Perché viene utilizzato memory.min anziché memory.low sui kernel MGLRU

In LRU multigenerazionale (MGLRU, il motore di recupero delle pagine predefinito sui kernel Android 6.6 e versioni successive), memory.low e memory.min si comportano in modo diverso in mm/vmscan.c:shrink_one:

  • memory.low (rinvio a passaggio singolo): un cgroup il cui utilizzo è inferiore a memory.low viene ruotato alla fine della LRU memcg (MEMCG_LRU_TAIL) e ignorato per un singolo passaggio di recupero. Al passaggio successivo, in caso di pressione della memoria prolungata, il kernel recupera completamente il cgroup e registra un evento MEMCG_LOW. Negli stress test sui dispositivi fisici con un consumo di memoria anonimo che cresce fino all'85% della RAM fisica, memory.low ha consentito a SystemUI su uno smartwatch di scendere da circa 45 MB a circa 5-10 MB e a audioserver su un pieghevole da 16 GB di scendere da 15 MB a 2 MB rispetto a un minimo di circa 14 MB.
  • memory.min (hard protection floor): un cgroup il cui utilizzo è inferiore a memory.min viene ignorato dal recupero globale. Come mostrato nella figura 2, con lo stesso workload con pressione della RAM dell'85%, memory.min elimina solo la coda del 10% più fredda di ogni daemon e mantiene SystemUI, surfaceflinger e audioserver ai loro livelli minimi del 90% con zero refault anonimi e nessun aumento delle terminazioni dei processi lmkd.

memory.min fornisce una prenotazione fisica rigida che corrisponde alla protezione del pinning PinnerService mlock che sostituisce, con il vantaggio che l'LRU del kernel (anziché un elenco di file statico) sceglie quali pagine di codice hot, heap e grafica rimangono residenti.

Confronto della memoria residente del daemon con l'aumento della pressione della memoria di sistema dal 40% al 95% della RAM fisica, che mostra che memory.min rimane al suo livello minimo del 90% mentre memory.low si arresta in modo anomalo dopo un singolo passaggio di rinvio di MGLRU

Figura 2. Conservazione del working set del daemon in caso di aumento della pressione della memoria.

Preriscaldamento all'avvio e recupero della cache dei file non mappati dopo l'avvio

La sostituzione della mlock permanente PinnerService di config_defaultPinnerServiceFiles richiede il coordinamento di due comportamenti all'avvio:

  • Preriscaldamento dell'avvio in background una tantum (PinnerService): saltare PinnerService mlock senza riscaldare la cache della pagina sposta circa 95 MB di letture del disco APK, ODEX e VDEX di SystemUI nel percorso di avvio sincrono all'avvio di SystemUI (OnBootPhase_550), aggiungendo da 300 a 450 ms al completamento dell'avvio. La chiamata di madvise(MADV_WILLNEED) non è sufficiente perché force_page_cache_ra nel kernel blocca una singola chiamata MADV_WILLNEED a una finestra di lettura anticipata (in genere da 128 KB a 2 MB). Invece, quando SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning è attivo, PinnerService mappa ogni file in config_defaultPinnerServiceFiles su un thread in background durante onStart, compila in modo sincrono il file nella cache della pagina con Os.mlock e chiama immediatamente Os.munlock, munmap e close. In questo modo, i tempi di avvio rapido vengono preservati, lasciando zero byte bloccati in modo permanente, così le pagine di avvio a freddo possono essere rimosse in un secondo momento.
  • Esclusione e recupero della cache di scansione dei pacchetti di avvio di system_server:durante l'avvio, PackageManagerService scansiona gli APK installati, lasciando da 1,5 a 2,5 GB di cache delle pagine dei file non mappati a freddo addebitati al cgroup system_server. Se inclusa nel calcolo di memory.min di system_server, la cache di avvio a freddo satura il limite globale del 10% della RAM e riduce proporzionalmente i floor di protezione di surfaceflinger, SystemUI e audioserver. Per evitare la carenza di capacità, SystemMemoryBudgetManager legge memory.stat per system_server, calcola la cache dei file non mappati come max(0, file - shmem - file_mapped) e sottrae la cache non mappata da memory.current prima di calcolare il limite minimo del 90%, come mostrato nella figura 3. Inoltre, 10 secondi dopo PHASE_BOOT_COMPLETED, il gestore esegue una scrittura una tantum di <unmapped_file> swappiness=0 nel nodo memory.reclaim di system_server sui kernel Linux 6.2 e versioni successive per rimuovere in modo proattivo la cache di scansione dei pacchetti inattivi senza scambiare l'heap anonimo o rimuovere le pagine DEX e di codice mappate. Sui kernel meno recenti (come android14-6.1) che rifiutano swappiness=0 con EINVAL, il gestore salta la scrittura memory.reclaim anziché eseguire il fallback a una scrittura di recupero non qualificata che scambierebbe l'heap anonimo durante l'avvio.

Cronologia della memoria system_server dopo l'avvio che mostra gli aggiornamenti di stabilizzazione geometrica a 1, 4, 16 e 64 secondi, l'esclusione della cache di scansione dei pacchetti di avvio non mappati dal limite minimo del 90% di memory.min e memory.reclaim one-shot con swappiness=0

Figura 3. Convergenza della soglia di protezione e recupero della cache dopo l'avvio.

Requisiti del kernel e overlay di configurazione dispositivo

SystemMemoryBudgetManager si basa su mem_cgroup_calculate_protection nel kernel Linux per propagare memory.min tra i cgroup nidificati. I kernel comuni di Android android15-6.6 e android16-6.12 includono il supporto completo della protezione nidificata MGLRU integrato. I dispositivi con android14-6.1 con MGLRU attivato richiedono tre backport upstream in mm/vmscan.c e mm/memcontrol.c in modo che le sezioni intermedie vuote e i cgroup UID propaghino la protezione ai cgroup dei processi foglia:

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

Gli OEM possono personalizzare o disattivare SystemMemoryBudgetManager utilizzando i seguenti overlay delle risorse del framework:

  • config_systemMemoryBudgetLowKb (numero intero, kB): 0 (predefinito) limita la somma di tutti i floor memory.min al 10% della RAM fisica totale. Un valore positivo imposta un limite esplicito in kB, mentre un valore negativo disattiva SystemMemoryBudgetManager sul dispositivo (mantenendo il blocco statico mlock di PinnerService precedente).
  • config_systemMemoryBudgetHighKb (numero intero, kB): 0 (valore predefinito) lascia /sys/fs/cgroup/system/memory.high a max. Un valore positivo imposta un limite massimo per la sezione superiore memory.high (deve essere maggiore o uguale al limite minimo).
  • config_systemMemoryBudgetAdditionalSystemProcesses (string-array): elenca nomi di processi daemon di sistema o del fornitore nativi aggiuntivi con latenza critica in /sys/fs/cgroup/system (confrontati con /proc/<pid>/comm o il nome di base di argv[0] in /proc/<pid>/cmdline per gli UID di sistema e del fornitore in 10000) da proteggere con soglie minime del 90% di memory.min insieme ai sei daemon di sistema predefiniti. Utilizza questo overlay anziché impostare memory.min in init.rc perché SystemMemoryBudgetManager ricalcola uid_<uid>/memory.min come somma dei processi foglia gestiti e cancella i nodi uid_* non gestiti a ogni aggiornamento. Ad esempio, un dispositivo XR può proteggere i daemon HAL di tracciamento spaziale e della fotocamera con il seguente overlay:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (stringa): nome del pacchetto dell'app SystemUI della piattaforma, risolto con MATCH_SYSTEM_ONLY.

Per ispezionare i floor SystemMemoryBudgetManager e i nodi cgroup attivi su un dispositivo, esegui:

# 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

Associa app di sistema persistenti e in background con budget di memoria

Sebbene SystemMemoryBudgetManager protegga i daemon interattivi dal recupero utilizzando i livelli memory.min, molte altre app di sistema e servizi della piattaforma con privilegi vengono eseguiti in background per lunghi periodi come processi PERSISTENT, servizi in primo piano associati o provider di contenuti sottoposti a query frequenti. Senza limiti di memoria espliciti, questi processi non vengono limitati finché non raggiungono i limiti di Memory Limiter di più gigabyte (ad esempio da 4 GB a 10 GB per le app visibili e da 2 GB a 5 GB per le app in background) o finché la pressione della memoria globale non attiva l'attività di kswapd e del killer di memoria insufficiente (lmkd) a livello di sistema.

Se non vengono limitati, i processi di sistema in background spesso accumulano grandi quantità di memoria cold che non fa parte del loro set di lavoro attivo:

  • Cache delle pagine dei file illimitata:i servizi che indicizzano i contenuti multimediali, eseguono ricerche nei database o sincronizzano le tabelle di calendario e provider (come AppSearch, MediaProvider e CalendarProvider) mappano o trasmettono in streaming file di grandi dimensioni sul disco. Senza un limite massimo di cgroup memory.high, le pagine pulite con backup su file provenienti da letture una tantum rimangono addebitate al cgroup del processo a tempo indeterminato, consumando centinaia di megabyte di RAM fisica.
  • Conservazione dell'heap anonimo post-burst:i servizi che subiscono picchi di attività temporanei, come i burst di rilevamento audio e BLE di Bluetooth, la convalida TLS del captive portal di NetworkStack o gli helper dell'interfaccia utente che passano in background, possono conservare le pagine dell'heap anonimo inattive molto tempo dopo il completamento del burst.

L'applicazione di dichiarazioni <memory-budget> calibrate (limiti flessibili memory.high, in genere da 30 MB a 190 MB) alle app di sistema offre due vantaggi principali:

  • Recupero proattivo e localizzato delle pagine:quando un processo di sistema supera il budget memory.high configurato, il kernel Linux indirizza il recupero delle pagine specificamente a quel cgroup, rimuovendo prima le pagine cold supportate da file e comprimendo le pagine anonime inattive in ZRAM senza attendere la pressione della memoria globale del dispositivo.
  • Più RAM disponibile per le app in primo piano e il pool di app memorizzate nella cache: la limitazione delle cache dei file cold nei servizi di sistema sempre attivi recupera da decine a centinaia di megabyte di RAM fisica sul dispositivo, riducendo i problemi di UI, la pressione di avvio della fotocamera e mantenendo attive più app utente nello stato memorizzato nella cache.

Configura i budget di memoria delle app di sistema

Le app di sistema utilizzano la stessa sintassi del manifest <memory-budget>, la stessa cascata dello stato del processo, la stessa scalabilità della densità di visualizzazione e la stessa regola di risoluzione dell'ultima clausola corrispondente delle app generali. Per l'elenco completo degli attributi, consulta il riferimento agli attributi XML.

Gli OEM possono dichiarare gli elementi <memory-budget> direttamente all'interno del file AndroidManifest.xml di un'app di sistema o unirli da un file XML separato utilizzando la proprietà additional_manifests in Android.bp. Per convenzione, AOSP archivia i budget delle app di sistema di riferimento in file *-budget.xml autonomi (in frameworks/base/core/memory_budgets/ per i pacchetti della piattaforma principale o insieme a singoli moduli come packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ e packages/services/Telephony/) e li allega con additional_manifests:

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

Ambito a livello di pacchetto (UID) rispetto all'ambito a livello di processo (PID)

La dichiarazione di un budget di base a livello di pacchetto (<application>) o di un budget multiprocesso (<processes><process>) determina se il kernel applica memory.high a livello di cgroup UID (/sys/fs/cgroup/uid_<uid>/memory.high) o a livello di cgroup per PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), come mostrato nella figura 4:

  • Budget a livello di pacchetto (<application>):da utilizzare quando l'APK esegue un singolo processo con il proprio UID dedicato (ad esempio Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur o AppSearch).
  • Budget a livello di processo (<processes><process>): utilizza questa opzione quando l'APK condivide android:sharedUserId (ad esempio android.uid.phone con UID 1001 o android.uid.system con UID 1000) con altri pacchetti di sistema, in modo che altri processi che condividono l'UID non vengano limitati insieme o quando un singolo APK esegue più processi con ruoli di memoria distinti (ad esempio, il daemon MediaProvider in background e il sottoprocesso UI :PhotoPicker).

Gerarchia di Cgroup v2 che confronta l'applicazione di memory.high a livello di UID per gli UID dedicati con l'applicazione di memory.high per PID per gli UID di sistema condivisi e gli APK multiprocesso

Figura 4. Limiti di memoria a livello di UID e di PID.

L'esempio seguente mostra MediaProvider-budget.xml, che limita i budget con livelli di RAM al processo MediaProvider predefinito (<process>) e un budget con scalabilità del display al processo UI :PhotoPicker. Ogni attributo android:maxDeviceRamMb corrisponde ai dispositivi la cui RAM fisica totale (MemTotal) è pari o inferiore alla soglia di MB specificata (o si applica a qualsiasi capacità della RAM se omessa). Le clausole sono ordinate dal valore predefinito non vincolato fino alla soglia maxDeviceRamMb più piccola, in modo che la regola di risoluzione dell'ultima clausola corrispondente selezioni il livello applicabile più restrittivo:

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

Come sono stati calibrati i budget di riferimento AOSP sui dispositivi Google Pixel

I budget di memoria delle app di sistema di riferimento in AOSP sono stati calibrati su tutti i dispositivi Google Pixel con una RAM fisica compresa tra 6 GB e 16 GB (inclusi smartphone, pieghevoli e tablet da 6 GB, 8 GB, 12 GB e 16 GB) e risoluzioni del display comprese tra ~1, 2 MP (720 x 1600) e ~4, 5 MP (2076 x 2152).

La calibrazione ha combinato due origini dati complementari: la telemetria dei campi cgroup v2 a livello di parco risorse e le scansioni memory.high dei dispositivi fisici controllati nei Critical User Journey (CUJ).

Analisi della telemetria di cgroup v2 su tutto il parco risorse

Sono stati raccolti snapshot periodici di cgroup v2 nel parco dispositivi e suddivisi per livello di RAM fisica (≤8 GB, 12 GB e ≥16 GB) e bucket di stato del processo (foreground, perceptible, background e cached). L'analisi statistica è stata guidata da sei principi:

  • Scomposizione di memory.current in working set e cache dei file recuperabile: poiché memory.high regola l'addebito del cgroup residente (memory.current), ogni snapshot è stato scomposto utilizzando memory.stat in heap anonimo (anon), pagine anonime scambiate (swap) e cache delle pagine con backup su file (file), come mostrato nella figura 5. La domanda anonima incomprimibile totale (anon + swap a P99) stabilisce il limite minimo del working set per il budget, in modo che il processo non rischi l'esaurimento della ZRAM, mentre un ampio divario tra memory.current P99 e anon + swap P99 indica una cache delle pagine dei file che memory.high può recuperare, a condizione che la cache non sia mappata e sia inattiva.
  • Tieni conto della memoria che memory.high non può recuperare: non tutta la memoria dei file e condivisa è cache cold. Le pagine bloccate (unevictable in memory.stat, ad esempio la memoria condivisa bloccata con mlock o la RAM guest bloccata di una macchina virtuale i cui processi host vengono addebitati al cgroup dell'app) impostano un limite minimo per il budget e le pagine mappate (file_mapped) fanno spesso parte del set di lavoro attivo. Controlla unevictable, shmem e file_mapped prima di considerare la memoria dei file come recuperabile. Ad esempio, circa 100 MB di memoria AppSearch sono bloccati e non possono essere rimossi, quindi il budget copre questo limite minimo più il working set dell'host attivo, con spazio per la crescita della regione bloccata.
  • Filtra la diluizione dello stato di inattività:i provider di contenuti (CalendarProvider) e gli strumenti per sviluppatori on demand (Traceur) trascorrono dal 70% al 99,9% degli snapshot sul campo in stato di inattività con un consumo di circa 1-3 MB. Il calcolo dei percentili non elaborati in tutti gli snapshot diluisce notevolmente le stime P95 e P99. Invece, i percentili e gli intervalli di affidabilità sono stati calcolati su snapshot attivi non diluiti (filtrando gli snapshot CACHED e gli snapshot di riferimento inattivi).
  • Utilizza intervalli di affidabilità raggruppati per dispositivo:poiché un singolo dispositivo contribuisce con molti snapshot periodici nel tempo, trattare gli snapshot come campioni indipendenti sottovaluta l'incertezza della coda. Tutte le stime del working set P95 e P99 sono state valutate utilizzando intervalli di affidabilità conservativi del 95% raggruppati per dispositivo (utilizzando il numero di dispositivi distinti N_dev come dimensione effettiva del campione).
  • Considera i burst temporanei con memory.peak: il campionamento periodico può non rilevare burst di breve durata con allocazione elevata (come la convalida TLS del captive portal NetworkStack e i probe DNS simultanei o l'inizializzazione di Bluetooth A2DP e BLE). Per i servizi con burst, i budget sono stati dimensionati per coprire il 90° e il 95° percentile dei valori massimi del kernel memory.peak nel corso del ciclo di vita nei livelli di RAM.
  • Verifica che un livello di RAM rifletta la RAM, non la generazione del dispositivo:i dispositivi più recenti tendono ad avere più RAM e più funzionalità, quindi un working set che cresce con il livello di RAM potrebbe in realtà crescere con le funzionalità. Prima di aggiungere un livello maxDeviceRamMb, suddividi i dati per modello di dispositivo. Ad esempio, com.android.phone raggiunge lo stesso P99 anon + swap (~85-90 MB) su smartphone da 12 GB e 16 GB della stessa generazione, mentre un pieghevole da 12 GB più vecchio utilizza 40 MB. Il suo budget utilizza quindi un unico valore senza livello di RAM, in modo che un dispositivo con meno RAM che include le nuove funzionalità di telefonia non abbia un budget insufficiente.

Scomposizione della memoria cgroup nel tempo in working set anonimo incomprimibile, finestra della cache dei file attiva e cache dei file cold una tantum recuperata oltre memory.high

Figura 5. Working set rispetto alla cache dei file recuperabili.

CUJ del dispositivo fisico e sweep di scalabilità del display

Per convalidare i budget derivati dalla telemetria e calibrare i budget proporzionali al display (come :PhotoPicker) e i budget in background specifici per lo stato (come Traceur), sono state eseguite scansioni memory.high in tempo reale su dispositivi fisici con RAM da 6 GB a 16 GB:

  • Individuazione del punto di flesso con zero errori di ricaricamento: durante l'esecuzione di CUJ end-to-end (come lo scorrimento rapido della griglia di foto, la navigazione negli album, lo scorrimento dell'anteprima di foto e video ad alta risoluzione a schermo intero e la registrazione e il salvataggio delle tracce), memory.high è stato ridotto mentre venivano misurati i contatori cgroup memory.stat (workingset_refault_anon, workingset_refault_file, pgscan e pgsteal) e memory.events (high). Come illustrato nella figura 6, il budget ottimale si trova appena sopra il punto di flesso con zero errori di ricaricamento. Il punto di flesso zero-refault è la soglia in cui il kernel recupera la cache dei file cold monouso (pgsteal > 0) senza attivare il thrashing del working set attivo (workingset_refault == 0).
  • Convalida della scalabilità dei pixel del display tra le risoluzioni: i test di :PhotoPicker su quattro risoluzioni del display fisiche ed emulate da ~1,2 MP (720 x 1600) a ~4,5 MP (2076 x 2152 con display interno aperto) hanno confermato che il set di lavoro bitmap attivo di Glide e ViewPager2 scala in modo lineare con il numero di pixel del display. Anche la base di riferimento non display varia in base al dispositivo: con un numero di pixel quasi identico, il punto di flesso di zero refault era di circa 140 MB sul display esterno di un pieghevole da 16 GB rispetto a 110-125 MB su uno smartphone da 6 GB. Una base di riferimento di 85 MB più 25 byte per pixel del display (android:maxMb="85" e android:additionalBytesPerDisplayPixel="25") rimane al di sopra del punto di flesso misurato su ogni display testato (~110 MB su HD+, ~145 MB su FHD+ e ~190 MB su display aperti).
  • Verifica della garbage collection di ART negli stati in background: in cgroup v2, le pagine di immagini del framework e boot.art condivise da Zygote rimangono addebitate al cgroup di Zygote e vengono trattate come uno spazio immune durante la garbage collection di ART Concurrent Mark-Compact (CMC). Gli stress test hanno confermato che quando un'app della UI di sistema passa a background, l'applicazione di memory.high più restrittivi (ad esempio, a 30 MB su Traceur) limita in modo pulito l'heap privato e la cache delle pagine senza causare thrashing del GC o attività di swap.

Grafico del recupero della cache cold (pgsteal) e del thrashing del working set (workingset_refault) mentre memory.high viene ridotto durante un CUJ, evidenziando il punto di flesso con zero refault e il budget prefissato

Figura 6. Individuazione del punto di flesso con zero errori di riferimento durante un percorso utente critico.

Riepilogo dei budget delle app di sistema di riferimento AOSP

La tabella seguente riassume i budget di riferimento calibrati in AOSP per i livelli di RAM e gli stati dei processi:

Pacchetto o processo di sistema Ambito ≤8 GB di RAM 12 GB di RAM ≥16 GB di RAM Driver di calibrazione principale
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB Copre i burst memory.peak di durata P90 (~75-110 MB) e >2x memory.current attivi P99.
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB Copre l'Host Card Emulation attiva P99 (fino a ~50 MB) e da P90 a P95 memory.peak durante il trimming della cache dei file cold.
com.android.providers.calendar UID (<application>) 70 MB predefinito (foreground)
50 MB perceptible e background
Il set di lavoro attivo è invariante tra i livelli di RAM. Il budget in background mantiene un margine anonimo per le sveglie dei promemoria, riducendo al contempo la cache della pagina SQLite durante le sincronizzazioni.
com.android.traceur UID (<application>) 85 MB predefiniti (foreground e percepibile)
30 MB background
Mantiene l'headroom completo di 85 MB durante la registrazione e l'esportazione delle tracce attive (TraceService), limitando al contempo lo stato di inattività e di trasmissione a 30 MB.
com.android.providers.media.module (principale) PID (<process>) 60 MB 75 MB 85 MB Fornisce circa 10-30 MB di margine rispetto al P99 anon + swap, recuperando al contempo la cache dei file di miniature e SQLite inattivi.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 MB + 25 byte per pixel
(~110 MB su HD+, ~145 MB su FHD+, ~190 MB su pieghevole da ~4,5 MP)
Scala proporzionalmente al numero di pixel del display (additionalBytesPerDisplayPixel="25") per corrispondere al punto di flesso del working set della bitmap Glide.
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB Memoria prevalentemente anonima, dimensionata in base alla durata P95 memory.peak per coprire i burst TLS, DNS e DHCP del captive portal.
com.android.appsearch.apk UID (<application>) 130 MB Copre circa 100 MB di memoria bloccata e non espellibile, oltre a un working set di host hot di circa 5 MB, con spazio per l'espansione della regione bloccata. Zero errori di pagina misurati a ≥125 MB mentre viene recuperata la cache di pagina cold (fino a ~330 MB a P99).
com.android.phone (TeleService) PID (<process>) 120 MB Ambito per PID su UID condiviso 1001. Il working set monitora la generazione del dispositivo anziché la RAM, quindi un singolo budget è di circa 30 MB superiore al P99 anon + swap più alto per modello.

Come gli OEM possono ricalibrare i budget delle app di sistema per i propri dispositivi

Gli OEM che personalizzano le app di sistema (ad esempio, aggiungendo profili Bluetooth del fornitore, estensioni di telefonia personalizzate o indicizzatori multimediali OEM) o che forniscono servizi di sistema preinstallati aggiuntivi possono ricalibrare i budget esistenti o creare nuovi file *-budget.xml per il loro portafoglio hardware.

Raccogli i profili di memoria cgroup v2 attivi

Per ogni processo del sistema di destinazione, leggi i nodi di memoria cgroup v2 in /sys/fs/cgroup/uid_<uid>/pid_<pid>/ tra i workload e i livelli di RAM rappresentativi:

  • memory.current: memoria residente corrente addebitata al cgroup (escluse le pagine condivise da Zygote)
  • memory.peak: picco massimo di memory.current dall'avvio del processo
  • memory.swap.current: utilizzo attuale dello swap compresso in ZRAM
  • memory.stat: suddivisione di anon, file, pgscan, pgsteal, workingset_refault_anon e workingset_refault_file
  • memory.events: conteggio cumulativo delle violazioni di high (eventi di limitazione e recupero di memory.high)

Quando aggreghi la telemetria dai dispositivi dogfood o sul campo, separa gli stati dei processi attivi (TOP, FOREGROUND_SERVICE, associazioni attive e sincronizzazioni attive) dagli stati di inattività CACHED o PERSISTENT, in modo che gli snapshot di inattività non alterino i calcoli del working set P95 e P99 verso il basso.

Separa l'heap non comprimibile dalla cache dei file recuperabili

Confronta il 99° percentile di anon + swap con il 99° percentile di memory.current:

  • Servizi dominati dalla cache dei file (file &gg; anon, come i provider di database, gli indicizzatori dell'indice della Ricerca e i daemon di log): imposta android:maxMb in modo da fornire un margine confortevole (in genere da 1, 5x a 4x) rispetto a anon + swap P99 più una finestra di lavoro per le pagine dei file attivi. Questa configurazione consente al kernel di rimuovere continuamente le pagine di file non mappate inattive senza toccare l'heap anonimo. Se unevictable o file_mapped è di grandi dimensioni (come nel caso della memoria bloccata o dei database mappati in memoria), conteggia anche questo footprint come working set.
  • Servizi dominati da heap anonimo o con burst (anon costituisce la maggior parte di memory.current, ad esempio stack di networking o connettività): dimensiona android:maxMb per coprire da P90 a P95 di memory.peak in modo che i burst temporanei non attivino lo swap-out sincrono di ZRAM su thread sensibili alla latenza.
  • Processi UI ad alta intensità di visualizzazione (come selettori, gallerie o helper UI di sistema): misura l'impatto di base non UI e l'impatto incrementale della bitmap per megapixel sui display con risoluzione più bassa e più alta ed esprimi il budget utilizzando android:maxMb più android:additionalBytesPerDisplayPixel.

Sweep memory.high sui dispositivi fisici

Prima di assegnare un nuovo budget alla build, utilizza am memory-limiter manual (o scrivi direttamente nel nodo cgroup memory.high del processo in una build userdebug) per testare i limiti candidati durante l'esecuzione dei test CUJ automatici:

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

Monitora memory.stat e memory.events prima e dopo il CUJ:

  • Il budget è troppo basso: workingset_refault_anon o workingset_refault_file aumenta notevolmente durante il CUJ, memory.swap.current cresce continuamente durante il lavoro attivo in primo piano o la latenza del frame CUJ o di Binder regredisce. Aumenta il budget oltre il punto di flesso del refault.
  • Il budget è troppo alto: memory.events:high rimane 0 anche dopo carichi di lavoro intensi di streaming o indicizzazione dei file e file in memory.stat cresce senza limiti con le pagine cold. Limita il budget al set di lavoro attivo.
  • Il budget è ben calibrato: l'memory.current in stato stabile rimane al di sotto di memory.high durante il normale funzionamento. Durante i burst di file o cache pesanti, pgsteal recupera le pagine dei file cold, mentre workingset_refault_anon rimane vicino allo zero e la latenza CUJ rimane invariata.