Ключевые выводы:
  • С миллиардами пользователей Android-приложений мы постоянно ищем способы улучшить опыт использования приложений Meta*. В этой статье расскажу о том, как мы использовали Baseline Profiles в Android, чтобы повысить производительность приложений.

  • Обсудим проблемы с производительностью, с которыми сталкивались приложения Meta*, как со временем усложнялись потребности пользователей, и какую инфраструктуру мы создали, чтобы эти проблемы решать.

  • Расскажу, как формируем Baseline Profiles на основе пользовательских данных и какие настройки применяем, чтобы сделать их ещё эффективнее. В сумме Baseline Profiles улучшили по ряду ключевых метрик производительность приложений Meta* вплоть до 40%.

  • Производительность приложения критически важна для хорошего пользовательского опыта. Медленный запуск, пропуски кадров и слабая отзывчивость напрямую ведут к раздражению пользователей и, в конечном итоге, к их оттоку.

Осознанное отношение к производительности в процессе разработки, а также правильный выбор структур данных, алгоритмов, стратегий кэширования и так далее — фундаментальные способы снизить влияние этих проблем. Однако не менее важно понимать, как устроено представление скомпилированного кода приложения и как он загружается и выполняется. Это позволяет оптимально настраивать и «тюнить» инструменты сборки и рантайм.

За последние несколько лет мы построили инфраструктуру оптимизаций компилятора и рантайма на основе профилей для Android-приложений. Один из ключевых компонентов этой инфраструктуры — функция Baseline Profiles в Android Runtime, которую мы применяли, чтобы улучшить производительность.

В этой статье мы разберём несколько аспектов производительности, связанных с Android Runtime (ART). Я расскажу, с какими проблемами мы столкнулись и как Baseline Profiles помогли их преодолеть.


Соображения о производительности ART

На Android предпочтительные, а значит и доминирующие языки разработки пользовательских приложений — Kotlin и Java. Код на Kotlin/Java компилируется в байткод Dalvik («dex-код») и упаковывается в файлы «.dex», которые организованы в классы и методы, отражающие исходный код.

Прежде чем Android Runtime сможет выполнить любой dex-код, относящийся к методу, рантайм должен загрузить содержащий его класс. Это происходит при первом обращении к классу во время выполнения приложения и включает поиск метаданных класса, регистрацию его в ART, инициализацию статических данных и всё остальное, что нужно для работы с этим классом.

Когда класс загружен, его методы могут выполняться. Dex-код, разумеется, не является машинным кодом, который можно напрямую исполнять на «железе», поэтому Android Runtime должен преобразовать его. По умолчанию во время выполнения методы из dex-кода одновременно исполняются в режиме интерпретации и профилируются, чтобы определить, являются ли они «горячими». Как только метод признаётся «горячим», ART компилирует его в машинный код с помощью JIT-компилятора, и дальше исполняется уже скомпилированная версия. (Исполнение машинного кода обычно заметно быстрее интерпретации)

И загрузка классов, и этап интерпретации/профилирования при выполнении dex-методов имеют свою цену во время работы, из-за чего часто возникает временная, но заметная для пользователя деградация производительности. Кроме того, после каждого «холодного старта» приложения классы нужно загружать заново. «Холодный старт» происходит, когда система запускает приложение в первый раз. После холодного старта приложение остаётся в памяти, и последующие запуски становятся намного быстрее. (На Android 14 и выше это частично смягчается за счёт «runtime app images»)

У ART есть механизм, позволяющий сохранять скомпилированные методы между холодными стартами (здесь мы упрощаем: строго говоря, они не «сохраняются» напрямую, и между холодными стартами требуется фоновый запуск dexopt). Но после обновления версии приложения методы нужно снова профилировать и компилировать заново.

Проблемы мобильных приложений Meta*

Мобильные приложения Meta* — основной способ доступа к нашим сервисам для большинства пользователей, и значительная часть из них использует Android. Перед нами стоит задача удерживать баланс между скоростью выпуска обновлений и целевыми показателями производительности. Особенно критична производительность запуска, поскольку именно она оказывает непропорционально сильное влияние на пользовательский опыт.

