IdeSummaryString.kt

/*
 * Copyright 2021 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.macro

import androidx.benchmark.BenchmarkResult
import androidx.benchmark.MetricResult
import androidx.benchmark.Outputs

/**
 * Returns a pair of ideSummaryStrings - v1 (pre Arctic-fox) and v2 (Arctic-fox+)
 *
 * These strings are to be displayed in Studio, depending on the version.
 *
 * The V2 string embeds links to trace files relative to the output path sent to the IDE via
 * `[link](file://<relative/path/to/trace>)`
 *
 * @see androidx.benchmark.InstrumentationResultScope#ideSummaryRecord
 */
internal fun ideSummaryStrings(
    warningLines: String,
    benchmarkName: String,
    measurements: BenchmarkResult.Measurements,
    absoluteTracePaths: List<String>
): Pair<String, String> {
    require(measurements.isNotEmpty()) { "Require non-empty list of metric results." }
    val allMetrics = measurements.singleMetrics + measurements.sampledMetrics

    val maxLabelLength = allMetrics.maxOf { it.name.length }

    fun Double.toDisplayString() = "%,.1f".format(this)

    // max string length of any printed min/median/max is the largest max value seen. used to pad.
    val maxValueLength = allMetrics
        .maxOf { it.max }
        .toDisplayString().length

    fun ideSummaryString(
        singleTransform: (
            name: String,
            min: String,
            median: String,
            max: String,
            metricResult: MetricResult
        ) -> String
    ) = (
        listOf(warningLines + benchmarkName) +
            measurements.singleMetrics.map {
                singleTransform(
                    it.name.padStart(maxLabelLength),
                    it.min.toDisplayString().padStart(maxValueLength),
                    it.median.toDisplayString().padStart(maxValueLength),
                    it.max.toDisplayString().padStart(maxValueLength),
                    it
                )
            } +
            measurements.sampledMetrics.map {
                val name = it.name.padStart(maxLabelLength)
                val p50 = it.p50.toDisplayString().padStart(maxValueLength)
                val p90 = it.p90.toDisplayString().padStart(maxValueLength)
                val p95 = it.p95.toDisplayString().padStart(maxValueLength)
                val p99 = it.p99.toDisplayString().padStart(maxValueLength)
                // we don't try and link percentiles, since they're grouped across multiple iters
                "  $name   P50  $p50,   P90  $p90,   P95  $p95,   P99  $p99"
            }
        ).joinToString("\n") + "\n"

    val relativeTracePaths = absoluteTracePaths.map { absolutePath ->
        Outputs.relativePathFor(absolutePath)
            .replace("(", "\(")
            .replace(")", "\)")
    }
    return Pair(
        first = ideSummaryString(
            singleTransform = { name, min, median, max, _ ->
                "  $name   min $min,   median $median,   max $max"
            }
        ),
        second = ideSummaryString { name, min, median, max, metricResult ->
            "  $name" +
                "   [min $min](file://${relativeTracePaths[metricResult.minIndex]})," +
                "   [median $median](file://${relativeTracePaths[metricResult.medianIndex]})," +
                "   [max $max](file://${relativeTracePaths[metricResult.maxIndex]})"
        } + "    Traces: Iteration " + relativeTracePaths.mapIndexed { index, path ->
            "[$index](file://$path)"
        }.joinToString(separator = " ") + "\n"
    )
}