Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Understanding Incremental Processing in the JVM...

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Understanding Incremental Processing in the JVM World

Presented at Dutch Android User Group January 2026 Meetup
https://www.meetup.com/dutch-aug/events/312882769/

Avatar for Subhrajyoti Sen

Subhrajyoti Sen

January 29, 2026
Tweet

More Decks by Subhrajyoti Sen

Other Decks in Programming

Transcript

  1. Java javac does minimal incremental processing on it’s own Relies

    on the build tool/IDE to orchestrate incremental processing
  2. Ant - <javac> task Compile a .java file only if:

    • .java doesn’t have a corresponding .class file, or • .java is newer than the .class file - based on timestamps
  3. Ant - <javac> task Compile a .java file only if:

    • .java doesn’t have a corresponding .class file, or • .java is newer than the .class file - based on timestamps This can be further tweaked with include/exclude attributes
  4. Ant - <depend> task • Reads the .class files instead

    of source files • From the information encoded in class files, determines the dependencies of the class files
  5. Gradle abstract class UppercaseTask : DefaultTask() { @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) abstract

    val inputFile: RegularFileProperty @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun process() {} }
  6. Gradle tasks.register<UppercaseTask>("makeUppercase") { // Specify which file to watch inputFile.set(layout.projectDirectory.file("source.txt"))

    // Specify where to put the result outputFile.set(layout.buildDirectory.file("generated/uppercase.txt")) }
  7. How does Gradle remember? For each input and output, Gradle

    creates a fingerprint The fingerprint has the hash of the input files along with some metadata.
  8. How does Gradle remember? For each input and output, Gradle

    creates a fingerprint The fingerprint has the hash of the input files along with some metadata. Once the task runs, Gradle calculates the fingerprint of the output.
  9. How does Gradle remember? For each input and output, Gradle

    creates a fingerprint The fingerprint has the hash of the input files along with some metadata. Once the task runs, Gradle calculates the fingerprint of the output. When the task is run again, Gradle makes new fingerprints and compares with old fingerprints.
  10. How does Gradle remember? For each input and output, Gradle

    creates a fingerprint The fingerprint has the hash of the input files along with some metadata. Once the task runs, Gradle calculates the fingerprint of the output. When the task is run again, Gradle makes new fingerprints and compares with old fingerprints. If fingerprints don’t match, the task is re-run
  11. Compilation Avoidance If a dependent project has changed in an

    ABI-compatible way (only its private API has changed), then Java compilation tasks will be up-to-date.
  12. Compilation Avoidance If a dependent project has changed in an

    ABI-compatible way (only its private API has changed), then Java compilation tasks will be up-to-date. This means that if project A depends on project B and a class in B is changed in an ABI-compatible way (typically, changing only the body of a method), then Gradle won’t recompile A.
  13. What’s an ABI? class UserAccount( val username: String, private val

    securityCode: Int ) { fun login() { println("Logging in $username...") } private fun validateCode(): Boolean = true }
  14. ABI public final class UserAccount { public UserAccount(java.lang.String, int); public

    final java.lang.String getUsername(); public final void login(); }
  15. Kotlin Incremental Processing Kotlin assists Gradle in incremental processing. We

    will focus on Kotlin/JVM implementation Kotlin relies on classpath snapshots to determine whether compilation is necessary.
  16. Kotlin Incremental Processing Kotlin assists Gradle in incremental processing. We

    will focus on Kotlin/JVM implementation Kotlin relies on classpath snapshots to determine whether compilation is necessary. Classpath snapshots also rely on the ABI surface.
  17. Types of classpath snapshots Fine-grained snapshots: Includes information about class

    members. When changes are detected, only classes affected by the changes are recompiled.
  18. Types of classpath snapshots Fine-grained snapshots: Includes information about class

    members. When changes are detected, only classes affected by the changes are recompiled. Coarse snapshots: only contains the ABI hash. When the ABI changes, the compiler recompiles all classes that depend on the changed class. This is useful for libraries and external dependencies. The compiler also keep coarse snapshots of .jar files.
  19. Incremental Annotation Processing - Gradle Gradle supports 2 modes of

    annotation processing: 1. Aggregating 2. Isolating
  20. Incremental Annotation Processing - Gradle Gradle supports 2 modes of

    annotation processing: 1. Aggregating 2. Isolating A processor can be marked as “dynamic” if it needs to decide the nature at runtime.
  21. Isolating 1-1 or 1-many relationship Example: Generating a `<Type>Repository` for

    each class annotated with `@Entity` All actions must be based on information reachable via the Abstract Syntax Tree(AST)
  22. Isolating 1-1 or 1-many relationship Example: Generating a `<Type>Repository` for

    each class annotated with `@Entity` All actions must be based on information reachable via the Abstract Syntax Tree(AST) If it’s based on anything outside the AST, it should be aggregating
  23. Isolating 1-1 or 1-many relationship Example: Generating a `<Type>Repository` for

    each class annotated with `@Entity` All actions must be based on information reachable via the Abstract Syntax Tree(AST) If it’s based on anything outside the AST, it should be aggregating Every generated file must be mapped to 1 originating file
  24. Incremental Annotation Processing - KSP KSP relies on a mapping

    of input sources and generated output. Each output needs to be mapped to a corresponding set of KSNode
  25. Incremental Annotation Processing - KSP KSP relies on a mapping

    of input sources and generated output. Each output needs to be mapped to a corresponding set of KSNode By default, KSP tracks the classpath in addition to Java and Kotlin sources.
  26. Incremental Annotation Processing - KSP KSP relies on a mapping

    of input sources and generated output. Each output needs to be mapped to a corresponding set of KSNode By default, KSP tracks the classpath in addition to Java and Kotlin sources. To disable classpath tracking,you can use `ksp.incremental.intermodule=false`
  27. Incremental Annotation Processing - KSP You can get a KSNode

    using the following: • Resolver.getAllFiles • Resolver.getAllSymbolsWithAnnotation • Resolver.getClassDeclarationByName • Resolver.getDeclarationsFromPackage
  28. KSP Output Modes KSP supports two modes, similar to Gradle

    annotation processing: • Aggregating • Isolating
  29. KSP Output Modes KSP supports two modes, similar to Gradle

    annotation processing: • Aggregating • Isolating Unlike Gradle annotation processing, KSP marks each output as either isolating or aggregating and not the entire processor.
  30. KSP Output Modes KSP supports two modes, similar to Gradle

    annotation processing: • Aggregating • Isolating Unlike Gradle annotation processing, KSP marks each output as either isolating or aggregating and not the entire processor. Unlike Java, an Isolating output can define multiple source files.
  31. Isolating Mode val dependencies = Dependencies( aggregating = false, listOf("ParkingEntity")

    ) val os = codeGenerator.createNewFile( dependencies = dependencies, packageName = "com.example.generated", fileName = "ParkingRepository" )
  32. Aggregating Mode val dependencies = Dependencies( aggregating = true, listOf("StartupWorker1",

    "StartupWorker2) ) val os = codeGenerator.createNewFile( dependencies = dependencies, packageName = "com.example.generated", fileName = "StartupRegistry" )
  33. Dirtiness Propagation 1. If an input file is changed, it

    will be reprocessed. 2. If an input file is changed and it’s associated with an output, then all other input files associated with that output will also be reprocessed 3. All input files associated with at least one aggregating output will be reprocessed
  34. But Code? class EntityProcessor( //.. dependencies ): SymbolProcessor { override

    fun process(resolver: Resolver): List<Annotated> { val files = resolver.getAllFiles() } }