Skip to content

Commit d648280

Browse files
committed
Amortize the cost of coroutine dispatch using message queue in all JS dispatchers.
Use Promise.resolve and process.nextTick as dispatch mechanism for cold starts
1 parent b37c296 commit d648280

File tree

3 files changed

+59
-26
lines changed

3 files changed

+59
-26
lines changed

kotlinx-coroutines-core/js/src/CoroutineContext.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when {
1616
// For details see https://github.com/Kotlin/kotlinx.coroutines/issues/236
1717
// The check for ReactNative is based on https://github.com/facebook/react-native/commit/3c65e62183ce05893be0822da217cb803b121c61
1818
jsTypeOf(navigator) != UNDEFINED && navigator != null && navigator.product == "ReactNative" ->
19-
NodeDispatcher()
19+
NodeDispatcher
2020
// Check if we are running under jsdom. WindowDispatcher doesn't work under jsdom because it accesses MessageEvent#source.
2121
// It is not implemented in jsdom, see https://github.com/jsdom/jsdom/blob/master/Changelog.md
2222
// "It's missing a few semantics, especially around origins, as well as MessageEvent source."
23-
isJsdom() -> NodeDispatcher()
23+
isJsdom() -> NodeDispatcher
2424
// Check if we are in the browser and must use window.postMessage to avoid setTimeout throttling
2525
jsTypeOf(window) != UNDEFINED && window.asDynamic() != null && jsTypeOf(window.asDynamic().addEventListener) != UNDEFINED ->
2626
window.asCoroutineDispatcher()
2727
// Fallback to NodeDispatcher when browser environment is not detected
28-
else -> NodeDispatcher()
28+
else -> NodeDispatcher
2929
}
3030

3131
private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED &&

kotlinx-coroutines-core/js/src/JSDispatcher.kt

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,17 @@
55
package kotlinx.coroutines
66

77
import kotlinx.coroutines.internal.*
8-
import kotlin.coroutines.*
98
import org.w3c.dom.*
9+
import kotlin.coroutines.*
10+
import kotlin.js.*
1011

1112
private const val MAX_DELAY = Int.MAX_VALUE.toLong()
1213

1314
private fun delayToInt(timeMillis: Long): Int =
1415
timeMillis.coerceIn(0, MAX_DELAY).toInt()
1516

16-
internal class NodeDispatcher : CoroutineDispatcher(), Delay {
17-
override fun dispatch(context: CoroutineContext, block: Runnable) {
18-
setTimeout({ block.run() }, 0)
19-
}
17+
internal object NodeDispatcher : CoroutineDispatcher(), Delay {
18+
override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block)
2019

2120
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
2221
val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
@@ -37,48 +36,77 @@ internal class NodeDispatcher : CoroutineDispatcher(), Delay {
3736
}
3837

3938
internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay {
40-
private val messageName = "dispatchCoroutine"
39+
private val queue = WindowMessageQueue(window)
40+
41+
override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block)
42+
43+
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
44+
window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
45+
}
4146

42-
private val queue = object : MessageQueue() {
43-
override fun schedule() {
44-
window.postMessage(messageName, "*")
47+
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
48+
val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis))
49+
return object : DisposableHandle {
50+
override fun dispose() {
51+
window.clearTimeout(handle)
52+
}
4553
}
4654
}
55+
}
56+
57+
private class WindowMessageQueue(private val window: Window) : MessageQueue() {
58+
private val messageName = "dispatchCoroutine"
4759

4860
init {
4961
window.addEventListener("message", { event: dynamic ->
5062
if (event.source == window && event.data == messageName) {
5163
event.stopPropagation()
52-
queue.process()
64+
process()
5365
}
5466
}, true)
5567
}
5668

57-
override fun dispatch(context: CoroutineContext, block: Runnable) {
58-
queue.enqueue(block)
69+
override fun schedule() {
70+
Promise.resolve(Unit).then({ process() })
5971
}
6072

61-
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
62-
window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
73+
override fun reschedule() {
74+
window.postMessage(messageName, "*")
6375
}
76+
}
6477

65-
override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
66-
val handle = window.setTimeout({ block.run() }, delayToInt(timeMillis))
67-
return object : DisposableHandle {
68-
override fun dispose() {
69-
window.clearTimeout(handle)
70-
}
71-
}
78+
private object NodeJsMessageQueue : MessageQueue() {
79+
override fun schedule() {
80+
// next tick is even faster than resolve
81+
process.nextTick({ process() })
82+
}
83+
84+
override fun reschedule() {
85+
setTimeout({ process() }, 0)
7286
}
7387
}
7488

89+
/**
90+
* An abstraction over JS scheduling mechanism that leverages micro-batching of [dispatch] blocks without
91+
* paying the cost of JS callbacks scheduling on every dispatch.
92+
*
93+
* Queue uses two scheduling mechanisms:
94+
* 1) [schedule] is used to schedule the initial processing of the message queue.
95+
* JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch
96+
* 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop.
97+
* JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks.
98+
*
99+
* Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size.
100+
*/
75101
internal abstract class MessageQueue : ArrayQueue<Runnable>() {
76-
val yieldEvery = 16 // yield to JS event loop after this many processed messages
102+
val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages
77103

78104
private var scheduled = false
79105

80106
abstract fun schedule()
81107

108+
abstract fun reschedule()
109+
82110
fun enqueue(element: Runnable) {
83111
addLast(element)
84112
if (!scheduled) {
@@ -98,7 +126,7 @@ internal abstract class MessageQueue : ArrayQueue<Runnable>() {
98126
if (isEmpty) {
99127
scheduled = false
100128
} else {
101-
schedule()
129+
reschedule()
102130
}
103131
}
104132
}
@@ -108,3 +136,4 @@ internal abstract class MessageQueue : ArrayQueue<Runnable>() {
108136
// using them via "window" (which only works in browser)
109137
private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int
110138
private external fun clearTimeout(handle: Int = definedExternally)
139+
private external val process: dynamic

kotlinx-coroutines-core/js/test/MessageQueueTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class MessageQueueTest {
1515
assertFalse(scheduled)
1616
scheduled = true
1717
}
18+
19+
override fun reschedule() {
20+
schedule()
21+
}
1822
}
1923

2024
inner class Box(val i: Int): Runnable {

0 commit comments

Comments
 (0)