Incremental compilation in Gradle 4 vs. 5

We recently upgraded our gradle version from 4.10 to 5.3. I had recently been testing incremental compilation in 4.10 so I repeated the exercise in 5.3. What I observed in 4.10 is that if I edit some method implementation that only that one file compiled. Incremental compilation correctly realized that no other file needed to be compiled. However in 5.3, Gradle appears to recompile all classes that have some dependency on that changed class. I have not dug too deeply but what I see is in a project with 1500 classes, about 400 recompile (399 are unnecessary).

Is this expected?

This has always been the case if the source files are in the same source set. Compile avoidance currently only works between project boundaries. There should be no difference in that regard between 4.x and 5.x

Sorry just getting back to this.

I’m not sure what you mean when you say “this has always been the case”. I see 2 different behaviors in 4.10 and 5.3. In both cases I have about 1500 classes in one source set (main). I change the private impl details of a method and in 4.10 I get one new class file and in 5.3 I get about 400 new class files. Project structures are the same.

What I mean is that there has never been a difference between “making an implementation change” and “making an API” change when inside a single sourceSet. It’s always been treated as an API change in that case. Doing it more efficiently is something that we’d like to do, but haven’t yet. This should be no different between 4.x and 5.x. If you see a difference, please share an example with us so we can investigate.

Ok there is a definitely a difference. I will try to built out a small sample.
Otherwise I will have to send you some evidence from my large repo.

This sample seems to dispute your claim that incremental compilation has sourceSet granularity.

Repo : git@github.com:RobReece/multi-project-build.git
Commit id: be21b3f5ac64b552b83c0ee89d625e5285a8a152

Note: This is a multi project build, which is probably not necessary to demonstrate the issue.

Scenario #1: Modify file PublicFinalStatic.java by changing the contents of String x in the only method. Run compileJava task.
Result: Project base does a Full recompilation because the method has a non-private, static, final. See output below and I also confirmed by checking timestamps on the class files.
Output:

Task :base:compileJava
Task ‘:base:compileJava’ is not up-to-date because:
Input property ‘source’ file D:\git-repos\multi-project-build\base\src\main\java\com\rar\base\internal\PublicFinalStatic.java has changed.
Created classpath snapshot for incremental compilation in 0.0 secs.
Class dependency analysis for incremental compilation took 0.025 secs.
Full recompilation is required because ‘PublicFinalStatic.java’ was changed. Analysis took 0.045 secs.
Compiling with JDK Java compiler API.

-a---- 4/11/2019 2:42 PM 576 BaseClass.class
-a---- 4/11/2019 2:42 PM 432 BaseClass2.class
-a---- 4/11/2019 2:42 PM 537 PrivateFinalStatic.class
-a---- 4/11/2019 2:42 PM 534 PublicFinalStatic.class

Scenario #2: Modify file PrivateFinalStatic.java by changing the contents of String x in the only method. Run compileJava task.
Result: Project base compiles one of four java files (as expected). Gradle output shows 1 file compiled and confirmed by looking at class file timestamps.
Output:

Task :base:compileJava
Task ‘:base:compileJava’ is not up-to-date because:
Input property ‘source’ file D:\git-repos\multi-project-build\base\src\main\java\com\rar\base\internal\PrivateFinalStatic.java has changed.
Created classpath snapshot for incremental compilation in 0.0 secs.
Class dependency analysis for incremental compilation took 0.018 secs.
Compiling with JDK Java compiler API.
Incremental compilation of 1 classes completed in 0.184 secs.
:base:compileJava (Thread[Execution worker for ‘:’,5,main]) completed. Took 0.322 secs.
:base:processResources (Thread[Execution worker for ‘:’,5,main]) started.


-a---- 4/11/2019 2:42 PM 576 BaseClass.class
-a---- 4/11/2019 2:42 PM 432 BaseClass2.class
-a---- 4/11/2019 2:52 PM 537 PrivateFinalStatic.class
-a---- 4/11/2019 2:42 PM 534 PublicFinalStatic.class

Scenario #3: Modify base/BaseClass2 by adding a new method (API change)
Result: One class compiles in base Project, other downstream projects avoid compilation
Output:

Task :base:compileJava
Task ‘:base:compileJava’ is not up-to-date because:
Input property ‘source’ file D:\git-repos\multi-project-build\base\src\main\java\com\rar\base\internal\BaseClass2.java has changed.
Created classpath snapshot for incremental compilation in 0.0 secs.
Class dependency analysis for incremental compilation took 0.007 secs.
Compiling with JDK Java compiler API.
Incremental compilation of 1 classes completed in 0.172 secs.
:base:compileJava (Thread[Execution worker for ‘:’,5,main]) completed. Took 0.288 secs.
:base:processResources (Thread[Execution worker for ‘:’,5,main]) started.

Task :A_base:compileJava UP-TO-DATE
Task ‘:A_base:compileJava’ is not up-to-date because:
Input property ‘classpath’ file D:\git-repos\multi-project-build\base\build\libs\base.jar has changed.
Created classpath snapshot for incremental compilation in 0.005 secs.
Class dependency analysis for incremental compilation took 0.008 secs.
None of the classes needs to be compiled! Analysis took 0.014 secs.
:A_base:compileJava (Thread[Execution worker for ‘:’,5,main]) completed. Took 0.163 secs.
:B_base:compileJava (Thread[Execution worker for ‘:’,5,main]) started.

I didn’t say it has sourceSet granularity. I said that there is no difference between ABI and non-ABI changes within a sourceSet. So if you modify a private detail, then all classes inside the sourceSet that transitively reference that changed class will be recompiled, even though they are not affected by that detail. It’s treated as if the API of that class had changed.

Your first example is a public constant change, which requires a full recompile, because constants can be inlined. If you want to investigate ABI vs non-ABI, do a change that’s not a constant.

1 Like

Ok I’m starting to understand your point about ABI vs non-ABI changes within a sourceSet. I did go back and compare 4.10 and 5.3 and they basically operate the same in regards to a change in a method implementation. I was pretty certain when I tested this some time ago that I only saw a single new class file, but I must be remembering that incorrectly. I have a project with 1030 source files and when I change the implementation details for a method, I see this in the compilation output for both 4.10 and 5.3:

Incremental compilation of 369 classes completed in 3.532 secs.

Now the downstream effect of this is that in Intellij we use Delegate to Gradle and when I attempt to compile a simple change, it generates lots of new class files and then hot code replace takes a long time (not too mention that extra seconds spent in Gradle).

Good to know that there isn’t some undiscovered bug :slight_smile: Splitting abi and impl within a single sourceSet would be great, but hasn’t quite made it up to the top of our priority list. You’re of course more than welcome to give it a shot yourself. I can give you code pointers if you’d like.

I’m happy to report that more fine-grained incremental compilation (within a single sourceset) will be in Gradle 6.0. https://github.com/gradle/gradle/issues/10420

1 Like