Zwracaj strony pamięci do systemu operacyjnego

Zwolnienie obiektu w kodzie aplikacji lub zamknięcie tymczasowego strumienia nie zawsze powoduje zwrócenie fizycznej pamięci RAM do systemu operacyjnego. Niestandardowe natywne alokatory, środowiska wykonawcze z mechanizmem odzyskiwania pamięci i tablice mieszające z adresowaniem otwartym często zachowują obszary pamięci wirtualnej, które pozostają wspierane przez strony fizyczne długo po tym, jak logiczne dane wewnątrz nich zostaną usunięte. Podobnie odczytywanie lub mapowanie pamięci dużych archiwów zasobów wypełnia pamięć podręczną stron jądra, która pozostaje obciążona grupą kontrolną pamięci (memcg) aplikacji.

Na tej stronie wyjaśniamy, jak używać wywołań systemowych madvise i posix_fadvise, aby zwracać nieużywane strony anonimowe i strony z plikami do systemu operacyjnego, zachowując rezerwacje adresów wirtualnych i deskryptory plików, oraz jak sprawdzać wpływ tych działań w adb shell i Perfetto.

Dlaczego logiczne wycofanie przydziału zachowuje fizyczną pamięć RAM

Jądro systemu Linux śledzi pamięć fizyczną z dokładnością do stron sprzętowych, które na urządzeniach z Androidem mają rozmiar 4 KB lub 16 KB. Gdy proces rezerwuje region wirtualnej przestrzeni adresowej za pomocą mmap, jądro tworzy obszar pamięci wirtualnej (VMA) bez przydzielania fizycznej pamięci RAM. Jądro przydziela i obciąża stronę fizyczną tylko wtedy, gdy proces po raz pierwszy jej dotyka. W przypadku pamięci anonimowej pierwszy odczyt mapuje współdzieloną stronę zerową, więc tylko zapis przydziela pamięć RAM.

Gdy strona zostanie załadowana do pamięci RAM, jądro nie wie, czy bajty na tej stronie nadal reprezentują aktywne obiekty w środowisku wykonawczym aplikacji. Istnieją 2 typowe wzorce, które powodują, że strony fizyczne pozostają w pamięci po zakończeniu korzystania z nich przez aplikację:

  • Anonimowe areny i pule: niestandardowe natywne alokatory (takie jak alokatory slab, pool lub frame-arena), zarządzane środowiska wykonawcze z nieprzenośnymi przestrzeniami sterty i duże tablice mieszające rezerwują ciągłe zakresy adresów wirtualnych za pomocą mmap przy użyciu MAP_PRIVATE | MAP_ANONYMOUS. Gdy aplikacja resetuje arenę, zwalnia blok z powrotem do niestandardowej puli lub czyści tablicę mieszającą przez wyzerowanie jej metadanych, bazowe strony pozostają nieaktualne. Bez wyraźnej wskazówki dla jądra te strony nadal będą wliczane do anonimowego rozmiaru zbioru roboczego (RSS) i proporcjonalnego rozmiaru zbioru roboczego (PSS) procesu oraz do obciążenia grupy kontrolnej memory.current. W przypadku braku pamięci jądro kompresuje je do ZRAM, gdzie nadal zajmują przestrzeń wymiany przypisaną do memory.swap.current.
  • Pamięć podręczna stron oparta na plikach z przesyłania strumieniowego zasobów: gdy aplikacja odczytuje duże pakiety zasobów, archiwa poziomów, kontenery multimediów lub modele uczenia maszynowego za pomocą read, pread lub opartego na plikach mmap, jądro zapisuje każdą odczytaną stronę w pamięci podręcznej stron i obciąża tymi stronami licznik file w pliku memory.stat grupy kontrolnej procesu. Jeśli aplikacja dekompresuje lub kopiuje ten zasób do anonimowego bufora środowiska wykonawczego albo przesyła go do GPU, zachowanie stron nieprzetworzonego pliku w pamięci podręcznej stron powoduje zduplikowanie zasobu w pamięci RAM.
