tl;dr
- The type of garbage collector is not inherited by the Kotlin process if it's specified by the jvmargs property
- When comparing ParallelGC for the Kotlin process against G1, the experiment showed:
- 22% Kotlin Compile aggregated task time improvement
- 60% garbage collector time improvement of the Kotlin process
- 51% usage reduction of the Kotlin process memory usage
- The results are based on ephemeral agents(CI) builds. Please, before adopting this type of configuration, measure your builds!
Context
The idea for this article was born when investigating the behavior of the JVM arguments in the Kotlin process in Android builds. The theory says if no argument were specified for the Kotlin process, it inherits the arguments from the Gradle daemon, given:
org.gradle.jvmargs=-Xmx500m
We will retrieve the PID for the process:
jps | grep KotlinCompileDaemon | sed 's/KotlinCompileDaemon//' | xargs jinfo
The result is:
VM Flags:
-XX:MaxHeapSize=528482304
However, when we set the type of collector like:
org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC
The Garbage collector defined for the Kotlin process is:
-XX:+UseG1GC
In this case, we need to declare the type of GC for the Kotlin process explicitly:
kotlin.daemon.jvmargs=-Xmx3g -XX:+UseParallelGC
And is here where I noticed that the references in terms of JVM arguments optimizations are always focused on the Gradle process without discussing one of the main components of our builds: The Kotlin process.
This article explains the results of measuring the different combinations of Garbage Collectors in the Gradle/Kotlin processes.
Experiment
Environment
- Linux 5.15.0-1035-azure (amd64)
- 2 CPU cores
- 2 Gradle workers
- 3 Gb memory heap size
- Github Action agent
Project
- nowinandroid (commit 67dae0c3c09f22745ccb8c4087a0f2ea69125d9f - 04/11)
- JDK 11
- Gradle 7.6
- AGP 7.4.1
- KGP 1.8.0
- Hilt 2.44.2
Experiment Variants
id | Gradle GC | Kotlin GC |
---|---|---|
gg_kg | G1 | G1 |
gp_kg | ParallelGC | G1 |
gg_kp | G1 | ParallelGC |
gp_kp | ParallelGC | ParallelGC |
Methodology
- Each variant runs 40 Gradle profiler scenarios
- Each scenario runs one warm-up and five builds
- Each scenario applies a Gradle profiler scenario with an abi change on the module
core:datastore
(Highest betweenness centrality in the project dependency graph). - The task executed for each scenario is
clean :app:assembleDebug
- This type of scenario generates for each execution:
- 512 tasks executed in 25 projects
- 24 Kotlin Compiler tasks executed in 12 projects
- 26 Java Compiler tasks executed in 12 projects
- We have ignored the warm-up and first build in all the results of each execution
Results
We have grouped the results by:
- Build duration
- Kotlin Compile tasks duration
- GC time by the Kotlin process
- Usage of the Kotlin process
Builds
The data was extracted from the outputs generated for each Gradle Profiler execution, duration in milliseconds
Mean by variant
Kotlin Compiler
We aggregated the build time of Kotlin Compiler tasks with the outcome "SUCCESS" using Gradle Enterprise API, duration in milliseconds:
Mean by variant
Kotlin process
Using the Plugin InfoKotlin process, we retrieved the information of the Kotlin process on the last build of the profiled scenario for each execution
GC Time
Garbage Collector time in minutes:
Mean by variant
Usage
Kotlin process usage in GB:
Mean by variant
Final words
In this experiment using ParallelGC in the Kotlin process, we showed the benefits over G1.
Again, the goal of this article is not to give you a reason to create a PR using ParallelGC. You need to measure what matters in your project, CI pipelines could contain expensive builds with R8 tasks or endless test tasks, and we just covered assembleDebug
.
Finally, as you know, with AGP 8, JDK is set to 17. I will publish a new article covering this experiment with the JDK 17 Garbage collector results.
Happy Building!
Top comments (0)