Impact of Dependency Tree Depth on Gradle Builds
Introduction
Great to see you take an interest to understand the impact of dependency tree depth on your Gradle builds.
After you finish reading this article here is what you’ll take away, depending on where you are with your project setup
- If you are new to the concept of modularization, you’ll have a better understanding of how to structure your dependencies when you modularize your project, to get faster Gradle builds.
- If you are working on a modularized project, you’ll get insights to further improve your build times by shortening your dependency tree, to speed up your Gradle builds.
Acquainting with Gradle Fundamentals
What is Gradle?
Gradle is an open-source build automation tool that allows you to build your software. You can read more about the design principles and terminology on their official website.
At the core of Gradle is a language for dependency-based programming.
A Gradle build is made up of Tasks. These tasks can depend on each other. They are executed in order of their dependencies. Together these tasks form a Directed Acyclic Graph. During a build, each task only runs once.
What is a Task?
Simply put it is an atomic piece of work that you want to do as a part of your build process.
Since the work may or may not be done in a single step. A task is made up of one or more Actions. As a result of its work, a task can produce TaskOutputs. Since a task might need to know some information to perform its work, they can also accept TaskInputs.
What is Task Output Cache?
Imagine you create a Gradle task to calculate the sum of ASCII values until the character Z.
The input of the task is a character between A-Z.
Let’s say when you first execute the task, you give it A
as input. Gradle executes the task and computes the result. You get the output as 2015. Now you run the task again with the same input.
This time instead of actually computing the result, Gradle just spits out 2015. How? This is what is called Task Output Caching.
Understanding the Sample Project Structure
Gradle Supports Single and Multi Project builds, For this post, we are focusing on a modularized (i.e Multi-Project Build).
To showcase various scenarios, I created a simple Gradle project that you can check out on my GitHub profile. The project is called dep-height-example
and it has 4 modules.
The dependency graph of the project looks like this
What happens when I change one of these modules? Does my build-time depend on which module I change? The answer is yes! We’ll see how a change in each module affects the build.
Understanding The Impact of Change in Modules
Before we begin, I want to do a clean build to see all the tasks Gradle will run for our project.
./gradlew clean build
The image below shows all the tasks that were executed by Gradle.
This is because, on the build scan summary, Gradle shows the top 6 tasks that took the longest time to execute.
Next, I will be making small changes in classes present in different modules and see the impact.
I can already tell you that the impact of this change will be different based on where the module lives in the dependency tree.
Change at the Top Of The Tree
First I will make a change in the :developer
module that lives on the top of the dependency tree as shown below.
The change will be simple. I will add a new single line that invokes a println()
method. Now when I build the project which modules do you think will get compiled?
Gradle only compiles the :developer
module as you can see in the Build Scan output below.
This happened because none of the dependencies of the :developer
module changed. So Gradle was able to reuse the cached task outputs for :contractor
, :foundation
and :wall
modules from a previous build.
Let's move down our dependency tree and see what happens!
Change in the Middle of the Tree
In this case, we will modify the :contractor
module.
The change here is identical to the previous step. I add a new single line that invokes a println()
method. Now when I build the project which modules do you think will get compiled?
The answer is :contractor
and :developer
. If you guessed them, great job you are starting to understand the impact of dependency tree depth on gradle builds.
Let's move further down the tree to see what happens when you modify a module that exists at the bottom of the dependency tree.
Change at the Bottom of the Tree
Although :foundation
and :wall
both are considered the bottommost nodes.
In this case, we will modify the :wall
module and add a new interface to :wall
the module. This is to show you what effect it will have since :developer
depends on this both directly and indirectly. Which modules do you think will get compiled in this case?
If you answered :wall
, :contractor
and :developer
congratulations, you've managed to understand the impact of dependency tree depth on Gradle builds.
Why did Gradle build :wall
, :contractor
and :developer
modules? Since the inputs of the compileKotlin
tasks for all of these changed. This prevented Gradled from reusing the cached outputs of this task from a previous run.
Important Points To Remember
- Modularization is great and should have a positive impact on your Gradle builds if implemented correctly.
- It is very easy to modularize your app incorrectly and negatively impact your Gradle builds.
- If a module in your graph changes frequently, ensure that not a lot of other modules depend on it i.e Reduce the Out-Degree of that Module.
- Your goal should be to optimize for maximum Gradle task cache hits when trying to improve build speed.
- Having a really deep dependency graph does not always mean it is bad.
- More changes you introduce at the bottom of the graph, the less caching benefit you get.
Conclusion
To sum it up, if you want to improve your Gradle build times, think about modularization and reducing the out-degree of your modules. A deep dependency graph is not always a bad thing - more changes at the bottom of the graph just mean less caching benefit. Keep in mind what your goal is - optimizing for maximum Gradle task cache hits.
Learn in-depth about Gradle Dependency Trees and Five Ways to Generate Gradle Dependency Tree.
Subscribe to my blog and never miss an update on new articles about Android and Gradle.