Budgets de mémoire système

Sous Android 17 et versions ultérieures, la plate-forme gère la mémoire des processus système à l'aide de deux mécanismes Linux cgroup v2 complémentaires qui fonctionnent en parallèle du limiteur de mémoire général de l'application :

  • Seuils de protection des daemons système (SystemMemoryBudgetManager avec memory.min) : les réservations memory.min de cgroup v2 à réglage automatique (définies sur 90 % de l'ensemble de travail résident de chaque daemon et plafonnées globalement à 10% de la RAM physique totale) protègent les daemons interactifs critiques et les processus de shell (system_server, surfaceflinger, la HAL graphique composer, audioserver, cameraserver, netd, SystemUI et le lanceur d'applications d'accueil) en cas de pression sur la mémoire, en remplaçant l'épinglage de fichiers statiques PinnerService mlock.
  • Plafonds de mémoire des applications système (<memory-budget> avec memory.high) : les limites souples memory.high de cgroup v2 calibrées pour la charge de travail (généralement de 30 Mo à 190 Mo) déclarées dans les fichiers manifestes pour les applications système persistantes et en arrière-plan (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch et TeleService) récupèrent de manière proactive le cache de pages de fichiers froids et le tas anonyme post-burst avant que la pression de mémoire globale ne se produise.

Le tableau suivant compare les deux mécanismes :

Dimension Niveaux de protection des daemons système Plafonds de mémoire des applications système
Service de plate-forme SystemMemoryBudgetManager MemoryLimiter
Contrôle cgroup v2 memory.min seuil de protection stricte memory.high plafond de récupération partielle
Processus cibles Démons interactifs et UI de shell essentiels pour la latence (system_server, surfaceflinger, HAL composer, audioserver, cameraserver, netd, SystemUI et l'application Home) Applications système persistantes et sans interface graphique, fournisseurs de contenu et services d'arrière-plan (tels que Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch et TeleService)
Modèle de dimensionnement Réglage automatique au moment de l'exécution (90% de l'ensemble de travail établi, plafonné globalement à 10% de la RAM totale) sans tables de réglage par appareil Attributs XML du fichier manifeste <memory-budget> déclaratif calibrés par niveau de RAM, état du processus et nombre de pixels de l'écran
Principal avantage Évite les à-coups de l'UI interactive et les blocages de réveil en cas de pression sur la mémoire, tout en remplaçant les listes de fichiers PinnerService mlock statiques. Évince en continu le cache de fichiers froids à usage unique et le tas de mémoire post-burst pour laisser plus de RAM physique aux applications au premier plan et mises en cache

Pour en savoir plus sur la syntaxe <memory-budget>, les attributs XML, les API d'exécution et la comptabilisation de la mémoire de l'OS, consultez Définir les budgets de mémoire de l'application et Comprendre et résoudre les problèmes de mémoire Android.

Protéger les daemons interactifs critiques avec SystemMemoryBudgetManager

SystemMemoryBudgetManager conserve les processus qui se trouvent directement sur le chemin interactif de l'utilisateur résidant dans la RAM physique pendant la pression de la mémoire. Plutôt que d'épingler une liste statique de fichiers APK, ODEX et VDEX en mémoire avec mlock (comme le fait PinnerService pour config_defaultPinnerServiceFiles et config_pinnerHomePinBytes), SystemMemoryBudgetManager attribue des seuils memory.min cgroup v2 dynamiques aux processus suivants :

  • Tranche du système de base (/sys/fs/cgroup/system) : system_server, surfaceflinger, HAL composer graphique, audioserver, cameraserver et netd, ainsi que tous les démons système ou de fournisseur natifs spécifiques au facteur de forme listés dans config_systemMemoryBudgetAdditionalSystemProcesses (par exemple, android.hardware.perceptionhost et auracamerahal sur les appareils XR).
  • Tranche d'application interactive (/sys/fs/cgroup/apps) : processus principal du package SystemUI configuré (config_systemUi) et processus principal du titulaire du rôle d'accueil de l'utilisateur actuel.

La protection du titulaire du rôle Home en plus de config_systemUi garantit une couverture sur tous les facteurs de forme. Sur les facteurs de forme où config_systemUi est un package de compatibilité léger (comme Wear OS, où le cadran, les notifications et les cartes s'exécutent dans l'application Home), l'interface visible par l'utilisateur bénéficie toujours d'une protection complète de l'ensemble de travail. Si l'intent Home est résolu dans le package config_systemUi, le processus est protégé une fois. Seul le processus principal de chaque package (processName == packageName) reçoit un niveau de protection. Les sous-processus auxiliaires (tels que :screenshot) ne consomment donc pas de budget de protection.

Règle d'autoréglage et délégation de cgroup à trois niveaux

Étant donné que l'ensemble de travail d'un daemon interactif varie considérablement selon les facteurs de forme et les capacités de RAM (par exemple, SystemUI utilise environ 45 Mo sur une montre connectée, 60 Mo sur un téléphone de 6 Go et 180 Mo sur un téléphone de 16 Go), les tables de conversion statiques par appareil sont fragiles. À la place, SystemMemoryBudgetManager dérive les planchers directement de l'empreinte résidente de chaque processus à l'aide de trois constantes :

Paramètre de règle Valeur par défaut Comportement
Prix plancher par processus 90% de la mémoire stable memory.current moins le cache de fichiers non mappés Protège le tas anonyme, la slab et le code mappé chaud, tout en laissant les 10% de pages les plus froides récupérables, de sorte que les pages véritablement froides puissent toujours être supprimées. Le cache de fichiers non mappés (max(0, file - shmem - file_mapped) de memory.stat) est soustrait avant d'appliquer le ratio de 90% afin que le cache d'analyse des packages de démarrage ou les lectures d'assets ponctuelles n'augmentent pas les seuils.
Limite de RAM globale 10% de la RAM totale (config_systemMemoryBudgetLowKb) Correspond à la protection PinnerService config_pinnerMaxPinnedMemoryPercentage. Si la somme des prix planchers à 90% dépasse 10% de MemTotal, chaque prix plancher est réduit proportionnellement du même facteur.
Fréquence d'actualisation 1, 4, 16 et 64 s après le démarrage, puis toutes les 30 min Une série de stabilisation géométrique converge sur l'ensemble de travail post-démarrage de chaque daemon sans coder en dur les délais de préchauffage par appareil, et une actualisation de l'état stable de 30 minutes suit la croissance et la réduction progressives de l'ensemble de travail pendant la durée de vie de l'appareil.

Dans le contrôleur de mémoire Linux cgroup v2 (mm/memcontrol.c:effective_protection), la protection memory.min effective d'un cgroup feuille est limitée par la protection memory.min configurée sur ses cgroups parents et ancêtres. La définition de memory.min uniquement sur un répertoire feuille pid_<pid> alors que uid_<uid>/memory.min est défini sur 0 n'offre aucune protection efficace. À l'inverse, définir un memory.min fixe uniquement sur la tranche /sys/fs/cgroup/system supérieure répartit la protection non réclamée entre tous les UID et processus enfants de manière proportionnelle (memory_recursiveprot), ce qui permet aux processus de diagnostic ou secondaires partageant uid_1000 (android.uid.system) de siphonner la protection de surfaceflinger et audioserver.

Pour fournir une isolation exacte par daemon, SystemMemoryBudgetManager écrit la protection sur les trois niveaux de la hiérarchie cgroup v2 à chaque actualisation, comme illustré dans la figure 1 :

  1. Nœud de processus feuille (pid_<pid>/memory.min) : défini sur le seuil de l'ensemble de travail (mis à l'échelle) de 90% du processus.
  2. Nœud UID (uid_<uid>/memory.min) : défini sur la somme exacte de ses planchers de feuilles pid_* gérés, de sorte que les processus non gérés partageant cet UID (comme d'autres daemons AID_SYSTEM ou packages sharedUserId="android.uid.system") ont memory.min = 0 et ne peuvent pas demander de protection.
  3. Tranche de niveau supérieur (/sys/fs/cgroup/system/memory.min et /sys/fs/cgroup/apps/memory.min) : définie sur la somme exacte de ses enfants uid_*. Toutes les valeurs memory.low obsolètes sur les nœuds gérés sont effacées et remplacées par 0, et tous les répertoires uid_* qui étaient gérés lors d'une actualisation précédente (comme un lanceur d'applications d'accueil précédent après que l'utilisateur a changé de lanceur d'applications) sont réinitialisés sur 0.

Hiérarchie de protection memory.min de cgroup v2 à trois niveaux dans /sys/fs/cgroup/system et /sys/fs/cgroup/apps, montrant les planchers des processus feuilles additionnés dans les nœuds UID et slice, tandis que les processus frères non protégés restent à zéro

Figure 1. Délégation de plancher de protection à trois niveaux entre les tranches de cgroup.

Pourquoi memory.min est utilisé à la place de memory.low sur les noyaux MGLRU

Sous MGLRU (Multi-Generational LRU, le moteur de récupération de page par défaut sur les kernels Android 6.6 et versions ultérieures), memory.low et memory.min se comportent différemment dans mm/vmscan.c:shrink_one :

  • memory.low (report en un seul passage) : un cgroup dont l'utilisation est inférieure à memory.low est déplacé à la fin de la liste LRU memcg (MEMCG_LRU_TAIL) et ignoré pour un seul passage de récupération. Lors du prochain passage sous une pression de mémoire soutenue, le noyau récupère entièrement le cgroup et consigne un événement MEMCG_LOW. Lors de tests de résistance sur des appareils physiques avec un processus anonyme gourmand en mémoire qui atteint 85% de la RAM physique, memory.low a permis à SystemUI sur une montre connectée de passer d'environ 45 Mo à environ 5 à 10 Mo, et à audioserver sur un appareil pliable de 16 Go de passer de 15 Mo à 2 Mo, contre un plancher d'environ 14 Mo.
  • memory.min (seuil de protection strict) : un cgroup dont l'utilisation est inférieure à memory.min est ignoré par la récupération globale. Comme illustré dans la figure 2, sous la même charge de travail de 85% de pression de RAM, memory.min ne supprime que les 10% les plus froids de chaque daemon et maintient SystemUI, surfaceflinger et audioserver à leur niveau de 90 %, sans aucun défaut de page anonyme ni augmentation des arrêts de processus lmkd.

memory.min fournit une réservation physique stricte correspondant à la protection de l'épinglage PinnerService mlock qu'il remplace, avec l'avantage que le LRU du noyau (plutôt qu'une liste de fichiers statique) choisit les pages de code, de tas et de graphiques actives qui restent résidentes.

