Create FUS Gradle plugin
fus-statistics-gradle-plugin can be used by any other Gradle plugins to
collect additional metrics for FUS.
KT-59627
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/ReadMe.md b/libraries/tools/gradle/fus-statistics-gradle-plugin/ReadMe.md
new file mode 100644
index 0000000..5537fd1
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/ReadMe.md
@@ -0,0 +1,4 @@
+## Description
+
+Contains a plugin for FUS statistics. fus-statistics-gradle-plugin can be used by other Gradle plugins to
+collect additional metrics for FUS.
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/build.gradle.kts b/libraries/tools/gradle/fus-statistics-gradle-plugin/build.gradle.kts
new file mode 100644
index 0000000..1f93955
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id("gradle-plugin-common-configuration")
+}
+
+
+dependencies {
+ commonApi(project(":kotlin-gradle-plugin-api"))
+ commonApi(project(":kotlin-gradle-plugin"))
+ commonCompileOnly(gradleKotlinDsl())
+}
+
+
+gradlePlugin {
+ plugins {
+ create("fus-statistics-gradle-plugin") {
+ id = "org.jetbrains.kotlin.fus-statistics-gradle-plugin"
+ displayName = "FusStatisticsPlugin"
+ description = displayName
+ implementationClass = "org.jetbrains.kotlin.gradle.fus.FusStatisticsPlugin"
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/FusStatisticsPlugin.kt b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/FusStatisticsPlugin.kt
new file mode 100644
index 0000000..de90880
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/FusStatisticsPlugin.kt
@@ -0,0 +1,19 @@
+package org.jetbrains.kotlin.gradle.fus
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.provider.ProviderFactory
+import javax.inject.Inject
+
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+class FusStatisticsPlugin @Inject constructor(
+ private val providerFactory: ProviderFactory
+) : Plugin<Project> {
+ override fun apply(project: Project) {
+ GradleBuildFusStatisticsService.registerIfAbsent(project).get()
+ }
+}
\ No newline at end of file
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatistics.kt b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatistics.kt
new file mode 100644
index 0000000..1612f64
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatistics.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+package org.jetbrains.kotlin.gradle.fus
+interface GradleBuildFusStatistics {
+ fun reportMetric(name: String, value: Any, subprojectName: String? = null)
+
+}
\ No newline at end of file
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatisticsService.kt b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatisticsService.kt
new file mode 100644
index 0000000..6f0689d
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatisticsService.kt
@@ -0,0 +1,114 @@
+package org.jetbrains.kotlin.gradle.fus
+
+
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+import org.gradle.api.tasks.Internal
+import org.gradle.kotlin.dsl.withType
+import java.io.File
+import java.util.UUID
+
+
+interface UsesGradleBuildFusStatisticsService : Task {
+ @get:Internal
+ val fusStatisticsBuildService: Property<GradleBuildFusStatistics?>
+}
+
+internal abstract class GradleBuildFusStatisticsService : GradleBuildFusStatistics,
+ BuildService<GradleBuildFusStatisticsService.Parameters>, AutoCloseable {
+
+ interface Parameters : BuildServiceParameters {
+ val path: Property<String>
+ val uuid: Property<String>
+ }
+
+ private val metrics = HashMap<Metric, Any>()
+
+ override fun close() {
+ val reportFile = File(parameters.path.get())
+ .resolve(STATISTICS_FOLDER_NAME)
+ .also { it.mkdirs() }
+ .resolve(parameters.uuid.get())
+ reportFile.createNewFile()
+
+ for ((metric, value) in metrics) {
+ reportFile.appendText("$metric=$value\n")
+ }
+
+ reportFile.appendText(BUILD_SESSION_SEPARATOR)
+ }
+
+ override fun reportMetric(name: String, value: Any, subprojectName: String?) {
+ metrics[Metric(name, subprojectName)] = value
+ }
+
+ companion object {
+ private const val FUS_STATISTICS_PATH = "kotlin.fus.statistics.path"
+ private const val STATISTICS_FOLDER_NAME = "kotlin-fus"
+
+ private const val BUILD_SESSION_SEPARATOR = "BUILD FINISHED"
+
+ private var statisticsIsEnabled: Boolean = true //KT-59629 Wait for user confirmation before start to collect metrics
+ private val serviceClass = GradleBuildFusStatisticsService::class.java
+ private val serviceName = "${serviceClass.name}_${serviceClass.classLoader.hashCode()}"
+
+ fun registerIfAbsent(project: Project): Provider<GradleBuildFusStatisticsService> {
+ project.gradle.sharedServices.registrations.findByName(serviceName)?.let {
+ @Suppress("UNCHECKED_CAST")
+ return it.service as Provider<GradleBuildFusStatisticsService>
+ }
+
+ return if (statisticsIsEnabled) {
+ project.gradle.sharedServices.registerIfAbsent(serviceName, serviceClass) {
+ val customPath: String = if (project.rootProject.hasProperty(FUS_STATISTICS_PATH)) {
+ project.rootProject.property(FUS_STATISTICS_PATH) as String
+ } else {
+ project.gradle.gradleUserHomeDir.path //fix
+ }
+ it.parameters.path.set(customPath)
+ it.parameters.uuid.set(UUID.randomUUID().toString())
+ }
+ } else {
+ project.gradle.sharedServices.registerIfAbsent(serviceName, DummyGradleBuildFusStatisticsService::class.java) {}
+ .map { it as GradleBuildFusStatisticsService }
+ }.also { configureTasks(project, it) }
+ }
+
+ private fun configureTasks(project: Project, serviceProvider: Provider<GradleBuildFusStatisticsService>) {
+ project.tasks.withType<UsesGradleBuildFusStatisticsService>().configureEach { task ->
+ task.fusStatisticsBuildService.value(serviceProvider).disallowChanges()
+ task.usesService(serviceProvider)
+ }
+ }
+ }
+}
+
+internal abstract class DummyGradleBuildFusStatisticsService : GradleBuildFusStatisticsService() {
+ override fun reportMetric(name: String, value: Any, subprojectName: String?) {
+ //do nothing
+ }
+
+ override fun close() {
+ //do nothing
+ }
+}
+
+data class Metric(val name: String, val projectHash: String?) : Comparable<Metric> {
+ override fun compareTo(other: Metric): Int {
+ val compareNames = name.compareTo(other.name)
+ return when {
+ compareNames != 0 -> compareNames
+ projectHash == other.projectHash -> 0
+ else -> (projectHash ?: "").compareTo(other.projectHash ?: "")
+ }
+ }
+
+ override fun toString(): String {
+ val suffix = if (projectHash == null) "" else ".${projectHash}"
+ return name + suffix
+ }
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt
index 9b2edd0..8c72375 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt
@@ -8,7 +8,11 @@
import org.gradle.api.logging.LogLevel
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
+import org.jetbrains.kotlin.test.KtAssert.assertEquals
import org.junit.jupiter.api.DisplayName
+import kotlin.io.path.name
+import kotlin.io.path.pathString
+import kotlin.io.path.readText
@DisplayName("Build FUS statistics")
class BuildFusStatisticsIT : KGPDaemonsBaseTest() {
@@ -37,4 +41,50 @@
}
}
+ @DisplayName("smoke test for fus-statistics-gradle-plugin")
+ @GradleTest
+ fun smokeTestForFusStatisticsPlugin(gradleVersion: GradleVersion) {
+ val metricName = "METRIC_NAME"
+ val metricValue = 1
+ project("simpleProject", gradleVersion) {
+ buildGradle.modify {
+ """
+ ${
+ it.replace(
+ "plugins {",
+ """
+ plugins {
+ id "org.jetbrains.kotlin.fus-statistics-gradle-plugin" version "${'$'}kotlin_version"
+ """.trimIndent()
+ )
+ }
+
+ import org.jetbrains.kotlin.gradle.fus.GradleBuildFusStatistics
+ class TestFusTask extends DefaultTask implements org.jetbrains.kotlin.gradle.fus.UsesGradleBuildFusStatisticsService {
+ private Property<GradleBuildFusStatistics> fusStatisticsBuildService = project.objects.property(GradleBuildFusStatistics.class)
+
+ org.gradle.api.provider.Property getFusStatisticsBuildService(){
+ return fusStatisticsBuildService
+ }
+
+ }
+ tasks.register("test-fus", TestFusTask.class).get().doLast {
+ fusStatisticsBuildService.get().reportMetric("$metricName", $metricValue, null)
+ }
+ """.trimIndent()
+ }
+
+ val reportRelativePath = "reports"
+ build("test-fus", "-Pkotlin.fus.statistics.path=${projectPath.resolve(reportRelativePath).pathString}") {
+ val fusReport = projectPath.getSingleFileInDir("$reportRelativePath/kotlin-fus")
+ assertFileContains(
+ fusReport,
+ "METRIC_NAME=1",
+ "BUILD FINISHED"
+ )
+ }
+ }
+ }
+
+
}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 8f596fe..a24e9a5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -216,6 +216,7 @@
":gradle:kotlin-compiler-args-properties",
":gradle:regression-benchmark-templates",
":gradle:regression-benchmarks",
+ ":gradle:fus-statistics-gradle-plugin",
":kotlin-tooling-metadata",
":kotlin-tooling-core",
":kotlin-allopen",
@@ -758,6 +759,7 @@
project(':gradle:gradle-warnings-detector').projectDir = "$rootDir/libraries/tools/gradle/gradle-warnings-detector" as File
project(':gradle:kotlin-compiler-args-properties').projectDir = "$rootDir/libraries/tools/gradle/kotlin-compiler-args-properties" as File
project(":gradle:regression-benchmark-templates").projectDir = "$rootDir/libraries/tools/gradle/regression-benchmark-templates" as File
+project(":gradle:kotlin-compiler-args-properties").projectDir = "$rootDir/libraries/tools/gradle/fus-statistics-gradle-plugin" as File
project(":gradle:regression-benchmarks").projectDir = "$rootDir/libraries/tools/gradle/regression-benchmarks" as File
project(':kotlin-tooling-metadata').projectDir = "$rootDir/libraries/tools/kotlin-tooling-metadata" as File
project(':kotlin-tooling-core').projectDir = "$rootDir/libraries/tools/kotlin-tooling-core" as File