Поддержание минимального набора классов, загружаемых при старте, — ключевой приоритет для производительности запуска. По мере добавления новых функций, таких как Instagram*** Reels или сквозное шифрование в Messenger, набор классов, задействованных на старте, тоже растёт. Помимо пользовательских возможностей, в процессе запуска участвует и критически важная инфраструктурная функциональность: сбор отчётов о сбоях, аутентификация, логирование производительности. Например, Facebook** и Instagram*** при запуске загружают более 20 000 классов каждый, а для прокрутки ленты дополнительно требуется ещё несколько тысяч.

Нам также важно улучшать производительность пользовательских сценариев после запуска. Под такими сценариями мы понимаем ключевые взаимодействия, влияющие на пользовательский опыт, например прокрутку ленты или время, необходимое для загрузки и отображения фотографии. При этом сценарий обычно учитывает не только действие пользователя — прокрутку или навигацию, — но и контекст, в котором оно происходит. Так, прокрутка ленты рассматривается отдельно от прокрутки списка сообщений, а переход к профилю — отдельно от перехода к ленте. Для каждого приложения мы определяем приоритетные сценарии оптимизации и регулярно пересматриваем их список — добавляем новые или исключаем устаревшие.

Оптимизация пользовательских сценариев требует точного понимания того, какие классы загружаются. Для этого мы собираем профили последовательностей загрузки классов от множества пользователей и анализируем, что в них общего. Мы обнаружили, что такие профили могут существенно различаться у разных пользователей даже в рамках одного и того же сценария. Более того, у одного и того же пользователя профиль загрузки классов может отличаться в разные дни: из-за экспериментов активируются разные ветки кода, а через неделю профиль снова меняется — вместе с кодом и поведением пользователя. В нашем монорепозитории ежедневно происходит тысячи коммитов. Универсального решения «на все случаи» здесь не существует.

В итоге мы управляем большим, постоянно растущим и динамичным объёмом кода. Нам требуется решение, которое сможет адаптироваться к частым изменениям между релизами, быстро формировать скомпилированный код и профили, а также приносить пользу как при запуске приложения, так и в других пользовательских сценариях.

Оптимизации ART на этапе установки

Начиная с Android 9 ART предлагает следующие оптимизации на этапе установки:

  • AOT-компиляция (Ahead of Time) для выбранных методов

  • Создание «образа приложения» (app image) для выбранных классов

AOT-компиляция означает, что ART компилирует указанные методы в машинный код ещё до первого запуска приложения. Это устраняет накладные расходы, связанные с интерпретацией и профилированием при первом выполнении метода.

App image — это файл, содержащий частичное представление структур данных ART в памяти, которые обычно создаются или заполняются при загрузке указанных классов. Когда приложение запускается с app image, этот файл отображается (map) в кучу процесса, и при необходимости применяются исправления. В результате многие классы могут фактически загружаться на старте чрезвычайно быстро, а любые последующие затраты во время выполнения, связанные с загрузкой этих классов, устраняются.

Эти оптимизации можно включить, передав ART специальный профиль на этапе установки приложения. Для этого есть два основных механизма: Cloud Profiles и Baseline Profiles.

Cloud Profiles

Cloud Profiles — это сводные данные профилирования от множества разных пользователей, которые Google Play собирает на начальном этапе выката новой версии приложения. После того как Cloud Profile сформирован, все последующие пользователи, устанавливающие эту версию через Google Play, получат этот Cloud Profile. ART использует его для AOT-компиляции и создания app image.

Однако у Cloud Profiles есть ряд недостатков:

  • Первые пользователи в ходе выката вовсе не получают выгоды от Cloud Profiles, потому что именно они предоставляют данные профилирования.

  • У разработчиков приложения нет возможности наблюдать или контролировать, какие классы и методы попадают в профиль.

  • Профили формируются таким образом, что они сильно смещены в сторону улучшения самых ранних этапов запуска приложения.

  • Они доступны только через Google Play: приложения, установленные другими способами, например из альтернативных магазинов или через сайдлоад, не могут их использовать.

Baseline Profiles

Baseline Profiles (базовые профили) похожи на Cloud Profiles тем, что тоже запускают install-time оптимизации ART, но есть несколько ключевых отличий. Cloud Profiles генерируются Google Play за счёт сбора и агрегации данных от ранних пользователей версии приложения, тогда как Baseline Profiles создаются и поставляются самими разработчиками. Разработчики могут просто упаковать Baseline Profile внутрь соответствующего APK или AAB. Когда доступны и Cloud Profiles, и Baseline Profiles, их можно использовать совместно.

Диаграмма, показывающая поток Baseline Profiles и Cloud Profiles в Google Play. 
Диаграмма, показывающая поток Baseline Profiles и Cloud Profiles в Google Play. 

Baseline Profiles дают разработчикам приложений полный контроль над оптимизациями на этапе установки и становятся доступны пользователям сразу. Это позволяет настраивать install-time оптимизации гораздо точнее под потребности конкретного приложения, чем в случае Cloud Profiles, в том числе оптимизировать сценарии, выходящие за рамки запуска.

Google предлагает несколько механизмов генерации baseline profiles на основе бенчмарков (например, Macrobenchmark). Однако их также можно создавать, напрямую перечисляя классы и методы в строго заданном формате и передавая их в инструмент profgen, что даёт больше гибкости.

Далее мы рассмотрим, почему Baseline Profiles оказались очень полезной технологией для повышения производительности приложений Meta*, решая многие проблемы, связанные с ART.

Как мы создаём Baseline Profiles в Meta*

Ранее мы описали проблемы производительности, с которыми сталкивались наши Android-приложения. В частности, мы говорили о том, что при каждом холодном старте наши приложения могут загружать десятки тысяч классов, а еженедельный выпуск новых версий приводит к тому, что при каждом обновлении «сбрасывается» весь скомпилированный код.

Мы давно осознаём эти сложности и уделяем им большое внимание, особенно в контексте холодного старта. В прошлом мы получали заметный прирост производительности, упорядочивая классы внутри dex-файла в соответствии с тем, как обычно происходит их загрузка при старте. Это улучшало локальность обращений к данным. Мы называем эту оптимизацию «Interdex Ordering». Она выполняется через InterdexPass в Redex — нашем оптимизаторе байткода. (Аналог у Google в R8 называется «startup profiles».)

Оптимизации ART на этапе установки дополняют и усиливают этот подход: они полностью устраняют стоимость загрузки для части классов и гарантируют, что их «горячие» методы будут скомпилированы ещё до первого запуска новой версии приложения.

Ранее мы упоминали, что разработчики не контролируют содержимое Cloud Profile напрямую. Для Meta* это оказалось особенно чувствительным: как только запуск приложения превышает пять секунд, Android Runtime автоматически считает, что запуск завершён. Из-за этого Cloud Profile недостаточно точно отмечал, какие классы действительно необходимы для запуска. Хотя Cloud Profiles, без сомнения, помогли и здесь, именно контроль и гибкость Baseline Profiles позволили нам полностью раскрыть потенциал этих оптимизаций и зафиксировать существенные выигрыши в производительности.

Чтобы формировать наши Baseline Profiles, мы используем данные из разных источников. Мы обрабатываем и агрегируем их вместе на основе конфигураций, которые постоянно проходят через эксперименты и настройку.

Сбор данных для профилей

В первых экспериментах с Baseline Profiles мы просто использовали статические профили для библиотек AndroidX, которые поставляются вместе с ними. Сегодня у нас есть более продвинутый набор технологий сбора данных, которые мы используем совместно, чтобы формировать профили для наших приложений.

Один из способов собирать данные для профилей — бенчмарки. В Meta* мы используем некоторые локальные бенчмарки при создании Baseline Profile для части наших приложений, применяя внутренние инструменты, которые написали сами, чтобы собирать информацию об использовании классов и методов. Однако для таких приложений, как Facebook** и Instagram***, бенчмарки недостаточно хорошо отражают поведение в продакшене. Для более сложных приложений такого уровня мы дополнительно собираем данные об использовании классов и методов от пользователей, чтобы получить более полную картину.

Чтобы собирать данные об использовании классов у пользователей, мы применяем кастомный ClassLoader, в который добавляем код, логирующий, какие классы загружаются. Затем эти данные периодически отправляются на сервер. Поскольку такой сбор имеет стоимость по производительности, он включается только при определённых условиях и с очень низкой долей выборки. Собранные логи загрузки классов затем агрегируются, чтобы посчитать частоту появления, и классы, превышающие заданный порог частоты, включаются в Baseline Profile для следующего релиза.

