Бюджеты системной памяти

В Android 17 и более поздних версиях платформа управляет памятью системных процессов с помощью двух дополнительных механизмов Linux cgroup v2, которые работают вместе с общим ограничителем памяти приложений:

  • Нижние пороги защиты системных демонов (SystemMemoryBudgetManager с memory.min). Саморегулирующиеся резервирования memory.min в cgroup v2 (установленные на уровне 90 % от рабочего набора каждого демона и ограниченные на глобальном уровне 10% от общего объема физической ОЗУ) защищают критически важные интерактивные демоны и процессы оболочки (system_server, surfaceflinger, графический HAL composer, audioserver, cameraserver, netd, SystemUI и главный экран) при нехватке памяти, заменяя статическое закрепление файлов PinnerService mlock.
  • Ограничения памяти для системных приложений (<memory-budget> с memory.high). Мягкие ограничения cgroup v2 memory.high, откалиброванные по рабочей нагрузке (обычно от 30 до 190 МБ), объявлены в файлах манифеста для постоянных и фоновых системных приложений (Bluetooth, Nfc, CalendarProvider, Traceur, MediaProvider, :PhotoPicker, NetworkStack, AppSearch и TeleService). Они упреждающе освобождают кэш страниц неиспользуемых файлов и анонимную кучу после пиковой нагрузки, прежде чем возникнет глобальная нехватка памяти.

В таблице ниже сравниваются эти два механизма.

Параметр Уровни защиты системных демонов Максимальный объем памяти системных приложений
Сервис платформы SystemMemoryBudgetManager MemoryLimiter
Управление cgroup v2 memory.min жесткая минимальная цена memory.high: максимальное количество возвращенных показов
Целевые процессы Интерактивные демоны, критичные к задержкам, и интерфейс оболочки (system_server, surfaceflinger, composer HAL, audioserver, cameraserver, netd, SystemUI и приложение Google Home). Постоянные и безголовые системные приложения, поставщики контента и фоновые службы, такие как Bluetooth, Nfc, MediaProvider, NetworkStack, AppSearch и TeleService.
Модель определения размера Самостоятельная настройка во время выполнения (90% от установленного рабочего набора, глобальное ограничение в 10% от общего объема ОЗУ) без таблиц настройки для каждого устройства. Декларативные XML-атрибуты манифеста <memory-budget>, откалиброванные по уровню ОЗУ, состоянию процесса и количеству пикселей на экране
Основное преимущество Предотвращает задержки в работе интерактивного интерфейса и зависания при пробуждении из-за нехватки памяти во время замены списков статических файлов PinnerService mlock. Постоянно удаляет неиспользуемый одноразовый кеш файлов и кучу после периода ускорения, чтобы освободить больше физической ОЗУ для активных и кешированных приложений.

Дополнительную информацию о синтаксисе <memory-budget>, атрибутах XML, API среды выполнения и учете памяти ОС можно найти в статьях Как задать бюджеты памяти для приложений и Как устроена память Android и как устранять неполадки с ней.

Защита критически важных интерактивных демонов с помощью SystemMemoryBudgetManager

SystemMemoryBudgetManager сохраняет процессы, которые находятся непосредственно на интерактивном пути пользователя, в физической оперативной памяти при нехватке памяти. Вместо того чтобы закреплять статический список файлов APK, ODEX и VDEX в памяти с помощью mlock (как PinnerService делает для config_defaultPinnerServiceFiles и config_pinnerHomePinBytes), SystemMemoryBudgetManager назначает динамические нижние границы memory.min cgroup v2 следующим процессам:

  • Срез основной системы (/sys/fs/cgroup/system). system_server, surfaceflinger, HAL графики composer, audioserver, cameraserver и netd, а также любые нативные системные демоны или демоны поставщика, зависящие от форм-фактора и перечисленные в config_systemMemoryBudgetAdditionalSystemProcesses (например, android.hardware.perceptionhost и auracamerahal на XR-устройствах).
  • Интерактивный фрагмент приложения (/sys/fs/cgroup/apps). Основной процесс настроенного пакета SystemUI (config_systemUi) и основной процесс приложения, которому назначена роль главного экрана текущего пользователя.

Защита роли Home наряду с config_systemUi обеспечивает охват всех типов устройств. На устройствах, где config_systemUi – это облегченный пакет совместимости (например, на Wear OS, где циферблат, уведомления и карточки работают в приложении Home), пользовательская оболочка по-прежнему получает полную защиту рабочего набора. Если намерение Home разрешается в пакет config_systemUi, процесс защищается один раз. Минимальный уровень защиты получает только основной процесс каждого пакета (processName == packageName), поэтому вспомогательные подпроцессы (например, :screenshot) не расходуют бюджет защиты.

