kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

JS Dispatcher yieldEvery 16 configurable

Open mvanassche opened this issue 2 years ago • 4 comments

The Default dispatcher in JS yields every 16 message, but it is not configurable easily. This is an issue, because the impact in a browser can be dramatic when a large number of coroutines are manipulating the DOM, as the browser will repaint every 16 messages.

16 is arbitrary, it is not a good or bad number as such, but since it can have a significant impact on performance, on user-experience, this should be configurable, no?

I suppose we can provide a custom Dispatcher, but the existing ones are fine, but it is internal/private.

mvanassche avatar Aug 22 '23 15:08 mvanassche

Actually, what I probably need is a different kind of queue, based on time: yield back to "JS macrotask event loop" minimum after a certain duration.

mvanassche avatar Aug 22 '23 16:08 mvanassche

I created a specific dispatcher that is time based, and allows to yield back to the browser after so many milliseconds (for example after 10ms, then 100ms then 500ms)

class WindowDispatcher(window: Window, yieldEveryMilliseconds: List<Int>) : CoroutineDispatcher() {
    private val queue = WindowMessageQueue(window, yieldEveryMilliseconds)

    override fun dispatch(context: CoroutineContext, block: Runnable) = queue.enqueue(block)
}

private class WindowMessageQueue(private val window: Window, yieldEveryMilliseconds: List<Int>) : MessageQueue(yieldEveryMilliseconds) {
    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, "*")
    }
}
// yield to JS macrotask event loop after certain duration, which can vary
internal abstract class MessageQueue(val yieldEveryMilliseconds: List<Int>) : MutableList<Runnable> by ArrayDeque() {
    var lastYield = Date.now()
    var yieldEveryIndex = -1
    private var scheduled = false

    abstract fun schedule()

    abstract fun reschedule()

    fun enqueue(element: Runnable) {
        add(element)
        if (!scheduled) {
            scheduled = true
            schedule()
        }
    }

    fun process() {
        lastYield = Date.now()
        if(yieldEveryIndex < yieldEveryMilliseconds.lastIndex) yieldEveryIndex++
        val yieldAfter = yieldEveryMilliseconds[yieldEveryIndex]
        try {
            while(Date.now() - lastYield < yieldAfter) {
                val element = removeFirstOrNull()
                if(element != null) {
                    element.run()
                } else {
                    yieldEveryIndex = -1
                    return
                }
            }
        } finally {
            if (isEmpty()) {
                yieldEveryIndex = -1
                scheduled = false
            } else {
                reschedule()
            }
        }
    }
}

mvanassche avatar Aug 23 '23 09:08 mvanassche

For things like timings, it's indeed preferable to introduce your own dispatcher, I believe we cannot introduce a general enough mechanism that fits them all.

16 is arbitrary, it is not a good or bad number as such, but since it can have a significant impact on performance, on user-experience, this should be configurable, no?

It's indeed arbitrary. Changing the hardcoded constant is fine (e.g. based on a versatile benchmarks suite or overall ecosystem observations), but making it configurable is much harder -- because different libraries might have different opinions on what this constant should be. This path is more or less explored with plugguble RxJava default schedulers and we would like to avoid that

qwwdfsad avatar Aug 28 '23 13:08 qwwdfsad

Ok, I understand, thank you.

It would be nice to expose some abstract WindowDispatcher/WindowMessageQueue such that it is easier to implement a custom MessageQueue.

mvanassche avatar Aug 29 '23 09:08 mvanassche