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