Comparaison de la mémoire résidente du daemon à mesure que la pression sur la mémoire système passe de 40% à 95% de la RAM physique, montrant que memory.min se maintient à son seuil de 90% tandis que memory.low s'effondre après une seule passe de report MGLRU

Figure 2. Conservation de la plage de travail du daemon en cas de sollicitation croissante de la mémoire.

Préchauffage au démarrage et récupération du cache de fichiers non mappés après le démarrage

Le remplacement du mlock permanent PinnerService de config_defaultPinnerServiceFiles nécessite de coordonner deux comportements au démarrage :

  • Préchauffage ponctuel du démarrage en arrière-plan (PinnerService) : ignorer PinnerService mlock sans préchauffer le cache de la page déplace environ 95 Mo de lectures de disque APK, ODEX et VDEX SystemUI vers le chemin de démarrage synchrone au démarrage de SystemUI (OnBootPhase_550), ce qui ajoute 300 à 450 ms à la fin du démarrage. L'appel de madvise(MADV_WILLNEED) n'est pas suffisant, car force_page_cache_ra dans le noyau limite un seul appel MADV_WILLNEED à une fenêtre de lecture anticipée (généralement de 128 Ko à 2 Mo). Au lieu de cela, lorsque SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning est actif, PinnerService mappe chaque fichier de config_defaultPinnerServiceFiles sur un thread d'arrière-plan pendant onStart, remplit de manière synchrone le fichier dans le cache de la page avec Os.mlock, et appelle immédiatement Os.munlock, munmap et close. Cela permet de préserver le temps de démarrage rapide tout en ne laissant aucun octet verrouillé de manière permanente, de sorte que les pages de démarrage à froid puissent être supprimées ultérieurement.
  • Exclusion et récupération du cache d'analyse du package de démarrage system_server : au démarrage, PackageManagerService analyse les APK installés, laissant 1,5 à 2,5 Go de cache de pages de fichiers non mappés froids imputés au cgroup system_server. S'il est inclus dans le calcul memory.min de system_server, ce cache de démarrage à froid sature la limite globale de 10% de RAM et réduit proportionnellement les seuils de protection de surfaceflinger, SystemUI et audioserver. Pour éviter le manque de capacité, SystemMemoryBudgetManager lit memory.stat pour system_server, calcule le cache de fichiers non mappés en tant que max(0, file - shmem - file_mapped) et soustrait ce cache non mappé de memory.current avant de calculer le seuil de 90 %, comme illustré dans la figure 3. De plus, 10 secondes après PHASE_BOOT_COMPLETED, le gestionnaire effectue une écriture unique de <unmapped_file> swappiness=0 sur le nœud memory.reclaim de system_server sur Linux 6.2 et les noyaux plus récents pour supprimer de manière proactive le cache d'analyse de package froid sans échanger de tas anonyme ni supprimer les pages DEX et de code mappées. Sur les anciens noyaux (comme android14-6.1) qui rejettent swappiness=0 avec EINVAL, le gestionnaire ignore l'écriture memory.reclaim au lieu de revenir à une écriture de récupération non qualifiée qui échangerait le tas anonyme au démarrage.

