Mastodon

Optimizing Your Kotlin Build

Kotlin build times are often slow, this is a laundry list of common issues and what you can or can't do about them.

A beluga whale, which is white.
An actual white whale

Kotlin, strictly speaking, is awesome. Kotlin build times, strictly speaking, are not awesome. While many developers have weighed these and concluded the build times are worth it, it doesn't change the fact that they're paying this cost.

There are promising changes coming down the pipeline, but the unfortunate reality is that Kotlin builds today are often slow for avoidable reasons. Some are Gradle issues, some are Kotlin issues. This post is intended to detail a number of common pitfalls to be aware of and what you can do about them.

TL;DR: Modularize, ensure every project is a Kotlin project with at least one Kotlin source file, help test KSP now and Kotlin FIR later.


Disclaimer: Usage of the word "broken" below means different things to different people, so substitute it with whatever word you think suits the described issue best. "unexpected", "unreliable", "actually this a feature request", "source of pain", "literally worse than my ex", etc.

Build Cache

One of Gradle's main tools for improving build times is build caching. Build caching is the act of caching task outputs and reusing them if inputs to the task have not changed. It can be enabled for local builds (where the cache is located in ~/.gradle) via org.gradle.caching=true Gradle property and can be enabled remotely for sharing cached builds across multiple workspaces (CI, developer machines, etc).

When it works, it's great! When it doesn't, it's frustrating at best (cache misses) and a major problem at worst (incorrect/corrupt cache entries).

It's worth noting that the local cache also breaks in the above cases if you have multiple repo clones in different locations, as the absolute paths fail to match in those places too.

There are a number of people that have deduced that disabling (remote) build cache for Kotlin compile tasks is a net-gain for them because incremental builds are more reliable as a result. See the discussion on KT-34862 for more details.

Incremental Compilation

You can find a detailed writeup on future needs for improving incremental compilation, caching, and current issues with both in KT-40184.

Task Configuration

Kapt

Future

Supplementary tools/patterns

What about Bazel/Buck/Pants/etc?

Your mileage may vary, but it's probably slower. There's no notion of IC for Bazel-like build systems and Kotlin relies heavily on IC to smoothen over its lack of better compilation avoidance. Bazel-like builds may handle simple compilation avoidance better in some cases though.

Conclusions

Some of it is on Kotlin for often allowing critical build tools like Kapt to fall behind, some of it is on Gradle for having legacy APIs that regularly invite you to do the wrong thing (I hope they'll consider a progressive mode or more aggressive deprecation policy).

The easiest way to test for caching issues is to:

  1. Run the same build twice with --scan and see which tasks weren't cached.
  2. After you've resolved #1, have two project clones in different locations and run identical builds with --scan, then compare them to see which tasks weren't cached.

You can find more common caching problems on Gradle's docs here.

The easiest way to test for IC issues is to run a Gradle build with --debug and grep for [IC] to log detailed incremental compilation data about all this from kotlinc. It's noisy so better to pipe it to a file and search in a text editor of your choice.

Edit: @atsvetkv pointed me at the following Gradle properties that will be less noisy for debugging IC issues and getting metrics!

# gradle.properties for debugging
kotlin.build.report.enable=true
kotlin.build.report.verbose=true

# gradle property for metrics
kotlin.build.report.metrics=true
My possibly controversial 2¢: You should take one pass at cleaning up IC issues in your project, then focus on modularizing in the long run rather than rely on IC. Simple* compilation avoidance will win every day of the week and no other investment will yield as significant of results or be as broadly applicable. Incremental compilation is and always will be the white whale of Gradle and Kotlin build performance.

Special thanks to Nelson, Ivan, Eugene, and Tony for reviewing this.

*I define simple compilation avoidance as just avoiding project dependencies. There is also "true", ABI-based compilation avoidance discussed above but not yet available.