| /* |
| * Copyright 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.benchmark |
| |
| import android.Manifest |
| import android.util.Log |
| import androidx.annotation.RestrictTo |
| import androidx.benchmark.WarningState.WARNING_PREFIX |
| import androidx.test.rule.GrantPermissionRule |
| import org.junit.Assert.assertTrue |
| import org.junit.rules.RuleChain |
| import org.junit.rules.TestRule |
| import org.junit.runner.Description |
| import org.junit.runners.model.Statement |
| |
| /** |
| * JUnit rule for benchmarking code on an Android device. |
| * |
| * In Kotlin, benchmark with [measureRepeated]: |
| * |
| * ``` |
| * @get:Rule |
| * val benchmarkRule = BenchmarkRule(); |
| * |
| * @Test |
| * fun myBenchmark() { |
| * ... |
| * benchmarkRule.measureRepeated { |
| * doSomeWork() |
| * } |
| * ... |
| * } |
| * ``` |
| * |
| * In Java, use `getState()`: |
| * |
| * ``` |
| * @Rule |
| * public BenchmarkRule benchmarkRule = new BenchmarkRule(); |
| * |
| * @Test |
| * public void myBenchmark() { |
| * ... |
| * BenchmarkState state = benchmarkRule.getState(); |
| * while (state.keepRunning()) { |
| * doSomeWork(); |
| * } |
| * ... |
| * } |
| * ``` |
| * |
| * Benchmark results will be output: |
| * - Summary in AndroidStudio in the test log, |
| * - In simple form in Logcat with the tag "Benchmark" |
| * - In csv form in Logcat with the tag "BenchmarkCsv" |
| * - To the instrumentation status result Bundle on the gradle command line |
| * |
| * Every test in the Class using this @Rule must contain a single benchmark. |
| */ |
| class BenchmarkRule : TestRule { |
| constructor() { |
| this.enableReport = true |
| } |
| |
| internal constructor(enableReport: Boolean) { |
| this.enableReport = enableReport |
| } |
| |
| internal // synthetic access |
| val internalState = BenchmarkState() |
| |
| /** |
| * Used to disable reporting, for correctness tests that shouldn't report values |
| * (and would trigger warnings if they did, e.g. debuggable=true) |
| */ |
| private val enableReport: Boolean |
| |
| /** |
| * Object used for benchmarking in Java. |
| * |
| * ``` |
| * @Rule |
| * public BenchmarkRule benchmarkRule = new BenchmarkRule(); |
| * |
| * @Test |
| * public void myBenchmark() { |
| * ... |
| * BenchmarkState state = benchmarkRule.getBenchmarkState(); |
| * while (state.keepRunning()) { |
| * doSomeWork(); |
| * } |
| * ... |
| * } |
| * ``` |
| */ |
| fun getState(): BenchmarkState { |
| // Note: this is an explicit method instead of an accessor to help convey it's only for Java |
| // Kotlin users should call the [measureRepeated] method. |
| if (!applied) { |
| throw IllegalStateException( |
| "Cannot get state before BenchmarkRule is applied to a test. Check that your " + |
| "BenchmarkRule is annotated correctly (@Rule in Java, @get:Rule in Kotlin)." |
| ) |
| } |
| return internalState |
| } |
| |
| internal // synthetic access |
| var applied = false |
| |
| /** @hide */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| val scope = Scope() |
| |
| /** |
| * Handle used for controlling timing during [measureRepeated]. |
| */ |
| inner class Scope internal constructor() { |
| /** |
| * Disable timing for a block of code. |
| * |
| * Used for disabling timing for work that isn't part of the benchmark: |
| * - When constructing per-loop randomized inputs for operations with caching, |
| * - Controlling which parts of multi-stage work are measured (e.g. View measure/layout) |
| * - Disabling timing during per-loop verification |
| * |
| * ``` |
| * @Test |
| * fun bitmapProcessing() = benchmarkRule.measureRepeated { |
| * val input: Bitmap = runWithTimingDisabled { constructTestBitmap() } |
| * processBitmap(input) |
| * } |
| * ``` |
| */ |
| inline fun <T> runWithTimingDisabled(block: () -> T): T { |
| getOuterState().pauseTiming() |
| val ret = block() |
| getOuterState().resumeTiming() |
| return ret |
| } |
| |
| /** |
| * Allows the inline function [runWithTimingDisabled] to be called outside of this scope. |
| */ |
| @PublishedApi |
| internal fun getOuterState(): BenchmarkState { |
| return getState() |
| } |
| } |
| |
| override fun apply(base: Statement, description: Description): Statement { |
| return RuleChain |
| .outerRule(GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)) |
| .around(::applyInternal) |
| .apply(base, description) |
| } |
| |
| private fun applyInternal(base: Statement, description: Description) = Statement { |
| applied = true |
| var invokeMethodName = description.methodName |
| Log.i(TAG, "Running ${description.className}#$invokeMethodName") |
| |
| // validate and simplify the function name. |
| // First, remove the "test" prefix which normally comes from CTS test. |
| // Then make sure the [subTestName] is valid, not just numbers like [0]. |
| if (invokeMethodName.startsWith("test")) { |
| assertTrue( |
| "The test name $invokeMethodName is too short", |
| invokeMethodName.length > 5 |
| ) |
| invokeMethodName = invokeMethodName.substring(4, 5).toLowerCase() + |
| invokeMethodName.substring(5) |
| } |
| |
| base.evaluate() |
| |
| if (enableReport) { |
| val fullTestName = |
| WARNING_PREFIX + description.testClass.simpleName + "." + invokeMethodName |
| internalState.sendStatus(fullTestName) |
| |
| ResultWriter.appendReport( |
| internalState.getReport( |
| testName = WARNING_PREFIX + invokeMethodName, |
| className = description.className |
| ) |
| ) |
| } |
| } |
| |
| internal companion object { |
| private const val TAG = "BenchmarkRule" |
| } |
| } |
| |
| /** |
| * Benchmark a block of code. |
| * |
| * ``` |
| * @get:Rule |
| * val benchmarkRule = BenchmarkRule(); |
| * |
| * @Test |
| * fun myBenchmark() { |
| * ... |
| * benchmarkRule.measureRepeated { |
| * doSomeWork() |
| * } |
| * ... |
| * } |
| * ``` |
| * |
| * @param block The block of code to benchmark. |
| */ |
| inline fun BenchmarkRule.measureRepeated(crossinline block: BenchmarkRule.Scope.() -> Unit) { |
| // Note: this is an extension function to discourage calling from Java. |
| |
| // Extract members to locals, to ensure we check #applied, and we don't hit accessors |
| val localState = getState() |
| val localScope = scope |
| |
| while (localState.keepRunningInline()) { |
| block(localScope) |
| } |
| } |
| |
| internal fun Statement(evaluate: () -> Unit) = object : Statement() { |
| override fun evaluate() = evaluate() |
| } |