diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/DatadogMeter.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/DatadogMeter.kt new file mode 100644 index 0000000000..c61cec1c36 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/DatadogMeter.kt @@ -0,0 +1,105 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark + +import com.datadog.benchmark.exporter.DatadogMetricExporter +import com.datadog.benchmark.internal.reader.CPUVitalReader +import com.datadog.benchmark.internal.reader.FpsVitalReader +import com.datadog.benchmark.internal.reader.MemoryVitalReader +import com.datadog.benchmark.internal.reader.VitalReader +import com.datadog.benchmark.noop.NoOpObservableDoubleGauge +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.metrics.Meter +import io.opentelemetry.api.metrics.ObservableDoubleGauge +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.metrics.SdkMeterProvider +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader +import java.util.concurrent.TimeUnit + +/** + * This class is responsible for managing the performance gauges related to CPU, FPS, and memory usage. + * It provides functionalities to start and stop monitoring these metrics, which will be uploaded to + * Datadog metric API. + */ +class DatadogMeter private constructor(private val meter: Meter) { + + private val cpuVitalReader: CPUVitalReader = CPUVitalReader() + private val memoryVitalReader: MemoryVitalReader = MemoryVitalReader() + private val fpsVitalReader: FpsVitalReader = FpsVitalReader() + + private val gaugesByMetricName: MutableMap = mutableMapOf() + + /** + * Starts cpu, memory and fps gauges. + */ + fun startGauges() { + startGauge(cpuVitalReader) + startGauge(memoryVitalReader) + startGauge(fpsVitalReader) + } + + /** + * Stops cpu, memory and fps gauges. + */ + fun stopAllGauges() { + stopGauge(cpuVitalReader) + stopGauge(memoryVitalReader) + stopGauge(fpsVitalReader) + } + + private fun startGauge(reader: VitalReader) { + synchronized(reader) { + // Close the gauge if it exists before. + val metricName = reader.metricName() + gaugesByMetricName[metricName]?.close() + reader.start() + meter.gaugeBuilder(metricName).apply { + reader.unit()?.let { unit -> + setUnit(unit) + } + }.buildWithCallback { observableDoubleMeasurement -> + reader.readVitalData()?.let { data -> + observableDoubleMeasurement.record(data) + } + }.also { observableDoubleGauge -> + gaugesByMetricName[metricName] = observableDoubleGauge + } + } + } + + private fun stopGauge(reader: VitalReader) { + synchronized(reader) { + reader.stop() + gaugesByMetricName[reader.metricName()]?.close() + gaugesByMetricName[reader.metricName()] = NoOpObservableDoubleGauge() + } + } + + companion object { + + /** + * Creates an instance of [DatadogMeter] with given configuration. + */ + fun create(datadogExporterConfiguration: DatadogExporterConfiguration): DatadogMeter { + val sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.builder(DatadogMetricExporter(datadogExporterConfiguration)) + .setInterval(datadogExporterConfiguration.intervalInSeconds, TimeUnit.SECONDS) + .build() + ) + .build() + + val openTelemetry: OpenTelemetry = OpenTelemetrySdk.builder() + .setMeterProvider(sdkMeterProvider) + .build() + val meter = openTelemetry.getMeter(METER_INSTRUMENTATION_SCOPE_NAME) + return DatadogMeter(meter) + } + + private const val METER_INSTRUMENTATION_SCOPE_NAME = "datadog.open-telemetry" + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/exporter/DatadogMetricExporter.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/exporter/DatadogMetricExporter.kt new file mode 100644 index 0000000000..5fc26c2031 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/exporter/DatadogMetricExporter.kt @@ -0,0 +1,51 @@ +package com.datadog.benchmark.exporter + +import android.os.Build +import com.datadog.benchmark.DatadogExporterConfiguration +import com.datadog.benchmark.internal.DatadogHttpClient +import com.datadog.benchmark.internal.model.MetricContext +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.metrics.InstrumentType +import io.opentelemetry.sdk.metrics.data.AggregationTemporality +import io.opentelemetry.sdk.metrics.data.MetricData +import io.opentelemetry.sdk.metrics.export.MetricExporter + +internal class DatadogMetricExporter(datadogExporterConfiguration: DatadogExporterConfiguration) : MetricExporter { + + private val metricContext: MetricContext = MetricContext( + deviceModel = Build.MODEL, + osVersion = Build.VERSION.RELEASE, + run = datadogExporterConfiguration.run ?: DEFAULT_RUN_NAME, + applicationId = datadogExporterConfiguration.applicationId ?: DEFAULT_APPLICATION_ID, + intervalInSeconds = datadogExporterConfiguration.intervalInSeconds + ) + private val metricHttpClient: DatadogHttpClient = DatadogHttpClient( + metricContext, + datadogExporterConfiguration + ) + + override fun getAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality { + return AggregationTemporality.DELTA + } + + override fun export(metrics: MutableCollection): CompletableResultCode { + // currently no aggregation is required + metricHttpClient.uploadMetric(metrics.toList()) + return CompletableResultCode.ofSuccess() + } + + override fun flush(): CompletableResultCode { + // currently do nothing + return CompletableResultCode.ofSuccess() + } + + override fun shutdown(): CompletableResultCode { + // currently do nothing + return CompletableResultCode.ofSuccess() + } + + companion object { + private const val DEFAULT_RUN_NAME = "unknown run" + private const val DEFAULT_APPLICATION_ID = "unassigned application id" + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/exporter/LogsMetricExporter.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/exporter/LogsMetricExporter.kt new file mode 100644 index 0000000000..ef7d1bf6d5 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/exporter/LogsMetricExporter.kt @@ -0,0 +1,59 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.exporter + +import android.os.Build +import android.util.Log +import com.datadog.benchmark.DatadogExporterConfiguration +import com.datadog.benchmark.internal.MetricRequestBodyBuilder +import com.datadog.benchmark.internal.model.MetricContext +import io.opentelemetry.sdk.common.CompletableResultCode +import io.opentelemetry.sdk.metrics.InstrumentType +import io.opentelemetry.sdk.metrics.data.AggregationTemporality +import io.opentelemetry.sdk.metrics.data.MetricData +import io.opentelemetry.sdk.metrics.export.MetricExporter + +internal class LogsMetricExporter(datadogExporterConfiguration: DatadogExporterConfiguration) : MetricExporter { + + private val metricContext: MetricContext = MetricContext( + deviceModel = Build.MODEL, + osVersion = Build.VERSION.RELEASE, + run = datadogExporterConfiguration.run ?: DEFAULT_RUN_NAME, + applicationId = datadogExporterConfiguration.applicationId ?: DEFAULT_APPLICATION_ID, + intervalInSeconds = datadogExporterConfiguration.intervalInSeconds + ) + + override fun getAggregationTemporality(instrumentType: InstrumentType): AggregationTemporality { + return AggregationTemporality.DELTA + } + + override fun export(metrics: Collection): CompletableResultCode { + print(metrics) + return CompletableResultCode.ofSuccess() + } + + private fun print(metrics: Collection) { + MetricRequestBodyBuilder(metricContext).buildJsonElement(metrics.toList()).toString().apply { + Log.i("LogsMetricExporter", this) + } + } + + override fun flush(): CompletableResultCode { + // currently do nothing + return CompletableResultCode.ofSuccess() + } + + override fun shutdown(): CompletableResultCode { + // currently do nothing + return CompletableResultCode.ofSuccess() + } + + companion object { + private const val DEFAULT_RUN_NAME = "log run" + private const val DEFAULT_APPLICATION_ID = "unassigned application id" + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/ext/FileExt.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/ext/FileExt.kt new file mode 100644 index 0000000000..639946e98e --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/ext/FileExt.kt @@ -0,0 +1,91 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.ext + +import java.io.File +import java.nio.charset.Charset + +/* + * The java.lang.File class throws a SecurityException for the following calls: + * - canRead() + * - canWrite() + * - delete() + * - exists() + * - isFile() + * - isDir() + * - listFiles(…) + * - length() + * The following set of extension make sure that every call to those methods + * is safeguarded to avoid crashing the customer's app. + */ + +@Suppress("TooGenericExceptionCaught", "SwallowedException") +private fun File.safeCall( + default: T, + lambda: File.() -> T +): T { + return try { + lambda() + } catch (e: SecurityException) { + default + } catch (e: Exception) { + default + } +} + +/** + * Non-throwing version of [File.canRead]. If exception happens, false is returned. + */ + +fun File.canReadSafe(): Boolean { + return safeCall(default = false) { + @Suppress("UnsafeThirdPartyFunctionCall") + canRead() + } +} + +/** + * Non-throwing version of [File.exists]. If exception happens, false is returned. + */ + +fun File.existsSafe(): Boolean { + return safeCall(default = false) { + @Suppress("UnsafeThirdPartyFunctionCall") + exists() + } +} + +/** + * Non-throwing version of [File.readText]. If exception happens, null is returned. + */ + +fun File.readTextSafe(charset: Charset = Charsets.UTF_8): String? { + return if (existsSafe() && canReadSafe()) { + safeCall(default = null) { + @Suppress("UnsafeThirdPartyFunctionCall") + readText(charset) + } + } else { + null + } +} + +/** + * Non-throwing version of [File.readLines]. If exception happens, null is returned. + */ +fun File.readLinesSafe( + charset: Charset = Charsets.UTF_8 +): List? { + return if (existsSafe() && canReadSafe()) { + safeCall(default = null) { + @Suppress("UnsafeThirdPartyFunctionCall") + readLines(charset) + } + } else { + null + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/internal/DatadogMetricProfiler.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/DatadogMetricProfiler.kt new file mode 100644 index 0000000000..fd8818b16c --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/DatadogMetricProfiler.kt @@ -0,0 +1,106 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.internal + +import android.view.Choreographer +import com.datadog.benchmark.ext.canReadSafe +import com.datadog.benchmark.ext.existsSafe +import com.datadog.benchmark.ext.readLinesSafe +import com.datadog.benchmark.ext.readTextSafe +import java.io.File +import java.util.concurrent.TimeUnit + +internal class DatadogMetricProfiler { + + private var currentFps: Int = 0 + private var frameCount = 0 + private var lastFrameTime: Long = 0 + private val intervalMs = FPS_SAMPLE_INTERVAL_IN_MS + + private val statusFile = STATUS_FILE + private val statFile = STAT_FILE + + private var lastCpuTicks = 0L + + private val frameCallback = object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + if (lastFrameTime == 0L) { + lastFrameTime = System.nanoTime() + } + + frameCount++ + val currentFrameTime = System.nanoTime() + val elapsedTime: Long = currentFrameTime - lastFrameTime + + if (elapsedTime >= TimeUnit.MILLISECONDS.toNanos(intervalMs)) { + val fps: Double = frameCount / (elapsedTime / NANO_IN_SECOND) + currentFps = fps.toInt() + lastFrameTime = currentFrameTime + frameCount = 0 + } + Choreographer.getInstance().postFrameCallback(this) + } + } + + fun readMemoryVitalData(): Double? { + if (!(statusFile.existsSafe() && statusFile.canReadSafe())) { + return null + } + + return statusFile.readLinesSafe()?.firstNotNullOfOrNull { line -> + VM_RSS_REGEX.matchEntire(line)?.groupValues?.getOrNull(1) + }?.toDoubleOrNull()?.div(KB_IN_MB) + } + + @Suppress("ReturnCount") + fun readCpuVitalData(): Double? { + if (!(statFile.existsSafe() && statFile.canReadSafe())) { + return null + } + + val stat = statFile.readTextSafe() ?: return null + val tokens = stat.split(' ') + val utime = if (tokens.size > UTIME_IDX) { + tokens[UTIME_IDX].toLong() + } else { + null + } + val cpuTicksDiff = utime?.let { it - lastCpuTicks } + lastCpuTicks = utime ?: 0L + return cpuTicksDiff?.toDouble() + } + + fun getCurrentFps(): Int { + return currentFps + } + + fun startFpsReader() { + Choreographer.getInstance().postFrameCallback(frameCallback) + } + + fun stopFpsReader() { + Choreographer.getInstance().removeFrameCallback(frameCallback) + } + + companion object { + + private const val KB_IN_MB = 1000 + + private const val NANO_IN_SECOND = 1e9 + + private const val FPS_SAMPLE_INTERVAL_IN_MS = 30L + + private const val STATUS_PATH = "/proc/self/status" + internal val STATUS_FILE = File(STATUS_PATH) + + private const val STAT_PATH = "/proc/self/stat" + internal val STAT_FILE = File(STAT_PATH) + private const val VM_RSS_PATTERN = "VmRSS:\\s+(\\d+) kB" + private val VM_RSS_REGEX = Regex(VM_RSS_PATTERN) + private const val UTIME_IDX = 13 + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/CPUVitalReader.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/CPUVitalReader.kt new file mode 100644 index 0000000000..936bb68c30 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/CPUVitalReader.kt @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.internal.reader + +import com.datadog.benchmark.ext.canReadSafe +import com.datadog.benchmark.ext.existsSafe +import com.datadog.benchmark.ext.readTextSafe +import java.io.File + +/** + * Reads the CPU `utime` based on the `/proc/self/stat` file. + * cf. documentation https://man7.org/linux/man-pages/man5/procfs.5.html + */ +internal class CPUVitalReader( + internal val statFile: File = STAT_FILE +) : VitalReader { + + private var lastCpuTicks = 0L + + @Suppress("ReturnCount") + override fun readVitalData(): Double? { + if (!(statFile.existsSafe() && statFile.canReadSafe())) { + return null + } + + val stat = statFile.readTextSafe() ?: return null + val tokens = stat.split(' ') + val utime = if (tokens.size > UTIME_IDX) { + tokens[UTIME_IDX].toLong() + } else { + null + } + val cpuTicksDiff = utime?.let { it - lastCpuTicks } + lastCpuTicks = utime ?: 0L + return cpuTicksDiff?.toDouble() + } + + override fun unit(): String? = null + + override fun metricName(): String = METRIC_NAME_CPU + + companion object { + + private const val STAT_PATH = "/proc/self/stat" + internal val STAT_FILE = File(STAT_PATH) + + private const val METRIC_NAME_CPU = "android.benchmark.cpu" + private const val UTIME_IDX = 13 + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/FpsVitalReader.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/FpsVitalReader.kt new file mode 100644 index 0000000000..1c5d59d388 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/FpsVitalReader.kt @@ -0,0 +1,62 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.internal.reader + +import android.view.Choreographer +import java.util.concurrent.TimeUnit + +internal class FpsVitalReader : VitalReader { + + private var currentFps: Double = 0.0 + private var frameCount = 0 + private var lastFrameTime: Long = 0 + private val intervalMs = FPS_SAMPLE_INTERVAL_IN_MS + + private val frameCallback = object : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + if (lastFrameTime == 0L) { + lastFrameTime = System.nanoTime() + } + + frameCount++ + val currentFrameTime = System.nanoTime() + val elapsedTime: Long = currentFrameTime - lastFrameTime + + if (elapsedTime >= TimeUnit.MILLISECONDS.toNanos(intervalMs)) { + val fps: Double = frameCount / (elapsedTime / NANO_IN_SECOND) + currentFps = fps + lastFrameTime = currentFrameTime + frameCount = 0 + } + Choreographer.getInstance().postFrameCallback(this) + } + } + + override fun readVitalData(): Double { + return currentFps + } + + override fun start() { + Choreographer.getInstance().postFrameCallback(frameCallback) + } + + override fun unit(): String = UNIT_FRAME + + override fun metricName(): String = METRIC_NAME_FPS + + override fun stop() { + Choreographer.getInstance().removeFrameCallback(frameCallback) + } + + companion object { + private const val FPS_SAMPLE_INTERVAL_IN_MS = 30L + private const val NANO_IN_SECOND = 1e9 + private const val UNIT_FRAME = "frame" + + private const val METRIC_NAME_FPS = "android.benchmark.fps" + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/MemoryVitalReader.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/MemoryVitalReader.kt new file mode 100644 index 0000000000..26042740bd --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/MemoryVitalReader.kt @@ -0,0 +1,57 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.internal.reader + +import com.datadog.benchmark.ext.canReadSafe +import com.datadog.benchmark.ext.existsSafe +import com.datadog.benchmark.ext.readLinesSafe +import java.io.File + +/** + * Reads the device's `VmRSS` based on the `/proc/self/status` file. + * cf. documentation https://man7.org/linux/man-pages/man5/procfs.5.html + */ +internal class MemoryVitalReader( + internal val statusFile: File = STATUS_FILE +) : VitalReader { + + override fun readVitalData(): Double? { + if (!(statusFile.existsSafe() && statusFile.canReadSafe())) { + return null + } + + val memorySizeKb = statusFile.readLinesSafe() + ?.mapNotNull { line -> + VM_RSS_REGEX.matchEntire(line)?.groupValues?.getOrNull(1) + } + ?.firstOrNull() + ?.toDoubleOrNull() + + return if (memorySizeKb == null) { + null + } else { + memorySizeKb * BYTES_IN_KB + } + } + + override fun unit() = UNIT_BYTE + + override fun metricName(): String = METRIC_NAME_MEMORY + + companion object { + + private const val BYTES_IN_KB = 1000 + + private const val STATUS_PATH = "/proc/self/status" + internal val STATUS_FILE = File(STATUS_PATH) + private const val VM_RSS_PATTERN = "VmRSS:\\s+(\\d+) kB" + private val VM_RSS_REGEX = Regex(VM_RSS_PATTERN) + + private const val UNIT_BYTE = "byte" + private const val METRIC_NAME_MEMORY = "android.benchmark.memory" + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/VitalReader.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/VitalReader.kt new file mode 100644 index 0000000000..250ebef5e7 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/internal/reader/VitalReader.kt @@ -0,0 +1,23 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.internal.reader + +internal interface VitalReader { + fun readVitalData(): Double? + + fun start() { + // do nothing by default + } + + fun unit(): String? + + fun metricName(): String + + fun stop() { + // do nothing by default + } +} diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/noop/NoOpObservableDoubleGauge.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/noop/NoOpObservableDoubleGauge.kt new file mode 100644 index 0000000000..838707c2a8 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/noop/NoOpObservableDoubleGauge.kt @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.noop + +import io.opentelemetry.api.metrics.ObservableDoubleGauge + +internal class NoOpObservableDoubleGauge : ObservableDoubleGauge diff --git a/tools/benchmark/src/main/java/com/datadog/benchmark/noop/NoOpObservableLongGauge.kt b/tools/benchmark/src/main/java/com/datadog/benchmark/noop/NoOpObservableLongGauge.kt new file mode 100644 index 0000000000..fe0d47e492 --- /dev/null +++ b/tools/benchmark/src/main/java/com/datadog/benchmark/noop/NoOpObservableLongGauge.kt @@ -0,0 +1,11 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.benchmark.noop + +import io.opentelemetry.api.metrics.ObservableLongGauge + +internal class NoOpObservableLongGauge : ObservableLongGauge diff --git a/tools/benchmark/src/test/java/internal/CPUVitalReaderTest.kt b/tools/benchmark/src/test/java/internal/CPUVitalReaderTest.kt new file mode 100644 index 0000000000..bbfa3cae13 --- /dev/null +++ b/tools/benchmark/src/test/java/internal/CPUVitalReaderTest.kt @@ -0,0 +1,196 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package internal + +import com.datadog.benchmark.internal.reader.CPUVitalReader +import com.datadog.benchmark.internal.reader.VitalReader +import forge.ForgeConfigurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class CPUVitalReaderTest { + + lateinit var testedReader: VitalReader + + @TempDir + lateinit var tempDir: File + + lateinit var fakeFile: File + + @IntForgery(1) + var fakePid: Int = 0 + + @StringForgery(regex = "\\(\\w+\\)") + lateinit var fakeCommand: String + + @StringForgery(regex = "[RSDZTtWXxKWP]") + lateinit var fakeState: String + + @IntForgery(1) + var fakePpid: Int = 0 + + @IntForgery(1) + var fakePgrp: Int = 0 + + @IntForgery(1) + var fakeSession: Int = 0 + + @IntForgery(1) + var fakeTtyNr: Int = 0 + + @IntForgery(1) + var fakeTpgid: Int = 0 + + @IntForgery(1) + var fakeFlags: Int = 0 + + @IntForgery(1) + var fakeMinFlt: Int = 0 + + @IntForgery(1) + var fakeCMinFlt: Int = 0 + + @IntForgery(1) + var fakeMajFlt: Int = 0 + + @IntForgery(1) + var fakeCMajFlt: Int = 0 + + @IntForgery(1) + var fakeUtime: Int = 0 + + @IntForgery(1) + var fakeStime: Int = 0 + + @IntForgery(1) + var fakeCUtime: Int = 0 + + @IntForgery(1) + var fakeCStime: Int = 0 + + @IntForgery(-100, -2) + var fakePriority: Int = 0 + + lateinit var fakeStatContent: String + + @BeforeEach + fun `set up`() { + fakeFile = File(tempDir, "stat") + fakeStatContent = generateFakeContent() + testedReader = CPUVitalReader(fakeFile) + } + + @Test + fun `M read unix stats file W init()`() { + // When + val testedReader = CPUVitalReader() + + // Then + assertThat(testedReader.statFile).isEqualTo(CPUVitalReader.STAT_FILE) + } + + @Test + fun `M read correct data W readVitalData()`() { + // Given + fakeFile.writeText(fakeStatContent) + + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isEqualTo(fakeUtime.toDouble()) + } + + @Test + fun `M read correct data W readVitalData() {multiple times}`( + @IntForgery(1) utimes: List + ) { + // Given + val results = mutableListOf() + + // When + utimes.forEach { utime -> + fakeUtime = utime + fakeFile.writeText(generateFakeContent()) + val result = testedReader.readVitalData() + results.add(result!!) + } + + // Then + assertThat(results).isEqualTo( + listOf(utimes.first().toDouble()) + utimes.zipWithNext { a, b -> (b - a).toDouble() } + ) + } + + @Test + fun `M return null W readVitalData() {file doesn't exist}`() { + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W readVitalData() {file isn't readable}`() { + // Given + val restrictedFile = mock() + whenever(restrictedFile.exists()) doReturn true + whenever(restrictedFile.canRead()) doReturn false + + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W readVitalData() {file has invalid data}`( + @StringForgery content: String + ) { + // Given + fakeFile.writeText(content) + + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isNull() + } + + private fun generateFakeContent(): String { + return listOf( + fakePid, fakeCommand, fakeState, fakePpid, fakePgrp, + fakeSession, fakeTtyNr, fakeTpgid, fakeFlags, + fakeMinFlt, fakeCMinFlt, fakeMajFlt, fakeCMajFlt, + fakeUtime, fakeStime, fakeCUtime, fakeCStime, + fakePriority + ).joinToString(" ") + } +} diff --git a/tools/benchmark/src/test/java/internal/MemoryVitalReaderTest.kt b/tools/benchmark/src/test/java/internal/MemoryVitalReaderTest.kt new file mode 100644 index 0000000000..bbed7118c0 --- /dev/null +++ b/tools/benchmark/src/test/java/internal/MemoryVitalReaderTest.kt @@ -0,0 +1,155 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package internal + +import com.datadog.benchmark.internal.reader.MemoryVitalReader +import com.datadog.benchmark.internal.reader.VitalReader +import forge.ForgeConfigurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(ForgeConfigurator::class) +internal class MemoryVitalReaderTest { + + lateinit var testedReader: VitalReader + + @TempDir + lateinit var tempDir: File + + lateinit var fakeFile: File + + @StringForgery(regex = "(\\.[a-z]+)+") + lateinit var fakeName: String + + @StringForgery(regex = "[RSDZTtWXxKWP]") + lateinit var fakeState: String + + @IntForgery(1) + var fakePid: Int = 0 + + @IntForgery(1, 0x7F) + var fakeVmRss: Int = 0 + + @IntForgery(1, 256) + var fakeThreads: Int = 0 + + lateinit var fakeStatusContent: String + + @BeforeEach + fun `set up`() { + fakeFile = File(tempDir, "stat") + fakeStatusContent = generateStatusContent(fakeVmRss) + testedReader = MemoryVitalReader(fakeFile) + } + + @Test + fun `M read unix stats file W init()`() { + // When + val testedReader = MemoryVitalReader() + + // Then + assertThat(testedReader.statusFile).isEqualTo(MemoryVitalReader.STATUS_FILE) + } + + @Test + fun `M read correct data W readVitalData()`() { + // Given + fakeFile.writeText(fakeStatusContent) + + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isEqualTo(fakeVmRss.toDouble() * 1000) + } + + @Test + fun `M read correct data W readVitalData() {multiple times}`( + @IntForgery(1) vmRssValuesKb: List + ) { + // Given + val results = mutableListOf() + // When + vmRssValuesKb.forEach { vmRss -> + fakeFile.writeText(generateStatusContent(vmRss)) + val result = testedReader.readVitalData() + results.add(result!!) + } + + // Then + assertThat(results).isEqualTo(vmRssValuesKb.map { vmRssKb -> vmRssKb.toDouble() * 1000 }) + } + + @Test + fun `M return null W readVitalData() {file doesn't exist}`() { + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W readVitalData() {file isn't readable}`() { + // Given + val restrictedFile = mock() + whenever(restrictedFile.exists()) doReturn true + whenever(restrictedFile.canRead()) doReturn false + + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W readVitalData() {file has invalid data}`( + @StringForgery content: String + ) { + // Given + fakeFile.writeText(content) + + // When + val result = testedReader.readVitalData() + + // Then + assertThat(result).isNull() + } + + private fun generateStatusContent(vmRss: Int): String { + return mapOf( + "Name" to fakeName, + "State" to fakeState, + "Pid" to fakePid, + "VmRSS" to "$vmRss kB".padStart(11, ' '), + "Threads" to fakeThreads + ) + .map { (key, value) -> "$key:\t$value" } + .joinToString("\n") + } +}