Правила самонастройки и трехуровневое делегирование cgroup

Поскольку рабочий набор интерактивного демона сильно зависит от форм-фактора и объема ОЗУ (например, SystemUI использует около 45 МБ на умных часах, 60 МБ на телефоне с 6 ГБ ОЗУ и 180 МБ на телефоне с 16 ГБ ОЗУ), статические таблицы поиска для каждого устройства ненадежны. Вместо этого SystemMemoryBudgetManager выводит минимальные значения непосредственно из резидентного объема памяти каждого процесса, используя три константы:

Параметр правила Значение по умолчанию Поведение
Минимальная цена за процесс 90% от урегулированной суммы memory.current за вычетом кеша несопоставленных файлов Защищает анонимную кучу, slab-аллокатор и часто используемый код, оставляя 10% наименее используемых страниц доступными для вытеснения. Кэш несопоставленных файлов (max(0, file - shmem - file_mapped) из memory.stat) вычитается до применения коэффициента 90 %, чтобы кэш сканирования пакетов при загрузке или однократное считывание объектов не завышали минимальные значения.
Общее ограничение на объем ОЗУ 10% от общего объема ОЗУ (config_systemMemoryBudgetLowKb) Соответствует ограничению PinnerService config_pinnerMaxPinnedMemoryPercentage. Если сумма 90% минимальных цен превышает 10% от MemTotal, то все минимальные цены пропорционально уменьшаются на один и тот же коэффициент.
Частота обновления Через 1, 4, 16 и 64 секунды после запуска, а затем каждые 30 минут. Геометрическая серия урегулирования сходится на рабочем наборе каждого демона после запуска без жесткого кодирования задержек прогрева для каждого устройства, а 30-минутное обновление в установившемся режиме отслеживает постепенный рост и сокращение рабочего набора в течение срока службы устройства.

В контроллере памяти Linux cgroup v2 (mm/memcontrol.c:effective_protection) эффективная защита memory.min дочерней группы cgroup ограничена значением memory.min, настроенным для ее родительской и вышестоящих групп cgroup. Если задать правило memory.min только для конечного каталога pid_<pid>, а для правила uid_<uid>/memory.min задать значение 0, защита не будет работать. И наоборот, если задать плоский memory.min только для верхнего слоя /sys/fs/cgroup/system, то невостребованная защита будет распределена между всеми дочерними UID и процессами пропорционально (memory_recursiveprot). В результате диагностические или вторичные процессы, использующие uid_1000 (android.uid.system), смогут забирать защиту у surfaceflinger и audioserver.

Чтобы обеспечить точную изоляцию каждого демона, SystemMemoryBudgetManager записывает защиту на всех трех уровнях иерархии cgroup v2 при каждом обновлении, как показано на рисунке 1:

  1. Дочерний узел процесса (pid_<pid>/memory.min). Установите значение, равное 90% от минимального рабочего набора процесса (с учетом масштабирования).
  2. Узел UID (uid_<uid>/memory.min). Устанавливается равным точной сумме управляемых дочерних ограничений pid_*, поэтому неуправляемые процессы, использующие этот UID (например, другие демоны AID_SYSTEM или пакеты sharedUserId="android.uid.system"), имеют memory.min = 0 и не могут претендовать на защиту.
  3. Фрагмент верхнего уровня (/sys/fs/cgroup/system/memory.min и /sys/fs/cgroup/apps/memory.min). Значение равно сумме значений uid_* дочерних фрагментов. Все устаревшие значения memory.low в управляемых узлах сбрасываются до 0, а все каталоги uid_*, которые управлялись при предыдущем обновлении (например, предыдущий главный экран после того, как пользователь переключился на другой), сбрасываются до 0.

Трехуровневая иерархия защиты memory.min в cgroup v2 для /sys/fs/cgroup/system и /sys/fs/cgroup/apps. Нижние уровни дочерних процессов суммируются в узлы UID и slice, а незащищенные дочерние процессы остаются на нулевом уровне.

Рисунок 1. Трехуровневое делегирование минимальной защиты между срезами cgroup.

Почему в ядрах MGLRU используется memory.min, а не memory.low

