Budżety pamięci systemowej

W Androidzie 17 i nowszych platforma zarządza pamięcią procesów systemowych za pomocą 2 uzupełniających się mechanizmów cgroup v2 systemu Linux, które działają razem z ogólnym limitem pamięci aplikacji:

  • Dolne limity ochrony demonów systemowych (SystemMemoryBudgetManager z memory.min): samoregulujące się rezerwacje cgroup v2 memory.min (ustawione na 90% ustalonego zestawu roboczego każdego demona i ograniczone globalnie do 10% całkowitej fizycznej pamięci RAM) chronią krytyczne demony interaktywne i procesy powłoki (system_server, surfaceflinger, graficzny HAL composer, audioserver, cameraserver, netd, SystemUI i program uruchamiający Home) w przypadku obciążenia pamięci, zastępując statyczne przypinanie plików PinnerService mlock.
  • Limity pamięci aplikacji systemowych (<memory-budget> z memory.high): skalibrowane pod kątem obciążenia limity miękkie cgroup v2 memory.high (zwykle od 30 MB do 190 MB) zadeklarowane w plikach manifestu dla trwałych i działających w tle aplikacji systemowych (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch i TeleService) proaktywnie odzyskują pamięć podręczną stron plików nieaktywnych i anonimową stertę po gwałtownym wzroście obciążenia, zanim nastąpi globalne obciążenie pamięci.

W tabeli poniżej porównano oba mechanizmy:

Wymiar Minimalne wartości ochrony demonów systemowych Limity pamięci aplikacji systemowych
Usługa platformowa SystemMemoryBudgetManager MemoryLimiter
Kontrola cgroup v2 memory.min – twarda minimalna cena docelowa memory.high – pułap miękkiego odzyskiwania
Procesy docelowe Interaktywne procesy i interfejs powłoki, w przypadku których czas oczekiwania ma kluczowe znaczenie (system_server, surfaceflinger, composer HAL, audioserver, cameraserver, netd, SystemUI i aplikacja Home) trwałe i bezgłowe aplikacje systemowe, mechanizmy dostarczania treści i usługi w tle (takie jak Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch i TeleService);
Model doboru rozmiaru samodzielne dostrajanie w czasie działania (90% ustalonego zestawu roboczego, globalnie ograniczone do 10% całkowitej pamięci RAM) bez tabel dostrajania na urządzenie; Atrybuty XML deklaratywnego pliku manifestu <memory-budget> skalibrowane według poziomu pamięci RAM, stanu procesu i liczby pikseli wyświetlacza
Główna korzyść Zapobiega zacinaniu się interaktywnego interfejsu i blokowaniu wybudzania przy dużym obciążeniu pamięci podczas zastępowania statycznych list plików PinnerService mlock Stale usuwa nieużywaną pamięć podręczną jednorazowych plików i stertę po gwałtownym wzroście, aby pozostawić więcej fizycznej pamięci RAM dla aplikacji na pierwszym planie i aplikacji w pamięci podręcznej

Więcej informacji o składni <memory-budget>, atrybutach XML, interfejsach API środowiska wykonawczego i rozliczaniu pamięci systemu operacyjnego znajdziesz w artykułach Ustawianie budżetów pamięci aplikacji i Omówienie i rozwiązywanie problemów z pamięcią Androida.

Ochrona krytycznych interaktywnych demonów za pomocą SystemMemoryBudgetManager

SystemMemoryBudgetManager utrzymuje procesy, które znajdują się bezpośrednio na ścieżce interaktywnej użytkownika, w fizycznej pamięci RAM podczas obciążenia pamięci. Zamiast przypinać statyczną listę plików APK, ODEX i VDEX do pamięci za pomocą mlock (jak robi to PinnerService w przypadku config_defaultPinnerServiceFiles i config_pinnerHomePinBytes), SystemMemoryBudgetManager przypisuje dynamiczne wartości minimalne cgroup v2 memory.min do tych procesów:

  • Wycinek systemu podstawowego (/sys/fs/cgroup/system): system_server, surfaceflinger, HAL grafiki composer, audioserver, cameraserver i netd, a także wszelkie natywne demony systemowe lub demony dostawcy specyficzne dla formatu wymienione w config_systemMemoryBudgetAdditionalSystemProcesses (np. android.hardware.perceptionhost i auracamerahal na urządzeniach z XR).
  • Interaktywny wycinek aplikacji (/sys/fs/cgroup/apps): główny proces skonfigurowanego pakietu SystemUI (config_systemUi) i główny proces bieżącego użytkownika z rolą ekranu głównego.

Ochrona posiadacza roli Home i config_systemUi zapewnia ochronę na wszystkich rodzajach urządzeń. Na urządzeniach, na których config_systemUi jest lekkim pakietem zgodności (np. Wear OS, gdzie tarcza zegarka, powiadomienia i kafelki działają w aplikacji Home), powłoka dla użytkownika nadal jest w pełni chroniona. Jeśli intencja Home zostanie rozwiązana do pakietu config_systemUi, proces jest chroniony raz. Tylko główny proces każdego pakietu (processName == packageName) otrzymuje minimalny poziom ochrony, więc procesy pomocnicze (takie jak :screenshot) nie zużywają budżetu ochrony.

Zasada samodzielnego dostrajania i 3-poziomowe przekazywanie uprawnień cgroup

