<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Iñaki Villar</title>
    <description>The latest articles on DEV Community by Iñaki Villar (@cdsap).</description>
    <link>https://dev.to/cdsap</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F862068%2F6c130543-fb52-4163-ac01-f6dc79b8b01f.png</url>
      <title>DEV Community: Iñaki Villar</title>
      <link>https://dev.to/cdsap</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cdsap"/>
    <language>en</language>
    <item>
      <title>ImNotOkay, a GC experiment for Android CI builds</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Wed, 01 Apr 2026 14:11:21 +0000</pubDate>
      <link>https://dev.to/cdsap/imnotokay-a-gc-experiment-for-android-ci-builds-489o</link>
      <guid>https://dev.to/cdsap/imnotokay-a-gc-experiment-for-android-ci-builds-489o</guid>
      <description>&lt;p&gt;Inspired by a very specific early-2000s song, I’m presenting a new garbage collector policy for Android CI builds: &lt;code&gt;ImNotOkayGC&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;org.gradle.jvmargs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-Xmx5g -XX:+UseImNotOkayGC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This collector is meant for large Android builds, where memory pressure quietly builds up across tasks, variants, and modules, and when memory finally runs out, instead of pretending everything is fine, it tells you exactly how bad things really are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[GC] I’m fine.
:app:generateReleaseRFile FROM-CACHE    
:app:compileReleaseKotlin   
[GC] Actually I’m not fine.
[GC] Full GC.
[GC] It didn’t help.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/dhZTNgAs4Fc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Funny, right? Consider this my small April Fools contribution.&lt;/p&gt;

&lt;p&gt;Jokes aside, I wanted to try something more serious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ephemeral Android CI Builds
&lt;/h2&gt;

&lt;p&gt;This is an idea I had wanted to try for a long time: Gradle builds have a very well-defined lifecycle.&lt;/p&gt;

&lt;p&gt;Android CI builds are not long-lived services. They are ephemeral workloads with recognizable phases: startup, rising memory pressure, heavy execution, and then termination. Even if the exact task graph changes between &lt;code&gt;assembleDebug&lt;/code&gt; and &lt;code&gt;assembleRelease&lt;/code&gt;, the overall shape is still much more structured than a backend service running for months or supporting thousands of requests per minute.&lt;/p&gt;

&lt;p&gt;And yet we use the same general-purpose garbage collectors for both worlds.&lt;/p&gt;

&lt;p&gt;Of course, we can tune JVM arguments, but that usually turns into trial and error. What made this experiment different is that I used Codex to push the idea much further. Instead of stopping at flag tuning, I was able to build an iterative workflow: capture a baseline, profile the build with JFR, look for recurring allocation and GC patterns across projects, modify the JDK, rerun the scenarios, and compare the results.&lt;/p&gt;

&lt;p&gt;That process is probably the most interesting part of this article.&lt;/p&gt;

&lt;p&gt;The original idea was simple: if Android CI builds have such a specific workload shape, could we get something closer to a GC policy designed for that kind of execution? Not a completely different collector, but a G1-based policy that reacts better to the memory pressure patterns we see in ephemeral Android builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Experiment
&lt;/h2&gt;

&lt;p&gt;The first step was to create a stable baseline. I forked the JDK, built a custom JDK 23 distribution, and used standard G1 in both the Gradle and Kotlin daemons so I could compare later policy changes against a consistent starting point.&lt;/p&gt;

&lt;p&gt;From there, the workflow became iterative:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run a representative set of Android builds using the published JDK 23 baseline build with standard G1.&lt;/li&gt;
&lt;li&gt;Capture telemetry, especially JFR and GC data.&lt;/li&gt;
&lt;li&gt;Identify common patterns in memory pressure and pause behavior.&lt;/li&gt;
&lt;li&gt;Modify the GC policy in the JDK.&lt;/li&gt;
&lt;li&gt;Rerun the same scenarios with the updated build.&lt;/li&gt;
&lt;li&gt;Compare again, then go back to step 4.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each project, I tested two tasks, &lt;code&gt;assembleDebug&lt;/code&gt; and &lt;code&gt;assembleRelease&lt;/code&gt;, under two execution modes: non-warm runs on fully clean agents, and warm runs that reused the Gradle user home while excluding task output cache artifacts.&lt;/p&gt;

&lt;p&gt;Each variant was executed 10 times, and every run generated the full set of profiling artifacts, including JFR recordings, GC data, and build metrics. That gave me something more reliable than a single build result. Instead of looking at isolated executions, I could compare repeated runs and see which patterns actually held.&lt;/p&gt;

&lt;p&gt;Codex played a major role in making that process practical. It helped me move faster through the mechanical parts of the experiment, from changing JDK behavior to wiring the runs and analyzing the outputs, so I could spend more time on the observations and less time on setup.&lt;/p&gt;

&lt;p&gt;If you are curious, here is an example of a warm baseline execution for one of the projects:&lt;br&gt;
&lt;a href="https://github.com/cdsap/im-not-ok-metrics/actions/runs/23775355907" rel="noopener noreferrer"&gt;https://github.com/cdsap/im-not-ok-metrics/actions/runs/23775355907&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each run produced a fairly detailed profiling summary. A single example looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Build Profiling Summary

- Build duration: 392.00s
- Build exit code: 0
- JDK: 23-internal-adhoc.runner.jdk
- Gradle: 9.4.0
- Build scan: https://gradle.com/s/vajkodzs7myja
- Declared GC profiles: gradle-daemon=openjdk-default, kotlin-daemon=repo-default, test-jvm=repo-default
- Reported collector labels: gradle-daemon=openjdk-default, kotlin-daemon=repo-default

## Per-process highlights

- gradle-daemon (pid 2901): max RSS 4304416.0 kB, avg RSS 2484696.2151898732 kB, max CPU 285.0%, collector openjdk-default, runtime GC G1, alloc mode n/a, alloc rate n/a MB/s, GC p95 112.87134999999998 ms, GC max 380.607 ms, total GC 25887.478 ms

- kotlin-daemon (pid 13382): max RSS 2422960.0 kB, avg RSS 2040902.820143885 kB, max CPU 302.0%, collector repo-default, runtime GC G1, alloc mode n/a, alloc rate n/a MB/s, GC p95 193.8508499999999 ms, GC max 239.578 ms, total GC 8645.073 ms

## Correlated peaks

- gradle-daemon pid 2901 peaked near :app-scaffold:assembleDebug at 2026-03-31T02:34:15Z
- kotlin-daemon pid 13382 peaked near :app-scaffold:extractDebugAnnotations at 2026-03-31T02:34:06Z
- gradle-worker pid 14199 peaked near :libraries:util:checkKotlinGradlePluginConfigurationErrors at 2026-03-31T02:31:16Z
- gradle-worker pid 14222 peaked near :libraries:util:checkKotlinGradlePluginConfigurationErrors at 2026-03-31T02:31:16Z
- gradle-worker pid 16210 peaked near :app-scaffold:extractDebugAnnotations at 2026-03-31T02:34:00Z

## Artifacts

- Metadata: /home/runner/work/im-not-ok-metrics/im-not-ok-metrics/project/artifacts/20260331T022747Z/metadata.json
- GC logs: /home/runner/work/im-not-ok-metrics/im-not-ok-metrics/project/artifacts/20260331T022747Z/logs/gc
- JFR: /home/runner/work/im-not-ok-metrics/im-not-ok-metrics/project/artifacts/20260331T022747Z/logs/jfr
- OS metrics: /home/runner/work/im-not-ok-metrics/im-not-ok-metrics/project/artifacts/20260331T022747Z/logs/os/process_metrics.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Changed in ImNotOkay
&lt;/h2&gt;

&lt;p&gt;After analyzing the baseline data, Codex proposed a set of changes based on the idea that Android Gradle builds on ephemeral agents behave differently from long-running JVM applications.&lt;/p&gt;

&lt;p&gt;The idea behind ImNotOkay is to make G1 a bit more aware of the build phase instead of treating the whole build the same way from start to finish.&lt;/p&gt;

&lt;p&gt;At the beginning, during roughly the first minute, it stays out of the way and lets Gradle run normally so the configuration phase can move as fast as possible.&lt;/p&gt;

&lt;p&gt;It only starts intervening once the build shows real signs of memory pressure. At that point, it tries to keep memory behavior more controlled by preventing the young generation from growing too aggressively.&lt;/p&gt;

&lt;p&gt;The goal is not to make G1 more restrictive all the time, but to react when the build starts entering a heavier phase. And if that pressure continues for longer, the policy gradually relaxes again so the build can recover throughput instead of staying constrained for too long.&lt;/p&gt;

&lt;p&gt;You can see the complete diff here:&lt;br&gt;
&lt;a href="https://github.com/cdsap/jdk/compare/imnotokay-jdk23-baseline...cdsap:jdk:im-not-okay" rel="noopener noreferrer"&gt;https://github.com/cdsap/jdk/compare/imnotokay-jdk23-baseline...cdsap:jdk:im-not-okay&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I should also be honest about the outcome: at least for now, this was a failed attempt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Duration Δ&lt;/th&gt;
&lt;th&gt;Gradle GC p95 Δ&lt;/th&gt;
&lt;th&gt;Gradle GC total Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;android-nowinandroid&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🔴 +1.24%&lt;/td&gt;
&lt;td&gt;🔴 +5.07%&lt;/td&gt;
&lt;td&gt;🔴 +2.10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;android-nowinandroid&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🟢 -2.45%&lt;/td&gt;
&lt;td&gt;🟢 -13.60%&lt;/td&gt;
&lt;td&gt;🔴 +3.21%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;android-nowinandroid&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🟢 -1.61%&lt;/td&gt;
&lt;td&gt;🔴 +21.51%&lt;/td&gt;
&lt;td&gt;🔴 +14.69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;android-nowinandroid&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +2.71%&lt;/td&gt;
&lt;td&gt;🔴 +6.41%&lt;/td&gt;
&lt;td&gt;🔴 +4.24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CatchUp&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🟢 -1.20%&lt;/td&gt;
&lt;td&gt;🔴 +5.48%&lt;/td&gt;
&lt;td&gt;🔴 +9.06%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CatchUp&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +2.32%&lt;/td&gt;
&lt;td&gt;🟢 -22.89%&lt;/td&gt;
&lt;td&gt;🔴 +10.96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CatchUp&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🟢 -0.36%&lt;/td&gt;
&lt;td&gt;🔴 +7.43%&lt;/td&gt;
&lt;td&gt;🔴 +1.63%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CatchUp&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +1.38%&lt;/td&gt;
&lt;td&gt;🔴 +12.89%&lt;/td&gt;
&lt;td&gt;🔴 +24.01%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectMedium&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🔴 +1.10%&lt;/td&gt;
&lt;td&gt;🔴 +3.67%&lt;/td&gt;
&lt;td&gt;🔴 +13.79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectMedium&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +0.79%&lt;/td&gt;
&lt;td&gt;🔴 +1.75%&lt;/td&gt;
&lt;td&gt;🟢 -7.34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectMedium&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🟢 -1.01%&lt;/td&gt;
&lt;td&gt;🟢 -1.65%&lt;/td&gt;
&lt;td&gt;🟢 -1.14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectMedium&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🟢 -1.60%&lt;/td&gt;
&lt;td&gt;🔴 +4.04%&lt;/td&gt;
&lt;td&gt;🔴 +6.93%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectSmall&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🟢 -0.35%&lt;/td&gt;
&lt;td&gt;🔴 +16.19%&lt;/td&gt;
&lt;td&gt;🟢 -2.62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectSmall&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +2.60%&lt;/td&gt;
&lt;td&gt;🟢 -25.00%&lt;/td&gt;
&lt;td&gt;🟢 -11.13%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectSmall&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🔴 +2.78%&lt;/td&gt;
&lt;td&gt;🟢 -4.02%&lt;/td&gt;
&lt;td&gt;🟢 -0.79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectSmall&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +0.15%&lt;/td&gt;
&lt;td&gt;🟢 -0.70%&lt;/td&gt;
&lt;td&gt;🟢 -0.67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectLarge&lt;/td&gt;
&lt;td&gt;assemble-debug&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🔴 +0.27%&lt;/td&gt;
&lt;td&gt;🟢 -13.34%&lt;/td&gt;
&lt;td&gt;🟢 -12.34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectLarge&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;nonwarm&lt;/td&gt;
&lt;td&gt;🟢 -1.14%&lt;/td&gt;
&lt;td&gt;🔴 +7.50%&lt;/td&gt;
&lt;td&gt;🟢 -0.36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GeneratedProjectLarge&lt;/td&gt;
&lt;td&gt;assemble-release&lt;/td&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;🟢 -2.37%&lt;/td&gt;
&lt;td&gt;🔴 +11.73%&lt;/td&gt;
&lt;td&gt;🟢 -0.28%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The results were not strong enough to justify presenting this as “here is a better GC policy for Android CI.” The behavior was too mixed and inconsistent to call it a success. So I ended up closing the experiment earlier than I expected, partly because I wanted to publish something today and the data did not support a stronger conclusion.&lt;/p&gt;

&lt;p&gt;Even if the collector changes did not clearly win, the process itself was still valuable. Workload tracing, JFR profiling, iterative JDK changes, and fast experimentation with Codex let me push the idea much further than I normally would. So while this is not a success story in terms of results, it still feels like a good example of how far this kind of experimentation can go.&lt;/p&gt;
&lt;h2&gt;
  
  
  Using the new ImNotOkay Collector Policy
&lt;/h2&gt;

&lt;p&gt;If you made it this far and still want to try it, the funny thing is that it is totally possible. You can use the new policy with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;org.gradle.jvmargs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;-XX:+UnlockExperimentalVMOptions -XX:+UseImNotOkayGC ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you would also need to use the custom JDK in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Download custom JDK artifact&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;gh run download 23719062741 \&lt;/span&gt;
      &lt;span class="s"&gt;--repo cdsap/im-not-ok-metrics \&lt;/span&gt;
      &lt;span class="s"&gt;--name custom-jdk-linux-9ad2e63f1763 \&lt;/span&gt;
      &lt;span class="s"&gt;--dir "$GITHUB_WORKSPACE/custom-jdk"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unpack and activate custom JDK&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;mkdir -p "$GITHUB_WORKSPACE/custom-jdk/unpacked"&lt;/span&gt;
    &lt;span class="s"&gt;tar -xzf "$GITHUB_WORKSPACE/custom-jdk/custom-jdk-linux-9ad2e63f1763.tar.gz" \&lt;/span&gt;
      &lt;span class="s"&gt;-C "$GITHUB_WORKSPACE/custom-jdk/unpacked"&lt;/span&gt;
    &lt;span class="s"&gt;echo "JAVA_HOME=$GITHUB_WORKSPACE/custom-jdk/unpacked/jdk" &amp;gt;&amp;gt; "$GITHUB_ENV"&lt;/span&gt;
    &lt;span class="s"&gt;echo "$GITHUB_WORKSPACE/custom-jdk/unpacked/jdk/bin" &amp;gt;&amp;gt; "$GITHUB_PATH"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew assembleDebug&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;Even if the collector changes did not clearly win, the process itself was promising. So while this is not a success story in terms of results, it still feels like a good example of how far this kind of experimentation can go.&lt;/p&gt;

&lt;p&gt;And yes, if you are wondering about the name, ImNotOkay came from the song that happened to be playing when I needed one. There is no deeper meaning behind it. The next songs in the playlist were &lt;em&gt;Lateralus&lt;/em&gt; by Tool and &lt;em&gt;People of the Sun&lt;/em&gt; by Rage Against the Machine, so this was probably the safest outcome.&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>android</category>
    </item>
    <item>
      <title>Using RSS to Understand Memory Pressure in CI Builds</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Sat, 28 Mar 2026 23:44:46 +0000</pubDate>
      <link>https://dev.to/cdsap/using-rss-to-understand-memory-pressure-in-ci-builds-1358</link>
      <guid>https://dev.to/cdsap/using-rss-to-understand-memory-pressure-in-ci-builds-1358</guid>
      <description>&lt;p&gt;Once in a while, you may have wondered why builds running on CI agents can still hit OOM errors, even on machines with large amounts of memory. For example, how is it possible to hit an OOM on a 32 GB machine even after setting a 16 GB heap?&lt;/p&gt;

&lt;p&gt;The first and most immediate answer is that the value configured via &lt;code&gt;jvmargs&lt;/code&gt; in &lt;code&gt;gradle.properties&lt;/code&gt; applies only to the heap of the Gradle process. From the operating system’s point of view, a JVM process is composed of more than just the heap. Several additional components contribute to the total memory footprint, and these are often overlooked when sizing CI agents or tuning memory limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metaspace&lt;/li&gt;
&lt;li&gt;Code cache&lt;/li&gt;
&lt;li&gt;Thread stacks&lt;/li&gt;
&lt;li&gt;Direct buffers&lt;/li&gt;
&lt;li&gt;GC native memory&lt;/li&gt;
&lt;li&gt;Native / OS memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are grouped under the RSS (Resident Set Size) of the Java process on Unix-like systems.&lt;/p&gt;