Chronologie de la mémoire system_server après le démarrage, montrant les actualisations de stabilisation géométrique à 1, 4, 16 et 64 secondes, l'exclusion du cache d'analyse des packages de démarrage non mappé du plancher memory.min de 90% et memory.reclaim ponctuel avec swappiness=0

Figure 3. Convergence du plancher de protection et récupération du cache après le démarrage.

Exigences du kernel et overlays de configuration de l'appareil

SystemMemoryBudgetManager s'appuie sur mem_cgroup_calculate_protection dans le noyau Linux pour propager memory.min entre les cgroups imbriqués. Les kernels Android courants android15-6.6 et android16-6.12 incluent une compatibilité complète avec la protection imbriquée MGLRU. Les appareils exécutant android14-6.1 avec MGLRU activé nécessitent trois rétroportages en amont dans mm/vmscan.c et mm/memcontrol.c afin que les cgroups de tranche intermédiaire et d'UID vides propagent la protection aux cgroups de processus feuille :

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

Les OEM peuvent personnaliser ou désactiver SystemMemoryBudgetManager à l'aide des superpositions de ressources de framework suivantes :

  • config_systemMemoryBudgetLowKb (entier, Ko) : 0 (par défaut) limite la somme de tous les seuils memory.min à 10% de la RAM physique totale. Une valeur positive définit un plafond explicite en Ko, et une valeur négative désactive SystemMemoryBudgetManager sur l'appareil (en conservant l'ancien épinglage mlock statique PinnerService).
  • config_systemMemoryBudgetHighKb (entier, Ko) : 0 (par défaut) laisse /sys/fs/cgroup/system/memory.high à max. Une valeur positive définit un plafond de tranche supérieure memory.high (qui doit être supérieur ou égal au plancher).
  • config_systemMemoryBudgetAdditionalSystemProcesses (string-array) : liste les noms de processus de daemon système ou de fournisseur natifs supplémentaires essentiels à la latence dans /sys/fs/cgroup/system (correspondant à /proc/<pid>/comm ou au nom de base de argv[0] dans /proc/<pid>/cmdline pour les UID système et de fournisseur sous 10000) à protéger avec des seuils de 90% de memory.min, en plus des six daemons système par défaut. Utilisez cette superposition plutôt que de définir memory.min dans init.rc, car SystemMemoryBudgetManager recalcule uid_<uid>/memory.min comme la somme de ses processus feuilles gérés et efface les nœuds uid_* non gérés à chaque actualisation. Par exemple, un appareil XR peut protéger ses démons HAL de suivi spatial et de caméra avec la superposition suivante :

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (chaîne) : nom du package de l'application SystemUI de la plate-forme, résolu avec MATCH_SYSTEM_ONLY.

