Presupuestos de memoria del sistema

En Android 17 y versiones posteriores, la plataforma rige la memoria de los procesos del sistema con dos mecanismos complementarios de cgroup v2 de Linux que funcionan junto con el limitador de memoria general de la aplicación:

  • Límites mínimos de protección de daemons del sistema (SystemMemoryBudgetManager con memory.min): Las reservas de memory.min de cgroup v2 con ajuste automático (establecidas en el 90% del conjunto de trabajo residente establecido de cada daemon y limitadas de forma global al 10% de la RAM física total) protegen los daemons interactivos críticos y los procesos de shell (system_server, surfaceflinger, la HAL de gráficos composer, audioserver, cameraserver, netd, SystemUI y el selector de la pantalla principal) bajo presión de memoria, lo que reemplaza la fijación estática de archivos PinnerService mlock.
  • Límites de memoria de las apps del sistema (<memory-budget> con memory.high): Los límites flexibles de cgroup v2 memory.high calibrados para la carga de trabajo (por lo general, de 30 MB a 190 MB) declarados en los archivos de manifiesto para las apps del sistema persistentes y en segundo plano (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch y TeleService) recuperan de forma proactiva la caché de páginas de archivos inactivos y el montón anónimo posterior a la ráfaga antes de que se produzca una presión de memoria global.

En la siguiente tabla, se comparan los dos mecanismos:

Dimensión Límites de protección del daemon del sistema Límites de memoria de las apps del sistema
Servicio de plataforma SystemMemoryBudgetManager MemoryLimiter
Control de cgroup v2 Piso de protección rígido de memory.min Límite de recuperación parcial de memory.high
Procesos de destino Daemons interactivos con latencia crítica y la IU de shell (system_server, surfaceflinger, HAL de composer, audioserver, cameraserver, netd, SystemUI y la app de Home) Apps del sistema persistentes y sin interfaz gráfica, proveedores de contenido y servicios en segundo plano (como Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch y TeleService)
Modelo de dimensionamiento Ajuste automático en el tiempo de ejecución (90% del conjunto de trabajo establecido, con un límite global del 10% de la RAM total) sin tablas de ajuste por dispositivo Atributos XML declarativos del manifiesto de <memory-budget> calibrados por nivel de RAM, estado del proceso y recuento de píxeles de la pantalla
Beneficio principal Evita los tirones de la IU interactiva y las detenciones de activación cuando hay presión de memoria mientras se reemplazan las listas de archivos estáticos PinnerService mlock Expulsa continuamente la caché de archivos de un solo uso inactivos y el montón posterior a la ráfaga para dejar más RAM física para las apps en primer plano y almacenadas en caché

Para obtener más información sobre la sintaxis de <memory-budget>, los atributos XML, las APIs de entorno de ejecución y la contabilización de la memoria del SO, consulta Cómo establecer presupuestos de memoria de la app y Cómo comprender y solucionar problemas de la memoria de Android.

Protege los daemons interactivos críticos con SystemMemoryBudgetManager

SystemMemoryBudgetManager mantiene los procesos que se encuentran directamente en la ruta interactiva del usuario residentes en la RAM física durante la presión de memoria. En lugar de fijar una lista estática de archivos APK, ODEX y VDEX en la memoria con mlock (como lo hace PinnerService para config_defaultPinnerServiceFiles y config_pinnerHomePinBytes), SystemMemoryBudgetManager asigna límites mínimos de memory.min de cgroup v2 dinámicos a los siguientes procesos:

  • Porción del sistema principal (/sys/fs/cgroup/system): system_server, surfaceflinger, la HAL de gráficos composer, audioserver, cameraserver y netd, además de cualquier daemon del sistema nativo o del proveedor específico del factor de forma que se indique en config_systemMemoryBudgetAdditionalSystemProcesses (por ejemplo, android.hardware.perceptionhost y auracamerahal en dispositivos de RE).
  • Porción de app interactiva (/sys/fs/cgroup/apps): Es el proceso principal del paquete SystemUI configurado (config_systemUi) y el proceso principal del titular del rol de página principal del usuario actual.

Proteger el contenedor de rol de inicio junto con config_systemUi garantiza la cobertura en todos los factores de forma. En los factores de forma en los que config_systemUi es un paquete de compatibilidad liviano (como Wear OS, en el que la cara de reloj, las notificaciones y las tarjetas se ejecutan dentro de la app de Home), la shell para el usuario sigue recibiendo protección completa del conjunto de trabajo. Si el intent de Home se resuelve en el paquete config_systemUi, el proceso se protege una vez. Solo el proceso principal de cada paquete (processName == packageName) recibe un piso de protección, por lo que los subprocesos auxiliares (como :screenshot) no consumen presupuesto de protección.