Zanieczyszczone strony areny zostały zwolnione za pomocą madvise, a strony zasobów z pamięci podręcznej zostały usunięte za pomocą posix_fadvise, podczas gdy zakres wirtualny i deskryptor pliku pozostały nienaruszone.
Rysunek 1. Odzyskiwanie stron fizycznych z aren wirtualnych i pamięci podręcznej stron.

Publikowanie stron anonimowych

Wywołanie systemowe madvise przekazuje jądru informacje o tym, jak proces ma używać zakresu adresów wirtualnych. W przypadku prywatnych mapowań anonimowych (MAP_PRIVATE | MAP_ANONYMOUS) przekazanie MADV_DONTNEED powoduje, że jądro natychmiast odrzuca strony fizyczne i wszystkie wpisy ZRAM w pamięci wymiany, które obsługują określony zakres wyrównany do strony.

Po madvise z MADV_DONTNEED zwraca:

  • Zakres adresów wirtualnych pozostaje zarezerwowany i prawidłowy w przestrzeni adresowej procesu (VmSize się nie zmienia), więc wskaźniki i granice areny pozostają nienaruszone bez wywoływania funkcji munmap lub mmap.
  • Strony fizyczne są natychmiast odmapowywane i zwalniane, co zmniejsza wartości RssAnon, Pss, licznika anon w memory.stat i memory.swap.current w przypadku wszystkich stron, które były w ZRAM.
  • Każdy kolejny odczyt lub zapis adresu w tym zakresie powoduje błąd na nowej stronie wypełnionej zerami na żądanie.
Flaga porady Mapowanie docelowe Działanie jądra systemu na Androidzie
MADV_DONTNEED Prywatne anonimowe (MAP_PRIVATE | MAP_ANONYMOUS) Natychmiast odrzuca strony fizyczne i miejsca wymiany ZRAM. Kolejne próby dostępu powodują błędy na stronach wypełnionych zerami. Deterministyczny spadek w RssAnon i memory.current.
MADV_DONTNEED Zapisane w pliku (MAP_PRIVATE lub MAP_SHARED) Usuwa mapowanie stron z tabel stron procesu (RssFile porzuceń). Prywatne strony typu copy-on-write w mapowaniu MAP_PRIVATE są odrzucane, więc modyfikacje są tracone, a przy następnym dostępie plik jest odczytywany ponownie. Zmodyfikowane strony MAP_SHARED są później zapisywane z powrotem w pamięci. Czyste strony pozostają w pamięci podręcznej, dopóki nie zostaną usunięte.
MADV_FREE Prywatne anonimowe (MAP_PRIVATE | MAP_ANONYMOUS) Oznacza strony jako możliwe do ponownego wykorzystania w trybie leniwym. Jeśli nie ma presji na pamięć, jądro zachowuje strony w RssAnon i odzyskuje je tylko wtedy, gdy uruchomione jest odzyskiwanie globalne lub memcg.
MADV_WILLNEED Anonimowe z plikiem kopii zapasowej lub zamienione Uruchamia asynchroniczny odczyt z wyprzedzeniem w przypadku mapowań opartych na plikach lub odczytuje z wyprzedzeniem zamienione anonimowe strony z ZRAM do pamięci podręcznej wymiany.

Wyrównaj zakresy wewnętrzne do rozmiaru strony środowiska wykonawczego

madvise działa na całych stronach. Wymaga adresu początkowego wyrównanego do strony i zaokrągla długość do następnej granicy strony. Jeśli zwolnisz zakres w udostępnionej arenie, gdzie sąsiednie bajty nadal zawierają aktywne obiekty, zaokrąglenie zakresu na zewnątrz może wyzerować aktywne alokacje, które współdzielą strony graniczne.

