Recuperação de memória: remoção e troca

Nas seções anteriores, vimos como o kernel gerencia a memória em todo o sistema. Neste capítulo, vamos nos aprofundar nos grupos de controle de memória (memcg), o mecanismo que o Android usa para particionar e controlar o uso da memória de apps individuais.

É fundamental entender o memcg porque é o nível em que o sistema pode segmentar apps individuais para recuperação ou definir limites de memória neles, permitindo o gerenciamento proativo das pegadas de memória do app.

Grupos de controle de memória (memcg)

Os grupos de controle (cgroups) são um recurso do kernel do Linux que permite organizar processos em grupos hierárquicos e distribuir recursos do sistema (como CPU, memória e E/S) entre eles. O memcg é o controlador de cgroup específico para memória.

Conceitos principais do memcg

  • Cobrança do memcg: quando um processo em um memcg aloca uma página de memória (anônima ou com suporte a arquivos), essa página é "cobrada" do memcg. A cobrança total de um memcg é a soma de todas as páginas usadas por todos os processos nele.
  • Contabilidade hierárquica: o uso da memória é contabilizado na árvore. Uma cobrança em um cgroup filho também conta para o uso do pai.
  • Limites de memória: cada memcg pode ter limites (como memory.max ou memory.high) que acionam a recuperação ou até mesmo o OOM killer se forem excedidos, independentemente da memória global do sistema.
  • Recuperação por memcg: quando um memcg está acima do limite ou quando o sistema precisa de memória, o kernel pode segmentar um memcg específico para recuperação. Isso significa despejar as páginas de arquivo ou trocar as páginas anônimas para ZRAM.

A hierarquia do memcg do Android

O Android usa uma hierarquia específica para gerenciar processos de apps. Essa estrutura permite que o sistema aplique políticas diferentes a tipos diferentes de apps (por exemplo, em primeiro plano x em segundo plano).

Hierarquia memcg do Android

  • /sys/fs/cgroup/apps/: a raiz de todos os aplicativos Android.
  • uid_<UID>/: um diretório para o ID de usuário de cada app. Todos os processos pertencentes ao mesmo pacote do app compartilham esse grupo.
  • pid_<PID>/: um diretório para cada processo individual e todos os filhos bifurcados dele. Isso permite o controle refinado e a contabilização de apps com vários processos.

Exercício prático: como explorar o memcg

Neste exercício, você vai encontrar o diretório memcg do MemoryLab e observar a cobrança de memória em tempo real.

1. Iniciar o MemoryLab

Verifique se o MemoryLab está em execução no dispositivo.

2. Encontrar o memcg do MemoryLab

Primeiro, receba o PID do app MemoryLab em execução:

adb shell pidof com.android.memorylab
# Example output: 11672

Agora, localize o diretório cgroup. Ele pode ser encontrado em /proc/<PID>/cgroup:

adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672

No cgroup v2, o caminho após 0:: representa a hierarquia do memcg em relação a /sys/fs/cgroup. Portanto, o caminho completo é /sys/fs/cgroup/apps/uid_10274/pid_11672/.

3. Ler estatísticas do memcg

Acesse esse diretório e confira os arquivos principais:

# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672

memory.current mostra a quantidade total de memória (em bytes) atualmente cobrada desse cgroup.

# Detailed statistics
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.stat | head -n 10
# Example output:
# anon 1585098752
# file 741376
# kernel 5484544
# kernel_stack 393216
# pagetables 4583424
# sec_pagetables 0
# percpu 216
# sock 0
# vmalloc 4096
# shmem 20480

memory.stat fornece um detalhamento: * anon: quantidade de memória anônima (heaps, pilhas). * file: quantidade de memória com suporte a arquivos (cache de página). * swap: quantidade de memória trocada para ZRAM.

4. Observar mudanças

  1. Abra o MemoryLab.
  2. Anote o valor de memory.current.
  3. Toque em Allocate Java Memory (10MB) várias vezes.
  4. Leia memory.current novamente. Ele deve aumentar em aproximadamente 10 MB para cada toque.
  5. Toque em Allocate Bitmaps. Verifique memory.stat para ver o aumento de anon.