При использовании Multi-Generational LRU (MGLRU, стандартного механизма возврата страниц в ядрах Android 6.6 и более поздних версий) memory.low и memory.min ведут себя в mm/vmscan.c:shrink_one по-разному:

  • memory.low (отсрочка на один проход). Контрольная группа, использование которой ниже memory.low, перемещается в конец списка LRU (MEMCG_LRU_TAIL) и пропускается при одном проходе освобождения памяти. При следующем проходе в условиях постоянной нехватки памяти ядро полностью освобождает cgroup и регистрирует событие MEMCG_LOW. В стресс-тестах физических устройств с анонимным потребителем памяти, занимающим до 85% физической ОЗУ, memory.low позволило SystemUI на умных часах сократиться с ~45 МБ до ~5–10 МБ, а audioserver на складном устройстве с 16 ГБ памяти – с 15 МБ до 2 МБ при минимальном значении ~14 МБ.
  • memory.min (жесткое ограничение снизу). Контрольная группа, использование которой ниже memory.min, пропускается при глобальном возврате. Как показано на рисунке 2, при той же рабочей нагрузке с 85% давления на ОЗУ memory.min сбрасывает только 10% самых холодных страниц каждого демона и удерживает SystemUI, surfaceflinger и audioserver на уровне 90% без анонимных повторных ошибок и без увеличения количества завершений процессов lmkd.

memory.min обеспечивает физическое резервирование, аналогичное защите, которую дает закрепление PinnerService mlock. Преимущество заключается в том, что LRU-алгоритм ядра (а не статический список файлов) выбирает, какие страницы горячего кода, кучи и графики остаются резидентными.

Сравнение резидентной памяти демона при увеличении нагрузки на системную память с 40% до 95% от физической ОЗУ. memory.min удерживается на уровне 90 %, а memory.low падает после одного прохода отложенной обработки MGLRU.

Рисунок 2. Сохранение рабочего набора демона при растущей нагрузке на память.

Предварительный прогрев при загрузке и освобождение кеша несопоставленных файлов после загрузки

Чтобы заменить постоянный mlock PinnerService в config_defaultPinnerServiceFiles, нужно скоординировать два действия при загрузке:

  • Однократный фоновый предварительный запуск при загрузке (PinnerService): пропуск. PinnerService mlock без предварительного запуска кеша страниц переносит чтение с диска файлов SystemUI APK, ODEX и VDEX размером около 95 МБ в синхронный путь загрузки при запуске SystemUI (OnBootPhase_550), что увеличивает время загрузки на 300–450 мс . Вызова madvise(MADV_WILLNEED) недостаточно, потому что force_page_cache_ra в ядре ограничивает один вызов MADV_WILLNEED одним окном упреждающего чтения (обычно от 128 КБ до 2 МБ). Вместо этого, когда SystemMemoryBudgetManager.shouldSkipDefaultSystemPinning активно, PinnerService сопоставляет каждый файл в config_defaultPinnerServiceFiles в фоновом потоке во время onStart, синхронно заполняет файл в кеше страницы с помощью Os.mlock и немедленно вызывает Os.munlock, munmap и close. Это позволяет сохранить быструю загрузку и не блокировать байты навсегда, чтобы страницы, загруженные при холодном запуске, можно было удалить позже.
  • Исключение и возврат кеша сканирования пакетов при загрузке system_server. Во время загрузки PackageManagerService сканирует установленные APK-файлы, оставляя от 1,5 до 2,5 ГБ холодного несопоставленного кеша страниц файлов, который относится к cgroup system_server. Если включить его в расчет memory.min для system_server, то кеш холодной загрузки заполнит 10% глобального ограничения ОЗУ и пропорционально уменьшит минимальные уровни защиты для surfaceflinger, SystemUI и audioserver. Чтобы избежать нехватки памяти, SystemMemoryBudgetManager считывает memory.stat для system_server, вычисляет кеш несопоставленных файлов как max(0, file - shmem - file_mapped) и вычитает этот кеш из memory.current перед вычислением 90% нижнего предела, как показано на рисунке 3. Кроме того, через 10 секунд после PHASE_BOOT_COMPLETED менеджер выполняет однократную запись <unmapped_file> swappiness=0 в узел memory.reclaim system_server на Linux 6.2 и более новых ядрах, чтобы упреждающе вытеснить холодный кеш сканирования пакетов без замены анонимной кучи или вытеснения сопоставленных страниц DEX и кода. В более старых ядрах (например, android14-6.1), которые отклоняют swappiness=0 с ошибкой EINVAL, менеджер пропускает запись memory.reclaim, а не возвращается к неквалифицированной записи возврата, которая заменяет анонимную кучу во время загрузки.

Хронология памяти system_server после загрузки, показывающая геометрические обновления урегулирования через 1, 4, 16 и 64 секунды, исключение несопоставленного кеша сканирования загрузочного пакета из минимального уровня памяти 90% и однократное восстановление памяти с swappiness=0

Рисунок 3. Сведение нижнего порога защиты и возврат кеша после загрузки.

Требования к ядру и оверлеи конфигурации устройства