Zawsze sprawdzaj rozmiar strony w czasie działania za pomocą sysconf(_SC_PAGESIZE), aby obsługiwać urządzenia ze stronami o rozmiarze zarówno 4 KB, jak i 16 KB. Zaokrąglaj wewnętrzny adres początkowy w górę do następnej granicy strony, a wewnętrzny adres końcowy w dół do poprzedniej granicy strony. Więcej informacji o alokatorach niezależnych od rozmiaru strony znajdziesz w artykule Optymalizacja niestandardowych alokatorów pamięci.

#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>

// Reclaims physical pages strictly inside [region_start, region_start + length)
// without disturbing live objects on partially occupied boundary pages.
void ReleaseUnusedArenaSpan(void* region_start, size_t length) {
  const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
  const uintptr_t page_mask = ~(page_size - 1);

  const uintptr_t raw_start = reinterpret_cast<uintptr_t>(region_start);
  const uintptr_t raw_end = raw_start + length;

  // Round start UP and end DOWN so only fully unused pages are discarded.
  const uintptr_t aligned_start = (raw_start + page_size - 1) & page_mask;
  const uintptr_t aligned_end = raw_end & page_mask;

  if (aligned_end > aligned_start) {
    madvise(reinterpret_cast<void*>(aligned_start),
            aligned_end - aligned_start,
            MADV_DONTNEED);
  }
}

Podczas tworzenia niestandardowych mapowań anonimowych za pomocą mmap oznacz VMA etykietą prctl za pomocą PR_SET_VMA_ANON_NAME, aby móc śledzić jego rezydencję według nazwy w showmap i /proc/<pid>/smaps. dumpsys meminfo zlicza niestandardowe anonimowe regiony w Unknown. Niektóre starsze jądra zachowują wskaźnik do nazwy zamiast ją kopiować, więc przekaż ciąg znaków, który pozostaje prawidłowy przez cały okres mapowania, np. ciąg znaków:

#include <sys/mman.h>
#include <sys/prctl.h>

#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif

void* ReserveNamedArena(size_t capacity_bytes, const char* arena_name) {
  void* ptr = mmap(nullptr, capacity_bytes, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (ptr != MAP_FAILED) {
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,
          reinterpret_cast<unsigned long>(ptr), capacity_bytes,
          reinterpret_cast<unsigned long>(arena_name));
  }
  return ptr;
}

Usuwanie stron z pamięci podręcznej plików

Gdy aplikacja odczytuje dane z deskryptora pliku, jądro zapisuje strony pliku w pamięci podręcznej stron, aby przyspieszyć przyszłe odczyty. Przyspiesza to wielokrotny dostęp do małych plików konfiguracyjnych lub bibliotek współdzielonych, ale działa na Twoją niekorzyść, gdy przesyłasz strumieniowo duże archiwa zasobów, pakiety tekstur lub fragmenty audio i wideo, które są analizowane raz i przechowywane w postaci zdekodowanej w innym miejscu.

Wywołanie systemowe posix_fadvise pozwala aplikacji poinformować jądro o oczekiwanym wzorcu dostępu do pliku w otwartym deskryptorze pliku:

  • POSIX_FADV_DONTNEED: informuje jądro, że określony zakres bajtów nie będzie używany w najbliższej przyszłości. Jądro natychmiast usuwa z pamięci podręcznej stron czyste, niezmapowane strony z pamięci podręcznej w tym zakresie wyrównanym do strony, co zmniejsza licznik file w memory.stat i obciążenie memory.current grupy kontrolnej.
  • POSIX_FADV_SEQUENTIAL: podwaja okno odczytu z wyprzedzeniem jądra dla deskryptora pliku, co zwiększa przepustowość podczas strumieniowego przesyłania dużego archiwum od początku do końca. Połącz to z POSIX_FADV_DONTNEED w przypadku fragmentów końcowych, gdy je wykorzystujesz, aby ślad pamięci podręcznej strony był ograniczony do małego okna przesuwnego.
  • POSIX_FADV_RANDOM: wyłącza spekulatywny odczyt jądra z wyprzedzeniem w deskryptorze pliku. Używaj tej opcji, gdy wykonujesz rozproszone, niespójne wyszukiwania według przesunięcia bajtowego w dużym archiwum lub pliku bazy danych, aby każdy mały odczyt nie pobierał sąsiednich nieużywanych stron do pamięci podręcznej stron. W przypadku plików mapowanych w pamięci wywołaj madvise z MADV_RANDOM na mapowaniu.

