Skip to content

Commit 3da6727

Browse files
committed
Add taskDuration report to capture task execution costs
1 parent 4700d02 commit 3da6727

14 files changed

Lines changed: 1724 additions & 6 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ can be used as a reference for how to add custom summarizers. Please refer to t
101101
documentation for more information on their purpose and usage:
102102
- [Config Cache Miss](src/main/kotlin/com/ebay/plugins/metrics/develocity/configcachemiss/README.md)
103103
- [Project Cost](src/main/kotlin/com/ebay/plugins/metrics/develocity/projectcost/README.md)
104+
- [Task Duration](src/main/kotlin/com/ebay/plugins/metrics/develocity/taskduration/README.md)
104105
- [User Query](src/main/kotlin/com/ebay/plugins/metrics/develocity/userquery/README.md)
105106

106107
## Run Books

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@file:Suppress("UnstableApiUsage")
2+
13
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
24
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
35

src/main/kotlin/com/ebay/plugins/metrics/develocity/MetricsForDevelocityPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ import org.gradle.api.Plugin
1313
* project.plugins.withType(MetricsForDevelocityPlugin::class.java) { ... }
1414
* ```
1515
*/
16-
interface MetricsForDevelocityPlugin<T> : Plugin<T>
16+
interface MetricsForDevelocityPlugin<T : Any> : Plugin<T>

src/main/kotlin/com/ebay/plugins/metrics/develocity/MetricsForDevelocityProjectPlugin.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.ebay.plugins.metrics.develocity.NameUtil.DURATION_TASK_PATTERN
1313
import com.ebay.plugins.metrics.develocity.configcachemiss.ConfigCacheMissPlugin
1414
import com.ebay.plugins.metrics.develocity.projectcost.ProjectCostPlugin
1515
import com.ebay.plugins.metrics.develocity.service.DevelocityBuildService
16+
import com.ebay.plugins.metrics.develocity.taskduration.TaskDurationPlugin
1617
import com.ebay.plugins.metrics.develocity.userquery.UserQueryPlugin
1718
import org.gradle.api.GradleException
1819
import org.gradle.api.Project
@@ -44,6 +45,7 @@ internal class MetricsForDevelocityProjectPlugin @Inject constructor(
4445
// Example summarizers
4546
project.plugins.apply(ConfigCacheMissPlugin::class.java)
4647
project.plugins.apply(ProjectCostPlugin::class.java)
48+
project.plugins.apply(TaskDurationPlugin::class.java)
4749
project.plugins.apply(UserQueryPlugin::class.java)
4850
}
4951

@@ -360,11 +362,7 @@ internal class MetricsForDevelocityProjectPlugin @Inject constructor(
360362
with(task) {
361363
inputTaskProviders.forEach { taskProvider ->
362364
sourceOutputDirectories.from(taskProvider.flatMap {
363-
if (it is MetricsIntermediateTask) {
364-
it.outputDirectoryProperty
365-
} else {
366-
throw IllegalStateException("Unexpected task type: ${it::class.java}")
367-
}
365+
it.outputDirectoryProperty
368366
})
369367
}
370368
zoneOffset.set(ext.zoneId)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Task Duration Plugin
2+
3+
This plugin adds a summarizer and a report task that aggregates the total execution duration
4+
of a specific task, identified by either task name (across all modules) or by task type.
5+
6+
## Usage
7+
8+
### By Task Name (Default)
9+
10+
The plugin provides a task rule that creates report tasks with the following pattern:
11+
12+
```
13+
taskDurationReport-<Task Name>-<Java Duration String>
14+
```
15+
16+
For example:
17+
- `taskDurationReport-compileJava-P7D` - Reports on all `compileJava` tasks across all modules
18+
for the last 7 days
19+
- `taskDurationReport-test-PT8H` - Reports on all `test` tasks across all modules for the
20+
last 8 hours
21+
- `taskDurationReport-assemble-P2DT8H` - Reports on all `assemble` tasks across all modules for
22+
the last 2 days and 8 hours
23+
24+
### By Task Type
25+
26+
You can also report on tasks by type using the following task name pattern, providing the
27+
type of the task as a task argument:
28+
29+
```
30+
taskTypeDurationReport-<Java Duration String>
31+
```
32+
33+
For example:
34+
- `taskTypeDurationReport-P7D --task-type com.osacky.flank.gradle.FlankExecutionTask` - Reports
35+
on all task invocations that had the `com.osacky.flank.gradle.FlankExecutionTask` type.
36+
- `taskTypeDurationReport-P7D --task-type org.jetbrains.kotlin.gradle.tasks.KotlinCompile` - Reports
37+
on all invocations of the Kotlin compilation task. This would - for example - include all
38+
invocations of `compileKotlin` as well as `compileDebugKotlin` on Android projects.
39+
40+
## Duration String Specification
41+
42+
The duration string used in the task names follows the
43+
[Java Duration format](https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#parse-java.lang.CharSequence-)
44+
standard.
45+
46+
## Filtering
47+
48+
This report may also be combined with the plugin's `develocityQueryFilter` specification
49+
to only include tasks run witin builds that match the filter specification.
50+
51+
For example:
52+
- `./gradlew -PmetricsForDevelocityQueryFilter=tag:ci-premerge taskDurationReport-test-PT8H`
53+
- `./gradlew -PmetricsForDevelocityQueryFilter='(value:"CI build number=938") and (tag:ci-premerge)' taskDurationReport-test-P7D`
54+
55+
## Report Output
56+
57+
The report output is generated in the `build/reports/taskDuration/` directory and would look
58+
similar to the following:
59+
```text
60+
Task Duration Report for task type 'com.osacky.flank.gradle.FlankExecutionTask'
61+
===============================================================================
62+
63+
Report Generated At: 2025-12-12T10 (America/Los_Angeles)
64+
Total Execution Duration: 2d9h36m
65+
Number of Task Executions: 367
66+
Average Execution Duration: 9m25s
67+
68+
Daily Breakdown
69+
===============
70+
71+
Date Count Total Min Avg Max
72+
---------- ------- ---------- ---------- ---------- ----------
73+
2025-12-10 113 19h28m52s 3m15s 10m20s 29m43s
74+
2025-12-11 190 1d5h46m 2m55s 9m24s 24m16s
75+
2025-12-12 64 8h20m44s 3m17s 7m49s 21m56s
76+
```
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.ebay.plugins.metrics.develocity.taskduration
2+
3+
import com.ebay.plugins.metrics.develocity.MetricSummarizerTask
4+
import org.gradle.api.DefaultTask
5+
import org.gradle.api.file.RegularFileProperty
6+
import org.gradle.api.model.ObjectFactory
7+
import org.gradle.api.provider.Property
8+
import org.gradle.api.tasks.Input
9+
import org.gradle.api.tasks.OutputFile
10+
import org.gradle.api.tasks.TaskAction
11+
import org.gradle.api.tasks.options.Option
12+
import javax.inject.Inject
13+
14+
/**
15+
* Create a report of the total execution duration of a specific task, by name.
16+
*/
17+
internal abstract class TaskDurationByNameReportTask @Inject constructor(
18+
objectFactory: ObjectFactory,
19+
) : DefaultTask(), MetricSummarizerTask {
20+
/**
21+
* The time span / duration being reported on.
22+
*/
23+
@get:Input
24+
val durationProvider: Property<String> = objectFactory.property(String::class.java)
25+
26+
/**
27+
* The name of the task to report on.
28+
*/
29+
@get:Input
30+
@get:Option(option = "task-name", description = "Unqualified task name")
31+
val taskName: Property<String> = objectFactory.property(String::class.java)
32+
33+
/**
34+
* The Time Zone ID to use when generating the report timestamps.
35+
*/
36+
@get:Input
37+
val zoneId: Property<String> = objectFactory.property(String::class.java)
38+
39+
/**
40+
* The output file where the report should be stored.
41+
*/
42+
@get:OutputFile
43+
val reportFile: RegularFileProperty = objectFactory.fileProperty()
44+
45+
@TaskAction
46+
fun createReport() {
47+
val summaryFile = summarizerDataProperty.get()
48+
val summarizer = TaskDurationSummarizer(zoneId)
49+
val model = summarizer.read(summaryFile)
50+
51+
val durationStr = durationProvider.get()
52+
val targetTaskName = taskName.get()
53+
TaskExecutionReportHelper.writeTaskExecutionReport(
54+
logger = logger,
55+
reportTitle = "Task Duration Report for task '$targetTaskName' for the last $durationStr (across all modules)",
56+
executionSummary = model.taskNameData[targetTaskName],
57+
reportFile = reportFile,
58+
timeZoneIdProvider = zoneId,
59+
)
60+
}
61+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.ebay.plugins.metrics.develocity.taskduration
2+
3+
import com.ebay.plugins.metrics.develocity.MetricSummarizerTask
4+
import org.gradle.api.DefaultTask
5+
import org.gradle.api.file.RegularFileProperty
6+
import org.gradle.api.model.ObjectFactory
7+
import org.gradle.api.provider.Property
8+
import org.gradle.api.tasks.Input
9+
import org.gradle.api.tasks.OutputFile
10+
import org.gradle.api.tasks.TaskAction
11+
import org.gradle.api.tasks.options.Option
12+
import javax.inject.Inject
13+
14+
/**
15+
* Create a report of the total execution duration of a specific task, by type.
16+
*/
17+
internal abstract class TaskDurationByTypeReportTask @Inject constructor(
18+
objectFactory: ObjectFactory,
19+
) : DefaultTask(), MetricSummarizerTask {
20+
/**
21+
* The time span / duration being reported on.
22+
*/
23+
@get:Input
24+
val durationProvider: Property<String> = objectFactory.property(String::class.java)
25+
26+
/**
27+
* The fully qualified type of the task to report on.
28+
*/
29+
@get:Input
30+
@get:Option(option = "task-type", description = "Fully qualified task type/class name")
31+
val taskType: Property<String> = objectFactory.property(String::class.java)
32+
33+
/**
34+
* The Time Zone ID to use when generating the report timestamps.
35+
*/
36+
@get:Input
37+
val zoneId: Property<String> = objectFactory.property(String::class.java)
38+
39+
/**
40+
* The output file where the report should be stored.
41+
*/
42+
@get:OutputFile
43+
val reportFile: RegularFileProperty = objectFactory.fileProperty()
44+
45+
@TaskAction
46+
fun createReport() {
47+
val summaryFile = summarizerDataProperty.get()
48+
val summarizer = TaskDurationSummarizer(zoneId)
49+
val model = summarizer.read(summaryFile)
50+
51+
val targetTaskType = taskType.get()
52+
TaskExecutionReportHelper.writeTaskExecutionReport(
53+
logger = logger,
54+
reportTitle = "Task Duration Report for task type '$targetTaskType'",
55+
executionSummary = model.taskTypeData[targetTaskType],
56+
reportFile = reportFile,
57+
timeZoneIdProvider = zoneId,
58+
)
59+
}
60+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.ebay.plugins.metrics.develocity.taskduration
2+
3+
import com.ebay.plugins.metrics.develocity.projectcost.merge
4+
import kotlinx.serialization.Serializable
5+
6+
/**
7+
* Intermediate data model used to aggregate the total execution duration of tasks.
8+
* Stores data by both task name (across all modules) and by task type. These details
9+
* are store both in an aggregate total as well as with a daily breakdown.
10+
*/
11+
@Serializable
12+
data class TaskDurationSummary(
13+
// Map of task name to its execution data
14+
val taskNameData: Map<String, TaskExecutionSummary> = emptyMap(),
15+
// Map of fully qualified task type/class name to its execution data
16+
val taskTypeData: Map<String, TaskExecutionSummary> = emptyMap(),
17+
) {
18+
operator fun plus(other: TaskDurationSummary): TaskDurationSummary {
19+
// Note: Because we're starting with a Map instead of a MutableMap,
20+
// Map.merge creates a new map rather than actually merge into the current Maps
21+
return TaskDurationSummary(
22+
taskNameData = taskNameData.merge(other.taskNameData) { left, right ->
23+
left + right
24+
},
25+
taskTypeData = taskTypeData.merge(other.taskTypeData) { left, right ->
26+
left + right
27+
},
28+
)
29+
}
30+
}
31+
32+
/**
33+
* Data model representing execution summary for a task, with a by-day breakdown.
34+
*/
35+
@Serializable
36+
data class TaskExecutionSummary(
37+
val total: TaskExecutionData = TaskExecutionData(),
38+
val byDay: Map<String, TaskExecutionData> = emptyMap(),
39+
) {
40+
operator fun plus(other: TaskExecutionSummary): TaskExecutionSummary {
41+
// Note: Because we're starting with a Map instead of a MutableMap,
42+
// Map.merge creates a new map rather than actually merge into the current Maps:
43+
return TaskExecutionSummary(
44+
total = total + other.total,
45+
byDay = byDay.merge(other.byDay) { left, right ->
46+
left + right
47+
}
48+
)
49+
}
50+
}
51+
52+
/**
53+
* Data for a single task's execution metrics.
54+
*/
55+
@Serializable
56+
data class TaskExecutionData(
57+
val totalDuration: Long = 0,
58+
val executionCount: Long = 0,
59+
val minDuration: Long = totalDuration,
60+
val maxDuration: Long = totalDuration,
61+
) {
62+
operator fun plus(other: TaskExecutionData): TaskExecutionData {
63+
return TaskExecutionData(
64+
totalDuration = totalDuration + other.totalDuration,
65+
executionCount = executionCount + other.executionCount,
66+
minDuration = when {
67+
executionCount != 0L && other.executionCount != 0L -> minOf(minDuration, other.minDuration)
68+
executionCount != 0L -> minDuration
69+
else -> other.minDuration
70+
},
71+
maxDuration = when {
72+
executionCount != 0L && other.executionCount != 0L -> maxOf(maxDuration, other.maxDuration)
73+
executionCount != 0L -> maxDuration
74+
else -> other.maxDuration
75+
},
76+
)
77+
}
78+
}

0 commit comments

Comments
 (0)