&lt;p&gt;Another important reason is that the Gradle process is not the only JVM involved in a build. We also have the Kotlin daemon, test JVMs, and in Android builds, additional isolated processes such as Lint or R8. Each of these processes has its own heap and its own RSS footprint. Together, all of them contribute to the total memory pressure on the machine.&lt;/p&gt;

&lt;p&gt;In OOM scenarios, there is an additional problem: the host machine may kill the Gradle process before the build finishes. When that happens, we lose valuable diagnostic data. In CI environments, and especially in GitHub Actions, this is even worse because we usually cannot attach post-build steps to collect more information.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbxup9y4mzwzqodh79bty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbxup9y4mzwzqodh79bty.png" alt="Post setup actions" width="428" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since OOM scenarios are exactly the situations where visibility matters most, I ended up building a GitHub Action for that: &lt;a href="https://process-watcher.web.app/" rel="noopener noreferrer"&gt;Process Watcher&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this article, we track memory behavior over time across JVM processes, combining RSS, heap usage, and GC activity. The goal is to move beyond static numbers and understand how memory pressure evolves during the build.&lt;/p&gt;
&lt;h3&gt;
  
  
  Capturing the RSS of a process
&lt;/h3&gt;

&lt;p&gt;To understand real memory usage during build execution, we need to analyze RSS, not just heap size. On Unix-like systems, RSS reflects the physical memory currently held by the process, which makes it a better signal for understanding memory pressure.&lt;/p&gt;

&lt;p&gt;To check the RSS of a process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;rss&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command outputs values in kilobytes, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;654321
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means ~639 MB of physical RAM.&lt;/p&gt;

&lt;p&gt;At first glance, we could collect this data at the end of the build. But this has two problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not all processes live until the end&lt;/li&gt;
&lt;li&gt;The build can be killed or time out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the build is killed, we lose everything, no data, no diagnostics.&lt;/p&gt;

&lt;p&gt;Because of that, I decided to take a different approach: run a separate monitoring process during the build.&lt;/p&gt;

&lt;p&gt;Initially, the approach was simple: capture RSS and heap usage during the build and archive the data at the end of the execution. A typical output looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Elapsed&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;Time&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;PID&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Name&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Heap&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;Used&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Heap&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;Capacity&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;RSS&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt;
&lt;span class="ld"&gt;00:00:05&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;149&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;GradleDaemon&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;29.7&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;86.0&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;241.0&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt;
&lt;span class="ld"&gt;00:00:10&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;149&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;GradleDaemon&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;191.7&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;338.0&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;560.1&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt;
&lt;span class="ld"&gt;00:00:16&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;149&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="k"&gt;GradleDaemon&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;113.1&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;198.0&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="mf"&gt;428.4&lt;/span&gt;&lt;span class="k"&gt;MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, we can visualize the data and calculate the total RSS across all Gradle processes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiua5kn883qlv61qm7t4r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiua5kn883qlv61qm7t4r.png" alt="Visualizing RSS monitoring" width="800" height="310"&gt;&lt;/a&gt;&lt;br&gt;
In addition to RSS and heap usage, we can also track cumulative GC time and better understand how memory behaves during the build.&lt;/p&gt;

&lt;p&gt;That worked for successful builds, but it had the same limitation: if the main Gradle process was killed, we lost all the data.&lt;/p&gt;

&lt;p&gt;To address that, I added a remote mode that publishes the data to a Firebase database, allowing live monitoring even when the build fails or is interrupted.&lt;/p&gt;

&lt;p&gt;With that in place, we can now look at some practical scenarios where this kind of visibility helps explain memory behavior in Android builds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The case of misaligned Kotlin versions
&lt;/h3&gt;

&lt;p&gt;We start with a known suspect. As mentioned in previous articles, this scenario can happen when the Kotlin version embedded in the Gradle distribution is misaligned with the Kotlin version used by the project.&lt;/p&gt;

&lt;p&gt;If we run a typical &lt;code&gt;nowinandroid&lt;/code&gt; build (&lt;code&gt;:app:assembleProdDebug&lt;/code&gt;) and attach our GitHub Actions instrumentation tool, we observe the following memory profile:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikuf64qdczlc6llk6sr1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikuf64qdczlc6llk6sr1.png" alt="Nowinandroid" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image clearly shows two Kotlin processes. The first one, PID 5133, is spawned during the compilation of the included builds and remains unused during the execution phase.&lt;/p&gt;

&lt;p&gt;Although its heap usage at the end of the build is only 429 MiB, its RSS footprint accounts for 8.4% of the total RSS memory of the build. In environments closer to the memory limit, for example on a free GitHub Actions runner, this alone can represent around 4% of the available memory.&lt;/p&gt;

&lt;p&gt;The key point is that the RSS of this first Kotlin process is never reclaimed, so that memory remains allocated for the entire build. In practice, this reduces the memory available for the rest of the build without providing any benefit during execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeouts
&lt;/h3&gt;

&lt;p&gt;In the second scenario, we analyze another common case. We have heard several times from users that some builds hit the timeout defined in the job configuration. &lt;/p&gt;

&lt;p&gt;These timeouts act as a safeguard against builds running indefinitely, but they also indicate that something is not behaving as expected. When the timeout kills the agent, we lose the Gradle process and any information that would normally be reported at the end of the build.&lt;/p&gt;

&lt;p&gt;In some cases, the issue is related to thread locks. In others, it is an unexpected memory situation that can be understood by analyzing memory metrics across the different JVM processes.&lt;/p&gt;

&lt;p&gt;For instance, let’s analyze this build:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpo9fiuiwjlx3eqds1m9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwpo9fiuiwjlx3eqds1m9.png" alt="Timeout build" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The total RSS is not hitting the maximum, and the agent is not killed due to memory pressure. Instead, the build is terminated by a timeout.&lt;/p&gt;

&lt;p&gt;At first glance, this does not look like a typical OOM scenario. But if we look at how memory behaves over time, a different pattern appears.&lt;/p&gt;

&lt;p&gt;One interesting detail is that we observe an almost flat pattern in the later stages of both the Kotlin and Gradle processes. In this case, it is useful to review the GC graph:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59npiyv1m0r3b77wp4aw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59npiyv1m0r3b77wp4aw.png" alt="GC growth in timeout scenario" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GC activity of the Gradle process shows a clear linear growth over time, which indicates that the heap is under pressure and memory is not being reclaimed efficiently. This is the kind of pattern that may not immediately fail the build, but still keeps it alive in a degraded state until the timeout is reached.&lt;/p&gt;

&lt;p&gt;The key point is that if we detect this pattern early, we can stop the build sooner and avoid wasting time and resources. In this example, that could save up to 30% of the build time.&lt;/p&gt;

&lt;p&gt;To get a more realistic view, this scenario shows that if we detect this behavior early, we could cancel the build and avoid the overhead of letting it continue:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft06ma6dvautzxmdhh5bl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft06ma6dvautzxmdhh5bl.gif" alt="Timeout gif" width="500" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  G1 vs Parallel GC
&lt;/h4&gt;

&lt;p&gt;Another common question is which GC is more suitable. Performance is important, but we should also consider the RSS footprint when comparing different GC strategies.&lt;/p&gt;

&lt;p&gt;In many cases, we focus only on build time, but memory behavior can vary significantly between GC implementations. Some may be faster, while others allow the OS to reclaim memory more efficiently.&lt;/p&gt;

&lt;p&gt;Let’s look at a G1 vs Parallel comparison of the build:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi5fyzyyhw6s2cejqn6us.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi5fyzyyhw6s2cejqn6us.gif" alt="Compare G1 Vs Parallel" width="720" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From these measurements, we can observe that, regardless of the performance outcome, the OS is able to reclaim memory more efficiently with G1. That tradeoff can matter in CI environments where staying below the memory threshold is more important than small differences in execution time.&lt;/p&gt;

&lt;p&gt;For completeness, here is the cumulative view, left G1, right Parallel:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfv5r1yvltvp6h62h8sx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcfv5r1yvltvp6h62h8sx.gif" alt="G1 vs Parallel cumulative GC comparison" width="720" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The OOM puzzle
&lt;/h4&gt;

&lt;p&gt;Finally, let’s look at perhaps the most valuable use case for this kind of monitoring: an OOM-killed build.&lt;br&gt;
It all starts with this discouraging message in GitHub Actions, where we don't get any additional feedback and the post steps haven’t been executed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F071vz7usacu0tqbyomnt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F071vz7usacu0tqbyomnt.png" alt="OOM failure in GitHub Actions" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We get no useful feedback, and as mentioned before, we also do not have the chance to run post steps to archive logs, measurements, or any other diagnostic data. &lt;/p&gt;

&lt;p&gt;In this case, if we enable the remote mode of the Build Process Watcher, we can at least preserve the latest snapshot of the Gradle processes before the agent kills the container or the build. What we get is this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb068ldt8vuy1aqgfdwkc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb068ldt8vuy1aqgfdwkc.png" alt="RSS before OOM kill" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We know that GitHub Actions free runners have a 16 GB memory limit, and in the image we can already see that, before the failure, the Gradle process was increasing its RSS in a clear high-memory-pressure scenario.&lt;/p&gt;

&lt;p&gt;In this case, the Gradle heap was configured to 10 GB. One common misconception is to assume that this is enough, or to leave the Kotlin daemon heap unspecified, assuming the defaults will be good enough. But the important detail is that the Kotlin process still contributes its own memory footprint. Looking at the data, we can see that its peak RSS reached 6962.0 MB, adding more pressure on top of the Gradle process.&lt;/p&gt;

&lt;p&gt;So it is easy to see how the build gets dangerously close to the machine limit. But the point is not only to spot the problem, it is to make the build work.&lt;/p&gt;

&lt;p&gt;What I did here was try different memory splits between the Gradle and Kotlin processes and compare the runs with Process Watcher. Looking at RSS growth, heap usage, GC behavior, and whether the build completed or not gave me a better direction, but it still took some experimentation to find a stable configuration.&lt;/p&gt;

&lt;p&gt;In this case, the fix was not just increasing memory. After trying different combinations, the only stable one was 7 GB for Gradle and 3 GB for the Kotlin process. Other combinations were still ending in OOM or timeout:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7kxde4vosbagr7leker.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7kxde4vosbagr7leker.png" alt="Optimized build" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So for me, the interesting part here is not only the final numbers, but how we got there. By observing the RSS pressure and comparing the runs, we were able to move from an unstable memory profile to a configuration that was sustainable for the runner limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;In my case, for GitHub Actions, I published Process Watcher, but the general idea is simple and can be implemented in different ways. The important part is not the specific tool, but having a way to observe RSS, heap usage, and GC behavior while the build is running. That visibility makes it much easier to understand memory pressure and iterate toward more stable configurations.&lt;/p&gt;

&lt;p&gt;One note: to use the visualization tools in Process Watcher, you do not need to enable the remote option. The site provides a &lt;a href="https://process-watcher.web.app/replay.html" rel="noopener noreferrer"&gt;replay&lt;/a&gt; view and a &lt;a href="https://process-watcher.web.app/compare.html" rel="noopener noreferrer"&gt;compare&lt;/a&gt; view that can be used with the artifacts generated at the end of the build, without publishing data to Firebase. You can also just download the generated HTML files and open them locally.&lt;/p&gt;

&lt;p&gt;Happy Building!&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>gradle</category>
    </item>
    <item>
      <title>What Happens When You Kill the Kotlin Daemon Before R8?</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Tue, 30 Dec 2025 15:15:04 +0000</pubDate>
      <link>https://dev.to/cdsap/what-happens-when-you-kill-the-kotlin-daemon-before-r8-el7</link>
      <guid>https://dev.to/cdsap/what-happens-when-you-kill-the-kotlin-daemon-before-r8-el7</guid>
      <description>&lt;p&gt;Every Android build spins up more JVM processes than most developers realize. Beyond the Gradle wrapper, there’s the main Gradle process orchestrating the build and several child processes created for specific tasks. These include test processes that execute in isolation, optional separate JVMs for tools like Lint or R8 when configured to run out of process, individual Java tasks that may also run in their own JVM, and the Kotlin compiler process, which delegates all Kotlin compilation units to a dedicated Kotlin daemon:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hxzexyzk792iyls0t34.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2hxzexyzk792iyls0t34.png" alt=" " width="518" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Understanding how these processes interact helps uncover hidden inefficiencies that affect both memory usage and build performance, especially in CI environments.&lt;/p&gt;

&lt;p&gt;In general, these child processes terminate once their associated task has completed. However, that’s not the case for the Kotlin process. If we inspect the running JVMs after a build finishes, we can still see both the main Gradle process and the Kotlin daemon alive — even after all compilation work has ended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3564 GradleDaemon
15564 KotlinCompileDaemon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This behavior is intentional. The Kotlin daemon stays alive to speed up incremental builds by avoiding the startup overhead of a new compiler process. While that optimization is useful for local development, it provides little benefit in CI environments, where builds are typically clean and short-lived.&lt;/p&gt;

&lt;p&gt;In Android builds, which are often highly modularized, the Kotlin compiler is required across all modules that include Kotlin sources. Because of this, the Kotlin process remains active throughout the compilation of all modules. The last compilation unit is typically the main entry point or Android application module, and these modules tend to be heavier, often running additional demanding tasks after Kotlin compilation. This makes it a natural point where the Kotlin process is no longer needed, and releasing its memory can benefit the tasks that follow. This detail can be crucial in environments that are close to their available memory threshold, where freeing the Kotlin process at the right moment can prevent OOM errors and improve overall build stability.&lt;/p&gt;

&lt;p&gt;The main goal of this article is to experiment and measure the impact of changing this process behavior, based on the idea that a persisted process is not required once it has completed all the tasks associated with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  R8 Tasks
&lt;/h3&gt;

&lt;p&gt;In this analysis, we focus specifically on release builds. These builds execute the R8 task, whose main responsibility is to shrink, obfuscate, and optimize the app components that will be packaged into the final binary. This task plays a critical role in Android builds and is often a major contributor to build duration due to its computational cost.&lt;/p&gt;

&lt;p&gt;An interesting aspect of R8 is that, by design, it runs at the very end of the build process, once all other compilation and linking phases have been completed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fey0056u84l0m327mpswj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fey0056u84l0m327mpswj.png" alt=" " width="800" height="78"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because of this sequencing, there’s no overlap between R8 and the Kotlin compiler within the same build variant. Once Kotlin compilation has finished, R8 operates independently, processing bytecode, resources, and dependencies. This means that the Kotlin process, still alive at this point, isn’t performing any work and can safely be terminated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6ly6j7x3zwuyv772rpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6ly6j7x3zwuyv772rpj.png" alt=" " width="501" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal is to free unnecessary memory before R8 executes, ensuring the system has as much available memory as possible for the final phase of the build. While this doesn’t directly address R8’s primary bottleneck, its CPU-bound processing, it introduces a secondary hypothesis worth testing:&lt;br&gt;
tasks may execute faster in more isolated process environments, where memory and CPU resources face less contention.&lt;/p&gt;
&lt;h3&gt;
  
  
  How to terminate the process
&lt;/h3&gt;

&lt;p&gt;The implementation is straightforward. We implemented a &lt;code&gt;ValueSource&lt;/code&gt; that, through an injected &lt;code&gt;ExecOperations&lt;/code&gt;, executes the command used to kill the existing Kotlin processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;obtain&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;execOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;commandLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ignored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allowed us to create a new provider with the value source and the termination command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KillKotlinCompileDaemonValueSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DEFAULT_COMMAND&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Terminate Kotlin processes:&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;DEFAULT_COMMAND&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"jps | grep -E \"KotlinCompileDaemon\" | awk '{print \$1}' | xargs -r kill -9"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we have created a task that receives the provider as input and executes the command during the task action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KillKotlinCompileDaemonTask&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;DefaultTask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;
    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;kotlinDaemonKillInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nd"&gt;@TaskAction&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;killDaemons&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;kotlinDaemonKillInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lifecycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Kill Kotlin compile daemon command executed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we wire up when we want to execute the termination process. We register our task and make it run after the Kotlin compile task in the main application module.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple demonstration
&lt;/h3&gt;

&lt;p&gt;How does this look in practice? If we analyze only the memory allocation of build child processes, we see that after our task runs and kills the Kotlin process, the impact is clear:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl430deq2s4cdntye69ir.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl430deq2s4cdntye69ir.png" alt=" " width="672" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also a bigger benefit. We are not just releasing the heap allocation of the Kotlin process, we are freeing the entire memory footprint of that process and giving it back to the operating system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphj2r5rqczhgw9ijui7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphj2r5rqczhgw9ijui7n.png" alt=" " width="664" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same applies in environments where Kotlin versions are not aligned and multiple Kotlin processes exist:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F14j9x83xue1owhynwd8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F14j9x83xue1owhynwd8h.png" alt=" " width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In summary, we are successfully returning the Kotlin process memory back to the OS and creating a lighter environment before R8 execution. Next, we analyze the results of the experiments. &lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment environment
&lt;/h3&gt;

