- Benchmarking your Gradle builds
- Introducing Gradle Profiler
- Moving to the cloud
- Problems
- What’s next
- Уменьшаем время сборки ваших Android-проектов
- Прежде всего!
- Шаг № 1: Обновите инструменты
- Шаг № 2: Обновите конфигурации
- (Инкрементная обработка аннотаций (с 1.3.30)
- Шаг № 3: свойства Gradle
- Заключительные замечания
Benchmarking your Gradle builds
Build times have historically been a problem in Android, with engineers usually being happy if they “only” wait a couple of minutes when making a small change and pushing it to the device, while longer waiting times are typical.
The more time you spend waiting for something to build the less time you actually spend on engineering. Some people end up moving to another task while waiting for a build, but that means that they pay the price of context-switching, and if that other task is checking the /r/androiddev subreddit, well we all know how long that takes 🙂
Here at Babylon, the Android build takes around 2.5 minutes for a clean build, with incremental builds taking a bit less than a minute. These times are for builds that run on Mainframer build servers, with dev machine builds taking slightly longer.
For our team of 17 engineers, a small increase of 10 seconds for a build has the following impact:
10 seconds x 17 engineers x 20 builds a day x 230 working days per year (ish) = 782,000 seconds or about 27 days!
Those 27 days could have been spent better, refactoring some screens, or introducing a cool new technology rather than waiting!
To combat this situation we wanted to make it easier for everyone to understand and measure how build configuration changes (e.g. enabling dex-in-process for our project) or application modularization can affect our builds times for better or worse. Understanding your build times is not as easy as it sounds:
- You need to run a build many times to get an average, which means you need to get the calculator out and add the numbers yourself 😅.
- Typically you need to run this on your dev machine.
- Using your dev machine for anything while benchmarking might affect the results.
- Any unrelated process running in the background might affect the results as well.
Introducing Gradle Profiler
Looking around we quickly found the Gradle Profiler project. With this tool, you can do 2 things, benchmark and profile a build.
From the library documentation:
“Benchmarking simply records the time it takes to execute your build several times and calculates a mean and standard error for it. It has zero impact on the execution time, so it is ideal for making before/after comparisons for new Gradle versions or changes to your build”.
While for profiling:
“Profiling allows you to get deeper insight into the performance of your build. The app will run the build several times to warm up a daemon, then enable the profiler and run the build.”
So to sum things up, if you are after timings for your Gradle build then you should run your build against the tool using the benchmark option, while if you want to get insights about the CPU utilisation, memory allocation, etc. then you should run it with the profile option. For this article, we’ll focus on benchmarking as we are after some timings for our builds, and not really insights on why things take as long as they do (without saying that this is not an interesting insight as well).
To run a benchmark, you first need to check out the gradle-profiler project and run the following command:
which will install the gradle-profiler executable into ./build/install/gradle-profiler/bin . The next step is to run the following:
where is the directory containing the build to be benchmarked, and is the name of the task to run, exactly as you would use for the gradle command. The results of this benchmark will be returned in 2 different formats, a CSV file containing all the timings along with some basic stats around it like the mean, median, standard deviation, etc., and an HTML file with a plotted graph for these timings.
Some interesting configuration options that the tool provides are the following:
- The ability to define how many times you want your builds to run.
- The number of warm-up builds to complete before running the measured builds.
- Complex build scenarios, for example, you can ask the gradle-profiler to run incremental builds, adding a public method to a Java or Kotlin source file, or adding a string to an Android resource file.
- Overriding system properties, which, for example, can be used to disable the build cache.
You can see the comprehensive list of options in the README file of the project.
By using this tool we have solved the first problem of running the build multiple times and calculating the average. However, we are still running gradle-profiler locally, making the dev machine unusable for the duration of the benchmarking.
Moving to the cloud
What we ended up doing to achieve what we wanted is to write a small Jenkins job to do the following:
- Given 2 branch names start up a Jenkins node in AWS
- Check out the gradle-profiler project and build it
- Benchmark and report against two provided branches (sequentially)
- Generate reports for both of these.
The benefits of this are clear:
- You can continue to use your computer, the benchmarking will run on AWS, and you’ll get the results after a while; how long depends on how complex the build scenarios you define are, and how many iterations you perform. For us, this usually takes about 2 hours.
- The gradle-profiler tool is essentially abstracted, making this very easy to run, with sensible defaults scenarios.
So after implementing this, any of the Android engineers in the team can go into our Jenkins portal and run a comparison between 2 branches, to get a sense of what kind of benefit a build configuration or application modularization can bring to the project. So far, using gradle-profiler on Jenkins, we have found the following:
- Updating the min SDK in dev builds from 19 to 23 improved build times by about 15%.
- Reducing our 40 SDK modules to 5 has no impact on build times that impact developer builds.
- Detekt runs faster when added to individual modules rather than run once across all modules, because of parallel execution.
Problems
I should note that we initially experienced some issues with the precision of this approach, as running a benchmark comparing the develop branch to develop reported back differences in build times of around 10%. We investigated whether the CPU cycles allocated to our AWS node instance were fluctuating, but the issue disappeared, and before we knew it we were getting up to 1% discrepancy between 2 identical builds, which I would say is acceptable.
What’s next
As a next step, we want to attribute build time regressions to specific commits. We are already using Gradle Enterprise so have some history of our build times across time, however, these are inconsistent as our usual CI builds don’t have a warm-up phase.
To that extent, we are planning to use the gradle-profiler running on a nightly build on our develop branch to alert us when the build times increase over a certain threshold. That would then be a great starting point for someone to investigate the commits of the day.
Источник
Уменьшаем время сборки ваших Android-проектов
Доброе утро! Начинаем понедельник с материала, перевод которого подготовлен специально для студентов курса «Android-разработчик. Продвинутый курс».
Недавно я переносил кодовую базу Android в Kure на AndroidX. Мне показалось, что это прекрасная возможность поработать над скоростью сборки проекта. У Gradle всегда была плохая репутация из-за медлительности и ресурсоемкости, но я был очень удивлен, что незначительные изменения в конфигурации сборки могут так значительно увеличить ее скорость.
Посмотрите на показатели сканирования сборки до/после оптимизации
до оптимизации
после оптимизации ️️
Снизились с 5,5 минут до 17 секунд?? С ума сойти!
Не так уж и сложно переусердствовать с оптимизацией, чтобы еще больше сократить время сборки. Но для того, чтобы пост был понятен начинающим, я намеренно сосредоточусь на незначительных, безболезненных мерах, которые я предпринял, чтобы приблизиться к такому показателю.
Прежде всего!
Перед тем, как начать оптимизацию, важно протестировать наш проект, чтобы узнать, сколько времени требуется на его сборку. Gradle имеет удобную опцию сканирования, которую вы можете использовать для анализа производительности вашей задачи. Запустите терминал в Android Studio и выполните следующую команду:
После успешного завершения сборки вам будет предложено принять условия обслуживания, чтобы загрузить результаты сканирования. Введите yes, чтобы продолжить. После завершения публикации вы получите ссылку на терминал для проверки сканирования. Откройте ее.
На сайте есть довольно много опций, но для краткости мы рассмотрим только то, что является наиболее важным.
В Summary отображается сводная информация о выполненных задачах и времени их выполнения. Но что нас здесь интересует, так это раздел Performance. Он делает более подробную разбивку общего времени сборки, как показано ниже.
В разделе Performance есть вкладка Settings and suggestions, в которой приведены рекомендации по улучшению скорости сборки. Давайте посмотрим на них.
В этом разделе мы можем найти несколько простых исправлений для повышения скорости. Итак, давайте продолжим и применим эти исправления в нашем проекте.
Шаг № 1: Обновите инструменты
Команда Android постоянно совершенствует и развивает систему сборки. Таким образом, в большинстве случаев вы можете получить значительное улучшение, просто установив последнюю версию инструментария.
Во время этого рефакторинга наш проект был на версии 3.2.1 плагина Gradle для Android Studio (на несколько версий старше, чем последний выпуск).
Вы можете перейти по этой ссылке, чтобы получить последнюю версию Gradle Plugin. На момент написания этого поста последней была версия 3.4.0.
Но здесь есть подвох, о котором мы должны помнить:
(Примечание: При использовании Gradle версии 5.0 или выше размер памяти демона Gradle по умолчанию уменьшается с 1 ГБ до 512 МБ. Это может привести к снижению производительности сборки. Чтобы переопределить этот параметр по умолчанию, укажите размер памяти для демона Gradle в файле gradle.properties вашего проекта.)
При использовании Gradle 5.0 и выше нам нужно будет явно увеличить размер памяти, чтобы скорость нашей сборки не ухудшилась. Мы вернемся к этому через минуту.
Откройте файл build.gradle верхнего уровня, который вы найдете в корне вашего проекта, и добавьте следующую строку в раздел зависимостей:
Вам также необходимо обновить distribution URL-адрес в файле свойств Gradle Wrapper, который находится по адресу gradle/wrapper/gradle-wrapper.properties . Обновите URL-адрес до следующего.
Вы столкнетесь с ошибкой при использовании Kotlin, если версия плагина Kotlin Gradle меньше 1.3.0. Если это так, воспользуйтесь подсказкой IDE, чтобы обновить Gradle плагин для Kotlin до последней версии (на момент написания этой статьи это версия 1.3.31).
Хорошо, давайте снова запустим сборку из терминала, чтобы посмотреть, добились ли мы каких-нибудь улучшений.
Шаг № 2: Обновите конфигурации
Итак, мы смогли срезать около 2,5 минут от времени сборки, но это все еще недостаточно хорошо. Изучив логи сборки в терминале, я наткнулся на одну строку, которая нас заинтересует:
Gradle может отключить инкрементную компиляцию, так как следующие процессоры аннотаций не являются инкрементными: butterknife-compiler-10.1.0.jar (com.jakewharton:butterknife-compiler:10.1.0), dagger-compiler-2.9.jar (com.google.dagger:dagger-compiler:2.9).
Рассмотрите возможность установки экспериментального флага android.enableSeparateAnnotationProcessing-true в файле gradle.properties для запуска обработки аннотаций в отдельной задаче и выполнения инкрементной компиляции.)
Инкрементная компиляция в основном предотвращает расточительную компиляцию всего набора исходных файлов и вместо этого компилирует только те файлы, которые были изменены. Из логов видно, что мы не пользуемся этой функцией. Он предлагает нам использовать android.enableSeparateAnnotationProcessing=true , но, в любом случае, мы не должны использовать конфигурацию «annotationProcessor» поскольку в нашем проекте используется Kotlin.
К счастью, в версии 1.3.30 Kotlin добавлена поддержка пошаговой обработки аннотаций.
(Инкрементная обработка аннотаций (с 1.3.30)
Начиная с версии 1.3.30, kapt поддерживает инкрементную обработку аннотаций в качестве экспериментальной функции. В настоящее время обработка аннотаций может выполняться инкрементально, только если все используемые процессоры аннотаций являются инкрементными.
Чтобы включить инкрементную обработку аннотаций, добавьте эту строку в файл gradle.properties :
Обратите внимание, что инкрементная обработка аннотаций требует, чтобы инкрементная компиляция также была включена.)
- 1. Измените конфигурацию annotationProcessor на kapt
- 2. Включите экспериментальный флаг инкрементной обработки аннотации
Откройте файл build.gradle уровня вашего модуля и добавьте следующую строку в начало файла:
apply plugin: ‘kotlin-kapt’
Затем измените все конфигурации annotationProcessor в разделе зависимостей для использования kapt. Например:
//До
annotationProcessor ‘com.google.dagger:dagger-compiler:2.9’
//После
kapt ‘com.google.dagger:dagger-compiler:2.9’
Теперь откройте файл gradle.properties, расположенный в корне вашего проекта, и добавьте следующую строку:
Давайте снова запустим сборку.
Хорошо, похоже, мы еще немного продвинулись.
Шаг № 3: свойства Gradle
Мы на последнем этапе. Помните подвох, с которым мы столкнулись при обновлении версии плагина Gradle? Оказывается, более новые версии Gradle уменьшают размер используемой памяти до 512 МБ. Это сделано для того, чтобы слабые машины не расходовали слишком много памяти. У меня компьютер с 16 гигабайтами оперативной памяти, поэтому я могу позволить себе скормить около 2-3 гигов демону Gradle, но ваши цифры могут отличаться.
Откройте файл gradle.properties, расположенный в корне вашего проекта, и добавьте следующую строку. Не забудьте выбрать размер в соответствии с вашими требованиями и спецификацией компьютера.
org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
Пока мы это делаем, давайте также включим параллельные сборки и настройку по требованию в свойствах.
Вот как выглядит мой окончательный вариант файла gradle.properties :
- org.gradle.parallel — этот флаг позволяет Gradle собирать модули внутри проекта параллельно, а не последовательно. Это полезно только для многомодульных проектов.
- org.gradle.configureondemand — этот флаг настраивает только те модули, которые необходимы для проекта, а не собирает их все.
Сделав это, давайте посмотрим, какие у нас теперь показатели скорости сборки:
Заключительные замечания
Это ни в коем случае не обширный охват всех способов оптимизации скорости сборки. Есть множество других вещей, которые я не рассмотрел в этом посте, таких как использование minSdk 21 при использовании MultiDex, предварительная индексация библиотек, отключение сжатия PNG и т. д., — это всего лишь некоторые из них.
Но большинство из этих конфигураций требуют более глубокого понимания системы сборки Android и опыта работы с крупными многомодульными проектами (где преимущества наиболее очевидны). Шаги, которые я упомянул выше, легко внедряются в проект даже джуниор разработчиками и имеют значительные выгоды. Я надеюсь, что это поможет вам увеличить скорость сборки!
Источник