Política de ajuste automático y delegación de cgroup de tres niveles

Debido a que el conjunto de trabajo de un daemon interactivo varía ampliamente según los factores de forma y las capacidades de RAM (por ejemplo, SystemUI usa alrededor de 45 MB en un reloj inteligente, 60 MB en un teléfono de 6 GB y 180 MB en un teléfono de 16 GB), las tablas de consulta estáticas por dispositivo son frágiles. En cambio, SystemMemoryBudgetManager deriva los límites inferiores directamente de la huella residente de cada proceso con tres constantes:

Parámetro de política Valor predeterminado Comportamiento
Precio mínimo por proceso 90% de memory.current establecido menos la caché de archivos no asignados Protege el montón anónimo, la losa y el código asignado activo, mientras que deja el 10% de la cola más inactiva recuperable para que las páginas realmente inactivas se puedan expulsar. La caché de archivos sin asignar (max(0, file - shmem - file_mapped) de memory.stat) se resta antes de aplicar la proporción del 90% para que la caché de análisis de paquetes de inicio o las lecturas de recursos únicas no aumenten los límites mínimos.
Límite de RAM global 10% de la RAM total (config_systemMemoryBudgetLowKb) Coincide con la protección config_pinnerMaxPinnedMemoryPercentage de PinnerService. Si la suma de los precios mínimos del 90% supera el 10% de MemTotal, todos los precios mínimos se reducen proporcionalmente por el mismo factor.
Frecuencia de actualización 1, 4, 16 y 64 s después del inicio, y luego cada 30 min Una serie de estabilización geométrica converge en el conjunto de trabajo posterior al inicio de cada daemon sin retrasos de preparación codificados por dispositivo, y una actualización de estado estable de 30 minutos hace un seguimiento del crecimiento y la reducción graduales del conjunto de trabajo durante la vida útil del dispositivo.

En el controlador de memoria de cgroup v2 de Linux (mm/memcontrol.c:effective_protection), la protección memory.min efectiva de un cgroup de hoja está limitada por la memory.min configurada en sus cgroups superiores y principales. Establecer memory.min solo en un directorio pid_<pid> de hoja mientras uid_<uid>/memory.min es 0 genera una protección efectiva nula. Por el contrario, establecer un memory.min plano solo en el segmento /sys/fs/cgroup/system superior distribuye la protección no reclamada en todos los UID secundarios y procesa de forma proporcional (memory_recursiveprot), lo que permite que los procesos de diagnóstico o secundarios que comparten uid_1000 (android.uid.system) desvíen la protección de surfaceflinger y audioserver.

Para proporcionar un aislamiento exacto por daemon, SystemMemoryBudgetManager escribe protección en los tres niveles de la jerarquía de cgroup v2 en cada actualización, como se muestra en la figura 1:

  1. Nodo de proceso de hoja (pid_<pid>/memory.min): Se establece en el límite inferior del conjunto de trabajo del 90% (escalado) del proceso.
  2. Nodo de UID (uid_<uid>/memory.min): Se establece en la suma exacta de sus pisos de hoja pid_* administrados, por lo que los procesos no administrados que comparten ese UID (como otros daemons de AID_SYSTEM o paquetes de sharedUserId="android.uid.system") tienen memory.min = 0 y no pueden reclamar protección.
  3. Porción de nivel superior (/sys/fs/cgroup/system/memory.min y /sys/fs/cgroup/apps/memory.min): Se establece en la suma exacta de sus elementos secundarios uid_*. Los valores de memory.low inactivos en los nodos administrados se borran a 0, y los directorios uid_* que se administraron en una actualización anterior (como un selector de página principal anterior después de que el usuario cambia de selector) se restablecen a 0.

Jerarquía de protección memory.min de cgroup v2 de tres niveles en /sys/fs/cgroup/system y /sys/fs/cgroup/apps, que muestra los límites mínimos de los procesos de hoja sumados en los nodos de UID y de porción, mientras que los procesos hermanos no protegidos permanecen en cero

Figura 1: Delegación de piso de protección de tres niveles en las porciones de cgroup.

Por qué se usa memory.min en lugar de memory.low en los kernels de MGLRU