&lt;p&gt;Because we want to measure the impact in CI builds, we set the entire experiment in GitHub Actions.&lt;br&gt;
We selected three different projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nowinandroid: &lt;a href="https://github.com/android/nowinandroid" rel="noopener noreferrer"&gt;https://github.com/android/nowinandroid&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A synthetic project with 120 modules created by &lt;a href="https://github.com/cdsap/ProjectGenerator" rel="noopener noreferrer"&gt;ProjectGenerator&lt;/a&gt;: &lt;a href="https://github.com/cdsap/androidRectangle120modules" rel="noopener noreferrer"&gt;https://github.com/cdsap/androidRectangle120modules&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Signal Android app: &lt;a href="https://github.com/signalapp/Signal-Android" rel="noopener noreferrer"&gt;https://github.com/signalapp/Signal-Android&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each project has two variants: the default main configuration, and a variant applying the &lt;a href="https://github.com/cdsap/R8Booster" rel="noopener noreferrer"&gt;R8Booster plugin&lt;/a&gt; in the Android application module. This plugin provides the task that terminates the Kotlin process right after the Kotlin compilation phase.&lt;br&gt;
For the iterations, we ran one warm-up build to download dependencies and initialize the Gradle User Home, followed by 100 iterations of the assembleRelease task on fresh agents, reusing only dependencies and transforms from the cache.&lt;/p&gt;

&lt;p&gt;The tasks under experiment were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;nowinandroid: &lt;code&gt;:app:assembleProdRelease&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;synthetic project: &lt;code&gt;assembleRelease&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signal Android: &lt;code&gt;:Signal-Android:assemblePlayProdRelease&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Results Experiment
&lt;/h3&gt;

&lt;p&gt;After preparing the environment and running 100 iterations per variant, we aggregated the results to compare build time, R8 task duration, and peak memory usage.&lt;/p&gt;

&lt;p&gt;The following table shows the results of the experiments for the main metrics across all projects, based on the mean values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Build Time Improvement&lt;/th&gt;
&lt;th&gt;R8 Task Improvement&lt;/th&gt;
&lt;th&gt;Max Memory Reduction&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Now in Android&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;td&gt;14.7%&lt;/td&gt;
&lt;td&gt;Minimal time change, strong memory reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Synthetic Project (120 modules)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.7%&lt;/td&gt;
&lt;td&gt;5.6%&lt;/td&gt;
&lt;td&gt;13.3%&lt;/td&gt;
&lt;td&gt;Moderate, consistent gains across all metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Signal Android App&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;7.0%&lt;/td&gt;
&lt;td&gt;14.5%&lt;/td&gt;
&lt;td&gt;Best overall results, closest to a real project scenario&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Terminating the Kotlin process after compilation had no negative impact on build performance and noticeably reduced memory usage across all projects.&lt;br&gt;
The Signal Android app, representing a real-world scenario, achieved the best overall gains, with build times 3% faster, R8 7% faster, and memory reduced by about 15%.&lt;/p&gt;

&lt;h4&gt;
  
  
  nowinandroid
&lt;/h4&gt;

&lt;p&gt;Build Scans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ge.solutions-team.gradle.com/scans/?search.startTimeMax=1766616310305&amp;amp;search.startTimeMin=1766270710305&amp;amp;search.tags=cdsap-259_varianta_r8_with_kotlin_process&amp;amp;search.timeZoneId=America%2FSanto_Domingo" rel="noopener noreferrer"&gt;Main Branch&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans/?search.startTimeMax=1766616310305&amp;amp;search.startTimeMin=1766270710305&amp;amp;search.tags=cdsap-259_variantb_kill_kotlin_before_r8&amp;amp;search.timeZoneId=America%2FSanto_Domingo" rel="noopener noreferrer"&gt;Terminate Kotlin Process&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Build time
&lt;/h5&gt;

&lt;p&gt;Unit: seconds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficic4k23h7cri85f03k0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficic4k23h7cri85f03k0.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;283&lt;/td&gt;
&lt;td&gt;283&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;283&lt;/td&gt;
&lt;td&gt;283&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;294&lt;/td&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h5&gt;
  
  
  R8 Task
&lt;/h5&gt;

&lt;p&gt;Unit: seconds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbwl11r8in4kl4oxlaenr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbwl11r8in4kl4oxlaenr.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;td&gt;137&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h5&gt;
  
  
  Peak Build Memory
&lt;/h5&gt;

&lt;p&gt;Unit: GiB&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wvrwv18msurhxynenus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wvrwv18msurhxynenus.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;8.69&lt;/td&gt;
&lt;td&gt;7.41&lt;/td&gt;
&lt;td&gt;1.28&lt;/td&gt;
&lt;td&gt;14.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;8.69&lt;/td&gt;
&lt;td&gt;7.38&lt;/td&gt;
&lt;td&gt;1.31&lt;/td&gt;
&lt;td&gt;15.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;8.81&lt;/td&gt;
&lt;td&gt;7.72&lt;/td&gt;
&lt;td&gt;1.09&lt;/td&gt;
&lt;td&gt;12.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Synthectic project 120 modules
&lt;/h4&gt;

&lt;p&gt;Build Scans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ge.solutions-team.gradle.com/scans/?search.startTimeMax=1766624555795&amp;amp;search.startTimeMin=1766278955795&amp;amp;search.tags=cdsap-260_varianta_r8&amp;amp;search.timeZoneId=America%2FSanto_Domingo" rel="noopener noreferrer"&gt;Main Branch&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans/?search.startTimeMax=1766624555795&amp;amp;search.startTimeMin=1766278955795&amp;amp;search.tags=cdsap-260_variantb_terminating_kotlin_before_r8&amp;amp;search.timeZoneId=America%2FSanto_Domingo" rel="noopener noreferrer"&gt;Terminate Kotlin Process&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Build time
&lt;/h5&gt;

&lt;p&gt;Unit: seconds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzz31h0nsc008wq23vjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqzz31h0nsc008wq23vjo.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;654&lt;/td&gt;
&lt;td&gt;643&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;652&lt;/td&gt;
&lt;td&gt;643&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;676&lt;/td&gt;
&lt;td&gt;660&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h5&gt;
  
  
  R8 Task
&lt;/h5&gt;

&lt;p&gt;Unit: seconds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyv4et8du5k70kbnanqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyv4et8du5k70kbnanqn.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h5&gt;
  
  
  Peak Build Memory
&lt;/h5&gt;

&lt;p&gt;Unit: GiB&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4adsgb1eb8rgypdeo82v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4adsgb1eb8rgypdeo82v.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;11.77&lt;/td&gt;
&lt;td&gt;10.20&lt;/td&gt;
&lt;td&gt;1.57&lt;/td&gt;
&lt;td&gt;13.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;11.72&lt;/td&gt;
&lt;td&gt;10.19&lt;/td&gt;
&lt;td&gt;1.53&lt;/td&gt;
&lt;td&gt;13.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;12.07&lt;/td&gt;
&lt;td&gt;10.54&lt;/td&gt;
&lt;td&gt;1.53&lt;/td&gt;
&lt;td&gt;12.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Signal Android
&lt;/h4&gt;

&lt;p&gt;Build Scans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ge.solutions-team.gradle.com/scans/?search.startTimeMax=1766715029526&amp;amp;search.startTimeMin=1766369429526&amp;amp;search.tags=cdsap-263_varianta_r8&amp;amp;search.timeZoneId=America%2FSanto_Domingo" rel="noopener noreferrer"&gt;Main Branch&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1766530799999&amp;amp;search.startTimeMin=1766271600000&amp;amp;search.tags=cdsap-259_variantb_kill_kotlin_before_r8&amp;amp;search.timeZoneId=Europe%2FMadrid" rel="noopener noreferrer"&gt;Terminate Kotlin Process&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Build time
&lt;/h5&gt;

&lt;p&gt;Unit: seconds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjgmqtvtmgar1s9upyqtb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjgmqtvtmgar1s9upyqtb.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;604&lt;/td&gt;
&lt;td&gt;585&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;601&lt;/td&gt;
&lt;td&gt;583&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;626&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h5&gt;
  
  
  R8 Task
&lt;/h5&gt;

&lt;p&gt;Unit: seconds&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1afh2zj6eili6bjl02c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1afh2zj6eili6bjl02c.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;225&lt;/td&gt;
&lt;td&gt;207&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h5&gt;
  
  
  Peak Build Memory
&lt;/h5&gt;

&lt;p&gt;Unit: GiB&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F798zzndh3y0834jj2l8b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F798zzndh3y0834jj2l8b.png" alt=" " width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;main&lt;/th&gt;
&lt;th&gt;Terminate Kotlin Process&lt;/th&gt;
&lt;th&gt;Diff&lt;/th&gt;
&lt;th&gt;Diff %&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mean&lt;/td&gt;
&lt;td&gt;15.38&lt;/td&gt;
&lt;td&gt;13.15&lt;/td&gt;
&lt;td&gt;2.23&lt;/td&gt;
&lt;td&gt;14.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;15.39&lt;/td&gt;
&lt;td&gt;13.13&lt;/td&gt;
&lt;td&gt;2.26&lt;/td&gt;
&lt;td&gt;14.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;15.45&lt;/td&gt;
&lt;td&gt;14.13&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Final Words
&lt;/h3&gt;

&lt;p&gt;The results show a positive impact. In the project closest to a real application, we saw the best gains in memory reduction and measurable decreases in R8 and overall build times. This confirms the expectation that releasing the Kotlin compiler process returns OS memory in full and can help builds that have heavier Kotlin phases. In practice, this translates into a lower peak memory usage, which, when close to system thresholds, can be the difference between a successful build and an out-of-memory error in CI environments.&lt;/p&gt;

&lt;p&gt;While we focused entirely on the R8 tasks, you may want to experiment with this approach in other scenarios where the Kotlin compiler is no longer required, but a heavier post-compilation task follows, such as Java compilation.&lt;/p&gt;

&lt;p&gt;Remember that this approach was tested only for single-variant release builds. If you want to apply the same idea elsewhere, you will need to adjust the orchestration for when the Kotlin process should be terminated based on your project’s requirements.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>android</category>
      <category>gradle</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Gradle Learning Day: Reinforcement Learning for Build Optimization</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Sat, 23 Aug 2025 18:07:55 +0000</pubDate>
      <link>https://dev.to/cdsap/gradle-learning-day-reinforcement-learning-for-build-optimization-2oh7</link>
      <guid>https://dev.to/cdsap/gradle-learning-day-reinforcement-learning-for-build-optimization-2oh7</guid>
      <description>&lt;p&gt;This month at Gradle, we had our Learning Day, a day dedicated to exploring new ideas and experimenting with technologies outside our usual work. The theme this time was AI.&lt;/p&gt;

&lt;p&gt;While brainstorming ideas, I remembered a video that completely blew my mind, one that I especially loved as a soccer fan:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ta99S6Fh53c"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;That led me to think about reinforcement learning, a branch of machine learning where a system learns through rewards and penalties from its actions. &lt;/p&gt;

&lt;p&gt;In the build engineering world, there’s always a recurring question: what’s the optimal configuration, for example in terms of heap memory or workers, for a project? It’s a tough one because the answer depends on many factors, and often the most honest reply is simply, "It depends."&lt;/p&gt;

&lt;p&gt;So my idea was: why not use reinforcement learning to help calculate the best build configuration? During Learning Day, I started a small experiment, later expanded it, and that’s what I’m sharing here today.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Quick Look at Reinforcement Learning
&lt;/h3&gt;

