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): gdy jest włączona flaga funkcji com.android.server.am.system_process_memory_budgets (domyślnie wyłączona w pierwszej wersji Androida 17), samoczynnie dostrajające się rezerwacje cgroup v2 memory.min (ustawione na 90% ustalonego zestawu roboczego każdego demona i ograniczone globalnie do 10% całkowitej pamięci RAM) chronią krytyczne interaktywne demony i procesy powłoki (system_server, surfaceflinger, graficzny composer HAL, audioserver, cameraserver, netd, SystemUI i program uruchamiający ekranu głównego) pod obciążeniem 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 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 Poziomy ochrony demonów systemowych Limity pamięci aplikacji systemowych
Usługa platformowa SystemMemoryBudgetManager MemoryLimiter
Kontrola cgroup v2 memory.min – minimalna cena docelowa (ochrona twarda) memory.high – pułap miękkiego odzyskiwania
Procesy docelowe Interaktywne demony 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 zawieszaniu się wybudzania przy obciążeniu pamięci podczas zastępowania statycznych list plików PinnerService mlock Stale usuwa nieużywaną pamięć podręczną plików jednorazowych i stertę po serii, 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:

  • Podstawowy wycinek systemu (/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 przez zestaw roboczy. 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 pomocnicze podprocesy (takie jak :screenshot) nie zużywają budżetu ochrony.

Zasada samodzielnego dostrajania i 3-poziomowa delegacja cgroup

Zestaw roboczy interaktywnego demona różni się znacznie w zależności od formatu i pojemności pamięci RAM (np. SystemUI używa około 45 MB na zegarku, 60 MB na telefonie z 6 GB pamięci i 180 MB na telefonie z 16 GB pamięci), 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 mogą zostać usunięte. Przed zastosowaniem współczynnika 90% odejmowana jest niezmapowana pamięć podręczna plików (max(0, file - shmem - file_mapped) z memory.stat), dzięki czemu pamięć podręczna skanowania pakietów rozruchowych ani jednorazowe odczyty zasobów nie zawyżają wartości minimalnych.
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 proporcjonalnie zmniejszana o ten sam współczynnik.
Częstotliwość odświeżania 1, 4, 16 i 64 s po uruchomieniu, a następnie co 30 minut 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 typu liść jest ograniczona przez memory.min skonfigurowane w jej nadrzędnych i nadrzędnych grupach cgroup. Ustawienie memory.min tylko w katalogu liścia pid_<pid>, gdy uid_<uid>/memory.min ma wartość 0, nie zapewnia żadnej ochrony. Z drugiej strony ustawienie stałej wartości memory.min tylko na najwyższym poziomie /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 dodatkowym, które współdzielą uid_1000 (android.uid.system), na przejęcie ochrony od 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_* liści, dzięki czemu niezarządzane procesy współdzielące ten identyfikator 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 rodzeństwa pozostają na poziomie 0

Rysunek 1. Trzypoziomowa ochrona delegowania poziomów w wycinkach 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 pojedynczym 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ło na zmniejszenie zużycia pamięci przez SystemUI na zegarku z ok. 45 MB do ok. 5–10 MB, a przez audioserver na składanym urządzeniu z 16 GB pamięci – z 15 MB do 2 MB przy minimalnym zużyciu 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 10% najmniej aktywnych procesów każdego demona i utrzymuje SystemUI, surfaceflinger i audioserver na poziomie 90% bez żadnych anonimowych błędów i bez wzrostu liczby zakończeń procesów lmkd.

memory.min zapewnia twardą rezerwację fizyczną, która odpowiada ochronie przypinania PinnerService mlock, którą zastępuje. Zaletą jest to, że LRU jądra (a nie statyczna lista plików) wybiera, które strony gorącego 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 pojedynczym 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 elementu config_defaultPinnerServiceFiles wymaga skoordynowania 2 zachowań podczas uruchamiania:

  • Jednorazowe wstępne wczytywanie w tle podczas uruchamiania (PinnerService): pomijanie PinnerService mlock bez wstępnego wczytywania pamięci podręcznej strony powoduje przeniesienie odczytów z dysku plików SystemUI APK, ODEX i VDEX o rozmiarze ok. 95 MB do ścieżki synchronicznego uruchamiania, gdy uruchamia się SystemUI (OnBootPhase_550), co wydłuża czas uruchamiania o 300–450 ms . 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ć szybkie uruchamianie, a jednocześnie nie pozostawia żadnych trwale zablokowanych bajtów, dzięki czemu strony uruchamiane „na zimno” można później usunąć.
  • Wykluczanie niezmapowanej pamięci podręcznej plików i odzyskiwanie pamięci podręcznej skanowania pakietów podczas uruchamiania: podczas uruchamiania PackageManagerService skanuje zainstalowane pliki APK, pozostawiając od 1,5 do 2,5 GB niezmapowanej pamięci podręcznej stron plików, która jest przypisana do grupy kontrolnej system_server. Inne chronione procesy (takie jak SystemUI odczytujący pliki zasobów 3D na urządzeniach z XR) mogą również zachowywać niezmapowaną pamięć podręczną plików z jednorazowych odczytów. Jeśli ta pamięć podręczna zostanie uwzględniona w obliczeniach memory.min, może nasycić globalny limit 10% pamięci RAM i proporcjonalnie skalować w dół dolne limity ochrony surfaceflinger, SystemUI i audioserver. Aby zapobiec wyczerpaniu limitu, SystemMemoryBudgetManager odczytuje memory.stat dla każdego chronionego procesu, 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 system_server w jądrach Linux 6.2 i nowszych, aby proaktywnie usunąć nieaktywną pamięć podręczną skanowania pakietów bez zamiany anonimowej sterty lub usuwania 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 do propagowania 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 z włączonym MGLRU wymagają 3 portów upstream w mm/vmscan.c i mm/memcontrol.c, aby puste pośrednie grupy kontrolne slice i UID przekazywał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ą włączać, dostosowywać i wyłączać SystemMemoryBudgetManager za pomocą tych flag funkcji i nakładek zasobów platformy:

  • com.android.server.am.system_process_memory_budgets (flaga funkcji aconfig): włącza SystemMemoryBudgetManager i pomija domyślne przypinanie systemu PinnerService i ekranu głównego mlock (domyślnie wyłączone w pierwszej wersji Androida 17).
  • config_systemMemoryBudgetLowKb (liczba całkowita, KB): 0 (domyślnie) ogranicza sumę wszystkich wartości memory.min do 10% całkowitej fizycznej 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 kamery (android.hardware.perceptionhost i auracamerahal), a urządzenie z Androidem na komputery może chronić demony HAL dźwięku i kamery USB (dras.core.audio i android.hardware.camera.provider-usb-service.android-desktop):

    <!-- XR device overlay -->
    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
    <!-- Android Desktop device overlay -->
    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>dras.core.audio</item>
        <item>android.hardware.camera.provider-usb-service.android-desktop</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
adb shell dumpsys system_server_dumper --name SystemMemoryBudgetManager
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 te procesy 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 aktywności kswapd i procesu zabijania procesów w przypadku małej ilości pamięci (lmkd) w całym systemie.

Jeśli nie zostaną ograniczone, procesy systemowe w tle 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 cgroup czyste strony z plikami z odczytów jednorazowych pozostają na zawsze przypisane do grupy kontrolnej procesu, zużywając setki megabajtów fizycznej pamięci RAM.
  • Zachowywanie 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ą zachowywać nieaktywne anonimowe strony 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 nieaktywne 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 plików „cold” w zawsze włączonych usługach systemowych 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 budżety referencyjnych 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).
  • Budżet 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 udostępnianych identyfikatorów UID systemu 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 RAM-tiered do domyślnego procesu MediaProvider (<process>) i display-scaled budget do procesu UI :PhotoPicker. Każdy atrybut android:maxDeviceRamMb pasuje do urządzeń, 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 nieograniczonej domyślnej 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>