En la LRU multigeneracional (MGLRU, el motor de recuperación de páginas predeterminado en los kernels de Android 6.6 y versiones posteriores), memory.low y memory.min se comportan de manera diferente en mm/vmscan.c:shrink_one:

  • memory.low (aplazamiento de un solo pase): Un cgroup cuyo uso es inferior a memory.low se rota al final del LRU de memcg (MEMCG_LRU_TAIL) y se omite para un solo pase de recuperación. En el siguiente pase con presión de memoria sostenida, el kernel recupera el cgroup por completo y registra un evento MEMCG_LOW. En las pruebas de esfuerzo de dispositivos físicos con un acaparador de memoria anónimo que crece hasta el 85% de la RAM física, memory.low permitió que SystemUI en un reloj inteligente se redujera de ~45 MB a ~5 a 10 MB y audioserver en un dispositivo plegable de 16 GB disminuyera de 15 MB a 2 MB en comparación con un piso de ~14 MB.
  • memory.min (límite de protección estricto): La recuperación global omite un cgroup cuyo uso es inferior a memory.min. Como se muestra en la figura 2, con la misma carga de trabajo de presión de RAM del 85%, memory.min solo descarta el 10% más frío de cada daemon y mantiene SystemUI, surfaceflinger y audioserver en sus pisos del 90% con cero fallas anónimas y sin aumento en las finalizaciones de procesos de lmkd.

memory.min proporciona una reserva física estricta que coincide con la protección de la fijación de PinnerService mlock que reemplaza, con la ventaja de que la LRU del kernel (en lugar de una lista de archivos estática) elige qué páginas de código, montón y gráficos activos permanecen residentes.

Comparación de la memoria residente del daemon a medida que la presión de la memoria del sistema aumenta del 40% al 95% de la RAM física, lo que muestra que memory.min se mantiene en su límite del 90%, mientras que memory.low se contrae después de un solo pase de aplazamiento de MGLRU

Figura 2: Retención del conjunto de trabajo del daemon bajo una presión de memoria creciente.

Precalentamiento de inicio y recuperación de caché de archivos no asignados después del inicio

Reemplazar el mlock permanente de PinnerService de config_defaultPinnerServiceFiles requiere coordinar dos comportamientos en el tiempo de inicio:

  • Preparación previa de inicio en segundo plano de un solo intento (PinnerService): Omitir PinnerService mlock sin preparar la caché de la página cambia ~95 MB de lecturas de disco de APK, ODEX y VDEX de SystemUI a la ruta de inicio síncrona cuando se inicia SystemUI (OnBootPhase_550), lo que agrega de 300 a 450 ms a la finalización del inicio. No basta con llamar a madvise(MADV_WILLNEED), ya que force_page_cache_ra en el kernel limita una sola llamada a MADV_WILLNEED a una ventana de lectura anticipada (por lo general, de 128 KB a 2 MB). En cambio, cuando SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning está activo, PinnerService asigna cada archivo en config_defaultPinnerServiceFiles en un subproceso en segundo plano durante onStart, propaga de forma síncrona el archivo en la caché de la página con Os.mlock y llama de inmediato a Os.munlock, munmap y close. Esto preserva el tiempo de inicio rápido y, al mismo tiempo, deja cero bytes bloqueados de forma permanente para que las páginas de inicio en frío se puedan expulsar más adelante.
  • Excluye y recupera la caché de análisis de paquetes de inicio de system_server: Durante el inicio, PackageManagerService analiza los APK instalados, lo que deja entre 1.5 GB y 2.5 GB de caché de páginas de archivos sin asignar en frío que se cargan en el cgroup de system_server. Si se incluye en el cálculo de memory.min de system_server, esa caché de inicio en frío satura el límite global del 10% de la RAM y reduce proporcionalmente los límites de protección de surfaceflinger, SystemUI y audioserver. Para evitar la falta de límite, SystemMemoryBudgetManager lee memory.stat para system_server, calcula la caché de archivos sin asignar como max(0, file - shmem - file_mapped) y resta esa caché sin asignar de memory.current antes de calcular el piso del 90%, como se muestra en la figura 3. Además, 10 segundos después de PHASE_BOOT_COMPLETED, el administrador realiza una escritura única de <unmapped_file> swappiness=0 en el nodo memory.reclaim de system_server en Linux 6.2 y kernels más recientes para expulsar de forma proactiva la caché de análisis de paquetes inactiva sin intercambiar el montón anónimo ni expulsar las páginas de código y DEX asignadas. En los kernels más antiguos (como android14-6.1) que rechazan swappiness=0 con EINVAL, el administrador omite la escritura memory.reclaim en lugar de recurrir a una escritura de recuperación no calificada que intercambiaría el montón anónimo durante el inicio.

Cronograma de la memoria de system_server después del inicio que muestra las actualizaciones de estabilización geométrica a los 1, 4, 16 y 64 segundos, la exclusión de la caché de análisis de paquetes de inicio sin asignar del piso del 90% de memory.min y memory.reclaim de un solo intento con swappiness=0

Figura 3: Se agregó la convergencia del piso de protección y la recuperación de la caché después del inicio.

Requisitos del kernel y superposiciones de configuración del dispositivo

