Gradle Configuration Caching — Replacing a Build Listener with Build Services

Gradle recently rolled out a new feature called the “configuration cache” aimed at “significantly improving build performance by caching the result of the configuration phase and reusing it for subsequent builds. Using the configuration cache, Gradle can skip the configuration phase entirely when nothing that affects the build configuration, such as build scripts, has changed.”
One requirement of enabling the configuration cache is that your project must not register any BuildListeners as these cannot be serialised to disk and thus, cached. As a result, it leaves developers stuck between a rock and a hard place, as they have to decide if they want to sacrifice build speed for build time information or vice versa.
In this blog post I will be talking about how you can enable the configuration cache and capture build data utilising Gradle BuildServices and their internal APIs, for which there are little resources online in achieving.
Introducing Build Services
Build services provide an alternative mechanism for hooking into a Gradle build and receiving information about task execution and operation completion. And most importantly, they’re configuration cacheable.
Capturing Build Time Information
To capture build time information we create a build service that implements Gradle’s internal BuildOperationListener interface. When BuildOperationListener#finished is called with the RunRootBuildWorkBuildOperationType the build has finished and the build start time is provided via its Details.
Next, to capture configuration time and confirmation the configuration phase finished successfully, we use the start time of the first task provided by operationFinishEvent and subtract from it the start time of the build.
Once the BuildDurationService is created, to start receiving BuildOperationListener callbacks, we register the service with Gradle’s BuildEventListenerRegistryInternal.
Capturing Task Execution Information
Similarly to capturing build time duration, Gradle allows you to register a service to listen to task completion events via OperationCompletionListener callbacks.
Filtering each event’s display name will provide their task execution result and (if any) a build failure message.
To start receiving callbacks we register the service as a task completion listener.
Reporting Data
To report this data we will create another build service to isolate the functionality between the generation and reporting, parsing the services as arguments to access their data. Implementing the AutoCloseable interface provides a close function to trigger the report, with it being called at the end of the build. And finally, we implement the BuildOperationListener with empty functions to ensure the build service is initialised during the build and that the AutoCloseable#onClose is executed (AutoCloseable#onClose is executed when the service is disposed of, because this service isn’t actually being used by any tasks we need to give Gradle another reason to initialise the service, hence registering it for build operation callbacks).
Finally, we register the build service and parse the other services as parameter.
If all has been done correctly you should see an output in your Build Output window similar to the following:
BuildReport(totalElapsedBuildTimeMs=23525, configurationTimeMs=8877, taskExecutionStatistics=org.gradle.api.internal.tasks.execution.statistics.TaskExecutionStatistics@6a5aabca, buildFailed=false, buildFailureMessage=null, buildTaskNames=:app:assembleDebug, gradleVersion=7.4.2, daemonsRunning=1)
Parsing Cacheable Values
You may also want to parse additional values to a build service such as the task names of the build, Gradle version etc. However, before you do it’s important to know these values will be provided by the configuration cache on builds when the cache is used so don’t parse any values that could change in between cached builds such as the number of Gradle daemons running.
To parse cacheable values you add additional get functions to the BuildServiceParameters interface defined in your service. Note: The function names must start with “get” and return the type wrapped in a Property.
Cached values are then set when registering the service.
Caveats
- There are numerous APIs used throughout that are either marked as unstable or live in an internal Gradle directory. With this comes increased risks of these APIs changing or being removed entirely in the future.
- Configuration time is not reported when the configuration phase fails. This is due to the fact that the start build time is acquired from the root task, if the configuration phase fails the root task won’t execute and no start time for us.
You can find a working project with all of the above code in my Github.
Happy building…
Shout out to Michael Tweed and Alejandro Rosas for proof reading and feedback.