W poniższym przykładzie duże archiwum zasobów jest przesyłane strumieniowo we fragmentach, co umożliwia sekwencyjne odczytywanie z wyprzedzeniem i usuwanie z pamięci podręcznej stron każdego przetworzonego zakresu wyrównanego do strony:

#include <fcntl.h>
#include <unistd.h>
#include <cstdint>
#include <vector>

void StreamAssetArchive(int fd, size_t total_size) {
  const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE));
  constexpr size_t kChunkSize = 4 * 1024 * 1024;  // 4 MB streaming window
  std::vector<uint8_t> buffer(kChunkSize);

  // Hint sequential access so the kernel prefetches upcoming chunks.
  posix_fadvise(fd, 0, total_size, POSIX_FADV_SEQUENTIAL);

  off_t offset = 0;
  while (offset < static_cast<off_t>(total_size)) {
    ssize_t bytes_read = pread(fd, buffer.data(), buffer.size(), offset);
    if (bytes_read <= 0) break;

    // Decode or upload buffer[0 .. bytes_read) into runtime structures here.

    // Evict full pages that have been completely consumed.
    const size_t aligned_len =
        (static_cast<size_t>(bytes_read) / page_size) * page_size;
    if (aligned_len > 0) {
      posix_fadvise(fd, offset, static_cast<off_t>(aligned_len),
                    POSIX_FADV_DONTNEED);
    }
    offset += bytes_read;
  }
}

Odzyskaj regiony mmap oparte na plikach

Jeśli aplikacja mapuje w pamięci duży plik zasobu za pomocą mmap zamiast odczytywać go za pomocą pread, wywołanie posix_fadvise z samym POSIX_FADV_DONTNEED nie spowoduje usunięcia stron, które są nadal mapowane w tabelach stron procesu. Aby usunąć wykorzystany zakres mapowania pliku tylko do odczytu bez usuwania mapowania całego VMA:

  1. Wywołaj madvise(addr, length, MADV_DONTNEED) na zmapowanym zakresie adresów wirtualnych, aby usunąć mapowanie tych stron z tabel stron procesu (RssFile drops).
  2. Wywołaj posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED) na odpowiednim przesunięciu pliku, aby jądro usunęło też niezmapowane czyste strony z pamięci podręcznej stron (file w memory.stat).

Przestrzeganie wytycznych dotyczących porad na stronie

