Начиная с версий Gradle 4.7 и Kotlin 1.3.30 появилась возможность ускорить инкрементную сборку проекта за счёт корректной работы инкрементной обработки аннотаций.
В этой статье мы рассмотрим, как в теории работает модель инкрементной компиляции в Gradle, что нужно сделать, чтобы раскрыть весь ее потенциал (без потери генерации кода) и какого увеличения скорости инкрементных сборок можно добиться за счет активация дополнительной обработки аннотаций на практике.
Как работает инкрементная компиляция?
Инкрементные сборки в Gradle реализованы на двух уровнях.Первый уровень — отменить начало перекомпиляции модуля с помощью компиляция уклонений .
Второй — непосредственно инкрементная компиляция, запускающая компилятор в рамках одного модуля только на тех файлах, которые изменились или имеют прямую зависимость от измененных файлов.
Давайте рассмотрим предотвращение компиляции на примере (взятом из статьи от Gradle) проект из трёх модулей: приложение , основной И утилиты .
Класс основного модуля приложение (зависит от основной ):
В модуле основной (зависит от утилиты ):public class Main { public static void main(String. args) { WordCount wc = new WordCount(); wc.collect(new File(args[0]); System.out.println("Word count: " + wc.wordCount()); } }
public class WordCount {
// .
void collect(File source) {
IOUtils.eachLine(source, WordCount::collectLine);
}
}
В модуле утилиты :
public class IOUtils {
void eachLine(File file, Callable<String> action) {
try {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// .
}
} catch (IOException e) {
// .
}
}
}
Порядок первой компиляции модулей следующий (в соответствии с порядком зависимостей): 1) утилиты 2) основной 3) приложение Теперь давайте посмотрим, что произойдет, если мы изменим внутреннюю реализацию класса IOUtils:
public class IOUtils { // IOUtils lives in project `utils`
void eachLine(File file, Callable<String> action) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8") )) {
// .
}
} catch (IOException e) {
// .
}
}
}
Это изменение не влияет на ABI модуля.
ABI (Application Binary Interface) — двоичное представление публичного интерфейса собранного модуля.
В случае, когда изменение касается только внутренней реализации модуля и никак не влияет на его публичный интерфейс, Gradle воспользуется функцией предотвращения компиляции и начнет перекомпилировать только модуль.
утилиты .
Если затронут ABI модуля утилиты (например, появится дополнительный публичный метод или изменится сигнатура уже существующего), то дополнительно запустится перекомпиляция мода основной , но в то же время в зависимости от основной модуль приложение не будет перекомпилироваться транзитивно, если зависимость в нем подключена через выполнение .
Иллюстрация предотвращения компиляции на уровне модуля проекта Второй уровень инкрементности — это инкрементность на уровне запуска компилятора для измененных файлов непосредственно внутри отдельных модулей.
Например, добавим в модуль новый класс основной : public class NGrams { // NGrams lives in project `core`
// .
void collect(String source, int ngramLength) { collectInternal(StringUtils.sanitize(source), ngramLength); } // .
}
И в утилиты : public class StringUtils {
static String sanitize(String dirtyString) { .
}
}
В этом случае в обоих модулях необходимо перекомпилировать всего два новых файла (не затрагивая существующие и неизмененные WordCount и IOUtils), так как между новым и старым классами нет никаких зависимостей.
Таким образом, инкрементальный компилятор анализирует зависимости между классами и перекомпилирует только:
- классы, содержащие изменения
- классы, которые напрямую зависят от измененных классов
Инкрементная обработка аннотаций
Что еще хуже, долгое время обработка аннотаций фундаментально нарушала возможности инкрементальной компиляции Gradle. Каждый обработчик аннотаций в проекте сообщает компилятору информацию о списке обрабатываемых им аннотаций.
Но с точки зрения сборки обработка аннотаций — это черный ящик: Gradle не знает, что будет делать процессор, в частности, какие файлы он будет генерировать и где.
Вплоть до Gradle 4.7 инкрементальная компиляция автоматически отключалась в тех наборах исходного кода, которые использовали процессоры аннотаций.
С выпускать Инкрементная компиляция Gradle 4.7 теперь поддерживает обработку аннотаций, но только для APT. KAPT поддерживает инкрементальную обработку аннотаций.
появился с Котлином 1.3.30. Чтобы это работало, вам также нужно поддерживать из библиотек, предоставляющих обработчики аннотаций.
Разработчики процессоров аннотаций теперь имеют возможность явно устанавливать категорию процессора, тем самым сообщая Gradle информацию, необходимую для работы инкрементной компиляции.
Категории процессоров аннотаций
Gradle поддерживает две категории процессоров: изоляция — такие процессоры должны принимать все решения по генерации кода только на основе информации из АСТ , который связан с определенным элементом аннотации.Это самая быстрая категория обработчиков аннотаций, поскольку Gradle может избежать перезапуска процессора и использовать файлы, которые он уже сгенерировал, если в исходном файле нет изменений.
Агрегирование — используется для процессоров, которые принимают решения на основе нескольких входных данных (например, анализируя аннотации в нескольких файлах одновременно или на основе проверки AST, транзитивно достижимого из аннотированного элемента).
Gradle будет запускать процессор каждый раз для файлов, использующих агрегирующие аннотации процессора, но не будет перекомпилировать генерируемые им файлы, если в них нет изменений.
Для многих популярных библиотек, основанных на генерации кода, поддержка инкрементальной компиляции уже реализована в последних версиях.
Вы можете увидеть список библиотек, которые его поддерживают. Здесь .
Наш опыт внедрения инкрементной обработки аннотаций
Теперь для проектов, которые начинаются с нуля и используют последние версии библиотек и плагинов Gradle, инкрементные сборки, скорее всего, будут активны по умолчанию.Но пошаговая обработка аннотаций может обеспечить наибольшую долю увеличения производительности сборки в крупных и долгосрочных проектах.
В этом случае может потребоваться массовое обновление версии.
Стоит ли оно того на практике? Давайте взглянем! Итак, чтобы инкрементная обработка аннотаций работала, нам нужно:
- Градл 4.7+
- Котлин 1.3.30+
- Все обработчики аннотаций в нашем проекте должны его поддерживать.
Это очень важно, поскольку если в конкретном модуле хотя бы один процессор не поддерживает инкрементальность, то Gradle отключит ее для всего модуля.
Все файлы в модуле каждый раз будут компилироваться заново! Один из альтернативных вариантов получения поддержки инкрементной компиляции без обновления версий — вынести весь код, использующий обработчики аннотаций, в отдельный модуль.
В модулях, не имеющих обработчиков аннотаций, инкрементная компиляция будет работать нормально.
Если Gradle был вынужден отключить инкрементальную обработку аннотаций для конкретного модуля, то в журнале сборки мы увидим сообщение о том, на каких процессорах и в каких модулях это происходит (см.
название задачи): > Task :common:kaptDebugKotlin
w: [kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: toothpick.compiler.factory.FactoryProcessor (NON_INCREMENTAL), toothpick.compiler.memberinjector.MemberInjectorProcessor (NON_INCREMENTAL).
В нашем проекте библиотек с обработчиками неинкрементных аннотаций их оказалось 3:
- Зубочистка
- Комната
- РазрешенияДиспетчер
Причём все обработчики аннотаций в последних версиях этих библиотек имеют оптимальную категорию — изолирующие.
В процессе поднятия версий нам пришлось столкнуться с рефакторингом из-за изменений в API библиотеки Toothpick, которые затронули практически каждый наш модуль.
Но в данном случае нам повезло, и мы смогли провести рефакторинг полностью автоматически с помощью автозамены названий используемых методов публичной библиотеки.
Обратите внимание: если вы используете библиотеку Room, вам нужно будет явно передать флаг комната.
инкрементальный: правда процессор аннотаций.
Пример .
В будущем разработчики Room Планируете включите этот флаг по умолчанию.
Для версий Kotlin 1.3.30-1.3.50 необходимо включить поддержку инкрементной обработки аннотаций.
очевидно через kapt.incremental.apt=истина в файле gradle.properties проекта.
Начиная с версии 1.3.50 для этого параметра по умолчанию установлено значение true.
Профилирование дополнительных сборок
После того как все необходимые зависимости версионированы, пришло время проверить скорость инкрементных сборок.Для этого мы использовали следующий набор инструментов и приемов:
- Сканирование сборки Gradle
- Gradle-профилировщик
- Для запуска скриптов с включенной и отключенной инкрементной обработкой аннотаций использовалось свойство gradle. kapt.incremental.apt=[истина|ложь]
- Для стабильных и информативных результатов сборки поднимались в отдельной CI-среде.
Инкрементность сборки была воспроизведена с помощью gradle-profiler.
Было составлено 4 сценария, исходя из следующих условий:
- Изменение файла влияет/не влияет на его ABI.
- Поддержка дополнительной обработки аннотаций включена/выключена.
- Перезапуск демона Gradle
- Выполнение разминочной сборки
- Запуск 10 инкрементных сборок, перед каждой из которых файл изменяется путем добавления нового метода (приватного для изменений без ABI и публичного для изменений ABI).
В этом файле используется аннотация изолирующего процессора.
Также стоит отметить, что тесты проводились на двух задачах Gradle: компилироватьDebugSources И сборкаОтладка .
Первый только начинает компилировать файлы исходного кода, не производя никакой работы с ресурсами и не связывая приложение в файл .
apk. Учитывая тот факт, что инкрементная компиляция затрагивает только файлы .
kt и .
java, задача компилироватьDedugSource был выбран для более изолированного и быстрого сравнительного анализа.
В реальных условиях разработки при перезапуске приложения Android Studio использует задачу сборкаОтладка , который включает в себя полную генерацию отладочной версии приложения.
Результаты тестов
На всех следующих графиках, созданных gradle-profiler, вертикальная ось — это дополнительное время сборки в миллисекундах, а горизонтальная ось — номер запуска сборки.
:compileDebugSource перед обновлением обработчиков аннотаций
Среднее время выполнения каждого сценария составляло 38 секунд до обновления процессоров аннотаций до инкрементальных версий.
В этом случае Gradle отключает поддержку инкрементальной компиляции, поэтому существенной разницы между сценариями нет.
:compileDebugSource после обновления обработчиков аннотаций
Сценарий | Постепенное изменение ABI | Неинкрементное изменение ABI | Постепенное изменение, не связанное с ABI | Неинкрементное изменение без ABI |
---|---|---|---|---|
иметь в виду | 23978 | 35370 | 23514 | 34602 |
медиана | 23879 | 35019 | 23424 | 34749 |
мин | 22618 | 33969 | 22343 | 33292 |
Макс | 26820 | 38097 | 25651 | 35843 |
стандартное отклонение | 1193.29 | 1240.81 | 888.24 | 815.91 |
:assembleDebug после обновления обработчиков аннотаций
Сценарий | Постепенное изменение ABI | Неинкрементное изменение ABI | Дополнительные изменения, не связанные с ABI | Неинкрементное изменение без ABI |
---|---|---|---|---|
иметь в виду | 39902 | 49850 | 39005 | 52123 |
медиана | 38974 | 49691 | 38713 | 50336 |
мин | 38563 | 48782 | 38233 | 48944 |
Макс | 48255 | 52364 | 41732 | 65941 |
стандартное отклонение | 2953.28 | 1011.20 | 1015.37 | 5039.11 |
Анатомия сборки в Gradle Build Scan
Для более глубокого понимания того, как были достигнуты преимущества при инкрементной компиляции, давайте сравним сканы инкрементных и неинкрементных сборок.В случае отключения инкрементальности KAPT большая часть времени сборки занимает компиляция модуля приложения, который нельзя распараллелить с другими задачами.
График неинкрементального KAPT выглядит следующим образом:
В этом случае выполнение задачи :kaptDebugKotlin нашего модуля приложения занимает около 8 секунд. Временная шкала для случая с включенной инкрементальностью KAPT:
Теперь модуль приложения перекомпилировался менее чем за секунду.
Стоит обратить внимание на визуальную несоизмеримость масштабов двух сканов на картинках выше.
Задачи, которые кажутся короче на первом изображении, не обязательно требуют больше времени для выполнения на втором изображении, где они кажутся длиннее.
Но явно заметно, насколько уменьшилась доля перекомпиляции app-модуля при включении инкрементального KAPT. В нашем случае мы выигрываем около 8 секунд на этом модуле и дополнительно 2 секунды на модулях меньшего размера, которые компилируются параллельно.
При этом общая продолжительность выполнения всех задач *kapt при отключенной инкрементальности обработки аннотаций составит 1 минуту 36 секунд против 55 секунд при ее включении.
То есть без учета параллельности сборки модулей выигрыш более существенен.
Также стоит отметить, что приведенные выше результаты тестов были подготовлены в среде CI с возможностью запуска 24 параллельных потоков для сборки.
В 8-поточной среде выигрыш от включения инкрементной обработки аннотаций в нашем проекте составляет около 20–30 секунд.
Инкрементальный или (?) параллельный
Еще один способ значительно ускорить сборку (как инкрементальную, так и чистую) — параллельное выполнение задач gradle путем разделения проекта на большое количество слабосвязанных модулей.Так или иначе, модульность имеет гораздо больший потенциал для ускорения сборки, чем использование инкрементального KAPT. Но чем монолитнее проект и чем больше в нем используется генерация кода, тем большая доля прироста будет приходиться на инкрементальную обработку аннотаций.
Легче получить эффект от полных инкрементальных сборок, чем разбивать приложение на модули.
Однако оба подхода не противоречат и прекрасно дополняют друг друга.
Нижняя граница
- Включение инкрементальной обработки аннотаций в нашем проекте позволило нам добиться увеличения скорости локальных перестроек на 20%.
- Чтобы включить инкрементную обработку аннотаций, будет полезно просмотреть полный журнал текущих сборок и найти предупреждающие сообщения с текстом «Запрошена инкрементальная обработка аннотаций, но поддержка отключена, поскольку следующие процессоры не являются инкрементными.
».
Необходимо обновить версии библиотеки до версий, поддерживающих инкрементную обработку аннотаций и имеющих версии Gradle 4.7+, Kotlin 1.3.30+.
Материалы и что почитать по теме
- О поддержке поэтапной обработки аннотаций на уровне плагина Gradle Java
- Статья о gradle-профайлере
- Узнайте больше о возможностях KAPT
- Доклад на Google I/O 2019 с текущими приемами ускорения сборки
- Еще один отчет об оптимизации Gradle на Google I/O 2017, включая материалы по инкрементным сборкам и предотвращению компиляции.
-
Антарктида
19 Oct, 24