Pour inspecter les limites SystemMemoryBudgetManager et les nœuds cgroup actifs sur un appareil, exécutez la commande suivante :

# 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

Limiter les applications système persistantes et en arrière-plan avec des budgets de mémoire

Bien que SystemMemoryBudgetManager protège les daemons interactifs de la récupération à l'aide des seuils memory.min, de nombreuses autres applications système et services de plate-forme privilégiés s'exécutent en arrière-plan pendant de longues périodes en tant que processus PERSISTENT, services de premier plan liés ou fournisseurs de contenu fréquemment interrogés. Sans plafond de mémoire explicite, ces processus ne sont pas limités tant qu'ils n'atteignent pas les limites de plusieurs gigaoctets du limiteur de mémoire (par exemple, 4 Go à 10 Go pour les applications visibles et 2 Go à 5 Go pour les applications en arrière-plan) ou que la pression de la mémoire globale ne déclenche pas l'activité kswapd et le low-memory killer (lmkd) à l'échelle du système.

S'ils ne sont pas limités, les processus système en arrière-plan accumulent souvent de grandes quantités de mémoire froide qui ne fait pas partie de leur ensemble de travail actif :

  • Cache de page de fichier illimité : les services qui indexent les contenus multimédias, effectuent des recherches dans les bases de données ou synchronisent les tables d'agenda et de fournisseur (telles que AppSearch, MediaProvider et CalendarProvider) mappent ou diffusent des fichiers volumineux sur le disque. Sans plafond memory.high de cgroup, les pages propres sauvegardées par des fichiers et issues de lectures ponctuelles restent indéfiniment imputées au cgroup du processus, ce qui consomme des centaines de mégaoctets de RAM physique.
  • Conservation du tas anonyme après une activité intense : les services qui connaissent des pics d'activité temporaires, tels que les pics de découverte audio et BLE Bluetooth, la validation TLS du portail captif NetworkStack ou les assistants d'interface utilisateur passant en arrière-plan, peuvent conserver des pages de tas anonymes froides longtemps après la fin de l'activité intense.

L'application de déclarations <memory-budget> calibrées (limites souples memory.high, généralement de 30 Mo à 190 Mo) aux applications système offre deux avantages principaux :

  • Récupération de page proactive et localisée : lorsqu'un processus système dépasse son budget memory.high configuré, le noyau Linux dirige la récupération de page spécifiquement vers ce cgroup, en supprimant d'abord les pages froides sauvegardées par des fichiers et en compressant les pages anonymes inactives vers ZRAM sans attendre la pression globale de la mémoire de l'appareil.
  • Plus de RAM disponible pour les applications au premier plan et le pool d'applications mises en cache : la limitation des caches de fichiers froids dans les services système toujours activés permet de récupérer des dizaines à des centaines de mégaoctets de RAM physique sur l'appareil, ce qui réduit les à-coups de l'interface utilisateur, diminue la pression de lancement de l'appareil photo et maintient plus d'applications utilisateur actives dans l'état mis en cache.

