Инкрементальная обработка
Инкрементальная обработка — это техника обработки, которая по возможности избегает повторной обработки исходных данных. Основная цель инкрементальной обработки — сократить время цикла изменение-компиляция-тестирование. Для общей информации см. статью Википедии о инкрементальном вычислении.
Для определения, какие исходные данные являются грязными (те, которые требуют повторной обработки), KSP нуждается в помощи процессоров для определения соответствия входных источников сгенерированным выходным данным. Для облегчения этой часто сложной и подверженной ошибкам задачи KSP разработан таким образом, чтобы требовать только минимальный набор корневых источников, которые процессоры используют в качестве начальных точек для навигации по структуре кода. Другими словами, процессор должен связать выходной результат с источниками соответствующего KSNode если KSNode получен из любого из следующих:
Resolver.getAllFilesResolver.getSymbolsWithAnnotationResolver.getClassDeclarationByNameResolver.getDeclarationsFromPackage
Инкрементальная обработка в настоящее время включена по умолчанию. Чтобы отключить её, установите свойство Gradle ksp.incremental=false. Чтобы включить логи, которые выводят набор грязных данных в соответствии с зависимостями и выходными данными, используйте ksp.incremental.log=true. Вы можете найти эти файлы логов в папке вывода build с расширением файла .log.
Агрегирование против изоляции
Аналогично концепциям в Обработке аннотаций Gradle, KSP поддерживает режимы как агрегирования, так и изоляции. Обратите внимание, что в отличие от обработки аннотаций Gradle, KSP классифицирует каждый выходной результат как агрегирующий или изолирующий, а не весь процессор.
Выходной результат агрегирования потенциально может быть затронут любыми изменениями входных данных, за исключением удаления файлов, не влияющих на другие файлы. Это означает, что любое изменение входных данных приводит к перестроению всех агрегированных выходных данных, что, в свою очередь, означает повторную обработку всех соответствующих зарегистрированных, новых и измененных исходных файлов.
В качестве примера, выходной результат, который собирает все символы с определённой аннотацией, считается агрегирующим выходным результатом.
Изолированный выходной результат зависит только от указанных источников. Изменения в других источниках не влияют на изолированный выходной результат. Обратите внимание, что в отличие от обработки аннотаций Gradle, вы можете определить несколько исходных файлов для заданного выходного результата.
В качестве примера, сгенерированный класс, посвящённый интерфейсу, который он реализует, считается изолированным.
Подводя итог, если выходной результат может зависеть от новых или изменённых источников, он считается агрегирующим. В противном случае выходной результат является изолированным.
Вот сводка для читателей, знакомых с обработкой аннотаций Java:
В изолированном Java процессоре обработки аннотаций все выходные данные изолированы в KSP.
В агрегированном Java процессоре обработки аннотаций некоторые выходные данные могут быть изолированными, а некоторые — агрегированными в KSP.
Как это реализовано
Зависимости вычисляются путём сопоставления входных и выходных файлов, а не аннотаций. Это отношение «многие ко многим».
Правила распространения грязности из-за соответствия входных и выходных данных:
Если входной файл изменён, он всегда будет переобработан.
Если входной файл изменён и он связан с выходным результатом, все другие входные файлы, связанные с тем же выходным результатом, также будут переобработаны. Это транзитивно, то есть недействительность происходит повторно до тех пор, пока не появится новый грязный файл.
Все входные файлы, связанные с одним или несколькими агрегированными выходными результатами, будут переобработаны. Другими словами, если входной файл не связан ни с какими агрегированными выходными результатами, он не будет переобработан (если он не соответствует условиям 1 или 2 выше).
Причин несколько:
Если входной файл изменён, может быть введена новая информация, и поэтому процессорам необходимо снова запустить обработку с этим входным файлом.
Выходной результат состоит из набора входных данных. Процессорам может потребоваться все входные данные для повторной генерации выходного результата.
aggregating=trueозначает, что выходной результат потенциально может зависеть от новой информации, которая может поступать как из новых файлов, так и из изменённых, существующих файлов.aggregating=falseозначает, что процессор уверен, что информация поступает только из определённых входных файлов и никогда не поступает из других или новых файлов.
Пример 1
Процессор генерирует outputForA после прочтения класса A в A.kt и класса B в B.kt, где A расширяет B. Процессор получил A с помощью Resolver.getSymbolsWithAnnotation и затем получил B с помощью KSClassDeclaration.superTypes из A. Поскольку включение B обусловлено A, B.kt не нужно указывать в dependencies для outputForA. Вы по-прежнему можете указать B.kt в этом случае, но это не нужно.
// A.kt
@Interesting
class A : B()
// B.kt
open class B
// Example1Processor.kt
class Example1Processor : SymbolProcessor {
override fun process(resolver: Resolver) {
val declA = resolver.getSymbolsWithAnnotation("Interesting").first() as KSClassDeclaration
val declB = declA.superTypes.first().resolve().declaration
// B.kt isn't required, because it can be deduced as a dependency by KSP
val dependencies = Dependencies(aggregating = true, declA.containingFile!!)
// outputForA.kt
val outputName = "outputFor${declA.simpleName.asString()}"
// outputForA depends on A.kt and B.kt
val output = codeGenerator.createNewFile(dependencies, "com.example", outputName, "kt")
output.write("// $declA : $declB\n".toByteArray())
output.close()
}
// ...
}
Пример 2
Предположим, что процессор генерирует outputA после прочтения sourceA и outputB после прочтения sourceB.
Когда sourceA изменено:
Если
outputBагрегированный, тоsourceAиsourceBпереобрабатываются.Если
outputBизолированный, переобрабатывается толькоsourceA.
Когда sourceC добавлено:
Если
outputBагрегированный, переобрабатываютсяsourceCиsourceB.Если
outputBизолированный, переобрабатывается толькоsourceC.
Когда sourceA удалено, ничего не нужно переобрабатывать.
Когда sourceB удалено, ничего не нужно переобрабатывать.
Как определяется грязь файла
Грязный файл — это файл, либо напрямую изменённый пользователем, либо косвенно затронутый другими грязными файлами. KSP распространяет грязь в двух этапах:
Распространение по трассировке разрешения: Разрешение ссылки на тип (неявно или явно) — единственный способ перемещения от одного файла к другому. Когда процессор разрешает ссылку на тип, изменённый или затронутый файл, содержащий изменение, которое потенциально может повлиять на результат разрешения, повлияет на файл, содержащий эту ссылку.
Распространение по соответствию входных и выходных данных: Если исходный файл изменён или затронут, все другие исходные файлы, имеющие общие выходные данные с этим файлом, также затронуты.
Обратите внимание, что оба метода транзитивны, а второй формирует классы эквивалентности.
Отчёт о проблемах
Чтобы сообщить об ошибке, установите свойства Gradle ksp.incremental=true и ksp.incremental.log=true, и выполните чистую сборку. Эта сборка создаёт два файла логов:
build/kspCaches/<source set>/logs/kspDirtySet.logbuild/kspCaches/<source set>/logs/kspSourceToOutputs.log
Затем вы можете выполнить последовательные инкрементальные сборки, которые сгенерируют два дополнительных файла логов:
build/kspCaches/<source set>/logs/kspDirtySetByDeps.logbuild/kspCaches/<source set>/logs/kspDirtySetByOutputs.log
Эти логи содержат имена файлов источников и выходных данных, а также метки времени сборок.
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/ksp-incremental.html