Хука, который позволял бы столь же просто логировать использование методов, нет. Однако мы добавляем в приложения специализированную телеметрию, которая позволяет детально выявлять кластеры методов, обычно вызываемых пользователями. Дальше мы так же, как и для классов, делаем выборку и агрегируем эти данные.

Затем все эти данные объединяются в человекочитаемый профиль и передаются в profgen, который генерирует итоговый Baseline Profile. Ниже пример:

Если разложить по пунктам, видно следующее:

  • Символ «#» используется для строк-комментариев.

  • Классы можно задавать напрямую через их дескриптор.

  • Методы можно задавать напрямую, при необходимости добавляя флаги.

  • Можно использовать шаблоны (wildcards), чтобы сопоставлять все классы или методы, которые начинаются с заданного префикса.

Настройка и эксперименты

«Холодный старт» был первым сценарием, который мы хотели оптимизировать с помощью Baseline Profiles. Мы начали осторожно, с высоких порогов частоты включения классов и методов в профили: класс или метод должен был встречаться более чем в 80–90% всех собранных пользовательских трейсов. Мы опасались, что слишком большой Baseline Profile может, наоборот, ухудшить производительность. Скомпилированный машинный код обычно примерно в 10 раз больше, чем исходный интерпретируемый код. Из-за разницы в размере растут затраты на ввод-вывод: увеличивается число страничных ошибок (page faults) и промахов кэша (cache misses).

Со временем мы экспериментировали с разными порогами включения и вышли за рамки «холодного старта», распространив подход на другие пользовательские взаимодействия. Сейчас для большинства приложений мы включаем в профиль классы и методы, которые встречаются как минимум в 20% пользовательских трейсов холодного старта. Среди взаимодействий, которые мы оптимизировали с помощью Baseline Profiles, — прокрутка новостной ленты в Facebook** и Instagram***, переходы от списка диалогов к конкретному чату в Messenger и во «Входящих» Instagram*** Direct, а также общая задержка при переходах между разными разделами приложения.

Иногда мы наблюдали регрессии при запуске и в других местах, когда в экспериментах увеличивали размер baseline profile. Обычно это сопровождалось признаками роста давления на память. Однако благодаря точечным и тщательно измеряемым добавлениям нам удалось вырастить профили значительно больше, чем мы ожидали изначально. Сейчас Baseline Profiles для всех наших приложений содержат несколько десятков тысяч записей.

Влияние Baseline Profiles в Meta*

За последние несколько лет мы внедрили Baseline Profiles во всех наших ключевых Android-приложениях и стабильно наблюдали положительный эффект. По мере интеграции и дальнейшего улучшения Baseline Profiles мы фиксировали заметные улучшения времени запуска, производительности прокрутки, задержек при навигации между разделами приложения и ряда других критически важных метрик. Величина улучшений варьировалась от 3% до 40%.

Baseline Profiles стали для наших команд мощным рычагом, позволяющим из года в год ощутимо улучшать пользовательский опыт. Постоянные инвестиции и эксперименты с Baseline Profiles полностью себя оправдали. Мы рекомендуем всем Android-разработчикам — и тем, кто уже использует Baseline Profiles, и тем, кто только собирается начать, — взять наши наблюдения и применить их в своих проектах.

Meta Platforms*, а также принадлежащие ей социальные сети Facebook** и Instagram*** — признана экстремистской организацией, её деятельность в России запрещена** — запрещены в России

Потянете программу Android Pro? Входной тест поможет понять
Потянете программу Android Pro? Входной тест поможет понять

Если вам интересна инженерная сторона Android, стоит собрать её в систему. На курсе «Android Developer. Professional» разбирают ART и сборку, архитектуру, тесты и профилирование, чтобы понимать, что происходит внутри SDK, и уверенно ускорять приложения на проде. Готовы к обучению? Пройдите вступительный тест.

Чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные демо-уроки:

  • 25 февраля, 20:00. «Работаем со списками как профессионалы». Записаться

  • 12 марта, 20:00. «Профессиональные Unit-тесты в Android: как тесты улучшают код». Записаться

  • 19 марта, 20:00. «Современная архитектура приложения и внедрение зависимостей». Записаться