Configurer les budgets de mémoire des applications système

Les applications système utilisent la même syntaxe de fichier manifeste <memory-budget>, la même mise en cascade de l'état du processus, la même mise à l'échelle de la densité d'affichage et la même règle de résolution de la dernière clause correspondante que les applications générales. Pour obtenir la liste complète des attributs, consultez la documentation de référence sur les attributs XML.

Les OEM peuvent déclarer des éléments <memory-budget> directement dans le fichier AndroidManifest.xml d'une application système ou les fusionner à partir d'un fichier XML distinct à l'aide de la propriété additional_manifests dans Android.bp. Par convention, l'AOSP stocke les budgets des applications système de référence dans des fichiers *-budget.xml autonomes (soit dans frameworks/base/core/memory_budgets/ pour les packages de la plate-forme de base, soit avec des modules individuels tels que packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ et packages/services/Telephony/) et les associe à additional_manifests :

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

Champ d'application au niveau du package (UID) ou au niveau du processus (PID)

Le fait de déclarer un budget de référence au niveau du package (<application>) ou un budget multiprocessus (<processes><process>) détermine si le noyau applique memory.high au niveau du cgroup UID (/sys/fs/cgroup/uid_<uid>/memory.high) ou au niveau du cgroup par PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), comme illustré dans la figure 4 :

  • Budget au niveau du package (<application>) : à utiliser lorsque l'APK exécute un seul processus sous son propre UID dédié (par exemple, Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur ou AppSearch).
  • Budget au niveau du processus (<processes><process>) : à utiliser lorsque l'APK partage android:sharedUserId (par exemple, android.uid.phone avec l'UID 1001 ou android.uid.system avec l'UID 1000) avec d'autres packages système afin que les autres processus partageant cet UID ne soient pas limités ensemble, ou lorsqu'un seul APK exécute plusieurs processus avec des rôles de mémoire distincts (par exemple, le daemon MediaProvider en arrière-plan et le sous-processus d'UI :PhotoPicker).

Hiérarchie Cgroup v2 comparant l'application de memory.high au niveau de l'UID pour les UID dédiés à l'application de memory.high par PID pour les UID système partagés et les APK multiprocessus

Figure 4. Plafonds de mémoire au niveau de l'UID et au niveau du PID.