Zestaw roboczy interaktywnego demona różni się znacznie w zależności od formatu i pojemności pamięci RAM (np. SystemUI wykorzystuje około 45 MB na zegarku, 60 MB na telefonie z 6 GB pamięci RAM i 180 MB na telefonie z 16 GB pamięci RAM), dlatego statyczne tabele przeglądowe dla poszczególnych urządzeń są niestabilne. Zamiast tego SystemMemoryBudgetManager określa wartości minimalne bezpośrednio na podstawie pamięci rezydentnej każdego procesu, używając 3 stałych:

Parametr zasady Wartość domyślna Zachowanie
Cena minimalna na proces 90% rozliczonych memory.current minus niezmapowana pamięć podręczna plików Chroni anonimową stertę, slab i gorący zmapowany kod, pozostawiając najzimniejsze 10% ogona do odzyskania, dzięki czemu naprawdę zimne strony nadal można usunąć. Niezmapowana pamięć podręczna plików (max(0, file - shmem - file_mapped) z memory.stat) jest odejmowana przed zastosowaniem współczynnika 90%, aby pamięć podręczna skanowania pakietów rozruchowych lub jednorazowe odczyty zasobów nie zawyżały wartości progowych.
Globalny limit pamięci RAM 10% całkowitej pamięci RAM (config_systemMemoryBudgetLowKb) Pasuje do zabezpieczenia PinnerService config_pinnerMaxPinnedMemoryPercentage. Jeśli suma 90% cen minimalnych przekracza 10% wartości MemTotal, każda cena minimalna jest skalowana w dół proporcjonalnie o ten sam współczynnik.
Częstotliwość odświeżania 1, 4, 16 i 64 s po uruchomieniu, a następnie co 30 min Geometryczna seria stabilizacji zbiega się z zestawem roboczym każdego demona po uruchomieniu bez kodowania na stałe opóźnień rozgrzewania na urządzenie, a 30-minutowe odświeżanie stanu stabilnego śledzi stopniowy wzrost i zmniejszanie się zestawu roboczego w ciągu okresu eksploatacji urządzenia.

W kontrolerze pamięci cgroup v2 w systemie Linux (mm/memcontrol.c:effective_protection) efektywna ochrona memory.min grupy cgroup liścia jest ograniczona przez memory.min skonfigurowane w jej grupach cgroup nadrzędnych i przodkach. Ustawienie memory.min tylko w katalogu liścia pid_<pid>, gdy uid_<uid>/memory.min ma wartość 0, nie zapewnia żadnej ochrony. Z kolei ustawienie stałej wartości memory.min tylko w górnym wycinku /sys/fs/cgroup/system powoduje rozłożenie nieprzypisanej ochrony na wszystkie podrzędne identyfikatory UID i procesy proporcjonalnie (memory_recursiveprot), co pozwala procesom diagnostycznym lub pomocniczym współdzielącym uid_1000 (android.uid.system) na przejęcie ochrony z surfaceflinger i audioserver.

Aby zapewnić dokładną izolację poszczególnych demonów, SystemMemoryBudgetManager zapisuje ochronę na wszystkich 3 poziomach hierarchii cgroup v2 przy każdym odświeżeniu, jak pokazano na rysunku 1:

  1. Węzeł procesu liścia (pid_<pid>/memory.min): ustawiony na (skalowany) 90-procentowy dolny limit zestawu roboczego procesu.
  2. Węzeł UID (uid_<uid>/memory.min): ustawiony na dokładną sumę zarządzanych przez niego pid_* poziomów liści, dzięki czemu niezarządzane procesy współdzielące ten UID (takie jak inne AID_SYSTEM demony lub pakiety sharedUserId="android.uid.system") mają memory.min = 0 i nie mogą żądać ochrony.
  3. Wycinek najwyższego poziomu (/sys/fs/cgroup/system/memory.min i /sys/fs/cgroup/apps/memory.min): ustawiony na dokładną sumę uid_* wycinków podrzędnych. Wszystkie nieaktualne wartości memory.low w zarządzanych węzłach są czyszczone do wartości 0, a wszystkie katalogi uid_*, które były zarządzane podczas poprzedniego odświeżania (np. poprzedni program uruchamiający ekranu głównego po przełączeniu przez użytkownika), są resetowane do wartości 0.

Trzypoziomowa hierarchia ochrony memory.min cgroup v2 w /sys/fs/cgroup/system i /sys/fs/cgroup/apps, pokazująca progi procesów liści zsumowane w węzłach UID i slice, podczas gdy niechronione procesy równorzędne pozostają na poziomie 0

Rysunek 1. Trzypoziomowa ochrona delegowania poziomów w ramach wycinków cgroup.

Dlaczego w jądrach MGLRU używana jest wartość memory.min zamiast memory.low

