Дополнительная Обработка Аннотаций Для Ускорения Сборки Gradle



Дополнительная обработка аннотаций для ускорения сборки Gradle

Начиная с версий 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 модуля утилиты (например, появится дополнительный публичный метод или изменится сигнатура уже существующего), то дополнительно запустится перекомпиляция мода основной , но в то же время в зависимости от основной модуль приложение не будет перекомпилироваться транзитивно, если зависимость в нем подключена через выполнение .



Дополнительная обработка аннотаций для ускорения сборки Gradle

Иллюстрация предотвращения компиляции на уровне модуля проекта Второй уровень инкрементности — это инкрементность на уровне запуска компилятора для измененных файлов непосредственно внутри отдельных модулей.

Например, добавим в модуль новый класс основной :

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

Генерация кода с использованием APT и KAPT сокращает время, необходимое для написания и отладки шаблонного кода, но обработка аннотаций может значительно увеличить время сборки.

Что еще хуже, долгое время обработка аннотаций фундаментально нарушала возможности инкрементальной компиляции 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 отключит ее для всего модуля.

    Все файлы в модуле каждый раз будут компилироваться заново! Один из альтернативных вариантов получения поддержки инкрементной компиляции без обновления версий — вынести весь код, использующий обработчики аннотаций, в отдельный модуль.

    В модулях, не имеющих обработчиков аннотаций, инкрементная компиляция будет работать нормально.

Чтобы обнаружить процессоры, не удовлетворяющие последнему условию, можно запустить сборку с флагом -Pkapt.verbose=истина .

Если 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.

gradle-profiler позволяет декларативную подготовку сценарии для инкрементных тестов сборки.

Было составлено 4 сценария, исходя из следующих условий:

  • Изменение файла влияет/не влияет на его ABI.
  • Поддержка дополнительной обработки аннотаций включена/выключена.

Запуск каждого из сценариев представляет собой последовательность действий:
  • Перезапуск демона Gradle
  • Выполнение разминочной сборки
  • Запуск 10 инкрементных сборок, перед каждой из которых файл изменяется путем добавления нового метода (приватного для изменений без ABI и публичного для изменений ABI).

Все сборки были выполнены с помощью Gradle 5.4.1. Файл, участвующий в изменениях, принадлежит одному из основных модулей проекта (common), от которого 40 модулей (включая core- и Feature-) имеют прямую зависимость.

В этом файле используется аннотация изолирующего процессора.

Также стоит отметить, что тесты проводились на двух задачах Gradle: компилироватьDebugSources И сборкаОтладка .

Первый только начинает компилировать файлы исходного кода, не производя никакой работы с ресурсами и не связывая приложение в файл .

apk. Учитывая тот факт, что инкрементная компиляция затрагивает только файлы .

kt и .

java, задача компилироватьDedugSource был выбран для более изолированного и быстрого сравнительного анализа.

В реальных условиях разработки при перезапуске приложения Android Studio использует задачу сборкаОтладка , который включает в себя полную генерацию отладочной версии приложения.



Результаты тестов

На всех следующих графиках, созданных gradle-profiler, вертикальная ось — это дополнительное время сборки в миллисекундах, а горизонтальная ось — номер запуска сборки.



:compileDebugSource перед обновлением обработчиков аннотаций



Дополнительная обработка аннотаций для ускорения сборки Gradle

Среднее время выполнения каждого сценария составляло 38 секунд до обновления процессоров аннотаций до инкрементальных версий.

В этом случае Gradle отключает поддержку инкрементальной компиляции, поэтому существенной разницы между сценариями нет.

:compileDebugSource после обновления обработчиков аннотаций



Дополнительная обработка аннотаций для ускорения сборки Gradle

Сценарий Постепенное изменение 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
Среднее сокращение времени сборки за счет инкрементности составило 31 % для изменений ABI и 32,5 % для изменений, не связанных с ABI. В абсолютном выражении около 10 секунд.

:assembleDebug после обновления обработчиков аннотаций



Дополнительная обработка аннотаций для ускорения сборки Gradle

Сценарий Постепенное изменение 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
При создании полной отладочной версии приложения в нашем проекте среднее сокращение времени сборки за счет инкрементальности составило 21,5 % для изменений ABI и 23 % для изменений, не связанных с ABI. В абсолютных цифрах примерно те же 10 секунд, поскольку инкрементальность компиляции исходного кода не влияет на скорость сборки ресурса.



Анатомия сборки в Gradle Build Scan

Для более глубокого понимания того, как были достигнуты преимущества при инкрементной компиляции, давайте сравним сканы инкрементных и неинкрементных сборок.

В случае отключения инкрементальности KAPT большая часть времени сборки занимает компиляция модуля приложения, который нельзя распараллелить с другими задачами.

График неинкрементального KAPT выглядит следующим образом:

Дополнительная обработка аннотаций для ускорения сборки Gradle

В этом случае выполнение задачи :kaptDebugKotlin нашего модуля приложения занимает около 8 секунд. Временная шкала для случая с включенной инкрементальностью KAPT:

Дополнительная обработка аннотаций для ускорения сборки Gradle

Теперь модуль приложения перекомпилировался менее чем за секунду.

Стоит обратить внимание на визуальную несоизмеримость масштабов двух сканов на картинках выше.

Задачи, которые кажутся короче на первом изображении, не обязательно требуют больше времени для выполнения на втором изображении, где они кажутся длиннее.

Но явно заметно, насколько уменьшилась доля перекомпиляции app-модуля при включении инкрементального KAPT. В нашем случае мы выигрываем около 8 секунд на этом модуле и дополнительно 2 секунды на модулях меньшего размера, которые компилируются параллельно.

При этом общая продолжительность выполнения всех задач *kapt при отключенной инкрементальности обработки аннотаций составит 1 минуту 36 секунд против 55 секунд при ее включении.

То есть без учета параллельности сборки модулей выигрыш более существенен.

Также стоит отметить, что приведенные выше результаты тестов были подготовлены в среде CI с возможностью запуска 24 параллельных потоков для сборки.

В 8-поточной среде выигрыш от включения инкрементной обработки аннотаций в нашем проекте составляет около 20–30 секунд.

Инкрементальный или (?) параллельный

Еще один способ значительно ускорить сборку (как инкрементальную, так и чистую) — параллельное выполнение задач gradle путем разделения проекта на большое количество слабосвязанных модулей.

Так или иначе, модульность имеет гораздо больший потенциал для ускорения сборки, чем использование инкрементального KAPT. Но чем монолитнее проект и чем больше в нем используется генерация кода, тем большая доля прироста будет приходиться на инкрементальную обработку аннотаций.

Легче получить эффект от полных инкрементальных сборок, чем разбивать приложение на модули.

Однако оба подхода не противоречат и прекрасно дополняют друг друга.



Нижняя граница

  • Включение инкрементальной обработки аннотаций в нашем проекте позволило нам добиться увеличения скорости локальных перестроек на 20%.

  • Чтобы включить инкрементную обработку аннотаций, будет полезно просмотреть полный журнал текущих сборок и найти предупреждающие сообщения с текстом «Запрошена инкрементальная обработка аннотаций, но поддержка отключена, поскольку следующие процессоры не являются инкрементными.

    ».

    Необходимо обновить версии библиотеки до версий, поддерживающих инкрементную обработку аннотаций и имеющих версии Gradle 4.7+, Kotlin 1.3.30+.



Материалы и что почитать по теме

Теги: #разработка Android #gradle #производительность сборки #обработка аннотаций
Вместе с данным постом часто просматривают: