From 6df86e1a98e954951ac64bb4ece828bdd0a672a7 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Mon, 6 Sep 2021 14:31:48 -0600 Subject: [PATCH 1/3] Added some prose to explain what this is --- README.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7861db3..f7d8e85 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # scala-js-macrotask-executor -See [scala-js#4129](https://github.com/scala-js/scala-js/issues/4129) for discussion. +An implementation of `ExecutionContext` in terms of JavaScript's [`setImmediate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate). Unfortunately for everyone involved, `setImmediate` is only available on Edge and NodeJS, meaning that this functionality must be polyfilled on all other environments. The details of this polyfill can be found in the readme of the excellent [YuzuJS/setImmediate](https://github.com/YuzuJS/setImmediate) project, though the implementation here is in terms of ScalaJS primitives rather than raw JavaScript. + +**Unless you have some very, very specific and unusual requirements, this is the optimal `ExecutionContext` implementation for use in any ScalaJS project.** If you're using `ExecutionContext` and *not* using this project, you likely have some serious bugs and/or performance issues waiting to be discovered. ## Usage @@ -8,8 +10,50 @@ See [scala-js#4129](https://github.com/scala-js/scala-js/issues/4129) for discus libraryDependencies += "org.scala-js" %% "scala-js-macrotask-executor" % "" ``` -Published for Scala 2.13.6. +Published for Scala 2.12.14, 2.13.6, 3.0.1. Functionality is fully supported on all platforms supported by ScalaJS (including web workers). In the event that a given platform does *not* have the necessary functionality to implement `setImmediate`-style yielding (usually `postMessage` is what is required), the implementation will transparently fall back to using `setTimeout`, which will drastically inhibit performance but remain otherwise functional. ```scala -import org.scalajs.macrotaskexecutor._ +import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._ ``` + +You can also simply import `MacrotaskExecutor` if using the `ExecutionContext` directly. + +Once imported, this executor functions exactly the same as `ExecutionContext.global`, except it does not suffer from the various limitations of a `Promise`- or `setTimeout`-based implementation. In other words, you can use `Future` (and other `ExecutionContext`-based tooling) effectively exactly as you would on the JVM, and it will behave effectively identically modulo the single-threaded nature of the runtime. + +## Background + +The original motivation for this functionality comes from the following case (written here in terms of `Future`, but originally discovered in terms of `IO` in the [Cats Effect project](https://github.com/typelevel/cats-effect)): + +```scala +var cancel = false + +def loop(): Future[Unit] = + Future(cancel) flatMap { canceled => + if (canceled) + Future.unit + else + loop() + } + +js.timers.setTimeout(100.millis) { + cancel = true +} + +loop() +``` + +The `loop()` future will run forever when using the default ScalaJS executor, which is written in terms of JavaScript's `Promise`. The *reason* this will run forever stems from the fact that JavaScript includes two separate work queues: the microtask and the macrotask queue. The microtask queue is used exclusively by `Promise`, while the macrotask queue is used by everything else, including UI rendering, `setTimeout`, and I/O such as XHR or NodeJS things. The semantics are such that, whenever the microtask queue has work, it takes full precedence over the macrotask queue until the microtask queue is completely exhausted. + +This explains why the above snippet will run forever on a `Promise`-based executor: the microtask queue is *never* empty because we're constantly adding new tasks! Thus, `setTimeout` is never able to run because the macrotask queue never receives control. + +This is fixable by using a `setTimeout`-based executor, such as the `QueueExecutionContext.timeouts()` implementation in ScalaJS. Unfortunately, this runs into an even more serious issue: `setTimeout` is *clamped* in all JavaScript environments. In particular, it is clamped to a minimum of 4ms and, in practice, usually somewhere between 4ms and 10ms. This clamping kicks in whenever more than 5 consecutive timeouts have been scheduled. You can read more details [in the MDM documentation](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setTimeout#Minimum.2F_maximum_delay_and_timeout_nesting). + +The only solution to this mess is to yield to the macrotask queue *without* using `setTimeout`. This is precisely what `setImmediate` does on Edge and NodeJS. In particular, `setImmediate(...)` is *semantically* equivalent to `setTimeout(0, ...)`, except without the associated clamping. Unfortunately, due to the fact that only a pair of platforms support this function, alternative implementations are required across other major browsers. In particular, *most* environments take advantage of `postMessage` in some way. + +### Performance Notes + +`setImmediate` in practice seems to be somewhat slower than `Promise.then()`, particularly on Chrome. However, since `Promise` also has seriously detrimental effects (such as blocking UI rendering), it doesn't seem to be a particularly fair comparison. `Promise` is also *slower* than `setImmediate` on Firefox for very unclear reasons likely having to do with fairness issues in the Gecko engine itself. + +`setImmediate` is *dramatically* faster than `setTimeout`, mostly due to clamping but also because `setTimeout` has other sources of overhead. In particular, executing 10,000 sequential tasks takes about 30 seconds with `setTimeout` and about 400 *milliseconds* using `setImmediate`. + +See [scala-js#4129](https://github.com/scala-js/scala-js/issues/4129) for some background discussion. From 6b12ebf0968bf4c0d3a273352ba1f1386fe7a4fa Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Mon, 6 Sep 2021 15:51:04 -0600 Subject: [PATCH 2/3] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sébastien Doeraene --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7d8e85..86054d8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # scala-js-macrotask-executor -An implementation of `ExecutionContext` in terms of JavaScript's [`setImmediate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate). Unfortunately for everyone involved, `setImmediate` is only available on Edge and NodeJS, meaning that this functionality must be polyfilled on all other environments. The details of this polyfill can be found in the readme of the excellent [YuzuJS/setImmediate](https://github.com/YuzuJS/setImmediate) project, though the implementation here is in terms of ScalaJS primitives rather than raw JavaScript. +An implementation of `ExecutionContext` in terms of JavaScript's [`setImmediate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate). Unfortunately for everyone involved, `setImmediate` is only available on Edge and Node.js, meaning that this functionality must be polyfilled on all other environments. The details of this polyfill can be found in the readme of the excellent [YuzuJS/setImmediate](https://github.com/YuzuJS/setImmediate) project, though the implementation here is in terms of Scala.js primitives rather than raw JavaScript. **Unless you have some very, very specific and unusual requirements, this is the optimal `ExecutionContext` implementation for use in any ScalaJS project.** If you're using `ExecutionContext` and *not* using this project, you likely have some serious bugs and/or performance issues waiting to be discovered. From 62afe8ace2b8ecededed197745c5512f3a8bc206 Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Mon, 6 Sep 2021 15:51:50 -0600 Subject: [PATCH 3/3] Corrected the other spellings --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 86054d8..bba45ec 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An implementation of `ExecutionContext` in terms of JavaScript's [`setImmediate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate). Unfortunately for everyone involved, `setImmediate` is only available on Edge and Node.js, meaning that this functionality must be polyfilled on all other environments. The details of this polyfill can be found in the readme of the excellent [YuzuJS/setImmediate](https://github.com/YuzuJS/setImmediate) project, though the implementation here is in terms of Scala.js primitives rather than raw JavaScript. -**Unless you have some very, very specific and unusual requirements, this is the optimal `ExecutionContext` implementation for use in any ScalaJS project.** If you're using `ExecutionContext` and *not* using this project, you likely have some serious bugs and/or performance issues waiting to be discovered. +**Unless you have some very, very specific and unusual requirements, this is the optimal `ExecutionContext` implementation for use in any Scala.js project.** If you're using `ExecutionContext` and *not* using this project, you likely have some serious bugs and/or performance issues waiting to be discovered. ## Usage @@ -10,7 +10,7 @@ An implementation of `ExecutionContext` in terms of JavaScript's [`setImmediate` libraryDependencies += "org.scala-js" %% "scala-js-macrotask-executor" % "" ``` -Published for Scala 2.12.14, 2.13.6, 3.0.1. Functionality is fully supported on all platforms supported by ScalaJS (including web workers). In the event that a given platform does *not* have the necessary functionality to implement `setImmediate`-style yielding (usually `postMessage` is what is required), the implementation will transparently fall back to using `setTimeout`, which will drastically inhibit performance but remain otherwise functional. +Published for Scala 2.12.14, 2.13.6, 3.0.1. Functionality is fully supported on all platforms supported by Scala.js (including web workers). In the event that a given platform does *not* have the necessary functionality to implement `setImmediate`-style yielding (usually `postMessage` is what is required), the implementation will transparently fall back to using `setTimeout`, which will drastically inhibit performance but remain otherwise functional. ```scala import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._ @@ -42,13 +42,13 @@ js.timers.setTimeout(100.millis) { loop() ``` -The `loop()` future will run forever when using the default ScalaJS executor, which is written in terms of JavaScript's `Promise`. The *reason* this will run forever stems from the fact that JavaScript includes two separate work queues: the microtask and the macrotask queue. The microtask queue is used exclusively by `Promise`, while the macrotask queue is used by everything else, including UI rendering, `setTimeout`, and I/O such as XHR or NodeJS things. The semantics are such that, whenever the microtask queue has work, it takes full precedence over the macrotask queue until the microtask queue is completely exhausted. +The `loop()` future will run forever when using the default Scala.js executor, which is written in terms of JavaScript's `Promise`. The *reason* this will run forever stems from the fact that JavaScript includes two separate work queues: the microtask and the macrotask queue. The microtask queue is used exclusively by `Promise`, while the macrotask queue is used by everything else, including UI rendering, `setTimeout`, and I/O such as XHR or Node.js things. The semantics are such that, whenever the microtask queue has work, it takes full precedence over the macrotask queue until the microtask queue is completely exhausted. This explains why the above snippet will run forever on a `Promise`-based executor: the microtask queue is *never* empty because we're constantly adding new tasks! Thus, `setTimeout` is never able to run because the macrotask queue never receives control. -This is fixable by using a `setTimeout`-based executor, such as the `QueueExecutionContext.timeouts()` implementation in ScalaJS. Unfortunately, this runs into an even more serious issue: `setTimeout` is *clamped* in all JavaScript environments. In particular, it is clamped to a minimum of 4ms and, in practice, usually somewhere between 4ms and 10ms. This clamping kicks in whenever more than 5 consecutive timeouts have been scheduled. You can read more details [in the MDM documentation](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setTimeout#Minimum.2F_maximum_delay_and_timeout_nesting). +This is fixable by using a `setTimeout`-based executor, such as the `QueueExecutionContext.timeouts()` implementation in Scala.js. Unfortunately, this runs into an even more serious issue: `setTimeout` is *clamped* in all JavaScript environments. In particular, it is clamped to a minimum of 4ms and, in practice, usually somewhere between 4ms and 10ms. This clamping kicks in whenever more than 5 consecutive timeouts have been scheduled. You can read more details [in the MDM documentation](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setTimeout#Minimum.2F_maximum_delay_and_timeout_nesting). -The only solution to this mess is to yield to the macrotask queue *without* using `setTimeout`. This is precisely what `setImmediate` does on Edge and NodeJS. In particular, `setImmediate(...)` is *semantically* equivalent to `setTimeout(0, ...)`, except without the associated clamping. Unfortunately, due to the fact that only a pair of platforms support this function, alternative implementations are required across other major browsers. In particular, *most* environments take advantage of `postMessage` in some way. +The only solution to this mess is to yield to the macrotask queue *without* using `setTimeout`. This is precisely what `setImmediate` does on Edge and Node.js. In particular, `setImmediate(...)` is *semantically* equivalent to `setTimeout(0, ...)`, except without the associated clamping. Unfortunately, due to the fact that only a pair of platforms support this function, alternative implementations are required across other major browsers. In particular, *most* environments take advantage of `postMessage` in some way. ### Performance Notes