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 androidx.annotation.RestrictTo 20 import java.util.concurrent.TimeUnit 21 import kotlin.coroutines.Continuation 22 import kotlin.coroutines.CoroutineContext 23 import kotlin.coroutines.EmptyCoroutineContext 24 import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED 25 import kotlin.coroutines.intrinsics.createCoroutineUnintercepted 26 import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn 27 import kotlin.coroutines.resume 28 29 /** 30 * This function is used to allow BenchmarkState to provide its non-suspending keepRunning(), but 31 * underneath incrementally progress the underlying coroutine microbenchmark API as needed. 32 * 33 * It is optimized to allow the underlying coroutine API to be incrementally upgraded without any 34 * changes to BenchmarkState. 35 * 36 * This is modeled after the suspending iterator {} sequence builder in kotlin, but optimized for: 37 * - minimal allocation 38 * - keeping yields:resumes at 1:1 (for simplicity) 39 * - minimal virtual functions 40 * 41 * @see kotlin.sequences.iterator 42 */ 43 private fun createSuspendedLoop( 44 block: suspend SuspendedLoopTrigger.() -> Unit 45 ): SuspendedLoopTrigger { 46 val suspendedLoopTrigger = SuspendedLoopTrigger() 47 suspendedLoopTrigger.nextStep = 48 block.createCoroutineUnintercepted( 49 receiver = suspendedLoopTrigger, 50 completion = suspendedLoopTrigger 51 ) 52 return suspendedLoopTrigger 53 } 54 55 /** 56 * SuspendedLoopTrigger functions as the bridge between the new coroutine measureRepeated 57 * implementation and the (soon to be) legacy Java API. 58 * 59 * It allows the vast majority of the benchmark library to be written in coroutines (with very 60 * deliberate suspend calls, generally just for yielding the main thread) and still function within 61 * a runBlocking block inside of `benchmarkState.keepRunning()` 62 * 63 * Eventually, the BenchmarkState api will be deprecated in favor of a Java-friendly variant of 64 * measureRepeated, but this code will remain (ideally without significant change) to support the 65 * BenchmarkState API in the long term. 66 */ 67 private class SuspendedLoopTrigger : Continuation<Unit> { 68 @JvmField var nextStep: Continuation<Unit>? = null 69 private var next: Int = -1 70 private var done: Boolean = false 71 72 /** 73 * Schedule the loop manager Yields a value of loops to be run by the user of the 74 * SuspendedLoopTrigger. 75 */ awaitLoopsnull76 suspend fun awaitLoops(loopCount: Int) { 77 next = loopCount 78 suspendCoroutineUninterceptedOrReturn { c -> 79 nextStep = c 80 COROUTINE_SUSPENDED 81 } 82 } 83 84 /** Gets the number of loops to run before calling [getNextLoopCount] again */ getNextLoopCountnull85 fun getNextLoopCount(): Int { 86 if (done) return 0 87 nextStep!!.resume(Unit) 88 return next 89 } 90 91 override val context: CoroutineContext 92 get() = EmptyCoroutineContext 93 resumeWithnull94 override fun resumeWith(result: Result<Unit>) { 95 result.getOrThrow() // just rethrow exception if it is there 96 done = true 97 } 98 } 99 100 /** 101 * Control object for microbenchmarking in Java. 102 * 103 * Query a state object with [androidx.benchmark.junit4.BenchmarkRule.getState], and use it to 104 * measure a block of Java with [BenchmarkStateLegacy.keepRunning]: 105 * ``` 106 * @Rule 107 * public BenchmarkRule benchmarkRule = new BenchmarkRule(); 108 * 109 * @Test 110 * public void sampleMethod() { 111 * BenchmarkState state = benchmarkRule.getState(); 112 * 113 * int[] src = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 114 * while (state.keepRunning()) { 115 * int[] dest = new int[src.length]; 116 * System.arraycopy(src, 0, dest, 0, src.length); 117 * } 118 * } 119 * ``` 120 * 121 * Note that BenchmarkState does not give access to Perfetto traces. 122 */ 123 class BenchmarkState 124 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 125 constructor(testDefinition: TestDefinition, private val config: MicrobenchmarkConfig) { 126 // Secondary explicit constructor allows for internal usage without experimental config opt in 127 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 128 constructor(testDefinition: TestDefinition) : this(testDefinition, MicrobenchmarkConfig()) 129 130 @JvmField 131 @PublishedApi // Previously used by [BenchmarkState.keepRunningInline()] 132 internal var iterationsRemaining = 0 133 134 /** Ideally we'd call into the top level function, but it's non-suspending */ <lambda>null135 private var internalIter = createSuspendedLoop { 136 // Theoretically we'd ideally call into the top level measureRepeated function, but 137 // that function isn't suspending. Making it suspend would allow this call to perform 138 // tracing, but would significantly complicate the thread management of outer layers (e.g. 139 // carefully scheduling where trace capture start/end happens). As this is compat code, we 140 // don't bother. 141 measureRepeatedImplNoTracing( 142 testDefinition, 143 config = config, 144 loopedMeasurementBlock = { microbenchScope, loops -> 145 scope = microbenchScope 146 awaitLoops(loops) 147 } 148 ) 149 } 150 151 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @JvmField var scope: MicrobenchmarkScope? = null 152 pauseTimingnull153 fun pauseTiming() { 154 scope!!.pauseMeasurement() 155 } 156 resumeTimingnull157 fun resumeTiming() { 158 scope!!.resumeMeasurement() 159 } 160 161 @PublishedApi keepRunningInternalnull162 internal fun keepRunningInternal(): Boolean { 163 iterationsRemaining = internalIter.getNextLoopCount() 164 if (iterationsRemaining > 0) { 165 iterationsRemaining-- 166 return true 167 } 168 return false 169 } 170 keepRunningnull171 fun keepRunning(): Boolean { 172 if (iterationsRemaining > 0) { 173 iterationsRemaining-- 174 return true 175 } 176 return keepRunningInternal() 177 } 178 179 // Note: Constants left here to avoid churn, but should eventually be moved out to more 180 // appropriate locations 181 companion object { 182 internal const val TAG = "Benchmark" 183 184 /** 185 * Conservative estimate for how much method tracing slows down runtime - how much longer 186 * will `methodTrace {x()}` be than `x()` for nontrivial workloads. 187 * 188 * This is a conservative estimate, better version of this would account for OS/Art version 189 * 190 * Value derived from observed numbers on bramble API 31 (600-800x slowdown) 191 */ 192 internal const val METHOD_TRACING_ESTIMATED_SLOWDOWN_FACTOR = 1000 193 194 /** 195 * Maximum duration to trace on main thread to avoid ANRs 196 * 197 * In practice, other types of tracing can be equally dangerous for ANRs, but method tracing 198 * is the default tracing mode. 199 */ 200 internal const val METHOD_TRACING_MAX_DURATION_NS = 4_000_000_000 201 202 internal val DEFAULT_MEASUREMENT_DURATION_NS = TimeUnit.MILLISECONDS.toNanos(100) 203 204 internal val SAMPLED_PROFILER_DURATION_NS = 205 TimeUnit.SECONDS.toNanos(Arguments.profilerSampleDurationSeconds) 206 207 /** 208 * Used to disable error to enable internal correctness tests, which need to use method 209 * tracing and can safely ignore measurement accuracy 210 * 211 * Ideally this would function as a true suppressible error like in Errors.kt, but existing 212 * error functionality doesn't handle changing error states dynamically 213 */ 214 internal var enableMethodTracingAffectsMeasurementError = true 215 } 216 } 217