Gdy deklarujesz <processes> w pliku manifestu budżetu, podaj wpisy <process> tylko dla procesów, które deklarują <memory-budget> (jeśli domyślny proces aplikacji deklaruje niestandardową nazwę android:process w <application>, podaj tę nazwę w elemencie <process android:process="...">, zamiast używać nienazwanego tagu <process />). Podczas kompilacji pliku APK aapt2 automatycznie dodaje wpisy <process> dla wszystkich procesów bez budżetu zadeklarowanych przez komponenty w pliku manifestu łączonego i w razie potrzeby, aby zapewnić zgodność z Androidem 17, wstawia element <memory-budget android:maxMb="1000000" /> w ramach <application>, dzięki czemu budżety poszczególnych procesów pozostają odizolowane od procesów docelowych.

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

Budżety 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 fizycznych urządzeń memory.high w ramach głównych ścieżek użytkownika.

Analiza danych telemetrycznych cgroup v2 w całej flocie

Okresowo zbierano migawki cgroup v2 na wszystkich urządzeniach i dzielono je według poziomu fizycznej pamięci RAM (≤ 8 GB, 12 GB i ≥ 16 GB) oraz grupy 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 rezydentnej 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, które są 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 budżet obejmuje tę wartość minimalną oraz zestaw roboczy hosta, z miejscem na rozszerzenie zablokowanego regionu.
  • Odfiltrowanie rozcieńczenia w stanie bezczynności: mechanizmy dostarczania 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 ok.1 MB do 3 MB pamięci. Obliczanie nieprzetworzonych percentyli we wszystkich zrzutach znacznie osłabia 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 stanu podstawowego 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 P95 i P99 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: okresowe próbkowanie może pomijać krótkotrwałe skoki o wysokiej alokacji (np. 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 określone na podstawie danych telemetrycznych i skalibrować budżety proporcjonalne do wyświetlania (takie jak :PhotoPicker) oraz budżety w tle zależne od stanu (takie jak Traceur), przeprowadzono testy memory.high na żywo na urządzeniach fizycznych z pamięcią RAM od 6 GB do 16 GB:

  • 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. Próg zerowego błędu ponownego odczytu to próg, przy którym jądro odzyskuje nieużywaną 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świetlacza 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 odzyskiwania pamięci ART w stanach tła: 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 odzyskiwania pamięci ART CMC (Concurrent Mark-Compact). 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 szamotania procesów 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 podczas CUJ.

Podsumowanie budżetów aplikacji systemowych w ramach referencyjnego projektu AOSP

Poniższa tabela 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 (~75–110 MB) i >2x 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 zimnych 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 na HD+, ~145 MB na FHD+, ~190 MB na ~4,5 MP fold)
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 obsłużyć skoki obciążenia związane z 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 ponownych przy ≥125 MB podczas odzyskiwania pamięci podręcznej zimnych stron (do ~330 MB przy P99).
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 P99 na model 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.