SystemMemoryBudgetManager использует mem_cgroup_calculate_protection в ядре Linux для распространения memory.min по вложенным cgroups. Общие ядра Android android15-6.6 и android16-6.12 по умолчанию поддерживают полную вложенную защиту MGLRU. Для устройств с android14-6.1 и включенным MGLRU требуются три бэкпорта в mm/vmscan.c и mm/memcontrol.c, чтобы пустые промежуточные срезы и UID cgroups распространяли защиту на дочерние процессы cgroups:

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

OEM-производители могут настраивать или отключать SystemMemoryBudgetManager с помощью следующих оверлеев ресурсов фреймворка:

  • config_systemMemoryBudgetLowKb (целое число, КБ): 0 (по умолчанию) ограничивает сумму всех нижних порогов memory.min на уровне 10% от общего объема физической ОЗУ. Положительное значение задает явное ограничение в КБ, а отрицательное – отключает SystemMemoryBudgetManager на устройстве (сохраняя устаревшее статическое закрепление mlock в PinnerService).
  • config_systemMemoryBudgetHighKb (целое число, КБ): 0 (по умолчанию) оставляет /sys/fs/cgroup/system/memory.high на уровне max. Положительное значение задает верхний предел для верхней части memory.high (должно быть больше или равно нижнему пределу).
  • config_systemMemoryBudgetAdditionalSystemProcesses (массив строк) – список дополнительных названий процессов демонов поставщика или системных процессов, критичных к задержкам, в /sys/fs/cgroup/system (сопоставляется с /proc/<pid>/comm или базовым именем argv[0] в /proc/<pid>/cmdline для системных UID и UID поставщика в 10000), которые нужно защитить с помощью 90% минимальных значений memory.min наряду с шестью системными демонами по умолчанию. Используйте этот оверлей вместо настройки memory.min в init.rc, потому что SystemMemoryBudgetManager пересчитывает uid_<uid>/memory.min как сумму управляемых дочерних процессов и очищает неуправляемые узлы uid_* при каждом обновлении. Например, XR-устройство может защитить свои демоны пространственного отслеживания и HAL камеры с помощью следующего оверлея:

    <string-array translatable="false" name="config_systemMemoryBudgetAdditionalSystemProcesses">
        <item>android.hardware.perceptionhost</item>
        <item>auracamerahal</item>
    </string-array>
    
  • config_systemUi (строка) – название пакета приложения SystemUI платформы, разрешенное с помощью MATCH_SYSTEM_ONLY.

Чтобы проверить активные нижние границы SystemMemoryBudgetManager и узлы cgroup на устройстве, выполните следующую команду:

# 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

Ограничение памяти для постоянно работающих и фоновых системных приложений

Хотя SystemMemoryBudgetManager защищает интерактивные демоны от возврата с помощью порогов memory.min, многие другие системные приложения и привилегированные сервисы платформы долго работают в фоновом режиме как процессы PERSISTENT, связанные активные службы или часто запрашиваемые поставщики контента. Без явных ограничений по памяти эти процессы не ограничиваются, пока не достигнут лимитов ограничителя памяти в несколько гигабайт (например, от 4 до 10 ГБ для видимых приложений и от 2 до 5 ГБ для фоновых приложений) или пока глобальная нагрузка на память не вызовет активность kswapd и lmkd на уровне системы.

Если не ограничивать фоновые системные процессы, они часто накапливают большие объемы холодной памяти, которая не входит в их активный рабочий набор.

  • Неограниченный кеш страниц файлов. Сервисы, которые индексируют медиафайлы, выполняют поиск по базам данных или синхронизируют таблицы календаря и поставщика (например, AppSearch, MediaProvider и CalendarProvider), сопоставляют или передают большие файлы на диске. Без ограничения cgroup memory.high чистые страницы, связанные с файлами, из однократных чтений остаются в cgroup процесса неопределенное время, занимая сотни мегабайт физической ОЗУ.
  • Сохранение анонимных страниц кучи после всплеска активности. Сервисы, в которых наблюдаются кратковременные всплески активности, например Bluetooth аудио и обнаружение BLE, NetworkStack проверка TLS на странице входа или переход помощников интерфейса в фоновый режим, могут сохранять холодные анонимные страницы кучи долгое время после всплеска активности.

Применение откалиброванных объявлений <memory-budget> (мягкие ограничения memory.high, обычно от 30 до 190 МБ) к системным приложениям дает два основных преимущества:

  • Упреждающее локализованное освобождение страниц. Когда системный процесс превышает заданный лимит memory.high, ядро Linux направляет освобождение страниц именно в эту группу cgroup, сначала вытесняя холодные страницы, поддерживаемые файлами, и сжимая неактивные анонимные страницы в ZRAM, не дожидаясь глобальной нехватки памяти устройства.
  • Больше доступной оперативной памяти для активных приложений и пула кешированных приложений. Ограничение кеша холодных файлов в постоянно работающих системных службах освобождает от десятков до сотен мегабайт физической оперативной памяти на устройстве. Это уменьшает задержки в интерфейсе, снижает нагрузку при запуске камеры и позволяет сохранять больше пользовательских приложений в кешированном состоянии.

