Add configuration_cache tag into statistic

This commit is contained in:
nataliya.valtman
2022-04-19 12:04:02 +03:00
committed by Space
parent f8f01941a8
commit 4b0ac511b2
8 changed files with 769 additions and 13 deletions
@@ -66,6 +66,10 @@ dependencies {
testImplementation(project(":kotlin-android-extensions"))
testImplementation(project(":kotlin-parcelize-compiler"))
testImplementation(commonDependency("org.jetbrains.intellij.deps", "trove4j"))
testImplementation(commonDependency("io.ktor", "ktor-server-test-host"))
testImplementation(commonDependency("io.ktor", "ktor-server-core"))
testImplementation(commonDependency("io.ktor", "ktor-server-netty"))
testImplementation(commonDependency("io.ktor", "ktor-client-mock"))
testImplementation(gradleApi())
testImplementation(gradleTestKit())
@@ -0,0 +1,276 @@
/*
* Copyright 2010-2022 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
import com.google.gson.Gson
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.util.collections.*
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData
import org.jetbrains.kotlin.gradle.plugin.stat.StatTag
import org.jetbrains.kotlin.gradle.report.BuildReportType
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.test.util.joinToArrayString
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.DisplayName
import java.io.IOException
import java.net.HttpURLConnection
import java.net.ServerSocket
import java.net.URL
import java.util.*
import kotlin.io.path.appendText
import kotlin.test.fail
@DisplayName("Build statistics")
@JvmGradlePluginTests
class BuildStatisticsWithKtorIT : KGPBaseTest() {
companion object {
fun CompileStatisticsData.validateMandatoryField(kotlinVersion: String, validationData: ValidationData): List<String> {
val validationErrors = LinkedList<String>()
if (taskResult != validationData.taskResult) {
validationErrors.add("Unexpected taskResult: $taskResult instead of ${validationData.taskResult}")
}
if (this.kotlinVersion != kotlinVersion) {
validationErrors.add("Unexpected kotlinVersion: ${this.kotlinVersion} instead of ${kotlinVersion}")
}
if (compilerArguments.isEmpty()) {
validationErrors.add("Empty compiler arguments")
}
if (performanceMetrics.isEmpty()) {
validationErrors.add("Empty performance metrics")
}
if (buildTimesMetrics.isEmpty()) {
validationErrors.add("Empty build metrics")
}
for (tag: String in validationData.expectedTags) {
if (!tags.contains(tag)) {
validationErrors.add("Does not contains \'$tag\' tag")
}
}
return validationErrors
}
fun CompileStatisticsData.validateIncrementalData(validationData: ValidationData): List<String> {
if (validationData.nonIncrementalReasons.isNotEmpty()) return emptyList()
val validationErrors = LinkedList<String>()
if (changes.size != validationData.changedFiles.size) {
validationErrors.add("Changed files do not equal: ${changes.joinToArrayString()} instead of ${validationData.changedFiles.joinToArrayString()} ")
}
if (!tags.contains(StatTag.INCREMENTAL.name)) {
validationErrors.add("INCREMENTAL tag was no set")
}
return validationErrors
}
fun CompileStatisticsData.validateNonIncrementalData(validationData: ValidationData): List<String> {
if (validationData.nonIncrementalReasons.isEmpty()) return emptyList()
val validationErrors = LinkedList<String>()
if (!tags.contains(StatTag.NON_INCREMENTAL.name)) {
validationErrors.add("NON_INCREMENTAL tag was no set")
}
return validationErrors
}
fun getEmptyPort(): ServerSocket {
for (port in 8080..8180) {
try {
return ServerSocket(port).also { println("Use $port port") }
} catch (_: IOException) {
continue // try next port
}
}
throw IOException("no free port found")
}
}
private lateinit var server: ApplicationEngine
private val port = getEmptyPort().localPort
@BeforeAll
fun initEmbeddedService() {
val ktorTestDataAnnotations = this.javaClass.methods.flatMap { it.annotations.filterIsInstance<KtorTestData>() }
println("start embedded server with data: ${ktorTestDataAnnotations.joinToArrayString()}")
server = embeddedServer(Netty, port = port)
{
val ktorServerData = ktorTestDataAnnotations.associateBy({ it.projectName }) { it.validationData.toMutableList() }
val failedResults = ConcurrentList<String>()
suspend fun responseBadRequest(call: ApplicationCall, message: String) {
println("Validation errors: $message")
failedResults.plus(message)
call.respond(status = HttpStatusCode.BadRequest, message)
}
routing {
post("/badRequest") {
call.respond(HttpStatusCode.BadRequest, "Some reason")
}
post("/validate") {
val body = call.receive<String>()
println("TRACE: routing was called: $body")
val statData = Gson().fromJson(body, CompileStatisticsData::class.java)
val projectValidation = ktorServerData[statData.projectName]
if (projectValidation == null) {
responseBadRequest(call, "Unknown validation for project ${statData.projectName}")
return@post
}
val ktorTestData = projectValidation.firstOrNull { it.taskName == statData.taskName }
if (ktorTestData == null) {
responseBadRequest(call, "${statData.projectName}: Unknown validation for task ${statData.taskName}")
return@post
}
//validate response
statData.validateMandatoryField(defaultBuildOptions.kotlinVersion, ktorTestData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${statData.projectName}: Fail to validate mandatory fields: ${it.joinToArrayString()}"
)
return@post
}
}
statData.validateIncrementalData(ktorTestData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${statData.projectName}: Fail to validate incremental fields: ${it.joinToArrayString()}"
)
return@post
}
}
statData.validateNonIncrementalData(ktorTestData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${statData.projectName}: Fail to validate non-incremental fields: ${it.joinToArrayString()}"
)
return@post
}
}
call.respond(HttpStatusCode.OK)
}
get("/results") {
if (failedResults.isEmpty()) {
call.respond(HttpStatusCode.OK)
} else {
call.respond(HttpStatusCode.InternalServerError, message = failedResults.joinToArrayString())
}
}
}
}
server.start()
}
@AfterAll
fun shutDownEmbeddedService() {
server.stop(1000, 1000)
}
@DisplayName("Http build report request problems are logged only ones")
@GradleTest
fun testHttpServiceWithBadRequest(gradleVersion: GradleVersion) {
project("incrementalMultiproject", gradleVersion) {
enableStatisticReports(BuildReportType.HTTP, "http://localhost:$port/badRequest")
build("assemble") {
assertOutputContainsExactTimes("Failed to send statistic to", 1)
}
}
}
@KtorTestData(
"validateMandatoryField",
[
//first run
ValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]),
ValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"])
]
)
@DisplayName("Validate mandatory field for http request body")
@GradleTest
fun testHttpRequest(gradleVersion: GradleVersion) {
project("incrementalMultiproject", gradleVersion) {
setProjectForTest("validateMandatoryField")
build("assemble") {
assertOutputDoesNotContain("Failed to send statistic to")
}
}
}
@KtorTestData(
"validateConfigurationCache",
[
//first run
ValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL", "CONFIGURATION_CACHE"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]),
ValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL", "CONFIGURATION_CACHE"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]),
//second run
ValidationData(":lib:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]),
ValidationData(":app:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]),
]
)
@DisplayName("Validate configuration cache tag")
@GradleTest
fun testConfigurationCache(gradleVersion: GradleVersion) {
val buildOptions = defaultBuildOptions.copy(configurationCache = true)
project("incrementalMultiproject", gradleVersion) {
setProjectForTest("validateConfigurationCache")
build("assemble", buildOptions = buildOptions) {
assertOutputDoesNotContain("Failed to send statistic to")
}
projectPath.resolve("lib/src/main/kotlin/bar/B.kt")
build("assemble", buildOptions = buildOptions) {
assertOutputDoesNotContain("Failed to send statistic to")
}
}
}
private fun TestProject.setProjectForTest(projectName: String) {
enableStatisticReports(BuildReportType.HTTP, "http://localhost:$port/validate")
settingsGradle.appendText("rootProject.name=\'$projectName\'")
}
}
@Repeatable
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class KtorTestData(
val projectName: String,
val validationData: Array<ValidationData>,
)
annotation class ValidationData(
//mandatory fields
val taskName: String,
val taskResult: String = "SUCCESS",
val expectedTags: Array<String> = [],
//fields for non-incremental compilation
val nonIncrementalReasons: Array<String> = [], //if empty incremental validation expected
//fields for incremental compilation
val changedFiles: Array<String> = [],
)
@@ -53,6 +53,11 @@ class BuildScanStatisticsListener(
readableString(data).forEach { buildScan.value(data.taskName, it) }
data.label?.takeIf { !tags.contains(it) }?.also {
buildScan.tag(it)
tags.add(it)
}
data.tags
.filter { !tags.contains(it) }
.forEach {
@@ -43,7 +43,9 @@ enum class StatTag {
INCREMENTAL,
NON_INCREMENTAL,
GRADLE_DEBUG,
KOTLIN_DEBUG
KOTLIN_DEBUG,
CONFIGURATION_CACHE,
BUILD_CACHE,
}
//Sensitive data. This object is used directly for statistic via http
@@ -9,8 +9,6 @@ import org.gradle.tooling.events.task.TaskFailureResult
import org.gradle.tooling.events.task.TaskFinishEvent
import org.gradle.tooling.events.task.TaskSkippedResult
import org.gradle.tooling.events.task.TaskSuccessResult
import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
import org.jetbrains.kotlin.cli.common.toBooleanLenient
import org.jetbrains.kotlin.gradle.plugin.internal.state.TaskExecutionResults
import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData
import org.jetbrains.kotlin.gradle.plugin.stat.StatTag
@@ -49,7 +47,8 @@ class KotlinBuildStatListener {
projectName: String,
uuid: String,
label: String?,
kotlinVersion: String
kotlinVersion: String,
additionalTags: List<StatTag> = emptyList()
): CompileStatisticsData? {
val result = event.result
val taskPath = event.descriptor.taskPath
@@ -89,7 +88,7 @@ class KotlinBuildStatListener {
projectName = projectName,
taskName = taskPath,
changes = changes,
tags = parseTags(taskExecutionResult).map { it.name },
tags = parseTags(taskExecutionResult, additionalTags).map { it.name },
nonIncrementalAttributes = taskExecutionResult?.buildMetrics?.buildAttributes?.asMap()?.filter { it.value > 0 }?.keys ?: emptySet(),
hostName = hostName,
kotlinVersion = kotlinVersion,
@@ -99,8 +98,8 @@ class KotlinBuildStatListener {
)
}
private fun parseTags(taskExecutionResult: TaskExecutionResult?): List<StatTag> {
val tags = ArrayList<StatTag>()
private fun parseTags(taskExecutionResult: TaskExecutionResult?, additionalTags: List<StatTag>): List<StatTag> {
val tags = ArrayList(additionalTags)
val nonIncrementalAttributes = taskExecutionResult?.buildMetrics?.buildAttributes?.asMap() ?: emptyMap()
@@ -17,8 +17,10 @@ import org.gradle.tooling.events.task.TaskFinishEvent
import org.jetbrains.kotlin.gradle.plugin.stat.BuildFinishData
import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData
import org.jetbrains.kotlin.gradle.plugin.stat.GradleBuildStartParameters
import org.jetbrains.kotlin.gradle.plugin.stat.StatTag
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatListener.Companion.prepareData
import org.jetbrains.kotlin.gradle.report.BuildMetricsReporterService.Companion.getStartParameters
import org.jetbrains.kotlin.gradle.utils.isConfigurationCacheAvailable
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
@@ -31,19 +33,24 @@ abstract class HttpReportService : BuildService<HttpReportService.Parameters>,
OperationCompletionListener, AutoCloseable {
var executorService: ExecutorService = Executors.newSingleThreadExecutor()
val uuid = UUID.randomUUID().toString()
val startTime = System.nanoTime()
interface Parameters : BuildServiceParameters {
var label: String?
var uuid: String
var projectName: String
var httpSettings: HttpReportSettings
var kotlinVersion: String
var additionalTags: List<StatTag>
var startParameters: GradleBuildStartParameters
}
private val log = Logging.getLogger(this.javaClass)
val log = Logging.getLogger(this.javaClass)
init {
log.info("Http report service is registered. Unique build id: $uuid")
}
// @Volatile for one thread executor it does not need
private var requestPreviousFailed = false
@@ -51,7 +58,8 @@ abstract class HttpReportService : BuildService<HttpReportService.Parameters>,
override fun onFinish(event: FinishEvent?) {
if (event is TaskFinishEvent) {
val data = prepareData(event, parameters.projectName, parameters.uuid, parameters.label, parameters.kotlinVersion)
val data =
prepareData(event, parameters.projectName, uuid, parameters.label, parameters.kotlinVersion, parameters.additionalTags)
data?.also { executorService.submit { report(data) } }
}
}
@@ -64,20 +72,30 @@ abstract class HttpReportService : BuildService<HttpReportService.Parameters>,
companion object {
fun registerIfAbsent(project: Project, kotlinVersion: String): Provider<HttpReportService>? {
val rootProject = project.gradle.rootProject
val gradle = project.gradle
val rootProject = gradle.rootProject
val reportingSettings = reportingSettings(rootProject)
return reportingSettings.httpReportSettings?.let { httpSettings ->
project.gradle.sharedServices.registerIfAbsent(
gradle.sharedServices.registerIfAbsent(
"build_http_metric_service_${HttpReportService::class.java.classLoader.hashCode()}",
HttpReportService::class.java
) {
it.parameters.label = reportingSettings.buildReportLabel
it.parameters.projectName = rootProject.name
it.parameters.uuid = UUID.randomUUID().toString()
it.parameters.httpSettings = httpSettings
it.parameters.kotlinVersion = kotlinVersion
it.parameters.startParameters = getStartParameters(project)
//init gradle tags, that present in build scan
val additionalTags = ArrayList<StatTag>()
if (isConfigurationCacheAvailable(gradle)) {
additionalTags.add(StatTag.CONFIGURATION_CACHE)
}
if (gradle.startParameter.isBuildCacheEnabled) {
additionalTags.add(StatTag.BUILD_CACHE)
}
it.parameters.additionalTags = additionalTags
}!!
}