Podczas integrowania madvise i posix_fadvise z alokatorami, modułami wczytywania zasobów i modułami obsługi cyklu życia stosuj te sprawdzone metody:

  • Grupowanie wywołań w duże regiony: każde wywołanie madvise przejmuje blokadę mapy pamięci procesu (mmap_lock), przechodzi przez tabele stron i unieważnia pasujące wpisy bufora TLB na innych rdzeniach procesora (za pomocą instrukcji konserwacji bufora TLB w architekturze Arm i przerwań międzyprocesorowych w architekturze x86). Nie wywołuj funkcji madvise przy każdym niewielkim zwolnieniu pamięci. Scalaj wolne zakresy i informuj jądro o fragmentach o rozmiarze od 64 KB do 1 MB lub większych albo czyść wolne zakresy podczas granic klatek i przejść w tle (onStop i TRIM_MEMORY_UI_HIDDEN).
  • Odzyskaj pustą pojemność w tablicach mieszających i pulach: gdy duża tablica mieszająca, pula obiektów lub bufor przejściowy zostaną wyczyszczone po przejściu sceny lub synchronizacji danych, zmniejsz przydział pamięci lub wywołaj MADV_DONTNEED w wyczyszczonej tablicy. Utrzymywanie tabeli z adresowaniem otwartym o rozmiarze 32 MB przydzielonej ze znacznikami usunięcia powoduje, że wszystkie 32 MB stron fizycznych jest obciążane procesem.
  • Flush dirty file data before evicting it (Opróżnij nieaktualne dane pliku przed ich usunięciem): POSIX_FADV_DONTNEED usuwa tylko aktualne strony z pamięci podręcznej. Jeśli aplikacja zapisuje duży plik tymczasowy lub plik pamięci podręcznej, którego nie odczyta od razu, wywołaj fdatasync przed posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED), aby zmodyfikowane strony zostały zapisane w pamięci, a następnie usunięte z pamięci RAM.
  • Nie usuwaj często używanego udostępnionego kodu ani zasobów: unikaj wywoływania funkcji POSIX_FADV_DONTNEED w przypadku plików DEX, ODEX, VDEX, bibliotek udostępnionych (.so) i zasobów interfejsu, do których często uzyskiwany jest dostęp. Usunięcie aktywnych stron kodu lub stron Komponentów wymusza synchroniczne błędy pamięci masowej, które blokują wątki i zwiększają obciążenie operacji wejścia-wyjścia, co jest zgłaszane przez informacje o blokadzie obciążenia (PSI).

Weryfikowanie odzyskiwania stron w powłoce

Sprawdź, czy madvise i posix_fadvise odzyskują fizyczną pamięć RAM za pomocą showmap i plików rozliczeniowych cgroup v2 aplikacji.

Sprawdzanie rozmiaru wirtualnego i fizycznego za pomocą polecenia showmap

Gdy nadasz anonimowej arenie nazwę PR_SET_VMA_ANON_NAME, showmap wyświetli zarówno jej rezerwację wirtualną (virtual size), jak i jej rezydentne strony fizyczne (RSS i PSS). Poniższe przykładowe dane wyjściowe pochodzą z przykładowej aplikacji MemoryLab w AOSP, która nazywa swoją arenę memorylab-arena. Zastąp com.example.app i memorylab-arena nazwą pakietu i nazwą areny. Przed wywołaniem madvise z MADV_DONTNEED w dotkniętej arenie 64 MB uruchom:

adb shell "showmap \$(pidof com.example.app) | grep -E '^ +virtual|^ +size|memorylab-arena' | head -3"
 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536    65536    65536        0        0        0    65536        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

Po wywołaniu madvise(arena_ptr, 64 * 1024 * 1024, MADV_DONTNEED) uruchom ponownie showmap:

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536        0        0        0        0        0        0        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

Wartość virtual size pozostaje na poziomie 65 536 KB (64 MB), co potwierdza, że zakres adresów wirtualnych jest nadal zarezerwowany, a wartości RSS, PSS i private dirty spadają do 0 KB.

Sprawdzanie parametrów memory.current i memory.stat w cgroup

Aby sprawdzić zarówno odzyskiwanie anonimowych stron, jak i wykluczanie niezmapowanej pamięci podręcznej stron (która nie pojawia się w tabelach stron procesu, jeśli jest odczytywana za pomocą pread), sprawdź katalog cgroup v2 procesu. Na Androidzie każdy proces aplikacji ma własną grupę kontrolną (np. /sys/fs/cgroup/apps/uid_<UID>/pid_<PID>/, w zależności od urządzenia). To polecenie znajduje katalog z wiersza 0:: pliku /proc/<PID>/cgroup i wyświetla liczniki:

adb shell '
  PID=$(pidof com.example.app)
  CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
  echo "memory.current: $(cat $CG/memory.current)"
  echo "memory.swap.current: $(cat $CG/memory.swap.current)"
  grep -E "^(anon|file) " $CG/memory.stat
'