Настройка лимитов памяти для системных приложений

Для системных приложений используется тот же синтаксис манифеста <memory-budget>, каскадирование состояний процесса, масштабирование плотности экрана и правило разрешения последнего совпадающего условия, что и для обычных приложений. Полный список атрибутов приведен в справочнике по атрибутам XML.

OEM-производители могут объявлять элементы <memory-budget> непосредственно в файле AndroidManifest.xml системного приложения или объединять их из отдельного XML-файла с помощью свойства additional_manifests в Android.bp. По соглашению AOSP хранит бюджеты системных приложений в отдельных файлах *-budget.xml (в frameworks/base/core/memory_budgets/ для пакетов основной платформы или вместе с отдельными модулями, такими как packages/modules/NetworkStack/memory_budgets/, packages/modules/AppSearch/apk/ и packages/services/Telephony/) и прикрепляет их с помощью additional_manifests:

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

Область действия на уровне пакета (UID) и на уровне процесса (PID)

В зависимости от того, какой бюджет вы объявляете, базовый бюджет на уровне пакета (<application>) или бюджет для нескольких процессов (<processes><process>), ядро применяет memory.high на уровне cgroup UID (/sys/fs/cgroup/uid_<uid>/memory.high) или на уровне cgroup PID (/sys/fs/cgroup/uid_<uid>/pid_<pid>/memory.high), как показано на рисунке 4.

  • Лимит на уровне пакета (<application>). Используйте, если APK запускает один процесс под собственным выделенным UID (например, Bluetooth, Nfc, NetworkStack, CalendarProvider, Traceur или AppSearch).
  • Бюджет на уровне процесса (<processes><process>). Используйте, если APK-файл использует android:sharedUserId (например, android.uid.phone с UID 1001 или android.uid.system с UID 1000) совместно с другими системными пакетами, чтобы другие процессы, использующие этот UID, не ограничивались вместе, или когда один APK-файл запускает несколько процессов с разными ролями памяти (например, фоновый демон MediaProvider и подпроцесс пользовательского интерфейса :PhotoPicker).

Иерархия Cgroup v2, в которой сравнивается применение memory.high на уровне UID для выделенных UID и применение memory.high на уровне PID для общих системных UID и многопроцессных APK

Рисунок 4. Ограничения памяти на уровне UID и PID.

В примере ниже показан параметр MediaProvider-budget.xml, который ограничивает бюджеты, зависящие от ОЗУ, процессом MediaProvider по умолчанию (<process>), а бюджет, зависящий от масштаба экрана, – процессом пользовательского интерфейса :PhotoPicker. Каждый атрибут android:maxDeviceRamMb соответствует устройствам, у которых общий объем физической ОЗУ (MemTotal) не превышает указанное пороговое значение в МБ. Если атрибут не задан, он применяется к устройствам с любым объемом ОЗУ. Условия упорядочены от значения по умолчанию без ограничений до наименьшего порогового значения maxDeviceRamMb, чтобы правило разрешения последнего совпадающего условия выбирало наиболее строгий применимый уровень:

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

Как калибровались эталонные бюджеты AOSP на устройствах Google Pixel

Лимиты памяти для системных приложений в AOSP были откалиброваны на устройствах Google Pixel с объемом физической ОЗУ от 6 до 16 ГБ (включая телефоны, складные устройства и планшеты с 6, 8, 12 и 16 ГБ ОЗУ) и разрешением экрана от ~1, 2 Мп (720 × 1600) до ~4, 5 Мп (2076 × 2152).

Калибровка объединила два дополнительных источника данных: телеметрию полей cgroup v2 для всего парка и контролируемые проверки физических устройств memory.high по критическим пользовательским путям (CUJ).

Анализ телеметрии cgroup v2 для всего парка устройств

