Using ParallelGC for the Kotlin process in Android Builds
Tags: android, gradle, kotlin
Published: 2023-04-19 03:41:29

 
## 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](https://github.com/android/nowinandroid) (commit [67dae0c3c09f22745ccb8c4087a0f2ea69125d9f](https://github.com/android/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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oyws4p2wg53nqynqyd3n.png)

Mean by variant
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztvxr6rb1hwlo20bo27m.png)

#### Kotlin Compiler
We aggregated the build time of Kotlin Compiler tasks with the outcome "SUCCESS" using Gradle Enterprise API, duration in milliseconds:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1rkuakg0g9jr77hyopa6.png)


Mean by variant
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p10erfn2jr55bd05pj7c.png)


#### Kotlin process
Using the Plugin [InfoKotlin](https://github.com/cdsap/InfoKotlinProcess) 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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2zs2y87e5hzlszrj3u5q.png)
 
Mean by variant
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s84a9q48i2ukpytptw7j.png)


**Usage**
Kotlin process usage in GB:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/km7097ea42skmy5sw8ix.png)

Mean by variant

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azv2d4bayh32tmrv0ugz.png)

## 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!