1 /*
2  * Copyright 2018 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.junit4
18 
19 import android.Manifest
20 import android.os.Build
21 import android.os.Looper
22 import android.util.Log
23 import androidx.annotation.RestrictTo
24 import androidx.benchmark.Arguments
25 import androidx.benchmark.BenchmarkStateLegacy
26 import androidx.benchmark.DeviceInfo
27 import androidx.benchmark.ExperimentalBenchmarkConfigApi
28 import androidx.benchmark.MicrobenchmarkConfig
29 import androidx.benchmark.perfetto.PerfettoCapture
30 import androidx.benchmark.perfetto.PerfettoCaptureWrapper
31 import androidx.benchmark.perfetto.PerfettoConfig
32 import androidx.benchmark.perfetto.UiState
33 import androidx.benchmark.perfetto.appendUiState
34 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
35 import androidx.test.rule.GrantPermissionRule
36 import androidx.tracing.Trace
37 import androidx.tracing.trace
38 import java.io.File
39 import java.util.concurrent.ExecutionException
40 import java.util.concurrent.FutureTask
41 import java.util.concurrent.TimeUnit
42 import org.junit.Assert.assertTrue
43 import org.junit.Assume.assumeFalse
44 import org.junit.Assume.assumeTrue
45 import org.junit.rules.RuleChain
46 import org.junit.rules.TestRule
47 import org.junit.runner.Description
48 import org.junit.runners.model.Statement
49 
50 /**
51  * JUnit rule for benchmarking code on an Android device.
52  *
53  * In Kotlin, benchmark with [measureRepeated]. In Java, use [getState].
54  *
55  * Benchmark results will be output:
56  * - Summary in Android Studio in the test log
57  * - In JSON format, on the host
58  * - In simple form in Logcat with the tag "Benchmark"
59  *
60  * Every test in the Class using this @Rule must contain a single benchmark.
61  *
62  * See the [Benchmark Guide](https://developer.android.com/studio/profile/benchmark) for more
63  * information on writing Benchmarks.
64  *
65  * @sample androidx.benchmark.samples.benchmarkRuleSample
66  */
67 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
68 class BenchmarkRuleLegacy
69 private constructor(
70     private val config: MicrobenchmarkConfig?,
71     /**
72      * This param is ignored, and just present to disambiguate the internal (nullable) vs external
73      * (non-null) variants of the constructor, since a lint failure occurs if they have the same
74      * signature, even if the external variant uses `this(config as MicrobenchmarkConfig?)`.
75      *
76      * In the future, we should just always pass a "default" config object, which can reference
77      * default values from Arguments, but that's a deeper change.
78      */
79     @Suppress("UNUSED_PARAMETER") ignored: Boolean = true
80 ) : TestRule {
81     constructor() : this(config = null, ignored = true)
82 
83     @ExperimentalBenchmarkConfigApi
84     constructor(config: MicrobenchmarkConfig) : this(config, ignored = true)
85 
86     internal // synthetic access
87     var internalState = BenchmarkStateLegacy(config)
88 
89     /**
90      * Object used for benchmarking in Java.
91      *
92      * ```java
93      * @Rule
94      * public BenchmarkRule benchmarkRule = new BenchmarkRule();
95      *
96      * @Test
97      * public void myBenchmark() {
98      *     ...
99      *     BenchmarkState state = benchmarkRule.getBenchmarkState();
100      *     while (state.keepRunning()) {
101      *         doSomeWork();
102      *     }
103      *     ...
104      * }
105      * ```
106      *
107      * @throws [IllegalStateException] if the BenchmarkRule isn't correctly applied to a test.
108      */
getStatenull109     public fun getState(): BenchmarkStateLegacy {
110         // Note: this is an explicit method instead of an accessor to help convey it's only for Java
111         // Kotlin users should call the [measureRepeated] method.
112         if (!applied) {
113             throw IllegalStateException(
114                 "Cannot get state before BenchmarkRule is applied to a test. Check that your " +
115                     "BenchmarkRule is annotated correctly (@Rule in Java, @get:Rule in Kotlin)."
116             )
117         }
118         return internalState
119     }
120 
121     internal // synthetic access
122     var applied = false
123 
124     @get:RestrictTo(RestrictTo.Scope.LIBRARY) public val scope: Scope = Scope()
125 
126     /** Handle used for controlling timing during [measureRepeated]. */
127     inner class Scope internal constructor() {
128         /**
129          * Disable timing for a block of code.
130          *
131          * Used for disabling timing for work that isn't part of the benchmark:
132          * - When constructing per-loop randomized inputs for operations with caching,
133          * - Controlling which parts of multi-stage work are measured (e.g. View measure/layout)
134          * - Disabling timing during per-loop verification
135          *
136          * @sample androidx.benchmark.samples.runWithMeasurementDisabledSample
137          */
runWithTimingDisablednull138         public inline fun <T> runWithTimingDisabled(block: () -> T): T {
139             getOuterState().pauseTiming()
140             // Note: we only bother with tracing for the runWithTimingDisabled function for
141             // Kotlin callers, as it's more difficult to corrupt the trace with incorrectly
142             // paired BenchmarkState pause/resume calls
143             val ret: T =
144                 try {
145                     // TODO: use `trace() {}` instead of this manual try/finally,
146                     //  once the block parameter is marked crossinline.
147                     Trace.beginSection("runWithTimingDisabled")
148                     block()
149                 } finally {
150                     Trace.endSection()
151                 }
152             getOuterState().resumeTiming()
153             return ret
154         }
155 
156         /**
157          * Allows the inline function [runWithTimingDisabled] to be called outside of this scope.
158          */
159         @Suppress("ShowingMemberInHiddenClass")
160         @PublishedApi
getOuterStatenull161         internal fun getOuterState(): BenchmarkStateLegacy {
162             return getState()
163         }
164     }
165 
applynull166     override fun apply(base: Statement, description: Description): Statement {
167         return RuleChain.outerRule(
168                 GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
169             )
170             .around(::applyInternal)
171             .apply(base, description)
172     }
173 
<lambda>null174     private fun applyInternal(base: Statement, description: Description) = Statement {
175         applied = true
176 
177         assumeTrue(Arguments.RuleType.Microbenchmark in Arguments.enabledRules)
178 
179         // When running on emulator and argument `skipOnEmulator` is passed,
180         // the test is skipped.
181         if (Arguments.skipBenchmarksOnEmulator) {
182             assumeFalse(
183                 "Skipping test because it's running on emulator and `skipOnEmulator` is enabled",
184                 DeviceInfo.isEmulator
185             )
186         }
187 
188         var invokeMethodName = description.methodName
189         Log.d(TAG, "-- Running ${description.className}#$invokeMethodName --")
190 
191         // validate and simplify the function name.
192         // First, remove the "test" prefix which normally comes from CTS test.
193         // Then make sure the [subTestName] is valid, not just numbers like [0].
194         if (invokeMethodName.startsWith("test")) {
195             assertTrue("The test name $invokeMethodName is too short", invokeMethodName.length > 5)
196             invokeMethodName =
197                 invokeMethodName.substring(4, 5).lowercase() + invokeMethodName.substring(5)
198         }
199         val uniqueName = description.testClass.simpleName + "_" + invokeMethodName
200         internalState.traceUniqueName = uniqueName
201 
202         val tracePath =
203             PerfettoCaptureWrapper()
204                 .record(
205                     fileLabel = uniqueName,
206                     config =
207                         PerfettoConfig.Benchmark(
208                             appTagPackages =
209                                 if (config?.traceAppTagEnabled == true) {
210                                     listOf(getInstrumentation().context.packageName)
211                                 } else {
212                                     emptyList()
213                                 },
214                             useStackSamplingConfig = false
215                         ),
216                     // TODO(290918736): add support for Perfetto SDK Tracing in
217                     //  Microbenchmark in other cases, outside of MicrobenchmarkConfig
218                     perfettoSdkConfig =
219                         if (
220                             config?.perfettoSdkTracingEnabled == true &&
221                                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
222                         ) {
223                             PerfettoCapture.PerfettoSdkConfig(
224                                 getInstrumentation().context.packageName,
225                                 PerfettoCapture.PerfettoSdkConfig.InitialProcessState.Alive
226                             )
227                         } else {
228                             null
229                         },
230 
231                     // Optimize throughput in dryRunMode, since trace isn't useful, and extremely
232                     //   expensive on some emulators. Could alternately use UserspaceTracing if
233                     // desired
234                     // Additionally, skip on misconfigured devices to still enable benchmarking.
235                     enableTracing = !Arguments.dryRunMode && !DeviceInfo.misconfiguredForTracing,
236                     inMemoryTracingLabel = "Microbenchmark"
237                 ) {
238                     trace(description.displayName) { base.evaluate() }
239                 }
240                 ?.apply {
241                     // trace completed, and copied into shell writeable dir
242                     val file = File(this)
243                     file.appendUiState(
244                         UiState(
245                             timelineStart = null,
246                             timelineEnd = null,
247                             highlightPackage = getInstrumentation().context.packageName
248                         )
249                     )
250                 }
251 
252         internalState.report(
253             fullClassName = description.className,
254             simpleClassName = description.testClass.simpleName,
255             methodName = invokeMethodName,
256             perfettoTracePath = tracePath
257         )
258     }
259 
260     internal companion object {
261         private const val TAG = "Benchmark"
262     }
263 }
264 
265 /**
266  * Benchmark a block of code.
267  *
268  * @param block The block of code to benchmark.
269  * @sample androidx.benchmark.samples.benchmarkRuleSample
270  */
271 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
measureRepeatednull272 inline fun BenchmarkRuleLegacy.measureRepeated(
273     crossinline block: BenchmarkRuleLegacy.Scope.() -> Unit
274 ) {
275     // Note: this is an extension function to discourage calling from Java.
276 
277     if (Arguments.throwOnMainThreadMeasureRepeated) {
278         check(Looper.myLooper() != Looper.getMainLooper()) {
279             "Cannot invoke measureRepeated from the main thread. Instead use" +
280                 " measureRepeatedOnMainThread()"
281         }
282     }
283 
284     // Extract members to locals, to ensure we check #applied, and we don't hit accessors
285     val localState = getState()
286     val localScope = scope
287 
288     try {
289         while (localState.keepRunningInline()) {
290             block(localScope)
291         }
292     } catch (t: Throwable) {
293         localState.cleanupBeforeThrow()
294         throw t
295     }
296 }
297 
298 /**
299  * Benchmark a block of code, which runs on the main thread, and can safely interact with UI.
300  *
301  * While `@UiThreadRule` works for a standard test, it doesn't work for benchmarks of arbitrary
302  * duration, as they may run for much more than 5 seconds and suffer ANRs, especially in continuous
303  * runs.
304  *
305  * @param block The block of code to benchmark.
306  * @throws java.lang.Throwable when an exception is thrown on the main thread.
307  * @throws IllegalStateException if a hard deadline is exceeded while the block is running on the
308  *   main thread.
309  * @sample androidx.benchmark.samples.measureRepeatedOnMainThreadSample
310  */
311 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
measureRepeatedOnMainThreadnull312 inline fun BenchmarkRuleLegacy.measureRepeatedOnMainThread(
313     crossinline block: BenchmarkRuleLegacy.Scope.() -> Unit
314 ) {
315     check(Looper.myLooper() != Looper.getMainLooper()) {
316         "Cannot invoke measureRepeatedOnMainThread from the main thread"
317     }
318 
319     var resumeScheduled = false
320     while (true) {
321         val task = FutureTask {
322             // Extract members to locals, to ensure we check #applied, and we don't hit accessors
323             val localState = getState()
324             val localScope = scope
325 
326             val initialTimeNs = System.nanoTime()
327             // we try to stop next measurement after soft deadline...
328             val softDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(2)
329             // ... and throw if took longer than hard deadline
330             val hardDeadlineNs = initialTimeNs + TimeUnit.SECONDS.toNanos(10)
331             var timeNs: Long = 0
332 
333             try {
334                 Trace.beginSection("measureRepeatedOnMainThread task")
335 
336                 if (resumeScheduled) {
337                     localState.resumeTiming()
338                 }
339 
340                 do {
341                     // note that this function can still block for considerable time, e.g. when
342                     // setting up / tearing down profiling, or sleeping to let the device cool off.
343                     if (!localState.keepRunningInline()) {
344                         return@FutureTask false
345                     }
346 
347                     block(localScope)
348 
349                     // Avoid checking for deadline on all but last iteration per measurement,
350                     // to amortize cost of System.nanoTime(). Without this optimization, minimum
351                     // measured time can be 10x higher.
352                     if (localState.getIterationsRemaining() != 1) {
353                         continue
354                     }
355                     timeNs = System.nanoTime()
356                 } while (timeNs <= softDeadlineNs)
357 
358                 resumeScheduled = true
359                 localState.pauseTiming()
360 
361                 if (timeNs > hardDeadlineNs && Arguments.measureRepeatedOnMainThrowOnDeadline) {
362                     localState.cleanupBeforeThrow()
363                     val overrunInSec = (timeNs - hardDeadlineNs) / 1_000_000_000.0
364                     throw IllegalStateException(
365                         "Benchmark loop overran hard time limit by $overrunInSec seconds"
366                     )
367                 }
368 
369                 return@FutureTask true // continue
370             } finally {
371                 Trace.endSection()
372             }
373         }
374         getInstrumentation().runOnMainSync(task)
375         val shouldContinue: Boolean =
376             try {
377                 // Ideally we'd implement the delay here, as a timeout, but we can't do this until
378                 // have a way to move thermal throttle sleeping off the UI thread.
379                 task.get()
380             } catch (e: ExecutionException) {
381                 // Expose the original exception
382                 throw e.cause!!
383             }
384         if (!shouldContinue) {
385             // all done
386             break
387         }
388     }
389 }
390