1 /* <lambda>null2 * Copyright 2020 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.compose.ui.test 18 19 import androidx.compose.runtime.MonotonicFrameClock 20 import androidx.compose.ui.test.platform.makeSynchronizedObject 21 import androidx.compose.ui.test.platform.synchronized 22 import kotlin.coroutines.ContinuationInterceptor 23 import kotlinx.coroutines.CoroutineDispatcher 24 import kotlinx.coroutines.CoroutineScope 25 import kotlinx.coroutines.ExperimentalCoroutinesApi 26 import kotlinx.coroutines.delay 27 import kotlinx.coroutines.launch 28 import kotlinx.coroutines.suspendCancellableCoroutine 29 import kotlinx.coroutines.test.TestCoroutineScheduler 30 31 private const val DefaultFrameDelay = 16_000_000L 32 33 /** 34 * A [MonotonicFrameClock] with a time source controlled by a `kotlinx-coroutines-test` 35 * [TestCoroutineScheduler]. This frame clock may be used to consistently drive time under 36 * controlled tests. 37 * 38 * Calls to [withFrameNanos] will schedule an upcoming frame [frameDelayNanos] nanoseconds in the 39 * future by launching into [coroutineScope] if such a frame has not yet been scheduled. The current 40 * frame time for [withFrameNanos] is provided by [delayController]. It is strongly suggested that 41 * [coroutineScope] contain the test dispatcher controlled by [delayController]. 42 * 43 * @param coroutineScope The [CoroutineScope] used to simulate the main thread and schedule frames 44 * on. It must contain a [TestCoroutineScheduler] and a [ContinuationInterceptor]. 45 * @param frameDelayNanos The number of nanoseconds to [delay] between executing frames. 46 * @param onPerformTraversals Called with the frame time of the frame that was just executed, after 47 * running all `withFrameNanos` callbacks, but before resuming their callers' continuations. Any 48 * continuations resumed while running frame callbacks or [onPerformTraversals] will not be 49 * dispatched until after [onPerformTraversals] finishes. If [onPerformTraversals] throws, all 50 * `withFrameNanos` callers will be cancelled. 51 */ 52 // This is intentionally not OptIn, because we want to communicate to consumers that by using this 53 // API, they're also transitively getting all the experimental risk of using the experimental API 54 // in the kotlinx testing library. DO NOT MAKE OPT-IN! 55 @ExperimentalCoroutinesApi 56 @ExperimentalTestApi 57 class TestMonotonicFrameClock( 58 private val coroutineScope: CoroutineScope, 59 @get:Suppress("MethodNameUnits") // Nanos for high-precision animation clocks 60 val frameDelayNanos: Long = DefaultFrameDelay, 61 private val onPerformTraversals: (Long) -> Unit = {} 62 ) : MonotonicFrameClock { 63 private val delayController = <lambda>null64 requireNotNull(coroutineScope.coroutineContext[TestCoroutineScheduler]) { 65 "TestMonotonicFrameClock's coroutineScope must have a TestCoroutineScheduler" 66 } 67 // The parentInterceptor resolves to the TestDispatcher 68 private val parentInterceptor = <lambda>null69 requireNotNull(coroutineScope.coroutineContext[ContinuationInterceptor]) { 70 "TestMonotonicFrameClock's coroutineScope must have a ContinuationInterceptor" 71 } 72 private val lock = makeSynchronizedObject() 73 private var awaiters = mutableListOf<(Long) -> Unit>() 74 private var spareAwaiters = mutableListOf<(Long) -> Unit>() 75 private var scheduledFrameDispatch = false 76 private val frameDeferringInterceptor = FrameDeferringContinuationInterceptor(parentInterceptor) 77 78 /** Returns whether there are any awaiters on this clock. */ 79 val hasAwaiters: Boolean 80 get() = 81 frameDeferringInterceptor.hasTrampolinedTasks || <lambda>null82 synchronized(lock) { awaiters.isNotEmpty() } 83 84 /** 85 * A [CoroutineDispatcher] that will defer continuation resumptions requested within 86 * [withFrameNanos] calls to until after the frame callbacks have finished running. Resumptions 87 * will then be dispatched before resuming the continuations from the [withFrameNanos] calls 88 * themselves. 89 */ 90 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") 91 @get:ExperimentalTestApi 92 @ExperimentalTestApi 93 val continuationInterceptor: ContinuationInterceptor 94 get() = frameDeferringInterceptor 95 96 /** 97 * Schedules [onFrame] to be ran on the next "fake" frame, and schedules the task to actually 98 * perform that frame if it hasn't already been scheduled. 99 * 100 * Instead of waiting for a vsync message to perform the next frame, it simply calls the 101 * coroutine [delay] function for the test frame time [frameDelayMillis] (which the underlying 102 * test coroutine scheduler will actually complete immediately without waiting), and then run 103 * all scheduled tasks. 104 */ withFrameNanosnull105 override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R = 106 suspendCancellableCoroutine { co -> 107 synchronized(lock) { 108 awaiters.add { frameTime -> co.resumeWith(runCatching { onFrame(frameTime) }) } 109 if (!scheduledFrameDispatch) { 110 scheduledFrameDispatch = true 111 coroutineScope.launch { 112 delay(frameDelayMillis) 113 performFrame() 114 } 115 } 116 } 117 } 118 119 /** 120 * Executes all scheduled frame callbacks, and then dispatches any continuations that were 121 * resumed by the callbacks and deferred by [continuationInterceptor]. 122 * 123 * This method performs a subset of the responsibilities of `Choreographer.doFrame` on Android, 124 * which is usually responsible for executing animation frames and coroutines, and also 125 * "performing traversals", which practically just means doing the layout pass on the view tree. 126 * Since this method replaces `doFrame`, it also needs to trigger the compose layout pass (see 127 * b/222093277). 128 * 129 * Typically, the only task that will have been enqueued will be the `Recomposer`'s 130 * `runRecomposeAndApplyChanges`' call to [withFrameNanos] – any app coroutines waiting for the 131 * next frame will actually be dispatched by `runRecomposeAndApplyChanges`' 132 * `BroadcastFrameClock`, not this method. 133 */ performFramenull134 private fun performFrame() { 135 frameDeferringInterceptor.runWithoutResumingCoroutines { 136 // This is set after acquiring the lock in case the virtual time was advanced while 137 // waiting for it. 138 var frameTime = -1L // it's re-initialized below 139 val toRun = 140 synchronized(lock) { 141 check(scheduledFrameDispatch) { "frame dispatch not scheduled" } 142 143 frameTime = delayController.currentTime * 1_000_000 144 scheduledFrameDispatch = false 145 awaiters.also { 146 awaiters = spareAwaiters 147 spareAwaiters = it 148 } 149 } 150 151 // Because runningFrameCallbacks is still true, all these resumptions will be queued to 152 // toRunTrampolined. 153 toRun.forEach { it(frameTime) } 154 toRun.clear() 155 156 onPerformTraversals(frameTime) 157 } 158 } 159 } 160 161 /** The frame delay time for the [TestMonotonicFrameClock] in milliseconds. */ 162 @OptIn(ExperimentalCoroutinesApi::class) 163 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET") 164 @get:ExperimentalTestApi // Required to annotate Java-facing APIs 165 @ExperimentalTestApi // Required by kotlinc to use frameDelayNanos 166 val TestMonotonicFrameClock.frameDelayMillis: Long 167 get() = frameDelayNanos / 1_000_000 168