Capacidade de memória do sistema

No Android 17 e versões mais recentes, a plataforma controla a memória do processo do sistema usando dois mecanismos complementares do cgroup v2 do Linux que funcionam com o limitador de memória geral do aplicativo:

  • Níveis de proteção do daemon do sistema (SystemMemoryBudgetManager com memory.min): as reservas de memory.min do cgroup v2 com ajuste automático (definidas como 90% do conjunto de trabalho residente de cada daemon e limitadas globalmente a 10% da RAM física total) protegem daemons interativos críticos e processos de shell (system_server, surfaceflinger, o HAL de gráficos composer, audioserver, cameraserver, netd, SystemUI e o inicializador da página inicial) sob pressão de memória, substituindo a fixação de arquivos estáticos PinnerService mlock estática.
  • Limites de memória do app do sistema (<memory-budget> com memory.high): os limites flexíveis do cgroup v2 memory.high calibrados para carga de trabalho (normalmente de 30 MB a 190 MB) declarados em arquivos de manifesto para apps do sistema persistentes e em segundo plano (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch e TeleService) recuperam proativamente o cache de páginas de arquivos frios e o heap anônimo pós-burst antes que ocorra pressão de memória global.

A tabela a seguir compara os dois mecanismos:

Dimensão Níveis de proteção do daemon do sistema Limites de memória do app do sistema
Serviço de plataforma SystemMemoryBudgetManager MemoryLimiter
Controle do Cgroup v2 memory.min de piso de proteção rígido memory.high de limite de recuperação flexível
Processos de destino Daemons interativos essenciais para latência e interface do shell (system_server, surfaceflinger, HAL composer, audioserver, cameraserver, netd, SystemUI e o app Home) Apps do sistema persistentes e headless, provedores de conteúdo e serviços em segundo plano (como Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch e TeleService)
Modelo de dimensionamento Ajuste automático no ambiente de execução (90% do conjunto de trabalho estabelecido, limitado globalmente a 10% da RAM total) sem tabelas de ajuste por dispositivo Atributos XML declarativos do manifesto <memory-budget> calibrados por nível de RAM, estado do processo e contagem de pixels da tela
Principal benefício Evita instabilidade na interface interativa e travamentos de ativação sob pressão de memória ao substituir listas de arquivos estáticos PinnerService mlock Remove continuamente o cache de arquivos únicos frios e o heap pós-burst para deixar mais RAM física para apps em primeiro plano e em cache

Para mais informações sobre a sintaxe de <memory-budget>, atributos XML, APIs de tempo de execução e contabilização de memória do SO, consulte Definir orçamentos de memória do app e Noções básicas e solução de problemas de memória do Android.

Proteger daemons interativos críticos com o SystemMemoryBudgetManager

O SystemMemoryBudgetManager mantém os processos que estão diretamente no caminho interativo do usuário residentes na RAM física durante a pressão de memória. Em vez de fixar uma lista estática de arquivos APK, ODEX e VDEX na memória com mlock (como PinnerService faz para config_defaultPinnerServiceFiles e config_pinnerHomePinBytes), o SystemMemoryBudgetManager atribui limites mínimos dinâmicos de cgroup v2 memory.min aos seguintes processos:

  • Fração do sistema principal (/sys/fs/cgroup/system): system_server, surfaceflinger, a HAL de gráficos composer, audioserver, cameraserver e netd, além de qualquer sistema nativo específico do formato ou daemons de fornecedor listados em config_systemMemoryBudgetAdditionalSystemProcesses (por exemplo, android.hardware.perceptionhost e auracamerahal em dispositivos XR).
  • Fração de app interativa (/sys/fs/cgroup/apps): o processo principal do pacote SystemUI configurado (config_systemUi) e o processo principal do usuário atual que tem a função de tela inicial.

Proteger o detentor da função de início junto com config_systemUi garante a cobertura em todos os formatos. Em formatos em que config_systemUi é um pacote de compatibilidade leve (como o Wear OS, em que o mostrador do relógio, as notificações e os blocos são executados no app Home), o shell voltado ao usuário ainda recebe proteção total do conjunto de trabalho. Se a intent Home for resolvida para o pacote config_systemUi, o processo será protegido uma vez. Somente o processo principal de cada pacote (processName == packageName) recebe um nível de proteção. Assim, os subprocessos auxiliares (como :screenshot) não consomem o orçamento de proteção.

Política de ajuste automático e delegação de cgroup em três níveis

