300% Boost! How Gradle Build Scan Improved My Build Time

In my project, I encountered a issue where it would take roughly 20 minutes to test, build, and publish the project. Since every engineer does this at least a couple of times a day, it is a huge pain point and a waste of time. Determined to resolve this, I set foot on a journey to improve performance. This involves extracting data to identify the bottleneck, understanding why it’s happening, addressing the issue, and then running benchmarks to measure the improvement. Like any performance debugging task, using the right tools is crucial. In this case, I turned to Gradle Build Scan for assistance.


1. Running Analysis

The first step in solving performance issues is to gather data that will help you pinpoint the bottleneck. This could involve profiling, sampling, or data dumping. The build process with Gradle is intricate, ranging from configuration of various plugins, scripts and dependencies to executions of compile, test and publish tasks. Without clear visibility into which part is consuming the most time, it’s challenging to identify where improvements are needed. For Gradle, utilizing the build scan feature is an effective way to acquire detailed metadata about your build.

Gradle Build Scan

Gradle includes a feature known as build scan, which provides a comprehensive report of your build. Contrary to what the name might suggest, it actually works with any Gradle tasks, not just the build task. To enable a build scan, append the --scan option to your Gradle command. Optionally, you can add --rerun-tasks to prevent Gradle from using the build cache so you can observe actual execution of all tasks.

./gradlew build --scan --rerun-tasks

Executing this command will prompt you to agree to terms of service, after that your build scan result will be publicly uploaded to scans.gradle.com.

⚠️Warning⚠️ Your build scan result URL will be publicly available.


2. Locating the Bottleneck

Now, let’s dive straight into the performance section of the build scan. The Build tab in the Performance page gives you a summary of the total build time, breaking down how much time each part took.

In this case, the total build time was 15 minutes and 3 seconds, with 2 minutes and 50 seconds spent on configuration and 12 minutes and 11 seconds on the execution of tasks. I recommend exploring every tab of the performance page, as each offers different insights into potential problem areas.

Configuration

In the “Configuration” tab, identify which configurations consumed the most amount of time.

Dependency resolution

The “Dependency Resolution” tab here indicates that resolving dependencies was a significant time consumer during configuration. Coupled with information from the Configuration tab, it seems likely that complex task dependencies were also contributing to the issue.

Task execution

The primary cause of the prolonged build time stemmed from task execution itself. The total time for executing all tasks was 19 min and 36 sec, but actual execution time was reduced to 12 minutes and 11 seconds thanks to Gradle’s parallel processing capabilities. To dive deeper, click on the “Tasks Executed” link.

By grouping tasks by type, it became evident that test tasks were the most time-consuming. We’ve identified our bottleneck! Still, it’s valuable to review other tabs in the Performance page for a comprehensive understanding.

Build Cache

For debugging purposes, I wasn’t utilizing any cache, but it’s crucial to leverage the build cache in actual builds to prevent unnecessary task reruns.

Daemon

Reusing the Gradle daemon can also enhance build performance. The daemon tab will indicate whether the Gradle daemon is functioning correctly or even running at all.

Network activity

The Network Activity tab can help identify network issues, problems connecting to the Gradle repository, or issues with remote dependencies not being cached.


3. Finding and Fixing the Issue

Having identified that slow tests were the bottleneck, the next step was to determine why and how to fix this. For insights into slow task execution, check out the Timeline tab.

The Timeline showed that the slowness was due to long-running tests (specifically, Spring Boot tests) being executed sequentially. With many Gradle workers idle for the duration of the build, it was clear we were not utilizing our CPU resources efficiently. The solution? Run tests in parallel.

Running Tests in Parallel

When using JUnit as the testing framework, there are two methods for parallel test execution: JUnit’s parallel test and Gradle’s parallel build. The former runs tests within the same module in parallel, while the latter runs tests of multiple modules in parallel. I opted for the Gradle parallel build as the fix for our problem. The reason behind this choice could be a topic for another post, as it falls outside the scope of this article.


4. Benchmark

Now that we’ve fixed the issue, let’s run a benchmark to check the fix in action. I’ve run test task under 4 different conditions, each using different test parallel setting.

try \ conditionNo parallelJUnit parallel
(unit test only)
gradle parallel onlygradle
+ junit parallel
1st14m 48s14m 18s6m 4s8m 19s
2nd14m 46s13m 57s5m 57s6m 17s
  1. No Parallel Execution
    • This is our baseline for comparison, with no parallelization implemented.
  2. JUnit Parallel (Unit Test Only)
    • This was our original configuration. JUnit parallel test was enabled only on unit tests as it had problems with running Integration and Functional tests with shared mock
    • A modest improvement in build times, suggesting that parallelizing unit tests provides some benefit.
  3. Gradle Parallel Only
    • A significant reduction in build times, indicating that Gradle’s parallel execution capability is highly effective.
  4. Gradle + JUnit Parallel
    • Despite combining both JUnit and Gradle parallelizations, there was an unexpected increase in build times, possibly due to excessive context switching.

The benchmarkvshows a clear picture: Gradle’s parallel execution capabilities are most effective for our project, drastically reducing build times and enhancing overall efficiency. The attempt to combine Gradle and JUnit parallel executions, while theoretically sound, proved counterproductive, likely due to the increased complexity and resource contention.


5. Conclusion

The journey to optimize our project’s build process has been both enlightening and impactful. It reinforced the importance of a systematic approach to performance issues and showcased how effective tools, such as Gradle Build Scan, can significantly aid in diagnosing and resolving bottlenecks. By employing a data-driven strategy, we were able to pinpoint the specific areas that required attention, notably the task execution phase dominated by time-consuming tests.

Leave a Reply

Your email address will not be published. Required fields are marked *