Улучшаем производительность android-приложения с помощью Baseline profiles

efrenhzstkwfaogs9rxslrg4cjg.png

Производительность важна для формирования положительного пользовательского опыта использования приложения, поэтому разработчики стремятся ускорить работу своих программ. Для приложений в области безопасности важно время инициализации: пара секунд может стать роковой для пользователя. Поэтому в «Лаборатории Касперского» мы пристально следим за производительностью наших приложений, анализируем влияние новых доработок на ключевые метрики и регулярно улучшаем их.

В большинстве случаев работа над производительностью сводится к оптимизации исходного кода: сперва находят узкие места при помощи метрик, утилит и инструментов, затем разработчик обращается к коду для поиска и устранения проблем. Иногда Google предоставляет дополнительные возможности для оптимизации. Baseline profile стал одной из таких возможностей и был анонсирован совсем недавно. Безусловно, такие доработки очень приятны для разработчиков и полезны для приложений, поэтому мы с огромным интересом знакомимся с новой возможностью и начинаем применять ее к нашему проекту.

В этой статье я хочу поделиться нашим опытом и результатами. Сперва мы коротко вспомним виды компиляции в Android, поймем принцип, на котором основывается данная оптимизация. Затем ознакомимся с пошаговой инструкцией по интеграции в свой проект и посмотрим на полученные результаты. В конце я расскажу о наших дальнейших шагах и планах.

Принцип работы


Для понимания принципа работы Baseline profile нужно вспомнить виды компиляции в Android. После сборки приложения мы получаем файл .apk, который содержит файлы .dex. В них находится bytecode, понятный интерпретатору. Android runtime транслирует bytecode в машинный код. Получение машинного кода из bytecode может происходить несколькими путями.

В случае с Dalvik девайсы производились с небольшой по объему оперативной памятью (RAM), поэтому шаги по оптимизации были нацелены на уменьшение ее использования. Для этого использовалась JIT (Just-In-Time) compilation — компиляция в runtime. Вместо того чтоб компилировать все приложение целиком, компилировались лишь некоторые участки кода. Так как вся компиляция происходит в процессе выполнения, это негативно сказывалось на производительности.

На замену Dalvik пришел ART с измененным подходом к компиляции. В ART появилась компиляция AOT (Ahead of time). К моменту запуска приложения весь код уже скомпилирован. Как результат, получаем выигрыш по производительности, но просадку по использованию RAM, долгую установку приложения и долгие системные обновления.

Частичная компиляция является компромиссом. По умолчанию bytecode JIT-прекомпилируется, но если встречаются участки кода, которые используются часто, тогда они AOT-компилируются. AOT-компиляция происходит при помощи утилиты dex2oat, сохраняя результат в бинарных файлах .oat. В итоге получаем гибридную схему компиляции.

image


Оригинал: https://source.android.com/devices/tech/dalvik/jit-compiler

Однако и эта схема имеет проблемы. AOT-компиляция происходит спустя какое-то время (после нескольких запусков приложения и прохождения ряда пользовательских сценариев). Поэтому первый опыт и впечатления могут быть подпорчены невысоким уровнем производительности. А ведь зачастую именно первый опыт формирует отношение пользователя к приложению. Поэтому в ART появились профили (profiles). Доступны два вида профилей.

Первый — облачный. В процессе использования приложения Google собирает и анализирует сценарии выполнения программы, находит часто используемые участки и отправляет эту информацию на сервер. Эти данные усредняют по множеству пользователей, и получается единый профиль, который попадет новым пользователям для AOT-компиляции при первой установке.

Второй вид — Baseline profile. Это возможность указать компилятору те участки кода, которые бы вы хотели прекомпилировать на девайсах пользователей.
Эти профили как раз и формируют начальный кеш, который позволяет ускорить время запуска и производительность.

Интеграция в продукт


Пошаговая инструкция для генерации Baseline profile доступна на официальном сайте Android по ссылке. Если коротко описывать процесс, то он выглядит так: генерируем профиль, извлекаем его, добавляем в проект, пересобираем, замеряем.

Для генерации и извлечения профиля нужно начать с получения root и выставления userdebug на девайсе. Google заявляет, что генерировать профиль и замерять можно и на AOSP-эмуляторе, но лично я предпочитаю работать над улучшением производительности только с реальным устройством. Однако если нужного физического девайса у вас нет, следует воспользоваться советом Google и создать эмулятор без Google API. В случае неполадок профиль можно генерировать на эмуляторе, а замерять на реальном девайсе.

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

Для генерации профиля необходимо подключить библиотеку Profileinstaller. К моменту написания этой статьи она находится в бете, а инструкция по генерации профиля недостаточно полная.

