Reputation: 24568
I’m the author of the build-time-tracker plugin that reports build time. It uses build listeners.
Using Configuration Cache along with a plugin that registers build listeners fail with the following error:
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache problems found in this build.
...
- Plugin 'com.asarkar.gradle.build-time-tracker': registration of listener on 'Gradle.addListener' is unsupported
Plugins and build scripts must not register any build listeners. That is listeners registered at configuration time that get notified at execution time. For example a BuildListener or a TaskExecutionListener.
These should be replaced by build services
Build services docs:
https://docs.gradle.org/7.2/userguide/build_services.html#build_services
A build service can be used to receive events as tasks are executed. To do this, create and register a build service that implements OperationCompletionListener. Then, you can use the methods on the BuildEventsListenerRegistry service to start receiving events.
It appears BuildEventsListenerRegistry only has a method for task completion from which we can get task execution time, but there are no methods for build initiation and completion. Without a way to find total build time, the plugin can’t start using build services.
There is a ticket I have opened with Gradle that is just sitting there. As quoted above, the docs make it sound like it build listeners can be easily replaced by build services, but the API doesn’t match up. The ticket also has a minimum, reproducible example.
Anyone know how to use build services to find total build time?
Upvotes: 1
Views: 1561
Reputation: 6508
Agreed: it looks like the Gradle team haven't exposed a new API to do this. There is a feature request out there, and it's currently marked as due for Gradle 8.1. So stay tuned.
In the meantime, it is possible to find internal Gradle classes where this information is exposed. In particular, there is a BuildOperationListener
class which can be used to listen for the end of the build.
To this end, I'm currently using the following plugin (written in Kotlin) to do the same job in Gradle 7.6:
import java.time.Duration
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.internal.build.event.BuildEventListenerRegistryInternal
import org.gradle.internal.operations.BuildOperationCategory
import org.gradle.internal.operations.BuildOperationDescriptor
import org.gradle.internal.operations.BuildOperationListener
import org.gradle.internal.operations.OperationFinishEvent
import org.gradle.internal.operations.OperationIdentifier
import org.gradle.internal.operations.OperationProgressEvent
import org.gradle.internal.operations.OperationStartEvent
class BuildFinishedPlugin @Inject constructor(
private val buildEvents: BuildEventListenerRegistryInternal
) : Plugin<Project> {
override fun apply(target: Project) {
val buildFinishedService = target.gradle.sharedServices.registerIfAbsent("buildFinished", BuildFinishedService::class.java) {
parameters.buildStartTime = LocalDateTime.now()
}
buildEvents.onOperationCompletion(buildFinishedService)
}
/**
* Class needs to be a [BuildService] in order to act as an [BuildOperationListener]
*/
abstract class BuildFinishedService : BuildService<BuildFinishedService.Parameters>, BuildOperationListener {
interface Parameters : BuildServiceParameters {
var buildStartTime: LocalDateTime
}
override fun started(buildOperation: BuildOperationDescriptor, startEvent: OperationStartEvent) {}
override fun progress(operationIdentifier: OperationIdentifier, progressEvent: OperationProgressEvent) {}
override fun finished(buildOperation: BuildOperationDescriptor, finishEvent: OperationFinishEvent) {
val category = buildOperation.metadata as? BuildOperationCategory
if (category == BuildOperationCategory.RUN_MAIN_TASKS)
buildFinishedAction()
}
private fun buildFinishedAction() {
val timeNow = LocalDateTime.now()
println("Finished build at ${timeNow.formatAsDate()}")
println("Build time: ${timeSinceBuildStarted(timeNow).formatAsDuration()}")
}
private fun timeSinceBuildStarted(buildFinishTime: LocalDateTime): Duration
= Duration.between(parameters.buildStartTime, buildFinishTime)
private fun LocalDateTime.formatAsDate(): String {
val formatter = DateTimeFormatter.ofPattern("HH:mm.ss")
return formatter.format(this)
}
private fun Duration.formatAsDuration(): String {
return when (val minutes = toMinutes()) {
0L -> "${seconds()}s"
else -> "$minutes min ${seconds()}s"
}
}
private fun Duration.seconds(): String {
return String.format("%1.3f", toMillisPart().toDouble() / 1000)
}
}
}
This blog post by Jonny Caley goes into further details on this approach (and has arguably a more elegant way of getting the build start time).
Upvotes: 1