Привет, меня зовут Даниль Гатиатуллин, я инженер юнита Performance в Авито. Наша команда отвечает за производительность приложения Авито: мы следим за скоростью старта приложения и отрисовки экранов, качеством скролла, отслеживаем сетевые ошибки и занимаемся оптимизациями.
В этой статье я расскажу, что такое Baseline Profiles, как он ускоряет запуск программы и каким приложениям он принесет больше пользы. В качестве примера возьму наш эксперимент, который ускорил время запуска приложения на 15%. Также расскажу, как мы автоматизировали добавление профилей в каждый релиз.
Текст основан на моём выступлении на Avito Android meetup #2.
Какие проблемы компиляции стояли перед разработчиками раньше и при чём тут Baseline Profiles
Немного копнём в историю компиляции байткода в Android — это даст чёткое понимание актуальности Baseline Profiles.
В первой версии Android весь код приложения интерпретировался. Из-за этого были проблемы с производительностью — как во время работы, так и при холодном запуске, для которого нужно выполнить много кода. Во второй версии добавилась Just-In-Time (JIT)-компиляция, благодаря которой инструкции после интерпретации сохраняются, чтобы в следующий раз выполниться быстрее. Это помогло с быстродействием во время работы, но холодный старт всё ещё происходил медленно — весь код, необходимый для запуска, интерпретировался. Существенный сдвиг в росте производительности произошёл позже.
Скорость запуска приложения сильно выросла с выходом версии Android 5.0, когда весь код приложения перед первым запуском стал проходить ahead-of-time-компиляцию.
С внедрением AOT-компиляции появились две новые проблемы:
- компиляция кода в больших приложениях занимала минуты, блокируя запуск сразу после установки;
- приложения стали занимать слишком много места на диске.
Обе проблемы решили в Android 7.0 и Android 9.0:
- в Android 7.0 появилась Profile-guided optimization — техника оптимизации, которая записывала в специальные профили для компиляции участки кода, используемые на старте. Такие профили составлялись и хранились локально на устройстве;
- в Android 9.0 появились Cloud Profiles — система, которая загружает локальные профили с устройств пользователей в облако, превращает усреднённые данные по запускам в единый профиль, а после раздаёт этот профиль на все устройства.
Эволюция развития Android. Каждая показанная версия добавляла что-то новое для ускорения компиляции:
В новых версиях приложения стали запускаться ещё быстрее, но проявился новый недостаток: перед тем, как система соберёт профили с важными участками кода и раздаст большей части устройств, должно пройти некоторое время. Приложение достигало высокой скорости запуска примерно спустя неделю.
График скорости запуска приложения по дням:
Со временем скорость выходила на плато, и пользователь получал быстрое приложение. Но этого момента приходилось ждать.
Это большая проблема для приложений с частыми обновлениями: с каждым новым релизом профили собираются заново, а значит пользователю постоянно придётся сталкиваться с долгими запусками после обновления.
Это большая проблема для приложений с частыми обновлениями: с каждым новым релизом профили собираются заново, а значит пользователю постоянно придётся сталкиваться с долгими запусками после обновления.
Пример описанной ситуации — пользователь только получил быстро работающее приложение, как через несколько дней выходит обновление, и вновь нужно дожидаться быстродействия:
Проблему с пересборкой профилей как раз и решает Baseline Profiles.
Что такое Baseline Profiles и зачем он нам понадобился
Baseline Profiles собирается не по пользователям в продакшене (в отличие от Cloud Profile), а локально во время разработки. Для генерации прогоняются тесты, затрагивающие критичный путь пользователя. Во время выполнения тестов система записывает список использованных деклараций методов и классов приложения в файл, а на этапе сборки приложения этот файл зашивается в apk.
Таким образом, они компилируются заранее и загружаются вместе с приложением, сокращая время запуска и улучшая производительность.
Интегрировать Baseline Profiles в свой проект мы решились по трём причинам:
- кода стало слишком много. Мы проделали массу работы, чтобы наше приложение запускалось быстро, но кода, необходимого для запуска, стало так много, что это влияло на скорость запуска. Простых методов ускорить холодный старт уже не было;
- частые обновления не дают полагаться на Cloud Profiles. Выше мы уже говорили о Cloud Profiles и что они не так эффективны для приложений с частыми обновлениями. Это как раз наш случай — еженедельные апдейты приложения ограничивают эффективность Cloud Profiles;
- начали использовать другие View-фреймворки. Мы стали тестировать Jetpack Compose и собственный Backend-Driven UI. Больное место UI-фреймворков в том, что всем им нужно одновременно много классов для отображения первого кадра.
Baseline Profiles закрывает сразу все эти боли. Кроме холодного старта, он ускоряет полную отрисовку контента и первого кадра, а ещё улучшает плавность скролла в первые секунды использования. Это важно для проектов с долгим холодным стартом — самым первым запуском программы, когда пользователь решает, будет он дальше использовать приложение или нет.
Число холодных стартов у нас — 40%. В абсолютных значениях их тоже много — около 100 млн в месяц:
Как мы тестировали Baseline Profiles
Мы собрали профиль локально и протестировали импакт на него прямо на ноутбуке. Делается это в 4 шага:
- Заводим отдельный flavor apk, пригодного для бенчмарков.
- Создаём модуль с macrobenchmark-тестами.
- Добавляем тест на генерацию профиля, в котором затрагиваем критичные экраны приложения.
- Добавляем macrobenchmark-тест на замер ускорения от профиля.
Мы записали перф-тест из последнего пункта на главную страницу приложения, чтобы проверить, насколько она будет ускоряться на «холодном экране». Результат обрадовал: мы получили 36% ускорения отрисовки первого кадра и 25% ускорения полной отрисовки контента.
После этого мы стали думать, как оценить ускорение в продакшене, ведь локальные тесты не дают полной картины. На ум пришло два варианта: A/B-тест и выпуск в продакшен двух релизов — один с Baseline Profiles, а другой без них. Но оба варианта нам не подошли.
В случае с A/B-тестами мы не могли контролировать установку Baseline Profiles. Для проведения теста нужно было бы включать профили в тестовой группе и выключать в контрольной. А это невозможно — в большинстве случаев профили устанавливаются системой перед установкой приложения, а флаг принадлежности к группе А/B-теста получается только в рантайме.
Идеальным вариантом протестировать было бы выпустить одновременно два релиза, которые отличаются лишь наличием Baseline Profile. Мы решили в этот вариант не идти, издалека показалось что могут быть проблемы, связанные с автоматизированной выкладкой релизов и отслеживанием их поведения (краши, отзывы) — система не рассчитана на одновременную раскатку двух релизов для сравнения и некоторые действия пришлось бы делать вручную.
В итоге мы придумали схему с тремя релизами подряд. Первый релиз обычный, без каких-либо интеграций. Второй релиз — с интеграцией Baseline Profiles. Третий тоже обычный, без Baseline Profiles. Сравнением первого и второго релиза мы убедились, что получили ускорение, а сравнением первого и третьего — что это ускорение целиком обусловлено влиянием Baseline Profiles, а не какого-то другого изменения.
Мы протестировали эту схему в продакшене и получили крутой результат — наш релиз с профилями стал на 20% быстрее для отрисовки первого кадра.
Почему результат локального тестирования отличается от результата тестирования в продакшене
Локальные перф-тесты дали нам 36% ускорения отрисовки первого кадра, а в продакшене мы получили только 20%. Мы проанализировали результаты и пришли к следующим выводам.
В перф-тестах доступны три режима:
- CompilationMode.Full — то есть Full Ahead Of Time-компиляция всего кода приложения (не имеет отношения к новым версиям и нам не интересна);
- CompilationMode.None — без AOT-компиляции, только с JIT;
- CompilationMode.Partial — с помощью AOT-компиляции прогревается код из файла Baseline Profile.
Мы проводили тестирование, сравнивая Partial и None, — то есть сравнивая Baseline Profile с полным отсутствием AOT-компиляции.
В продакшене же сравниваем версию с Baseline Profile и версию с Cloud Profiles. Если брать результат на большом отрезке — от трёх недель — то разница в ускорении только от Baseline Profiles и от связки Cloud Profiles + Baseline Profiles уже не такая сильная, мы видим, что Cloud Profiles действительно начинают помогать.
В тестах на генерацию профиля и на проверку этого профиля подтягиваем один и тот же код, то есть идеально «прогреваем» все классы.
В продакшене же у нас больше вариативности, из-за чего пользователь получает разный код, и какой-то из них может быть не так «прогрет».
В перф-тестах берём одно быстрое устройство. В продакшене же приложение работает на разных устройствах, что тоже влияет на результат будущего ускорения.
Перф-тесты — это тестирование в стерильных условиях. Держите в голове, что в продакшене результат будет иным:
Как мы автоматизировали сборку профиля и что из этого получилось
Когда мы провели все тесты и поняли, что будем интегрировать Baseline Profiles в наше приложение, встал вопрос автоматизации. Надо было определиться, на каком этапе будем собирать профиль.
Было несколько идей, но в итоге мы добавили сбор профиля в задачу по выкатке новой релизной версии. Раньше эта задача состояла из трёх этапов: сбора релизных APK, прогона регресионных тестов, и загрузки артефактов в магазины.
Теперь в самое начало добавился новый этап — подготовка профиля и добавление его в проект.
Так теперь выглядит задача по выпуску обновления:
В Авито используется кастомный раннер для запуска тестов. Он умеет шардировать и генерировать отчёты, перезапускать флакующие тесты, но не умеет работать с macrobenchmark-тестами и доставать дополнительные артефакты.
Чтобы это исправить, мы внесли ряд доработок в раннер:
- научили его запускаться в рамках плагина com.android.test вместо com.android.application. То есть начали передавать build-директорию с релизной APK и её Package Name, чтобы раннер мог устанавливать артефакт и запускать приложение. Для обычных, не-macrobenchmark тестов, эти данные достаются из com.android.application-плагина;
Конфигурация instrumentation-раннера для теста с профилем:
- научили его парсить вывод am instrument, специфичный для macrobenchmark-тестов, — и из него доставали путь к сгенерированному baseline profile;
- научили копировать этот файл профиля с устройства в артефакты билда.
После этого мы решили, что сгенерированный профиль нужно сохранять в VCS — во-первых, для того чтобы он остался в истории изменений, а во-вторых — чтобы при перезапуске билда не выполнять второй раз генерацию профиля, если он уже был собран.
Итоговый пайплайн сборки профиля состоял из шести шагов:
- Проверка последнего коммита — если последний коммит это коммит с профилем, то пропускаем все шаги.
- Сбор APK для профилирования.
- Прогон instrumentation-теста для генерации профиля.
- Копирование профиля с устройства на билд-агент.
- Копирование профиля в исходники проекта, после которого делаем git commit/push.
После чего профиль готов и запускается сборка релизного APK, который будет содержать в себе оптимизации профиля. Этот пайплайн мы обернули в gradle-плагин, его код лежит на GitHub нашей инфраструктуры, а его конфигурация выглядит примерно так:
Итоговый пайплайн сборки релиза. Конфигурация плагина для подготовки профиля:
Какие результаты принесли Baseline Profiles
Интеграция Baseline Profile в наше приложение снизила количество медленных холодных запусков с 8,3% до 5,4% — почти на 3 процентных пункта. Медленные холодные старты — это метрика от Google Play, медленным считается запуск приложения, который длился более 5 секунд.
Размер ускорения холодного старта зависел от устройства пользователя, а точнее — от его мощности. Приложение стало открываться:
- на 16% быстрее у пользователей со средними устройствами (50 персентиль);
- на 20% быстрее — с медленными устройствами (90 персентиль);
- на 12% быстрее — с самыми медленными устройствами (95 персентиль).
Не совсем понятно, почему у самых медленных устройств процент ускорения оказался так мал, мы ожидали большего. В будущем постараемся выяснить, в чем причина такого результата.
Что мы будем делать дальше
«Прогревать» больше вариантов экранов — для авторизованных и неавторизованных пользователей, для разных состояний тогглов и АБ-тестов.
«Прогревать» больше экранов — сейчас мы «прогреваем» только первые три экрана, самые большие и важные. Нам хочется попробовать распространить ускорение на 10–15 самых популярных экранов.
Попробовать внедрить в приложение startup profiles. Это ещё один тип оптимизации. В нём мы тоже создаём тесты для отсмотра сценариев запуска и определяем, какой код нужен для быстрой отрисовки первого кадра. После этого при сборке dex-файлов код для отрисовки первого кадра будет паковаться в первые несколько dex-файлов, а не распределяться равномерно по всем — и поэтому будет загружаться быстрее.
Мы пробовали интегрировать startup profiles год назад, но тогда это не дало ускорения. Попробуем ещё раз.
Добавить мониторинг Baseline Profiles. Хотя мониторингом в полном смысле слова это назвать сложно, потому что профайлы дают возможность узнать только статус установки, но не дают посмотреть детали ошибок.
Итог: нужны ли вам Baseline Profiles и что я советую сделать
Google обещает 30% ускорения выполнения кода с Baseline Profiles и мы убедились в этом на локальном тесте, получив 36% ускорения отрисовки первого кадра. Но это — сравнение с отсутствием AOT-компиляции, а реальное ускорение в продакшене в нашем случае оказалось существенно ниже — порядка 12-20% на разных персентилях.
Что ещё приятно — Baseline Profiles и macrobenchmark-тесты стали стабильнее. Когда я пробовал использовать их год назад, пришлось помучаться с версиями библиотек и странными падениями тестов, но спустя время проблем стало существенно меньше.
Два совета тем, кто раздумывает, нужно ли ему интегрировать Baseline Profiles в своё приложение:
- если ваше приложение обновляется не чаще раза в месяц, то эффект от Baseline Profiles может быть не такой уж и заметным по сравнению с затратностью их интеграции. Тут можно обойтись и Cloud Profiles. Если же обновления частые (как у нас) то стоит подумать о Baseline Profiles;
- запрофилируйте свой сценарий холодного старта и посмотрите, что можно улучшить. Если вы не уверены, что все простые ускорения, связанные с кодом, уже сделаны, нет смысла идти в решения вроде Baseline Profiles — они намного дороже в разработке и поддержке.
Если вы решились потратить силы и время на интеграцию Baseline Profiles, то помните три вещи:
- сначала протестируйте влияние Baseline Profiles локально. Соберите профиль на ноутбуке, напишите для него тест на генерацию профиля и перф-тест на проверку эффекта от ускорения. Посмотрите, какие результаты будут на вашем конкретном сценарии;
- результат в продакшене будет хуже, чем при локальных тестах. В среднем в 1,5–2 раза, но в нашем случае это было всё ещё крутым ускорением. О том, почему так происходит, я рассказываю выше;
- автоматизация — это дорого. В автоматизации много подводных камней, которые обязательно всплывут, как только вы возьмётесь за эту задачу. Поэтому перед тем, как тратить ресурсы на автоматизацию, синхронизируйтесь со своей инфраструктурной командой и вместе проведите груминг предполагаемой задачи.
Спасибо за уделенное статье время! Кстати, недавно мои коллеги рассказывали про эксперимент по написанию 5000 интеграционных тестов за пару часов и сборку генератора для тестирования: как мы к этому пришли и что это нам дало. А в другой статье речь шла о том, как мы тестируем в микросервисах, что такое TaaS и зачем нужны пять шлюзов качества.
Подписывайтесь на канал AvitoTech в Telegram: там мы рассказываем больше о профессиональном опыте наших инженеров, проектах и работе в Авито, а также анонсируем митапы и статьи.