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