Como o conjunto de trabalho de um daemon interativo varia muito entre formatos e capacidades de RAM (por exemplo, o SystemUI usa cerca de 45 MB em um smartwatch, 60 MB em um smartphone de 6 GB e 180 MB em um smartphone de 16 GB), as tabelas de consulta estáticas por dispositivo são frágeis. Em vez disso, SystemMemoryBudgetManager deriva os limites diretamente do consumo residente de cada processo usando três constantes:

Parâmetro da política Valor padrão Comportamento
Preço mínimo por processo 90% do memory.current liquidado menos o cache de arquivos não mapeados Protege o heap anônimo, o slab e o código mapeado quente, deixando os 10% mais frios recuperáveis para que as páginas genuinamente frias ainda possam ser removidas. O cache de arquivos não mapeados (max(0, file - shmem - file_mapped) de memory.stat) é subtraído antes de aplicar a proporção de 90% para que o cache de verificação de pacotes de inicialização ou as leituras de recursos únicos não inflem os pisos.
Limite global de RAM 10% da RAM total (config_systemMemoryBudgetLowKb) Corresponde à proteção PinnerService config_pinnerMaxPinnedMemoryPercentage. Se a soma dos preços mínimos de 90% exceder 10% de MemTotal, todos os preços mínimos serão reduzidos proporcionalmente pelo mesmo fator.
Cadência de atualização 1, 4, 16 e 64 s após o início e, em seguida, a cada 30 min Uma série de estabilização geométrica converge no conjunto de trabalho pós-inicialização de cada daemon sem codificar atrasos de aquecimento por dispositivo, e uma atualização de estado estável de 30 minutos rastreia o crescimento e a redução gradual do conjunto de trabalho ao longo da vida útil do dispositivo.

No controlador de memória cgroup v2 do Linux (mm/memcontrol.c:effective_protection), a proteção memory.min efetiva de um cgroup de folha é limitada pelo memory.min configurado nos cgroups pai e ancestral. Definir memory.min apenas em um diretório folha pid_<pid> enquanto uid_<uid>/memory.min é 0 resulta em zero proteção efetiva. Por outro lado, definir um memory.min fixo apenas na fração /sys/fs/cgroup/system superior distribui a proteção não reivindicada entre todos os UIDs e processos filhos proporcionalmente (memory_recursiveprot), permitindo que processos de diagnóstico ou secundários que compartilham uid_1000 (android.uid.system) desviem a proteção de surfaceflinger e audioserver.

Para fornecer isolamento exato por daemon, o SystemMemoryBudgetManager grava proteção em todos os três níveis da hierarquia do cgroup v2 em cada atualização, conforme mostrado na figura 1:

  1. Nó do processo de folha (pid_<pid>/memory.min): definido como o limite mínimo do conjunto de trabalho de 90% (escalonado) do processo.
  2. Nó de UID (uid_<uid>/memory.min): definido como a soma exata dos níveis de folha pid_* gerenciados, para que os processos não gerenciados que compartilham esse UID (como outros daemons AID_SYSTEM ou pacotes sharedUserId="android.uid.system") tenham memory.min = 0 e não possam reivindicar proteção.
  3. Fração de nível superior (/sys/fs/cgroup/system/memory.min e /sys/fs/cgroup/apps/memory.min): definida como a soma exata dos filhos uid_*. Todos os valores memory.low desatualizados em nós gerenciados são limpos para 0, e todos os diretórios uid_* que foram gerenciados em uma atualização anterior (como um inicializador de início anterior depois que o usuário troca de inicializador) são redefinidos para 0.

Hierarquia de proteção memory.min do cgroup v2 de três níveis em /sys/fs/cgroup/system e /sys/fs/cgroup/apps, mostrando os limites de processos de folha somados em nós de UID e de fração, enquanto os processos irmãos desprotegidos permanecem em zero

Figura 1. Delegação de piso de proteção de três níveis em fatias de cgroup.

Por que memory.min é usado em vez de memory.low em kernels MGLRU