SystemMemoryBudgetManager se basa en mem_cgroup_calculate_protection en el kernel de Linux para propagar memory.min en cgroups anidados. Los kernels comunes de Android android15-6.6 y android16-6.12 incluyen compatibilidad completa con la protección anidada de MGLRU de forma predeterminada. Los dispositivos que ejecutan android14-6.1 con MGLRU habilitado requieren tres backports upstream en mm/vmscan.c y mm/memcontrol.c para que los cgroups de UID y porción intermedia vacíos propaguen la protección a los cgroups de procesos de hoja:

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

Los OEM pueden personalizar o inhabilitar SystemMemoryBudgetManager con las siguientes superposiciones de recursos del framework:

  • config_systemMemoryBudgetLowKb (número entero, KB): 0 (predeterminado) limita la suma de todos los límites mínimos de memory.min al 10% de la RAM física total. Un valor positivo establece un límite explícito en KB, y un valor negativo inhabilita SystemMemoryBudgetManager en el dispositivo (lo que conserva la fijación estática mlock heredada de PinnerService).
  • config_systemMemoryBudgetHighKb (número entero, KB): 0 (predeterminado) deja /sys/fs/cgroup/system/memory.high en max. Un valor positivo establece un límite máximo de porción superior memory.high (debe ser mayor o igual que el límite mínimo).
  • config_systemMemoryBudgetAdditionalSystemProcesses (array de cadenas): Enumera nombres de procesos daemon adicionales del sistema nativo o del proveedor que son críticos para la latencia en /sys/fs/cgroup/system (coincide con /proc/<pid>/comm o el nombre base de argv[0] en /proc/<pid>/cmdline para los UID del sistema y del proveedor en 10000) para proteger con un 90% de pisos de memory.min junto con los seis daemons del sistema predeterminados. Usa esta superposición en lugar de configurar memory.min en init.rc porque SystemMemoryBudgetManager vuelve a calcular uid_<uid>/memory.min como la suma de sus procesos de hoja administrados y borra los nodos uid_* no administrados en cada actualización. Por ejemplo, un dispositivo de RE puede proteger sus daemons de HAL de cámara y seguimiento espacial con la siguiente superposición:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (cadena): Es el nombre del paquete de la app de SystemUI de la plataforma, resuelto con MATCH_SYSTEM_ONLY.

Para inspeccionar los niveles de SystemMemoryBudgetManager activos y los nodos de cgroup en un dispositivo, ejecuta lo siguiente:

# 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

Vincula las apps del sistema persistentes y en segundo plano con presupuestos de memoria

Si bien SystemMemoryBudgetManager protege los daemons interactivos de la recuperación con límites mínimos de memory.min, muchas otras apps del sistema y servicios de plataforma con privilegios se ejecutan en segundo plano durante períodos prolongados como procesos PERSISTENT, servicios en primer plano vinculados o proveedores de contenido que se consultan con frecuencia. Sin límites de memoria explícitos, estos procesos no se restringen hasta que alcanzan los límites de varios gigabytes del limitador de memoria (como de 4 GB a 10 GB para las apps visibles y de 2 GB a 5 GB para las apps en segundo plano) o la presión de memoria global activa la actividad de kswapd y de eliminación de memoria baja (lmkd) en todo el sistema.

Si no se restringen, los procesos del sistema en segundo plano suelen acumular grandes cantidades de memoria inactiva que no forma parte de su conjunto de trabajo activo:

  • Caché de páginas de archivos sin límites: Los servicios que indexan contenido multimedia, buscan bases de datos o sincronizan tablas de proveedores y calendarios (como AppSearch, MediaProvider y CalendarProvider) asignan o transmiten archivos grandes en el disco. Sin un límite de cgroup memory.high, las páginas limpias respaldadas por archivos de lecturas únicas permanecen cargadas en el cgroup del proceso de forma indefinida, lo que consume cientos de megabytes de RAM física.
  • Retención de montón anónimo posterior a la ráfaga: Los servicios que experimentan aumentos repentinos de actividad transitorios, como las ráfagas de descubrimiento de audio y BLE de Bluetooth, la validación de TLS del portal cautivo de NetworkStack o los asistentes de IU que pasan a segundo plano, pueden retener páginas de montón anónimas inactivas mucho después de que se complete la ráfaga.

Aplicar declaraciones <memory-budget> calibradas (límites flexibles de memory.high, por lo general, de 30 MB a 190 MB) a las apps del sistema proporciona dos beneficios principales:

  • Recuperación de páginas proactiva y localizada: Cuando un proceso del sistema supera su presupuesto de memory.high configurado, el kernel de Linux dirige la recuperación de páginas específicamente a ese cgroup, lo que expulsa primero las páginas respaldadas por archivos inactivos y comprime las páginas anónimas inactivas en ZRAM sin esperar a que se produzca una presión de memoria global del dispositivo.
  • Más RAM disponible para las apps en primer plano y el grupo de apps almacenadas en caché: Limitar las cachés de archivos inactivos en los servicios del sistema siempre activos recupera de decenas a cientos de megabytes de RAM física en todo el dispositivo, lo que reduce los tirones de la IU, disminuye la presión de inicio de la cámara y mantiene activas más apps del usuario en el estado almacenado en caché.

