Skip to content

Commit

Permalink
MBS-7244 Collect remote cache hit rate metrics (#958)
Browse files Browse the repository at this point in the history
  • Loading branch information
eugene-krivobokov authored Apr 27, 2021
1 parent d9c139f commit 635ae0f
Show file tree
Hide file tree
Showing 15 changed files with 547 additions and 105 deletions.
6 changes: 6 additions & 0 deletions docs/content/projects/internal/BuildMetrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ They will be referred in docs as `<placeholder>`.
[Http build cache](https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_configure_remote) errors:

- `<namespace>.build.cache.errors.[load|store].<http status code>`: errors counter

Remote cache statistics:

- `<namespace>.build.cache.remote.[hit|miss].env.<environment>`: remote cache operations count by environments.
Shows count of cacheable tasks that were requested from the remote cache.
This is the same as **Performance** | **Build cache** | **Remote cache** | **Operations** | **Hit\Miss** in build scan.

### Common build metrics

Expand Down
2 changes: 1 addition & 1 deletion subprojects/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ style:
# we use todos a lot, maybe there is a better way
ForbiddenComment:
active: true
values: [ 'STOPSHIP:' ]
values: [ 'STOPSHIP' ]
allowedPatterns: ''
# https://detekt.github.io/detekt/style.html#forbiddenimport
# todo maybe use it to ban junit 4 in test code
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.avito.android.plugin.build_metrics

import com.avito.android.stats.TimeMetric
import com.avito.test.gradle.TestProjectGenerator
import com.avito.test.gradle.TestResult
import com.avito.test.gradle.gradlew
Expand Down Expand Up @@ -51,7 +52,7 @@ internal class BuildMetricsPluginTest {
result.assertThat()
.buildSuccessful()

result.expectMetric("time", "init_configuration.total")
result.assertHasMetric<TimeMetric>(".init_configuration.total")
}

@Test
Expand All @@ -61,7 +62,7 @@ internal class BuildMetricsPluginTest {
result.assertThat()
.buildSuccessful()

result.expectMetric("time", "build-time.total")
result.assertHasMetric<TimeMetric>(".build-time.total")
}

@TestFactory
Expand Down Expand Up @@ -90,7 +91,7 @@ internal class BuildMetricsPluginTest {
result.assertThat()
.buildSuccessful()

result.expectMetric("time", it.metricName)
result.assertHasMetric<TimeMetric>(it.metricName)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,70 @@
package com.avito.android.plugin.build_metrics

import com.avito.android.stats.CountMetric
import com.avito.android.stats.GaugeDoubleMetric
import com.avito.android.stats.GaugeLongMetric
import com.avito.android.stats.SeriesName
import com.avito.android.stats.StatsMetric
import com.avito.android.stats.TimeMetric
import com.avito.test.gradle.TestResult
import com.google.common.truth.Truth.assertWithMessage

private const val loggerPrefix = "[StatsDSender@:] "

internal fun TestResult.expectMetric(type: String, metricName: String) {
internal inline fun <reified T : StatsMetric> TestResult.assertHasMetric(path: String): T {
val type = T::class.java
val metrics = statsdMetrics()
val filtered = filter(metrics, type, metricName)
val filtered = metrics
.filterIsInstance(type)
.filter {
it.name.toString().contains(path)
}

assertWithMessage("Expected metric $type $metricName in $metrics")
assertWithMessage("Expected metric ${type.simpleName}($path) in $metrics")
.that(filtered).hasSize(1)

return filtered.first()
}

private fun filter(metrics: List<MetricRecord>, type: String, metricName: String): List<MetricRecord> {
return metrics
.filter { it.type == type }
.filter { it.name.endsWith(".$metricName") }
internal inline fun <reified T : StatsMetric> TestResult.assertNoMetric(path: String) {
val type = T::class.java
val metrics = statsdMetrics()
val filtered = metrics
.filterIsInstance(type)
.filter {
it.name.toString().contains(path)
}

assertWithMessage("Expected no metric ${type.simpleName}($path) in $metrics")
.that(filtered).isEmpty()
}

/**
* Example:
* ... time:apps.mobile.statistic.android.local.user.id.success.init_configuration.total:5821
*/
// TODO: Intercept on network layer by simulating statsd server
internal fun TestResult.statsdMetrics(): List<MetricRecord> {
internal fun TestResult.statsdMetrics(): List<StatsMetric> {
return output.lines().asSequence()
.filter { it.contains(loggerPrefix) }
.map { it.substringAfter(loggerPrefix) }
.map { it.substringAfter("event: ") }
.map { it.substringBeforeLast(':') }
.map { line ->
val type = line.substringBefore(':')
val name = line.substringAfter(':')
MetricRecord(type, name)
}
.map { parseStatsdMetric(it) }
.toList()
}

internal data class MetricRecord(val type: String, val name: String)
private fun parseStatsdMetric(raw: String): StatsMetric {
val type = raw.substringBefore(':')
val name = raw.substringBeforeLast(':').substringAfter(':')
val value = raw.substringAfterLast(':')
return when (type) {
"count" -> CountMetric(SeriesName.create(name, multipart = true), value.toLong())
"time" -> TimeMetric(SeriesName.create(name, multipart = true), value.toLong())
"gauge" -> if (value.contains('.')) {
GaugeLongMetric(SeriesName.create(name, multipart = true), value.toLong())
} else {
GaugeDoubleMetric(SeriesName.create(name, multipart = true), value.toDouble())
}
else -> throw IllegalStateException("Unknown statsd metric: $raw")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.avito.android.plugin.build_metrics.cache

import com.avito.android.plugin.build_metrics.assertHasMetric
import com.avito.android.stats.CountMetric
import com.avito.test.gradle.TestResult
import com.google.common.truth.Truth.assertThat
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.Test
import java.io.File

internal class BuildCacheMetricsTest : BuildCacheTestFixture() {

override fun setupProject(projectDir: File) {
File(projectDir, "build.gradle.kts").writeText(
"""
plugins {
id("com.avito.android.build-metrics")
}
@CacheableTask
abstract class CustomTask @Inject constructor(objects: ObjectFactory) : DefaultTask() {
@Input
var input: Long = 0
@OutputFile
val outputFile = objects.fileProperty()
@TaskAction
fun createFile() {
outputFile.get().asFile.writeText("Output of CacheableTask: " + input)
}
}
tasks.register("cacheMissTask", CustomTask::class.java) {
input = System.currentTimeMillis()
outputFile.set(file("build/cacheMissTask.txt"))
}
tasks.register("cacheHitTask", CustomTask::class.java) {
outputFile.set(file("build/cacheHitTask.txt"))
}
tasks.register("nonCacheableTask", CustomTask::class.java) {
outputs.cacheIf { false }
outputFile.set(file("build/nonCacheableTask.txt"))
}
""".trimIndent()
)
}

@Test
fun `metrics - local cache - hit`() {
val result = warmupAndBuild(
":cacheHitTask",
warmupRemote = false,
expectedOutcome = TaskOutcome.FROM_CACHE
)

result.assertHasMetric<CountMetric>(".build.cache.remote.hit.env.").also {
assertThat(it.delta).isEqualTo(0)
}

result.assertHasMetric<CountMetric>(".build.cache.remote.miss.env.").also {
assertThat(it.delta).isEqualTo(0)
}
}

@Test
fun `metrics - local cache - miss`() {
val result = warmupAndBuild(
":cacheMissTask",
warmupRemote = false,
expectedOutcome = TaskOutcome.SUCCESS
)

result.assertHasMetric<CountMetric>(".build.cache.remote.hit.env.").also {
assertThat(it.delta).isEqualTo(0)
}

result.assertHasMetric<CountMetric>(".build.cache.remote.miss.env.").also {
assertThat(it.delta).isEqualTo(1)
}
}

@Test
fun `metrics - local cache - non cacheable`() {
val result = warmupAndBuild(
":nonCacheableTask",
warmupRemote = false,
expectedOutcome = TaskOutcome.SUCCESS
)

result.assertHasMetric<CountMetric>(".build.cache.remote.hit.env.").also {
assertThat(it.delta).isEqualTo(0)
}

result.assertHasMetric<CountMetric>(".build.cache.remote.miss.env.").also {
assertThat(it.delta).isEqualTo(0)
}
}

@Test
fun `metrics - remote cache - hit`() {
val result = warmupAndBuild(
":cacheHitTask",
warmupLocal = false,
expectedOutcome = TaskOutcome.FROM_CACHE
)

result.assertHasMetric<CountMetric>(".build.cache.remote.hit.env.").also {
assertThat(it.delta).isEqualTo(1)
}

result.assertHasMetric<CountMetric>(".build.cache.remote.miss.env.").also {
assertThat(it.delta).isEqualTo(0)
}
}

@Test
fun `metrics - remote cache - miss`() {
val result = warmupAndBuild(
":cacheMissTask",
warmupLocal = false,
expectedOutcome = TaskOutcome.SUCCESS
)

result.assertHasMetric<CountMetric>(".build.cache.remote.hit.env.").also {
assertThat(it.delta).isEqualTo(0)
}

result.assertHasMetric<CountMetric>(".build.cache.remote.miss.env.").also {
assertThat(it.delta).isEqualTo(1)
}
}

@Test
fun `metrics - remote cache - non cacheable`() {
val result = warmupAndBuild(
":nonCacheableTask",
warmupLocal = false,
expectedOutcome = TaskOutcome.SUCCESS
)

result.assertHasMetric<CountMetric>(".build.cache.remote.hit.env.").also {
assertThat(it.delta).isEqualTo(0)
}

result.assertHasMetric<CountMetric>(".build.cache.remote.miss.env.").also {
assertThat(it.delta).isEqualTo(0)
}
}

private fun warmupAndBuild(
task: String,
warmupLocal: Boolean = true,
warmupRemote: Boolean = true,
expectedOutcome: TaskOutcome
): TestResult {
build(task, useLocalCache = warmupLocal, useRemoteCache = warmupRemote)
clean()

val result = build(task, useLocalCache = true, useRemoteCache = true)

result.assertThat().taskWithOutcome(task, expectedOutcome)

return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.avito.android.plugin.build_metrics.cache

import com.avito.test.gradle.TestResult
import com.avito.test.gradle.gradlew
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.io.TempDir
import java.io.File

internal abstract class BuildCacheTestFixture {

private lateinit var projectDir: File

@BeforeEach
fun setup(@TempDir tempDir: File) {
this.projectDir = tempDir

File(projectDir, "settings.gradle.kts").writeText(
buildCacheBlock()
)
setupProject(projectDir)
}

abstract fun setupProject(projectDir: File)

private fun buildCacheBlock(): String {
return """
fun booleanProperty(name: String, defaultValue: Boolean): Boolean {
return if (settings.extra.has(name)) {
settings.extra[name]?.toString()?.toBoolean() ?: defaultValue
} else {
defaultValue
}
}
buildCache {
local {
isEnabled = booleanProperty("localCacheEnabled", false)
directory = file(".gradle/build-cache")
}
remote<DirectoryBuildCache> {
isEnabled = booleanProperty("remoteCacheEnabled", false)
directory = file(".gradle/remote-build-cache")
isPush = true
}
}
""".trimIndent()
}

protected fun clean() {
File(projectDir, "build").deleteRecursively()
}

protected fun build(
vararg tasks: String,
useLocalCache: Boolean = true,
useRemoteCache: Boolean = true,
): TestResult {
return gradlew(
projectDir,
*tasks,
"-Pavito.build.metrics.enabled=true",
"-Pavito.stats.enabled=false",
"-PlocalCacheEnabled=$useLocalCache",
"-PremoteCacheEnabled=$useRemoteCache",
"--build-cache",
"--debug", // to read statsd logs from stdout
)
}
}
Loading

0 comments on commit 635ae0f

Please sign in to comment.