Разработка
Поиск регрессий в Compose
Регресс все еще трудно найти, особенно когда у вас есть сотни, если не тысячи Composable, которые рендерятся каждую секунду.
Программное обеспечение подобно энтропии: ее трудно понять, она ничего не весит и подчиняется второму закону термодинамики, то есть всегда увеличивается, — Норман Огастин.
Отказ от ответственности
Эта статья не о том, как лучше всего обнаружить снижение производительности. Это просто один из подходов, который я использую и который работает для меня. Это решение может подойти вам, а может и не подойти. Как и в случае с любым другим инструментом, важно знать, когда и где его использовать, поэтому подумайте, подходит ли этот метод для вашей ситуации.
Вперед!
Введение
Каждый день в кодовые базы добавляется новый код. Новый код в основном означает большее потребление памяти и большее время выполнения. Это в конечном итоге снижает производительность приложения, если код написан неаккуратно. Эти регрессии можно отлавливать двумя способами: либо проактивно (во время обзоров PR), либо реактивно (после того, как приложение попадет к пользователям и они начнут жаловаться).
Для проактивного выявления регрессии производительности мы можем использовать фреймворк Macrobenchmark. Macrobenchmark выполняет заданный сценарий, например, запуск приложения, нажатие кнопок, прокрутка и т.д., несколько раз и выдает статистическую сводку. Эта сводка сообщает вам число. Число, которое вы можете сравнить с предыдущими, чтобы проверить, есть ли регресс или нет.
Затем наступает черед реактивного способа. В этом случае у вас, скорее всего, нет такой навороченной системы, как Macrobenchmark. Вы знаете, что новая версия приложения имеет регресс производительности, только потому, что либо ваши показатели Frozen Frame/Slow Frame показывают регресс в Play Store, либо ваши пользователи жалуются, что приложение работает медленно и лагает.
В обоих этих сценариях (проактивном и реактивном), «что» именно вызвало регресс, все еще трудно найти, особенно когда у вас есть сотни, если не тысячи Composable, которые рендерятся каждую секунду.
Эта статья НЕ о том, как определить, есть ли регресс или нет. Скорее, этот блог посвящен тому, как легко найти «что» или «какой» компонент вызвал регрессию.
В этой статье мы создадим приложение, добавим регрессии и сравним версию до и после с помощью Perfetto и Diffetto.
Контекст
Что такое Perfetto?
Perfetto — это веб-инструмент для визуализации и изучения файлов трассировки. Он предлагает сервисы и библиотеки для записи трассеров на уровне системы и приложений. Если вы впервые знакомитесь с Perfetto, я рекомендую посмотреть это видео (необязательно). Это одно из лучших видео, в котором рассказывается об основах Perfetto.
Что такое Diffetto?
Diffetto — это крошечный инструмент, который я написал для сравнения двух трейсов Perfetto. В этой статье мы рассмотрим, как его использовать.
Приложение
Чтобы упростить статью, я собираюсь написать композабл Counter
без регрессии (до) и с регрессией (после).
До:
@Composable private fun MyApp() { var count by remember { mutableIntStateOf(0) } // state Text("Count is $count", fontSize = 20.sp) // text Button(onClick = { count++ }) { // button to update state Text("INCREMENT") } AnotherComposable(count) SomeOtherComposable(count) } @Composable fun AnotherComposable(count: Int) { Text("Hello from AnotherComposable -> $count") runBlocking { delay(200) } // simulating existing jank (200ms) } @Composable fun SomeOtherComposable(count: Int) { Text("Hello from SomeOtherComposable -> $count") runBlocking { delay(100) } // simulating existing jank (100ms) }
После:
@Composable private fun MyApp() { var count by remember { mutableIntStateOf(0) } Text("Count is $count", fontSize = 20.sp) runBlocking { delay(1000) } // introducing new regression of 1 second 🔴 Button(onClick = { count++ }) { Text("INCREMENT") } AnotherComposable(count) SomeOtherComposable(count) }
Пожалуйста, помните, что наша цель здесь — не просто найти плохой код, а найти регрессию (новый плохой код).
Трассировка
Настройка фреймворка трассировки Compose и обучение генерации файла трассировки выходит за рамки этой статьи. Официальные руководства довольно хороши и понятны. Вы можете прочитать этот руководство для настройки трассировки и это для генерации файлов трассировки.
Хотя вышеприведенное руководство охватывает основы трассировки, я хотел бы упомянуть несколько моментов, которые очень важны при трассировке Compose.
- Не отслеживайте
debug
сборку. Создайте новый вариантbenchmark
, расширяющий конфигурацию вашей релизной сборки, но без обфускации ProGuard. Причина в том, что Jetpack Compose включает множество тяжелых фич в отладочные сборки из инструментов IDE, а также компилятор выполняет множество оптимизаций в релизной сборке. Если вы профилируетеdebug
сборку, то в итоге вы можете исправить проблемы, которые не являются реальными проблемами, с которыми сталкиваются пользователи, и упустить из виду то, что реально происходит. Дополнительную информацию о настройке варианта бенчмарка можно найти здесь. - Существует множество способов создания файлов трассировки. Вы можете использовать IDE, терминал, а большинство последних устройств Android также поддерживают трассировку в устройстве. Вы также можете использовать фреймворк Macrobenchmark, который сгенерирует файлы трассировки за вас. По моему мнению, самый простой способ — использовать IDE, но есть еще более крутой способ, который лично мне нравится больше всего, а именно команда
record_android_trace
. Эта команда записывает трассеры и открывает их автоматически в perfetto одним щелчком мыши. Это огромная экономия времени. - Не добавляйте зависимость трассировки с помощью
implementation(...)
Используйте сборочную вариацию вызоваimplementation
. Например, используйтеbenchmarkImplementation
. Это позволит избежать утечки зависимости трассировки в релизную сборку, что приведет к увеличению размера APK. - Старайтесь использовать трассировки с небольшой продолжительностью. Это поможет вам быстрее выявить проблему. Длинные трассировки принесут много шума, что затруднит фокусировку на одной проблеме. Например, если вы знаете, что есть проблемы при загрузке страницы, а также при нажатии на кнопку внутри страницы, не объединяйте эти два взаимодействия в одну трассировку. Разбейте их на два отдельных файла трассировки и проанализируйте.
Шаги трассировки
Я настроил трассировку, следуя приведенным выше инструкциям, и теперь мы можем перейти к генерации файлов трассировки. Для создания файла трассировки я выполнил те же шаги, что и в приложениях «до» (лучшая версия) и «после» (регрессированная версия).
- Откройте приложение
- Начните трассировку
- Нажмите кнопку
- Остановить трассировку
Анализ трассировки
На этом этапе у вас должно быть два файла трассировки. Назовем их before.trace
и after.trace
. Теперь откройте их в Perfetto. Для удобства сравнения я обычно открываю их рядом друг с другом следующим образом.
И, как вы можете видеть, мы добавили регрессию.
Теперь вы можете задаться вопросом: «Зачем нам нужен еще один инструмент для поиска компонента? Он хорошо виден на самом скриншоте». Вы правы! В данном случае это действительно так. Но в реальном приложении могут быть сотни, если не больше, компонентов, которые рендерятся одновременно, и очень сложно получить такую визуализацию и сразу увидеть регрессию.
Diffetto
Diffetto — это крошечное веб-приложение (созданное с помощью Compose Web), которое может помочь определить регрессию по двум файлам трассировки. Вот как его использовать.
В Perfetto при выборе региона вы получите «Pivot Table». Скопируйте данные таблицы «до» и «после» следующим образом:
а затем вставьте их в поля Diffetto «до» и «после» соответственно:
После этого нажмите кнопку «Find Diff», и инструмент сгенерирует таблицу следующего вида:
Как вы уже поняли, Diffetto — это не что иное, как инструмент для обработки текста. Он преобразует предоставленный вами текст в таблицу. Но Diffetto — это не просто конвертер текста в таблицу. Он знает некоторые вещи об этих данных трассировки. По сути, он знает об определенных узлах и о том, что каждый из них означает. Эти знания затем используются для фильтров, которые вы видите в правом верхнем углу. Фильтры используются для выявления виновника и уменьшения шума, который присутствует в файле трассировки. Давайте рассмотрим некоторые фильтры.
Фильтры
- Hide framework calls: Скрыть определенные вызовы фреймворка Android, которые присутствуют в большинстве трассировок. Например:
androidx.compose.*
,Choreographer#*
и т.д.
- Ignore line number : Чтобы игнорировать номера строк в узле трассировки, например:
MyApp (MainActivity.kt:44)
будет показан какMainActivity.MyApp
, так что различные использования одного и того же Composable будут объединены в один узел. Другой пример, если у вас естьMyApp (MainActivity.kt:44)
с 100 мс иMyApp (MainActivity.kt:60)
с 150 мс, оба этих узла будут объединены в одну строку сMyApp
, и продолжительность будет 250 мс, аCount
будет равен 2.
Аналогично, другие фильтры также используются для изменения строк для различных случаев использования. Вы можете посмотреть все фильтры здесь. Это очень помогает при работе с файлом трассировки реального приложения, в котором слишком много шума.
Что касается самой таблицы, то по умолчанию она отсортирована по столбцу Diff (ms)
в порядке убывания. Это означает, что самая верхняя регрессия будет первой строкой. Как видно на скриншоте выше, задержка в 2000 мс, которую мы добавили, хорошо видна в столбце Diff
. Также, если вы заметили, для других композитных функций столбец Diff практически равен нулю, что означает отсутствие регрессии. Вы можете управлять упорядочиванием, щелкая по заголовку каждого столбца, и это соответствующим образом перестроит таблицу.
Рекомпозиционная регрессия
С помощью Diffetto вы также можете отлаживать рекомпозиционные регрессии, чтобы найти наиболее перекомпонуемый элемент. Колонки Before count
, After count
и Count diff
(после — до) показывают количество узлов с одинаковыми именами в логе. Это означает, что одна нода = 1 (ре)композиция. Давайте изменим AnotherComposable
так, чтобы в нем появились ненужные рекомпозиции, и посмотрим, как это покажет Diffetto.
@Composable fun AnotherComposable(count: Int) { var anotherState by remember { mutableIntStateOf(0) } // adds a new state LaunchedEffect(count) { anotherState = 0 delay(2000) // wait for parent runBlocking to settle down while (anotherState < 10) { anotherState++ // increments it every 100ms until it reach 10; delay(100) } } Text("Hello from AnotherComposable -> $count $anotherState") runBlocking { delay(200) } }
Теперь таблица Diffetto выглядит следующим образом:
Как видите, дополнительные 10 рекомпозиций, которые мы добавили для AnotherComposable, теперь видны в таблице инструмента.
Заключение
Должен сказать, что хотя Diffetto — это крошечное приложение, его мощь не очень заметна в этом примере.
Чтобы дать вам некоторый контекст, приложение, над которым я работаю, полностью написано на Compose. В первые дни существования Compose, когда у нас не было достаточной поддержки Macrobenchmarking, найти регрессию производительности, когда за секунду отрисовывалось более 200 композабл, было сложно. В такой среде трассировка становится слишком шумной. Diffetto сыграл решающую роль в снижении шума и, в свою очередь, помог быстрее найти виновных. Если у вас похожая ситуация, когда у вас есть две версии приложения и вы не знаете, что в них регрессировало, я бы посоветовал попробовать этот подход. Кстати, Diffetto можно использовать и в не-Compose приложениях.