1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.benchmark
18 
19 import android.os.Bundle
20 import androidx.annotation.RestrictTo
21 import kotlin.math.pow
22 import kotlin.math.sqrt
23 
24 /**
25  * Results for a given metric from a benchmark, including each measurement made and general stats
26  * for those measurements (min/median/max).
27  */
28 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
29 public class MetricResult(
30     val name: String,
31     val data: List<Double>,
32     val iterationData: List<List<Double>>? = null
33 ) {
34     val median: Double
35     val medianIndex: Int
36     val min: Double
37     val minIndex: Int
38     val max: Double
39     val maxIndex: Int
40     val standardDeviation: Double
41     val coefficientOfVariation: Double
42 
43     val p50: Double
44     val p90: Double
45     val p95: Double
46     val p99: Double
47 
48     init {
49         val values = data.sorted()
50         val size = values.size
<lambda>null51         require(size >= 1) { "At least one result is necessary, $size found for $name." }
52 
53         val mean: Double = data.average()
54         min = values.first()
55         max = values.last()
56         median = getPercentile(values, 50)
57 
58         p50 = getPercentile(values, 50)
59         p90 = getPercentile(values, 90)
60         p95 = getPercentile(values, 95)
61         p99 = getPercentile(values, 99)
62 
<lambda>null63         minIndex = data.indexOfFirst { it == min }
<lambda>null64         maxIndex = data.indexOfFirst { it == max }
65         medianIndex = data.size / 2
66 
67         standardDeviation =
68             if (data.size == 1) {
69                 0.0
70             } else {
<lambda>null71                 val sum = values.map { (it - mean).pow(2) }.sum()
72                 sqrt(sum / (size - 1).toDouble())
73             }
74         coefficientOfVariation =
75             if (mean == 0.0) {
76                 0.0
77             } else {
78                 standardDeviation / mean
79             }
80     }
81 
getSummarynull82     internal fun getSummary(): String {
83         return "Metric ($name) results: median $median, min $min, max $max, " +
84             "standardDeviation: $standardDeviation"
85     }
86 
putInBundlenull87     public fun putInBundle(status: Bundle, prefix: String) {
88         // format string to be in instrumentation results format
89         val bundleName = name.toOutputMetricName()
90 
91         status.putDouble("${prefix}${bundleName}_min", min)
92         status.putDouble("${prefix}${bundleName}_median", median)
93         status.putDouble("${prefix}${bundleName}_stddev", standardDeviation)
94     }
95 
putPercentilesInBundlenull96     public fun putPercentilesInBundle(status: Bundle, prefix: String) {
97         // format string to be in instrumentation results format
98         val bundleName = name.toOutputMetricName()
99 
100         status.putDouble("${prefix}${bundleName}_p50", p50)
101         status.putDouble("${prefix}${bundleName}_p90", p90)
102         status.putDouble("${prefix}${bundleName}_p95", p95)
103         status.putDouble("${prefix}${bundleName}_p99", p99)
104     }
105 
106     // NOTE: Studio-generated, re-generate if members change
equalsnull107     override fun equals(other: Any?): Boolean {
108         if (this === other) return true
109         if (javaClass != other?.javaClass) return false
110 
111         other as MetricResult
112 
113         if (name != other.name) return false
114         if (median != other.median) return false
115         if (medianIndex != other.medianIndex) return false
116         if (min != other.min) return false
117         if (minIndex != other.minIndex) return false
118         if (max != other.max) return false
119         if (maxIndex != other.maxIndex) return false
120         if (standardDeviation != other.standardDeviation) return false
121 
122         return true
123     }
124 
hashCodenull125     override fun hashCode(): Int {
126         var result = name.hashCode()
127         result = 31 * result + median.hashCode()
128         result = 31 * result + medianIndex
129         result = 31 * result + min.hashCode()
130         result = 31 * result + minIndex
131         result = 31 * result + max.hashCode()
132         result = 31 * result + maxIndex
133         result = 31 * result + standardDeviation.hashCode()
134         return result
135     }
136 
137     companion object {
lerpnull138         internal fun lerp(a: Double, b: Double, ratio: Double): Double {
139             return (a * (1 - ratio) + b * (ratio))
140         }
141 
getPercentilenull142         fun getPercentile(sortedData: List<Double>, percentile: Int): Double {
143             val idealIndex = percentile.coerceIn(0, 100) / 100.0 * (sortedData.size - 1)
144             val firstIndex = idealIndex.toInt()
145             val secondIndex = firstIndex + 1
146 
147             val firstValue = sortedData[firstIndex]
148             val secondValue = sortedData.getOrElse(secondIndex) { firstValue }
149             return lerp(firstValue, secondValue, idealIndex - firstIndex)
150         }
151     }
152 }
153