Configura los presupuestos de memoria de las apps del sistema

Las apps del sistema usan la misma sintaxis de manifiesto <memory-budget>, cascada de estado del proceso, escalamiento de densidad de pantalla y regla de resolución de la última cláusula coincidente que las apps generales. Para obtener la lista completa de atributos, consulta la referencia de atributos XML.

Los OEM pueden declarar elementos <memory-budget> directamente dentro del archivo AndroidManifest.xml de una app del sistema o combinarlos desde un archivo en formato XML independiente con la propiedad additional_manifests en Android.bp. Por convención, el AOSP almacena los presupuestos de las apps del sistema de referencia en archivos *-budget.xml independientes (ya sea en frameworks/base/core/memory_budgets/ para los paquetes de la plataforma principal o junto con módulos individuales, como packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ y packages/services/Telephony/) y los adjunta con additional_manifests:

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

Alcance a nivel del paquete (UID) frente al alcance a nivel del proceso (PID)

Si declaras un presupuesto de referencia a nivel del paquete (<application>) o un presupuesto de varios procesos (<processes><process>), se determinará si el kernel aplica memory.high a nivel del cgroup del UID (/sys/fs/cgroup/uid_<uid>/memory.high) o a nivel del cgroup por PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), como se muestra en la figura 4:

  • Presupuesto a nivel del paquete (<application>): Se usa cuando el APK ejecuta un solo proceso con su propio UID dedicado (por ejemplo, Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur o AppSearch).
  • Presupuesto a nivel de proceso (<processes><process>): Se usa cuando el APK comparte android:sharedUserId (como android.uid.phone con el UID 1001 o android.uid.system con el UID 1000) con otros paquetes del sistema para que otros procesos que comparten ese UID no se limiten juntos, o cuando un solo APK ejecuta varios procesos con diferentes funciones de memoria (por ejemplo, el daemon MediaProvider en segundo plano y el subproceso de IU :PhotoPicker).

Jerarquía de cgroup v2 que compara la aplicación de memory.high a nivel de UID para los UID dedicados con la aplicación de memory.high por PID para los UID del sistema compartidos y los APK de varios procesos

Figura 4: Límites de memoria a nivel de UID frente a nivel de PID

En el siguiente ejemplo, se muestra MediaProvider-budget.xml, que limita los presupuestos con niveles de RAM al proceso MediaProvider predeterminado (<process>) y un presupuesto ajustado a la pantalla al proceso de IU :PhotoPicker. Cada atributo android:maxDeviceRamMb coincide con los dispositivos cuya RAM física total (MemTotal) es igual o inferior al umbral de MB especificado (o se aplica a cualquier capacidad de RAM cuando se omite). Las cláusulas se ordenan desde el valor predeterminado sin restricciones hasta el umbral de maxDeviceRamMb más pequeño, de modo que la regla de resolución de última cláusula coincidente selecciona el nivel aplicable más estricto:

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

Cómo se calibraron los presupuestos de referencia de AOSP en los dispositivos Google Pixel

Los presupuestos de memoria de la app de referencia del sistema en el AOSP se calibraron en todos los dispositivos Google Pixel con una RAM física de 6 GB a 16 GB (incluidos teléfonos, plegables y tablets de 6 GB, 8 GB, 12 GB y 16 GB) y resoluciones de pantalla que van de ~1.2 MP (720 × 1600) a ~4.5 MP (2076 × 2152).

La calibración combinó dos fuentes de datos complementarias: la telemetría de campo de cgroup v2 en toda la flota y los barridos de memory.high de dispositivos físicos controlados en los recorridos críticos del usuario (CUJ).

Análisis de telemetría de cgroup v2 en toda la flota