W przypadku wielogeneracyjnego LRU (MGLRU, domyślnego mechanizmu odzyskiwania stron w jądrach Androida 6.6 i nowszych) memory.low i memory.min zachowują się inaczej w mm/vmscan.c:shrink_one:

  • memory.low (odroczenie jednoprzebiegowe): grupa kontrolna, której wykorzystanie jest poniżej memory.low, jest przenoszona na koniec listy LRU memcg (MEMCG_LRU_TAIL) i pomijana w jednym przebiegu odzyskiwania. Przy następnym przejściu w warunkach utrzymującego się obciążenia pamięci jądro odzyskuje całą grupę kontrolną i rejestruje zdarzenie MEMCG_LOW. W testach obciążeniowych urządzeń fizycznych z anonimowym procesem zużywającym pamięć, który rozrastał się do 85% fizycznej pamięci RAM, memory.low pozwolił na zmniejszenie zużycia pamięci przez SystemUI na zegarku z ok. 45 MB do ok. 5–10 MB, a w przypadku audioserver na składanym urządzeniu z 16 GB pamięci – z 15 MB do 2 MB przy minimalnym zużyciu na poziomie ok. 14 MB.
  • memory.min (sztywny dolny limit ochrony): grupa kontrolna, której wykorzystanie jest poniżej memory.min, jest pomijana przez globalne odzyskiwanie. Jak pokazano na rysunku 2, przy tym samym obciążeniu pamięci RAM na poziomie 85% memory.min odrzuca tylko najmniej używane 10% każdego demona i utrzymuje SystemUI, surfaceflinger i audioserver na poziomie 90% bez ponownych błędów anonimowych i bez wzrostu liczby zakończeń procesów lmkd.

memory.min zapewnia twardą rezerwację fizyczną odpowiadającą ochronie przypinania PinnerService mlock, które zastępuje, z tą zaletą, że LRU jądra (a nie statyczna lista plików) wybiera, które strony kodu, sterty i grafiki pozostają rezydentne.

Porównanie pamięci rezydentnej demona przy wzroście obciążenia pamięci systemowej z 40% do 95% fizycznej pamięci RAM, pokazujące, że memory.min utrzymuje się na poziomie 90%, podczas gdy memory.low spada po jednym przejściu odroczenia MGLRU

Rysunek 2. Utrzymywanie zestawu roboczego demona przy rosnącym obciążeniu pamięci.

Wstępne rozgrzewanie podczas uruchamiania i odzyskiwanie pamięci podręcznej niezmapowanych plików po uruchomieniu

Zastąpienie stałego PinnerService mlock config_defaultPinnerServiceFiles wymaga skoordynowania 2 zachowań podczas uruchamiania:

  • Jednorazowe wstępne rozgrzewanie podczas uruchamiania w tle (PinnerService): pomijanie PinnerService mlock bez rozgrzewania pamięci podręcznej strony powoduje przesunięcie odczytów dysku APK, ODEX i VDEX o rozmiarze ok. 95 MB do ścieżki synchronicznego uruchamiania, gdy aplikacja SystemUI się uruchamia (OnBootPhase_550), co wydłuża czas uruchamiania o 300–450 ms.SystemUI Wywołanie madvise(MADV_WILLNEED) nie wystarczy, ponieważ force_page_cache_ra w jądrze ogranicza pojedyncze wywołanie MADV_WILLNEED do jednego okna odczytu z wyprzedzeniem (zwykle od 128 KB do 2 MB). Zamiast tego, gdy SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning jest aktywny, PinnerService mapuje każdy plik w config_defaultPinnerServiceFiles w wątku w tle podczas onStart, synchronicznie wypełnia plik w pamięci podręcznej strony za pomocą Os.mlock i natychmiast wywołuje Os.munlock, munmap i close. Pozwala to zachować czas szybkiego uruchamiania, nie pozostawiając przy tym żadnych trwale zablokowanych bajtów, dzięki czemu strony uruchamiane „na zimno” można później usunąć.
  • Wykluczanie i odzyskiwanie pamięci podręcznej skanowania pakietów rozruchowych system_server: podczas uruchamiania PackageManagerService skanuje zainstalowane pakiety APK, pozostawiając od 1,5 do 2,5 GB nieodwzorowanej pamięci podręcznej stron plików, która jest przypisana do grupy kontrolnej system_server. Jeśli uwzględnimy to w obliczeniach memory.min dla system_server, pamięć podręczna zimnego rozruchu nasyci 10% globalnego limitu pamięci RAM i proporcjonalnie zmniejszy dolne limity ochrony dla surfaceflinger, SystemUI i audioserver. Aby zapobiec niedoborowi limitu, SystemMemoryBudgetManager odczytuje memory.stat dla system_server, oblicza niezmapowaną pamięć podręczną plików jako max(0, file - shmem - file_mapped) i odejmuje tę niezmapowaną pamięć podręczną od memory.current przed obliczeniem dolnego limitu 90%, jak pokazano na rysunku 3. Dodatkowo 10 sekund po PHASE_BOOT_COMPLETED menedżer wykonuje jednorazowy zapis <unmapped_file> swappiness=0 w węźle memory.reclaim elementu system_server w systemie Linux 6.2 i nowszych jądra, aby proaktywnie usunąć nieużywaną pamięć podręczną skanowania pakietów bez zamiany anonimowej sterty lub usunięcia zmapowanych stron DEX i stron kodu. W przypadku starszych jąder (takich jak android14-6.1), które odrzucają swappiness=0 z błędem EINVAL, menedżer pomija zapis memory.reclaim, zamiast powracać do niekwalifikowanego zapisu odzyskiwania, który podczas uruchamiania zamieniłby anonimową stertę.

Oś czasu pamięci system_server po uruchomieniu pokazująca geometryczne odświeżanie po 1, 4, 16 i 64 sekundach, wykluczenie niezmapowanej pamięci podręcznej skanowania pakietów rozruchowych z 90% minimalnego progu memory.min oraz jednorazowe memory.reclaim z swappiness=0

Rysunek 3. Konwergencja minimalnego poziomu ochrony i odzyskiwanie pamięci podręcznej po uruchomieniu.

Wymagania dotyczące jądra i nakładki konfiguracji urządzenia