Zbieraj aktywne profile pamięci cgroup v2

W przypadku każdego procesu systemu docelowego odczytaj węzły pamięci cgroup v2 w /sys/fs/cgroup/uid_<uid>/pid_<pid>/ w reprezentatywnych zadaniach i warstwach pamięci RAM:

  • memory.current: bieżąca pamięć rezydentna obciążająca cgroup (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.

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 zdominowane przez pamięć podręczną 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 unevictable lub file_mapped jest duży (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ń): rozmiar android:maxMb powinien obejmować od 90 do 95 centyla memory.peak, aby chwilowe impulsy nie powodowały synchronicznej wymiany ZRAM w wątkach wrażliwych na czas oczekiwania.
  • Procesy interfejsu intensywnie korzystające z wyświetlacza (np. selektory, galerie lub pomocnicze elementy interfejsu systemu): zmierz podstawowe wykorzystanie pamięci przez elementy inne niż interfejs oraz przyrostowe wykorzystanie pamięci przez bitmapę 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 CUJ i po nim:

  • 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 0 nawet po intensywnym strumieniowaniu plików lub indeksowaniu, a file w memory.stat rośnie bez ograniczeń w przypadku stron nieaktywnych. Zmniejsz budżet, aby dopasować go do aktywnego zbioru roboczego.
  • Budżet jest dobrze skalibrowany: podczas normalnego działania wartość memory.current w stanie ustalonym utrzymuje się poniżej memory.high. Podczas intensywnego przesyłania plików lub zapełniania 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.