1 /* 2 * Copyright (C) 2024 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 com.android.bedstead.performanceanalyzer 18 19 import android.util.Log 20 import com.android.bedstead.performanceanalyzer.exceptions.PerformanceTestFailedException 21 import kotlin.math.round 22 import kotlin.time.measureTime 23 24 /** 25 * A helper class to analyze performance of a runnable. 26 * 27 * Example usage: 28 * ``` 29 * assertThat( 30 * analyzeThat(runnable) 31 * .cleanUpUsing(runnable) 32 * .runsNumberOfTimes(100) 33 * .finishesIn(2000)) 34 * .isTrue() 35 * ``` 36 */ 37 class PerformanceAnalyzer 38 private constructor( 39 private var subjectRunnable: Runnable 40 ) { 41 private var cleanUpRunnable: Runnable? = null 42 private var iterations: Int = 1 43 44 companion object { 45 /** 46 * Entry point to the [PerformanceAnalyzer]. Takes the [subjectRunnable] under review as an 47 * argument. 48 */ 49 @JvmStatic analyzeThatnull50 fun analyzeThat(subjectRunnable: Runnable): PerformanceAnalyzer { 51 return PerformanceAnalyzer(subjectRunnable) 52 } 53 54 private const val LOG_TAG = "PerformanceAnalyzer" 55 } 56 57 /** 58 * Specify the number of times the [subjectRunnable] is supposed to run before we analyze its 59 * performance. 60 * 61 * Default value: 1. 62 */ runsNumberOfTimesnull63 fun runsNumberOfTimes(times: Int): PerformanceAnalyzer { 64 iterations = times 65 return this 66 } 67 68 /** 69 * Checks that [subjectRunnable] under review finishes in [expectedTimeInMs], 70 * throws [PerformanceTestFailedException] with the summary report otherwise. 71 * 72 * In case the performance test is passed, the summary report can be found in the logcat 73 * file. 74 * 75 * The [subjectRunnable] is executed the specified number of times using [runsNumberOfTimes()], 76 * default being 1. 77 * 78 * The [cleanUpRunnable] specified using [cleanUpUsing()] is also invoked after every execution 79 * but its execution time is not analyzed. 80 */ finishesInnull81 fun finishesIn(expectedTimeInMs: Long): Boolean { 82 val stats = analyze(expectedTimeInMs) 83 val summary = PerformanceSummary.of(stats) 84 85 if (stats.executionTimesUnderExpectedTimePc < RUNTIME_SLO) { 86 throw PerformanceTestFailedException(summary.toString()) 87 } 88 89 // Log summary into logcat. 90 Log.i(LOG_TAG, summary.toString()) 91 92 return true 93 } 94 95 /** 96 * Use this to specify a runnable action that is required to clean up the resources initialized 97 * by [subjectRunnable]. 98 * The runnable would be executed once after every execution of [subjectRunnable]. 99 */ cleanUpUsingnull100 fun cleanUpUsing(cleanUpRunnable: Runnable): PerformanceAnalyzer { 101 this.cleanUpRunnable = cleanUpRunnable 102 return this 103 } 104 analyzenull105 private fun analyze(expectedTimeInMs: Long): PerformanceStats { 106 val executionTimes = mutableListOf<Long>() 107 var failuresCount = 0 108 109 for (i in 1..iterations) { 110 try { 111 val executionTime = measureTime { 112 subjectRunnable.run() 113 } 114 executionTimes.add(executionTime.inWholeMilliseconds) 115 } catch (e: Throwable) { 116 failuresCount++ 117 } finally { 118 runCleanup() 119 } 120 } 121 122 return stats(expectedTimeInMs, executionTimes, failuresCount) 123 } 124 statsnull125 private fun stats(expectedTimeInMs: Long, 126 executionTimes: List<Long>, 127 failuresCount: Int): PerformanceStats { 128 val executionTimesUnderExpectedTime = executionTimes.count { it <= expectedTimeInMs } 129 val executionTimesUnderExpectedTimePc = 130 executionTimesUnderExpectedTime.toDouble() / iterations.toDouble() * 100 131 132 val executionTimesSorted = executionTimes.sorted() 133 val successfulExecutions = executionTimes.count() 134 var percentile90 = 0L 135 var percentile99 = 0L 136 if (successfulExecutions > 0) { 137 percentile90 = 138 executionTimesSorted[(successfulExecutions * 0.9).toInt()].roundToNearestHundred() 139 percentile99 = 140 executionTimesSorted[(successfulExecutions * 0.99).toInt()].roundToNearestHundred() 141 } 142 143 return PerformanceStats(expectedTimeInMs, iterations, executionTimesUnderExpectedTimePc, 144 failuresCount, percentile90, percentile99) 145 } 146 runCleanupnull147 private fun runCleanup() { 148 try { 149 cleanUpRunnable?.run() 150 } catch (e: Throwable) { 151 throw RuntimeException("The cleanup function failed", e) 152 } 153 } 154 Longnull155 private fun Long.roundToNearestHundred(): Long { 156 return (round(this / 100.0) * 100).toLong() 157 } 158 } 159