Skip to content

Commit f41d420

Browse files
authored
Better CPU and configuration metrics (#2)
* Add more information to reports * Configuration measurement works again * Provided reporter used again * Fix issues * Add percents to graph
1 parent 69e031e commit f41d420

15 files changed

Lines changed: 235 additions & 82 deletions

src/main/kotlin/com/smplio/gradle/build/insights/GradleInsightsPlugin.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import com.smplio.gradle.build.insights.modules.graph.GraphBuilder
44
import com.smplio.gradle.build.insights.modules.load.SystemLoadModule
55
import com.smplio.gradle.build.insights.modules.timing.ExecutionTimeMeasurementModule
66
import com.smplio.gradle.build.insights.report.CompositeReportBuildService
7+
import com.smplio.gradle.build.insights.report.IReporter
78
import com.smplio.gradle.build.insights.report.impl.html.HTMLReporter
89
import org.gradle.api.Plugin
910
import org.gradle.api.Project
1011
import org.gradle.api.plugins.ReportingBasePlugin
12+
import org.gradle.api.provider.Provider
1113
import org.gradle.build.event.BuildEventsListenerRegistry
1214
import javax.inject.Inject
1315

@@ -39,17 +41,17 @@ class GradleInsightsPlugin @Inject constructor(private val registry: BuildEvents
3941
CompositeReportBuildService::class.java.simpleName,
4042
CompositeReportBuildService::class.java,
4143
) { buildServiceSpec ->
42-
buildServiceSpec.parameters.reporters.set(mutableListOf(
43-
pluginConfig.getExecutionTimeMeasurementConfiguration().executionTimeReporter.get(),
44+
buildServiceSpec.parameters.reporters.set(mutableListOf<Provider<IReporter>>(
45+
pluginConfig.getExecutionTimeMeasurementConfiguration().executionStatsReporter as Provider<IReporter>,
4446
).also { list ->
4547
if (pluginConfig.gatherHtmlReport.get()) {
46-
list.add(HTMLReporter(project))
48+
list.add(project.provider { HTMLReporter(project) })
4749
}
4850
})
49-
executionTimeMeasurementModule.getConfigurationTimeReportProvider().let {
50-
buildServiceSpec.parameters.configurationTimeReportProvider.set(it)
51+
executionTimeMeasurementModule.getConfigurationTimeTimeMeasurementService()?.let {
52+
buildServiceSpec.parameters.configurationTimeReportService.set(it)
5153
}
52-
executionTimeMeasurementModule.getExecutionTimeReportProvider()?.let {
54+
executionTimeMeasurementModule.getExecutionTimeTimeMeasurementService()?.let {
5355
buildServiceSpec.parameters.executionTimeReportService.set(it)
5456
}
5557
systemLoadModule.getSystemLoadReportProvider()?.let {

src/main/kotlin/com/smplio/gradle/build/insights/modules/load/SystemLoadMetric.kt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.smplio.gradle.build.insights.modules.load
33
import com.codahale.metrics.Gauge
44
import com.codahale.metrics.MetricRegistry
55
import java.lang.management.ManagementFactory
6+
import java.time.Duration
67

78
sealed class SystemLoadMetric<T: Number>(val name: String, val metricProvider: () -> T): Gauge<T> {
89

@@ -22,6 +23,96 @@ sealed class SystemLoadMetric<T: Number>(val name: String, val metricProvider: (
2223
val memoryBean = ManagementFactory.getMemoryMXBean()
2324
memoryBean.heapMemoryUsage.used
2425
})
26+
27+
// CPU usage (percent) for everything running inside the current Gradle JVM (100% equals one fully utilized core)
28+
class GradleJvmCpuPercentMetric : SystemLoadMetric<Double>("gradleJvmCpuPercent", {
29+
CpuLoadSampler.sampleJvmProcessCpuPercent()
30+
})
31+
32+
// CPU usage (percent) aggregated across all descendant processes started by Gradle that run outside this JVM (100% equals one fully utilized core)
33+
class GradleDescendantsCpuPercentMetric : SystemLoadMetric<Double>("gradleDescendantsCpuPercent", {
34+
CpuLoadSampler.sampleChildrenCpuPercent()
35+
})
36+
37+
private object CpuLoadSampler {
38+
private val processors: Int = Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
39+
40+
// JVM process sampling state
41+
private var lastWallTimeJvmNanos: Long = System.nanoTime()
42+
private var lastCpuTimeJvmNanos: Long = currentJvmProcessCpuTimeNanos()
43+
44+
// Children sampling state
45+
private var lastWallTimeChildrenNanos: Long = System.nanoTime()
46+
private var lastCpuTimeChildrenNanos: Long = currentChildrenCpuTimeNanos()
47+
48+
@Synchronized
49+
fun sampleJvmProcessCpuPercent(): Double {
50+
val now = System.nanoTime()
51+
val cpuNow = currentJvmProcessCpuTimeNanos()
52+
val deltaCpu = (cpuNow - lastCpuTimeJvmNanos).coerceAtLeast(0L)
53+
val deltaWall = (now - lastWallTimeJvmNanos).coerceAtLeast(1L)
54+
lastCpuTimeJvmNanos = cpuNow
55+
lastWallTimeJvmNanos = now
56+
val pct = (deltaCpu.toDouble() / deltaWall.toDouble()) * 100.0
57+
return pct.coerceIn(0.0, 100.0 * processors)
58+
}
59+
60+
@Synchronized
61+
fun sampleChildrenCpuPercent(): Double {
62+
val now = System.nanoTime()
63+
val cpuNow = currentChildrenCpuTimeNanos()
64+
val deltaCpu = (cpuNow - lastCpuTimeChildrenNanos).coerceAtLeast(0L)
65+
val deltaWall = (now - lastWallTimeChildrenNanos).coerceAtLeast(1L)
66+
lastCpuTimeChildrenNanos = cpuNow
67+
lastWallTimeChildrenNanos = now
68+
val pct = (deltaCpu.toDouble() / deltaWall.toDouble()) * 100.0
69+
// With 100% meaning one full core, allow up to cores*100%
70+
return pct.coerceIn(0.0, 100.0 * processors)
71+
}
72+
73+
private fun currentJvmProcessCpuTimeNanos(): Long {
74+
// Prefer com.sun.management.OperatingSystemMXBean for precise JVM CPU time
75+
val osBean = ManagementFactory.getOperatingSystemMXBean()
76+
val cpuTimeFromOsBean = try {
77+
val sunBean = osBean as? com.sun.management.OperatingSystemMXBean
78+
sunBean?.processCpuTime ?: -1L
79+
} catch (_: Throwable) {
80+
-1L
81+
}
82+
if (cpuTimeFromOsBean >= 0L) return cpuTimeFromOsBean
83+
84+
// Fallback to ProcessHandle info
85+
return try {
86+
val duration: Duration? = ProcessHandle.current().info().totalCpuDuration().orElse(null)
87+
duration?.toNanos() ?: 0L
88+
} catch (_: Throwable) {
89+
0L
90+
}
91+
}
92+
93+
private fun currentChildrenCpuTimeNanos(): Long {
94+
return try {
95+
var sum = 0L
96+
val current = ProcessHandle.current()
97+
// Use descendants to include nested children
98+
current.descendants().forEach { descendant ->
99+
try {
100+
val d: Duration? = descendant.info().totalCpuDuration().orElse(null)
101+
if (d != null) {
102+
sum += d.toNanos()
103+
}
104+
} catch (e: Throwable) {
105+
// ignore processes we cannot inspect
106+
e.printStackTrace()
107+
}
108+
}
109+
sum
110+
} catch (e: Throwable) {
111+
e.printStackTrace()
112+
0L
113+
}
114+
}
115+
}
25116
}
26117

27118
fun <T: Number> MetricRegistry.register(metric: SystemLoadMetric<T>) {

src/main/kotlin/com/smplio/gradle/build/insights/modules/load/SystemLoadService.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ abstract class SystemLoadService: BuildService<BuildServiceParameters.None>,
2121
registry.register(SystemLoadMetric.HeapUsedMetric())
2222
registry.register(SystemLoadMetric.HeapMaxMetric())
2323

24+
registry.register(SystemLoadMetric.GradleJvmCpuPercentMetric())
25+
registry.register(SystemLoadMetric.GradleDescendantsCpuPercentMetric())
26+
2427
metricsReporter = LocalCacheReporter(
2528
registry,
2629
"LocalCacheRegistry",
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.smplio.gradle.build.insights.modules.timing
22

33
import com.smplio.gradle.build.insights.modules.timing.report.ConsoleExecutionTimeReporter
4+
import com.smplio.gradle.build.insights.report.execution.IExecutionStatsReceiver
45
import com.smplio.gradle.build.insights.report.timing.ITaskExecutionTimeReportReceiver
56
import org.gradle.api.model.ObjectFactory
67
import org.gradle.api.provider.Property
@@ -9,9 +10,9 @@ import javax.inject.Inject
910
abstract class ExecutionTimeMeasurementConfiguration @Inject constructor(objects: ObjectFactory) {
1011
val enabled: Property<Boolean> = objects.property(Boolean::class.java).convention(true)
1112

12-
val executionTimeReporter: Property<ITaskExecutionTimeReportReceiver> = (objects.property(
13-
ITaskExecutionTimeReportReceiver::class.java
14-
) as Property<ITaskExecutionTimeReportReceiver>).convention(
13+
val executionStatsReporter: Property<IExecutionStatsReceiver> = (objects.property(
14+
IExecutionStatsReceiver::class.java
15+
) as Property<IExecutionStatsReceiver>).convention(
1516
ConsoleExecutionTimeReporter()
1617
)
1718
}
Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package com.smplio.gradle.build.insights.modules.timing
22

3-
import com.smplio.gradle.build.insights.report.timing.IConfigurationTimeReportProvider
3+
import com.smplio.gradle.build.insights.modules.timing.report_providers.ConfigurationTimeMeasurementService
44
import org.gradle.api.Project
55
import org.gradle.build.event.BuildEventsListenerRegistry
6-
import com.smplio.gradle.build.insights.modules.timing.report_providers.ConfigurationTimeReportProvider
7-
import com.smplio.gradle.build.insights.modules.timing.report_providers.TaskTaskExecutionTimeMeasurementService
6+
import com.smplio.gradle.build.insights.modules.timing.report_providers.TaskExecutionTimeMeasurementService
87
import org.gradle.api.provider.Provider
98

109
class ExecutionTimeMeasurementModule(
@@ -13,24 +12,34 @@ class ExecutionTimeMeasurementModule(
1312
private val configuration: ExecutionTimeMeasurementConfiguration,
1413
) {
1514

16-
private var taskExecutionTimeMeasurementService: Provider<TaskTaskExecutionTimeMeasurementService>? = null
17-
private val configurationTimeReportProvider = ConfigurationTimeReportProvider()
15+
private var configurationTimeMeasurementService: Provider<ConfigurationTimeMeasurementService>? = null
16+
private var taskExecutionTimeMeasurementService: Provider<TaskExecutionTimeMeasurementService>? = null
1817

1918
fun initialize() {
2019
val buildStartTime = System.currentTimeMillis()
2120

22-
project.gradle.beforeProject {
23-
configurationTimeReportProvider.onBeforeProject(it)
24-
}
21+
if (configuration.enabled.get()) {
22+
configurationTimeMeasurementService = project.gradle.sharedServices.registerIfAbsent(
23+
ConfigurationTimeMeasurementService::class.java.simpleName,
24+
ConfigurationTimeMeasurementService::class.java,
25+
) {}.also {
26+
registry.onTaskCompletion(it)
27+
}
2528

26-
project.gradle.afterProject {
27-
configurationTimeReportProvider.onAfterProject(it)
29+
configurationTimeMeasurementService?.get()?.onBeforeProject(project)
30+
31+
project.gradle.beforeProject {
32+
configurationTimeMeasurementService?.get()?.onBeforeProject(it)
33+
}
34+
project.gradle.afterProject {
35+
configurationTimeMeasurementService?.get()?.onAfterProject(it)
36+
}
2837
}
2938

3039
if (configuration.enabled.get()) {
3140
taskExecutionTimeMeasurementService = project.gradle.sharedServices.registerIfAbsent(
32-
TaskTaskExecutionTimeMeasurementService::class.java.simpleName,
33-
TaskTaskExecutionTimeMeasurementService::class.java,
41+
TaskExecutionTimeMeasurementService::class.java.simpleName,
42+
TaskExecutionTimeMeasurementService::class.java,
3443
) {
3544
it.parameters.buildStartTime.set(buildStartTime)
3645
}.also {
@@ -39,11 +48,11 @@ class ExecutionTimeMeasurementModule(
3948
}
4049
}
4150

42-
fun getConfigurationTimeReportProvider(): IConfigurationTimeReportProvider {
43-
return configurationTimeReportProvider
51+
fun getConfigurationTimeTimeMeasurementService(): Provider<ConfigurationTimeMeasurementService>? {
52+
return configurationTimeMeasurementService
4453
}
4554

46-
fun getExecutionTimeReportProvider(): Provider<TaskTaskExecutionTimeMeasurementService>? {
55+
fun getExecutionTimeTimeMeasurementService(): Provider<TaskExecutionTimeMeasurementService>? {
4756
return taskExecutionTimeMeasurementService
4857
}
4958
}

src/main/kotlin/com/smplio/gradle/build/insights/modules/timing/report/ConsoleExecutionTimeReporter.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
package com.smplio.gradle.build.insights.modules.timing.report
2+
import com.smplio.gradle.build.insights.report.execution.IExecutionStatsReceiver
23
import com.smplio.gradle.build.insights.report.timing.IConfigurationTimeReportReceiver
34
import com.smplio.gradle.build.insights.report.timing.ITaskExecutionTimeReportReceiver
45
import java.time.Duration
56

6-
class ConsoleExecutionTimeReporter: IConfigurationTimeReportReceiver, ITaskExecutionTimeReportReceiver {
7+
class ConsoleExecutionTimeReporter:
8+
IExecutionStatsReceiver,
9+
IConfigurationTimeReportReceiver,
10+
ITaskExecutionTimeReportReceiver
11+
{
712

813
private var configurationTimeReport: ConfigurationTimeReport? = null
914
private var taskExecutionTimeReport: TaskExecutionTimeReport? = null
1015

16+
override fun reportExecutionStats(stats: ExecutionStats) {
17+
stats.configurationTimeline?.let { reportConfigurationTime(it) }
18+
stats.taskExecutionTimeline?.let { reportTaskExecutionTime(it) }
19+
}
20+
1121
override fun reportConfigurationTime(configurationTimeReport: ConfigurationTimeReport) {
1222
this.configurationTimeReport = configurationTimeReport
1323
}
@@ -24,7 +34,7 @@ class ConsoleExecutionTimeReporter: IConfigurationTimeReportReceiver, ITaskExecu
2434
override fun submitReport() {
2535
var firstConfigurationStartTime = 0L
2636
var lastTaskEndTime = 0L
27-
configurationTimeReport?.let { report ->
37+
configurationTimeReport?.takeIf { it.isNotEmpty() }?.let { report ->
2838
println("Configuration time:")
2939
firstConfigurationStartTime = report.minOf { it.startTime }
3040
val lastConfigurationEndTime = report.maxOf { it.endTime }
@@ -40,7 +50,7 @@ class ConsoleExecutionTimeReporter: IConfigurationTimeReportReceiver, ITaskExecu
4050
}
4151
}
4252

43-
taskExecutionTimeReport?.let { report ->
53+
taskExecutionTimeReport?.takeIf { it.isNotEmpty() }?.let { report ->
4454
println("Task execution time:")
4555
val firstTaskStartTime = report.minOf { it.startTime }
4656

src/main/kotlin/com/smplio/gradle/build/insights/modules/timing/report_providers/ConfigurationTimeReportProvider.kt renamed to src/main/kotlin/com/smplio/gradle/build/insights/modules/timing/report_providers/ConfigurationTimeMeasurementService.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ import com.smplio.gradle.build.insights.modules.timing.models.Measured
55
import com.smplio.gradle.build.insights.modules.timing.report.ConfigurationTimeReport
66
import com.smplio.gradle.build.insights.report.timing.IConfigurationTimeReportProvider
77
import org.gradle.api.Project
8+
import org.gradle.api.services.BuildService
9+
import org.gradle.api.services.BuildServiceParameters
10+
import org.gradle.tooling.events.FinishEvent
11+
import org.gradle.tooling.events.OperationCompletionListener
812
import java.util.concurrent.ConcurrentHashMap
13+
import java.util.concurrent.ConcurrentLinkedQueue
914

10-
class ConfigurationTimeReportProvider: IConfigurationTimeReportProvider {
11-
15+
abstract class ConfigurationTimeMeasurementService : BuildService<BuildServiceParameters.None>,
16+
IConfigurationTimeReportProvider,
17+
OperationCompletionListener
18+
{
1219
private val configurationStartTimes = ConcurrentHashMap<String, Long>()
13-
private val configurationTimeline = mutableListOf<Measured<ConfigurationInfo>>()
14-
15-
override fun provideConfigurationTimeReport(): ConfigurationTimeReport? {
16-
return configurationTimeline
17-
}
20+
private val configurationTimeline = ConcurrentLinkedQueue<Measured<ConfigurationInfo>>()
1821

1922
fun onBeforeProject(project: Project) {
2023
configurationStartTimes[project.displayName] = System.currentTimeMillis()
@@ -30,4 +33,10 @@ class ConfigurationTimeReportProvider: IConfigurationTimeReportProvider {
3033
endTime = System.currentTimeMillis(),
3134
))
3235
}
36+
37+
override fun onFinish(event: FinishEvent) {}
38+
39+
override fun provideConfigurationTimeReport(): ConfigurationTimeReport? {
40+
return configurationTimeline.toList()
41+
}
3342
}

src/main/kotlin/com/smplio/gradle/build/insights/modules/timing/report_providers/TaskTaskExecutionTimeMeasurementService.kt renamed to src/main/kotlin/com/smplio/gradle/build/insights/modules/timing/report_providers/TaskExecutionTimeMeasurementService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import org.gradle.tooling.events.task.TaskSkippedResult
1616
import org.gradle.tooling.events.task.TaskSuccessResult
1717
import java.util.concurrent.ConcurrentLinkedQueue
1818

19-
abstract class TaskTaskExecutionTimeMeasurementService : BuildService<TaskTaskExecutionTimeMeasurementService.Parameters>,
19+
abstract class TaskExecutionTimeMeasurementService : BuildService<TaskExecutionTimeMeasurementService.Parameters>,
2020
OperationCompletionListener,
2121
ITaskExecutionTimeReportProvider,
2222
IReportProvider

src/main/kotlin/com/smplio/gradle/build/insights/report/CompositeReportBuildService.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import com.smplio.gradle.build.insights.modules.timing.models.Measured
77
import com.smplio.gradle.build.insights.modules.timing.models.TaskInfo
88
import com.smplio.gradle.build.insights.modules.timing.report.BuildHostInfo
99
import com.smplio.gradle.build.insights.modules.timing.report.ExecutionStats
10-
import com.smplio.gradle.build.insights.report.timing.IConfigurationTimeReportProvider
10+
import com.smplio.gradle.build.insights.modules.timing.report_providers.ConfigurationTimeMeasurementService
1111
import com.smplio.gradle.build.insights.report.timing.IConfigurationTimeReportReceiver
1212
import com.smplio.gradle.build.insights.report.timing.ITaskExecutionTimeReportReceiver
13-
import com.smplio.gradle.build.insights.modules.timing.report_providers.TaskTaskExecutionTimeMeasurementService
13+
import com.smplio.gradle.build.insights.modules.timing.report_providers.TaskExecutionTimeMeasurementService
1414
import com.smplio.gradle.build.insights.report.execution.IExecutionStatsReceiver
1515
import org.gradle.api.provider.ListProperty
1616
import org.gradle.api.provider.Property
17+
import org.gradle.api.provider.Provider
1718
import org.gradle.api.services.BuildService
1819
import org.gradle.api.services.BuildServiceParameters
1920
import org.gradle.tooling.events.FinishEvent
@@ -26,17 +27,17 @@ abstract class CompositeReportBuildService : BuildService<CompositeReportBuildSe
2627
AutoCloseable
2728
{
2829
interface Parameters: BuildServiceParameters {
29-
val reporters: ListProperty<IReporter>
30-
val configurationTimeReportProvider: Property<IConfigurationTimeReportProvider>
30+
val reporters: ListProperty<Provider<IReporter>>
3131
val systemLoadReportService: Property<SystemLoadService>
32-
val executionTimeReportService: Property<TaskTaskExecutionTimeMeasurementService>
32+
val configurationTimeReportService: Property<ConfigurationTimeMeasurementService>
33+
val executionTimeReportService: Property<TaskExecutionTimeMeasurementService>
3334
}
3435

3536
override fun close() {
36-
val reporters = parameters.reporters.orNull ?: return
37+
val reporters = parameters.reporters.orNull?.map { it.get() } ?: return
3738
val systemLoadService = parameters.systemLoadReportService.orNull ?: return
3839
val executionTimeReportService = parameters.executionTimeReportService.orNull ?: return
39-
val configurationTimeReportProvider = parameters.configurationTimeReportProvider.orNull ?: return
40+
val configurationTimeReportProvider = parameters.configurationTimeReportService.orNull ?: return
4041

4142
reporters.filterIsInstance<ISystemLoadReportReceiver>().forEach { reporter ->
4243
systemLoadService.provideSystemLoadReport()?.let {
@@ -59,6 +60,7 @@ abstract class CompositeReportBuildService : BuildService<CompositeReportBuildSe
5960
reporters.filterIsInstance<IExecutionStatsReceiver>().forEach { reporter ->
6061
val configurationTimeline = configurationTimeReportProvider.provideConfigurationTimeReport()
6162
val taskExecutionTimeline = executionTimeReportService.provideTaskExecutionTimeReport()
63+
6264
reporter.reportExecutionStats(ExecutionStats(
6365
buildHostInfo = BuildHostInfo(),
6466
buildInfo = Measured(

src/main/kotlin/com/smplio/gradle/build/insights/report/IReporter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ package com.smplio.gradle.build.insights.report
33
import java.io.Serializable
44

55
interface IReporter: Serializable {
6-
fun submitReport()
6+
fun submitReport() {}
77
}

0 commit comments

Comments
 (0)