<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