Przed zwolnieniem anonimowego obszaru o rozmiarze 64 MB i usunięciem z pamięci podręcznej strony pliku zasobu o rozmiarze 128 MB mapowanego na pamięć:

memory.current: 242962432
memory.swap.current: 16633856
anon 100671488
file 140009472

Natychmiast po wywołaniu madvise z MADV_DONTNEED w obszarze i w mapowaniu pliku zasobu, a następnie posix_fadvise z POSIX_FADV_DONTNEED w deskryptorze pliku zasobu:

memory.current: 41766912
memory.swap.current: 16502784
anon 33722368
file 5914624

anon zmniejsza się o 64 MB, a file o 128 MB, co zmniejsza memory.current o około 192 MB bez zamykania deskryptora pliku ani wyrejestrowywania wirtualnej przestrzeni adresowej areny. memory.swap.current prawie się nie zmienia, ponieważ prawie żadna ze stron areny nie została przeniesiona do ZRAM. Wartości zależą od urządzenia i od tego, jaką część pliku jądro już usunęło samodzielnie. Więcej informacji o tym, jak jądro obciąża i odzyskuje pamięć na cgroup, znajdziesz w artykule Odzyskiwanie pamięci: wykluczanie i wymiana.

Analizowanie odzyskiwania stron w Perfetto

Perfetto umożliwia korelację wywołań madvise i posix_fadvise z licznikami tabeli stron jądra i pamięci podręcznej stron na ujednoliconej osi czasu.

Nagrywanie logu czasu

Przechwyć log śledzenia za pomocą linux.process_stats w przypadku okresowych liczników procesów i linux.ftrace w przypadku zdarzeń RSS i pamięci podręcznej stron w czasie rzeczywistym, a następnie pobierz go na komputer:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/page_advice.perfetto-trace <<EOF
buffers: {
    size_kb: 65536
    fill_policy: RING_BUFFER
}
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config {
            scan_all_processes_on_start: true
            proc_stats_poll_ms: 100
        }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "kmem/rss_stat"
            ftrace_events: "filemap/mm_filemap_add_to_page_cache"
            ftrace_events: "filemap/mm_filemap_delete_from_page_cache"
        }
    }
}
duration_ms: 20000
EOF
adb pull /data/misc/perfetto-traces/page_advice.perfetto-trace

Sprawdzanie ścieżek w interfejsie Perfetto

Otwórz log czasu w interfejsie Perfetto, rozwiń grupę procesów aplikacji i przypnij te ścieżki liczników, aby pozostały u góry osi czasu:

  • mem.virt: pozostaje na tym samym poziomie w MADV_DONTNEED wywołaniach, co potwierdza, że rezerwacja adresu wirtualnego (mmap) nie uległa zmianie.
  • mem.rss.anon: gwałtownie spada w dokładnej sygnaturze czasowej, gdy aplikacja wywołuje funkcję madvise z parametrem MADV_DONTNEED w przypadku zmodyfikowanych anonimowych aren lub regionów sterty.
  • mem.rss.file: zmniejsza się, gdy aplikacja wywołuje madvise z MADV_DONTNEED w regionie mmap opartym na plikach, czemu towarzyszy seria zdarzeń ftrace mm_filemap_delete_from_page_cache, gdy posix_fadvise z POSIX_FADV_DONTNEED usuwa czyste strony z pamięci podręcznej stron.
  • mem.swap: spada, gdy MADV_DONTNEED odrzuca anonimowe strony, które zostały wcześniej skompresowane w ZRAM.
Ścieżki liczników Perfetto dla jednego procesu. mem.rss.anon i mem.rss.file zwiększają się, gdy arena i plik zasobu są wypełnione, i zmniejszają się po madvise i posix_fadvise, podczas gdy mem.swap i mem.virt pozostają na tym samym poziomie.
Rysunek 2. Spadek RSS po wywołaniach porad dotyczących strony.