Так, в ней отсутствует информация о том, как дружат между собой оптимизация и разные обфускаторы, в частности Proguard. Это был один из первых подводных камней, на который мы наткнулись. После того как мы сгенерировали первый профиль, добавили его в проект и пересобрали код, мы не получили прироста производительности. Это обстоятельство смутило нас. Согласно инструкции, в файле baseline-prof.txt должны содержаться методы и классы, рекомендованные к AOT-прекомпиляции. Мы решили более детально взглянуть на содержимое полученного профиля и заметили, что большинство наших классов было подвергнуто обфускации. Генерацию профиля следует производить с добавлением флага -dontobfuscate к правилам Proguard. Я нашел Request на Issue tracker Google об этом. Надеюсь, эту информацию скоро добавят к основной инструкции. От себя могу порекомендовать первые замеры делать еще и без обфускации самого приложения. Чем меньше у вас дополнительных этапов в сборке, тем проще будет контролировать достижение ожидаемого результата на промежуточных этапах. Минусом будет увеличение количества действий и сроков по задаче в целом, поэтому решение должен принимать каждый сам.

После генерации профиля я рекомендую просмотреть и проанализировать его. Возможно, возникнет желание что-то в нем заменить. Google рекомендует сильно не нагружать профиль (согласно официальной рекомендации, Baseline profiles не могут быть больше 1,5 MБ в сжатом виде).

Итоговые замеры можно осуществлять как при помощи автотестов, так и вручную. При ручном тестировании я рекомендую проверять статус dexopt state после каждой установки:

adb shell dumpsys package dexopt | grep -A 1 $PACKAGE_NAME

Результат поможет понять, какой вид компиляции был применен, и отловить ошибки в своих действиях до начала замеров. Существует четыре статуса компиляции. Более подробно об этом можно прочитать здесь. В случае установки с baseline profile статус должен быть speed-profile. Этот статус сообщает о том, что baseline profile был успешно применен.

После установки приложения статус компиляции можно изменить, для этого нужно скомпилировать пакет снова, указав в аргументах желаемый тип. Напомню, что для выполнения этих команд нужен root. В качестве эксперимента рекомендую всем попробовать разные режимы компиляции:

Profile-based:


adb shell cmd package compile -m speed-profile -f my-package

Эта команда запустит компиляцию указанного пакета с применением профиля.

Full:


adb shell cmd package compile -m speed -f my-package

Эта команда запустит компиляцию всех методов для указанного пакета.

Reset:


adb shell cmd package compile --reset my-package

Эта команда сбросит компиляцию для указанного пакета.

Наши результаты


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

Помимо рекомендуемой к замеру Time to initial display метрики, мы смотрели и на время полной инициализации приложения. Под полной инициализацией мы подразумеваем время от вызова метода Application#onCreate() до конца работы цепочки инициализации всех фич и стартовой логики проверки статуса лицензии, авторизации и прочее. Эта инициализация происходит не в главном потоке, но от ее продолжительности зависит функционал и время показа актуальных статусов в приложении.

В начале статьи мы говорили про обфускацию кода. В нашем проекте используется дополнительный механизм защиты кода, который обфусцирует и шифрует важные классы и файлы, логику которых мы решили дополнительно защитить. В результате часть классов подвергается повторной обфускации, а некоторые изымаются из файлов .dex и подгружаются в процессе выполнения. Это значит, что Baseline profile будет применен в неполном объеме. Поэтому таблица разделена по принципу — с/без Baseline profile и с/без дополнительной защиты. В таблице приведены замеры нашей метрики времени полной фоновой инициализации и метрики Time to initial display. На предпоследней строке жирным шрифтом приведены усредненные величины, а на последней можно увидеть прирост производительности в сравнении с соответствующими показателями из правых столбцов.

image

Результаты на сборке без дополнительной защиты кода:

image


Получили ускорение на 12 и 33 процента по App Init и App startup соответственно.

Результаты на сборке с дополнительной защитой кода:

image


Получили ускорение на 7 и 17 процентов по App Init и App startup соответственно.

Что дальше?


Работа над производительностью зачастую бывает долгой и мало результативной. В случае с Baseline profile мы получили неплохой буст с маленькими вложениями.

После неплохих локальных результатов мы планируем раскатить изменения и посмотреть метрики на девайсах пользователей. Результаты могут быть хуже локальных по разным причинам. Одна из них — Cloud Profile. Поэтому мы будем анализировать пересечения профилей Cloud и Baseline. Также мы планируем включить генерацию профилей в наш CI/CD для получения актуального профиля для каждой новой релизной сборки. Это важно для проектов, в которых активно ведется разработка. Если периодически не обновлять профили, то их эффективность может снизиться, потому что могут измениться как пользовательские сценарии, так и названия классов и методов. Если вам было интересно и вы хотите углубиться в тему вместе с нами — поэкспериментировать с разными сценариями и экранами, находить слабые места и пробовать ускорить работу нашего приложения вновь, — приходите к нам в команду :)

Полезные ссылки:


Implementing ART Just-In-Time (JIT) Compiler
Инструкция по Baseline profile
Writing a Macrobenchmark

© Habrahabr.ru