Периодические снимки cgroup v2 собирались на всех устройствах и разбивались по объему физической ОЗУ (≤8 ГБ, 12 ГБ и ≥16 GB) и сегменту состояния процесса (foreground, perceptible, background и cached). Статистический анализ проводился на основе шести принципов:

  • Разделение memory.current на рабочий набор и освобождаемый кеш файлов. Поскольку memory.high управляет резидентной нагрузкой cgroup (memory.current), каждый снимок был разделен с помощью memory.stat на анонимную кучу (anon), анонимные страницы подкачки (swap) и кеш страниц на основе файлов (file), как показано на рисунке 5. Общий несжимаемый анонимный спрос (anon + swap при P99) устанавливает минимальный рабочий набор для бюджета, чтобы процесс не рисковал исчерпанием ZRAM, в то время как большой разрыв между memory.current P99 и anon + swap P99 указывает на кэш страниц файлов, который memory.high может восстановить, при условии, что кэш не сопоставлен и не используется.
  • Учитывайте память, которую memory.high не может освободить. Не все файлы и общая память – это холодный кеш. Заблокированные страницы (unevictable в memory.stat, например общая память, заблокированная с помощью mlock, или заблокированная гостевая ОЗУ виртуальной машины, хост-процессы которой относятся к cgroup приложения) устанавливают жесткий нижний предел бюджета, а сопоставленные страницы (file_mapped) часто являются частью активного рабочего набора. Прежде чем считать память, занятую файлами, доступной для повторного использования, проверьте unevictable, shmem и file_mapped. Например, около 100 МБ памяти AppSearch заблокировано и не может быть освобождено, поэтому бюджет покрывает этот минимум плюс рабочий набор горячего хоста с запасом для роста заблокированной области.
  • Отфильтруйте данные, полученные в режиме ожидания. Поставщики контента (CalendarProvider) и инструменты разработчика, работающие по запросу (Traceur), от 70 до 99,9 % времени находятся в режиме ожидания, потребляя от 1 до 3 МБ памяти. Вычисление необработанных процентилей по всем снимкам сильно размывает оценки P95 и P99. Вместо этого процентили и доверительные интервалы рассчитывались по неразбавленным активным снимкам (с фильтрацией снимков CACHED и снимков базового уровня бездействия).
  • Используйте доверительные интервалы, сгруппированные по устройствам. Поскольку одно устройство со временем предоставляет множество периодических снимков, обработка снимков как независимых выборок занижает неопределенность хвоста. Все оценки рабочих наборов P95 и P99 были рассчитаны с использованием консервативных 95 % доверительных интервалов, сгруппированных по устройствам (в качестве эффективного размера выборки использовалось количество отдельных устройств N_dev).
  • Учитывайте кратковременные всплески с помощью memory.peak. Периодическая выборка может не учитывать кратковременные всплески с высоким уровнем выделения памяти, например NetworkStack проверку TLS на странице входа и одновременные DNS-пробы или Bluetooth инициализацию A2DP и BLE. Для сервисов с пиковыми нагрузками бюджеты были рассчитаны так, чтобы покрывать от 90 до 95 % максимального объема памяти memory.peak, используемой ядром за все время работы, на разных уровнях ОЗУ.
  • Убедитесь, что уровень ОЗУ отражает объем ОЗУ, а не поколение устройства. Новые устройства обычно имеют больше ОЗУ и больше функций, поэтому рабочий набор, который растет вместе с уровнем ОЗУ, на самом деле может расти вместе с функциями. Прежде чем добавлять уровень maxDeviceRamMb, сегментируйте данные по моделям устройств. Например, com.android.phone достигает того же P99 anon + swap (примерно 85–90 МБ) на телефонах с 12 и 16 ГБ ОЗУ одного поколения, а на более старом складном телефоне с 12 ГБ ОЗУ использует 40 МБ. Поэтому в бюджете используется одно значение без уровня ОЗУ, чтобы устройство с меньшим объемом ОЗУ, которое поставляется с новыми функциями телефонии, не было недофинансировано.

Разбивка памяти cgroup по времени на несжимаемый анонимный рабочий набор, активное окно кеша файлов и холодный одноразовый кеш файлов, освобождаемый за пределами memory.high

Рисунок 5. Рабочий набор и кеш файлов, доступных для освобождения.

Проверка критических путей пользователя и масштабирования экрана на физических устройствах