Na LRU multigeracional (MGLRU, na sigla em inglês, o mecanismo de recuperação de página padrão no Android 6.6 e kernels mais recentes), memory.low e memory.min se comportam de maneira diferente em mm/vmscan.c:shrink_one:

  • memory.low (adiamento de passagem única): um cgroup com uso abaixo de memory.low é rotacionado para o final do LRU do memcg (MEMCG_LRU_TAIL) e ignorado em uma única passagem de recuperação. Na próxima passagem sob pressão de memória sustentada, o kernel recupera o cgroup por completo e registra um evento MEMCG_LOW. Em testes de estresse de dispositivos físicos com um consumo de memória anônimo que chega a 85% da RAM física, o memory.low permitiu que o SystemUI em um smartwatch caísse de ~45 MB para ~5 a 10 MB, e o audioserver em um dobrável de 16 GB caísse de 15 MB para 2 MB em comparação com um mínimo de ~14 MB.
  • memory.min (piso de proteção rígido): um cgroup com uso abaixo de memory.min é ignorado pela recuperação global. Como mostrado na figura 2, sob a mesma carga de trabalho de pressão de RAM de 85%, o memory.min descarta apenas os 10% mais frios de cada daemon e mantém SystemUI, surfaceflinger e audioserver em seus pisos de 90% com zero refaults anônimos e nenhum aumento nas finalizações de processo lmkd.

O memory.min oferece uma reserva física rígida que corresponde à proteção da fixação do PinnerService mlock que ele substitui, com a vantagem de que o LRU do kernel (em vez de uma lista de arquivos estáticos) escolhe quais páginas de código, heap e gráficos quentes permanecem residentes.

Comparação da memória residente do daemon à medida que a pressão da memória do sistema aumenta de 40% para 95% da RAM física, mostrando que memory.min se mantém no limite de 90%, enquanto memory.low cai após uma única passagem de adiamento do MGLRU

Figura 2. Retenção do conjunto de trabalho do daemon sob pressão de memória crescente.

Pré-aquecimento de inicialização e recuperação de cache de arquivos não mapeados pós-inicialização

A substituição do mlock permanente de PinnerService de config_defaultPinnerServiceFiles requer a coordenação de dois comportamentos no momento da inicialização:

  • Pré-aquecimento de inicialização em segundo plano único (PinnerService): pular PinnerService mlock sem aquecer o cache da página desloca cerca de 95 MB de leituras de disco de APK, ODEX e VDEX de SystemUI para o caminho de inicialização síncrona quando SystemUI é iniciado (OnBootPhase_550), adicionando de 300 a 450 ms à conclusão da inicialização. Chamar madvise(MADV_WILLNEED) não é suficiente porque force_page_cache_ra no kernel fixa uma única chamada MADV_WILLNEED a uma janela de leitura antecipada (normalmente de 128 KB a 2 MB). Em vez disso, quando SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning está ativo, PinnerService mapeia cada arquivo em config_defaultPinnerServiceFiles em uma linha de execução em segundo plano durante onStart, preenche o arquivo de forma síncrona no cache de página com Os.mlock e chama imediatamente Os.munlock, munmap e close. Isso preserva o tempo de inicialização rápida, deixando zero bytes permanentemente bloqueados para que as páginas de inicialização a frio possam ser removidas mais tarde.
  • Exclusão e recuperação do cache de verificação de pacotes de inicialização do system_server:durante a inicialização, o PackageManagerService verifica os APKs instalados, deixando de 1,5 a 2,5 GB de cache de página de arquivo não mapeado frio cobrado do cgroup system_server. Se incluído no cálculo de memory.min de system_server, esse cache de inicialização a frio satura o limite global de 10% de RAM e reduz proporcionalmente os limites de proteção de surfaceflinger, SystemUI e audioserver. Para evitar a falta de limite, o SystemMemoryBudgetManager lê memory.stat para system_server, calcula o cache de arquivos não mapeados como max(0, file - shmem - file_mapped) e subtrai esse cache não mapeado de memory.current antes de calcular o limite mínimo de 90%, conforme mostrado na Figura 3. Além disso, 10 segundos após PHASE_BOOT_COMPLETED, o gerenciador realiza uma gravação única de <unmapped_file> swappiness=0 no nó memory.reclaim de system_server no Linux 6.2 e em kernels mais recentes para remover proativamente o cache frio de verificação de pacotes sem trocar o heap anônimo ou remover páginas de código e DEX mapeadas. Em kernels mais antigos (como android14-6.1) que rejeitam swappiness=0 com EINVAL, o gerenciador pula a gravação memory.reclaim em vez de voltar para uma gravação de recuperação não qualificada que trocaria o heap anônimo durante a inicialização.

Linha do tempo da memória system_server após a inicialização, mostrando as atualizações de estabilização geométrica em 1, 4, 16 e 64 segundos, a exclusão do cache de verificação de pacotes de inicialização não mapeado do limite mínimo de 90% de memory.min e memory.reclaim único com swappiness=0

Figura 3. Convergência do piso de proteção e recuperação de cache após a inicialização.