Rysunek 2 przedstawia te ścieżki w przykładowej aplikacji MemoryLab. Aby wyświetlić ten sam widok, otwórz przykładowy ślad w interfejsie Perfetto.

Sprawdzanie szczytowego i końcowego wykorzystania pamięci za pomocą PerfettoSQL

Uruchom to zapytanie PerfettoSQL w interfejsie Perfetto (karta Zapytanie (SQL)), aby porównać wartości szczytowe i końcowe mem.virt, mem.rss.anon, mem.rss.file i mem.swap dla procesu docelowego:

WITH counters AS (
  SELECT t.name AS counter_name, c.ts, c.value
  FROM counter c
  JOIN process_counter_track t ON c.track_id = t.id
  JOIN process p USING (upid)
  WHERE p.name = 'com.example.app'
    AND t.name IN ('mem.virt', 'mem.rss.anon', 'mem.rss.file', 'mem.swap')
)
SELECT
  counter_name,
  CAST(MAX(value) / 1048576 AS INT) AS peak_mb,
  CAST((
    SELECT value FROM counters AS last
    WHERE last.counter_name = counters.counter_name
    ORDER BY ts DESC LIMIT 1
  ) / 1048576 AS INT) AS final_mb
FROM counters
GROUP BY counter_name
ORDER BY counter_name;

Przykładowe dane wyjściowe logu czasu z rysunku 2:

counter_name  peak_mb  final_mb
mem.rss.anon      111        48
mem.rss.file      212        84
mem.swap           36        36
mem.virt        13952     13952

mem.rss.anon spada o 63 MB, a mem.rss.file o 128 MB od wartości szczytowych, natomiast mem.virt kończy się na wartości szczytowej, ponieważ rezerwacje wirtualne nie ulegają zmianie.

Rozwiązywanie problemu z usuniętymi stronami w plikach

Zdarzenia mm_filemap_add_to_page_cache i mm_filemap_delete_from_page_cache identyfikują pliki według numeru i-węzła (i_ino), a nie ścieżki. Aby sprawdzić, czy usunięte strony należą do wskazanego przez Ciebie pliku, policz usunięcia na i-węzeł:

SELECT
  EXTRACT_ARG(arg_set_id, 'i_ino') AS inode,
  COUNT(*) AS evicted_pages
FROM ftrace_event
WHERE name = 'mm_filemap_delete_from_page_cache'
GROUP BY inode
ORDER BY evicted_pages DESC
LIMIT 5;

Przykładowe dane wyjściowe logu czasu z rysunku 2:

inode     evicted_pages
9761              35781
76792192            767
126                 668
0                   612
11587712            514

Gdy urządzenie, na którym zarejestrowano log, jest nadal połączone, rozwiąż każdy i-węzeł do ścieżki. Numery i-węzłów są unikalne tylko w obrębie jednego systemu plików, dlatego ogranicz wyszukiwanie do systemu plików, który zawiera dane aplikacji:

INODE=<inode from the query>
adb shell "find /data -xdev -inum $INODE 2>/dev/null"

Przykładowe dane wyjściowe dla i-węzła 9761:

/data/user/0/com.android.memorylab/files/asset_pack.bin
/data/data/com.android.memorylab/files/asset_pack.bin

Obie ścieżki odnoszą się do tego samego pliku, ponieważ /data/user/0 i /data/data to 2 nazwy tego samego katalogu danych aplikacji. Liczba wykluczeń dla pliku zasobu (35 781 stron) jest większa niż sam plik (32 768 stron po 4 KB), ponieważ kswapd wykluczył już część pliku pod wpływem obciążenia pamięci, zanim aplikacja odczytała go i wywołała posix_fadvise.

Skrypt, który rozwiązuje wiele problemów z i-węzłami naraz, znajdziesz w sekcji Rozwiązywanie problemów z i-węzłami, a następnie z plikami.


← Interakcja kswapd i lmkd | ↑ W górę | Odchudzony proces w tle →