- Notifications
You must be signed in to change notification settings - Fork7
License
scala-js/scala-js-macrotask-executor
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
An implementation ofExecutionContext
in terms of JavaScript'ssetImmediate
. 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 excellentYuzuJS/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 optimalExecutionContext
implementation for use in any Scala.js project. If you're usingExecutionContext
andnot using this project, you likely have some serious bugs and/or performance issues waiting to be discovered.
libraryDependencies+="org.scala-js"%%%"scala-js-macrotask-executor"%"1.1.1"
Published for Scala 2.11, 2.12, 2.13, 3. Functionality is fully supported on all platforms supported by Scala.js (including web workers). In the event that a given platform doesnot have the necessary functionality to implementsetImmediate
-style yielding (usuallypostMessage
is what is required), the implementation will transparently fall back to usingsetTimeout
, which will drastically inhibit performance but remain otherwise functional.
importorg.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
You can also simply importMacrotaskExecutor
if using theExecutionContext
directly.
Once imported, this executor functions exactly the same asExecutionContext.global
, except it does not suffer from the various limitations of aPromise
- orsetTimeout
-based implementation. In other words, you can useFuture
(and otherExecutionContext
-based tooling) effectively exactly as you would on the JVM, and it will behave effectively identically modulo the single-threaded nature of the runtime.
The original motivation for this functionality comes from the following case (written here in terms ofFuture
, but originally discovered in terms ofIO
in theCats Effect project):
varcancel=falsedefloop():Future[Unit]=Future(cancel) flatMap { canceled=>if (canceled)Future.unitelse loop() }js.timers.setTimeout(100.millis) { cancel=true}loop()
Theloop()
future will run forever when using the default Scala.js executor, which is written in terms of JavaScript'sPromise
. Thereason this will run forever stems from the fact that JavaScript includes two separate work queues: themicrotask and the macrotask queue. The microtask queue is used exclusively byPromise
, while the macrotask queue is used by everything else, including UI rendering,setTimeout
, and I/O such as Fetch 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 aPromise
-based executor: the microtask queue isnever 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 asetTimeout
-based executor, such as theQueueExecutionContext.timeouts()
implementation in Scala.js. Available in all browsers since the dawn of time,setTimeout
takes two arguments: a time delay and a callback to invoke. The callback is invoked by the event loop once the time delay expires, and this is implemented by pushing the callback onto the back of the event queue at the appropriate time. CallingsetTimeout
with a delay of0
would seem to achieveexactly the semantics we want: yield back to the event loop and allow it to resume our callback when it's our turn once again.
Unfortunately,setTimeout
is slow. Very, very, very slow. The timing mechanism imposes quite a bit of overhead, even when the delay is0
, and there are other complexities which ultimately impose a performance penalty too severe to accept. Any significant application of anExecutionContext
backed bysetTimeout
, would be almost unusable.
To make matters worse,setTimeout
isclamped 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:
setTimeout(()=>{setTimeout(()=>{setTimeout(()=>{setTimeout(()=>{setTimeout(()=>{// this one (and all after it) are clamped!},0);},0);},0);},0);},0);
Each timeout sets a new timeout, and so on and so on. This is exactly the sort of situation that we get into when chainingFuture
s, where eachmap
/flatMap
/transform
/etc. schedules anotherFuture
which, in turn will schedule another... etc. etc. This is exactly where we see clamping. In particular, the innermostsetTimeout
in this example will be clamped to 4 milliseconds (meaning there is no difference betweensetTimeout(.., 0)
andsetTimeout(.., 4)
), which would slow down executioneven more.
You can read more detailsin the MDN documentation.
Fortunately, we aren't the only ones to have this problem. What wewant is something which uses the macrotask queue (so we play nicely withsetTimeout
, I/O, and other macrotasks), but which doesn't have as much overhead assetTimeout
. The answer issetImmediate
.
ThesetImmediate
function was first introduced in Node.js, and its purpose is to solveexactly this problem: a fastersetTimeout(..., 0)
. In particular,setImmediate(...)
issemantically equivalent tosetTimeout(0, ...)
, except without the associated clamping: it doesn't include a delay mechanism of any sort, it simply takes a callback and immediately submits it to the event loop, which in turn will run the callback as soon as its turn comes up.
Unfortunately,setImmediate
isn't available on every platform. For reasons of... their own, Mozilla, Google, and Apple have all strenuously objected to the inclusion ofsetImmediate
in the W3C standard set, despite the proposal (which originated at Microsoft) and obvious usefulness. This in turn has resulted in inconsistency across the JavaScript space.
That's the bad news. The good news is that all modern browsers includesome sort of functionality which can be exploited to emulatesetImmediate
with similar performance characteristics. In particular,most environments take advantage ofpostMessage
in some way. If you're interested in the nitty-gritty details of how this works, you are referred tothis excellent readme.
scala-js-macrotask-executor implementsmost of thesetImmediate
polyfill in terms of Scala.js, wrapped up in anExecutionContext
interface. The only elements of the polyfill which arenot implemented are as follows:
process.nextTick
is used by the JavaScript polyfill when running on Node.js versions below 0.9. However, Scala.js itself does not support Node.js 0.9 or below, so there's really no point in supporting this case.- Similarly, older versions of IE (6 through 8, specifically) allow a particular exploitation of the
onreadystatechange
event fired when a<script>
element is inserted into the DOM. However, Scala.js does not support these environmentseither, and so there is no benefit to implementing this case.
On environments where the polyfill is unsupported,setTimeout
is still used as a final fallback.
Optimal performance is currently available in the following environments:
- Node.js 0.9.1+
- Browsers implementing
window.postMessage()
, including:- Chrome 1+
- Safari 4+
- Internet Explorer 9+ (including Edge)
- Firefox 3+
- Opera 9.5+
- Web Workers implementing
MessageChannel
setImmediate
in practice seems to be somewhat slower thanPromise.then()
, particularly on Chrome. However, sincePromise
also has seriously detrimental effects (such as blocking UI rendering), it doesn't seem to be a particularly fair comparison.Promise
is alsoslower thansetImmediate
on Firefox for very unclear reasons likely having to do with fairness issues in the Gecko engine itself.
setImmediate
isdramatically faster thansetTimeout
, mostly due to clamping but also becausesetTimeout
has other sources of overhead. In particular, executing 10,000 sequential tasks takes about 30 seconds withsetTimeout
and about 400milliseconds usingsetImmediate
.
Seescala-js#4129 for additional background discussion.
About
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors6
Uh oh!
There was an error while loading.Please reload this page.