SystemMemoryBudgetManager korzysta z mem_cgroup_calculate_protection w jądrze Linuksa, aby propagować memory.min w zagnieżdżonych grupach kontrolnych. Wspólne jądra Androida android15-6.6 i android16-6.12 od razu zapewniają pełną obsługę ochrony zagnieżdżonej MGLRU. Urządzenia z systemem android14-6.1 i włączoną funkcją MGLRU wymagają 3 backportów upstream w mm/vmscan.c i mm/memcontrol.c, aby puste pośrednie grupy kontrolne slice i UID propagowały ochronę do grup kontrolnych procesów liści:

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

Producenci OEM mogą dostosować lub wyłączyć SystemMemoryBudgetManager za pomocą tych nakładek zasobów platformy:

  • config_systemMemoryBudgetLowKb (liczba całkowita, KB): 0 (domyślnie) ogranicza sumę wszystkich wartości memory.min do 10% całkowitej pamięci RAM. Wartość dodatnia ustawia wyraźny limit w KB, a wartość ujemna wyłącza SystemMemoryBudgetManager na urządzeniu (zachowując starsze statyczne przypinanie mlock PinnerService).
  • config_systemMemoryBudgetHighKb (liczba całkowita, KB): 0 (domyślnie) pozostawia /sys/fs/cgroup/system/memory.high na poziomie max. Wartość dodatnia ustawia pułap ceny minimalnej dla najwyższego wycinka memory.high (musi być większa lub równa pułapowi ceny minimalnej).
  • config_systemMemoryBudgetAdditionalSystemProcesses (tablica tekstowa): zawiera listę dodatkowych nazw procesów demonów dostawcy lub systemu natywnego o krytycznym znaczeniu dla opóźnienia w /sys/fs/cgroup/system (dopasowanych do /proc/<pid>/comm lub nazwy podstawowej argv[0] w /proc/<pid>/cmdline dla identyfikatorów UID systemu i dostawcy w 10000), które mają być chronione przez 90% memory.min wraz z 6 domyślnymi demonami systemowymi. Użyj tej nakładki zamiast ustawiać memory.min w init.rc, ponieważ SystemMemoryBudgetManager ponownie oblicza uid_<uid>/memory.min jako sumę zarządzanych procesów liści i czyści niezarządzane węzły uid_* przy każdym odświeżeniu. Na przykład urządzenie z XR może chronić demony HAL śledzenia przestrzennego i aparatu za pomocą tej nakładki:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (ciąg tekstowy): nazwa pakietu aplikacji SystemUI platformy, rozwiązana za pomocą MATCH_SYSTEM_ONLY.

Aby sprawdzić aktywne piętra SystemMemoryBudgetManager i węzły cgroup na urządzeniu, uruchom:

# 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

Powiązanie trwałych i działających w tle aplikacji systemowych z budżetami pamięci

SystemMemoryBudgetManager chroni interaktywne demony przed odzyskaniem za pomocą poziomów memory.min, ale wiele innych aplikacji systemowych i uprzywilejowanych usług platformy działa w tle przez długi czas jako procesy PERSISTENT, powiązane usługi na pierwszym planie lub często odpytywane mechanizmy dostarczania treści. Bez wyraźnych limitów pamięci procesy te nie są ograniczane, dopóki nie osiągną limitów ogranicznika pamięci wynoszących kilka gigabajtów (np. od 4 GB do 10 GB w przypadku widocznych aplikacji i od 2 GB do 5 GB w przypadku aplikacji działających w tle) lub dopóki globalne obciążenie pamięci nie wywoła działania kswapd i procesu zabijania procesów przy małej ilości pamięci (lmkd) w całym systemie.

Jeśli procesy systemowe w tle nie są ograniczone, często gromadzą duże ilości pamięci nieaktywnej, która nie jest częścią ich aktywnego zestawu roboczego:

  • Nieograniczona pamięć podręczna stron plików: usługi, które indeksują multimedia, przeszukują bazy danych lub synchronizują tabele kalendarza i dostawcy (takie jak AppSearch, MediaProvider i CalendarProvider), mapują lub przesyłają strumieniowo duże pliki na dysku. Bez limitu memory.high grupy kontrolnej czyste strony z plikami z jednorazowych odczytów pozostają obciążone grupą kontrolną procesu na czas nieokreślony, zużywając setki megabajtów fizycznej pamięci RAM.
  • Przechowywanie anonimowej sterty po gwałtownym wzroście aktywności: usługi, w których występują przejściowe skoki aktywności, takie jak Bluetooth gwałtowne wzrosty aktywności związane z wykrywaniem dźwięku i BLE, NetworkStack weryfikacja TLS portalu przechwytującego lub przechodzenie pomocników interfejsu w tło, mogą przechowywać nieaktywne strony anonimowej sterty długo po zakończeniu gwałtownego wzrostu aktywności.

Zastosowanie skalibrowanych deklaracji <memory-budget> (memory.high limity miękkie, zwykle od 30 MB do 190 MB) do aplikacji systemowych zapewnia 2 główne korzyści:

  • Proaktywne, zlokalizowane odzyskiwanie stron: gdy proces systemowy przekroczy skonfigurowany budżet memory.high, jądro Linuksa kieruje odzyskiwanie stron konkretnie do tej grupy kontrolnej, najpierw usuwając nieużywane strony powiązane z plikami i kompresując nieaktywne strony anonimowe do ZRAM bez czekania na globalne obciążenie pamięci urządzenia.
  • Więcej dostępnej pamięci RAM dla aplikacji na pierwszym planie i puli aplikacji w pamięci podręcznej: ograniczenie pamięci podręcznej nieużywanych plików w usługach systemowych, które są zawsze włączone, pozwala odzyskać od kilkudziesięciu do kilkuset megabajtów fizycznej pamięci RAM na urządzeniu, co zmniejsza zacinanie się interfejsu, obniża obciążenie podczas uruchamiania aparatu i utrzymuje więcej aplikacji użytkownika w stanie zapisanym w pamięci podręcznej.

