На ранней стадии разработки мы, Android-разработчики, не спешим задумываться об оптимизации производительности будущего приложения. Этому есть объяснение: преждевременная оптимизация невыгодна бизнесу на первых порах, когда в приоритете высокая скорость создания жизнеспособного продукта при условии минимальных затрат. Однако, однажды оптимизация производительности становится просто необходимой. Поскольку тема оптимизации производительности Android-приложений достойна целого цикла статей, сегодня рассмотрим лишь один ее аспект ― бенчмаркинг.   В статье разберемся с тем, что за зверь такой этот бенчмаркинг и для чего он нужен, а также получим базовые знания для написания первого бенчмарк-теста. Помогать в этом деле буду я, Диана Федотова, Android-разработчица в Технократии. Что такое бенчмаркинг Наивная реализация бенчмаркинга   Библиотека Jetpack Benchmark   Макробенчмарки   Микробенчмарки   Краткие выводы   Оптимизация производительности приложения, как мы уже говорили выше, ― задача комплексная. Она может включать в себя различные действия, для каждого из которого существуют свои подходы и инструменты.   Google разбивает эту задачу на три этапа: inspect, improve, monitor. Вместе они образуют замкнутый цикл. Один из методов, используемых на этапе inspect, ― бенчмаркинг. Бенчмаркингом называют тестирование производительности программного кода. С помощью бенчмарк-тестов выявляют просадки производительности и анализируют то, как новый код аффектит на работу кода существующего.  Давайте представим ситуацию: мы обнаружили проблемы с производительностью нашего приложения. Причину выяснили ― некий фрагмент кода выполняется чересчур длительное время. Пусть этот код лежит методе doSomeWork. Мы отрефакторили его, а теперь хотим проверить, стал ли этот фрагмент кода выполняться быстрее. В первом приближении задача кажется довольно простой: измерим время выполнения метода до и после правок, обернем в тест. Это и будет являться бенчмаркингом. Пишем первое, наивное решение для вычисления времени выполнения doSomeWork. Вычитаем время начала работы метода из времени окончания. Предельно просто, должно работать. Запускаем тест один раз, запускаем тест еще раз и обнаруживаем проблему: мы получаем разные результаты при каждом перезапуске одного и того же теста. Что ж, нужно повышать точность. Для этого оборачиваем измерение в цикл, выполняем вызов метода несколько раз и находим среднее значение времени выполнения. Работает, и, кажется, работает немного поточнее. Но и к такому решению есть некоторые вопросы. Почему мы проводим именно 5 измерений? Как правильно выбирать это число? Как обрабатывать различные выбросы в измерениях? Как учитывать состояние системы и нагрузку на нее, а также влияние задач, выполняющихся в фоне, ведь все это может влиять на результат измерений?  Пока неясно. Ясно лишь одно: провести эти вычисления руками — задача нетривиальная. Наше наивное решение: неточно; нестабильно. Значит, нужен автоматизированный подход для измерения производительности кода. Решение есть — воспользуемся библиотекой Jetpack Benchmark 🚀. Библиотека была представлена на Google I/O’19 и стала  спасительной соломинкой для разработчиков, заинтересованных в бенчмаркинге своих приложений. Jetpack Benchmark Library: предоставляет API для измерения производительности кода Android-приложений; интегрирована с Android Studio; позволяет писать инструментальные JUnit тесты, которые выполняются прямо на устройстве. Посмотрим, во что превращается наш тест для измерения времени работы метода doSomeWork с библиотекой Jetpack Benchmark. Как видите, все, что нам нужно, это создать экземпляр класса BenchmarkRule, предоставляемого библиотекой, и вызвать метод BenchmarkRule#measureRepeated, передав в него вызов метода doSomeWork.  Здорово🔥 Код выглядит довольно просто, а работает — стабильно. Написанный тест, кстати, не просто бенчмарк, а микробенчмарк. Давайте разберем, что это значит. На самом деле, Jetpack Benchmark предлагает нам две библиотеки и, соответственно, два разных подхода для бенчмаркинга: Macrobenchmark и Microbenchmark. Между ними есть существенные отличия. Библиотека 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. Чтобы дать возможность macrobenchmark-модулю проводить тестирование target-модуля, нужно сделать target-модуль profileable. Этот маневр позволит получать подробную информацию о трассировке. Тэг profileable в target-модуль добавляется автоматически при генерации модуля macrobenchmark. Заглянем теперь в build.gradle target-модуля. Во первых, для запуска тестов в defaultConfig  нужно указать testInstrumentationRunner — AndroidJUnitRunner.  Во вторых, нужно создать отдельный build type для бенчмаркинга. Не подойдет ни релизный, ни дебажный build type. Нам нужны все те оптимизации, что есть в релизном build type и подпись дебажными ключами. Поэтому при генерации benchmark-модуля автоматически создает новый  build type — benchmark. Делаем его копией релизного с помощью ключевого слова initWith. Переопределяем signingConfig — указываем дебажный вариант.  В третьих, нужно перевести флаг debuggable в положение false. Если мы все сделали правильно, после gradle синка во вкладке Build Variants отобразятся модули :app (target-модуль) и :macrobenchmark (benchmark-модуль), а также новые build variants. Для многомодульного проекта такой конфигурации не будет достаточно. При синке произойдет сбой и вылетет следующее сообщение об ошибке: Нужно дать понять Gradle, какие build types компилировать для остальных модулей. Чтобы решить этот вопрос, достаточно добавить фоллбек на release build type в случае отсутствия в них benchmark build type. Делаем это, указывая matchingFallbacks в benchmark build type для модулей :app и :macrobenchmark. Вот так будет выглядеть правильная настройка для многомодульного приложения. Взглянем на код самого простого макробенчмарка. Он представляет собой обыкновенный JUnit тест. Бенчмаркинг осуществляется с использованием класса MacrobenchmarkRule. Для справки, Rules в JUnit предоставляют гибкий механизм для буста тестов путем запуска некоторого кода вокруг выполнения самого теста. В каком-то смысле Rules напоминают аннотации @Before и @After в тестовом классе.   Вызываем  measureRepeated на macrobenchmarkRule, эта функция позволяет определить различные условия того, как должен отработать бенчмаркинг. Подробнее рассмотрим параметры в разделе Конфигурация макробенчмарка. Есть несколько вариантов запуска тестов:  через графический интерфейс Android Studio через терминал, запустив один конкретный тест или все тесты сразу Перед запуском для повышения точности измерений библиотека проверяет правильность конфигурации согласно некоторым условиям. Так, например, бенчмаркинг должен осуществляться на физическом устройстве с достаточным уровнем заряда батареи (не менее 25%). Если какая-либо из проверок завершается неудачей, библиотека останавливает запуск, чтобы предотвратить некорректные измерения. При необходимости, некоторые проверки можно отключить с помощью аргумента android.benchmark.suppressErrors. Так мы сообщаем о том, что принимаем риски и хотим запустить бенчмарки даже в неправильно настроенном состоянием. Выходные файлы таких бенчмарков намеренно переименовываются библиотекой путем добавления префикса с тестами ошибок. После того, как бенчмарк успешно запущен и выполнен, полученные измерения записываются в json файл, например,  для использования на CI. По умолчанию отчет в записывается на устройство по указанному адресу: Кроме того, результаты работы отображаются прямо в Android Studio во вкладке Run. Помимо измерений предоставляются ссылки на трассировки итераций. Трассировки доступны для анализа во встроенной тулзе CPU Profiler. История бенчмаркинга сохраняется в Android Studio и на диске устройства. Ниже мы разберем, как расшифровать полученные данные. Вся работа по конфигурации макробенчмарка производится в рамках функции measureRepeated.  Она принимает семь параметров, среди них четыре обязательных: packageName — имя пакета target-модуля; metrics — список метрик для измерения; iterations — число итераций измерения; measureBlock — измеряемое действие. И три опциональных: compilationMode — режим компиляции приложения; startupMode — режим запуска приложения; setupBlock — действие, выполняемое перед каждой итерацией. Некоторые из них довольно нетривиальные, они связаны с одним из назначений макробенчмарков — измерением времени запуска приложения. Остановимся на них более подробно. Начнем с параметра startupMode. Он отвечает за режим запуска приложения. Режимы нам давно знакомы: COLD — запуск с нуля, процесс приложения не запущен, активити еще не создана; WARM — процесс приложения запущен, но активити еще не создана; HOT — процесс и активити запущены, активити переносится в foreground. Следующий параметр функции measureRepeated — compilationMode. Он определяет режим компиляции, а именно то, какая часть приложения должна быть предварительно скомпилирована перед проведением измерений. Перед каждым новым бенчмаркингом состояние компиляция сбрасывается, чтобы предыдущие запуски не оказывали влияние на последующие. На Android N+ (API 24+) поддерживаются следующие режимы компиляции: Partial — частичная прекомпиляция приложения при наличии Baseline Profile. Это наиболее реалистичный опыт установки приложения на устройство конечного пользователя. Full — полная предварительная компиляция. Режим не отражает реального пользовательского опыта, но может быть использован либо для иллюстрации идеальной производительности, либо для уменьшения шума при компиляции во время выполнения бенчмаркинга. None — приложение вообще не компилируется, минуя компиляцию по умолчанию, которая обычно должна выполняться во время установки. Этот режим иллюстрирует производительность в наихудшем случае. Ignore — игнорирование состояния компиляции. Используется, чтобы оставлять состояние компиляции неизменным при перезапуске тестов. На Android M (API 23) поддерживается только режим Full, все приложения всегда полностью прекомпилированы. Заключительный параметр функции measureRepeated, который мы разберем, — metrics. Метрики — это основной тип информации, извлекаемой из бенчмарков. Они передаются в measureRepeated по одной или списком. Доступные метрики: StartupTimingMetric, FrameTimingMetric и экспериментальные TraceSectionMetric и PowerMetric. 1️⃣ StartupTimingMetric измеряет время запуска приложения, а именно следующие значения: timeToInitialDisplayMs — время с момента получения системой launch интента до рендеринга первого кадра активити timeToFullDisplayMs — время с момента получения системой launch интента до того, как приложение не сообщит о полной отрисовке с помощью метода reportFullyDrawn. Значение доступно только с Android 11 (API level 30) 2️⃣ FrameTimingMetric работает с кадрами. frameOverrunMs — отражает то, на сколько времени кадр опоздал к дедлайну по отрисовке. Положительные числа указывают на пропущенный кадр и видимые лаги, отрицательные числа указывают, насколько быстрее был подготовлен кадр. Доступно только на Android 12 (уровень API 31) и выше. frameDurationCpuMs — отражает то, сколько времени потребовалось для создания кадра на процессоре — как на MainThread, так и на  RenderThread. Измерения собраны в процентили: 50-й, 90-й, 95-й и 99-й. Процентиль (P) — термин из статистики. Так, например, запись frameDurationCpuMs P90 7.9 означает, что 90% кадров отрисовались быстрее 7.9 мс 3️⃣ TraceSectionMetric измеряет время выполнения определенной разработчиком секции кода. Секция определяется самим разработчиком в коде вызовом функции trace(sectionName){} или с помощью статических функций Trace.beginSection(sectionName) и Trace.endSection(). 4️⃣ PowerMetric измеряет потребление power или energy для заданных категорий. Доступные категории: CPU, DISPLAY, GPU, GPS, MEMORY, MACHINE_LEARNING, NETWORK, и UNCATEGORIZED. Значения измерений отражают общесистемное потребление, а не потребление для каждого приложения, и в настоящее время ограничены устройствами Pixel 6 и Pixel 6 Pro.   Итак, мы рассмотрели макробенчмарки. Теперь разберемся с микробенчмарками. Вспомним, что они предназначены для измерения работы быстрых, часто вызываемых функций. Для их реализации также нужно создать специальный microbenchmark-модуль. Генерируем его с помощью встроенного темплейта по аналогии с макробенчмарками.  После помещаем код, который хотим протестировать, в отдельный модуль, назовем его benchmarkable-модуль.  Затем изменяем файл build.gradle microbenchmark-модуля, добавляем зависимость на benchmarkable-модуль, содержащий код для тестирования.  Чтобы создать микробенчмарк, используем класс BenchmarkRule, предоставляемый библиотекой. Тестируемый код передаем в функцию measureRepeated. В отличие от макробенчмарков, в микробенчмарках число выполняемых итераций определяет самой библиотекой, measureRepeated никаких параметров для кастомизации не принимает. Процессы запуска и просмотра результатов микробенчмаркинга аналогичны тем же процессам для макробенчмаркинга, их мы уже успели рассмотреть выше ☝️ Итак, давайте резюмируем то, что успели обсудить сегодня: Бенчмаркинг — тестирование производительности программного кода. Jetpack Benchmark Library предоставляет API для автоматизации измерения производительности кода Android-приложений. Бенчмарк — обыкновенный инструментальный JUnit-тест, который выполняется прямо на устройстве. Macrobenchmark предназначена для бенчмаркинга запуска приложения, взаимодействий с пользователем, манипуляций с интерфейсом, скролла списков, анимаций.  Microbenchmark предназначена для бенчмаркинга часто вызываемых функций, например, measure или layout pass во View, layout inflating, парсинга данных, CPU-вычислений и других фрагментов кода, которые выполняются неоднократно. Любой код, который выполняется нечасто или выполняется по-разному при многократном вызове, может не подходить для микробенчмаркинга. Для реализации как макро-, так и микробенчмарков необходимо создать для них отдельные модули и правильно настроить их конфигурацию в gradle файлах. Для написания бенчмарк-теста необходимо передать вызов тестируемого кода в функцию measureRepeated. В случае макробенчмаркинга функция принимает параметры для кастомизации. Результаты бенчмаркинга отображаются в Android Studio, записываются в json файл, сохраняются в студии и на диске устройства. На этом все. Спасибо, что дочитали до конца 💗 Вы также можете ознакомиться с видео-версией статьи — записью митапа TechnoMeetsDroid. Рады будем видеть вас в наших следующих постах и митапах! Android-разработчица в Технократии Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.    ИТ-Компания