Recuperação proativa com memory.reclaim

Um recurso avançado do memcg (v2) é o arquivo memory.reclaim. A gravação de um valor nesse arquivo instrui o kernel a tentar recuperar imediatamente essa quantidade de memória desse memcg e de todos os memcgs abaixo dele.

Como o Android usa memory.reclaim

O CachedAppOptimizer do Android usa esse recurso para recuperar ao máximo a memória dos apps depois que eles são congelados. O App Freezer do Android garante que os apps em cache consumam o mínimo de RAM possível enquanto não estiverem em execução. Quando um app é movido para o segundo plano e congelado, o sistema grava o uso atual de memória do app no arquivo memory.reclaim. Isso força o kernel a despejar todas as páginas de arquivo possíveis e trocar todas as páginas anônimas para ZRAM, minimizando a pegada residente do app.

É possível alcançar a mesma "recuperação máxima" manualmente lendo memory.current e gravando-o de volta em memory.reclaim:

# Force reclaim of everything
adb shell "cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"

Exercício: forçar a recuperação e o rastreamento

Agora vamos forçar o kernel a recuperar a memória do MemoryLab e capturar a atividade em um rastreamento do Perfetto.

  1. Preparar: verifique se o MemoryLab tem algumas alocações (Java e bitmaps).
  2. Iniciar o rastreamento: use esta configuração de rastreamento inline para capturar eventos de agendamento, recuperação e cache de página.

    # Use a config that captures scheduling, reclaim and page cache events
    adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/reclaim.perfetto-trace <<EOF
    buffers: {
        size_kb: 131072
        fill_policy: RING_BUFFER
    }
    data_sources: {
        config {
            name: "linux.ftrace"
            ftrace_config {
                ftrace_events: "sched/sched_switch"
                ftrace_events: "sched/sched_wakeup"
                ftrace_events: "kmem/rss_stat"
                ftrace_events: "mm_filemap_add_to_page_cache"
                ftrace_events: "mm_filemap_delete_from_page_cache"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_end"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_end"
                symbolize_ksyms: true
            }
        }
    }
    data_sources: {
        config {
            name: "linux.process_stats"
            process_stats_config {
                scan_all_processes_on_start: true
            }
        }
    }
    EOF
    
  3. Acionar pressão e recuperação:

    • No MemoryLab, toque em Allocate Native Memory (1GB). Isso vai acionar a pressão em todo o sistema.
    • Em um terminal separado, recupere 200 MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Falha de volta: volte para o MemoryLab. Toque em Thrash Pagecache (Refault test).

  5. Parar o rastreamento: pressione Ctrl + C no terminal de rastreamento.

Como analisar a recuperação no Perfetto

Ao abrir o rastreamento, você pode conferir a recuperação em todo o sistema e por app em ação:

Perfetto mostrando a recuperação direta e do memcg

Procure o seguinte no rastreamento:

  • kswapd: pesquise kswapd0 em Kernel threads (na captura de tela acima, ele foi fixado manualmente na parte de cima). Você vai notar que ele está ativando e executando (fatias verdes) à medida que o sistema tenta encontrar páginas livres.
  • Recuperação direta: confira as linhas de execução do processo com.android.memorylab. Você vai notar fatias de eventos ftrace roxas (como mm_vmscan_direct_reclaim_begin) aparecendo diretamente na faixa de agendamento da linha de execução. Isso indica que a linha de execução do app está paralisada aguardando que o kernel libere páginas.
  • Contadores de RSS e troca:
    • mem.rss.anon: aumenta quando você toca nos botões de alocação.
    • mem.swap: aumenta constantemente à medida que kswapd e as próprias linhas de execução do app (sujeitas à recuperação direta) compactam essas páginas anônimas em ZRAM.
  • Recuperação do memcg: se você aumentar o zoom no momento em que acionou a recuperação manual, vai notar uma queda acentuada em rss.anon e rss.file, acompanhada de eventos mm_vmscan_memcg_reclaim.

← Em todo o sistema | ↑ Acima | Interação kswapd e lmkd →