Requisitos do kernel e sobreposições de configuração do dispositivo

O SystemMemoryBudgetManager depende de mem_cgroup_calculate_protection no kernel do Linux para propagar memory.min em cgroups aninhados. Os kernels comuns do Android android15-6.6 e android16-6.12 incluem suporte completo à proteção aninhada do MGLRU. Os dispositivos que executam o android14-6.1 com o MGLRU ativado exigem três backports upstream em mm/vmscan.c e mm/memcontrol.c para que a fração intermediária vazia e os cgroups de UID propaguem a proteção para os cgroups de processo de folha:

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

Os OEMs podem personalizar ou desativar o SystemMemoryBudgetManager usando as seguintes sobreposições de recursos do framework:

  • config_systemMemoryBudgetLowKb (número inteiro, KB): 0 (padrão) limita a soma de todos os pisos de memory.min a 10% da RAM física total. Um valor positivo define um limite explícito em KB, e um valor negativo desativa SystemMemoryBudgetManager no dispositivo (mantendo a fixação estática mlock legada de PinnerService).
  • config_systemMemoryBudgetHighKb (número inteiro, KB): 0 (padrão) deixa /sys/fs/cgroup/system/memory.high em max. Um valor positivo define um limite máximo de fração superior memory.high (precisa ser maior ou igual ao limite mínimo).
  • config_systemMemoryBudgetAdditionalSystemProcesses (matriz de strings): lista outros nomes de processos daemon de fornecedores ou sistemas nativos essenciais para a latência em /sys/fs/cgroup/system (correspondentes a /proc/<pid>/comm ou ao nome de base de argv[0] em /proc/<pid>/cmdline para UIDs de sistemas e fornecedores em 10000) para proteger com limites de 90% de memory.min, além dos seis daemons padrão do sistema. Use essa sobreposição em vez de definir memory.min em init.rc, porque SystemMemoryBudgetManager recalcula uid_<uid>/memory.min como a soma dos processos de folha gerenciados e limpa os nós uid_* não gerenciados em cada atualização. Por exemplo, um dispositivo XR pode proteger os daemons de HAL de câmera e rastreamento espacial com a seguinte sobreposição:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (string): nome do pacote do app SystemUI da plataforma, resolvido com MATCH_SYSTEM_ONLY.

Para inspecionar os limites mínimos de SystemMemoryBudgetManager e os nós de cgroup ativos em um dispositivo, execute:

# 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

Vincular apps do sistema persistentes e em segundo plano com orçamentos de memória

Enquanto o SystemMemoryBudgetManager protege daemons interativos contra a recuperação usando limites de memory.min, muitos outros apps do sistema e serviços de plataforma privilegiados são executados em segundo plano por longos períodos como processos PERSISTENT, serviços em primeiro plano vinculados ou provedores de conteúdo consultados com frequência. Sem limites de memória explícitos, esses processos não são restritos até que alcancem os limites de vários gigabytes do limitador de memória (como 4 GB a 10 GB para apps visíveis e 2 GB a 5 GB para apps em segundo plano) ou até que a pressão de memória global acione a atividade do kswapd em todo o sistema e do eliminador de pouca memória (lmkd).

Sem restrições, os processos do sistema em segundo plano geralmente acumulam grandes quantidades de memória fria que não faz parte do conjunto de trabalho ativo:

  • Cache de página de arquivo ilimitado:serviços que indexam mídia, pesquisam bancos de dados ou sincronizam tabelas de agenda e provedor (como AppSearch, MediaProvider e CalendarProvider) mapeiam ou transmitem arquivos grandes no disco. Sem um limite de cgroup memory.high, as páginas limpas com suporte de arquivo de leituras únicas permanecem carregadas no cgroup do processo indefinidamente, consumindo centenas de megabytes de RAM física.
  • Retenção de heap anônimo pós-burst:serviços que têm picos de atividade transitórios, como bursts de áudio Bluetooth e descoberta de BLE, validação de TLS de portal cativo NetworkStack ou auxiliares de interface em transição para o segundo plano, podem reter páginas de heap anônimo frio muito tempo depois que o burst é concluído.

