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
diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
index 87ed1f4..de02723 100644
--- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt
+++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt
@@ -16,16 +16,16 @@
// For details see https://github.com/Kotlin/kotlinx.coroutines/issues/236
// The check for ReactNative is based on https://github.com/facebook/react-native/commit/3c65e62183ce05893be0822da217cb803b121c61
jsTypeOf(navigator) != UNDEFINED && navigator != null && navigator.product == "ReactNative" ->
- NodeDispatcher()
+ NodeDispatcher
// Check if we are running under jsdom. WindowDispatcher doesn't work under jsdom because it accesses MessageEvent#source.
// It is not implemented in jsdom, see https://github.com/jsdom/jsdom/blob/master/Changelog.md
// "It's missing a few semantics, especially around origins, as well as MessageEvent source."
- isJsdom() -> NodeDispatcher()
+ isJsdom() -> NodeDispatcher
// Check if we are in the browser and must use window.postMessage to avoid setTimeout throttling
jsTypeOf(window) != UNDEFINED && window.asDynamic() != null && jsTypeOf(window.asDynamic().addEventListener) != UNDEFINED ->
window.asCoroutineDispatcher()
// Fallback to NodeDispatcher when browser environment is not detected
- else -> NodeDispatcher()
+ else -> NodeDispatcher
}
private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED &&
diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
index f2e3b90..e113777 100644
--- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt
+++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt
@@ -5,18 +5,17 @@
package kotlinx.coroutines
import kotlinx.coroutines.internal.*
-import kotlin.coroutines.*
import org.w3c.dom.*
+import kotlin.coroutines.*
+import kotlin.js.*
private const val MAX_DELAY = Int.MAX_VALUE.toLong()
private fun delayToInt(timeMillis: Long): Int =
timeMillis.coerceIn(0, MAX_DELAY).toInt()
-internal class NodeDispatcher : CoroutineDispatcher(), Delay {
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- setTimeout({ block.run() }, 0)
- }
+internal object NodeDispatcher : CoroutineDispatcher(), Delay {
+ override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block)
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
@@ -37,26 +36,9 @@
}
internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay {
- private val messageName = "dispatchCoroutine"
+ private val queue = WindowMessageQueue(window)
- private val queue = object : MessageQueue() {
- override fun schedule() {
- window.postMessage(messageName, "*")
- }
- }
-
- init {
- window.addEventListener("message", { event: dynamic ->
- if (event.source == window && event.data == messageName) {
- event.stopPropagation()
- queue.process()
- }
- }, true)
- }
-
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- queue.enqueue(block)
- }
+ override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block)
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
window.setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis))
@@ -72,13 +54,59 @@
}
}
+private class WindowMessageQueue(private val window: Window) : MessageQueue() {
+ private val messageName = "dispatchCoroutine"
+
+ init {
+ window.addEventListener("message", { event: dynamic ->
+ if (event.source == window && event.data == messageName) {
+ event.stopPropagation()
+ process()
+ }
+ }, true)
+ }
+
+ override fun schedule() {
+ Promise.resolve(Unit).then({ process() })
+ }
+
+ override fun reschedule() {
+ window.postMessage(messageName, "*")
+ }
+}
+
+private object NodeJsMessageQueue : MessageQueue() {
+ override fun schedule() {
+ // next tick is even faster than resolve
+ process.nextTick({ process() })
+ }
+
+ override fun reschedule() {
+ setTimeout({ process() }, 0)
+ }
+}
+
+/**
+ * An abstraction over JS scheduling mechanism that leverages micro-batching of [dispatch] blocks without
+ * paying the cost of JS callbacks scheduling on every dispatch.
+ *
+ * Queue uses two scheduling mechanisms:
+ * 1) [schedule] is used to schedule the initial processing of the message queue.
+ * JS engine-specific microtask mechanism is used in order to boost performance on short runs and a dispatch batch
+ * 2) [reschedule] is used to schedule processing of the queue after yield to the JS event loop.
+ * JS engine-specific macrotask mechanism is used not to starve animations and non-coroutines macrotasks.
+ *
+ * Yet there could be a long tail of "slow" reschedules, but it should be amortized by the queue size.
+ */
internal abstract class MessageQueue : ArrayQueue<Runnable>() {
- val yieldEvery = 16 // yield to JS event loop after this many processed messages
+ val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages
private var scheduled = false
abstract fun schedule()
+ abstract fun reschedule()
+
fun enqueue(element: Runnable) {
addLast(element)
if (!scheduled) {
@@ -98,7 +126,7 @@
if (isEmpty) {
scheduled = false
} else {
- schedule()
+ reschedule()
}
}
}
@@ -108,3 +136,4 @@
// using them via "window" (which only works in browser)
private external fun setTimeout(handler: dynamic, timeout: Int = definedExternally): Int
private external fun clearTimeout(handle: Int = definedExternally)
+private external val process: dynamic
diff --git a/kotlinx-coroutines-core/js/test/MessageQueueTest.kt b/kotlinx-coroutines-core/js/test/MessageQueueTest.kt
index 4943f74..de514c7 100644
--- a/kotlinx-coroutines-core/js/test/MessageQueueTest.kt
+++ b/kotlinx-coroutines-core/js/test/MessageQueueTest.kt
@@ -15,6 +15,10 @@
assertFalse(scheduled)
scheduled = true
}
+
+ override fun reschedule() {
+ schedule()
+ }
}
inner class Box(val i: Int): Runnable {