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