Aplicar declarações <memory-budget> calibradas (limites flexíveis de memory.high, normalmente de 30 MB a 190 MB) a apps do sistema oferece dois benefícios principais:

  • Recuperação de página proativa e localizada:quando um processo do sistema ultrapassa o orçamento de memory.high configurado, o kernel do Linux direciona a recuperação de página especificamente para esse cgroup, removendo primeiro as páginas frias com backup de arquivos e compactando as páginas anônimas inativas para a ZRAM sem esperar pela pressão global da memória do dispositivo.
  • Mais RAM disponível para aplicativos em primeiro plano e o pool de apps em cache:vincular caches de arquivos frios em serviços do sistema sempre ativos recupera de dezenas a centenas de megabytes de RAM física em todo o dispositivo, reduzindo instabilidades na interface, diminuindo a pressão de inicialização da câmera e mantendo mais apps do usuário ativos no estado em cache.

Configurar orçamentos de memória de apps do sistema

Os apps do sistema usam a mesma sintaxe de manifesto <memory-budget>, cascata de estado do processo, escalonamento de densidade de exibição e regra de resolução de última cláusula correspondente que os apps gerais. Para ver a lista completa de atributos, consulte a referência de atributos XML.

Os OEMs podem declarar elementos <memory-budget> diretamente no AndroidManifest.xml de um app do sistema ou mesclá-los de um arquivo XML separado usando a propriedade additional_manifests em Android.bp. Por convenção, o AOSP armazena orçamentos de apps do sistema de referência em arquivos *-budget.xml independentes (em frameworks/base/core/memory_budgets/ para pacotes de plataforma principal ou ao lado de módulos individuais, como packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ e packages/services/Telephony/) e os anexa com additional_manifests:

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

Escopo no nível do pacote (UID) x no nível do processo (PID)

A declaração de um orçamento de referência no nível do pacote (<application>) ou de um orçamento de vários processos (<processes><process>) determina se o kernel aplica memory.high no nível do cgroup do UID (/sys/fs/cgroup/uid_<uid>/memory.high) ou no nível do cgroup por PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), conforme mostrado na Figura 4:

  • Orçamento no nível do pacote (<application>):use quando o APK executar um único processo no próprio UID dedicado (por exemplo, Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur ou AppSearch).
  • Orçamento no nível do processo (<processes><process>):use quando o APK compartilha android:sharedUserId (como android.uid.phone com UID 1001 ou android.uid.system com UID 1000) com outros pacotes do sistema para que outros processos que compartilham esse UID não sejam limitados juntos ou quando um único APK executa vários processos com funções de memória distintas (por exemplo, o daemon MediaProvider em segundo plano e o subprocesso de interface :PhotoPicker).

Hierarquia do cgroup v2 que compara a aplicação de memory.high no nível do UID para UIDs dedicados com a aplicação de memory.high por PID para UIDs de sistema compartilhados e APKs de vários processos

Figura 4. Limites de memória no nível do UID em comparação com o nível do PID.

O exemplo a seguir mostra MediaProvider-budget.xml, que define o escopo dos orçamentos em camadas de RAM para o processo MediaProvider padrão (<process>) e um orçamento dimensionado para exibição para o processo de interface :PhotoPicker. Cada atributo android:maxDeviceRamMb corresponde a dispositivos com RAM física total (MemTotal) igual ou inferior ao limite de MB especificado (ou se aplica a qualquer capacidade de RAM quando omitido). As cláusulas são ordenadas do padrão sem restrições até o menor limite de maxDeviceRamMb para que a regra de resolução de última cláusula correspondente selecione o nível aplicável mais restrito:

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

Como os orçamentos de referência do AOSP foram calibrados em dispositivos Google Pixel

Os orçamentos de memória do app do sistema de referência no AOSP foram calibrados em dispositivos Google Pixel que abrangem de 6 GB a 16 GB de RAM física (incluindo smartphones, dobráveis e tablets de 6 GB, 8 GB, 12 GB e 16 GB) e resoluções de tela que variam de ~1, 2 MP (720×1600) a ~4, 5 MP (2076×2152).

A calibragem combinou duas fontes de dados complementares: telemetria de campo do cgroup v2 em toda a frota e varreduras de memory.high em dispositivos físicos controlados em jornadas ideais do usuário (CUJs, na sigla em inglês) críticas.

Análise de telemetria do cgroup v2 em toda a frota