Konfigurowanie budżetów pamięci aplikacji systemowych

Aplikacje systemowe używają tej samej składni pliku manifestu <memory-budget>, kaskadowego stanu procesu, skalowania gęstości wyświetlania i reguły rozwiązywania ostatniej pasującej klauzuli co aplikacje ogólne. Pełną listę atrybutów znajdziesz w dokumentacji atrybutów XML.

Producenci OEM mogą deklarować elementy <memory-budget> bezpośrednio w pliku AndroidManifest.xml aplikacji systemowej lub scalać je z osobnego pliku XML za pomocą właściwości additional_manifests w pliku Android.bp. Zgodnie z konwencją AOSP przechowuje referencyjne budżety aplikacji systemowych w samodzielnych plikach *-budget.xml (w frameworks/base/core/memory_budgets/ w przypadku pakietów platformy podstawowej lub obok poszczególnych modułów, takich jak packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ i packages/services/Telephony/) i dołącza je za pomocą additional_manifests:

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

Zakres na poziomie pakietu (UID) i na poziomie procesu (PID)

To, czy zadeklarujesz podstawowy budżet na poziomie pakietu (<application>) czy budżet wieloprocesowy (<processes><process>), określa, czy jądro wymusza memory.high na poziomie grupy kontrolnej UID (/sys/fs/cgroup/uid_<uid>/memory.high) czy na poziomie grupy kontrolnej PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), jak pokazano na rysunku 4:

  • Budżet na poziomie pakietu (<application>): używaj, gdy pakiet APK uruchamia pojedynczy proces w ramach własnego dedykowanego identyfikatora UID (np. Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur lub AppSearch).
  • Limit na poziomie procesu (<processes><process>): używaj, gdy plik APK udostępnia android:sharedUserId (np. android.uid.phone z identyfikatorem UID 1001 lub android.uid.system z identyfikatorem UID 1000) innym pakietom systemowym, aby inne procesy udostępniające ten identyfikator UID nie były ograniczane razem, lub gdy pojedynczy plik APK uruchamia wiele procesów z różnymi rolami pamięci (np. proces MediaProvider działający w tle i podproces interfejsu :PhotoPicker).

Hierarchia Cgroup v2 porównująca egzekwowanie memory.high na poziomie UID w przypadku dedykowanych identyfikatorów UID z egzekwowaniem memory.high na poziomie PID w przypadku współdzielonych systemowych identyfikatorów UID i plików APK z wieloma procesami

Rysunek 4. Limity pamięci na poziomie UID i PID.

Poniższy przykład pokazuje MediaProvider-budget.xml, który ogranicza budżety na poziomie pamięci RAM do domyślnego procesu MediaProvider (<process>) i budżetu skalowanego do wyświetlacza do procesu interfejsu :PhotoPicker. Każdy atrybut android:maxDeviceRamMb odpowiada urządzeniom, których łączna fizyczna pamięć RAM (MemTotal) jest równa lub mniejsza od określonego progu w MB (lub dotyczy dowolnej pojemności pamięci RAM, jeśli zostanie pominięty). Klauzule są uporządkowane od domyślnej bez ograniczeń do najmniejszego progu maxDeviceRamMb, dzięki czemu reguła rozstrzygania ostatniej pasującej klauzuli wybiera najbardziej rygorystyczny poziom:

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

Jak skalibrowano referencyjne budżety AOSP na urządzeniach Google Pixel

Limity pamięci aplikacji systemowych w AOSP zostały skalibrowane na urządzeniach Google Pixel z pamięcią RAM od 6 GB do 16 GB (w tym telefony, urządzenia składane i tablety z 6 GB, 8 GB, 12 GB i 16 GB pamięci RAM) oraz rozdzielczością wyświetlacza od ok.1, 2 MP (720 × 1600) do ok.4, 5 MP (2076 × 2152).

Kalibracja łączyła 2 uzupełniające się źródła danych: dane telemetryczne z pól cgroup v2 z całej floty oraz kontrolowane skanowanie memory.high na urządzeniach fizycznych w ramach głównych ścieżek użytkownika.

Analiza danych telemetrycznych cgroup v2 w całej flocie

