Skip to content

Commit 5322c8d

Browse files
authored
Ensure awaitFrame() only awaits a single frame when used from the main looper (#3437)
Fixes #3432
1 parent 3327273 commit 5322c8d

File tree

5 files changed

+26
-66
lines changed

5 files changed

+26
-66
lines changed

ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt

+12-7
Original file line numberDiff line numberDiff line change
@@ -185,22 +185,27 @@ private var choreographer: Choreographer? = null
185185
public suspend fun awaitFrame(): Long {
186186
// fast path when choreographer is already known
187187
val choreographer = choreographer
188-
if (choreographer != null) {
189-
return suspendCancellableCoroutine { cont ->
188+
return if (choreographer != null) {
189+
suspendCancellableCoroutine { cont ->
190190
postFrameCallback(choreographer, cont)
191191
}
192+
} else {
193+
awaitFrameSlowPath()
192194
}
193-
// post into looper thread to figure it out
194-
return suspendCancellableCoroutine { cont ->
195-
Dispatchers.Main.dispatch(EmptyCoroutineContext, Runnable {
195+
}
196+
197+
private suspend fun awaitFrameSlowPath(): Long = suspendCancellableCoroutine { cont ->
198+
if (Looper.myLooper() === Looper.getMainLooper()) { // Check if we are already in the main looper thread
199+
updateChoreographerAndPostFrameCallback(cont)
200+
} else { // post into looper thread to figure it out
201+
Dispatchers.Main.dispatch(cont.context, Runnable {
196202
updateChoreographerAndPostFrameCallback(cont)
197203
})
198204
}
199205
}
200206

201207
private fun updateChoreographerAndPostFrameCallback(cont: CancellableContinuation<Long>) {
202-
val choreographer = choreographer ?:
203-
Choreographer.getInstance()!!.also { choreographer = it }
208+
val choreographer = choreographer ?: Choreographer.getInstance()!!.also { choreographer = it }
204209
postFrameCallback(choreographer, cont)
205210
}
206211

ui/kotlinx-coroutines-android/test/HandlerDispatcherTest.kt

+2-6
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,12 @@ class HandlerDispatcherTest : TestBase() {
109109
launch(Dispatchers.Main, start = CoroutineStart.UNDISPATCHED) {
110110
expect(1)
111111
awaitFrame()
112-
expect(5)
112+
expect(3)
113113
}
114114
expect(2)
115115
// Run choreographer detection
116116
mainLooper.runOneTask()
117-
expect(3)
118-
mainLooper.scheduler.advanceBy(50, TimeUnit.MILLISECONDS)
119-
expect(4)
120-
mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS)
121-
finish(6)
117+
finish(4)
122118
}
123119

124120
private fun CoroutineScope.doTestAwaitWithDetectedChoreographer() {

ui/kotlinx-coroutines-javafx/build.gradle.kts

-37
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,3 @@ javafx {
2121
modules = listOf("javafx.controls")
2222
configuration = "javafx"
2323
}
24-
25-
val JDK_18: String? by lazy {
26-
System.getenv("JDK_18")
27-
}
28-
29-
val checkJdk8 by tasks.registering {
30-
// only fail w/o JDK_18 when actually trying to test, not during project setup phase
31-
doLast {
32-
if (JDK_18 == null) {
33-
throw GradleException(
34-
"""
35-
JDK_18 environment variable is not defined.
36-
Can't run JDK 8 compatibility tests.
37-
Please ensure JDK 8 is installed and that JDK_18 points to it.
38-
""".trimIndent()
39-
)
40-
}
41-
}
42-
}
43-
44-
val jdk8Test by tasks.registering(Test::class) {
45-
// Run these tests only during nightly stress test
46-
onlyIf { project.properties["stressTest"] != null }
47-
48-
val test = tasks.test.get()
49-
50-
classpath = test.classpath
51-
testClassesDirs = test.testClassesDirs
52-
executable = "$JDK_18/bin/java"
53-
54-
dependsOn("compileTestKotlin")
55-
dependsOn(checkJdk8)
56-
}
57-
58-
tasks.build {
59-
dependsOn(jdk8Test)
60-
}

ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt

+6-5
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,22 @@ public sealed class JavaFxDispatcher : MainCoroutineDispatcher(), Delay {
3434

3535
/** @suppress */
3636
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
37-
val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS) {
37+
val timeline = schedule(timeMillis) {
3838
with(continuation) { resumeUndispatched(Unit) }
3939
}
4040
continuation.invokeOnCancellation { timeline.stop() }
4141
}
4242

4343
/** @suppress */
4444
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
45-
val timeline = schedule(timeMillis, TimeUnit.MILLISECONDS) {
45+
val timeline = schedule(timeMillis) {
4646
block.run()
4747
}
4848
return DisposableHandle { timeline.stop() }
4949
}
5050

51-
private fun schedule(time: Long, unit: TimeUnit, handler: EventHandler<ActionEvent>): Timeline =
52-
Timeline(KeyFrame(Duration.millis(unit.toMillis(time).toDouble()), handler)).apply { play() }
51+
private fun schedule(timeMillis: Long, handler: EventHandler<ActionEvent>): Timeline =
52+
Timeline(KeyFrame(Duration.millis(timeMillis.toDouble()), handler)).apply { play() }
5353
}
5454

5555
internal class JavaFxDispatcherFactory : MainDispatcherFactory {
@@ -97,7 +97,7 @@ public suspend fun awaitPulse(): Long = suspendCancellableCoroutine { cont ->
9797
}
9898

9999
private class PulseTimer : AnimationTimer() {
100-
val next = CopyOnWriteArrayList<CancellableContinuation<Long>>()
100+
private val next = CopyOnWriteArrayList<CancellableContinuation<Long>>()
101101

102102
override fun handle(now: Long) {
103103
val cur = next.toTypedArray()
@@ -116,6 +116,7 @@ internal fun initPlatform(): Boolean = PlatformInitializer.success
116116

117117
// Lazily try to initialize JavaFx platform just once
118118
private object PlatformInitializer {
119+
@JvmField
119120
val success = run {
120121
/*
121122
* Try to instantiate JavaFx platform in a way which works

ui/kotlinx-coroutines-swing/src/SwingDispatcher.kt

+6-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package kotlinx.coroutines.swing
77
import kotlinx.coroutines.*
88
import kotlinx.coroutines.internal.*
99
import java.awt.event.*
10-
import java.util.concurrent.*
1110
import javax.swing.*
1211
import kotlin.coroutines.*
1312

@@ -29,26 +28,22 @@ public sealed class SwingDispatcher : MainCoroutineDispatcher(), Delay {
2928

3029
/** @suppress */
3130
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
32-
val timer = schedule(timeMillis, TimeUnit.MILLISECONDS, ActionListener {
31+
val timer = schedule(timeMillis) {
3332
with(continuation) { resumeUndispatched(Unit) }
34-
})
33+
}
3534
continuation.invokeOnCancellation { timer.stop() }
3635
}
3736

3837
/** @suppress */
3938
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
40-
val timer = schedule(timeMillis, TimeUnit.MILLISECONDS, ActionListener {
39+
val timer = schedule(timeMillis) {
4140
block.run()
42-
})
43-
return object : DisposableHandle {
44-
override fun dispose() {
45-
timer.stop()
46-
}
4741
}
42+
return DisposableHandle { timer.stop() }
4843
}
4944

50-
private fun schedule(time: Long, unit: TimeUnit, action: ActionListener): Timer =
51-
Timer(unit.toMillis(time).coerceAtMost(Int.MAX_VALUE.toLong()).toInt(), action).apply {
45+
private fun schedule(timeMillis: Long, action: ActionListener): Timer =
46+
Timer(timeMillis.coerceAtMost(Int.MAX_VALUE.toLong()).toInt(), action).apply {
5247
isRepeats = false
5348
start()
5449
}

0 commit comments

Comments
 (0)