Speeding Up Builds with Dagger Reflect

A large portion of an Android app’s build time can consist of Dagger1 annotation processing,2 and most developers agree that productivity is important, so we decided to experiment to see if we could save time when compiling the SoundCloud Android app. This blog post covers how we used Dagger Reflect to save developer time with minimal changes to our codebase. Before we dive deep into how we saved Dagger annotation processing time, let’s make sure we’re on the same page in understanding what dependency injection is.

droidconsf

Basics of Dependency Injection

Dependency injection is a great way to write maintainable, testable code by using the dependency inversion principle. For more information on the basics of dependency injection, take a look at this introduction tutorial.

History of Dependency Injection

Dependency injection frameworks in the Android world started by bringing practices from the Java server side to Android. At the time, these frameworks, e.g. Guice, were reflection based, meaning they inspected the program’s code during runtime. This approach was especially slow on Android. Then Dagger came along and alleviated this problem by generating code at compile time instead of using reflection at runtime. However, the tradeoff to the faster app startup time was a longer time spent compiling, which was especially noticeable as apps grew in size.

42 seconds

In the above example from our app, a 59-second build spent 42 seconds processing annotations. More specifically, the 42-second processing time is spent checking the code, analyzing the object graph, and generating the code. To put this in perspective, in the SoundCloud Android app, Dagger generates a very large component class with more than 10,000 lines of code.

So this is how we as developers using Dagger started to suffer. What this meant for us here at SoundCloud is that we needed to find a solution that saved time. This sparked looking into Dagger Reflect. We did this because it offered the possibility of the best app startup time for users and the best compile time for developers, with little changes to the production code. Another option would have been to migrate our entire codebase to a runtime- or reflection-based dependency injection framework. However, we decided against it because the cost of migration and the slower startup times were not worth it.

How Dagger Reflect Works

Dagger Reflect is an open source project by Jake Wharton that mimics the API of Dagger.3 However, instead of generating code at compile time, it parses injectable objects and the component dependencies at runtime. This skips generating and regenerating the same code for every build. It instead uses a dynamic proxy class, which implements an interface specified at runtime. Dagger Reflect proxies an application’s Dagger Component to the Dagger Reflect runtime.

How to Use Dagger Reflect in Your Project

There are a few ways to use Dagger Reflect in your project. One approach is full reflection, and the other approach is partial reflection, as outlined in the Dagger Reflect README. You can also swap the dependencies manually for every module. The simplest approach is to use the Delect Gradle Plugin, something that was created here at SoundCloud to make integrating Dagger Reflect simple and painless. For this blog post, we are going to cover the partial reflection approach using Delect.

The partial reflection approach requires the least amount of changes to your existing codebase, and almost no downside — it takes only 60 ms of code generation to glue your code to Dagger Reflect. Here’s how to start using Dagger Reflect in your project.

Step 1

Make sure to use the Dagger Component Factory or Component Module annotations:

@Component
  public interface ApplicationComponent {
    @Component.Factory
    interface Builder { }
  }
}

Step 2

Ensure that all Qualifier annotations have AnnotationRetention.RUNTIME. Additionally, all MapKey annotations must have runtime retention:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LibraryStorage

Step 3

Apply the Delect plugin to your root build script:

buildscript {
  classpath 'com.soundcloud.delect:delect-plugin:<latest_version>'
}
apply plugin: 'com.soundcloud.delect'

This will automatically swap Dagger for Dagger Reflect when the Gradle property dagger.reflect is set to true.

Delect — the Dagger Reflect Gradle Plugin

Delect is our open source Gradle plugin that makes it much easier to switch out all the regular Dagger dependencies for the Dagger Reflect versions in your project. Here is an example of the following dependency swap you would need to perform in each Gradle module:

implementation “com.google.dagger:dagger:<latest-version>”
implementation “com.google.dagger:dagger-android-support:<latest-version>”
lintChecks “com.jakewharton.dagger:dagger-reflect-lint:<latest-version>”
if (useDaggerReflect()) {
  implementation “com.jakewharton.dagger:dagger-reflect:<latest-version>”
  kapt “com.jakewharton.dagger:dagger-reflect-compiler:<latest-version>”
} else {
  kapt “com.google.dagger:dagger-compiler:<latest-version>”
  kapt “com.google.dagger:dagger-android-processor:<latest-version>”
}

The plugin works by using the Gradle Dependency Substitution API. When it detects that there is a dependency on the Dagger runtime, it adds the Dagger Reflect runtime. When it detects the Dagger compiler or the Dagger Android compiler, it swaps them out for the Dagger Reflect compiler. There is no need for a separate runtime or compiler component in order to use the Dagger Android processor.

To give you an idea of how it works, here’s a small snippet of code from the plugin where the Dagger annotation processor is substituted for the Dagger Reflect annotation processor:

substitute(
  module("$com.google.dagger:dagger-compiler")
).apply {
  with(module("$com.jakewharton.dagger:dagger-reflect-compiler:${extension.daggerReflectVersion}"))
}

In some initial trials, we noticed that developers’ workflows switched from the IDE to the command line while working on the same task. Only enabling Dagger Reflect for IDE builds triggered a full rebuild when switching and vice versa. Because of this painful rebuild, we chose not to enable Dagger Reflect in the Gradle Plugin only in IDE builds. Instead, Delect is enabled or disabled via a user controlled Gradle property, (dagger.reflect).

A quick way to enable Dagger Reflect when the plugin is applied is the following:

echo "dagger.reflect=true" > ~/.gradle/gradle.properties

We also seed our Gradle build cache with Dagger Reflect enabled; otherwise, building with Dagger Reflect would always be the slower option.

Performance Benchmark

Before we dig into the numbers, we want to note that we’ve spent a lot of time optimizing our builds by using incremental annotation processors and enabling caching, and as a result, have solved as many build cache misses as we possibly could.

Though on average, local developer builds are under 60 seconds, some builds can take as long as 3-5 minutes here at SoundCloud. As an example, the following build scan shows a build that took 5 minutes and 44 seconds.

3 mins annotation processing

Out of all the annotation processors in use here, Dagger takes the most amount of time by far. It took a total of 3m 9s in the above example.4

When using Dagger Reflect, the compiler never spends more than 80 ms processing annotations — a huge decrease in build times.

Our Dagger Reflect benchmark in our Android project led to a decrease in build times from 62 seconds to 31 seconds for an ABI change to a module. Dagger Reflect makes builds take half as long!

But what about overall build speeds for all our developers? In a four-week period, our average build time for all local builds without Dagger Reflect was 81.13 seconds. The build time with Dagger Reflect enabled was 55.33 seconds. This is, on average, a 26 second and 32% decrease in build speeds!

No Dagger Reflect With Dagger Reflect
81.13 secs average 55.33 secs average

However, we see the biggest wins on the longest builds because those are the ones that spend the most amount of time in annotation processing. For simply applying a Gradle plugin in the root of a project, this is probably the easiest build speed win!

To sum it up, we saw that Dagger was taking a large portion of our build times. We saved up to 50 percent of our local build times with little maintenance cost by swapping regular Dagger for Dagger Reflect. Try it out in your project!

Additional Reading


  1. Dagger is a popular dependency injection framework in the Android and JVM world that leverages annotation processing.

  2. Annotation processing is a way of hooking in to the compilation to analyze source code and generate code.

  3. Dagger Reflect is designed to be used for development builds only.

  4. This time is cumulative, not wall clock time. Multiple threads or Gradle tasks could be compiling using Dagger in parallel and the time spent in parallel might be double counted.