Разработка
Повышение производительности модульных тестов в MEGA Android
В этом посте рассказывается о наших усилиях по аудиту и изобретению «легкой» тестовой конфигурации — конфигурации с отключением ненужных задач Gradle. Это приводит к ускорению времени компиляции юнит-тестов как на локальном компьютере, так и в CI.
В кодовой базе MEGA Android (клиент для облачного хранилища) есть более 3K юнит-тестов, выполняемых в наших ежедневных конвейерах.
Благодаря поддержке сканированию билдов мы заметили, что каждый раз, когда мы запускаем юнит-тест, система сборки выполняет следующие основные задачи:
- Рисунок 1. Основные задачи при компиляции юнит-тестов
Из всех этих задач компиляция main исходного кода занимает больше всего времени, поскольку она выполняется в той же фазе, что и сборка APK. Это означает, что выполняются все ее подзадачи, например, injectCrashlyticsBuildIds, kapt* и т.д., что может быть очень накладным только для запуска юнит-тестов.
В этом посте рассказывается о наших усилиях по аудиту и изобретению «легкой» тестовой конфигурации — конфигурации с отключением ненужных задач Gradle. Это приводит к ускорению времени компиляции юнит-тестов как на локальном компьютере, так и в CI.
Дисклеймер
Эксперимент проводится на локальной машине со следующими характеристиками:
- Операционная система: macOS 13.5 (aarch64)
- Ядра процессора: 10 ядер (MacBook Pro M1 Max)
- Максимальное количество воркеров Gradle: 10
- Рантайм Java: JetBrains s.r.o. OpenJDK Runtime Environment 17.0.6+0-17.0.6b829.9-10027231
- Java VM: JetBrains s.r.o. OpenJDK 64-Bit Server VM 17.0.6+0-17.0.6b829.9-10027231 (смешанный режим)
- Максимальный размер кучи памяти JVM: 10 ГБ
injectCrashlyticsBuildIds
Начнем с самого простого. Мы используем Crashlytics для отслеживания качества наших приложений, однако его Gradle-задача по умолчанию зарегистрирована в графе сборки, что не вносит никакого вклада в выполнение юнит-тестов. Отключив ее, мы можем легко сэкономить ~10 секунд с помощью следующего скрипта:
tasks.matching { it.name.startsWith("injectCrashlytics") } .configureEach { enabled = false }
kaptKotlin
Kapt печально известен тем, что замедляет сборку из-за самой своей природы — он служит мостом совместимости для запуска APT Java. Kapt часто нарушает инкрементную компиляцию из-за своей чувствительности к изменениям в classpath и склонности к частому повторному запуску. Это означает, что кэш сборки, скорее всего, будет аннулирован каждый раз, когда мы меняем исходники в main
во время итерации тестов, хотя никакой ABI не изменился.
Но в отличие от предыдущей задачи, которую мы можем легко отключить, мы не можем полностью отключить эту задачу Kapt, потому что некоторые из ее подзадач, например, дата биндинг, все еще необходимы для успешной компиляции main
. Вместо этого, лучшее, что мы можем сделать, это провести аудит зарегистрированных библиотек Kapt и удалить ненужные, чтобы Kapt мог эффективно работать во время компиляции юнит-тестов.
Dagger-Hilt
Наши юнит-тесты соответствуют философии тестирования Hilt, где мы непосредственно инстанцируем SUT, передавая фальшивые или мокирующие зависимости, поэтому инъекции Hilt не требуются. При этом конфигурации kapt
и kaptTest
для этой библиотеки не нужны, потому что сгенерированные классы не нужны вообще.
Логика эффективности заключается в том, что если Kapt отключен, то задача генерирования Java-заглушек пропускается. Поскольку Java-заглушки не генерируются, процессор аннотаций не имеет входных данных для обработки, которая обычно занимает раунды для выдачи других Java-источников. Поскольку нет выдачи Java-исходников, задаче javac
приходится компилировать гораздо меньше исходников, что дает огромный выигрыш во времени.
Чтобы доказать этот тезис, мы запустили ./gradlew :app:testDebugUnitTest
, и все тесты прошли.
Google AutoValue
Это просто неиспользуемый kapt в некоторых модулях, о котором мы, вероятно, забыли и который теперь можно удалить после некоторого рефакторинга. Несмотря на отсутствие процессорных трат, отключение неиспользуемого kapt может сэкономить пару секунд из-за времени прогрева конфигурации.
Плагин Hilt
Поскольку капчи для Dagger-Hilt отключились, запуск задач из плагина Hilt не приносит никакого значимого результата. Удалив плагин dagger.hilt.android.plugin
, мы сэкономили ~20 секунд.
kaptUnitTestKotlin
Удалив все конфигурации kaptTest
, мы заметили, что эта задача каким-то образом все еще запущена. Хотя Gradle пометил ее как NO-SOURCE
, что означает отсутствие процессорных ресурсов, она все равно может занимать несколько секунд при каждом запуске.
Мы обратились к сообществу по поводу этого странного поведения, и оказалось, что оно вполне ожидаемо, так как любая задача Kapt расширяет основной плагин kotlin-kapt, как обсуждалось в KT-29481. Меня также направили на эту статью, в которой рассматривается точно такая же проблема. Предлагаемый обходной путь, не нарушающий последующие задачи kotlinc, заключается в том, чтобы просто удалить задачу с помощью скрипта:
tasks.matching { it.name.startsWith("kapt") && it.name.endsWith("TestKotlin") } .configureEach { enabled = false }
Результат
С учетом отфильтрованных задач время компиляции юнит-тестов сократилось до ~2 минут (базовый уровень ~8 минут → +25%) для чистой сборки, до ~40 секунд (базовый уровень ~160 секунд → +25%) для инкрементной сборки и сэкономило ~15 МБ размера сгенерированного класса из Dagger-Hilt. Более подробную информацию вы можете увидеть в результатах нового сканирования и эталонных бенчмарках.
Улучшение при инкрементной сборке зависит от количества/типа измененных модулей. Например, мы наблюдаем улучшение на ~40 секунд при модификации только модуля :app.
Заключение
Получив такие положительные результаты, мы продолжили наблюдать, не вносит ли конфигурация регрессию, но все тесты по-прежнему работают нормально. Это означает, что «легкая» конфигурация — это абсолютная победа с нулевыми потерями!
Что касается следующего улучшения, то мы с нетерпением ждем полного удаления задач kapt при компиляции юнит-тестов, как только мы отрефакторим использование data binding к view binding или compose.
-
Интегрированные среды разработки2 недели назад
Лучшая работа с Android Studio: 5 советов
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2024.43
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2024.44
-
Исследования2 недели назад
Поможет ли новая архитектура React Native отобрать лидерство у Flutter в кроссплатформенной разработке?