Os snapshots periódicos do cgroup v2 foram coletados em toda a frota de dispositivos e divididos por nível de RAM física (≤8 GB, 12 GB e ≥16 GB) e bucket de estado do processo (foreground, perceptible, background e cached). Seis princípios orientaram a análise estatística:

  • Decompor memory.current em conjunto de trabalho versus cache de arquivos recuperáveis:como memory.high controla a carga do cgroup residente (memory.current), cada snapshot foi decomposto usando memory.stat em heap anônimo (anon), páginas anônimas trocadas (swap) e cache de páginas com suporte de arquivo (file), como mostrado na Figura 5. A demanda anônima total incompressível (anon + swap no P99) estabelece o piso mínimo do conjunto de trabalho para o orçamento, de modo que o processo não corra o risco de esgotar a ZRAM, enquanto uma grande lacuna entre o P99 de memory.current e o P99 de anon + swap indica o cache de página de arquivo que o memory.high pode recuperar, desde que o cache esteja desmapeado e frio.
  • Considere a memória que o memory.high não pode recuperar:nem toda a memória de arquivos e compartilhada é cache frio. As páginas bloqueadas (unevictable em memory.stat, como a memória compartilhada bloqueada com mlock ou a RAM de convidado bloqueada de uma máquina virtual com processos de host cobrados no cgroup do app) definem um limite rígido abaixo do orçamento, e as páginas mapeadas (file_mapped) geralmente fazem parte do conjunto de trabalho ativo. Verifique unevictable, shmem e file_mapped antes de tratar a memória de arquivos como recuperável. Por exemplo, cerca de 100 MB de memória AppSearch estão bloqueados e não podem ser removidos. Portanto, o orçamento cobre esse limite mínimo e o conjunto de trabalho do host ativo, com espaço para que a região bloqueada cresça.
  • Filtrar a diluição do estado inativo:os provedores de conteúdo (CalendarProvider) e as ferramentas para desenvolvedores sob demanda (Traceur) passam de 70% a 99,9% dos snapshots de campo inativos em cerca de 1 MB a 3 MB. O cálculo de percentis brutos em todos os snapshots dilui severamente as estimativas de P95 e P99. Em vez disso, os percentis e os intervalos de confiança foram calculados com base em snapshots ativos não diluídos (filtrando snapshots de CACHED e snapshots de referência inativos).
  • Use intervalos de confiança agrupados por dispositivo:como um único dispositivo contribui com muitos snapshots periódicos ao longo do tempo, tratar os snapshots como amostras independentes subestima a incerteza da cauda. Todas as estimativas de conjunto de trabalho P95 e P99 foram avaliadas usando intervalos de confiança conservadores de 95% agrupados por dispositivo (com a contagem de dispositivos distintos N_dev como o tamanho da amostra efetiva).
  • Considere picos temporários com memory.peak:a amostragem periódica pode não detectar picos de curta duração e alta alocação, como a validação de TLS de portal cativo NetworkStack e sondagens de DNS simultâneas ou a inicialização de A2DP e BLE Bluetooth. Para serviços com picos de uso, os orçamentos foram dimensionados para cobrir do 90º ao 95º percentil de marcas d'água altas do kernel memory.peak durante o ciclo de vida em todos os níveis de RAM.
  • Confirme se um nível de RAM reflete a RAM, não a geração do dispositivo:dispositivos mais recentes tendem a ter mais RAM e mais recursos. Portanto, um conjunto de trabalho que aumenta com o nível de RAM pode estar aumentando com os recursos. Antes de adicionar um nível maxDeviceRamMb, divida os dados por modelo de dispositivo. Por exemplo, o com.android.phone alcança o mesmo P99 anon + swap (cerca de 85 a 90 MB) em smartphones de 12 GB e 16 GB da mesma geração, enquanto um dobrável mais antigo de 12 GB usa 40 MB. Portanto, o orçamento usa um único valor sem nível de RAM, para que um dispositivo com menos RAM que forneça os recursos de telefonia mais recentes não tenha um orçamento insuficiente.

Decomposição da memória do cgroup ao longo do tempo em conjunto de trabalho anônimo não compactável, janela de cache de arquivos ativos e cache de arquivos frios de uso único recuperado além de memory.high

Figura 5. Conjunto de trabalho versus cache de arquivos recuperável.

CUJ de dispositivo físico e varreduras de dimensionamento de tela