L'exemple suivant montre MediaProvider-budget.xml, qui limite les budgets par niveau de RAM au processus MediaProvider par défaut (<process>) et un budget mis à l'échelle de l'affichage au processus d'interface utilisateur :PhotoPicker. Chaque attribut android:maxDeviceRamMb correspond aux appareils dont la RAM physique totale (MemTotal) est inférieure ou égale au seuil spécifié en Mo (ou s'applique à n'importe quelle capacité de RAM lorsqu'il est omis). Les clauses sont classées de la valeur par défaut sans contrainte au seuil maxDeviceRamMb le plus bas, de sorte que la règle de résolution de la dernière clause correspondante sélectionne le niveau applicable le plus strict :

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

Comment les budgets de référence AOSP ont été calibrés sur les appareils Google Pixel

Les budgets de mémoire de l'application système de référence dans l'AOSP ont été calibrés sur les appareils Google Pixel, avec une RAM physique de 6 Go à 16 Go (y compris les téléphones, les pliables et les tablettes de 6 Go, 8 Go, 12 Go et 16 Go) et des résolutions d'écran allant d'environ 1,2 MP (720 x 1 600) à environ 4,5 MP (2 076 x 2 152).

La calibration a combiné deux sources de données complémentaires : la télémétrie de champ cgroup v2 à l'échelle du parc et les balayages memory.high d'appareils physiques contrôlés sur les parcours utilisateur critiques (CUJ).

Analyse de la télémétrie cgroup v2 à l'échelle du parc

Des instantanés cgroup v2 périodiques ont été collectés sur l'ensemble du parc d'appareils et segmentés par niveau de RAM physique (≤ 8 Go, 12 Go et ≥ 16 Go) et par bucket d'état de processus (foreground, perceptible, background et cached). Six principes ont guidé l'analyse statistique :

  • Décomposer memory.current en ensemble de travail et en cache de fichiers récupérable : étant donné que memory.high régit la charge de cgroup résidente (memory.current), chaque instantané a été décomposé à l'aide de memory.stat en tas anonyme (anon), en pages anonymes échangées (swap) et en cache de pages basé sur des fichiers (file), comme illustré à la figure 5. La demande anonyme incompressible totale (anon + swap au P99) établit le plancher minimal de l'ensemble de travail pour le budget afin que le processus ne risque pas d'épuiser la ZRAM, tandis qu'un écart important entre memory.current au P99 et anon + swap au P99 indique un cache de pages de fichiers que memory.high peut récupérer, à condition que ce cache ne soit pas mappé et qu'il soit froid.
  • Prendre en compte la mémoire que memory.high ne peut pas récupérer : la mémoire partagée et les fichiers ne sont pas tous des caches froids. Les pages verrouillées (unevictable dans memory.stat, comme la mémoire partagée verrouillée avec mlock ou la RAM d'invité verrouillée d'une machine virtuelle dont les processus hôtes sont facturés au cgroup de l'application) définissent un plancher strict sous le budget, et les pages mappées (file_mapped) font souvent partie de l'ensemble de travail actif. Vérifiez unevictable, shmem et file_mapped avant de traiter la mémoire de fichiers comme récupérable. Par exemple, environ 100 Mo de mémoire AppSearch sont verrouillés et ne peuvent pas être supprimés. Son budget couvre donc ce seuil, ainsi que son ensemble de travail d'hôte actif, avec une marge de croissance pour la région verrouillée.
  • Filtrer la dilution de l'état inactif : les fournisseurs de contenu (CalendarProvider) et les outils pour les développeurs à la demande (Traceur) passent 70% à 99,9% des instantanés de champ inactifs à environ 1 Mo à 3 Mo. Le calcul des centiles bruts sur tous les instantanés dilue considérablement les estimations des 95e et 99e centiles. À la place, les centiles et les intervalles de confiance ont été calculés sur des instantanés actifs non dilués (en filtrant les instantanés CACHED et les instantanés de référence inactifs).
  • Utiliser des intervalles de confiance regroupés par appareil : étant donné qu'un seul appareil fournit de nombreux instantanés périodiques au fil du temps, le fait de traiter les instantanés comme des échantillons indépendants sous-estime l'incertitude de la queue de distribution. Toutes les estimations de l'ensemble de travail P95 et P99 ont été évaluées à l'aide d'intervalles de confiance prudents de 95 % regroupés par appareil (en utilisant le nombre d'appareils distincts N_dev comme taille d'échantillon effective).
  • Prendre en compte les pics temporaires avec memory.peak : l'échantillonnage périodique peut manquer des pics d'allocation élevée de courte durée (comme la validation TLS du portail captif NetworkStack et les vérifications DNS simultanées, ou l'initialisation A2DP et BLE Bluetooth). Pour les services en rafale, les budgets ont été dimensionnés pour couvrir le 90e au 95e centile des pics d'utilisation de memory.peak du noyau sur l'ensemble des niveaux de RAM.
  • Vérifiez qu'un niveau de RAM reflète la RAM, et non la génération de l'appareil : les appareils plus récents ont tendance à disposer de plus de RAM et de fonctionnalités. Par conséquent, un ensemble de travail qui augmente avec le niveau de RAM peut en réalité augmenter avec les fonctionnalités. Avant d'ajouter un niveau maxDeviceRamMb, segmentez les données par modèle d'appareil. Par exemple, com.android.phone atteint le même P99 anon + swap (environ 85 à 90 Mo) sur les téléphones de 12 Go et 16 Go de la même génération, tandis qu'un appareil pliable plus ancien de 12 Go utilise 40 Mo. Son budget utilise donc une seule valeur sans niveau de RAM, de sorte qu'un appareil doté de moins de RAM qui intègre les nouvelles fonctionnalités de téléphonie ne soit pas sous-budgétisé.

Décomposition de la mémoire cgroup au fil du temps en un ensemble de travail anonyme incompressible, une fenêtre de cache de fichiers actifs et un cache de fichiers froids à usage unique récupéré au-delà de memory.high

Figure 5. Ensemble de travail par rapport au cache de fichiers récupérable.

CUJ sur appareil physique et balayages de mise à l'échelle de l'affichage

Pour valider les budgets dérivés de la télémétrie et calibrer les budgets proportionnels à l'affichage (comme :PhotoPicker) et les budgets d'arrière-plan spécifiques à l'état (comme Traceur), des balayages memory.high en direct ont été exécutés sur des appareils physiques de 6 Go à 16 Go de RAM :

  • Trouver le point d'inflexion zéro défaut : lors de l'exécution de CUJ de bout en bout (comme le défilement rapide de la grille de photos, la navigation dans les albums, le balayage de l'aperçu des photos et vidéos haute résolution en plein écran, ainsi que l'enregistrement et la sauvegarde des traces), memory.high a été réduit tout en mesurant les compteurs memory.stat de cgroup (workingset_refault_anon, workingset_refault_file, pgscan et pgsteal) et memory.events (high). Comme illustré dans la figure 6, le budget optimal se situe juste au-dessus du point d'inflexion zéro défaut. Le seuil de zéro défaut est le seuil auquel le noyau récupère le cache de fichiers froids à usage unique (pgsteal > 0) sans déclencher de thrashing de l'ensemble de travail actif (workingset_refault == 0).
  • Validation de la mise à l'échelle des pixels d'affichage sur différentes résolutions : les tests de :PhotoPicker sur quatre résolutions d'affichage physiques et émulées, d'environ 1,2 Mpx (720 × 1 600) à environ 4,5 Mpx (2 076 × 2 152, écran intérieur déplié), ont confirmé que son ensemble de travail de bitmap Glide et ViewPager2 actif évolue de manière linéaire avec le nombre de pixels d'affichage. La référence sans affichage varie également selon l'appareil : pour un nombre de pixels presque identique, le point d'inflexion sans défaut était d'environ 140 Mo sur l'écran externe d'un appareil pliable de 16 Go, contre 110 à 125 Mo sur un téléphone de 6 Go. Une référence de 85 Mo plus 25 octets par pixel d'affichage (android:maxMb="85" et android:additionalBytesPerDisplayPixel="25") reste au-dessus du point d'inflexion mesuré sur chaque écran testé (~110 Mo sur HD+, ~145 Mo sur FHD+ et ~190 Mo sur les écrans dépliés).
  • Vérification de la récupération de mémoire ART en arrière-plan : dans cgroup v2, les pages d'images boot.art et de framework partagées par Zygote restent chargées dans le cgroup de Zygote et sont traitées comme un espace immunisé lors de la récupération de mémoire ART CMC (Concurrent Mark-Compact). Les tests de contrainte ont confirmé que lorsqu'une application d'interface utilisateur système passe à background, le resserrement de memory.high (par exemple, à 30 Mo sur Traceur) limite proprement le tas de mémoire privé et le cache de pages sans provoquer de thrashing de récupération de mémoire ni d'activité d'échange.

Graphique de la récupération du cache à froid (pgsteal) et du thrashing de l'ensemble de travail (workingset_refault) lorsque memory.high est réduit pendant un CUJ, mettant en évidence le point d'inflexion sans refault et le budget cible

Figure 6. Trouver le point d'inflexion de zéro défaut lors d'un CUJ.

Résumé des budgets des applications système de référence AOSP

Le tableau suivant récapitule les budgets de référence calibrés dans l'AOSP pour les différents niveaux de RAM et états de processus :

Package ou processus système Champ d'application ≤ 8 Go de RAM 12 Go de RAM ≥ 16 Go de RAM Pilote de calibrage principal
com.android.bluetooth UID (<application>) 80 Mo 95 Mo 110 Mo Couvre les pics de memory.peak de durée de vie P90 (~75 à 110 Mo) et > 2x de memory.current actif P99.
com.android.nfc UID (<application>) 50 Mo 60 Mo 60 Mo Couvre l'émulation de carte hôte active P99 (jusqu'à environ 50 Mo) et P90 à P95 memory.peak tout en réduisant le cache de fichiers inactifs.
com.android.providers.calendar UID (<application>) 70 Mo par défaut (premier plan)
50 Mo perceptible et background
L'ensemble de travail actif est invariant entre les niveaux de RAM. Le budget en arrière-plan conserve une marge de manœuvre anonyme pour les alarmes de rappel tout en réduisant le cache de pages SQLite lors des synchronisations.
com.android.traceur UID (<application>) 85 Mo par défaut (premier plan et perceptible)
30 Mo background
Conserve une marge de 85 Mo pendant l'enregistrement et l'exportation de traces actives (TraceService), tout en limitant l'état d'inactivité et de diffusion à 30 Mo.
com.android.providers.media.module (principal) PID (<process>) 60 Mo 75 Mo 85 Mo Fournit une marge d'environ 10 à 30 Mo par rapport au anon + swap P99 tout en récupérant le cache des fichiers SQLite et des vignettes inactifs.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 Mo + 25 octets par pixel
(~110 Mo en HD+, ~145 Mo en FHD+, ~190 Mo en ~4,5 MP plié)
S'ajuste proportionnellement au nombre de pixels de l'écran (additionalBytesPerDisplayPixel="25") pour correspondre au point d'inflexion de l'ensemble de travail du bitmap Glide.
com.android.networkstack UID (<application>) 65 Mo 70 Mo 85 Mo Mémoire principalement anonyme, dimensionnée pour la durée de vie P95 memory.peak afin de couvrir les pics de TLS, DNS et DHCP du portail captif.
com.android.appsearch.apk UID (<application>) 130 Mo Couvre environ 100 Mo de mémoire verrouillée et non expulsable, plus un ensemble de travail d'hôte actif d'environ 5 Mo, avec une marge de croissance pour la région verrouillée. Zéro défaut de page mesuré à ≥ 125 Mo pendant la récupération du cache de pages froid (jusqu'à environ 330 Mo au 99e centile).
com.android.phone (TeleService) PID (<process>) 120 Mo Limité par PID sur l'UID partagé 1001. Le jeu de travail suit la génération de l'appareil plutôt que la RAM. Un seul budget se situe donc environ 30 Mo au-dessus du anon + swap P99 le plus élevé par modèle.

Comment les OEM peuvent recalibrer les budgets des applications système pour leurs propres appareils

Les OEM qui personnalisent les applications système (par exemple, en ajoutant des profils Bluetooth de fournisseurs, des extensions de téléphonie personnalisées ou des indexeurs multimédias OEM) ou qui fournissent des services système préinstallés supplémentaires peuvent recalibrer les budgets existants ou créer des fichiers *-budget.xml pour leur portefeuille de matériel.

Collecter les profils de mémoire cgroup v2 actifs

Pour chaque processus système cible, lisez ses nœuds de mémoire cgroup v2 sous /sys/fs/cgroup/uid_<uid>/pid_<pid>/ pour les charges de travail et les niveaux de RAM représentatifs :

  • memory.current : mémoire résidente actuelle facturée au cgroup (à l'exclusion des pages partagées par Zygote)
  • memory.peak : pic de memory.current depuis le début du processus
  • memory.swap.current : utilisation actuelle de l'espace d'échange compressé dans la ZRAM
  • memory.stat : répartition de anon, file, pgscan, pgsteal, workingset_refault_anon et workingset_refault_file
  • memory.events : nombre cumulé de violations de high (événements de limitation et de récupération memory.high)

Lorsque vous agrégez les données de télémétrie d'appareils de test ou de terrain, séparez les états de processus actifs (TOP, FOREGROUND_SERVICE, liaisons actives et synchronisations actives) des états CACHED ou PERSISTENT inactifs afin que les instantanés inactifs ne faussent pas vos calculs de jeu de travail P95 et P99 à la baisse.

Séparer le tas incompressible du cache de fichiers récupérable

Comparez le 99e centile de anon + swap au 99e centile de memory.current :

  • Services dominés par le cache de fichiers (file &gg; anon, tels que les fournisseurs de bases de données, les indexeurs de recherche et les démons de journalisation) : définissez android:maxMb pour fournir une marge confortable (généralement de 1,5x à 4x) par rapport au anon + swap P99, plus une fenêtre de travail pour les pages de fichiers actives. Cette configuration permet au noyau d'expulser en continu les pages de fichiers non mappées inactives sans toucher au tas anonyme. Si unevictable ou file_mapped est volumineux (comme avec la mémoire verrouillée ou les bases de données mappées en mémoire), comptabilisez également cette empreinte comme un ensemble de travail.
  • Services dominés par un tas anonyme ou en rafale (anon constitue la majeure partie de memory.current, comme les piles de mise en réseau ou de connectivité) : dimensionnez android:maxMb pour couvrir les percentiles 90 à 95 de memory.peak afin que les pics temporaires ne déclenchent pas de permutation ZRAM synchrone sur les threads sensibles à la latence.
  • Processus d'interface utilisateur gourmands en affichage (tels que les sélecteurs, les galeries ou les assistants d'interface utilisateur système) : mesurez l'empreinte de référence hors interface utilisateur et l'empreinte bitmap incrémentielle par mégapixel sur vos écrans de résolution la plus faible et la plus élevée, et exprimez le budget à l'aide de android:maxMb plus android:additionalBytesPerDisplayPixel.

Balayer memory.high sur les appareils physiques

Avant d'allouer un nouveau budget à la compilation, utilisez am memory-limiter manual (ou écrivez directement dans le nœud cgroup memory.high du processus sur une compilation userdebug) pour tester les limites candidates lors de l'exécution de vos tests CUJ automatisés :

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

Surveillez memory.stat et memory.events avant et après le CUJ :

  • Budget trop faible : workingset_refault_anon ou workingset_refault_file augmente fortement pendant le CUJ, memory.swap.current augmente continuellement pendant le travail actif au premier plan, ou la latence de la frame CUJ ou de Binder régresse. Augmentez le budget au-delà du point d'inflexion par défaut.
  • Le budget est trop élevé : memory.events:high reste 0 même après des charges de travail d'indexation ou de streaming de fichiers importantes, et file dans memory.stat augmente de manière illimitée avec les pages froides. Réduisez le budget pour l'ensemble de travail actif.
  • Le budget est bien calibré : le memory.current en régime permanent reste inférieur à memory.high en fonctionnement normal. Lors de pics d'activité importants liés aux fichiers ou au cache, pgsteal récupère les pages de fichiers froids, tandis que workingset_refault_anon reste proche de zéro et que la latence CUJ reste inchangée.