Okresowe migawki cgroup v2 były zbierane na wszystkich urządzeniach i dzielone według poziomu fizycznej pamięci RAM (≤ 8 GB, 12 GB i ≥ 16 GB) oraz zasobnika stanu procesu (foreground, perceptible, background i cached). Analiza statystyczna opierała się na 6 zasadach:

  • Rozkład memory.current na zestaw roboczy i pamięć podręczną plików, którą można odzyskać: ponieważ memory.high zarządza obciążeniem grupy kontrolnej (memory.current), każdy zrzut został rozłożony za pomocą memory.stat na anonimową stertę (anon), zamienione anonimowe strony (swap) i pamięć podręczną stron opartą na plikach (file), jak pokazano na rysunku 5. Łączne nieskompresowane anonimowe zapotrzebowanie (anon + swap przy P99) określa minimalny poziom zbioru roboczego dla budżetu, dzięki czemu proces nie jest narażony na wyczerpanie pamięci ZRAM, a duża różnica między memory.current P99 a anon + swap P99 wskazuje na pamięć podręczną stron plików, którą memory.high może odzyskać, pod warunkiem że pamięć podręczna jest niezmapowana i nieaktywna.
  • Uwzględnij pamięć, której memory.high nie może odzyskać: nie wszystkie pliki i pamięć współdzielona są zimną pamięcią podręczną. Strony zablokowane (unevictable w memory.stat, np. pamięć współdzielona zablokowana za pomocą mlock lub zablokowana pamięć RAM gościa maszyny wirtualnej, której procesy hosta są obciążane w grupie kontrolnej aplikacji) ustawiają twardy limit budżetu, a zmapowane strony (file_mapped) często są częścią aktywnego zestawu roboczego. Zanim uznasz, że pamięć pliku można odzyskać, sprawdź unevictable, shmem i file_mapped. Na przykład około 100 MB pamięci AppSearch jest zablokowane i nie można go usunąć, więc jego budżet obejmuje tę wartość minimalną oraz zestaw roboczy hosta, z miejscem na rozszerzenie zablokowanego regionu.
  • Odfiltrowanie rozcieńczenia w stanie bezczynności: dostawcy treści (CalendarProvider) i narzędzia dla programistów na żądanie (Traceur) spędzają od 70% do 99,9% czasu w stanie bezczynności, zajmując od 1 MB do 3 MB pamięci. Obliczanie nieprzetworzonych percentyli we wszystkich zrzutach znacznie obniża szacunki P95 i P99. Zamiast tego percentyle i przedziały ufności zostały obliczone na podstawie nierozcieńczonych aktywnych zrzutów (po odfiltrowaniu zrzutów CACHED i zrzutów bazowych stanu bezczynności).
  • Używaj przedziałów ufności pogrupowanych według urządzeń: ponieważ jedno urządzenie dostarcza wiele okresowych zrzutów w czasie, traktowanie zrzutów jako niezależnych próbek zaniża niepewność ogona. Wszystkie szacunki dotyczące zestawu roboczego w 95 i 99 percentylu zostały ocenione przy użyciu konserwatywnych 95-procentowych przedziałów ufności dla klastrów urządzeń (przy użyciu liczby różnych urządzeń N_dev jako efektywnej wielkości próby).
  • Uwzględnianie przejściowych skoków za pomocą memory.peak: próbkowanie okresowe może pomijać krótkotrwałe skoki o wysokiej alokacji (takie jak walidacja TLS portalu przechwytującego NetworkStack i równoczesne sondy DNS lub inicjowanie Bluetooth A2DP i BLE). W przypadku usług o zmiennym obciążeniu budżety zostały dostosowane tak, aby pokrywały od 90 do 95 percentyla maksymalnego wykorzystania pamięci memory.peak jądra w okresie eksploatacji na różnych poziomach pamięci RAM.
  • Sprawdź, czy poziom pamięci RAM odzwierciedla pamięć RAM, a nie generację urządzenia: nowsze urządzenia mają zwykle więcej pamięci RAM i więcej funkcji, więc zestaw roboczy, który rośnie wraz z poziomem pamięci RAM, może w rzeczywistości rosnąć wraz z liczbą funkcji. Zanim dodasz poziom maxDeviceRamMb, podziel dane według modelu urządzenia. Na przykład com.android.phone osiąga ten sam 99 centyl anon + swap (~85–90 MB) na telefonach z 12 GB i 16 GB pamięci RAM tej samej generacji, podczas gdy starszy składany telefon z 12 GB pamięci RAM wykorzystuje 40 MB. Dlatego budżet wykorzystuje jedną wartość bez poziomu pamięci RAM, dzięki czemu urządzenie z mniejszą ilością pamięci RAM, które obsługuje nowsze funkcje telefoniczne, nie ma zbyt małego budżetu.

Rozkład pamięci cgroup w czasie na nieskompresowany anonimowy zestaw roboczy, aktywne okno pamięci podręcznej plików i zimną jednorazową pamięć podręczną plików odzyskaną poza memory.high

Rysunek 5. Zestaw roboczy a pamięć podręczna plików, które można odzyskać.

CUJ na urządzeniu fizycznym i skalowanie wyświetlacza

