Инкрементальная обработка
Инкрементальная обработка — это техника обработки, которая минимизирует повторную обработку исходных данных. Основная цель инкрементальной обработки — сократить время цикла изменения-компиляции-тестирования. Для общей информации см. статью Википедии о инкрементальном вычислении.
Чтобы определить, какие исходные данные являются изменёнными (те, которые нуждаются в повторной обработке), 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–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/ksp-incremental.html