На ранней стадии разработки мы, Android-разработчики, не спешим задумываться об оптимизации производительности будущего приложения. Этому есть объяснение: преждевременная оптимизация невыгодна бизнесу на первых порах, когда в приоритете высокая скорость создания жизнеспособного продукта при условии минимальных затрат. Однако, однажды оптимизация производительности становится просто необходимой.
Поскольку тема оптимизации производительности Android-приложений достойна целого цикла статей, сегодня рассмотрим лишь один ее аспект ― бенчмаркинг.
В статье разберемся с тем, что за зверь такой этот бенчмаркинг и для чего он нужен, а также получим базовые знания для написания первого бенчмарк-теста. Помогать в этом деле буду я, Диана Федотова, Android-разработчица в Технократии.
Содержание
Что есть бенчмаркинг
Оптимизация производительности приложения, как мы уже говорили выше, ― задача комплексная. Она может включать в себя различные действия, для каждого из которого существуют свои подходы и инструменты.
Google разбивает эту задачу на три этапа: inspect, improve, monitor. Вместе они образуют замкнутый цикл.
![Цикл оптимизации производительности Цикл оптимизации производительности](https://habrastorage.org/getpro/habr/upload_files/a4b/bd0/d21/a4bbd0d21706b4c901c45fe45e3ca5ce.png)
Один из методов, используемых на этапе inspect, ― бенчмаркинг. Бенчмаркингом называют тестирование производительности программного кода. С помощью бенчмарк-тестов выявляют просадки производительности и анализируют то, как новый код аффектит на работу кода существующего.
![Бенчмаркинг в цикл оптимизации производительности Бенчмаркинг в цикл оптимизации производительности](https://habrastorage.org/getpro/habr/upload_files/60d/07c/ddc/60d07cddcea671c5ac34fd39c6dd17a9.png)
Нативная реализация бенчмаркинга
Давайте представим ситуацию: мы обнаружили проблемы с производительностью нашего приложения. Причину выяснили ― некий фрагмент кода выполняется чересчур длительное время. Пусть этот код лежит методе doSomeWork. Мы отрефакторили его, а теперь хотим проверить, стал ли этот фрагмент кода выполняться быстрее.
В первом приближении задача кажется довольно простой: измерим время выполнения метода до и после правок, обернем в тест. Это и будет являться бенчмаркингом.
Пишем первое, наивное решение для вычисления времени выполнения doSomeWork. Вычитаем время начала работы метода из времени окончания. Предельно просто, должно работать.
![Решение первое. Простой запуск Решение первое. Простой запуск](https://habrastorage.org/getpro/habr/upload_files/c6f/4f2/3ce/c6f4f23cefc3ccf8c9523a683228c8ae.png)
Запускаем тест один раз, запускаем тест еще раз и обнаруживаем проблему: мы получаем разные результаты при каждом перезапуске одного и того же теста. Что ж, нужно повышать точность. Для этого оборачиваем измерение в цикл, выполняем вызов метода несколько раз и находим среднее значение времени выполнения.
![Решение второе. Запуск в цикле Решение второе. Запуск в цикле](https://habrastorage.org/getpro/habr/upload_files/a69/2ed/892/a692ed8929d31595fc41944f11ee85be.png)
Работает, и, кажется, работает немного поточнее. Но и к такому решению есть некоторые вопросы. Почему мы проводим именно 5 измерений? Как правильно выбирать это число? Как обрабатывать различные выбросы в измерениях? Как учитывать состояние системы и нагрузку на нее, а также влияние задач, выполняющихся в фоне, ведь все это может влиять на результат измерений?
Пока неясно. Ясно лишь одно: провести эти вычисления руками — задача нетривиальная. Наше наивное решение:
неточно;
нестабильно.
Значит, нужен автоматизированный подход для измерения производительности кода. Решение есть — воспользуемся библиотекой Jetpack Benchmark ?.
Библиотека Jetpack Benchmark
Библиотека была представлена на Google I/O’19 и стала спасительной соломинкой для разработчиков, заинтересованных в бенчмаркинге своих приложений.
Jetpack Benchmark Library:
предоставляет API для измерения производительности кода Android-приложений;
интегрирована с Android Studio;
позволяет писать инструментальные JUnit тесты, которые выполняются прямо на устройстве.
Посмотрим, во что превращается наш тест для измерения времени работы метода doSomeWork с библиотекой Jetpack Benchmark.
![Решение третье. Jetpack Benchmark Library Решение третье. Jetpack Benchmark Library](https://habrastorage.org/getpro/habr/upload_files/873/db0/afb/873db0afbdaed7c54260894510bd4697.png)
Как видите, все, что нам нужно, это создать экземпляр класса BenchmarkRule
, предоставляемого библиотекой, и вызвать метод BenchmarkRule#measureRepeated
, передав в него вызов метода doSomeWork
.
Здорово? Код выглядит довольно просто, а работает — стабильно. Написанный тест, кстати, не просто бенчмарк, а микробенчмарк. Давайте разберем, что это значит.
На самом деле, Jetpack Benchmark предлагает нам две библиотеки и, соответственно, два разных подхода для бенчмаркинга: Macrobenchmark и Microbenchmark. Между ними есть существенные отличия.
![Сравнение библиотек Сравнение библиотек](https://habrastorage.org/getpro/habr/upload_files/a73/2d3/409/a732d3409c9011143bbf14847a50095f.png)
Библиотека Macrobenchmark предназначена для бенчмаркинга запуска приложения, взаимодействий с пользователем, манипуляций с интерфейсом, скролла списков, анимаций. Тестирование библиотека проводит в отдельном от самого приложения процессе. Одна итерация цикла тестирования может длиться довольно долго и даже превышать минуту. Библиотека работает только с 23 API, а часть функционала доступна на еще более поздних версиях.
Библиотека Microbenchmark предназначена для бенчмаркинга часто вызываемых функций. Например, measure
или layout pass
во View, layout inflating, парсинга данных, CPU-вычислений и других фрагментов кода, которые выполняются неоднократно. Любой код, который выполняется нечасто или выполняется по-разному при многократном вызове, может не подходить для микробенчмаркинга. Тестирование осуществляется в процессе приложения, а итерации занимают не более 10 секунд. Библиотека доступна для работы с 14 API.
Примеры реализации макро- и микробенчмарков можно подсмотреть в репозитории Google на GitHub.
Макробенчмарки
Конфигурация модуля
Итак, давайте завезем Jetpack Benchmark в проект. Начнем с макробенчмарков. Google рекомендует использовать Android Studio Bumblebee 2021.1.1 или новее, поскольку с этой версии IDE появились возможности для более быстрой интеграции макробенчмарков.
Для корректной работы макробенчмарков необходимо создать для них отдельный benchmark-модуль. Он будет отвечать за запуск тестов и проведение измерений.
В Android Studio Bumblebee 2021.1.1 и новее доступен шаблон для упрощения создания и настройки модуля. Шаблон генерирует модуль, в котором лежит сэмпл бенчмарка. Чтобы использовать шаблон, необходимо:
В списке темплейтов выбрать Benchmark.
В селекторе указать библиотеку Macrobenchmark.
Кастомизировать target-application, к которому будут применены бенчмарки. Это один из app-модулей нашего проекта, далее будем называть его target-модулем.
Определить имя пакета, модуля, язык, minSDK.
![Создание модуля для макробенчмарков Создание модуля для макробенчмарков](https://habrastorage.org/getpro/habr/upload_files/800/9a8/318/8009a8318496e7b74e364e9ea42440a7.png)
Чтобы дать возможность macrobenchmark-модулю проводить тестирование target-модуля, нужно сделать target-модуль profileable
. Этот маневр позволит получать подробную информацию о трассировке. Тэг profileable
в target-модуль добавляется автоматически при генерации модуля macrobenchmark
.
![AndroidManifest.xml target-модуля AndroidManifest.xml target-модуля](https://habrastorage.org/getpro/habr/upload_files/64a/7cc/d64/64a7ccd64c74947c9f86e1d608c949ac.png)
Заглянем теперь в build.gradle target-модуля. Во первых, для запуска тестов в defaultConfig
нужно указать testInstrumentationRunner
— AndroidJUnitRunner.
![build.gradle target-модуля build.gradle target-модуля](https://habrastorage.org/getpro/habr/upload_files/851/506/d13/851506d13fd774c32a861aad016257e8.png)
Во вторых, нужно создать отдельный build type для бенчмаркинга. Не подойдет ни релизный, ни дебажный build type. Нам нужны все те оптимизации, что есть в релизном build type и подпись дебажными ключами. Поэтому при генерации benchmark-модуля автоматически создает новый build type — benchmark
. Делаем его копией релизного с помощью ключевого слова initWith. Переопределяем signingConfig
— указываем дебажный вариант.
В третьих, нужно перевести флаг debuggable
в положение false
.
![build.gradle target-модуля build.gradle target-модуля](https://habrastorage.org/getpro/habr/upload_files/236/a3e/5bb/236a3e5bbb26dfd4965e904dad2103a9.png)
Если мы все сделали правильно, после gradle синка во вкладке Build Variants отобразятся модули :app (target-модуль) и :macrobenchmark (benchmark-модуль), а также новые build variants.
![Build Variants. Одномодульный проект Build Variants. Одномодульный проект](https://habrastorage.org/getpro/habr/upload_files/b3e/a10/b1d/b3ea10b1dfafd60897060cf34a035f73.png)
Для многомодульного проекта такой конфигурации не будет достаточно. При синке произойдет сбой и вылетет следующее сообщение об ошибке:
![Ошибка синка многомодульного проекта Ошибка синка многомодульного проекта](https://habrastorage.org/getpro/habr/upload_files/6e2/f4e/1f4/6e2f4e1f4128e23deddf9574bcdc890c.png)
Нужно дать понять Gradle, какие build types компилировать для остальных модулей. Чтобы решить этот вопрос, достаточно добавить фоллбек на release build type в случае отсутствия в них benchmark build type. Делаем это, указывая matchingFallbacks в benchmark build type для модулей :app и :macrobenchmark.
![build.gradle target-модуля build.gradle target-модуля](https://habrastorage.org/getpro/habr/upload_files/c76/afa/a3a/c76afaa3abc13afd81afbaa61c50532d.png)
Вот так будет выглядеть правильная настройка для многомодульного приложения.
![Build Variants. Многомодульный проект Build Variants. Многомодульный проект](https://habrastorage.org/getpro/habr/upload_files/1de/153/225/1de153225eb847e00188e0a63c259e08.png)
Создание макробенчмарка
Взглянем на код самого простого макробенчмарка. Он представляет собой обыкновенный JUnit тест. Бенчмаркинг осуществляется с использованием класса MacrobenchmarkRule
. Для справки, Rules в JUnit предоставляют гибкий механизм для буста тестов путем запуска некоторого кода вокруг выполнения самого теста. В каком-то смысле Rules напоминают аннотации @Before
и @After
в тестовом классе.
![Создание макробенчмарка Создание макробенчмарка](https://habrastorage.org/getpro/habr/upload_files/77c/474/b27/77c474b27fcf6a9c56b8cfeb9d88c1de.png)
Вызываем measureRepeated
на macrobenchmarkRule
, эта функция позволяет определить различные условия того, как должен отработать бенчмаркинг. Подробнее рассмотрим параметры в разделе Конфигурация макробенчмарка.
Запуск макробенчмарка
Есть несколько вариантов запуска тестов:
через графический интерфейс Android Studio
через терминал, запустив один конкретный тест или все тесты сразу
![Запуск бенчмарк-тестов Запуск бенчмарк-тестов](https://habrastorage.org/getpro/habr/upload_files/b80/190/24a/b8019024a7123578d6e17696c48a68c7.png)
Перед запуском для повышения точности измерений библиотека проверяет правильность конфигурации согласно некоторым условиям. Так, например, бенчмаркинг должен осуществляться на физическом устройстве с достаточным уровнем заряда батареи (не менее 25%). Если какая-либо из проверок завершается неудачей, библиотека останавливает запуск, чтобы предотвратить некорректные измерения.
При необходимости, некоторые проверки можно отключить с помощью аргумента android.benchmark.suppressErrors
.
![benchmark-модуль. build.gradle benchmark-модуль. build.gradle](https://habrastorage.org/getpro/habr/upload_files/b1e/26e/3ed/b1e26e3ed577c72d5886669048d5db10.png)
Так мы сообщаем о том, что принимаем риски и хотим запустить бенчмарки даже в неправильно настроенном состоянием. Выходные файлы таких бенчмарков намеренно переименовываются библиотекой путем добавления префикса с тестами ошибок.
После того, как бенчмарк успешно запущен и выполнен, полученные измерения записываются в json файл, например, для использования на CI. По умолчанию отчет в записывается на устройство по указанному адресу:
![json с результатами бенчмаркинга и путь его сохранения json с результатами бенчмаркинга и путь его сохранения](https://habrastorage.org/getpro/habr/upload_files/886/633/685/886633685c230bca28d924ea908b16fa.png)
Кроме того, результаты работы отображаются прямо в Android Studio во вкладке Run. Помимо измерений предоставляются ссылки на трассировки итераций. Трассировки доступны для анализа во встроенной тулзе CPU Profiler. История бенчмаркинга сохраняется в Android Studio и на диске устройства. Ниже мы разберем, как расшифровать полученные данные.
![Отображение результатов в Android Studio Отображение результатов в Android Studio](https://habrastorage.org/getpro/habr/upload_files/c6e/a0e/1e7/c6ea0e1e7db646306c8fb81f6b6e2b9b.png)
Конфигурация макробенчмарка
Вся работа по конфигурации макробенчмарка производится в рамках функции measureRepeated
.
![Параметры функции measureRepeated Параметры функции measureRepeated](https://habrastorage.org/getpro/habr/upload_files/364/0f9/bb5/3640f9bb5f3acddd24bcf56de60a737b.png)
Она принимает семь параметров, среди них четыре обязательных:
packageName
— имя пакета target-модуля;metrics
— список метрик для измерения;iterations
— число итераций измерения;measureBlock
— измеряемое действие.
И три опциональных:
compilationMode
— режим компиляции приложения;startupMode
— режим запуска приложения;setupBlock
— действие, выполняемое перед каждой итерацией.
Некоторые из них довольно нетривиальные, они связаны с одним из назначений макробенчмарков — измерением времени запуска приложения. Остановимся на них более подробно.
StartupMode
Начнем с параметра startupMode
. Он отвечает за режим запуска приложения. Режимы нам давно знакомы:
COLD — запуск с нуля, процесс приложения не запущен, активити еще не создана;
WARM — процесс приложения запущен, но активити еще не создана;
HOT — процесс и активити запущены, активити переносится в foreground.
![StartupMode StartupMode](https://habrastorage.org/getpro/habr/upload_files/580/3ee/61f/5803ee61f3f4ee76b5be2aac74891fb1.png)
CompilationMode
Следующий параметр функции measureRepeated
— compilationMode
. Он определяет режим компиляции, а именно то, какая часть приложения должна быть предварительно скомпилирована перед проведением измерений. Перед каждым новым бенчмаркингом состояние компиляция сбрасывается, чтобы предыдущие запуски не оказывали влияние на последующие.
На Android N+ (API 24+) поддерживаются следующие режимы компиляции:
Partial — частичная прекомпиляция приложения при наличии Baseline Profile. Это наиболее реалистичный опыт установки приложения на устройство конечного пользователя.
Full — полная предварительная компиляция. Режим не отражает реального пользовательского опыта, но может быть использован либо для иллюстрации идеальной производительности, либо для уменьшения шума при компиляции во время выполнения бенчмаркинга.
None — приложение вообще не компилируется, минуя компиляцию по умолчанию, которая обычно должна выполняться во время установки. Этот режим иллюстрирует производительность в наихудшем случае.
Ignore — игнорирование состояния компиляции. Используется, чтобы оставлять состояние компиляции неизменным при перезапуске тестов.
На Android M (API 23) поддерживается только режим Full, все приложения всегда полностью прекомпилированы.
Metric
Заключительный параметр функции measureRepeated
, который мы разберем, — metrics
. Метрики — это основной тип информации, извлекаемой из бенчмарков. Они передаются в measureRepeated
по одной или списком. Доступные метрики: StartupTimingMetric
, FrameTimingMetric
и экспериментальные TraceSectionMetric
и PowerMetric
.
![Передача списка метрик в measureRepeated Передача списка метрик в measureRepeated](https://habrastorage.org/getpro/habr/upload_files/5b3/218/c4e/5b3218c4ed1ee3fd1e9aac0a44aca478.png)
1️⃣ StartupTimingMetric
измеряет время запуска приложения, а именно следующие значения:
timeToInitialDisplayMs
— время с момента получения системой launch интента до рендеринга первого кадра активитиtimeToFullDisplayMs
— время с момента получения системой launch интента до того, как приложение не сообщит о полной отрисовке с помощью метода reportFullyDrawn. Значение доступно только с Android 11 (API level 30)
![Отображение результатов StartupTimingMetric Отображение результатов StartupTimingMetric](https://habrastorage.org/getpro/habr/upload_files/856/975/14e/85697514ebd10bb129557697eb1cd32c.png)
2️⃣ FrameTimingMetric
работает с кадрами.
frameOverrunMs
— отражает то, на сколько времени кадр опоздал к дедлайну по отрисовке. Положительные числа указывают на пропущенный кадр и видимые лаги, отрицательные числа указывают, насколько быстрее был подготовлен кадр. Доступно только на Android 12 (уровень API 31) и выше.frameDurationCpuMs
— отражает то, сколько времени потребовалось для создания кадра на процессоре — как на MainThread, так и на RenderThread.
Измерения собраны в процентили: 50-й, 90-й, 95-й и 99-й. Процентиль (P) — термин из статистики. Так, например, запись frameDurationCpuMs P90 7.9 означает, что 90% кадров отрисовались быстрее 7.9 мс
![Отображение результатов FrameTimingMetric Отображение результатов FrameTimingMetric](https://habrastorage.org/getpro/habr/upload_files/bb9/b04/9de/bb9b049dee077dea1550157dbdddb2f9.png)
3️⃣ TraceSectionMetric
измеряет время выполнения определенной разработчиком секции кода. Секция определяется самим разработчиком в коде вызовом функции trace(sectionName){}
или с помощью статических функций Trace.beginSection(sectionName) и Trace.endSection()
.
![Отображение результатов TraceSectionMetric Отображение результатов TraceSectionMetric](https://habrastorage.org/getpro/habr/upload_files/b87/899/986/b878999868e2e4e8dd58e624f0242bc3.png)
4️⃣ PowerMetric
измеряет потребление power или energy для заданных категорий. Доступные категории: CPU, DISPLAY, GPU, GPS, MEMORY, MACHINE_LEARNING, NETWORK, и UNCATEGORIZED. Значения измерений отражают общесистемное потребление, а не потребление для каждого приложения, и в настоящее время ограничены устройствами Pixel 6 и Pixel 6 Pro.
Микробенчмарки
Итак, мы рассмотрели макробенчмарки. Теперь разберемся с микробенчмарками. Вспомним, что они предназначены для измерения работы быстрых, часто вызываемых функций.
Для их реализации также нужно создать специальный microbenchmark-модуль. Генерируем его с помощью встроенного темплейта по аналогии с макробенчмарками.
![Создание модуля для микробенчмарков Создание модуля для микробенчмарков](https://habrastorage.org/getpro/habr/upload_files/caa/536/749/caa536749d4185bd2643f00b41731454.png)
После помещаем код, который хотим протестировать, в отдельный модуль, назовем его benchmarkable-модуль.
![Зависимости между модулями Зависимости между модулями](https://habrastorage.org/getpro/habr/upload_files/1bd/1d3/d85/1bd1d3d85e5c523a6b065427e58efe5a.png)
Затем изменяем файл build.gradle microbenchmark-модуля, добавляем зависимость на benchmarkable-модуль, содержащий код для тестирования.
![benchmark-модуль. build.gradle benchmark-модуль. build.gradle](https://habrastorage.org/getpro/habr/upload_files/989/34e/97a/98934e97ac8d2a9e306b33ede307569a.png)
Чтобы создать микробенчмарк, используем класс BenchmarkRule
, предоставляемый библиотекой. Тестируемый код передаем в функцию measureRepeated
. В отличие от макробенчмарков, в микробенчмарках число выполняемых итераций определяет самой библиотекой, measureRepeated
никаких параметров для кастомизации не принимает.
![Создание микробенчмарка Создание микробенчмарка](https://habrastorage.org/getpro/habr/upload_files/42c/9a5/1d8/42c9a51d859409f4af179e005eeb9f36.png)
Процессы запуска и просмотра результатов микробенчмаркинга аналогичны тем же процессам для макробенчмаркинга, их мы уже успели рассмотреть выше ☝️
Краткие выводы
Итак, давайте резюмируем то, что успели обсудить сегодня:
Бенчмаркинг — тестирование производительности программного кода.
Jetpack Benchmark Library предоставляет API для автоматизации измерения производительности кода Android-приложений. Бенчмарк — обыкновенный инструментальный JUnit-тест, который выполняется прямо на устройстве.
Macrobenchmark предназначена для бенчмаркинга запуска приложения, взаимодействий с пользователем, манипуляций с интерфейсом, скролла списков, анимаций.
Microbenchmark предназначена для бенчмаркинга часто вызываемых функций, например, measure или layout pass во View, layout inflating, парсинга данных, CPU-вычислений и других фрагментов кода, которые выполняются неоднократно. Любой код, который выполняется нечасто или выполняется по-разному при многократном вызове, может не подходить для микробенчмаркинга.
Для реализации как макро-, так и микробенчмарков необходимо создать для них отдельные модули и правильно настроить их конфигурацию в gradle файлах.
Для написания бенчмарк-теста необходимо передать вызов тестируемого кода в функцию measureRepeated. В случае макробенчмаркинга функция принимает параметры для кастомизации.
Результаты бенчмаркинга отображаются в Android Studio, записываются в json файл, сохраняются в студии и на диске устройства.
На этом все. Спасибо, что дочитали до конца ? Вы также можете ознакомиться с видео-версией статьи — записью митапа TechnoMeetsDroid. Рады будем видеть вас в наших следующих постах и митапах!
![](https://habrastorage.org/getpro/habr/upload_files/9d2/ef5/b5d/9d2ef5b5dcd74cf6bbd7a926bafd3e0b.jpg)
Диана Федотова
Android-разработчица в Технократии
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.