Se recopilaron instantáneas periódicas de cgroup v2 en toda la flota de dispositivos y se dividieron por nivel de RAM física (≤8 GB, 12 GB y ≥16 GB) y bucket de estado del proceso (foreground, perceptible, background y cached). El análisis estadístico se basó en seis principios:

  • Descompón memory.current en un conjunto de trabajo en comparación con la caché de archivos recuperables: Debido a que memory.high rige la carga de cgroup residente (memory.current), cada instantánea se descompuso con memory.stat en un montón anónimo (anon), páginas anónimas intercambiadas (swap) y caché de páginas respaldada por archivos (file), como se muestra en la figura 5. La demanda anónima incompresible total (anon + swap en P99) establece el piso mínimo del conjunto de trabajo para el presupuesto, de modo que el proceso no corra el riesgo de agotar la ZRAM, mientras que una gran brecha entre memory.current P99 y anon + swap P99 indica una caché de páginas de archivos que memory.high puede reclamar, siempre que la caché no esté asignada y esté inactiva.
  • Ten en cuenta la memoria que memory.high no puede recuperar: No toda la memoria de archivos y compartida es caché inactiva. Las páginas que están bloqueadas (unevictable en memory.stat, como la memoria compartida bloqueada con mlock o la RAM de invitado bloqueada de una máquina virtual cuyos procesos de host se cobran al cgroup de la app) establecen un límite mínimo en el presupuesto, y las páginas asignadas (file_mapped) suelen formar parte del conjunto de trabajo activo. Verifica unevictable, shmem y file_mapped antes de tratar la memoria de archivos como recuperable. Por ejemplo, alrededor de 100 MB de memoria AppSearch están bloqueados y no se pueden expulsar, por lo que su presupuesto cubre ese mínimo más su conjunto de trabajo de host activo, con espacio para que crezca la región bloqueada.
  • Filtrar la dilución del estado inactivo: Los proveedores de contenido (CalendarProvider) y las herramientas para desarrolladores on demand (Traceur) pasan del 70% al 99.9% de las instantáneas de campo inactivas en ~1 MB a 3 MB. Calcular los percentiles sin procesar en todas las instantáneas diluye de forma significativa las estimaciones de P95 y P99. En su lugar, los percentiles y los intervalos de confianza se calcularon en función de las instantáneas activas sin diluir (filtrando las instantáneas de CACHED y las instantáneas de referencia inactivas).
  • Usa intervalos de confianza agrupados por dispositivo: Como un solo dispositivo proporciona muchas instantáneas periódicas a lo largo del tiempo, tratar las instantáneas como muestras independientes subestima la incertidumbre de la cola. Todas las estimaciones del conjunto de trabajo de P95 y P99 se evaluaron con intervalos de confianza conservadores del 95% agrupados por dispositivo (con el recuento de dispositivos distintos N_dev como el tamaño de la muestra efectivo).
  • Ten en cuenta las ráfagas transitorias con memory.peak: El muestreo periódico puede pasar por alto ráfagas de asignación alta de corta duración (como la validación de TLS del portal cautivo de NetworkStack y los sondeos de DNS simultáneos, o la inicialización de A2DP y BLE de Bluetooth). Para los servicios con ráfagas, los presupuestos se dimensionaron para cubrir los percentiles 90 a 95 de los niveles máximos de memory.peak del kernel durante su vida útil en los niveles de RAM.
  • Confirma que un nivel de RAM refleje la RAM, no la generación del dispositivo: Los dispositivos más nuevos suelen tener más RAM y más funciones, por lo que un conjunto de trabajo que crece con el nivel de RAM podría estar creciendo con las funciones. Antes de agregar un nivel de maxDeviceRamMb, divide los datos por modelo de dispositivo. Por ejemplo, com.android.phone alcanza el mismo P99 anon + swap (de ~85 a 90 MB) en teléfonos de 12 GB y 16 GB de la misma generación, mientras que un dispositivo plegable más antiguo de 12 GB usa 40 MB. Por lo tanto, su presupuesto usa un solo valor sin nivel de RAM, de modo que un dispositivo con menos RAM que incluya las funciones de telefonía más recientes no tenga un presupuesto insuficiente.

Descomposición de la memoria de cgroup a lo largo del tiempo en un conjunto de trabajo anónimo incompresible, una ventana de caché de archivos activa y una caché de archivos fría de un solo uso recuperada más allá de memory.high

Figura 5: Conjunto de trabajo frente a caché de archivos recuperable.

CUJ de dispositivos físicos y barridos de escalamiento de pantalla

