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