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