본문으로 건너뛰기

증분 처리

증분 처리(Incremental processing)는 가능한 한 소스의 재처리를 피하는 처리 기술입니다. 증분 처리의 주요 목표는 일반적인 변경-컴파일-테스트 주기의 소요 시간을 줄이는 것입니다. 일반적인 정보는 Wikipedia의 incremental computing 관련 글을 참조하십시오.

어떤 소스가 dirty (재처리해야 하는 소스)인지 판단하기 위해 KSP는 프로세서가 어떤 입력 소스가 어떤 생성된 출력에 해당하는지 식별하도록 도와야 합니다. 이러한 종종 번거롭고 오류가 발생하기 쉬운 프로세스를 돕기 위해 KSP는 프로세서가 코드 구조를 탐색하기 위한 시작점으로 사용하는 최소한의 root sources 집합만 필요하도록 설계되었습니다. 즉, 프로세서는 다음 중 하나에서 KSNode를 얻은 경우 해당 KSNode의 소스와 출력을 연결해야 합니다.

  • Resolver.getAllFiles
  • Resolver.getSymbolsWithAnnotation
  • Resolver.getClassDeclarationByName
  • Resolver.getDeclarationsFromPackage

증분 처리는 현재 기본적으로 활성화되어 있습니다. 비활성화하려면 Gradle 속성 ksp.incremental=false를 설정하세요. 종속성 및 출력에 따라 dirty set을 덤프하는 로그를 활성화하려면 ksp.incremental.log=true를 사용하세요. 이러한 로그 파일은 .log 파일 확장자와 함께 build 출력 디렉터리에서 찾을 수 있습니다.

JVM에서는 classpath 변경 사항과 Kotlin 및 Java 소스 변경 사항이 기본적으로 추적됩니다. Kotlin 및 Java 소스 변경 사항만 추적하려면 ksp.incremental.intermodule=false Gradle 속성을 설정하여 classpath 추적을 비활성화하세요.

Aggregating vs Isolating

Gradle annotation processing의 개념과 유사하게 KSP는 aggregatingisolating 모드를 모두 지원합니다. Gradle annotation processing과 달리 KSP는 전체 프로세서가 아닌 각 출력을 aggregating 또는 isolating으로 분류합니다.

aggregating 출력은 다른 파일에 영향을 주지 않는 파일 제거를 제외하고 모든 입력 변경의 영향을 받을 수 있습니다. 이는 모든 입력 변경으로 인해 모든 aggregating 출력이 다시 빌드되고, 결과적으로 등록된 새 소스 파일과 수정된 소스 파일이 모두 다시 처리됨을 의미합니다.

예를 들어, 특정 annotation이 있는 모든 심볼을 수집하는 출력은 aggregating 출력으로 간주됩니다.

isolating 출력은 지정된 소스에만 의존합니다. 다른 소스에 대한 변경 사항은 isolating 출력에 영향을 주지 않습니다. Gradle annotation processing과 달리 지정된 출력에 대해 여러 소스 파일을 정의할 수 있습니다.

예를 들어, 구현하는 인터페이스 전용으로 생성된 클래스는 isolating으로 간주됩니다.

요약하자면, 출력이 새 소스 또는 변경된 소스에 의존할 수 있는 경우 aggregating으로 간주됩니다. 그렇지 않으면 출력이 isolating입니다.

Java annotation processing에 익숙한 독자를 위한 요약은 다음과 같습니다.

  • isolating Java annotation processor에서 모든 출력은 KSP에서 isolating입니다.
  • aggregating Java annotation processor에서 일부 출력은 isolating일 수 있고 일부 출력은 KSP에서 aggregating일 수 있습니다.

구현 방법

종속성은 annotation 대신 입력 및 출력 파일의 연결을 통해 계산됩니다. 이는 다대다 관계입니다.

입출력 연결로 인한 dirtiness 전파 규칙은 다음과 같습니다.

  1. 입력 파일이 변경되면 항상 재처리됩니다.
  2. 입력 파일이 변경되고 출력과 연결된 경우 동일한 출력과 연결된 다른 모든 입력 파일도 재처리됩니다. 이는 전이적이며, 즉 새로운 dirty 파일이 없을 때까지 무효화가 반복적으로 발생합니다.
  3. 하나 이상의 aggregating 출력과 연결된 모든 입력 파일이 재처리됩니다. 즉, 입력 파일이 aggregating 출력과 연결되어 있지 않으면 위 1 또는 2를 충족하지 않는 한 재처리되지 않습니다.