Para validar los presupuestos derivados de la telemetría y calibrar los presupuestos proporcionales a la pantalla (como :PhotoPicker) y los presupuestos en segundo plano específicos del estado (como Traceur), se ejecutaron barridos de memory.high en vivo en dispositivos físicos que abarcan de 6 GB a 16 GB de RAM:

  • Cómo encontrar el punto de inflexión de cero fallas repetidas: Mientras se ejecutan CUJ de extremo a extremo (como el desplazamiento rápido por la cuadrícula de fotos, la exploración de álbumes, el deslizamiento de vistas previas de fotos y videos de alta resolución en pantalla completa, y el registro y guardado de seguimientos), se redujo memory.high mientras se medían los contadores de cgroup memory.stat (workingset_refault_anon, workingset_refault_file, pgscan y pgsteal) y memory.events (high). Como se ilustra en la figura 6, el presupuesto óptimo se encuentra justo por encima del punto de inflexión de cero fallas repetidas. El punto de inflexión de cero fallas de referencia es el umbral en el que el kernel recupera la caché de archivos de un solo uso inactivos (pgsteal > 0) sin activar la hiperpaginación del conjunto de trabajo activo (workingset_refault == 0).
  • Validación del escalamiento de píxeles de pantalla en todas las resoluciones: Las pruebas de :PhotoPicker en cuatro resoluciones de pantalla físicas y emuladas, desde ~1.2 MP (720 × 1,600) hasta ~4.5 MP (pantalla interna desplegada de 2,076 × 2,152), confirmaron que su conjunto de trabajo de mapas de bits activo de Glide y ViewPager2 se escala de forma lineal con el recuento de píxeles de la pantalla. La línea de base sin pantalla también varía según el dispositivo: con casi la misma cantidad de píxeles, el punto de inflexión sin fallas fue de alrededor de 140 MB en la pantalla externa de un dispositivo plegable de 16 GB, en comparación con 110 a 125 MB en un teléfono de 6 GB. Un valor de referencia de 85 MB más 25 bytes por píxel de pantalla (android:maxMb="85" y android:additionalBytesPerDisplayPixel="25") se mantiene por encima del punto de inflexión medido en cada pantalla probada (~110 MB en HD+, ~145 MB en FHD+ y ~190 MB en pantallas desplegadas).
  • Verificación de la recolección de elementos no utilizados de ART en estados en segundo plano: En cgroup v2, las páginas de imágenes de framework y boot.art compartidas por Zygote permanecen cargadas en el cgroup de Zygote y se tratan como un espacio inmune durante la recolección de elementos no utilizados de compactación de marcas (CMC) simultánea de ART. Las pruebas de esfuerzo confirmaron que, cuando una app de la IU del sistema pasa a background, ajustar memory.high (por ejemplo, a 30 MB en Traceur) limita de forma limpia el montón privado y la caché de páginas sin causar un uso excesivo de la recolección de elementos no utilizados ni actividad de intercambio.

Gráfico de la recuperación de la caché en frío (pgsteal) y la hiperpaginación del conjunto de trabajo (workingset_refault) a medida que memory.high disminuye durante un CUJ, lo que destaca el punto de inflexión de cero fallas de referencia y el presupuesto objetivo

Figura 6: Encontrar el punto de inflexión de cero fallas de referencia durante un CUJ

Resumen de los presupuestos de las apps del sistema de referencia del AOSP

En la siguiente tabla, se resumen los presupuestos de referencia calibrados en AOSP en los niveles de RAM y los estados de los procesos:

Paquete o proceso del sistema Alcance ≤8 GB de RAM 12 GB de RAM ≥16 GB de RAM Controlador de calibración principal
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB Cubre ráfagas de memory.peak de por vida del P90 (de ~75 a 110 MB) y >2x de memory.current activo del P99.
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB Abarca la emulación de tarjeta de host activa P99 (hasta ~50 MB) y P90 a P95 memory.peak mientras se reduce la caché de archivos inactivos.
com.android.providers.calendar UID (<application>) 70 MB predeterminado (primer plano)
50 MB perceptible y background
El conjunto de trabajo activo es invariable en todos los niveles de RAM. El presupuesto en segundo plano mantiene un margen anónimo para las alarmas de recordatorio, a la vez que reduce la caché de páginas de SQLite durante las sincronizaciones.
com.android.traceur UID (<application>) 85 MB de forma predeterminada (en primer plano y perceptible)
30 MB background
Conserva un margen completo de 85 MB durante la grabación y exportación de registros activos (TraceService), mientras que limita el estado inactivo y de transmisión a 30 MB.
com.android.providers.media.module (principal) PID (<process>) 60 MB 75 MB 85 MB Proporciona un margen de ~10 a 30 MB sobre el P99 de anon + swap mientras se recupera la caché de archivos de miniaturas y SQLite inactivos.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 MB + 25 bytes por píxel
(aproximadamente 110 MB en HD+, 145 MB en FHD+ y 190 MB en dispositivos plegables de ~4.5 MP)
Se escala de forma proporcional con el recuento de píxeles de la pantalla (additionalBytesPerDisplayPixel="25") para que coincida con el punto de inflexión del conjunto de trabajo de mapas de bits de Glide.
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB Memoria predominantemente anónima, con un tamaño de memory.peak de vida útil de P95 para cubrir las ráfagas de TLS, DNS y DHCP del portal cautivo.
com.android.appsearch.apk UID (<application>) 130 MB Cubre ~100 MB de memoria bloqueada y no expulsable, además de un conjunto de trabajo de host activo de ~5 MB, con margen para que crezca la región bloqueada. Se midieron cero fallas de página repetidas con ≥125 MB mientras se recuperaba la caché de páginas frías (hasta ~330 MB en P99).
com.android.phone (TeleService) PID (<process>) 120 MB Se limita por PID en el UID compartido 1001. El conjunto de trabajo hace un seguimiento de la generación del dispositivo en lugar de la RAM, por lo que un solo presupuesto se encuentra ~30 MB por encima del P99 anon + swap más alto por modelo.

