From c73fc0b332147fecb7c1d688af9d8ac1e85caec3 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 5 Aug 2019 13:14:11 +0300 Subject: [PATCH 1/2] Use setTimeout-based dispatcher when process is not available on the target runtime Fixes #1404 --- .../js/src/CoroutineContext.kt | 3 ++ .../js/src/JSDispatcher.kt | 44 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index de02723a81..3390fc1b8c 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -9,6 +9,7 @@ import kotlin.coroutines.* private external val navigator: dynamic private const val UNDEFINED = "undefined" +internal external val process: dynamic internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { // Check if we are running under ReactNative. We have to use NodeDispatcher under it. @@ -24,6 +25,8 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { // 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() + // If process is undefined (e.g. in NativeScript, #1404), use SetTimeout-based dispatcher + jsTypeOf(process) == UNDEFINED -> SetTimeoutDispatcher // Fallback to NodeDispatcher when browser environment is not detected else -> NodeDispatcher } diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index e11377718c..ba29172254 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -7,32 +7,45 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* import org.w3c.dom.* import kotlin.coroutines.* -import kotlin.js.* +import kotlin.js.Promise private const val MAX_DELAY = Int.MAX_VALUE.toLong() private fun delayToInt(timeMillis: Long): Int = timeMillis.coerceIn(0, MAX_DELAY).toInt() -internal object NodeDispatcher : CoroutineDispatcher(), Delay { - override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block) +internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) + return ClearTimeout(handle) + } override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { val handle = setTimeout({ with(continuation) { resumeUndispatched(Unit) } }, delayToInt(timeMillis)) // Actually on cancellation, but clearTimeout is idempotent continuation.invokeOnCancellation(handler = ClearTimeout(handle).asHandler) } +} + +internal object NodeDispatcher : SetTimeoutBasedDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block) +} + +internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) = SetTimeoutMessageQueue.enqueue(block) +} - private class ClearTimeout(private val handle: Int) : CancelHandler(), DisposableHandle { - override fun dispose() { clearTimeout(handle) } - override fun invoke(cause: Throwable?) { dispose() } - override fun toString(): String = "ClearTimeout[$handle]" +private class ClearTimeout(private val handle: Int) : CancelHandler(), DisposableHandle { + + override fun dispose() { + clearTimeout(handle) } - override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { - val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) - return ClearTimeout(handle) + override fun invoke(cause: Throwable?) { + dispose() } + + override fun toString(): String = "ClearTimeout[$handle]" } internal class WindowDispatcher(private val window: Window) : CoroutineDispatcher(), Delay { @@ -86,6 +99,16 @@ private object NodeJsMessageQueue : MessageQueue() { } } +private object SetTimeoutMessageQueue : MessageQueue() { + override fun schedule() = scheduleProcess() + + override fun reschedule() = scheduleProcess() + + private fun scheduleProcess() { + 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. @@ -136,4 +159,3 @@ internal abstract class MessageQueue : ArrayQueue() { // 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 From 13a060d91aed426ab50aff0f9fc6cf80a9f45912 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 5 Aug 2019 16:47:07 +0300 Subject: [PATCH 2/2] Reduce amount of internal classes in js dispatcher --- .../js/src/JSDispatcher.kt | 52 +++++++++--------- .../js/test/SetTimeoutDispatcherTest.kt | 53 +++++++++++++++++++ 2 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt diff --git a/kotlinx-coroutines-core/js/src/JSDispatcher.kt b/kotlinx-coroutines-core/js/src/JSDispatcher.kt index ba29172254..5a85244d4a 100644 --- a/kotlinx-coroutines-core/js/src/JSDispatcher.kt +++ b/kotlinx-coroutines-core/js/src/JSDispatcher.kt @@ -15,6 +15,26 @@ private fun delayToInt(timeMillis: Long): Int = timeMillis.coerceIn(0, MAX_DELAY).toInt() internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { + inner class ScheduledMessageQueue : MessageQueue() { + internal val processQueue: dynamic = { process() } + + override fun schedule() { + scheduleQueueProcessing() + } + + override fun reschedule() { + setTimeout(processQueue, 0) + } + } + + internal val messageQueue = ScheduledMessageQueue() + + abstract fun scheduleQueueProcessing() + + override fun dispatch(context: CoroutineContext, block: Runnable) { + messageQueue.enqueue(block) + } + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { val handle = setTimeout({ block.run() }, delayToInt(timeMillis)) return ClearTimeout(handle) @@ -28,11 +48,15 @@ internal sealed class SetTimeoutBasedDispatcher: CoroutineDispatcher(), Delay { } internal object NodeDispatcher : SetTimeoutBasedDispatcher() { - override fun dispatch(context: CoroutineContext, block: Runnable) = NodeJsMessageQueue.enqueue(block) + override fun scheduleQueueProcessing() { + process.nextTick(messageQueue.processQueue) + } } internal object SetTimeoutDispatcher : SetTimeoutBasedDispatcher() { - override fun dispatch(context: CoroutineContext, block: Runnable) = SetTimeoutMessageQueue.enqueue(block) + override fun scheduleQueueProcessing() { + setTimeout(messageQueue.processQueue, 0) + } } private class ClearTimeout(private val handle: Int) : CancelHandler(), DisposableHandle { @@ -88,27 +112,6 @@ private class WindowMessageQueue(private val window: Window) : MessageQueue() { } } -private object NodeJsMessageQueue : MessageQueue() { - override fun schedule() { - // next tick is even faster than resolve - process.nextTick({ process() }) - } - - override fun reschedule() { - setTimeout({ process() }, 0) - } -} - -private object SetTimeoutMessageQueue : MessageQueue() { - override fun schedule() = scheduleProcess() - - override fun reschedule() = scheduleProcess() - - private fun scheduleProcess() { - 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. @@ -123,9 +126,8 @@ private object SetTimeoutMessageQueue : MessageQueue() { */ internal abstract class MessageQueue : ArrayQueue() { val yieldEvery = 16 // yield to JS macrotask event loop after this many processed messages - private var scheduled = false - + abstract fun schedule() abstract fun reschedule() diff --git a/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt b/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt new file mode 100644 index 0000000000..78700776eb --- /dev/null +++ b/kotlinx-coroutines-core/js/test/SetTimeoutDispatcherTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class SetTimeoutDispatcherTest : TestBase() { + @Test + fun testDispatch() = runTest { + launch(SetTimeoutDispatcher) { + expect(1) + launch { + expect(3) + } + expect(2) + yield() + expect(4) + }.join() + finish(5) + } + + @Test + fun testDelay() = runTest { + withContext(SetTimeoutDispatcher) { + val job = launch(SetTimeoutDispatcher) { + expect(2) + delay(100) + expect(4) + } + expect(1) + yield() // Yield uses microtask, so should be in the same context + expect(3) + job.join() + finish(5) + } + } + + @Test + fun testWithTimeout() = runTest { + withContext(SetTimeoutDispatcher) { + val result = withTimeoutOrNull(10) { + expect(1) + delay(100) + expectUnreached() + 42 + } + assertNull(result) + finish(2) + } + } +}