-
Notifications
You must be signed in to change notification settings - Fork 7
Merge CE setImmediate
docs into README
#28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -44,16 +44,66 @@ loop() | |||||
|
||||||
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](https://javascript.info/event-loop). 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 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 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 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 horrible, even by JavaScript standards. | ||||||
|
||||||
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). | ||||||
### `setTimeout` | ||||||
|
||||||
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. | ||||||
This is fixable by using a `setTimeout`-based executor, such as the `QueueExecutionContext.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. Calling `setTimeout` with a delay of `0` would seem to achieve *exactly* 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 is `0`, and there are other complexities which ultimately impose a performance penalty too severe to accept. Any significant application of an `ExecutionContext` backed by `setTimeout`, would be almost unusable. | ||||||
|
||||||
To make matters worse, `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: | ||||||
|
||||||
```javascript | ||||||
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 chaining `Future`s, where each `map`/`flatMap`/`transform`/etc. schedules another `Future` which, in turn will schedule another... etc. etc. This is exactly where we see clamping. In particular, the innermost `setTimeout` in this example will be clamped to 4 milliseconds (meaning there is no difference between `setTimeout(.., 0)` and `setTimeout(.., 4)`), which would slow down execution *even more*. | ||||||
|
||||||
You can read more details [in the MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setTimeout#Minimum.2F_maximum_delay_and_timeout_nesting). | ||||||
|
||||||
### `setImmediate` | ||||||
|
||||||
Fortunately, we aren't the only ones to have this problem. What we *want* is something which uses the macrotask queue (so we play nicely with `setTimeout`, I/O, and other macrotasks), but which doesn't have as much overhead as `setTimeout`. The answer is `setImmediate`. | ||||||
|
||||||
The `setImmediate` function was first introduced in NodeJS, and its purpose is to solve *exactly* this problem: a faster `setTimeout(..., 0)`. In particular, `setImmediate(...)` is *semantically* equivalent to `setTimeout(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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There are more of those in the page. |
||||||
|
||||||
Unfortunately, `setImmediate` isn't available on every platform. For reasons of... their own, Mozilla, Google, and Apple have all strenuously objected to the inclusion of `setImmediate` in the W3C standard set, despite the proposal (which originated at Microsoft) and obvious usefulness. This in turn has resulted in an all-too familiar patchwork of inconsistency across the JavaScript space. | ||||||
armanbilge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
That's the bad news. The good news is that all modern browsers include *some* sort of functionality which can be exploited to emulate `setImmediate` with similar performance characteristics. In particular, *most* environments take advantage of `postMessage` in some way. If you're interested in the nitty-gritty details of how this works, you are referred to [this excellent readme](https://github.com/YuzuJS/setImmediate#the-tricks). | ||||||
|
||||||
scala-js-macrotask-executor implements *most* of the `setImmediate` polyfill in terms of ScalaJS, wrapped up in an `ExecutionContext` interface. The only elements of the polyfill which are *not* implemented are as follows: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There are more of those in the page. |
||||||
|
||||||
- `process.nextTick` is used by the JavaScript polyfill when running on NodeJS versions below 0.9. However, ScalaJS itself does not support NodeJS 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, ScalaJS does not support these environments *either*, 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. | ||||||
|
||||||
### Performance Notes | ||||||
|
||||||
Optimal performance is currently available in the following environments: | ||||||
|
||||||
- [NodeJS 0.9.1+](https://nodejs.org/api/timers.html#timers_setimmediate_callback_args) | ||||||
- [Browsers implementing `window.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#browser_compatibility), including: | ||||||
- Chrome 1+ | ||||||
- Safari 4+ | ||||||
- Internet Explorer 9+ (including Edge) | ||||||
- Firefox 3+ | ||||||
- Opera 9.5+ | ||||||
- [Web Workers implementing `MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel#browser_compatibility) | ||||||
|
||||||
`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. | ||||||
See [scala-js#4129](https://github.com/scala-js/scala-js/issues/4129) for additional background discussion. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we avoid being condescending at JavaScript, please? Scala.js doesn't make that kind of judgment calls. I don't think this new sentence brings any positive value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, I knew better but I shoe-horned this in anyway. Besides adjusting for the new audience I forgot to adjust for the new venue.
I suppose line 81 below should be dropped/reworked too? Anything else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, perhaps remove "an all-too familiar patchwork of" from line 81. The sentence should otherwise stand on its own.
Other than that I think it's fine.