Aby zweryfikować budżety pochodzące z telemetrii i skalibrować budżety proporcjonalne do wyświetlania (takie jak :PhotoPicker) oraz budżety tła zależne od stanu (takie jak Traceur), na urządzeniach fizycznych z pamięcią RAM od 6 GB do 16 GB przeprowadzono testy memory.high:

  • Znajdowanie punktu zerowego ponownego błędu: podczas wykonywania głównych ścieżek użytkownika od początku do końca (takich jak szybkie przewijanie siatki zdjęć, przeglądanie albumów, przesuwanie podglądu zdjęć i filmów w wysokiej rozdzielczości na pełnym ekranie oraz nagrywanie i zapisywanie rekordów śledzenia) wartość memory.high była zmniejszana podczas pomiaru liczników cgroup memory.stat (workingset_refault_anon, workingset_refault_file, pgscan i pgsteal) oraz memory.events (high). Jak pokazano na rysunku 6, optymalny budżet znajduje się tuż nad punktem zerowym ponownego błędu. Punkt zerowego ponownego błędu to próg, przy którym jądro odzyskuje zimną jednorazową pamięć podręczną plików (pgsteal > 0) bez wywoływania aktywnego thrashingu zbioru roboczego (workingset_refault == 0).
  • Weryfikacja skalowania pikseli wyświetlacza w różnych rozdzielczościach: testy:PhotoPicker w 4 fizycznych i emulowanych rozdzielczościach wyświetlacza od ok.1,2 Mpix (720 × 1600) do ok.4,5 Mpix (2076 × 2152, rozłożony wyświetlacz wewnętrzny) potwierdziły, że aktywny zestaw roboczy bitmap Glide i ViewPager2 skaluje się liniowo wraz z liczbą pikseli wyświetlacza. Wartość bazowa bez wyświetlania również różni się w zależności od urządzenia: przy prawie tej samej liczbie pikseli punkt zerowy wynosił około 140 MB na zewnętrznym wyświetlaczu składanego urządzenia z 16 GB pamięci, a na telefonie z 6 GB pamięci – od 110 do 125 MB. Wartość bazowa 85 MB plus 25 bajtów na piksel wyświetlacza (android:maxMb="85" i android:additionalBytesPerDisplayPixel="25") pozostaje powyżej zmierzonego punktu przegięcia na każdym testowanym wyświetlaczu (~110 MB na HD+, ~145 MB na FHD+ i ~190 MB na rozłożonych wyświetlaczach).
  • Weryfikowanie czyszczenia pamięci ART w tle: w cgroup v2 strony obrazu frameworka i boot.art udostępniane przez Zygote pozostają przypisane do cgroup Zygote i są traktowane jako przestrzeń odporna podczas czyszczenia pamięci ART w trybie równoczesnego oznaczania i kompaktowania (CMC). Testy obciążeniowe potwierdziły, że gdy aplikacja interfejsu systemu przechodzi do stanu background, zaostrzenie memory.high (na przykład do 30 MB na Traceur) wyraźnie ogranicza prywatną stertę i pamięć podręczną stron bez powodowania nadmiernego czyszczenia pamięci ani aktywności wymiany.

Wykres odzyskiwania zimnej pamięci podręcznej (pgsteal) i thrashingu zbioru roboczego (workingset_refault) w miarę obniżania wartości memory.high podczas CUJ, z wyróżnieniem punktu zerowego i budżetu docelowego

Rysunek 6. Znajdowanie punktu zerowego błędu podczas CUJ.

Podsumowanie budżetów aplikacji systemowych w systemie referencyjnym AOSP

Tabela poniżej zawiera podsumowanie skalibrowanych budżetów referencyjnych w AOSP na różnych poziomach pamięci RAM i w różnych stanach procesów:

Pakiet lub proces systemowy Zakres ≤ 8 GB RAM 12 GB RAM ≥ 16 GB RAM Główny sterownik kalibracji
com.android.bluetooth UID (<application>) 80 MB 95 MB 110 MB Obejmuje skoki memory.peak w ciągu cyklu życia P90 (ok. 75–110 MB) i ponad 2-krotność aktywnych memory.current P99.
com.android.nfc UID (<application>) 50 MB 60 MB 60 MB Obejmuje aktywną emulację karty hosta P99 (do ok. 50 MB) i P90 do P95 memory.peak podczas przycinania pamięci podręcznej nieużywanych plików.
com.android.providers.calendar UID (<application>) 70 MB domyślnie (na pierwszym planie)
50 MB perceptible i background
Aktywny zestaw roboczy jest niezmienny na różnych poziomach pamięci RAM. Budżet w tle zachowuje anonimowy zapas na alarmy przypomnień, jednocześnie przycinając pamięć podręczną stron SQLite podczas synchronizacji.
com.android.traceur UID (<application>) 85 MB domyślnie (pierwszy plan i procesy widoczne)
30 MBbackground
Zachowuje pełny zapas 85 MB podczas aktywnego nagrywania i eksportowania śladu (TraceService), a w stanie bezczynności i transmisji ogranicza go do 30 MB.
com.android.providers.media.module (główny) Identyfikator zamówienia reklamowego (<process>) 60 MB 75 MB 85 MB Zapewnia około 10–30 MB wolnego miejsca powyżej P99 anon + swap, jednocześnie odzyskując nieużywaną pamięć podręczną SQLite i plików miniatur.
com.android.providers.media.module:PhotoPicker Identyfikator zamówienia reklamowego (<process>) 85 MB + 25 bajtów na piksel
(~110 MB w przypadku HD+, ~145 MB w przypadku FHD+, ~190 MB w przypadku ~4,5 MP)
Skaluje się proporcjonalnie do liczby pikseli wyświetlacza (additionalBytesPerDisplayPixel="25"), aby dopasować się do punktu przegięcia zestawu roboczego bitmapy Glide.
com.android.networkstack UID (<application>) 65 MB 70 MB 85 MB Głównie pamięć anonimowa, o rozmiarze odpowiadającym 95 centylowi czasu życia memory.peak, aby pokryć skoki TLS, DNS i DHCP portalu przechwytującego.
com.android.appsearch.apk UID (<application>) 130 MB Obejmuje około 100 MB zablokowanej, nieusuwalnej pamięci oraz około 5 MB aktywnego zestawu roboczego hosta, z miejscem na rozszerzenie zablokowanego regionu. Zero błędów ponownego odczytu przy ≥125 MB, gdy pamięć podręczna zimnych stron (do ~330 MB przy P99) jest odzyskiwana.
com.android.phone (TeleService) Identyfikator zamówienia reklamowego (<process>) 120 MB Zakres dla poszczególnych identyfikatorów PID na udostępnionym identyfikatorze UID 1001. Zestaw roboczy śledzi generację urządzenia, a nie pamięć RAM, więc pojedynczy budżet jest o około 30 MB wyższy niż najwyższy percentyl P99 dla danego modelu (anon + swap).