Para validar os orçamentos derivados de telemetria e calibrar os orçamentos proporcionais à tela (como :PhotoPicker) e os orçamentos em segundo plano específicos do estado (como Traceur), foram executadas varreduras de memory.high em tempo real em dispositivos físicos com 6 GB a 16 GB de RAM:

  • Como encontrar o ponto de inflexão de zero refault:ao executar CUJs de ponta a ponta (como rolagem rápida de grade de fotos, navegação em álbuns, deslizar para pré-visualizar fotos e vídeos em alta resolução em tela cheia e gravação e salvamento de rastros), memory.high foi reduzido ao medir os contadores memory.stat do cgroup (workingset_refault_anon, workingset_refault_file, pgscan e pgsteal) e memory.events (high). Como ilustrado na figura 6, o orçamento ideal fica logo acima do ponto de inflexão de zero refault. O ponto de inflexão de zero refault é o limite em que o kernel recupera o cache de arquivos único frio (pgsteal > 0) sem acionar o thrashing do conjunto de trabalho ativo (workingset_refault == 0).
  • Validação do dimensionamento de pixels de tela em várias resoluções:testes do :PhotoPicker em quatro resoluções de tela físicas e emuladas, de ~1,2 MP (720 × 1600) a ~4,5 MP (tela interna desdobrada de 2076 × 2152), confirmaram que o conjunto de trabalho de bitmap ativo do Glide e do ViewPager2 é dimensionado linearmente com a contagem de pixels da tela. A referência sem exibição também varia de acordo com o dispositivo: com quase a mesma contagem de pixels, o ponto de inflexão de zero falhas de página foi de cerca de 140 MB na tela externa de um dispositivo dobrável de 16 GB, em comparação com 110 a 125 MB em um smartphone de 6 GB. Uma linha de base de 85 MB mais 25 bytes por pixel de tela (android:maxMb="85" e android:additionalBytesPerDisplayPixel="25") permanece acima do ponto de inflexão medido em todas as telas testadas (~110 MB em HD+, ~145 MB em FHD+ e ~190 MB em telas abertas).
  • Verificação da coleta de lixo do ART em estados em segundo plano:no cgroup v2, as páginas de imagem do framework e do boot.art compartilhadas pelo Zygote permanecem cobradas do cgroup do Zygote e são tratadas como um espaço imune durante a coleta de lixo de marcação e compactação simultânea (CMC, na sigla em inglês) do ART. Os testes de estresse confirmaram que, quando um app de interface do sistema faz a transição para background, o aperto de memory.high (por exemplo, para 30 MB em Traceur) limita de forma limpa o heap privado e o cache de páginas sem causar thrashing de GC ou atividade de troca.

Gráfico de recuperação de cache frio (pgsteal) e thrashing do conjunto de trabalho (workingset_refault) à medida que memory.high é reduzido durante uma CUJ, destacando o ponto de inflexão de zero refault e o orçamento desejado

Figura 6. Encontrar o ponto de inflexão de zero falha de página durante uma CUJ.

Resumo dos orçamentos de apps do sistema de referência do AOSP

A tabela a seguir resume os orçamentos de referência calibrados no AOSP em todos os níveis de RAM e estados de processo:

Pacote ou processo do sistema Escopo ≤8 GB de RAM 12 GB de RAM ≥16 GB de RAM Driver de calibragem principal
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB Abrange picos de memory.peak de ciclo de vida P90 (cerca de 75 a 110 MB) e >2x memory.current ativo P99.
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB Abrange a emulação de cartão host ativa P99 (até ~50 MB) e P90 a P95 memory.peak ao cortar o cache de arquivos frios.
com.android.providers.calendar UID (<application>) 70 MB padrão (primeiro plano)
50 MB perceptible e background
O conjunto de trabalho ativo é invariável em todos os níveis de RAM. O orçamento em segundo plano mantém uma margem anônima para alarmes de lembrete enquanto reduz o cache de páginas do SQLite durante as sincronizações.
com.android.traceur UID (<application>) 85 MB padrão (primeiro plano e perceptível)
30 MB background
Preserva o espaço de 85 MB durante a gravação e exportação de rastreamento ativo (TraceService), limitando o estado inativo e de transmissão a 30 MB.
com.android.providers.media.module (principal) PID (<process>) 60 MB 75 MB 85 MB Oferece cerca de 10 a 30 MB de espaço livre em relação ao P99 anon + swap ao recuperar o SQLite frio e o cache de arquivos de miniatura.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 MB + 25 bytes por pixel
(~110 MB em HD+, ~145 MB em FHD+, ~190 MB em dobra de ~4,5 MP)
É dimensionado proporcionalmente com a contagem de pixels da tela (additionalBytesPerDisplayPixel="25") para corresponder ao limite do conjunto de trabalho de bitmap do Glide.
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB Memória predominantemente anônima, dimensionada para o ciclo de vida P95 memory.peak para cobrir picos de TLS, DNS e DHCP do portal cativo.
com.android.appsearch.apk UID (<application>) 130 MB Abrange cerca de 100 MB de memória bloqueada e não removível, além de um conjunto de trabalho de host ativo de cerca de 5 MB, com espaço para a região bloqueada crescer. Zero falhas de página medidas em ≥125 MB enquanto o cache de página fria (até ~330 MB em P99) é recuperado.
com.android.phone (TeleService) PID (<process>) 120 MB Com escopo por PID no UID compartilhado 1001. O conjunto de trabalho rastreia a geração do dispositivo em vez da RAM. Portanto, um único orçamento fica cerca de 30 MB acima do P99 anon + swap mais alto por modelo.

