New year, faster builds

So, we just welcomed a new year and one of your New Year’s resolutions is to — finally — reduce your build times, thus becoming more productive and happier. 🙏

This (contemporary) guide will help you identify the culprits that kill your productivity and take a swing at eliminating them. 😈

We will list a set of tools and techniques, ranging from the simpler to the more complex one and try to “decipher” what these reports/outputs you will get actually mean and how to proceed.

Disclaimer: We assume that you have already updated to the latest versions of your dependencies, including Kotlin, AGP and Gradle version.

Check your gradle.properties file

The what file now? Yes, you may be surprised but copying&paste ambiguous flags in our gradle.properties file can result in dreadful experiences. For example, if the flag -Dkotlin.compiler.execution.strategy=in-process finds its way in, incremental compilation will be disabled (💀) for your project. So be careful with what you paste in.

My personal gradle.proprties file

Check your BuildConfig file

You may think that it sounds unimportant, but you will be glad if it is just that. If you notice a full recompilation every time you start a build, or in a peculiar pattern, without having previously made a change, you should observe your module’s BuildConfig file.

Chances are that you will notice some of the fields inside the file have different values after each recompilation. You should, then, search your Gradle build scripts for these fields in order to alter the way their values gets assigned.

Do your Gradle scripts need a doctor?

A fairly easy to use Gradle plugin, called Gradle Doctor, can help you solve some irritating issues that slow you down. Most notably, it will:

  • Ensure JAVA_HOME is set and matches IDE's JAVA_HOME: such a mismatch can cause significant RAM usage, as multiple daemons can be spawned. Such a warning can is also present in the latest versions of Android Studio, but nobody reads warnings, right? 😛
  • Warn you on excessive Garbage Collection: This can probably indicate that you need to increase your heap size.
  • Measure the time spent interacting with Dagger: With Dagger being a heavily annotation based library, your annotation processing time can be off the roof and you should consider using a reflection-based library.
  • Output a report regarding build cache connection speed: With this report, you can check if the use of the Remote Build Cache will be beneficial for you; useful feature during our WFH times. We will talk more in depth for the Remote Build Cache later.

You just apply it to your root level module and sit back while it prints suggestions for your build. Check it out here.

Do you really know your dependencies?

Another very promising and helpful plugin is the Dependency Analysis Plugin.

I won’t go into great detail explaining what and how it does it, since, apart from the excellent official wiki, these have been covered in two great articles by its creator Tony Robalik, here and here.

In simple terms, this plugin by analyzing your dependencies can help you identify unused and transitive ones, indicate if they have been declared properly (api, implementation, compileOnly) and, also, check for unused annotation processors.

Scan your build

Gradle’s Build Scans feature provides insights and helpful suggestions around your build. The way it works is by generating a “build report” with info and analytics regarding your build.

While generating a scan report is as easy as appending--scan to your command (e.g assembleDebug), it’s recommended to apply the plugin manually in order to be sure you are running the latest version and skipping the acceptance of ToS every time you run a build.

The process is pretty straightforward as stated here and configuring the ToS agreement is as simple as adding the following block in your build.gradle file:

gradleEnterprise {
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
}

Your report should look like this:

Now, we are going to look over the different sections of this scan to pinpoint culprits that slow down your build:

  1. Timeline section: In this page you can order the executed tasks by the duration it took them to complete. This is a good place to search for 3rd party plugins that may be doing more work than needed and consequently notify their maintainers.

Tip: Execute the build one more time, without any changes and check if any task ran; if it did, it’s definitely a red flag and you should investigate it.

Task execution page

2. Configuration tab: Head over to Performance>Configuration. This is one of the most important parts of your build, as this phase gets executed every time you start your build. As a result, it’s important to check for tasks (especially for custom ones) that execute on every consecutive build and refactor them properly.

Configuration phase

3. Build Cache page: Navigate to Build Cache and check for cache misses. Tasks whose inputs are not (properly) defined will not be cached and keep your build time increased. Make sure your tasks adhere to Lazy Configuration and use Task Configuration Avoidance; two important principles that will also assist on your Configuration phase execution time!

Build cache hits & misses

4. Network activity tab: In the next tab, Network activity, you can observe any unnecessary interaction with artifact repositories. This can, sometimes, significantly impact your build time.

Tip: If you notice that some of your artifacts are being downloaded extremely slow, consider moving them to a private repository. For more on that, check this post.

Network activity-Identify slow repos and consider moving to a private one

Profile your build

After scanning the internals of your build, it is time to go deeper and profile them. Gradle Profiler is an open source tool maintained by the Gradle team, developers in the Android developer team and other enthusiasts, that will give you more fine-grained control over your build analysis.

Gradle Profiler can be paired up with other… profilers to produce various types of reports. It can also be used with the Build Scan feature we mentioned above. In this post, we will focus on the Java Flight Recorder profiler and, specifically, on the produced Flame Graphs.

To generate those graphs, execute the command:

gradle-profiler --profile jfr --project-dir <root-dir-of-build><task>...
Flame graph — https://jakewharton.com/tracing-gradle-task-execution/
Flame graph — https://jakewharton.com/tracing-gradle-task-execution/
Flame graph (CPU)— https://jakewharton.com/tracing-gradle-task-execution/

You will get multiple folders, each one “touching” a different system component, but I tend to stick only to the tasks that interact with the CPU. Learning your way around such a graph can be tricky, but the aforementioned resources can help you grasp the gist of them.

What it basically shows you is where each task spent its time over the course of the build. Keep in mind that the different colors of the stacks are just a visual aid and are not there to correlate different parts/methods of a task.

Color is random — https://www.slideshare.net/brendangregg/usenix-atc-2017-visualizing-performance-with-flame-graphs

If you think a disproportional amount of time is spent by a 3rd party library and you have covered that none of your custom tasks pose a problem, reach out to the maintainers of it. Don’t forget you can always contact the Gradle team (performance@gradle.com), in case you can not figure out what exactly is the problem.

Again, check out the amazing presentation & video by Brendan Gregg to ease your way in the world of Flame Graphs.

Phew, it has been a long read but I hope one that will assist you in your quest of reaching maximum levels 💯 of productivity. Drop a comment with what you (dis)liked or for any related questions.. Thanks again!

Special thanks to Nelson Osacky, Tony Robalik and Pantelis Chasapis.

Passionate Android Engineer, who loves experimenting with new ideas, tools. Android Software Engineer at Beat.