<lambda>null1 package androidx.benchmark
2 
3 import android.annotation.SuppressLint
4 import android.util.Log
5 import androidx.benchmark.CpuEventCounter.Event
6 import java.util.concurrent.TimeUnit
7 import kotlinx.coroutines.delay
8 
9 internal class MicrobenchmarkPhase(
10     val label: String,
11     val measurementCount: Int,
12     val loopMode: LoopMode,
13     val metrics: Array<MetricCapture> = arrayOf(TimeCapture()),
14     val profiler: Profiler? = null,
15     val gcBeforePhase: Boolean = false,
16     val thermalThrottleSleepsMax: Int = 0,
17 ) {
18     val metricsContainer = MetricsContainer(metrics, measurementCount)
19     private var thermalThrottleSleepsRemaining = thermalThrottleSleepsMax
20     var thermalThrottleSleepSeconds = 0L
21 
22     init {
23         if (loopMode.warmupManager != null) {
24             check(metricsContainer.names.first() == "timeNs" && metricsContainer.names.size <= 2) {
25                 "If warmup is enabled, expect to only capture one or two metrics"
26             }
27         }
28     }
29 
30     /** @return If true, finishing the phase was successful, otherwise must be retried */
31     fun tryEnd(): Boolean {
32         return if (thermalThrottleSleepsRemaining > 0 && sleepIfThermalThrottled()) {
33             thermalThrottleSleepsRemaining--
34             false // don't start next phase, do-over
35         } else {
36             if (thermalThrottleSleepsMax > 0 && thermalThrottleSleepsRemaining == 0) {
37                 // If we ran out of throttle detection retries, it's possible the throttle baseline
38                 // is incorrect. Force next benchmark to recompute it.
39                 ThrottleDetector.resetThrottleBaseline()
40             }
41             true // start next phase
42         }
43     }
44 
45     @SuppressLint("BanThreadSleep") // we all need sleep to cool off sometimes
46     private fun sleepIfThermalThrottled(): Boolean =
47         when {
48             ThrottleDetector.isDeviceThermalThrottled() -> {
49                 Log.d(
50                     BenchmarkState.TAG,
51                     "THERMAL THROTTLE DETECTED, SLEEPING FOR $THROTTLE_BACKOFF_S SECONDS"
52                 )
53                 val startTimeNs = System.nanoTime()
54                 inMemoryTrace("Sleep due to Thermal Throttle") {
55                     Thread.sleep(TimeUnit.SECONDS.toMillis(THROTTLE_BACKOFF_S))
56                 }
57                 val sleepTimeNs = System.nanoTime() - startTimeNs
58                 thermalThrottleSleepSeconds += TimeUnit.NANOSECONDS.toSeconds(sleepTimeNs)
59                 true
60             }
61             else -> false
62         }
63 
64     internal suspend inline fun execute(
65         traceUniqueName: String,
66         scope: MicrobenchmarkScope,
67         state: MicrobenchmarkRunningState,
68         loopedMeasurementBlock: LoopedMeasurementBlock
69     ) {
70         var thermalThrottleSleepsRemaining = thermalThrottleSleepsMax
71         val loopsPerMeasurement = loopMode.getIterations(state.warmupEstimatedIterationTimeNs)
72         state.maxIterationsPerRepeat =
73             state.maxIterationsPerRepeat.coerceAtLeast(loopsPerMeasurement)
74 
75         var phaseProfilerResult: Profiler.ResultFile?
76         try {
77             InMemoryTracing.beginSection(label)
78             if (gcBeforePhase) {
79                 // Run GC to avoid memory pressure from previous run from affecting this one.
80                 // Note, we don't use System.gc() because it doesn't always have consistent behavior
81                 Runtime.getRuntime().gc()
82             }
83             var profilerStartBegin = 0L
84             var profilerStartEnd = 0L
85             while (true) { // keep running until phase successful
86                 try {
87                     phaseProfilerResult =
88                         profiler?.run {
89                             profilerStartBegin = System.nanoTime()
90                             startIfNotRiskingAnrDeadline(
91                                     traceUniqueName = traceUniqueName,
92                                     estimatedDurationNs = state.warmupEstimatedIterationTimeNs
93                                 )
94                                 .also { profilerStartEnd = System.nanoTime() }
95                         }
96                     state.metrics = metricsContainer // needed for pausing
97                     metricsContainer.captureInit()
98 
99                     // warmup the container
100                     metricsContainer.captureStart()
101                     metricsContainer.captureStop()
102                     metricsContainer.captureInit()
103 
104                     repeat(measurementCount) {
105                         // perform measurement
106                         metricsContainer.captureStart()
107                         loopedMeasurementBlock.invoke(scope, loopsPerMeasurement)
108                         metricsContainer.captureStop()
109                         state.yieldThreadIfDeadlinePassed()
110                     }
111                     if (loopMode.warmupManager != null) {
112                         // warmup, so retry until complete
113                         metricsContainer.captureInit()
114                         // Note that warmup is based on repeat time, *not* the timeNs metric, since
115                         // we
116                         // want to account for paused time during warmup (paused work should
117                         // stabilize
118                         // too)
119                         val lastMeasuredWarmupValue = metricsContainer.peekSingleRepeatTime()
120                         if (loopMode.warmupManager.onNextIteration(lastMeasuredWarmupValue)) {
121                             state.warmupEstimatedIterationTimeNs = lastMeasuredWarmupValue
122                             state.warmupIterations = loopMode.warmupManager.iteration
123                             break
124                         } else {
125                             continue
126                         }
127                     }
128                 } finally {
129                     profiler?.run {
130                         val profilerStopBegin = System.nanoTime()
131                         stop()
132                         val profilerStopEnd = System.nanoTime()
133                         // instead of actually using inMemoryTrace(){} directly to trace profiling,
134                         // we record timestamps and defer to avoid profiling the tracing logic
135                         // itself, since it's very intrusive to method traces
136                         InMemoryTracing.beginSection("start profiling", profilerStartBegin)
137                         InMemoryTracing.endSection(profilerStartEnd)
138                         InMemoryTracing.beginSection("stop profiling", profilerStopBegin)
139                         InMemoryTracing.endSection(profilerStopEnd)
140                     }
141                     state.yieldThreadIfDeadlinePassed()
142                 }
143                 if (!ThrottleDetector.isDeviceThermalThrottled()) {
144                     // not thermal throttled, phase complete
145                     break
146                 } else {
147                     // thermal throttled! delay and retry!
148                     Log.d(
149                         BenchmarkState.TAG,
150                         "THERMAL THROTTLE DETECTED, DELAYING FOR " +
151                             "${Arguments.thermalThrottleSleepDurationSeconds} SECONDS"
152                     )
153                     val startTimeNs = System.nanoTime()
154                     inMemoryTrace("Sleep due to Thermal Throttle") {
155                         delay(
156                             TimeUnit.SECONDS.toMillis(Arguments.thermalThrottleSleepDurationSeconds)
157                         )
158                     }
159                     val sleepTimeNs = System.nanoTime() - startTimeNs
160                     state.totalThermalThrottleSleepSeconds +=
161                         TimeUnit.NANOSECONDS.toSeconds(sleepTimeNs)
162                     thermalThrottleSleepsRemaining--
163                     if (thermalThrottleSleepsRemaining <= 0) break
164                 }
165             }
166         } finally {
167             InMemoryTracing.endSection()
168         }
169         if (loopMode.warmupManager == null) {
170             // Save captured metrics except during warmup, where we intentionally discard
171             state.metricResults.addAll(
172                 metricsContainer.captureFinished(maxIterations = loopsPerMeasurement)
173             )
174         }
175         if (phaseProfilerResult != null) {
176             state.profilerResults.add(phaseProfilerResult)
177         }
178     }
179 
180     internal sealed class LoopMode(val warmupManager: WarmupManager? = null) {
181         /** Warmup looping mode - reports a single iteration, but there is specialized code in */
182         class Warmup(warmupManager: WarmupManager) : LoopMode(warmupManager) {
183             // always return one iter per measurement as we remeasure warmup after each loop
184             override fun getIterations(warmupEstimatedIterationTimeNs: Long): Int = 1
185         }
186 
187         /** Each repeat of the phase will run a predefined number of iterations */
188         class FixedIterations(private val iterations: Int) : LoopMode() {
189             override fun getIterations(warmupEstimatedIterationTimeNs: Long): Int = iterations
190         }
191 
192         class Duration(private val targetRepeatDurationNs: Long) : LoopMode() {
193             override fun getIterations(warmupEstimatedIterationTimeNs: Long): Int {
194                 check(warmupEstimatedIterationTimeNs >= 0) {
195                     "Cannot dynamically determine repeat duration, warmup has not run!"
196                 }
197                 return (targetRepeatDurationNs / warmupEstimatedIterationTimeNs.coerceAtLeast(1))
198                     .toInt()
199                     .coerceIn(MIN_TEST_ITERATIONS, MAX_TEST_ITERATIONS)
200             }
201         }
202 
203         abstract fun getIterations(warmupEstimatedIterationTimeNs: Long): Int
204 
205         companion object {
206             internal const val MAX_TEST_ITERATIONS = 1_000_000
207             internal const val MIN_TEST_ITERATIONS = 1
208         }
209     }
210 
211     companion object {
212         private val THROTTLE_BACKOFF_S = Arguments.thermalThrottleSleepDurationSeconds
213 
214         // static instance ensures there's only one, and we don't leak native memory
215         internal val cpuEventCounter: CpuEventCounter by lazy {
216             // As this is only ever enabled by experimental arguments, we force enable this
217             // permanently once the first benchmark uses it, for local runs only.
218             CpuEventCounter.forceEnable()?.let { errorMessage ->
219                 throw IllegalStateException(errorMessage)
220             }
221             CpuEventCounter()
222         }
223 
224         fun dryRunModePhase() =
225             MicrobenchmarkPhase(
226                 label = "Benchmark DryRun Timing",
227                 measurementCount = 1,
228                 loopMode = LoopMode.FixedIterations(1),
229             )
230 
231         fun startupModePhase() =
232             MicrobenchmarkPhase(
233                 label = "Benchmark Startup Timing (experimental)",
234                 measurementCount = 10,
235                 loopMode = LoopMode.FixedIterations(1),
236             )
237 
238         fun warmupPhase(
239             warmupManager: WarmupManager,
240             collectCpuEventInstructions: Boolean,
241         ) =
242             MicrobenchmarkPhase(
243                 label = "Benchmark Warmup",
244                 measurementCount = 1,
245                 loopMode = LoopMode.Warmup(warmupManager),
246                 metrics =
247                     if (collectCpuEventInstructions) {
248                         arrayOf(
249                             TimeCapture(),
250                             CpuEventCounterCapture(cpuEventCounter, listOf(Event.Instructions))
251                         )
252                     } else {
253                         arrayOf(TimeCapture())
254                     },
255                 gcBeforePhase = true
256             )
257 
258         fun timingMeasurementPhase(
259             loopMode: LoopMode,
260             measurementCount: Int,
261             simplifiedTimingOnlyMode: Boolean,
262             metrics: Array<MetricCapture>
263         ) =
264             MicrobenchmarkPhase(
265                 label = "Benchmark Time",
266                 measurementCount = measurementCount,
267                 loopMode = loopMode,
268                 metrics = metrics,
269                 thermalThrottleSleepsMax = if (simplifiedTimingOnlyMode) 0 else 2
270             )
271 
272         fun profiledTimingPhase(
273             profiler: Profiler,
274             metrics: Array<MetricCapture>,
275             loopModeOverride: LoopMode?,
276             measurementCountOverride: Int?
277         ): MicrobenchmarkPhase {
278             val measurementCount =
279                 measurementCountOverride
280                     ?: if (profiler.requiresSingleMeasurementIteration) 1 else 50
281             return MicrobenchmarkPhase(
282                 label = "Benchmark Profiled Time",
283                 measurementCount = measurementCount,
284                 loopMode =
285                     loopModeOverride
286                         ?: if (profiler.requiresSingleMeasurementIteration) {
287                             LoopMode.FixedIterations(1)
288                         } else {
289                             LoopMode.Duration(
290                                 if (profiler.requiresExtraRuntime) {
291                                     BenchmarkState.SAMPLED_PROFILER_DURATION_NS / measurementCount
292                                 } else {
293                                     BenchmarkState.DEFAULT_MEASUREMENT_DURATION_NS
294                                 }
295                             )
296                         },
297                 profiler = profiler,
298                 metrics = metrics
299             )
300         }
301 
302         fun allocationMeasurementPhase(loopMode: LoopMode) =
303             MicrobenchmarkPhase(
304                 label = "Benchmark Allocations",
305                 measurementCount = 5,
306                 loopMode = loopMode,
307                 metrics = arrayOf(AllocationCountCapture())
308             )
309     }
310 
311     /**
312      * Configuration for phase and looping behavior in a microbenchmark.
313      *
314      * Note that many arguments can override subsequent ones in the list (e.g. dryRunMode=true
315      * ignores all subsequent args).
316      */
317     class Config(
318         val dryRunMode: Boolean,
319         val startupMode: Boolean,
320         val simplifiedTimingOnlyMode: Boolean,
321         val profiler: Profiler?,
322         val profilerPerfCompareMode: Boolean,
323         val warmupCount: Int?,
324         val measurementCount: Int?,
325         val metrics: Array<MetricCapture>,
326     ) {
327         constructor(
328             microbenchmarkConfig: MicrobenchmarkConfig,
329             simplifiedTimingOnlyMode: Boolean
330         ) : this(
331             dryRunMode = Arguments.dryRunMode,
332             startupMode = Arguments.startupMode,
333             profiler = microbenchmarkConfig.profiler?.profiler ?: Arguments.profiler,
334             profilerPerfCompareMode = Arguments.profilerPerfCompareEnable,
335             warmupCount = microbenchmarkConfig.warmupCount,
336             measurementCount = Arguments.iterations ?: microbenchmarkConfig.measurementCount,
337             simplifiedTimingOnlyMode = simplifiedTimingOnlyMode,
338             metrics = microbenchmarkConfig.metrics.toTypedArray()
339         )
340 
341         val warmupManager = WarmupManager(overrideCount = warmupCount)
342 
343         init {
344             require(warmupCount == null || warmupCount > 0) {
345                 "warmupCount ($warmupCount) must null or positive"
346             }
347             require(measurementCount == null || measurementCount > 0) {
348                 "measurementCount ($measurementCount) must be null or positive"
349             }
350         }
351 
352         fun generatePhases(): List<MicrobenchmarkPhase> {
353             return if (dryRunMode) {
354                 listOf(dryRunModePhase())
355             } else
356                 if (startupMode) {
357                         listOf(startupModePhase())
358                     } else {
359                         val timingMeasurementCount = measurementCount ?: 50
360 
361                         val profiler = if (simplifiedTimingOnlyMode) null else profiler
362                         // note that it's currently important that allocation runs for the same
363                         // target
364                         // duration as timing, since we only report a single value for
365                         // "repeatIterations" in the output JSON. If we ever want to avoid loopMode
366                         // sharing between these phases, we should update that JSON representation.
367                         val loopMode =
368                             if (profilerPerfCompareMode) {
369                                 // single fixed iteration as a compromise choice that can be matched
370                                 // between
371                                 // measurement and profiler, and not produce overwhelming method
372                                 // tracing capture
373                                 // durations/file sizes
374                                 LoopMode.FixedIterations(1)
375                             } else {
376                                 LoopMode.Duration(BenchmarkState.DEFAULT_MEASUREMENT_DURATION_NS)
377                             }
378                         listOfNotNull(
379                             warmupPhase(
380                                 warmupManager = warmupManager,
381                                 // Collect the instructions metric to ensure that behavior and
382                                 // timing aren't significantly skewed between warmup and timing
383                                 // phases. For example, if only timing phase has a complex impl of
384                                 // pause/resume, then behavior changes drastically, and the
385                                 // warmupManager will estimate a far faster impl of
386                                 // `measureRepeated { runWithMeasurementDisabled }`
387                                 collectCpuEventInstructions =
388                                     metrics.any {
389                                         it is CpuEventCounterCapture && it.names.isNotEmpty()
390                                     }
391                             ),
392                             // Regular timing phase
393                             timingMeasurementPhase(
394                                 measurementCount = timingMeasurementCount,
395                                 loopMode = loopMode,
396                                 metrics = metrics,
397                                 simplifiedTimingOnlyMode = simplifiedTimingOnlyMode
398                             ),
399                             if (simplifiedTimingOnlyMode || profiler == null) {
400                                 null
401                             } else {
402                                 if (profilerPerfCompareMode) {
403                                     // benchmark the profiler, matching the timing phases for fair
404                                     // compare
405                                     profiledTimingPhase(
406                                         profiler = profiler,
407                                         metrics = arrayOf(TimeCapture("profilerTimeNs")),
408                                         loopModeOverride = loopMode,
409                                         measurementCountOverride = timingMeasurementCount
410                                     )
411                                 } else {
412                                     // standard profiling
413                                     profiledTimingPhase(
414                                         profiler,
415                                         metrics = emptyArray(),
416                                         loopModeOverride = null,
417                                         measurementCountOverride = null
418                                     )
419                                 }
420                             },
421                             if (simplifiedTimingOnlyMode) {
422                                 null // skip allocations
423                             } else {
424                                 allocationMeasurementPhase(loopMode)
425                             }
426                         )
427                     }
428                     .also {
429                         if (simplifiedTimingOnlyMode) {
430                             // can't use thermal throttle checks with simplifiedTimingOnlyMode,
431                             // since we're already checking for throttling
432                             check(it.all { phase -> phase.thermalThrottleSleepsMax == 0 }) {
433                                 "Thermal throttle check banned within simplifiedTimingOnlyMode"
434                             }
435                         }
436                     }
437         }
438     }
439 }
440