&lt;p&gt;Reinforcement Learning is a type of machine learning where an agent makes decisions by interacting with an environment. Each action gives the agent a reward or a penalty, and over time, the agent learns which decisions lead to better results:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvtx401dadin1g7runyx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvtx401dadin1g7runyx.png" alt="RL wikipedia"&gt;&lt;/a&gt;&lt;br&gt;
(Image: &lt;a href="https://en.wikipedia.org/wiki/Reinforcement_learning" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Reinforcement_learning&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;
  
  
  Building an RL Framework for Gradle
&lt;/h3&gt;

&lt;p&gt;This was the initial idea: explore whether reinforcement learning could treat Gradle builds as its environment and use performance as the reward signal. Faster builds or lower memory usage would yield a positive reward, while slower or heavier builds would yield a negative reward. From there, the agent could learn which configurations produce the best outcomes.&lt;/p&gt;

&lt;p&gt;Beyond the RL approach, I also wanted to understand how to deploy the agent. The goal was to demonstrate the full cycle—from defining an experiment to orchestrating the build executions and collecting the resulting data. I’m happy to say I built a working proof of concept: an agent deployed on GCP, integrated with Cloud Functions, with GitHub Runners orchestrating the build executions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tx3c4mltcywxr2ryahv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6tx3c4mltcywxr2ryahv.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, I simplified some parts to get a working POC without spending too much time. You’ll find more details in the next sections. Here’s a high-level diagram of the setup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzroqogzfaxbuobj8b6yl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzroqogzfaxbuobj8b6yl.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s walk through the main parts.&lt;/p&gt;
&lt;h4&gt;
  
  
  The RL Agent
&lt;/h4&gt;

&lt;p&gt;The RL agent is the brain of the system, proposing which configurations to try. What do I consider “configurations”? Any setting that materially affects build performance. Today there are hundreds of such parameters across the JVM, Gradle, Kotlin, and even component-specific systems like AGP or Dagger. Initially, I targeted JVM parameters. A production-ready optimization system would expand to include individual JVM flags (e.g., -XX:NewRatio, -XX:MaxMetaspaceSize), garbage collector selection, and compiler optimizations. For this POC, I focused on just three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle Workers&lt;/li&gt;
&lt;li&gt;Xmx for Gradle process&lt;/li&gt;
&lt;li&gt;Xmx for Kotlin process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I know it’s simple, but it’s a solid starting point. Even with just three parameters, the number of combinations grows quickly. Since we don’t have infinite resources or time to test every combination, I added guardrails to constrain the search space and define which options we’ll explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I’m constrained by the environment (GHA), which provides only 4 workers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All experiments were run on modularized Android projects, and by 2026 I know it’s not realistic to build with just 1 GB of memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To avoid OOMs, I limited the max heap to 8 GB in both processes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this experiment, the reward was build time — yes, just build time. I initially started with a formula that included GC metrics for both processes and the mean Kotlin compile time, but the idea was to keep it simple and working first, so I can iterate later.&lt;/p&gt;

&lt;p&gt;Next, I’ll go over the different learning models I tested:&lt;/p&gt;
&lt;h4&gt;
  
  
  First Attempt with Q-tables
&lt;/h4&gt;

&lt;p&gt;In the initial iteration, I was relying entirely on a Q-table. The Q-table is a lookup table that stores the learned value (Q-value) for each action-state combination. In our Gradle build optimization context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Actions&lt;/strong&gt;: Parameter combinations (max_workers, gradle_heap_gb, kotlin_heap_gb)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q-Values&lt;/strong&gt;: Learned rewards for each parameter combination&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is an example Q-Table entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"4_6_8"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.12524971&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gradle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kotlin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Q-value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.125&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"2_7_5"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.12335408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;workers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gradle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kotlin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Q-value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.123&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"1_2_8"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.11712929&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;gradle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;kotlin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Q-value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.117&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the first experiment run, I observed the following behavior:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n1zbge4zk7uvcpehhqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n1zbge4zk7uvcpehhqg.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One iteration was repeated six times, and with such a small total (15 iterations), I missed the chance to explore further.&lt;/p&gt;

&lt;p&gt;In Q-tables, there are three different phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initialization&lt;/li&gt;
&lt;li&gt;Exploration&lt;/li&gt;
&lt;li&gt;Exploitation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s critical to define appropriate values for exploration and exploitation when, as in our environments, the set of actions is finite. Too much exploitation too early could lead to the problem described earlier. As we’ll see in the GitHub Runner section, I parallelize N builds during action execution, which can increase exploration. I didn’t go deeper into Q-Tables and instead moved on to a simpler approach.&lt;/p&gt;
&lt;h4&gt;
  
  
  Adaptive Exploration Strategy
&lt;/h4&gt;

&lt;p&gt;For the version described in this article, I chose an adaptive exploration strategy where the final “best action” is determined purely by observed build performance, not by learned Q-values. This makes the current implementation even simpler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;best_variant = max(variants, key=lambda v: v.get('reward', 0))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And build performance here is measured purely by build time. I initially tried incorporating GC time from the processes into a distributed reward formula — that was the original idea — but I still need to better understand its implications.&lt;/p&gt;

&lt;h4&gt;
  
  
  RL API Component
&lt;/h4&gt;

&lt;p&gt;The RL API is built with FastAPI, deployed on GCP, and acts as the communication layer between GitHub Actions and the reinforcement learning engine. It exposes endpoints that cover the full experiment lifecycle. The primary endpoint, &lt;code&gt;/get-action&lt;/code&gt;, receives experiment requests and returns Gradle configurations. Another key endpoint, &lt;code&gt;/send-feedback&lt;/code&gt;, ingests build results from GitHub Actions and computes rewards on a continuous logarithmic scale. We also use Firestore to persist action results and experiment metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Github Runners
&lt;/h3&gt;

&lt;p&gt;If you think about it, one might assume we could simply run this locally, serving the RL agent and measuring build executions. While technically possible, this would essentially hijack our system resources during the RL experiment, and any other processes running at the same time could distort the results.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://github.com/cdsap/Telltale" rel="noopener noreferrer"&gt;Telltale&lt;/a&gt;, I’ve already demonstrated that it’s possible to orchestrate sequences of builds across different scenarios while maintaining both isolation and fairness. Following the same philosophy, I didn’t want to base our results on a single build — instead, we ran multiple builds to reduce noise and avoid the trap of regression to the mean.&lt;/p&gt;

&lt;p&gt;Inspired by Telltale, we adopted a similar approach: whatever the RL agent decides to execute, we delegate to GitHub Actions, ensuring it runs in an isolated and repeatable environment:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwbwu0yd99y0labhgwcz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftwbwu0yd99y0labhgwcz.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, we use a seed step with two main purposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Populate the GHA runner cache with dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modify the project using actions to save the project state for later.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this, we execute the build n times based on the number of iterations defined for the experiment. In this initial version, we limited the total to 150 builds to avoid overloading the GHA runners and impacting my teammates in Develocity.&lt;/p&gt;

&lt;p&gt;For each experiment, the parallel builds are distributed as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;15 iterations: 1 seed build + 10 builds per iteration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;30 iterations: 1 seed build + 5 builds per iteration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;50 iterations: 1 seed build + 3 builds per iteration&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, it’s worth noting that the action proposed by the RL agent is passed through a workflow dispatch input defined as &lt;code&gt;rl-actions&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  rl-actions:
    description: 'RL-generated action parameters (JSON string)'
    required: false
    default: '{}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we’ll be able to track the progress of the different actions executed in the experiment directly from GHA:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzv4gepj9ndm77hmxqi9q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzv4gepj9ndm77hmxqi9q.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Collecting data
&lt;/h3&gt;

&lt;p&gt;Having the GitHub runners execute builds in parallel, we still need to collect the action data from each experiment and submit the feedback to the RL Agent.&lt;/p&gt;

&lt;p&gt;The projects under experimentation in this article are connected to &lt;a href="https://gradle.com/develocity/" rel="noopener noreferrer"&gt;Develocity&lt;/a&gt;. Each build publishes a Build Scan to Develocity, and to identify the non-seeding builds of each action we tag them with the experiment and action identifiers, such as &lt;code&gt;experiment-1755896319523_W2_G4_K8&lt;/code&gt;: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxypzeiqnf81t8o282rz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxypzeiqnf81t8o282rz.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.gradle.com/develocity/api-manual/" rel="noopener noreferrer"&gt;Develocity API&lt;/a&gt; provides endpoints to retrieve the initial reward fields needed for calculation, such as build duration and task execution information. Additionally, you can extend the Build Scan data, as I’m doing, to report both the Kotlin process GC time and the Gradle process GC time with the plugins &lt;a href="https://github.com/cdsap/InfoKotlinProcess" rel="noopener noreferrer"&gt;InfoKotlinProcess&lt;/a&gt; and &lt;a href="https://github.com/cdsap/InfoGradleProcess" rel="noopener noreferrer"&gt;InfoGradleProcess&lt;/a&gt;, as custom values:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89hgay84lulhppqknr39.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89hgay84lulhppqknr39.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can aggregate the data using your preferred Develocity API client. In the scope of this article, we are using: &lt;a href="https://github.com/cdsap/BuildExperimentResults" rel="noopener noreferrer"&gt;BuildExperimentResults&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results in Action
&lt;/h3&gt;

&lt;p&gt;I built a working POC where you can trigger an experiment by including the Repository name, the task, and the number of iterations you want to run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspxfkicqiftq3db7qnt1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspxfkicqiftq3db7qnt1.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
The URL is available at: &lt;a href="https://rlgradleld.web.app/" rel="noopener noreferrer"&gt;https://rlgradleld.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’ve disabled the creation of new experiments, but feel free to ping me if you’d like to see a live demo with one of your preferred projects that can run within the free tier of GitHub Actions.&lt;br&gt;
In the UI, you’ll find a set of experiments we’ve run for different projects:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkjhowk5d2ip92nnzdie.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftkjhowk5d2ip92nnzdie.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, I’ve published the repo that contains the RL Agent, Cloud Functions, UI, and GitHub Actions runners:&lt;br&gt;
&lt;a href="https://github.com/cdsap/RLGradleBuilds" rel="noopener noreferrer"&gt;https://github.com/cdsap/RLGradleBuilds&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please follow the instructions and let me know if you have any questions about the setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Working on this Learning Day project was very interesting. I still have some mixed feelings, since the rewards were purely performance-based, and I would have liked to explore more advanced RL mechanisms such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Learning from actions we already know will be bad, to avoid further exploration of those (for instance, running with 1 worker or a very low Gradle heap size).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using a composite reward formula that incorporates GC times and average Kotlin compiler task durations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;So far, all experiments have run in scenarios with only cached dependencies. I plan to extend this to other cases, such as best-case builds or incremental builds, with the ultimate goal of dynamically configuring memory per scenario, guided by the RL mechanism.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the infrastructure side, all the core components are already in place: the RL agent is connected to Cloud Functions, Firestore stores actions and experiment data, and GitHub Actions runners orchestrate executions and process build information published to Develocity. &lt;/p&gt;

&lt;p&gt;Happy experimenting and happy building!&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>android</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Results After 3 Months of Android Gradle Build Experiments with Telltale</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Mon, 23 Jun 2025 03:41:50 +0000</pubDate>
      <link>https://dev.to/cdsap/results-after-3-months-of-android-gradle-build-experiments-with-telltale-4b7l</link>
      <guid>https://dev.to/cdsap/results-after-3-months-of-android-gradle-build-experiments-with-telltale-4b7l</guid>
      <description>&lt;p&gt;It's been three months since I released the &lt;a href="https://cdsap.github.io/Telltale/" rel="noopener noreferrer"&gt;Telltale&lt;/a&gt; GitHub Pages site, automating the creation and analysis of different Gradle build experiments. Previously, running experiments required manual analysis and a companion article. This automation has added both flexibility and speed, enabling us to run more experiments more easily.&lt;/p&gt;

&lt;p&gt;As a quick reminder, Telltale is a framework that automates the infrastructure to run Gradle builds across different variants. It works alongside &lt;a href="https://github.com/cdsap/BuildExperimentResults" rel="noopener noreferrer"&gt;Build Experiment Results&lt;/a&gt; to aggregate the data published to &lt;a href="https://gradle.com/develocity/" rel="noopener noreferrer"&gt;Develocity&lt;/a&gt;. In the latest iteration, I’ve added integration with the OpenAI API to analyze and compare experiment results.&lt;/p&gt;

&lt;p&gt;While many experiments didn’t yield meaningful insights, this article highlights a few that did.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kotlin 2.1.20 vs 2.1.0
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Analysis: &lt;a href="https://cdsap.github.io/Telltale/posts/2025-03-21-cdsap-150-experiment/#summary" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-03-21-cdsap-150-experiment/#summary&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Results: &lt;a href="https://cdsap.github.io/Telltale/reports/experiment_results_20250317175549.html" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/reports/experiment_results_20250317175549.html&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this routine experiment, updating to a patch version of the Kotlin Gradle Plugin, I didn’t observe significant differences in build duration, configuration time, or Kotlin compilation duration. However, one metric did stand out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8dfz56ax8wgjixnfiya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc8dfz56ax8wgjixnfiya.png" alt="Image description" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The IR translation phase of the Kotlin compiler increased significantly in 2.1.20. Although the overall compilation time remained similar, we reported the behavior in &lt;a href="https://youtrack.jetbrains.com/issue/KT-76697" rel="noopener noreferrer"&gt;JetBrains' YouTrack&lt;/a&gt;. The root cause remains unclear—it’s possible the compiler is shifting work between phases, keeping total time stable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing &lt;code&gt;-Xms&lt;/code&gt; usage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Analysis: &lt;a href="https://cdsap.github.io/Telltale/posts/2025-03-21-cdsap-149-experiment/summary" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-03-21-cdsap-149-experiment/summary&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Results: &lt;a href="https://cdsap.github.io/Telltale/reports/experiment_results_20250321002356.html" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/reports/experiment_results_20250321002356.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The -Xms JVM flag defines the initial memory allocation pool. In Android builds, I initially assumed it wouldn't matter much, until I saw &lt;a href="https://www.jasonpearson.dev/" rel="noopener noreferrer"&gt;Jason Pearson&lt;/a&gt; submit a &lt;a href="https://github.com/android/nowinandroid/commit/15a6b583d44322d05cc7f1ac9458f77c69b68d8e" rel="noopener noreferrer"&gt;PR&lt;/a&gt; to nowinandroid enabling this flag for Gradle and Kotlin processes. It seemed like the perfect candidate for analysis.&lt;/p&gt;

&lt;p&gt;The result: a 3.5% reduction in build time by setting the initial heap size.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0afxxl8n7zqsaf5lq3ia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0afxxl8n7zqsaf5lq3ia.png" alt="Image description" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More interestingly, the number of garbage collection operations dropped significantly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4jkrrhpt8tb8d2tnqiik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4jkrrhpt8tb8d2tnqiik.png" alt="Image description" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And naturally, GC duration also improved:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmowull0xlwze5mdorql6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmowull0xlwze5mdorql6.png" alt="Image description" width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bottom line: enabling -Xms led to more efficient memory usage and GC behavior. Thanks, Jason!&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing worker parallelization of the Kotlin compiler
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Analysis: &lt;a href="https://cdsap.github.io/Telltale/posts/2025-03-18-cdsap-136-experiment/" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-03-18-cdsap-136-experiment/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Results: &lt;a href="https://cdsap.github.io/Telltale/reports/experiment_results_20250318005824.html" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/reports/experiment_results_20250318005824.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This experiment explored whether reducing Kotlin compiler parallelization, while keeping the overall Gradle worker pool unchanged, would impact build performance. In highly modularized projects, parallel compilation of many Kotlin modules can increase memory pressure on the system.&lt;/p&gt;

&lt;p&gt;Since our experiments run on GitHub Actions runners with only 4 available workers, we’re constrained in testing more vertically scaled scenarios. Still, this setup allowed us to observe meaningful differences.&lt;/p&gt;

&lt;p&gt;The overall build duration—in both mean and median—remained nearly identical between the two configurations. This indicated that reducing Kotlin parallelism did not negatively impact the total build time.&lt;/p&gt;

&lt;p&gt;However, two key observations stood out:&lt;/p&gt;

&lt;p&gt;The aggregated Kotlin compiler duration remained the same, showing no major speed-up in the Kotlin phase itself.&lt;/p&gt;

&lt;p&gt;The memory usage of the Gradle process decreased slightly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdglnj7705oltkepmjlrm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdglnj7705oltkepmjlrm.png" alt="Image description" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More notably, the task &lt;code&gt;app:mergeExtDexDemoDebug&lt;/code&gt;, one of the most expensive in the build, improved by 10.3% when Kotlin compiler parallelization was reduced:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8ekez3424oxhe37fifd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp8ekez3424oxhe37fifd.png" alt="Image description" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This suggests that reducing Kotlin parallelization can relieve memory pressure, enabling better performance for unrelated Gradle tasks. The Kotlin compiler might be competing for resources with other parts of the build, so reducing its concurrency can help other tasks run more efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel vs G1
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Analysis: &lt;a href="https://cdsap.github.io/Telltale/posts/2025-06-22-cdsap-194-experiment/" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-06-22-cdsap-194-experiment/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Results: &lt;a href="https://cdsap.github.io/Telltale/reports/experiment_results_20250622042015.html" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/reports/experiment_results_20250622042015.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a previous experiment, we compared Parallel GC and G1 GC in the context of the nowinandroid project and observed that Parallel GC consistently outperformed G1. However, nowinandroid is a relatively lightweight project, and we wanted to validate whether those results held in a large build.&lt;/p&gt;

&lt;p&gt;For this experiment, we used a project with 400 modules, which demands significantly more heap during compilation phase. The results confirmed our hypothesis:&lt;br&gt;
Parallel GC was still faster, with a ~6% reduction in build time (around 55 seconds) compared to G1 GC:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7j9btqrj7idtv83v3jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7j9btqrj7idtv83v3jn.png" alt="Image description" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tasks like Kotlin compilation and DEX merging saw consistent performance gains when using Parallel GC:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzfiim6mwx3gf4bc3ouv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzfiim6mwx3gf4bc3ouv.png" alt="Image description" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even at the process level, the main Gradle process showed better performance and resource efficiency under Parallel GC:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy33vn0rjuyhdww2ro9j7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy33vn0rjuyhdww2ro9j7.png" alt="Image description" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This experiment reinforces that Parallel GC continues to perform better even in larger Android projects with high memory demands. However, it’s important to note that these results are specific to CI environments, where consistent throughput and short-lived processes are key.&lt;/p&gt;

&lt;p&gt;If you're tuning a local development environment, G1 GC may still be preferable, as it tends to reclaim OS memory more efficiently, which can improve system responsiveness.&lt;/p&gt;

&lt;p&gt;Ultimately, you should run these experiments in the context of your own project—the optimal GC strategy can vary based on project size, memory constraints, and whether you're building locally or in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other experiments
&lt;/h3&gt;

&lt;p&gt;As we mentioned, not all the experiments are going to show regressions and serve as a safety net to verify that the build performance is not impacted, some of them are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comparing AGP 8.9 vs 8.10.1 &lt;a href="https://cdsap.github.io/Telltale/posts/2025-06-09-cdsap-189-experiment/" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-06-09-cdsap-189-experiment/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gradle 9.0.0-RC1 vs 8.14.2: &lt;a href="https://cdsap.github.io/Telltale/posts/2025-06-19-cdsap-192-experiment/" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-06-19-cdsap-192-experiment/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other experiments didn't confirm our initial hypothesis of reducing build duration based on different configurations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using R8 in a different process with 4gb and G1 &lt;a href="https://cdsap.github.io/Telltale/posts/2025-03-26-cdsap-152-experiment/" rel="noopener noreferrer"&gt;https://cdsap.github.io/Telltale/posts/2025-03-26-cdsap-152-experiment/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;With this, we have completed the first review of experiments during these three months of automating the execution and analysis with Telltale. We will publish a new article in the next months with the results of more experiments. &lt;/p&gt;

&lt;p&gt;Happy Building &lt;/p&gt;

</description>
      <category>gradle</category>
      <category>android</category>
    </item>
    <item>
      <title>Balancing Memory Heap and Performance in Gradle Builds</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Fri, 21 Mar 2025 22:00:06 +0000</pubDate>
      <link>https://dev.to/cdsap/balancing-memory-heap-and-performance-in-gradle-builds-454j</link>
      <guid>https://dev.to/cdsap/balancing-memory-heap-and-performance-in-gradle-builds-454j</guid>
      <description>&lt;p&gt;In this post, we analyze the impact of different memory heap configurations on the performance of a given project. A common assumption is that increasing memory heap allocation improves build performance. However, in this article, we evaluate various metrics to determine the actual effects of different memory settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment Setup
&lt;/h2&gt;

&lt;p&gt;The setup for this experiment is straightforward. The free GitHub Actions runners provide a maximum of 16 GB of memory. Our build process, which involves running &lt;code&gt;assembleDebug&lt;/code&gt; on &lt;code&gt;nowinandroid&lt;/code&gt;, consists of two main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Gradle process&lt;/li&gt;
&lt;li&gt;The Kotlin compiler process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To analyze the impact of memory allocation, we define several configurations within the range of 2.5 GB to 7 GB, increasing in 1 GB increments. The minimum of 2.5 GB was chosen because a 2 GB allocation resulted in an OutOfMemory error.&lt;/p&gt;

&lt;p&gt;The tested configurations are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2.5 GB&lt;/li&gt;
&lt;li&gt;3 GB&lt;/li&gt;
&lt;li&gt;4 GB&lt;/li&gt;
&lt;li&gt;5 GB&lt;/li&gt;
&lt;li&gt;6 GB&lt;/li&gt;
&lt;li&gt;7 GB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These values are set using the following JVM arguments in gradle.properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;org.gradle.jvmargs=-Xmx{$VARIANT}g -Xms{$VARIANT}g ...
kotlin.daemon.jvmargs=-Xmx{$VARIANT}g -Xms{$VARIANT}g ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the same configuration as &lt;a href="https://github.com/android/nowinandroid" rel="noopener noreferrer"&gt;nowinandroid&lt;/a&gt;, where &lt;code&gt;Xmx&lt;/code&gt; and &lt;code&gt;Xms&lt;/code&gt; values are identical, and the G1 garbage collector is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;Each configuration is executed 20 times on clean agents, with dependencies preloaded in the Gradle user home. Each iteration generates a build scan, which is later analyzed using the Develocity API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build Time
&lt;/h3&gt;

&lt;p&gt;The first metric we evaluate is the overall build time for each memory configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8bgdky35gqf5vqgobep.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8bgdky35gqf5vqgobep.png" alt="Image description" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A key observation is that there is no clear correlation between increasing heap allocation and reducing build time. Additionally, the low standard deviation suggests that median build times are similar across configurations. Below is a breakdown of the median build times per configuration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1z29zqd0e9bhejhq2svw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1z29zqd0e9bhejhq2svw.png" alt="Image description" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Maximum Memory Usage
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://gradle.com/develocity/" rel="noopener noreferrer"&gt;Develocity&lt;/a&gt; provides &lt;a href="https://docs.gradle.com/develocity/gradle-plugin/current/#capturing_resource_usage" rel="noopener noreferrer"&gt;resource usage&lt;/a&gt; data in the build scans, we analyze the maximum memory usage of the build process. The results align well with the allocated memory configurations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx2e9hwbwojopi003f3dl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx2e9hwbwojopi003f3dl.png" alt="Image description" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We observe a linear increase in memory usage up to 4 GB. However, for larger allocations, variance increases, suggesting that the Gradle process might be overconfigured in scenarios with 6 GB or 7 GB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kotlin Compiler Memory Usage
&lt;/h3&gt;

&lt;p&gt;Next, we examine memory usage for the Kotlin compiler process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8l3gjh19at9o2ymmc3k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8l3gjh19at9o2ymmc3k.png" alt="Image description" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data here is more scattered. We did not perform a detailed analysis of this variance. From this point onward, we focus on Gradle process data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Garbage Collection Time
&lt;/h3&gt;

&lt;p&gt;Now, we analyze Gradle's garbage collection (GC) time across all observations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4rc9bjbk0ocdnil3lrr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4rc9bjbk0ocdnil3lrr.png" alt="Image description" width="677" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a clearer comparison, we examine the median GC times:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6k7rp3cyghsv5rj3bs3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6k7rp3cyghsv5rj3bs3.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configurations with 2.5 GB and 3 GB allocations exhibit significantly higher GC times. The differences between larger configurations are much smaller.&lt;/p&gt;

&lt;h3&gt;
  
  
  Garbage Collections events
&lt;/h3&gt;

&lt;p&gt;For additional insights, we use the &lt;a href="https://github.com/cdsap/GCReport" rel="noopener noreferrer"&gt;GC Report Plugin&lt;/a&gt;, which logs information about garbage collections during build execution. The first metric to analyze is the aggregated number of collections(excluding &lt;code&gt;Concurrent Mark Cycle&lt;/code&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw23pnmygbapfi7zp0dka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw23pnmygbapfi7zp0dka.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variants with 2.5 GB and 3 GB experience a significantly higher number of GC events.&lt;/li&gt;
&lt;li&gt;As memory allocation increases, the number of collections decreases, but the reduction is not linear.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we analyze &lt;code&gt;Pause Young (Normal) (G1 Evacuation Pause)&lt;/code&gt; events. These pauses occur when application threads stop while objects in the young generation are collected and moved to either survivor spaces or the old generation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu3kmr9lhvvc1vq668c0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuu3kmr9lhvvc1vq668c0.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When examining other factors, such as Humongous objects (Normal), we observe that only the 2.5 GB variant has these entries, Showing short memory state for this variant.&lt;/p&gt;

&lt;p&gt;More interestingly, we analyze &lt;code&gt;Pause Young (Concurrent Start) (G1 Humongous Allocation)&lt;/code&gt; events. These occur when the JVM anticipates old-generation GC pressure and preemptively starts a concurrent cycle—an indicator that memory pressure is increasing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmi22rv91ve1nxshu8xdj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmi22rv91ve1nxshu8xdj.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, lower-memory configurations trigger more collections of this type. Starting at 5 GB, configurations show more stability, with the median value stabilizing at one event per iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;This article analyzed the behavior of the Gradle process under different heap configurations. Key findings include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Increasing memory allocation does not significantly improve build duration in this project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maximum memory usage shows slight variance at higher allocations (6 GB and 7 GB).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Larger heap allocations reduce GC time, but the difference is not substantial.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The number of GC collections decreases with higher allocations, stabilizing around 5 GB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;G1 Humongous Allocation events suggest that configurations with 5 GB or more are better optimized.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Based on these findings, CI environments should balance memory allocation to optimize performance while minimizing resource usage. A 4 GB or 5 GB allocation appears to offer the best trade-off between build performance and memory efficiency.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>gradle</category>
    </item>
    <item>
      <title>Gradle 8.11: Faster Configuration Cache and Improved Configuration Time</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Sun, 17 Nov 2024 03:03:07 +0000</pubDate>
      <link>https://dev.to/cdsap/gradle-811-faster-configuration-cache-and-improved-configuration-time-ja1</link>
      <guid>https://dev.to/cdsap/gradle-811-faster-configuration-cache-and-improved-configuration-time-ja1</guid>
      <description>&lt;p&gt;As modularization becomes increasingly common in Android projects, it increases the number of subprojects within a Gradle build. While modularization brings many benefits—such as improved software development practices and reduced build times by reusing tasks unaffected by code changes—it also has a side effect: the configuration time in Gradle projects increases as the project structure grows. For instance, the following graph represents a build executing the &lt;code&gt;:help&lt;/code&gt; task in a project containing between 100 and 1000 modules, incremented in steps of 100, during a fresh daemon build:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficjbubwcl47x6dy0if2m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficjbubwcl47x6dy0if2m.png" alt="Image description" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.gradle.org/current/userguide/configuration_cache.html" rel="noopener noreferrer"&gt;Configuration Cache feature&lt;/a&gt;, introduced by the Gradle team, addresses this problem by caching the result of the configuration phase of the build, then reusing it in subsequent builds if no relevant changes have occurred. This feature made local development faster and easier to work with, enabling faster build cycles. However, as projects grow, new optimizations to the configuration cache are needed to continue providing the best possible developer experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.gradle.org/current/release-notes.html" rel="noopener noreferrer"&gt;Gradle 8.11&lt;/a&gt; introduces new &lt;a href="https://docs.gradle.org/current/release-notes.html#configuration-cache-improvements" rel="noopener noreferrer"&gt;improvements&lt;/a&gt; to the configuration cache process, including per-project serialization and string deduplication. Additionally, it introduces a new incubating feature that enables storing and loading the configuration cache in parallel, resulting in improved performance.&lt;br&gt;
To enable the feature, add the following flag in &lt;code&gt;gradle.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;org.gradle.configuration-cache.parallel=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this article, we will share the results of our experiments with the new Gradle 8.11 parallel configuration cache feature in the &lt;code&gt;nowinandroid&lt;/code&gt; project and explore how both local and CI builds can benefit from decreased configuration time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;As always, before diving into the results, let's take a look at the experiment:&lt;/p&gt;

&lt;h3&gt;
  
  
  Project
&lt;/h3&gt;

&lt;p&gt;The experiment uses a project forked from &lt;a href="https://github.com/android/nowinandroid" rel="noopener noreferrer"&gt;nowinandroid&lt;/a&gt;(latest &lt;a href="https://github.com/android/nowinandroid/commit/d42262c9391ccd1d59a0c92476c2b349a5acc3af" rel="noopener noreferrer"&gt;commit&lt;/a&gt;).&lt;br&gt;
The task used for this experiment is &lt;code&gt;assembleDebug&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Variants Experiment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/cdsap/Experiment_Gradle_8_11/tree/main" rel="noopener noreferrer"&gt;gradle_8_10&lt;/a&gt;, main branch using 8.10 and configuration cache.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cdsap/Experiment_Gradle_8_11/tree/gradle_8_11" rel="noopener noreferrer"&gt;gradle_8_11&lt;/a&gt;, project using Gradle 8.11 and configuration cache. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/cdsap/Experiment_Gradle_8_11/tree/gradle_8_11_parallel" rel="noopener noreferrer"&gt;gradle_8_11_parallel&lt;/a&gt;, project using Gradle 8.11 and parallel configuration cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scenarios
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Build with configuration cache miss:

&lt;ul&gt;
&lt;li&gt;Dependencies prepopulated in the Gradle user home.&lt;/li&gt;
&lt;li&gt;Using clean agents.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Build with configuration cache hit.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Environment
&lt;/h3&gt;

&lt;p&gt;Github Action runner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux 6.5.0-1025-azure &lt;/li&gt;
&lt;li&gt;4 cores&lt;/li&gt;
&lt;li&gt;JDK 17&lt;/li&gt;
&lt;li&gt;Xmx 4GB (Gradle-Kotlin)&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Scenario Configuration Cache Miss
&lt;/h5&gt;

&lt;p&gt;100 iterations for each variant using &lt;a href="https://github.com/cdsap/Telltale" rel="noopener noreferrer"&gt;Telltale&lt;/a&gt; to orchestrate the execution.&lt;/p&gt;

&lt;h5&gt;
  
  
  Scenario Configuration Cache Hit
&lt;/h5&gt;

&lt;p&gt;20 iterations for each variant using &lt;a href="https://github.com/gradle/gradle-profiler" rel="noopener noreferrer"&gt;Gradle Profiler&lt;/a&gt; and Telltale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The build metrics data is published to &lt;a href="https://gradle.com/develocity/" rel="noopener noreferrer"&gt;Develocity&lt;/a&gt; using build scans.&lt;/li&gt;
&lt;li&gt;With the &lt;a href="https://docs.gradle.com/develocity/api-manual/" rel="noopener noreferrer"&gt;Develocity API&lt;/a&gt;, experiment configuration cache metrics are now accessible via the new &lt;a href="https://docs.gradle.com/enterprise/api-manual/ref/2024.2.html#tag/Builds/operation/GetGradleConfigurationCache" rel="noopener noreferrer"&gt;endpoint&lt;/a&gt;: &lt;code&gt;/api/builds/{id}/gradle-configuration-cache&lt;/code&gt;, introduced in Develocity 2024.2. Example output:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "result": {
    "outcome": "HIT",
    "entrySize": 1254385,
    "load": {
      "duration": 500,
      "hasFailed": false
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Results
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Configuration cache entry size
&lt;/h3&gt;

&lt;p&gt;Before diving into the results related to durations, we first analyze the impact of these optimizations on reducing the size of the cache artifact:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupmlhrrucisctrjsr4av.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupmlhrrucisctrjsr4av.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.11 has reduced the size of the cache entry for the assembleDebug task by 14.67%.&lt;/li&gt;
&lt;li&gt;Enabling parallel configuration results in the same cache artifact size as Gradle 8.11.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration time with cache miss (dependencies already downloaded)
&lt;/h3&gt;

&lt;p&gt;This scenario simulates a configuration cache miss, ensuring that all dependencies are pre-downloaded to eliminate the impact of network latency during dependency resolution. The median result for each variant is as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7nj845xb0tpdm1wvafo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu7nj845xb0tpdm1wvafo.png" alt="Image description" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.11 reduced the configuration time by 4.26%&lt;/li&gt;
&lt;li&gt;Gradle 8.11 with parallel configuration cache reduced the configuration time by 8.16%&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Cache Miss (using clean agents)
&lt;/h3&gt;

&lt;p&gt;In this scenario, we are working with clean agents that request dependencies. The median result for each variant is as follows:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7r44z5nsjdt226ep02fm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7r44z5nsjdt226ep02fm.png" alt="Image description" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.11 reduced the configuration time by 14.5%.&lt;/li&gt;
&lt;li&gt;Gradle 8.11 with parallel configuration further reduced configuration time by 31.72%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here, we have some very interesting results showing that using the parallel configuration cache reduces configuration time by &lt;strong&gt;85 seconds&lt;/strong&gt;. This highlights the significant benefits of enabling parallel configuration cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration cache operations (dependencies already downloaded)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Storing cache entry
&lt;/h4&gt;

&lt;p&gt;Using the Develocity API, we extracted the store operation duration for each variant in the scenario where the dependencies are already provided:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dn9n83qhd0wbx72rga6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dn9n83qhd0wbx72rga6.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comparing 8.10 to 8.11 with parallel configuration cache shows a significant improvement of 29.58% on the median for the operation of saving the configuration cache entry. &lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Load cache entry
&lt;/h4&gt;

&lt;p&gt;Using the Develocity API, this time we extracted the load operation duration for each variant in the scenario where the dependencies are already provided:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdumohs2z5nlqtptrtwn3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdumohs2z5nlqtptrtwn3.png" alt="Image description" width="609" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.11: Offers a significant improvement over 8.10 for this metric, reducing the value by over 20%.&lt;/li&gt;
&lt;li&gt;Parallel Configuration in 8.11 increases the value slightly compared to default 8.11, it still performs better than 8.10 overall.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration cache operations (using clean agents)
&lt;/h3&gt;

&lt;p&gt;In the scenario with clean agents, we noticed high variability due to non-deterministic connectivity operations. For instance, in the case of the load operation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnrlxzieft3300fr20gd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnrlxzieft3300fr20gd.png" alt="Image description" width="694" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and for the store operation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl10e53f1btvpnwvya2bb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl10e53f1btvpnwvya2bb.png" alt="Image description" width="670" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We observed a slight improvement when using 8.11 parallel, but the visualization is noisy. For this reason, we chose to present the percentiles instead:&lt;/p&gt;

&lt;h4&gt;
  
  
  Storing cache entry
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F57lafh0ku3awvub3a489.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F57lafh0ku3awvub3a489.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Gradle 8.11 offers modest improvements over 8.10 across all percentiles, particularly at the median and upper quartile levels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gradle 8.11 with Parallel Configuration Cache dramatically reduces metrics across all percentiles, like the median with an improvement of 65%.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Loading cache entry
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnk70a08k8ni5dmg2davf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnk70a08k8ni5dmg2davf.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.11 median value improved (decreased) by approximately 11.6% when moving from Gradle 8.10.&lt;/li&gt;
&lt;li&gt;Gradle 8.11 parallel configuration median value improved (decreased) by approximately 14.6% when moving from Gradle 8.10.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuration Cache Hit
&lt;/h3&gt;

&lt;p&gt;For the second scenario, using Gradle Profiler, we iterated over the same runner executing the same build. In this case, the configuration cache was hit, and we measured the median configuration time for those builds. The results are as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop4ajts09nwkxaclrr4z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop4ajts09nwkxaclrr4z.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We noticed a slight improvement in the median configuration time when hitting the cache. However, given the size of the project, the reduction in duration is not significant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Cache Hit - load time
&lt;/h3&gt;

&lt;p&gt;Finally, for the same scenario—hitting the configuration cache—we analyzed the output of the Develocity endpoint for the load operation:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pfli2a3x1unep3g8oyt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6pfli2a3x1unep3g8oyt.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.11 and Gradle 8.11 parallel offer significant improvements over 8.10, with reductions of 30.6% and 26%, respectively. However, in the context of this project, the value of the load operation in the experiment is not significant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  References Experiment
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Build Scans
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Configuration cache miss scenarios
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Dependencies Cache&lt;/th&gt;
&lt;th&gt;Clean Agents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731484799999&amp;amp;search.startTimeMin=1731398400000&amp;amp;search.tags=cdsap-18,varianta_main&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731571199999&amp;amp;search.startTimeMin=1731484800000&amp;amp;search.tags=cdsap-20,varianta_main&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731484799999&amp;amp;search.startTimeMin=1731398400000&amp;amp;search.tags=cdsap-19%2Cvarianta_gradle_8_11&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731571199999&amp;amp;search.startTimeMin=1731484800000&amp;amp;search.tags=cdsap-21%2Cvarianta_gradle_8_11&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11 Parallel&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731484799999&amp;amp;search.startTimeMin=1731398400000&amp;amp;search.tags=cdsap-19,variantb_gradle_8_11_parallel&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731571199999&amp;amp;search.startTimeMin=1731484800000&amp;amp;search.tags=cdsap-21,variantb_gradle_8_11_parallel&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Configuration cache hit scenarios
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gradle Profiler Builds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731743999999&amp;amp;search.startTimeMin=1731657600000&amp;amp;search.tags=profiler-cdsap-3,varianta_main&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731743999999&amp;amp;search.startTimeMin=1731657600000&amp;amp;search.tags=profiler-cdsap-4,varianta_gradle_8_11&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11 Parallel&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1731743999999&amp;amp;search.startTimeMin=1731657600000&amp;amp;search.tags=profiler-cdsap-4,variantb_gradle_8_11_parallel&amp;amp;search.tasks=assembleDebug&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;Build Scans&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Experiments
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experiment&lt;/th&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Results&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.10 vs Gradle 8.11&lt;/td&gt;
&lt;td&gt;Dependencies Cache&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Telltale/actions/runs/11809935825" rel="noopener noreferrer"&gt;Experiment&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11 vs Gradle 8.11 Parallel&lt;/td&gt;
&lt;td&gt;Dependencies Cache&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Telltale/actions/runs/11809944800" rel="noopener noreferrer"&gt;Experiment&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.10 vs Gradle 8.11&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Telltale/actions/runs/11827513348" rel="noopener noreferrer"&gt;Experiment&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11 vs Gradle 8.11 Parallel&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Telltale/actions/runs/11828867614" rel="noopener noreferrer"&gt;Experiment&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.10 vs Gradle 8.11&lt;/td&gt;
&lt;td&gt;Gradle Profiler&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Telltale/actions/runs/11864889165" rel="noopener noreferrer"&gt;Experiment&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradle 8.11 vs Gradle 8.11 Parallel&lt;/td&gt;
&lt;td&gt;Gradle Profiler&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Telltale/actions/runs/11865094223" rel="noopener noreferrer"&gt;Experiment&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Final notes
&lt;/h1&gt;

&lt;p&gt;Analyzing the results, we've observed a significant improvement in configuration time when enabling &lt;code&gt;org.gradle.configuration-cache.parallel&lt;/code&gt;. This feature not only reduces the configuration time but also reduces the configuration cache entry size. This means the Gradle model saved in the cache uses less space on disk. As a result, the cache is stored and loaded faster, which is especially helpful for big and complex projects.&lt;/p&gt;

&lt;p&gt;Of course, the results are based on the project under experiment and may vary depending on your project, but we strongly recommend enabling &lt;code&gt;org.gradle.configuration-cache.parallel&lt;/code&gt; to take advantage of these improvements.&lt;/p&gt;

&lt;p&gt;Happy Building!&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>android</category>
    </item>
    <item>
      <title>Telltale: Automating Experimentation in Gradle Builds</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Sat, 28 Sep 2024 21:01:21 +0000</pubDate>
      <link>https://dev.to/cdsap/telltale-automating-experimentation-in-gradle-builds-1h9m</link>
      <guid>https://dev.to/cdsap/telltale-automating-experimentation-in-gradle-builds-1h9m</guid>
      <description>&lt;p&gt;In this article, I introduce the latest iteration of &lt;a href="https://github.com/cdsap/Telltale" rel="noopener noreferrer"&gt;Telltale&lt;/a&gt;, a framework designed to automate experimentation in Gradle builds. This new version extends the execution environment to include different caching modes and environment properties, offering more comprehensive testing capabilities.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5o9lyfusnhtuhfg0j6v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5o9lyfusnhtuhfg0j6v1.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But before we explore these new features, let’s briefly revisit the core concept of Telltale to understand its foundation.&lt;/p&gt;

&lt;p&gt;The original idea behind Telltale was to create a framework that orchestrates experiments across Gradle builds to understand performance impacts by collecting data and providing insights. These experiments are based on comparing the results of executions between two variants. It supports two types of workflow experiments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle Profiler (&lt;code&gt;experiment-with-gradle-profiler.yaml&lt;/code&gt;): The iterations of the variant experiments are executed on the same agent.&lt;/li&gt;
&lt;li&gt;Isolated Iterations (&lt;code&gt;experiment.yaml&lt;/code&gt;): Each iteration is executed on a different agent. This article explains these types of experiments in detail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today, you can use Gradle Profiler to achieve similar results, and in fact, Telltale offers an experiment workflow mode that integrates with Gradle Profiler. It’s an excellent tool that provides flexibility in setting up the experimental environment and includes scenarios for applying incremental changes across iterations. However, with Telltale, my goal was to ensure that each iteration of the experiment runs in complete isolation by executing the builds on different agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But why is such a framework necessary for Gradle builds?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first reason is the nature of experimentation itself. Software projects are in constant flux, evolving with changes in modules, compilation unit sizes, and new tool updates. Additionally, as the infrastructure changes, such as updated JVM configurations, past performance settings can be obsolete. Experimenting with different configurations helps identify the optimal setup for a project’s current state. While we are increasingly familiar with performance factors, there’s always an element of trial and error to empirically understand how changes affect a project.&lt;/p&gt;

&lt;p&gt;The second reason is to create a safety net that helps prevent performance regressions. Once a change is merged into the main branch, it’s often too late to catch these regressions. To address this, a more conservative approach is needed, where the performance impact is evaluated before merging changes. Running regression tests on every pull request (PR), however, is costly and time-consuming. We assume that not all types of changes require regression test execution, so we can limit the scope to PRs that update critical components, such as Java/AGP/KGP/Gradle updates, convention plugins, or central build logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment frameworks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An effective experimentation framework must orchestrate multiple iterations of experiment variants and ensure consistency in the environment for each build execution. It should enable parallel execution of the variants to reduce the overall duration of the experiment. The framework also needs to implement a seeding step to prepare the Gradle caching state for the experiments.&lt;/p&gt;

&lt;p&gt;Additionally, the framework should be flexible enough to allow multiple iterations for each variant, minimizing build variance. The number of iterations will depend on this variance and, of course, on the cost of the resources used by the experiment—you don't want to upset your infrastructure team. Afterward, you need to process the metrics generated by the builds, which should be published for each execution. Finally, the framework needs to analyze this data and provide the results of the experiment.&lt;/p&gt;

&lt;p&gt;The visualization of this process would look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmn65gly3pb16imj384v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhmn65gly3pb16imj384v.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given these requirements, how does Telltale provide a solution?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Telltale approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Telltale provides an opinionated solution to this challenge. It uses GitHub Actions to execute the experiments, relies on &lt;a href="https://gradle.com/develocity/" rel="noopener noreferrer"&gt;Develocity&lt;/a&gt; to publish the data, and utilizes a custom CLI that makes use of &lt;a href="https://docs.gradle.com/develocity/api-manual/" rel="noopener noreferrer"&gt;Develocity API&lt;/a&gt; to process the experiment results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialization&lt;/strong&gt;&lt;br&gt;
At the initialization step, Telltale defines the parameters of the experiment. Those parameters are defined in the workflow experiment template:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftspy2ozsp9y9wy1yinoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftspy2ozsp9y9wy1yinoo.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The parameters of the experiment are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;repository&lt;/code&gt;: The GitHub repository where the experiment will run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;variantA&lt;/code&gt; and &lt;code&gt;variantB&lt;/code&gt;: Branch names for the experiment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;task&lt;/code&gt;: The Gradle task to execute.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iterations&lt;/code&gt;: Number of iterations for each experiment run.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mode&lt;/code&gt;: The type of caching to apply during the experiment. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os_args&lt;/code&gt;: OS for each variant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;java_args&lt;/code&gt;: JDK versions and vendors for each variant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extra_build_args&lt;/code&gt;: Additional Gradle arguments for each variant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extra_report_args&lt;/code&gt;: Configuration for generating reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the new version, we have introduced a mechanism called cache mode. Previously, we executed the variants on fresh agents, which worked well, but in some cases, we want to reduce the interaction with external components—such as downloading dependencies or task caching—to focus on the specific aspects of the experiment. We are now using the &lt;a href="https://github.com/gradle/actions/tree/main/setup-gradle" rel="noopener noreferrer"&gt;Gradle setup action&lt;/a&gt;, and thanks to the flexibility of this GitHub action, we can offer different caching modes in the experiment. The supported modes are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Caching mode&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dependencies cache&lt;/td&gt;
&lt;td&gt;Caches dependencies only, without caching task outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dependencies cache - transforms cache&lt;/td&gt;
&lt;td&gt;Caches dependencies, excluding transforms cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local task cache&lt;/td&gt;
&lt;td&gt;Enables caching of task outputs locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local task cache + dependencies cache&lt;/td&gt;
&lt;td&gt;Combines local task caching with dependency caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local task cache - transforms cache&lt;/td&gt;
&lt;td&gt;Caches task outputs locally, excluding transforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local task cache + dependencies cache - transforms cach&lt;/td&gt;
&lt;td&gt;Combines local task, dependency caching, and excludes transforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remote task cache&lt;/td&gt;
&lt;td&gt;Uses a remote server to cache task outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remote task cache + dependencies cache&lt;/td&gt;
&lt;td&gt;Combines remote task caching with dependency caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remote task cache - transforms cache&lt;/td&gt;
&lt;td&gt;Caches task outputs remotely, excluding transforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remote task cache + dependencies cache - transforms cache&lt;/td&gt;
&lt;td&gt;Combines remote task, dependency caching, and excludes transforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no caching&lt;/td&gt;
&lt;td&gt;Disables all forms of caching&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Seeding&lt;/strong&gt;&lt;br&gt;
As mentioned earlier, in this new version, we are implementing caching modes. Therefore, if the experiment involves caching, we are adding a new step to seed the cache. Thanks to the flexibility of the setup action, we can define how we want to populate the cache, which will later be used during execution. Each variant will execute one build to populate the cache with the elements required for the experiment. For example, if I'm using 'local task cache + dependencies cache,' the task build cache and dependencies used by the project will be provided during the execution of subsequent steps.&lt;/p&gt;

&lt;p&gt;In this step, it is important to mark those builds as seeders to exclude them from the final results. Since we are using Develocity, we add a prefix to the tags used in the build.&lt;br&gt;
Once the cache is seeded, the next step is executing the experiments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;br&gt;
Each variant is executed for &lt;strong&gt;n&lt;/strong&gt; iterations, where the &lt;strong&gt;n&lt;/strong&gt; value is defined during the initialization of the experiment. This is achieved by defining a GitHub Actions matrix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strategy:
   matrix:
      runs: ${{ fromJson(needs.seed.outputs.iterations) }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The builds need to include the various aspects of the experiments. Similar to the seeding steps, we use Develocity tags to indicate the different properties of the experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./gradlew ${{ inputs.task }} ${{ inputs.extra-args }} \
     -Dscan.tag.${{ inputs.run-id }} \
     -Dscan.tag.${{ inputs.variant-prefix }}${{ inputs.variant }} \
     -Dscan.tag."${{ inputs.mode }}" \
     -Dscan.tag.experiment \
     -Dscan.tag.${{ inputs.experiment-id }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reporting&lt;/strong&gt;&lt;br&gt;
Reporting is an optional step enabled by the input &lt;code&gt;extra_report_args&lt;/code&gt; property &lt;code&gt;report_enabled&lt;/code&gt;. In Telltale, reporting is tied to the assumption that the platform processing the builds is Develocity, allowing the use of the Develocity API to process build information for each variant. Specifically, Telltale uses a CLI to process experiment results: &lt;a href="https://github.com/cdsap/BuildExperimentResults" rel="noopener noreferrer"&gt;https://github.com/cdsap/BuildExperimentResults&lt;/a&gt;. The CLI processes the experiment execution with a command like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./build-experiment-results --url=${{ inputs.url }}  \
   --api-key $DV_API
   --variants $VARIANT_A  --variants $VARIANT_B \
   --experiment-id=${{ inputs.experiment-id }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with an output like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9i1dkc025p0qd9p0va8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9i1dkc025p0qd9p0va8.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The type of reports included is configurable, allowing different types:&lt;br&gt;
      - &lt;code&gt;tasktype_report&lt;/code&gt;: Include task type reports.&lt;br&gt;
      - &lt;code&gt;taskpath_report&lt;/code&gt;: Include task path reports.&lt;br&gt;
      - &lt;code&gt;kotlin_build_report&lt;/code&gt;: Include Kotlin build reports. Requires &lt;a href="https://blog.jetbrains.com/kotlin/2022/06/introducing-kotlin-build-reports/" rel="noopener noreferrer"&gt;Kotlin Build Reports&lt;/a&gt;.&lt;br&gt;
      - &lt;code&gt;process_report&lt;/code&gt;: Include process-related reports. Requires &lt;a href="https://github.com/cdsap/InfoKotlinProcess" rel="noopener noreferrer"&gt;InfoKotlinProcess&lt;/a&gt; and &lt;a href="https://github.com/cdsap/InfoGradleProcess" rel="noopener noreferrer"&gt;InfoGradleProcess&lt;/a&gt;.&lt;br&gt;
      - &lt;code&gt;resource_usage_report&lt;/code&gt;: Include resource usage reports. Require builds using Develocity 2024.2.&lt;/p&gt;

&lt;p&gt;Enough talk, let's explore real implementations of Telltale in various scenarios. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use case: Reducing number of workers&lt;/strong&gt; &lt;br&gt;
Let’s start with a simple experiment: verifying if reducing the number of workers impacts build duration and performance. In the first experiment, simulating a worst-case scenario, we are not providing task caching, and to reduce the noise from network interactions, we are providing the dependencies during execution. We will test the main branch using the default configuration with 4 workers, and for variant B, we are using 2 workers. Parameters for the experiment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;repository&lt;/td&gt;
&lt;td&gt;cdsap/TelltaleExperiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant a&lt;/td&gt;
&lt;td&gt;main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant b&lt;/td&gt;
&lt;td&gt;main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterations&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache mode&lt;/td&gt;
&lt;td&gt;dependencies cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build arguments&lt;/td&gt;
&lt;td&gt;variant b: "-Dorg.gradle.workers.max=2"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(&lt;a href="https://github.com/cdsap/TelltaleExperiments" rel="noopener noreferrer"&gt;cdsap/TelltaleExperiments&lt;/a&gt;, the repository used in all of the experiments in this article, it's a fork of the &lt;a href="https://github.com/android/nowinandroid" rel="noopener noreferrer"&gt;nowinandroid&lt;/a&gt; project)&lt;/p&gt;

&lt;p&gt;Experiment results: &lt;a href="https://github.com/cdsap/Telltale/actions/runs/11078433199" rel="noopener noreferrer"&gt;https://github.com/cdsap/Telltale/actions/runs/11078433199&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When comparing the build durations in seconds of both variants, we observe the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6r3s3fd9d0020hgc6s9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6r3s3fd9d0020hgc6s9e.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using all available workers is faster, with a median improvement of 3.30%. Next, we analyze the Kotlin compiler duration for all tasks in the iterations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6abrgyjyzmovsntrozvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6abrgyjyzmovsntrozvr.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The duration of the Kotlin compiler decreased when using two workers. From this, we infer that parallelization affects the performance of the Kotlin compilation. However, this decrease in Kotlin compiler duration does not translate into better overall build times.&lt;/p&gt;

&lt;p&gt;Wondering if this correlates with the Kotlin process max usage, we have the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhiux6qzmjyreuacu6ddz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhiux6qzmjyreuacu6ddz.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We observe better behavior in the variant that reduces the number of workers. This could be an interesting consideration when working in scenarios with high memory pressure, as reducing the process load might benefit build duration.&lt;/p&gt;

&lt;p&gt;The previous experiment was based on a worst-case scenario where all tasks are executed. However, reducing parallelization in this scenario could impact other types of builds. In the next experiment, we will apply the same parameters but add the build cache to simulate a best-case scenario where cache hits occur.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;repository&lt;/td&gt;
&lt;td&gt;cdsap/TelltaleExperiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant a&lt;/td&gt;
&lt;td&gt;main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant b&lt;/td&gt;
&lt;td&gt;main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterations&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache mode&lt;/td&gt;
&lt;td&gt;local task cache + dependencies cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build arguments&lt;/td&gt;
&lt;td&gt;variant b: "-Dorg.gradle.workers.max=2"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Experiment results: &lt;a href="https://github.com/cdsap/Telltale/actions/runs/11079181145" rel="noopener noreferrer"&gt;https://github.com/cdsap/Telltale/actions/runs/11079181145&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The results of the build duration in seconds are:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jl7u2xxyvabqsr3hcxh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3jl7u2xxyvabqsr3hcxh.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The median duration shows better results when using all available workers with the local build cache; however, the difference is not significant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use case: Reducing parallelization of the Kotlin Compiler&lt;/strong&gt; &lt;br&gt;
In the previous section, we verified that reducing the number of workers increases the build duration. At the same time, we observed an interesting insight regarding the Kotlin compiler duration and Kotlin process memory usage. In this experiment, instead of impacting all tasks, we will reduce the parallelization of Kotlin compiler tasks without affecting the other build tasks. By implementing the &lt;a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/services/R8ParallelBuildService.kt;l=23?q=r8parallelb" rel="noopener noreferrer"&gt;same approach&lt;/a&gt; that AGP uses to reduce the parallelization of R8 tasks, we declare a Build service as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KotlinCompileBuildService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;BuildService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BuildServiceParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxParallelUsages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nc"&gt;ServiceRegistrationAction&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;KotlinCompileBuildService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;KotlinCompileBuildService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;maxParallelUsages&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BuildServiceParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To later update the convention plugin that defines the Android or Kotlin library with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureKotlinWithBuildServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxParallelUsage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;RegistrationAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;maxParallelUsage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;withType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;KotlinCompile&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;configureEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;usesService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;getBuildService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gradle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedServices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;KotlinCompileBuildService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameters of the experiment are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;repository&lt;/td&gt;
&lt;td&gt;cdsap/TelltaleExperiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant a&lt;/td&gt;
&lt;td&gt;main&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant b&lt;/td&gt;
&lt;td&gt;kotlin_service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterations&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache mode&lt;/td&gt;
&lt;td&gt;dependencies cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Results experiment: &lt;a href="https://github.com/cdsap/Telltale/actions/runs/11079901282" rel="noopener noreferrer"&gt;https://github.com/cdsap/Telltale/actions/runs/11079901282&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build duration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcki7q1s7ssocsiwlogp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqcki7q1s7ssocsiwlogp.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reducing the parallelization of the Kotlin compiler task is still slower than the main branch variant, but the build time is improved compared to the previous experiment, where the number of build workers was reduced:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo66jomlinwvx3lahx14m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo66jomlinwvx3lahx14m.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another interesting insight is how we are reducing the Kotlin compiler's memory max usage when comparing the three variants:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnipkvvax90qpeat90g8o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnipkvvax90qpeat90g8o.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given the nature of the project and the limited resources available in the GitHub Action runner (4 cores), the results are not impressive. However, in scenarios with a higher number of cores and larger compilation units, this could be an interesting experiment to perform, especially if you're experiencing high memory pressure in builds that heavily utilize the Kotlin compiler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use case: Disabling Artifact transform cacheability&lt;/strong&gt; &lt;br&gt;
Since Develocity includes Artifact Transforms information in the build scans, we have found some cases where significant negative avoidance savings are observed when those transforms are requested from the remote cache:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0ggt9za8hdzxw4p35li.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0ggt9za8hdzxw4p35li.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Given the high volume of transforms requesting cache entries in some poor connectivity scenarios, this could create a performance impact on the build duration. Gradle 8.9 introduces a new 'internal' property that allows disabling the cacheability of the transforms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-Dorg.gradle.internal.transform-caching-disabled=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The usage of this internal property does not guarantee stability or continued support in future versions. As this is an internal feature, it may be subject to changes or removal without prior notice, and its behavior may not be consistent across different versions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this experiment, we will use the remote cache mode, providing the dependencies cache but excluding the transforms to force execution or cache requests. Parameters experiment:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;repository&lt;/td&gt;
&lt;td&gt;cdsap/TelltaleExperiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant a&lt;/td&gt;
&lt;td&gt;main_with_remote_cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;variant b&lt;/td&gt;
&lt;td&gt;main_with_remote_cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task&lt;/td&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterations&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache mode&lt;/td&gt;
&lt;td&gt;remote task cache + dependencies cache - transforms cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build arguments&lt;/td&gt;
&lt;td&gt;variant b: "-Dorg.gradle.internal.transform-caching-disabled=true"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Experiment results: &lt;a href="https://github.com/cdsap/Telltale/actions/runs/11080852114" rel="noopener noreferrer"&gt;https://github.com/cdsap/Telltale/actions/runs/11080852114&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build Duration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Few7y1hh206s73nlbie70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Few7y1hh206s73nlbie70.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The build duration increased when comparing the variants. Upon analyzing the reason, we observed that the &lt;code&gt;DexMergingTask&lt;/code&gt;tasks were &lt;a href="https://ge.solutions-team.gradle.com/c/jzrovceaixlqw/fq4wm3efpap46/task-inputs?expanded=WyIzaHhqN3N4eWgyejdjLWRleGRpcnMiXQ#3hxj7sxyh2z7c" rel="noopener noreferrer"&gt;executed&lt;/a&gt; in the variant that disables the artifact transforms cache. This is related to the &lt;a href="https://issuetracker.google.com/issues/359616078" rel="noopener noreferrer"&gt;issue&lt;/a&gt;, where the Dexing task/transform generates non-deterministic classes.dex contents. Thanks to the Google team, this issue was fixed in Android Gradle Plugin 8.6.1. We repeated the experiment after updating the AGP version to 8.6.1. &lt;br&gt;
Experiment results: &lt;a href="https://github.com/cdsap/Telltale/actions/runs/11084537192" rel="noopener noreferrer"&gt;https://github.com/cdsap/Telltale/actions/runs/11084537192&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build duration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki2jlebfiz3qmpv3cdnj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki2jlebfiz3qmpv3cdnj.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still, the build duration increases significantly even though the tasks have the same hit ratio. In this case, however, it is cheaper to retrieve the artifact transforms output from the remote cache.&lt;/p&gt;

&lt;p&gt;To be fair, the experiment scenario is favored by the location of the remote cache node (us-central), which is closer to the location of the GitHub Action runners. This is not always the case in our CI environments, so in the final experiment, we created a new cache node farther from the location of the agents and repeated the experiment with the artifact transforms cache disabled. &lt;br&gt;
Experiment results: &lt;a href="https://github.com/cdsap/Telltale/actions/runs/11085054664" rel="noopener noreferrer"&gt;https://github.com/cdsap/Telltale/actions/runs/11085054664&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build duration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq7gf7gjda3an168dudhp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq7gf7gjda3an168dudhp.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The build duration improves when using the remote cache for artifact transforms despite the negative avoidance savings. However, in this case, the difference is much smaller compared to faster cache nodes. This data is interesting because, in scenarios with a high volume of transform requests and increased cache latency, disabling the transform cache might lead to better performance.&lt;/p&gt;

&lt;p&gt;Note:&lt;br&gt;
The internal Gradle property &lt;code&gt;org.gradle.internal.transform-caching-disabled&lt;/code&gt; allows disabling cache for specific artifact transforms types, you can use the Develocity API or tools like &lt;a href="https://github.com/cdsap/ArtifactTransformReport" rel="noopener noreferrer"&gt;ArtifactTransformReport&lt;/a&gt; to collect data of negative avoidance savings by artifact type and disable cacheability for those with higher values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final words&lt;/strong&gt; &lt;br&gt;
I want to emphasize that this is simply an opinionated approach I’m using to automate experiments. Of course, this approach is closely tied to the use of Develocity for consuming build data, but you can still use the experiment orchestration and opt for another component to collect job duration, such as the GitHub API.&lt;br&gt;
The key takeaway from this article is the importance of having a reliable framework to run experiments and make informed, data-driven decisions.&lt;/p&gt;

&lt;p&gt;Looking ahead, the future roadmap for Telltale includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support for more than two variants in experiments: Currently, we focus on comparing two variants, but in some cases, we’d like to extend this to test multiple variants, such as different heap sizes. This extension will require careful management of the number of jobs in the experiment to avoid hitting quota limits.&lt;/li&gt;
&lt;li&gt;Container argument configuration: While we currently provide variants by OS, some experiments need more flexibility. For example, when measuring builds with different native memory allocators, we require distinct OS environments. By introducing the option to use different container images, we can offer greater flexibility for more advanced experiments.&lt;/li&gt;
&lt;li&gt;Support for additional reporting tools: We plan to extend support to other reporting tools, such as Talaiot or the Gradle Analytics plugin, to provide richer data insights.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gradle</category>
      <category>android</category>
    </item>
    <item>
      <title>Resource observability case study: jemalloc in Android builds</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Tue, 20 Aug 2024 05:22:15 +0000</pubDate>
      <link>https://dev.to/cdsap/resource-observability-case-study-jemalloc-in-android-builds-22og</link>
      <guid>https://dev.to/cdsap/resource-observability-case-study-jemalloc-in-android-builds-22og</guid>
      <description>&lt;p&gt;As build engineers, one of our biggest concerns is running out of memory during our Gradle builds. This issue has a significant impact on our developers. When memory runs out, it can cause the system to kill the Gradle daemon, resulting in failed CI builds, frequent garbage collection (GC) overhead leading to slow build times, and, most importantly, undermining the team's confidence in running builds on CI.&lt;/p&gt;

&lt;p&gt;We often spend time searching for magic formulas to configure the system optimally, but the reality is more complex, with multiple dimensions of problems that vary for each project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent resources: Hardware always matters—not just CPU/memory, but also network and disk.&lt;/li&gt;
&lt;li&gt;Type of build: For instance, executing costly test tasks in parallel or performing intensive R8 operations at the end of the build.&lt;/li&gt;
&lt;li&gt;Nature of the build: Is the build dominated by cache hits, or do we have builds that consistently apply memory pressure? Are we providing layers of caching, like dependencies or wrappers, in our scenarios?&lt;/li&gt;
&lt;li&gt;Project structure: Do we have a large legacy module with thousands of compilation units?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, it's hard to find the perfect formula. That's why I'm opinionated and always try to design for the worst-case scenario, ensuring reliability in CI builds when running all tasks on a clean agent. However, once that state is achieved, we still need to consider multiple cases to continue making improvements.&lt;/p&gt;

&lt;p&gt;This underscores the importance of having the appropriate tools to monitor performance and automated tools to experiment under different scenarios. With these, we can systematically observe and analyze how different configurations behave, ensuring we make data-driven decisions.&lt;/p&gt;

&lt;p&gt;Fortunately, Develocity 2024.2 introduces &lt;a href="https://gradle.com/develocity/releases/2024.2#build-environment-resource-usage-observability" rel="noopener noreferrer"&gt;build resource usage observability&lt;/a&gt;, a feature that was previously missing in build scans:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fineyl1fsdb767o4zrqct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fineyl1fsdb767o4zrqct.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we have complete information on key metrics like memory usage, CPU, network, disk, and more. What's even better is the availability of new API endpoints that we can integrate with our monitoring systems to evaluate performance across these different metrics.&lt;/p&gt;

&lt;p&gt;As a demonstration, I want to measure something that caught my attention months ago—an interesting topic brought up by &lt;a href="https://www.jasonpearson.dev" rel="noopener noreferrer"&gt;Jason Pearson&lt;/a&gt;: the use of &lt;a href="https://jemalloc.net/" rel="noopener noreferrer"&gt;jemalloc&lt;/a&gt; as a native memory allocator for Android builds. The initial claim is that this usage brings a reduction in memory usage by optimizing how memory is allocated and deallocated. jemalloc is designed to minimize memory fragmentation and improve performance, particularly in multithreaded applications, making it ideal for resource-intensive builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experiment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To explore the impact of different native memory allocators on Android build performance, we designed an experiment with two variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default native memory allocator&lt;/li&gt;
&lt;li&gt;jemalloc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We conducted the experiment using GitHub Actions, where we created two distinct Docker images based on &lt;code&gt;amazoncorretto:17-al2023-jdk&lt;/code&gt;. For the jemalloc docker image variant, we configured the allocator by running the following commands:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

RUN curl -L "https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2" -o jemalloc.tar.bz2
RUN tar -xf jemalloc.tar.bz2
RUN cd jemalloc-5.3.0/ &amp;amp;&amp;amp; ./configure &amp;amp;&amp;amp; make &amp;amp;&amp;amp; make install
ENV LD_PRELOAD /usr/local/lib/libjemalloc.so


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, in the build.yaml file, we set up the configuration to test both variants:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

strategy:
    matrix:
        variant: ["cdsap/android-builder:0.5", "cdsap/android-builder-jemalloc:0.5"]
        runs: ${{ fromJson(needs.iterations.outputs.iterations) }}
runs-on: ubuntu-latest
container:
    image: ${{ matrix.variant }}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The experiment was executed with 100 iterations(fresh agent without cache) for each variant, using the &lt;a href="https://github.com/android/nowinandroid" rel="noopener noreferrer"&gt;nowinandroid&lt;/a&gt; project and focusing on the &lt;code&gt;assembleRelease&lt;/code&gt; task. By comparing the results, we aimed to assess the effectiveness of jemalloc in reducing memory usage and improving build performance in a CI environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;br&gt;
To get a clear picture of the performance metrics during our experiment, we utilized one of the new endpoints provided by the Develocity API: &lt;code&gt;api/builds/$buildScanId/gradle-resource-usage&lt;/code&gt;. This endpoint delivers detailed insights into various resource usage metrics throughout the build process.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

{
    "totalMemory": 68719476736,
    "total": {
        "allProcessesCpu": {
            "max": 98,
            "average": 79,
            "median": 86,
            "p5": 40,
            "p25": 85,
            "p75": 89,
            "p95": 95
        },
        "buildProcessCpu": {},
        "buildChildProcessesCpu": {},
        "allProcessesMemory": {},
        "buildProcessMemory": {},
        "buildChildProcessesMemory": {},
        "diskReadThroughput": {},
        "diskWriteThroughput": {},
        "networkUploadThroughput": {},
        "networkDownloadThroughput": {}
    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;One key insight from the metrics exposed by the new endpoint is that it provides not only memory metrics specific to the build process but also the total memory usage on the agent. This is perfect for the purpose of our experiment.&lt;/p&gt;

&lt;p&gt;Using the tags added to each variant execution, we then pulled the data and aggregated the results from all 100 iterations for each version of the experiment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/cdsap/jemallocExperiment/actions/runs/10462832287" rel="noopener noreferrer"&gt;Experiment execution&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1724137199999&amp;amp;search.startTimeMin=1724050800000&amp;amp;search.tags=cdsap%2Fandroid-builder-jemalloc:0.5,experiment-resources&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;jemalloc build scans&lt;/a&gt; &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ge.solutions-team.gradle.com/scans?search.startTimeMax=1724137199999&amp;amp;search.startTimeMin=1724050800000&amp;amp;search.tags=cdsap%2Fandroid-builder:0.5,experiment-resources&amp;amp;search.timeZoneId=America%2FLos_Angeles" rel="noopener noreferrer"&gt;malloc build scans&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All processes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubjy7ifagycxs9np8gcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fubjy7ifagycxs9np8gcn.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The time-series data shows that jemalloc has generally lower and more stable memory usage over time compared to malloc.&lt;/li&gt;
&lt;li&gt;There are fewer spikes in memory usage with jemalloc, indicating that it may provide a more consistent memory footprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Main build process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjzmd57nrzkui7i1576k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjzmd57nrzkui7i1576k.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For the main build process, jemalloc again shows a more stable and lower memory usage pattern compared to malloc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;malloc has higher peaks and more variability, which can be less desirable in a memory-constrained environment.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Summary results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5tzq7w76dijtkplumjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5tzq7w76dijtkplumjs.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final words&lt;/strong&gt;&lt;br&gt;
The results of this experiment are inherently influenced by the specific project and the environment in which the scenarios were executed. Nevertheless, it is evident that jemalloc offers slightly better performance in terms of memory usage.&lt;/p&gt;

&lt;p&gt;The primary focus of this article is to highlight the new opportunities introduced with the latest release of &lt;a href="https://gradle.com/develocity/releases/2024.2" rel="noopener noreferrer"&gt;Develocity 2024.2&lt;/a&gt;, particularly the enhanced build resource usage information now available in build scans and through the Develocity API. These new features provide deeper insights into memory usage, enabling more informed decision-making and optimization in your development workflows.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>android</category>
    </item>
    <item>
      <title>Performance Impact Analysis of Gradle 8.7 in Android Projects</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Sat, 23 Mar 2024 16:57:58 +0000</pubDate>
      <link>https://dev.to/cdsap/performance-impact-analysis-of-gradle-87-in-android-projects-5288</link>
      <guid>https://dev.to/cdsap/performance-impact-analysis-of-gradle-87-in-android-projects-5288</guid>
      <description>&lt;p&gt;Yesterday was released &lt;a href="https://docs.gradle.org/current/release-notes.html"&gt;Gradle 8.7&lt;/a&gt;. Our repositories are roaring with bot-generated PRs helping us with the processes of updating with the latest and the greatest versions of our dependencies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53qsdjjrbijpk84a94ba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53qsdjjrbijpk84a94ba.png" alt="Image description" width="800" height="651"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite those tools being great for automating the updates for small/sample repositories or non-critical dependencies, applying a performance regression test when updating critical build components in highly modularized projects with hundreds of developers is strongly recommended. Something that hurts the performance in this kind of repos will affect the team's development cycle and early detection is crucial. Ultimately, it is too late to detect the issue once the change is merged. &lt;/p&gt;

&lt;p&gt;As a simple example, today we will apply a performance test within an experiment on the new Gradle version in the project &lt;a href="https://github.com/android/nowinandroid"&gt;nowinandroid&lt;/a&gt;. The goal is to verify that the update has no impact on our codebase. &lt;br&gt;
In the experiment, we will cover the worst-case scenario where all tasks are executed and we don't have build-cache available. &lt;/p&gt;

&lt;p&gt;The project under test, &lt;code&gt;nowinandroid&lt;/code&gt;, is still working with Gradle 8.5, so we will add a new variant in the experiment representing Gradle 8.6. So the variants of the experiment are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.5 &lt;/li&gt;
&lt;li&gt;Gradle 8.6&lt;/li&gt;
&lt;li&gt;Gradle 8.7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The execution environment is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux 6.5.0-1016-azure (amd64) (GHA runner)&lt;/li&gt;
&lt;li&gt;4 CPU cores&lt;/li&gt;
&lt;li&gt;4 Gradle workers&lt;/li&gt;
&lt;li&gt;JDK 17&lt;/li&gt;
&lt;li&gt;6 GB Gradle Process&lt;/li&gt;
&lt;li&gt;6 GB Kotlin process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We executed 100 iterations for each variant, each iteration executed the task &lt;code&gt;assembleRelease&lt;/code&gt; in a clean GHA runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results&lt;/strong&gt;&lt;br&gt;
The first obvious check is the overall build time(seconds):&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3babtwzmjvsolc8qvt3q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3babtwzmjvsolc8qvt3q.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;br&gt;
We obtained similar results noticing a 1.78% improvement on the median using 8.7.&lt;/p&gt;

&lt;p&gt;Because of the nature of our experiment, fresh agent and no build/remote cache, next we analyzed exclusively the execution time to reduce the noise caused by components like the network(seconds): &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksi72n7c97bcv6bs2820.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksi72n7c97bcv6bs2820.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;br&gt;
Everything looks good, with a decrease of 1.93% in the median of 8.7.&lt;/p&gt;

&lt;p&gt;Next, we will focus on the more expensive tasks by plugin. First, we will start with the AGP and the task &lt;code&gt;:app:minifyDemoReleaseWithR8&lt;/code&gt;(ms):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqkmbjlqfcjdo2uyruyu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqkmbjlqfcjdo2uyruyu.png" alt="Image description" width="640" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We don't observe any significant impact on the task duration and the overall change related to the main median is -0.6%.&lt;/p&gt;

&lt;p&gt;Another task that dominates the build times is the DexMergintTask. In &lt;code&gt;nowinandroid&lt;/code&gt; the longest task is &lt;code&gt;:app-nia-catalog:mergeExtDexRelease&lt;/code&gt;, let's see the results(ms):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21yx1rmr48e7talf342s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F21yx1rmr48e7talf342s.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All good. We don't observe any impact in the update. &lt;/p&gt;

&lt;p&gt;Let's move to the Kotlin Gradle Plugin. In the main branch, the task with the longest duration is &lt;code&gt;:core:model:compileKotlin&lt;/code&gt;:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjml346t6fkucxlfpz0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjml346t6fkucxlfpz0u.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's going on? Why may the new Gradle version bring benefits in terms of the Kotlin compiler tasks? Sadly, Gradle 8.7 doesn't hide magical optimizations for our Kotlin tasks. The reason is the embedded Kotlin compiler has been updated from 1.9.10 to Kotlin 1.9.22 in Gradle and is now aligned with the version used in the &lt;code&gt;nowiandroid&lt;/code&gt; repository.&lt;br&gt;
That means the Gradle build doesn't need to download additional dependencies required for 1.9.22 because they are embedded. And that's why we are seeing an improvement in the task, which is the first Kotlin task executed outside the build logic in the project. &lt;br&gt;
We could have a clear picture if we analyze the build dependencies and network metrics for a build on each variant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gradle 8.5&lt;/th&gt;
&lt;th&gt;Gradle 8.6&lt;/th&gt;
&lt;th&gt;Gradle 8.7&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build Dependencies&lt;/td&gt;
&lt;td&gt;241&lt;/td&gt;
&lt;td&gt;241&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files downloaded&lt;/td&gt;
&lt;td&gt;1654&lt;/td&gt;
&lt;td&gt;1654&lt;/td&gt;
&lt;td&gt;1601&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data downloaded&lt;/td&gt;
&lt;td&gt;726.6 MiB&lt;/td&gt;
&lt;td&gt;726.6 MiB&lt;/td&gt;
&lt;td&gt;651.5 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number of network requests&lt;/td&gt;
&lt;td&gt;2138&lt;/td&gt;
&lt;td&gt;2138&lt;/td&gt;
&lt;td&gt;2117&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Finally, we analyzed the usage of the processes involved in the build starting with the Gradle process(Gb):&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8auua6d67axov2sxurha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8auua6d67axov2sxurha.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;br&gt;
And for the Kotlin process(Gb):&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F814bg7z526ch78qkxe5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F814bg7z526ch78qkxe5d.png" alt="Image description" width="600" height="371"&gt;&lt;/a&gt;&lt;br&gt;
We noticed an increase in process usage caused by the fact that we have the Kotlin versions aligned and we require only one process. In previous versions, two Kotlin processes were created during the build. We could verify this behavior if we analyze the Kotlin processes available at the end of the build for Gradle 8.5/8.6:&lt;br&gt;
&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39e87gwmgyhdpglk12ad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F39e87gwmgyhdpglk12ad.png" alt="Image description" width="712" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Against the processes in Gradle 8.7:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F507ebo67ayg9kn06ik40.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F507ebo67ayg9kn06ik40.png" alt="Image description" width="692" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final words&lt;/strong&gt;&lt;br&gt;
When updating components like the AGP, KGP, Gradle, or additional critical build components, a performance test regression is recommended to verify the correct behavior of the new version introduced. Even in the case, we explained that it doesn't bring significant duration improvements, it will give us an understanding of not-so-visible changes like the embedded Kotlin compiler update. &lt;br&gt;
The article was just an example covering a few metrics. Depending on the update type and the processes involved in the development cycle, you may consider different tests.&lt;/p&gt;

&lt;p&gt;Happy Building!&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>android</category>
    </item>
    <item>
      <title>nowinandroid builds with Gradle 8.5 and JDK 21</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Thu, 18 Jan 2024 02:44:07 +0000</pubDate>
      <link>https://dev.to/cdsap/nowinandroid-builds-with-gradle-85-and-jdk-21-7fp</link>
      <guid>https://dev.to/cdsap/nowinandroid-builds-with-gradle-85-and-jdk-21-7fp</guid>
      <description>&lt;p&gt;Gradle 8.5 fully supports compiling, testing and running on Java 21. Java updates frequently include optimizations that improve the performance of both the JVM and the Java applications running on it. &lt;br&gt;
At the Devoxx Belgium &lt;a href="https://www.youtube.com/watch?v=T6X2Yytrzyg"&gt;presentation&lt;/a&gt; "With Java 21, Your Code Runs Even Faster But How is that Possible?", Per Minborg explains some of the optimizations shipped in Java 21 like &lt;a href="https://bugs.openjdk.org/browse/JDK-8298639"&gt;Perform I/O operations in bulk for RandomAccessFile highlighting &lt;/a&gt; or &lt;a href="https://bugs.openjdk.org/browse/JDK-8282664"&gt;Unroll by hand StringUTF16 and StringLatin1 polynomial hash loops&lt;/a&gt;, and what is better, these optimizations have an immediate impact without changing the java application because by direct &lt;a href="https://github.com/search?q=repo%3Agradle%2Fgradle%20import%20java.io.RandomAccessFile&amp;amp;type=code"&gt;usage&lt;/a&gt; or by transitive dependency use.&lt;/p&gt;

&lt;p&gt;Following the previous &lt;a href="https://dev.to/cdsap/measuring-jdk-updates-for-local-builds-in-android-projects-3m4g"&gt;Java 17 article&lt;/a&gt;, in this article we share the results of measuring an Android project with Java 21.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nowinandroid
&lt;/h2&gt;

&lt;p&gt;The project used is &lt;a href="https://github.com/android/nowinandroid"&gt;nowinandroid&lt;/a&gt;. The experiment is based on the commit &lt;a href="https://github.com/android/nowinandroid/commit/f5b3ae56dcf0022456d061d0c4c121be5a144984"&gt;f5b3ae5&lt;/a&gt; of the main branch(12/22). At this point, the project was already using Gradle 8.5. &lt;br&gt;
The only change applied was to update the AGP to 8.2.1 because it included the fix for the issue: &lt;a href="https://issuetracker.google.com/issues/317235925"&gt;JdkImageTransform fails when using JDK 21&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment methodology
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two variants 

&lt;ul&gt;
&lt;li&gt;JDK 17&lt;/li&gt;
&lt;li&gt;JDK 21&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Scenarios:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;assembleDebug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assembleRelease&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;testDemoDebug&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lintDemoRelease&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Each variant/scenario runs 100 clean builds in GHA runners&lt;/li&gt;
&lt;li&gt;Memory configuration for all builds: &lt;code&gt;-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;First, we explore the overall build time for the different scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;JDK 17 - Median (secs)&lt;/th&gt;
&lt;th&gt;JDK 21 - Median (secs)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;td&gt;448&lt;/td&gt;
&lt;td&gt;433&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assembleRelease&lt;/td&gt;
&lt;td&gt;659&lt;/td&gt;
&lt;td&gt;591&lt;/td&gt;
&lt;td&gt;10.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;testDemoDebug&lt;/td&gt;
&lt;td&gt;334&lt;/td&gt;
&lt;td&gt;320&lt;/td&gt;
&lt;td&gt;4.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lintDemoRelease&lt;/td&gt;
&lt;td&gt;306&lt;/td&gt;
&lt;td&gt;296&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In four evaluated scenarios, we are observing modest improvements in three of them, while the &lt;code&gt;assembleRelease&lt;/code&gt; scenario shows a significant reduction of 10% in build time.&lt;/p&gt;

&lt;p&gt;Next, we explore where the improvements came from at the task level:&lt;/p&gt;

&lt;h3&gt;
  
  
  assembleDebug
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Diff Median (seconds)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:app:mergeExtDexDemoDebug&lt;/td&gt;
&lt;td&gt;8.5&lt;/td&gt;
&lt;td&gt;5.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app-nia-catalog:mergeExtDexDebug&lt;/td&gt;
&lt;td&gt;2.8&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app:mergeExtDexProdDebug&lt;/td&gt;
&lt;td&gt;2.3&lt;/td&gt;
&lt;td&gt;10.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app:l8DexDesugarLibDemoDebug&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;td&gt;8.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app:hiltJavaCompileProdDebug&lt;/td&gt;
&lt;td&gt;1.7&lt;/td&gt;
&lt;td&gt;7.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  assembleRelease
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Diff Median (seconds)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:app:minifyDemoReleaseWithR8&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;18.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app-nia-catalog:mergeReleaseGlobalSynthetics&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;29.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app-nia-catalog:mergeExtDexRelease&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;19.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app-nia-catalog:l8DexDesugarLibRelease&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;24.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app:minifyProdReleaseWithR8&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;9.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  testDemoDebugUnitTest
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Diff Median (seconds)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:feature:foryou:testDemoDebugUnitTest&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;td&gt;7.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app:testDemoDebugUnitTest&lt;/td&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;td&gt;6.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:core:designsystem:testDemoDebugUnitTest&lt;/td&gt;
&lt;td&gt;2.4&lt;/td&gt;
&lt;td&gt;4.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:core:data:testDemoDebugUnitTest&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;10.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:app:hiltJavaCompileDemoDebug&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;9.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  lintDemoRelease
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Diff Median (seconds)&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;:core:data:lintAnalyzeDemoRelease&lt;/td&gt;
&lt;td&gt;1.9&lt;/td&gt;
&lt;td&gt;9.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:core:designsystem:compileDemoReleaseKotlin&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:core:designsystem:lintAnalyzeDemoRelease&lt;/td&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;6.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:core:analytics:lintAnalyzeDemoRelease&lt;/td&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;:core:datastore:lintAnalyzeDemoRelease&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Again the release scenario shows a significant improvement in the expensive R8 tasks. &lt;/p&gt;

&lt;p&gt;Finally, we aggregated the absolute diff of the median for the tasks in each scenario providing the "serial" improvement using JDK 21:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Aggregated diff  (seconds)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assembleRelease&lt;/td&gt;
&lt;td&gt;158&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;testDebugUnitTest&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lintDemoRelease&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experiment scenario&lt;/th&gt;
&lt;th&gt;Results&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assembleDebug&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Pagan/actions/runs/7509423908"&gt;https://github.com/cdsap/Pagan/actions/runs/7509423908&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assembleRelease&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Pagan/actions/runs/7513465432"&gt;https://github.com/cdsap/Pagan/actions/runs/7513465432&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;testDemoDebug&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Pagan/actions/runs/7520610718"&gt;https://github.com/cdsap/Pagan/actions/runs/7520610718&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lintDemoRelease&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/cdsap/Pagan/actions/runs/7516005997"&gt;https://github.com/cdsap/Pagan/actions/runs/7516005997&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Experiments spreadsheet: &lt;a href="https://docs.google.com/spreadsheets/d/1wdXYp4ri5XUcBSGNc-ssUpjpcl1e-Pdns4-byNMyvwg/edit?usp=sharing"&gt;https://docs.google.com/spreadsheets/d/1wdXYp4ri5XUcBSGNc-ssUpjpcl1e-Pdns4-byNMyvwg/edit?usp=sharing&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;In this article, we explored how using JDK 21 enhances build efficiency in an Android project, particularly noting reduced build times. While some improvements were moderate, the assembleRelease tasks demonstrated notably greater enhancements.&lt;br&gt;
The outcomes may differ based on the specific project, but considering the minimal adjustments needed, it's certainly worthwhile to experiment with this approach if you're utilizing Gradle 8.5.&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>gradle</category>
      <category>java</category>
      <category>android</category>
    </item>
    <item>
      <title>KSP in Android projects</title>
      <dc:creator>Iñaki Villar</dc:creator>
      <pubDate>Fri, 01 Sep 2023 03:22:57 +0000</pubDate>
      <link>https://dev.to/cdsap/ksp-in-android-projects-5cj3</link>
      <guid>https://dev.to/cdsap/ksp-in-android-projects-5cj3</guid>
      <description>&lt;p&gt;The Android community had awesome news this week: Dagger and Hilt KSP processors are now available in the latest release, v2.48:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqg8npfx21x4nkr6g0uqb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqg8npfx21x4nkr6g0uqb.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The benefits of using KSP over kapt, in terms of build performance, are explained in the &lt;a href="https://kotlinlang.org/docs/ksp-why-ksp.html#comparison-to-kapt" rel="noopener noreferrer"&gt;official doc&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The major advantages of KSP over kapt are improved build performance&lt;br&gt;
...&lt;br&gt;
To run Java annotation processors unmodified, kapt compiles Kotlin code into Java stubs that retain information that Java annotation processors care about. To create these stubs, kapt needs to resolve all symbols in the Kotlin program. The stub generation costs roughly 1/3 of a full kotlinc analysis and the same order of kotlinc code-generation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Regarding Android projects, KSP was already used by different libraries, but Dagger/Hilt, one of the main DI frameworks used in the Android projects, required using kapt. Mixing KSP and kapt didn't bring benefits in terms of build performance. We were waiting anxiously to test a project using KSP exclusively.  &lt;/p&gt;

&lt;p&gt;This article compares the results of building nowinadroid with KSP, using Dagger/Hilt 2.48, against the current configuration with kapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;As usual in these articles, the project under experimentation is &lt;a href="https://github.com/android/nowinandroid" rel="noopener noreferrer"&gt;nowinandroid&lt;/a&gt;. We are based on the main branch on this &lt;a href="https://github.com/android/nowinandroid/commit/d0909a9c8f91153f0171d9f89e515a2f7df95202" rel="noopener noreferrer"&gt;commit&lt;/a&gt;. Build stack components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gradle 8.2&lt;/li&gt;
&lt;li&gt;AGP 8.1.0&lt;/li&gt;
&lt;li&gt;KGP 1.9.0&lt;/li&gt;
&lt;li&gt;Hilt 2.47&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main branch is one of the variants of this experiment, representing the kapt build. Currently, the kapt configuration is used on 21 projects defining the dependency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;com.google.dagger:hilt-android-compiler:2.47&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, we need a KSP branch acting as another variant in the experiment. We updated the required convention plugin to use KSP instead of kapt. Additionally, we had to apply small changes. The complete list of changes: &lt;a href="https://github.com/cdsap/KspVsKapt/commit/5c7bab7b0241142f71caabde1d3558782db0bef4" rel="noopener noreferrer"&gt;https://github.com/cdsap/KspVsKapt/commit/5c7bab7b0241142f71caabde1d3558782db0bef4&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Methodology&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We created two different scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean builds: 50 builds per variant executed in parallel in GitHub Action runners. Task: &lt;code&gt;:app:assembleProdDebug&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Incremental change: 20 builds per variant executed in Github Action runners applying an incremental change on &lt;code&gt;core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/NewsRepository.kt&lt;/code&gt; using Gradle Profiler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We retrieved the build information with Gradle Enterprise API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Before reviewing the build results, it's important to mention that replacing kapt with KSP affects the nature of the build. When applying the kapt plugin, we are adding two tasks to the project: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt; &lt;code&gt;org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, KSP adds only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;com.google.devtools.ksp.gradle.KspTaskJvm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will reduce the number of tasks on the KTS variant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tasks executed (kapt variant)&lt;/th&gt;
&lt;th&gt;Tasks executed (KSP variant)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clean Build&lt;/td&gt;
&lt;td&gt;310&lt;/td&gt;
&lt;td&gt;292&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental Change&lt;/td&gt;
&lt;td&gt;164&lt;/td&gt;
&lt;td&gt;154&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Clean Builds&lt;/strong&gt;&lt;br&gt;
The build time, in seconds, for both variants was:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu6mfzq7g0wthztpc497m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu6mfzq7g0wthztpc497m.png" alt="Image description"&gt;&lt;/a&gt;&lt;br&gt;
We noticed that the configuration time took around 30% of the build time. Because the builds are executed in clean runners, we preferred to exclude the configuration time and reduce the noise from the configuration phase. The results were:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2hk85lv5jkvn7tteo7f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2hk85lv5jkvn7tteo7f.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still, this data included the execution of tasks unrelated to the kapt/KSP. We picked the modules implementing kapt/KSP and measured the task duration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For kapt variant, is the sum of &lt;code&gt;KaptGenerateStubsTask&lt;/code&gt; + &lt;code&gt;KaptWithoutKotlincTask&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For the KSP variant, it represents the duration of &lt;code&gt;KspTaskJvm&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following view represents the median grouped by module of the projects using the plugins under investigation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09hmpsk1gf3tzjvvq639.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09hmpsk1gf3tzjvvq639.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We noticed a general improvement in the build duration on each module, showing the &lt;code&gt;app&lt;/code&gt; module a decrease of 60% in the processor duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental Builds&lt;/strong&gt;&lt;br&gt;
Because an incremental change affects a specific tree of the task graph, we have fewer modules involved compared to the previous scenario. &lt;br&gt;
The configuration time is not a concern because we have incremental builds. The results after removing the warm-up builds:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff47l8u7w9p3pwt957il4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff47l8u7w9p3pwt957il4.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Analyzing the median duration of the processor execution by module:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fna5g7jp0etzcoqvx6rte.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fna5g7jp0etzcoqvx6rte.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All modules decreased the processing time when using KSP. In this case, we didn't observe the same excellent results of clean builds, but we had significant wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;We showed that KSP decreases the processing build time in the project nowinandroid. The project is small, and all the task durations in both variants take seconds, far from the expensive kapt executions we are used to in real projects where it takes minutes in large modules. This first version is just the beginning because the release notes mention the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dagger’s KSP processors are still in the alpha stage. So far we’ve focused mainly on trying to ensure correctness rather than optimize performance. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Exciting times and kudos to the Dagger/Hilt team.&lt;/p&gt;

&lt;p&gt;Happy Building&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>gradle</category>
    </item>
  </channel>
</rss>