이유는 다음과 같습니다.

  1. 입력이 변경되면 새로운 정보가 도입될 수 있으므로 프로세서가 입력을 다시 실행해야 합니다.
  2. 출력은 입력 세트로 구성됩니다. 프로세서는 출력을 다시 생성하기 위해 모든 입력이 필요할 수 있습니다.
  3. aggregating=true는 출력이 잠재적으로 새로운 정보에 의존할 수 있음을 의미하며, 이는 새로운 파일 또는 변경된 기존 파일에서 나올 수 있습니다. aggregating=false는 프로세서가 정보가 특정 입력 파일에서만 나오고 다른 파일이나 새 파일에서는 절대 나오지 않는다고 확신한다는 의미입니다.

예제 1

프로세서는 A.kt에서 클래스 AB.kt에서 클래스 B를 읽은 후 outputForA를 생성합니다. 여기서 AB를 확장합니다. 프로세서는 Resolver.getSymbolsWithAnnotation으로 A를 가져온 다음 A에서 KSClassDeclaration.superTypesB를 가져왔습니다. B의 포함은 A 때문이므로 B.ktoutputForA에 대한 dependencies에 지정할 필요가 없습니다. 이 경우 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는 KSP에서 종속성으로 추론할 수 있으므로 필요하지 않습니다.
val dependencies = Dependencies(aggregating = true, declA.containingFile!!)
// outputForA.kt
val outputName = "outputFor${declA.simpleName.asString()}"
// outputForA는 A.kt 및 B.kt에 의존합니다.
val output = codeGenerator.createNewFile(dependencies, "com.example", outputName, "kt")
output.write("// $declA : $declB
".toByteArray())
output.close()
}
// ...
}

예제 2

프로세서가 sourceA를 읽은 후 outputA를 생성하고 sourceB를 읽은 후 outputB를 생성한다고 가정합니다.

sourceA가 변경된 경우:

  • outputB가 aggregating이면 sourceAsourceB가 모두 재처리됩니다.
  • outputB가 isolating이면 sourceA만 재처리됩니다.

sourceC가 추가된 경우:

  • outputB가 aggregating이면 sourceCsourceB가 모두 재처리됩니다.
  • outputB가 isolating이면 sourceC만 재처리됩니다.

sourceA가 제거되면 아무것도 재처리할 필요가 없습니다.

sourceB가 제거되면 아무것도 재처리할 필요가 없습니다.

파일 dirtiness가 결정되는 방법

dirty 파일은 사용자가 직접 변경 하거나 다른 dirty 파일의 영향을 간접적으로 받은 파일입니다. KSP는 두 단계로 dirtiness를 전파합니다.

  • _resolution tracing_을 통한 전파: 유형 참조를 (암시적으로 또는 명시적으로) resolve하는 것은 한 파일에서 다른 파일로 이동하는 유일한 방법입니다. 유형 참조가 프로세서에 의해 resolve되면 resolution 결과에 잠재적으로 영향을 줄 수 있는 변경 사항이 포함된 변경되거나 영향을 받는 파일은 해당 참조를 포함하는 파일에 영향을 미칩니다.
  • _입출력 대응_에 의한 전파: 소스 파일이 변경되거나 영향을 받으면 해당 파일과 일부 출력을 공유하는 다른 모든 소스 파일이 영향을 받습니다.

둘 다 전이적이며 두 번째는 동등 클래스를 형성합니다.

버그 보고

버그를 보고하려면 Gradle 속성 ksp.incremental=trueksp.incremental.log=true를 설정하고 clean 빌드를 수행하세요. 이 빌드는 다음 두 개의 로그 파일을 생성합니다.

  • build/kspCaches/<source set>/logs/kspDirtySet.log
  • build/kspCaches/<source set>/logs/kspSourceToOutputs.log

그런 다음 연속적인 증분 빌드를 실행하여 다음 두 개의 추가 로그 파일을 생성할 수 있습니다.

  • build/kspCaches/<source set>/logs/kspDirtySetByDeps.log
  • build/kspCaches/<source set>/logs/kspDirtySetByOutputs.log

이러한 로그에는 소스 및 출력의 파일 이름과 빌드의 타임스탬프가 포함되어 있습니다.