Чтобы проверить бюджеты, полученные на основе телеметрии, и откалибровать бюджеты, пропорциональные дисплею (например, :PhotoPicker), и бюджеты для фонового режима в зависимости от состояния (например, Traceur), на физических устройствах с ОЗУ от 6 до 16 ГБ были выполнены проверки memory.high.

  • Поиск точки, в которой повторные ошибки отсутствуют. При выполнении сквозных пользовательских сценариев (таких как быстрая прокрутка сетки фотографий, просмотр альбома, пролистывание полноэкранных предварительных версий фотографий и видео в высоком разрешении, а также запись и сохранение трассировки) значение memory.high уменьшалось, а счетчики cgroup memory.stat (workingset_refault_anon, workingset_refault_file, pgscan и pgsteal) и memory.events (high) измерялись. Как показано на рисунке 6, оптимальный бюджет находится чуть выше точки, в которой повторные ошибки отсутствуют. Точка перегиба – это пороговое значение, при котором ядро освобождает холодный одноразовый файловый кеш (pgsteal > 0), не вызывая пробуксовки активного рабочего набора (workingset_refault == 0).
  • Проверка масштабирования пикселей экрана при разных разрешениях. Тестирование :PhotoPicker на четырех физических и эмулированных разрешениях экрана от ~1,2 Мпкс (720 × 1600) до ~4,5 Мпкс (2076 × 2152, развернутый внутренний экран) подтвердило, что активный рабочий набор битовых карт Glide и ViewPager2 масштабируется линейно с количеством пикселей экрана. Базовый объем памяти, не связанный с экраном, также зависит от устройства. При почти одинаковом количестве пикселей точка перегиба без перезагрузок составила около 140 МБ на внешнем экране складного устройства с 16 ГБ памяти и 110–125 МБ на телефоне с 6 ГБ памяти. Базовый объем 85 МБ плюс 25 байт на пиксель экрана (android:maxMb="85" и android:additionalBytesPerDisplayPixel="25") превышает измеренное пороговое значение на каждом протестированном экране (около 110 МБ на HD+, около 145 МБ на FHD+ и около 190 МБ на раскладных экранах).
  • Проверка сборки мусора ART в фоновом режиме. В cgroup v2 общие страницы boot.art Zygote и страницы изображений фреймворка остаются в cgroup Zygote и обрабатываются как иммунное пространство во время параллельной сборки мусора ART с пометкой и сжатием (CMC). Стресс-тестирование подтвердило, что когда приложение системного интерфейса переходит в состояние background, ограничение memory.high (например, до 30 МБ на Traceur) четко ограничивает частную кучу и кэш страниц, не вызывая сбоев сборки мусора или активности подкачки.

График освобождения холодного кеша (pgsteal) и пробуксовки рабочего набора (workingset_refault) при снижении memory.high во время CUJ, на котором выделены колено нулевого refault и целевой бюджет

Рисунок 6. Поиск точки перегиба с нулевым количеством повторных сбоев во время CUJ.

Сводка по бюджетам системных приложений в эталонной версии AOSP

В таблице ниже приведены калиброванные контрольные бюджеты в AOSP для разных уровней ОЗУ и состояний процессов.

Системный пакет или процесс Область действия ≤8 ГБ ОЗУ 12 ГБ ОЗУ ≥16 ГБ ОЗУ Основной драйвер калибровки
com.android.bluetooth UID (<application>) 80 МБ 95 МБ 110 МБ Охватывает 90 % времени работы устройства, memory.peak пакетов (~75–110 МБ) и более чем в два раза больше активных memory.current, чем 99 % времени работы устройства.
com.android.nfc UID (<application>) 50 МБ 60 МБ 60 МБ Охватывает активную технологию Host Card Emulation P99 (до ~50 МБ) и P90–P95 memory.peak при очистке холодного кеша файлов.
com.android.providers.calendar UID (<application>) 70 МБ по умолчанию (активный режим)
50 МБ perceptible и background
Активный рабочий набор не зависит от объема ОЗУ. Бюджет фонового режима сохраняет анонимный запас для напоминаний, сокращая кеш страниц SQLite во время синхронизации.
com.android.traceur UID (<application>) 85 МБ по умолчанию (активные и заметные)
30 МБbackground
Сохраняет полный запас в 85 МБ во время активной записи и экспорта трассировки (TraceService), ограничивая состояние бездействия и трансляции до 30 МБ.
com.android.providers.media.module (основной) PID (<process>) 60 МБ 75 МБ 85 МБ Освобождает около 10–30 МБ сверх 99-го процентиля anon + swap, очищая кеш неиспользуемых файлов SQLite и значков.
com.android.providers.media.module:PhotoPicker PID (<process>) 85 МБ + 25 байт на пиксель
(~110 МБ на HD+, ~145 МБ на FHD+, ~190 МБ на складном экране с разрешением ~4,5 Мп)
Масштабируется пропорционально количеству пикселей экрана (additionalBytesPerDisplayPixel="25") в соответствии с точкой перегиба рабочего набора битовых карт Glide.
com.android.networkstack UID (<application>) 65 МБ 70 МБ 85 МБ Преимущественно анонимная память, размер которой соответствует 95-му процентилю времени жизни memory.peak, чтобы покрывать всплески трафика TLS, DNS и DHCP на странице входа.
com.android.appsearch.apk UID (<application>) 130 МБ Охватывает около 100 МБ заблокированной памяти, которую нельзя освободить, и около 5 МБ рабочего набора хоста, а также предусматривает возможность увеличения заблокированной области. При освобождении кеша холодных страниц (до ~330 МБ при P99) не было зафиксировано повторных ошибок при ≥125 МБ.
com.android.phone (TeleService) PID (<process>) 120 МБ Область действия ограничена PID на общем UID 1001. Рабочий набор отслеживает поколение устройства, а не ОЗУ, поэтому один бюджет примерно на 30 МБ выше самого высокого значения P99 anon + swap для каждой модели.