Cómo los OEM pueden recalibrar los presupuestos de las apps del sistema para sus propios dispositivos

Los OEM que personalizan las apps del sistema (por ejemplo, agregan perfiles de Bluetooth de proveedores, extensiones de telefonía personalizadas o indexadores de contenido multimedia de OEM) o envían servicios del sistema preinstalados adicionales pueden recalibrar los presupuestos existentes o crear nuevos archivos *-budget.xml para su cartera de hardware.

Recopila perfiles de memoria de cgroup v2 activos

Para cada proceso del sistema de destino, lee sus nodos de memoria de cgroup v2 en /sys/fs/cgroup/uid_<uid>/pid_<pid>/ en las cargas de trabajo y los niveles de RAM representativos:

  • memory.current: Memoria residente actual que se cobra al cgroup (sin incluir las páginas compartidas de Zygote)
  • memory.peak: Es el nivel máximo de memory.current desde el inicio del proceso.
  • memory.swap.current: Uso actual del espacio de intercambio comprimido en ZRAM
  • memory.stat: Desglose de anon, file, pgscan, pgsteal, workingset_refault_anon y workingset_refault_file
  • memory.events: Es el recuento acumulativo de incumplimientos de high (eventos de limitación y de reclamo de memory.high).

Cuando agregues telemetría de dispositivos de prueba interna o de campo, separa los estados de proceso activos (TOP, FOREGROUND_SERVICE, vinculaciones activas y sincronizaciones activas) de los estados CACHED o PERSISTENT inactivos para que las instantáneas inactivas no desvíen hacia abajo tus cálculos de conjunto de trabajo P95 y P99.

Separa el montón incompresible de la caché de archivos recuperable

Compara el percentil 99 de anon + swap con el percentil 99 de memory.current:

  • Servicios dominados por la caché de archivos (file &gg; anon, como proveedores de bases de datos, indexadores de búsqueda y daemons de registro): Establece android:maxMb para proporcionar un margen cómodo (por lo general, de 1.5x a 4x) sobre el P99 anon + swap más una ventana de trabajo para las páginas de archivos activos. Esta configuración permite que el kernel desaloje de forma continua las páginas de archivos fríos sin asignar sin tocar el montón anónimo. Si unevictable o file_mapped son grandes (como con la memoria bloqueada o las bases de datos asignadas a la memoria), cuenta esa huella como conjunto de trabajo también.
  • Servicios con ráfagas o dominados por montones anónimos (anon constituye la mayor parte de memory.current, como las pilas de redes o conectividad): Ajusta el tamaño de android:maxMb para cubrir del P90 al P95 de memory.peak, de modo que las ráfagas transitorias no activen el intercambio síncrono de ZRAM en subprocesos sensibles a la latencia.
  • Procesos de IU con uso intensivo de la pantalla (como selectores, galerías o asistentes de IU del sistema): Mide el espacio de referencia que no es de IU y el espacio de mapa de bits incremental por megapíxel en las pantallas de resolución más baja y más alta, y expresa el presupuesto con android:maxMb más android:additionalBytesPerDisplayPixel.

Barrido de memory.high en dispositivos físicos

Antes de confirmar un presupuesto nuevo para la compilación, usa am memory-limiter manual (o escribe directamente en el nodo cgroup memory.high del proceso en una compilación userdebug) para probar los límites de candidatos mientras ejecutas tus pruebas de CUJ automatizadas:

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

Supervisa memory.stat y memory.events antes y después del CUJ:

  • El presupuesto es demasiado bajo: workingset_refault_anon o workingset_refault_file aumentan de forma repentina durante el CUJ, memory.swap.current crece de forma continua durante el trabajo activo en primer plano, o bien se produce una regresión en la latencia de Binder o del fotograma del CUJ. Aumentar el presupuesto más allá del punto de inflexión predeterminado
  • El presupuesto es demasiado alto: memory.events:high sigue siendo 0 incluso después de cargas de trabajo intensas de transmisión o indexación de archivos, y file en memory.stat crece sin límites con páginas inactivas. Ajusta el presupuesto para el conjunto de trabajo activo.
  • El presupuesto está bien calibrado: El memory.current de estado estable se mantiene por debajo de memory.high durante el funcionamiento normal. Durante ráfagas intensas de archivos o de caché, pgsteal recupera páginas de archivos inactivos, mientras que workingset_refault_anon permanece cerca de cero y la latencia de CUJ no cambia.