1 /*
<lambda>null2 * Copyright 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 androidx.benchmark
18
19 import android.os.Build
20 import android.util.Log
21 import androidx.annotation.RestrictTo
22 import androidx.benchmark.BenchmarkState.Companion.enableMethodTracingAffectsMeasurementError
23 import androidx.benchmark.perfetto.PerfettoCapture
24 import androidx.benchmark.perfetto.PerfettoCaptureWrapper
25 import androidx.benchmark.perfetto.PerfettoConfig
26 import androidx.benchmark.perfetto.UiState
27 import androidx.benchmark.perfetto.appendUiState
28 import androidx.test.platform.app.InstrumentationRegistry
29 import androidx.tracing.Trace
30 import androidx.tracing.trace
31 import java.io.File
32 import java.util.concurrent.TimeUnit
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.Dispatchers
35 import kotlinx.coroutines.runBlocking
36 import kotlinx.coroutines.yield
37
38 /**
39 * Scope handle for pausing/resuming microbenchmark measurement.
40 *
41 * This is functionally an equivalent to `BenchmarkRule.Scope` which will work without the JUnit
42 * dependency.
43 */
44 open class MicrobenchmarkScope
45 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
46 constructor(internal val state: MicrobenchmarkRunningState) {
47 /**
48 * Disable measurement for a block of code.
49 *
50 * Used for disabling timing/measurement for work that isn't part of the benchmark:
51 * - When constructing per-loop randomized inputs for operations with caching,
52 * - Controlling which parts of multi-stage work are measured (e.g. View measure/layout)
53 * - Per-loop verification
54 *
55 * @sample androidx.benchmark.samples.runWithMeasurementDisabledSample
56 */
57 inline fun <T> runWithMeasurementDisabled(block: () -> T): T {
58 pauseMeasurement()
59 // Note: we only bother with tracing for the runWithMeasurementDisabled function for
60 // Kotlin callers, since we want to avoid corrupting the trace with incorrectly paired
61 // pauseMeasurement/resumeMeasurement calls
62 val ret: T =
63 try {
64 // have to use begin/end since block isn't crossinline
65 Trace.beginSection("runWithMeasurementDisabled")
66 block()
67 } finally {
68 Trace.endSection()
69 }
70 resumeMeasurement()
71 return ret
72 }
73
74 /**
75 * Pause measurement until the next call to [resumeMeasurement].
76 *
77 * [resumeMeasurement] must be called before exiting the measurement loop.
78 *
79 * Re-entrant pausing is not supported.
80 *
81 * Kotlin callers should generally instead use [runWithMeasurementDisabled].
82 */
83 fun pauseMeasurement() {
84 state.pauseMeasurement()
85 }
86
87 /**
88 * Resume measurement after a call to [pauseMeasurement]
89 *
90 * Kotlin callers should generally instead use [runWithMeasurementDisabled].
91 */
92 fun resumeMeasurement() {
93 state.resumeMeasurement()
94 }
95 }
96
97 /**
98 * State carried across multiple phases, including metric and output files
99 *
100 * This is maintained as a state object rather than return objects from each phase to avoid
101 * allocation
102 */
103 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
104 class MicrobenchmarkRunningState
105 internal constructor(metrics: MetricsContainer, val yieldThreadPeriodically: Boolean) {
106 internal var warmupEstimatedIterationTimeNs: Long = 0
107 internal var warmupIterations: Int = 0
108 internal var totalThermalThrottleSleepSeconds: Long = 0
109 internal var maxIterationsPerRepeat = 0
110 internal var metrics: MetricsContainer = metrics
111 internal var metricResults = mutableListOf<MetricResult>()
112 internal var profilerResults = mutableListOf<Profiler.ResultFile>()
113 internal var paused = false
114
115 internal var initialTimeNs: Long = 0
116 internal var softDeadlineNs: Long = 0
117 internal var hardDeadlineNs: Long = 0
118
pauseMeasurementnull119 fun pauseMeasurement() {
120 check(!paused) { "Unable to pause the benchmark. The benchmark has already paused." }
121 metrics.capturePaused()
122 paused = true
123 }
124
resumeMeasurementnull125 fun resumeMeasurement() {
126 check(paused) { "Unable to resume the benchmark. The benchmark is already running." }
127 metrics.captureResumed()
128 paused = false
129 }
130
beginTaskTracenull131 fun beginTaskTrace() {
132 if (yieldThreadPeriodically) {
133 Trace.beginSection("benchmark task")
134 initialTimeNs = System.nanoTime()
135 // we try to stop next measurement after soft deadline...
136 softDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(2)
137 // ... and throw if took longer than hard deadline
138 hardDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(10)
139 }
140 }
141
endTaskTracenull142 fun endTaskTrace() {
143 if (yieldThreadPeriodically) {
144 Trace.endSection()
145 }
146 }
147
yieldThreadIfDeadlinePassednull148 internal suspend inline fun yieldThreadIfDeadlinePassed() {
149 if (yieldThreadPeriodically) {
150 val timeNs = System.nanoTime()
151 if (timeNs >= softDeadlineNs) {
152
153 if (timeNs > hardDeadlineNs && Arguments.measureRepeatedOnMainThrowOnDeadline) {
154 val overrunInSec = (timeNs - hardDeadlineNs) / 1_000_000_000.0
155 // note - we throw without cancelling task trace, since outer layer handles that
156 throw IllegalStateException(
157 "Benchmark loop overran hard time limit by $overrunInSec seconds"
158 )
159 }
160
161 // pause and resume task trace around yield
162 endTaskTrace()
163 yield()
164 beginTaskTrace()
165 }
166 }
167 }
168 }
169
170 private var firstBenchmark = true
171
checkForErrorsnull172 private fun checkForErrors() {
173 Errors.throwIfError()
174 if (!firstBenchmark && Arguments.startupMode) {
175 throw AssertionError(
176 "Error - multiple benchmarks in startup mode. Only one " +
177 "benchmark may be run per 'am instrument' call, to ensure result " +
178 "isolation."
179 )
180 }
181 check(DeviceInfo.artMainlineVersion != DeviceInfo.ART_MAINLINE_VERSION_UNDETECTED_ERROR) {
182 "Unable to detect ART mainline module version to check for interference from method" +
183 " tracing, please see logcat for details, and/or file a bug with logcat."
184 }
185 check(
186 !enableMethodTracingAffectsMeasurementError ||
187 !DeviceInfo.methodTracingAffectsMeasurements ||
188 !MethodTracing.hasBeenUsed
189 ) {
190 "Measurement prevented by method trace - Running on a device/configuration where " +
191 "method tracing affects measurements, and a method trace has been captured " +
192 "- no additional benchmarks can be run without restarting the test suite. Use " +
193 "ProfilerConfig.MethodTracing.affectsMeasurementOnThisDevice to detect affected " +
194 "devices, see its documentation for more info."
195 }
196 }
197
198 internal typealias LoopedMeasurementBlock = suspend (MicrobenchmarkScope, Int) -> Unit
199
200 internal typealias ScopeFactory = (MicrobenchmarkRunningState) -> MicrobenchmarkScope
201
runBlockingOverrideMainnull202 private fun <T> runBlockingOverrideMain(
203 runOnMainDispatcher: Boolean,
204 block: suspend CoroutineScope.() -> T
205 ): T {
206 return if (runOnMainDispatcher) {
207 runBlocking(Dispatchers.Main, block)
208 } else {
209 runBlocking { block(this) }
210 }
211 }
212
captureMicroPerfettoTracenull213 internal fun captureMicroPerfettoTrace(
214 definition: TestDefinition,
215 config: MicrobenchmarkConfig?,
216 block: () -> Unit
217 ): String? =
218 PerfettoCaptureWrapper()
219 .record(
220 fileLabel = definition.traceUniqueName,
221 config =
222 PerfettoConfig.Benchmark(
223 appTagPackages =
224 if (config?.traceAppTagEnabled == true) {
225 listOf(InstrumentationRegistry.getInstrumentation().context.packageName)
226 } else {
227 emptyList()
228 },
229 useStackSamplingConfig = false
230 ),
231 // TODO(290918736): add support for Perfetto SDK Tracing in
232 // Microbenchmark in other cases, outside of MicrobenchmarkConfig
233 perfettoSdkConfig =
234 if (
235 config?.perfettoSdkTracingEnabled == true &&
236 Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
237 ) {
238 PerfettoCapture.PerfettoSdkConfig(
239 InstrumentationRegistry.getInstrumentation().context.packageName,
240 PerfettoCapture.PerfettoSdkConfig.InitialProcessState.Alive
241 )
242 } else {
243 null
244 },
245
246 // Optimize throughput in dryRunMode, since trace isn't useful, and extremely
247 // expensive on some emulators. Could alternately use UserspaceTracing if
248 // desired
249 // Additionally, skip on misconfigured devices to still enable benchmarking.
250 enableTracing = !Arguments.dryRunMode && !DeviceInfo.misconfiguredForTracing,
251 inMemoryTracingLabel = "Microbenchmark",
252 block = block
253 )
254
255 /**
256 * Core engine of microbenchmark, used in one of three ways:
257 * 1. [measureRepeatedImplWithTracing] - standard tracing microbenchmark
258 * 2. [measureRepeatedImplNoTracing] - legacy, non-tracing, suspending functionality backing
259 * [BenchmarkState] compatibility
260 * 3. [measureRepeatedCheckNanosReentrant] - microbenchmark which avoids modifying global state,
261 * which runs *within* other variants to check for thermal throttling
262 */
263 internal class Microbenchmark(
264 private val definition: TestDefinition,
265 private val phaseConfig: MicrobenchmarkPhase.Config,
266 private val yieldThreadPeriodically: Boolean,
267 private val scopeFactory: ScopeFactory,
268 private val loopedMeasurementBlock: LoopedMeasurementBlock
269 ) {
270 constructor(
271 definition: TestDefinition,
272 config: MicrobenchmarkConfig,
273 simplifiedTimingOnlyMode: Boolean,
274 yieldThreadPeriodically: Boolean,
runningStatenull275 scopeFactory: ScopeFactory = { runningState -> MicrobenchmarkScope(runningState) },
276 loopedMeasurementBlock: LoopedMeasurementBlock
277 ) : this(
278 definition = definition,
279 phaseConfig = MicrobenchmarkPhase.Config(config, simplifiedTimingOnlyMode),
280 yieldThreadPeriodically = yieldThreadPeriodically,
281 scopeFactory = scopeFactory,
282 loopedMeasurementBlock = loopedMeasurementBlock
283 )
284
285 private var startTimeNs = System.nanoTime()
286
287 init {
288 if (!phaseConfig.simplifiedTimingOnlyMode) {
289 Log.d(TAG, "-- Running ${definition.fullNameUnsanitized} --")
290 checkForErrors()
291 }
292 }
293
294 private val phases = phaseConfig.generatePhases()
295 private val state =
296 MicrobenchmarkRunningState(phases[0].metricsContainer, yieldThreadPeriodically)
297 private val scope = scopeFactory(state)
298
executePhasesnull299 suspend fun executePhases() {
300 state.beginTaskTrace()
301 try {
302 if (!phaseConfig.simplifiedTimingOnlyMode) {
303 ThrottleDetector.computeThrottleBaselineIfNeeded()
304 ThreadPriority.bumpCurrentThreadPriority()
305 }
306 firstBenchmark = false
307 phases.forEach {
308 it.execute(
309 traceUniqueName = definition.traceUniqueName,
310 scope = scope,
311 state = state,
312 loopedMeasurementBlock = loopedMeasurementBlock
313 )
314 }
315 } finally {
316 if (!phaseConfig.simplifiedTimingOnlyMode) {
317 // Don't modify thread priority in simplified timing mode, since 'outer'
318 // measureRepeated owns thread priority
319 ThreadPriority.resetBumpedThread()
320 }
321 phaseConfig.warmupManager.logInfo()
322 state.endTaskTrace()
323 }
324 }
325
outputnull326 fun output(perfettoTracePath: String?): MicrobenchmarkOutput {
327 Log.i(
328 BenchmarkState.TAG,
329 definition.outputTestName +
330 state.metricResults.map { it.getSummary() } +
331 "count=${state.maxIterationsPerRepeat}"
332 )
333 return MicrobenchmarkOutput(
334 definition = definition,
335 metricResults = state.metricResults,
336 profilerResults = processProfilerResults(perfettoTracePath),
337 totalRunTimeNs = System.nanoTime() - startTimeNs,
338 warmupIterations = state.warmupIterations,
339 repeatIterations = state.maxIterationsPerRepeat,
340 thermalThrottleSleepSeconds = state.totalThermalThrottleSleepSeconds,
341 reportMetricsInBundle = !Arguments.dryRunMode
342 )
343 .apply {
344 InstrumentationResults.reportBundle(createBundle())
345 ResultWriter.appendTestResult(createJsonTestResult())
346 }
347 }
348
getMinTimeNanosnull349 fun getMinTimeNanos(): Double {
350 return state.metricResults.first { it.name == "timeNs" }.min
351 }
352
processProfilerResultsnull353 private fun processProfilerResults(perfettoTracePath: String?): List<Profiler.ResultFile> {
354 // prepare profiling result files
355 perfettoTracePath?.apply {
356 // trace completed, and copied into shell writeable dir
357 val file = File(this)
358 file.appendUiState(
359 UiState(
360 timelineStart = null,
361 timelineEnd = null,
362 highlightPackage =
363 InstrumentationRegistry.getInstrumentation().context.packageName
364 )
365 )
366 }
367 state.profilerResults.forEach {
368 it.convertBeforeSync?.invoke()
369 if (perfettoTracePath != null) {
370 it.embedInPerfettoTrace(perfettoTracePath)
371 }
372 }
373 val profilerResults =
374 listOfNotNull(
375 perfettoTracePath?.let {
376 Profiler.ResultFile.ofPerfettoTrace(label = "Trace", absolutePath = it)
377 }
378 ) + state.profilerResults
379 return profilerResults
380 }
381
382 companion object {
383 internal const val TAG = "Benchmark"
384 }
385 }
386
measureRepeatedCheckNanosReentrantnull387 internal inline fun measureRepeatedCheckNanosReentrant(
388 crossinline measureBlock: MicrobenchmarkScope.() -> Unit
389 ): Double {
390 return Microbenchmark(
391 TestDefinition(
392 fullClassName = "ThrottleDetector",
393 simpleClassName = "ThrottleDetector",
394 methodName = "checkThrottle"
395 ),
396 config = MicrobenchmarkConfig(),
397 simplifiedTimingOnlyMode = true,
398 yieldThreadPeriodically = false,
399 loopedMeasurementBlock = { scope, iterations ->
400 var remainingIterations = iterations
401 do {
402 measureBlock.invoke(scope)
403 remainingIterations--
404 } while (remainingIterations > 0)
405 }
406 )
407 .run {
408 runBlocking { executePhases() }
409 getMinTimeNanos()
410 }
411 }
412
413 /**
414 * Limited version of [measureRepeatedImplWithTracing] which doesn't capture a trace, and doesn't
415 * support posting work to main thread.
416 */
measureRepeatedImplNoTracingnull417 internal suspend fun measureRepeatedImplNoTracing(
418 definition: TestDefinition,
419 config: MicrobenchmarkConfig,
420 loopedMeasurementBlock: LoopedMeasurementBlock
421 ) {
422 Microbenchmark(
423 definition = definition,
424 config = config,
425 simplifiedTimingOnlyMode = false,
426 yieldThreadPeriodically = false,
427 loopedMeasurementBlock = loopedMeasurementBlock
428 )
429 .apply {
430 executePhases()
431 output(perfettoTracePath = null)
432 }
433 }
434
435 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
measureRepeatedImplWithTracingnull436 fun measureRepeatedImplWithTracing(
437 definition: TestDefinition,
438 config: MicrobenchmarkConfig?,
439 postToMainThread: Boolean,
440 scopeFactory: ScopeFactory = { runningState -> MicrobenchmarkScope(runningState) },
441 loopedMeasurementBlock: LoopedMeasurementBlock
442 ) {
443 val microbenchmark =
444 Microbenchmark(
445 definition = definition,
446 config = config ?: MicrobenchmarkConfig(),
447 simplifiedTimingOnlyMode = false,
448 yieldThreadPeriodically = postToMainThread,
449 scopeFactory = scopeFactory,
450 loopedMeasurementBlock = loopedMeasurementBlock
451 )
452 val perfettoTracePath =
<lambda>null453 captureMicroPerfettoTrace(definition, config) {
454 trace(definition.fullNameUnsanitized) {
455 runBlockingOverrideMain(runOnMainDispatcher = postToMainThread) {
456 microbenchmark.executePhases()
457 }
458 }
459 }
460 microbenchmark.output(perfettoTracePath)
461 }
462
463 /**
464 * Top level entry point for capturing a microbenchmark with a trace.
465 *
466 * Eventually this method (or one like it) should be public, and also expose a results object
467 */
468 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
measureRepeatednull469 inline fun measureRepeated(
470 definition: TestDefinition,
471 config: MicrobenchmarkConfig? = null,
472 crossinline measureBlock: MicrobenchmarkScope.() -> Unit
473 ) {
474 measureRepeatedImplWithTracing(
475 postToMainThread = false,
476 definition = definition,
477 config = config,
478 loopedMeasurementBlock = { scope, iterations ->
479 var remainingIterations = iterations
480 do {
481 measureBlock.invoke(scope)
482 remainingIterations--
483 } while (remainingIterations > 0)
484 }
485 )
486 }
487