Как производители оригинального оборудования могут перенастроить лимиты для системных приложений на своих устройствах

Производители оригинального оборудования, которые настраивают системные приложения (например, добавляют профили Bluetooth поставщика, пользовательские расширения телефонии или индексаторы мультимедиа) или поставляют дополнительные предустановленные системные сервисы, могут перенастроить существующие бюджеты или создать новые файлы *-budget.xml для своего аппаратного портфолио.

Сбор активных профилей памяти cgroup v2

Для каждого целевого системного процесса прочитайте узлы памяти cgroup v2 в /sys/fs/cgroup/uid_<uid>/pid_<pid>/ для репрезентативных рабочих нагрузок и уровней ОЗУ:

  • memory.current – текущая резидентная память, выделенная для cgroup (за исключением страниц, общих с Zygote).
  • memory.peak: максимальное значение memory.current за все время с момента запуска процесса
  • memory.swap.current – текущий объем сжатой памяти подкачки в ZRAM.
  • memory.stat: разбивка по anon, file, pgscan, pgsteal, workingset_refault_anon и workingset_refault_file
  • memory.events – общее количество нарушений high (memory.high событий ограничения и возврата).

При агрегировании телеметрии с устройств, используемых для тестирования или в полевых условиях, отделяйте состояния активных процессов (TOP, FOREGROUND_SERVICE, активные привязки и активные синхронизации) от состояний бездействия CACHED или PERSISTENT. Это позволит избежать искажения расчетов рабочих наборов P95 и P99 в сторону уменьшения.

Отделение несжимаемой кучи от восстанавливаемого кеша файлов

Сравните 99-й процентиль anon + swap с 99-м процентилем memory.current:

  • Сервисы, в которых преобладает кеш файлов (file &gg; anon, например поставщики баз данных, индексаторы поиска и демоны журналов). Установите android:maxMb, чтобы обеспечить комфортный запас (обычно от 1,5 до 4 раз) по сравнению с P99 anon + swap, а также рабочее окно для страниц горячих файлов. Эта конфигурация позволяет ядру непрерывно вытеснять холодные несопоставленные страницы файлов, не затрагивая анонимную кучу. Если unevictable или file_mapped имеет большой размер (как в случае с заблокированной памятью или базами данных, сопоставленными с памятью), учитывайте этот объем как рабочий набор.
  • Сервисы, в которых преобладает анонимная куча или наблюдаются всплески (anon составляет большую часть memory.current, например стеки сети или подключения). Размер android:maxMb должен покрывать от 90 до 95 % memory.peak, чтобы временные всплески не запускали синхронную выгрузку ZRAM в потоках, чувствительных к задержке.
  • Процессы пользовательского интерфейса, интенсивно использующие экран (например, средства выбора, галереи или системные помощники пользовательского интерфейса). Измерьте базовый объем памяти, не связанный с пользовательским интерфейсом, и дополнительный объем памяти, занимаемый растровыми изображениями, на мегапиксель на экранах с самым низким и самым высоким разрешением и выразите бюджет с помощью android:maxMb плюс android:additionalBytesPerDisplayPixel.

Очистка memory.high на физических устройствах

Прежде чем выделять новый бюджет на сборку, используйте am memory-limiter manual (или запишите непосредственно в узел cgroup memory.high процесса в сборке userdebug), чтобы протестировать возможные ограничения при выполнении автоматических тестов CUJ:

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

Отслеживайте memory.stat и memory.events до и после CUJ:

  • Бюджет слишком мал. workingset_refault_anon или workingset_refault_file резко возрастает во время CUJ, memory.swap.current непрерывно растет во время активной работы в активном режиме или задержка кадра CUJ или Binder регрессирует. Увеличьте бюджет, чтобы он превышал значение по умолчанию.
  • Бюджет слишком велик. Значение memory.events:high остается равным 0 даже после интенсивной потоковой передачи файлов или индексирования, а значение file в memory.stat неограниченно растет за счет неиспользуемых страниц. Ограничьте бюджет ещё больше.
  • Бюджет хорошо откалиброван. В нормальном режиме работы memory.current не превышает memory.high. При резком увеличении объема файлов или кеша pgsteal восстанавливает страницы неиспользуемых файлов, в то время как workingset_refault_anon остается близким к нулю, а задержка CUJ не меняется.