BenchmarkData.kt

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.benchmark.json

import androidx.benchmark.CpuInfo
import androidx.benchmark.IsolationActivity
import androidx.benchmark.MemInfo
import androidx.benchmark.Profiler
import androidx.benchmark.ResultWriter
import com.squareup.moshi.JsonClass

/**
 * Top level json object for benchmark output for a multi-test run
 *
 * Corresponds to <packagename>BenchmarkData.json file output.
 *
 * Must be public, restrict to for usage from macrobench.
 * We avoid @RestrictTo on these objects, and rely on package-info instead, as that works for
 * adapters as well, which fail to be detected by metalava: b/331978183.
 */
@JsonClass(generateAdapter = true)
data class BenchmarkData(
    val context: Context,
    val benchmarks: List<TestResult>
) {
    /**
     * Device & OS information
     */
    @JsonClass(generateAdapter = true)
    data class Context(
        val build: Build,
        val cpuCoreCount: Int,
        @Suppress("GetterSetterNames") // 1.0 JSON compat
        @get:Suppress("GetterSetterNames") // 1.0 JSON compat
        val cpuLocked: Boolean,
        val cpuMaxFreqHz: Long,
        val memTotalBytes: Long,
        @Suppress("GetterSetterNames") // 1.0 JSON compat
        @get:Suppress("GetterSetterNames") // 1.0 JSON compat
        val sustainedPerformanceModeEnabled: Boolean,
    ) {
        /**
         * Default constructor populates with current run state
         */
        constructor() : this(
            build = Build(),
            cpuCoreCount = CpuInfo.coreDirs.size,
            cpuLocked = CpuInfo.locked,
            cpuMaxFreqHz = CpuInfo.maxFreqHz,
            memTotalBytes = MemInfo.memTotalBytes,
            sustainedPerformanceModeEnabled = IsolationActivity.sustainedPerformanceModeInUse
        )

        /**
         * Device & OS information, corresponds to `android.os.Build`
         */
        @JsonClass(generateAdapter = true)
        data class Build(
            val brand: String,
            val device: String,
            val fingerprint: String,
            val model: String,
            val version: Version
        ) {
            /**
             * Default constructor which populates values from `android.os.BUILD`
             */
            constructor() : this(
                brand = android.os.Build.BRAND,
                device = android.os.Build.DEVICE,
                fingerprint = android.os.Build.FINGERPRINT,
                model = android.os.Build.MODEL,
                version = Version(android.os.Build.VERSION.SDK_INT)
            )

            @JsonClass(generateAdapter = true)
            data class Version(
                val sdk: Int
            )
        }
    }

    /**
     * Measurements corresponding to a single test's invocation.
     *
     * Note that one parameterized test in code can produce more than one test result.
     */
    @JsonClass(generateAdapter = true)
    data class TestResult(
        val name: String,
        val params: Map<String, String>,
        val className: String,
        @Suppress("MethodNameUnits")
        @get:Suppress("MethodNameUnits")
        val totalRunTimeNs: Long,
        val metrics: Map<String, SingleMetricResult>,
        val sampledMetrics: Map<String, SampledMetricResult>,
        val warmupIterations: Int?,
        val repeatIterations: Int?,
        val thermalThrottleSleepSeconds: Long?,
        val profilerOutputs: List<ProfilerOutput>?,
    ) {
        init {
            profilerOutputs?.let { profilerOutput ->
                val labels = profilerOutput.map { it.label }
                require(labels.toSet().size == profilerOutput.size) {
                    "Each profilerOutput must have a distinct label. Labels seen: " +
                        labels.joinToString()
                }
            }
        }

        constructor(
            name: String,
            className: String,
            totalRunTimeNs: Long,
            metrics: List<androidx.benchmark.MetricResult>,
            warmupIterations: Int,
            repeatIterations: Int,
            thermalThrottleSleepSeconds: Long,
            profilerOutputs: List<ProfilerOutput>?
        ) : this(
            name = name,
            params = ResultWriter.getParams(name),
            className = className,
            totalRunTimeNs = totalRunTimeNs,
            metrics = metrics.filter {
                it.iterationData == null // single metrics only
            }.associate {
                it.name to SingleMetricResult(it)
            },
            sampledMetrics = metrics.filter {
                it.iterationData != null // single metrics only
            }.associate {
                it.name to SampledMetricResult(it)
            },
            warmupIterations = warmupIterations,
            repeatIterations = repeatIterations,
            thermalThrottleSleepSeconds = thermalThrottleSleepSeconds,
            profilerOutputs = profilerOutputs,
        )

        @JsonClass(generateAdapter = true)
        data class ProfilerOutput(
            /**
             * Type of trace.
             *
             * Note that multiple data formats may use the same type here, like simpleperf vs art
             * stack sampling traces.
             *
             * This isn't meant to be a specific data format, but more conceptual category.
             */
            val type: Type,
            /**
             * User facing label for the profiler output.
             *
             * If more than one profiler output has the same type, this label gives context
             * explaining the distinction.
             */
            val label: String,
            /** Filename of trace file. */
            val filename: String
        ) {
            constructor(profilerResult: Profiler.ResultFile) : this(
                type = profilerResult.type,
                label = profilerResult.label,
                filename = profilerResult.outputRelativePath,
            )

            enum class Type {
                MethodTrace,
                PerfettoTrace,
                StackSamplingTrace
            }
        }

        sealed class MetricResult

        @JsonClass(generateAdapter = true)
        data class SingleMetricResult(
            val minimum: Double,
            val maximum: Double,
            val median: Double,
            val runs: List<Double>
        ) : MetricResult() {
            constructor(metricResult: androidx.benchmark.MetricResult) : this(
                minimum = metricResult.min,
                maximum = metricResult.max,
                median = metricResult.median,
                runs = metricResult.data
            )
        }

        @JsonClass(generateAdapter = true)
        data class SampledMetricResult(
            @Suppress("PropertyName") val P50: Double,
            @Suppress("PropertyName") val P90: Double,
            @Suppress("PropertyName") val P95: Double,
            @Suppress("PropertyName") val P99: Double,
            val runs: List<List<Double>>
        ) : MetricResult() {
            constructor(metricResult: androidx.benchmark.MetricResult) : this(
                P50 = metricResult.p50,
                P90 = metricResult.p90,
                P95 = metricResult.p95,
                P99 = metricResult.p99,
                runs = metricResult.iterationData!!
            )
        }
    }
}