Como os OEMs podem recalibrar as cotas de apps do sistema para os próprios dispositivos

Os OEMs que personalizam apps do sistema (por exemplo, adicionando perfis de Bluetooth do fornecedor, extensões de telefonia personalizadas ou indexadores de mídia do OEM) ou que enviam serviços do sistema pré-instalados adicionais podem recalibrar os orçamentos atuais ou criar novos arquivos *-budget.xml para o portfólio de hardware.

Coletar perfis de memória ativos do cgroup v2

Para cada processo do sistema de destino, leia os nós de memória do cgroup v2 em /sys/fs/cgroup/uid_<uid>/pid_<pid>/ em cargas de trabalho representativas e níveis de RAM:

  • memory.current: memória residente atual cobrada do cgroup (exceto páginas compartilhadas do Zygote)
  • memory.peak: pico de uso de memory.current desde o início do processo.
  • memory.swap.current: uso atual de troca compactada na ZRAM.
  • memory.stat: detalhamento de anon, file, pgscan, pgsteal, workingset_refault_anon e workingset_refault_file
  • memory.events: contagem cumulativa de violações de high (eventos de limitação e recuperação de memory.high)

Ao agregar telemetria de dispositivos de dogfood ou de campo, separe os estados de processo ativos (TOP, FOREGROUND_SERVICE, vinculações ativas e sincronizações ativas) dos estados CACHED ou PERSISTENT inativos para que os snapshots inativos não distorçam os cálculos do conjunto de trabalho P95 e P99 para baixo.

Separar heap não compactável do cache de arquivos recuperável

Compare o 99º percentil de anon + swap com o 99º percentil de memory.current:

  • Serviços dominados por cache de arquivos (file &gg; anon, como provedores de banco de dados, indexadores de pesquisa e daemons de registro): defina android:maxMb para fornecer uma margem confortável (normalmente de 1,5x a 4x) em relação ao P99 anon + swap, além de uma janela de trabalho para páginas de arquivos quentes. Essa configuração permite que o kernel remova continuamente páginas de arquivos frios não mapeados sem tocar no heap anônimo. Se unevictable ou file_mapped for grande (como com memória bloqueada ou bancos de dados mapeados na memória), conte essa pegada como conjunto de trabalho também.
  • Serviços dominados por heap anônimo ou com picos (anon compõe a maior parte de memory.current, como pilhas de rede ou conectividade): dimensione android:maxMb para cobrir de P90 a P95 de memory.peak para que picos transitórios não acionem a troca síncrona de ZRAM em linhas de execução sensíveis à latência.
  • Processos de interface com uso intensivo de tela (como seletores, galerias ou auxiliares de interface do sistema): meça o consumo de memória de referência que não é de interface e o consumo de memória de bitmap incremental por megapixel nas telas de menor e maior resolução e expresse o orçamento usando android:maxMb mais android:additionalBytesPerDisplayPixel.

Limpar memory.high em dispositivos físicos

Antes de confirmar um novo orçamento para o build, use am memory-limiter manual (ou grave diretamente no nó do cgroup memory.high do processo em um build userdebug) para testar os limites candidatos ao executar os testes automatizados de CUJ:

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

Monitore memory.stat e memory.events antes e depois da CUJ:

  • O orçamento é muito baixo:workingset_refault_anon ou workingset_refault_file aumenta drasticamente durante a CUJ, memory.swap.current cresce continuamente durante o trabalho ativo em primeiro plano ou a latência do frame da CUJ ou do Binder regride. Aumente o orçamento além do ponto de inflexão de falha.
  • O orçamento é muito alto:memory.events:high permanece 0 mesmo após cargas de trabalho pesadas de streaming de arquivos ou indexação, e file em memory.stat cresce sem limites com páginas frias. Reduza o orçamento para o conjunto de trabalho ativo.
  • O orçamento está bem calibrado:o memory.current em estado estável permanece abaixo de memory.high durante a operação normal. Durante picos de arquivos ou cache, o pgsteal recupera páginas de arquivos frios, enquanto o workingset_refault_anon permanece perto de zero e a latência de CUJ não muda.