Jak producenci OEM mogą ponownie skalibrować budżety aplikacji systemowych na swoich urządzeniach

Producenci OEM, którzy dostosowują aplikacje systemowe (np. dodają profile Bluetooth dostawcy, niestandardowe rozszerzenia telefoniczne lub indeksatory multimediów OEM) lub dostarczają dodatkowe wstępnie zainstalowane usługi systemowe, mogą ponownie skalibrować istniejące budżety lub utworzyć nowe pliki *-budget.xml dla swojego portfolio sprzętu.

Zbieranie aktywnych profili pamięci cgroup v2

Dla każdego docelowego procesu systemowego odczytaj jego węzły pamięci cgroup v2 w /sys/fs/cgroup/uid_<uid>/pid_<pid>/ w reprezentatywnych zbiorach zadań i warstwach pamięci RAM:

  • memory.current: bieżąca pamięć rezydentna obciążająca grupę kontrolną (z wyłączeniem stron udostępnianych przez Zygote)
  • memory.peak: najwyższy poziom memory.current od początku procesu
  • memory.swap.current: bieżące wykorzystanie skompresowanej przestrzeni wymiany w ZRAM
  • memory.stat: zestawienie danych dotyczących: anon, file, pgscan, pgsteal, workingset_refault_anon i workingset_refault_file
  • memory.events: łączna liczba naruszeń high (memory.high zdarzenia ograniczania i odzyskiwania)

Podczas agregowania danych telemetrycznych z urządzeń testowych lub urządzeń w terenie oddziel aktywne stany procesów (TOP, FOREGROUND_SERVICE, aktywne powiązania i aktywne synchronizacje) od stanów bezczynności CACHED lub PERSISTENT, aby migawki bezczynności nie zniekształcały obliczeń zestawu roboczego P95 i P99 w dół.

Oddzielenie nieskompresowanej sterty od pamięci podręcznej plików, którą można odzyskać

Porównaj 99 centyl anon + swap z 99 centylem memory.current:

  • Usługi, w których dominuje pamięć podręczna plików (file &gg; anon, takie jak dostawcy baz danych, indeksatory wyszukiwania i demony logów): ustaw android:maxMb, aby zapewnić wygodny margines (zwykle od 1,5 raza do 4 razy) powyżej P99 anon + swap oraz okno robocze dla stron z często używanymi plikami. Ta konfiguracja umożliwia jądru ciągłe usuwanie nieużywanych, niezmapowanych stron plików bez wpływu na anonimową stertę. Jeśli wartość unevictable lub file_mapped jest duża (jak w przypadku zablokowanej pamięci lub baz danych mapowanych w pamięci), uwzględnij ten rozmiar również w zestawie roboczym.
  • Usługi zdominowane przez anonimowe sterty lub usługi o charakterze impulsowym (anon stanowi większość memory.current, np. stosy sieciowe lub stosy połączeń): ustaw rozmiar android:maxMb tak, aby obejmował od 90 do 95 centyla memory.peak, dzięki czemu przejściowe impulsy nie będą wyzwalać synchronicznej wymiany ZRAM w wątkach wrażliwych na opóźnienia.
  • Procesy interfejsu intensywnie korzystające z wyświetlacza (takie jak selektory, galerie czy pomocnicy interfejsu systemowego): zmierz podstawowe wykorzystanie pamięci przez procesy niezwiązane z interfejsem oraz przyrostowe wykorzystanie pamięci przez bitmapy na megapiksel na wyświetlaczach o najniższej i najwyższej rozdzielczości, a następnie określ budżet za pomocą android:maxMb i android:additionalBytesPerDisplayPixel.

Czyszczenie pamięci na urządzeniach fizycznych

Zanim przeznaczysz nowy budżet na kompilację, użyj am memory-limiter manual (lub zapisz bezpośrednio w węźle cgroup memory.high procesu w kompilacji userdebug), aby przetestować limity kandydatów podczas automatycznych testów CUJ:

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

Monitoruj memory.stat i memory.events przed i po głównej ścieżce użytkownika:

  • Budżet jest za niski: workingset_refault_anon lub workingset_refault_file gwałtownie rośnie podczas CUJ, memory.swap.current stale rośnie podczas aktywnej pracy na pierwszym planie lub następuje regresja klatki CUJ lub opóźnienia Binder. Zwiększ budżet powyżej domyślnego punktu przegięcia.
  • Budżet jest za wysoki: memory.events:high pozostaje na poziomie 0 nawet po intensywnym przesyłaniu strumieniowym plików lub indeksowaniu, a file w memory.stat rośnie bez ograniczeń w przypadku stron rzadko odwiedzanych. Zmniejsz budżet, aby zbliżyć go do aktywnego zestawu roboczego.
  • Budżet jest dobrze skalibrowany: podczas normalnego działania wartość memory.current w stanie ustalonym utrzymuje się poniżej memory.high. Podczas intensywnego korzystania z plików lub pamięci podręcznej pgsteal odzyskuje strony nieużywanych plików, podczas gdy workingset_refault_anon pozostaje blisko zera, a opóźnienie CUJ nie ulega zmianie.