Skip to content

Commit 18aa42b

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/safari
2 parents 419e29e + 7fbe516 commit 18aa42b

File tree

6 files changed

+76
-22
lines changed

6 files changed

+76
-22
lines changed

.github/workflows/ci.yml

+13-9
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ jobs:
2626
strategy:
2727
matrix:
2828
os: [ubuntu-latest, macos-latest]
29-
scala: [2.11.12, 2.12.14, 2.13.6, 3.0.2]
30-
java: [adopt@1.8]
29+
scala: [2.11.12, 2.12.15, 2.13.7, 3.0.2]
30+
java: [temurin@11]
3131
ci: [ciNode, ciFirefox, ciChrome, ciSafari, ciJSDOMNodeJS]
3232
exclude:
3333
- ci: ciSafari
@@ -47,10 +47,12 @@ jobs:
4747
with:
4848
fetch-depth: 0
4949

50-
- name: Setup Java and Scala
51-
uses: olafurpg/setup-scala@v13
50+
- name: Setup Java (temurin@11)
51+
if: matrix.java == 'temurin@11'
52+
uses: actions/setup-java@v2
5253
with:
53-
java-version: ${{ matrix.java }}
54+
distribution: temurin
55+
java-version: 11
5456

5557
- name: Cache sbt
5658
uses: actions/cache@v2
@@ -87,18 +89,20 @@ jobs:
8789
matrix:
8890
os: [ubuntu-latest]
8991
scala: [3.0.2]
90-
java: [adopt@1.8]
92+
java: [temurin@11]
9193
runs-on: ${{ matrix.os }}
9294
steps:
9395
- name: Checkout current branch (full)
9496
uses: actions/checkout@v2
9597
with:
9698
fetch-depth: 0
9799

98-
- name: Setup Java and Scala
99-
uses: olafurpg/setup-scala@v13
100+
- name: Setup Java (temurin@11)
101+
if: matrix.java == 'temurin@11'
102+
uses: actions/setup-java@v2
100103
with:
101-
java-version: ${{ matrix.java }}
104+
distribution: temurin
105+
java-version: 11
102106

103107
- name: Cache sbt
104108
uses: actions/cache@v2

README.md

+56-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ An implementation of `ExecutionContext` in terms of JavaScript's [`setImmediate`
77
## Usage
88

99
```sbt
10-
libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % "0.1.0"
10+
libraryDependencies += "org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0"
1111
```
1212

13-
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.
13+
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 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.
1414

1515
```scala
1616
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits._
@@ -42,18 +42,68 @@ js.timers.setTimeout(100.millis) {
4242
loop()
4343
```
4444

45-
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 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.
45+
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.
4646

4747
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.
4848

49-
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).
49+
### `setTimeout`
5050

51-
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.
51+
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.
52+
53+
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.
54+
55+
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:
56+
57+
```javascript
58+
setTimeout(() => {
59+
setTimeout(() => {
60+
setTimeout(() => {
61+
setTimeout(() => {
62+
setTimeout(() => {
63+
// this one (and all after it) are clamped!
64+
}, 0);
65+
}, 0);
66+
}, 0);
67+
}, 0);
68+
}, 0);
69+
```
70+
71+
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*.
72+
73+
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).
74+
75+
### `setImmediate`
76+
77+
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`.
78+
79+
The `setImmediate` function was first introduced in Node.js, 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.
80+
81+
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 inconsistency across the JavaScript space.
82+
83+
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).
84+
85+
scala-js-macrotask-executor implements *most* of the `setImmediate` polyfill in terms of Scala.js, wrapped up in an `ExecutionContext` interface. The only elements of the polyfill which are *not* implemented are as follows:
86+
87+
- `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.
88+
- 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 environments *either*, and so there is no benefit to implementing this case.
89+
90+
On environments where the polyfill is unsupported, `setTimeout` is still used as a final fallback.
5291

5392
### Performance Notes
5493

94+
Optimal performance is currently available in the following environments:
95+
96+
- [Node.js 0.9.1+](https://Node.js.org/api/timers.html#timers_setimmediate_callback_args)
97+
- [Browsers implementing `window.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#browser_compatibility), including:
98+
- Chrome 1+
99+
- Safari 4+
100+
- Internet Explorer 9+ (including Edge)
101+
- Firefox 3+
102+
- Opera 9.5+
103+
- [Web Workers implementing `MessageChannel`](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel#browser_compatibility)
104+
55105
`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.
56106

57107
`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`.
58108

59-
See [scala-js#4129](https://github.com/scala-js/scala-js/issues/4129) for some background discussion.
109+
See [scala-js#4129](https://github.com/scala-js/scala-js/issues/4129) for additional background discussion.

build.sbt

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import java.util.concurrent.TimeUnit
2828
val MUnitFramework = new TestFramework("munit.Framework")
2929
val MUnitVersion = "0.7.29"
3030

31-
ThisBuild / baseVersion := "0.1"
31+
ThisBuild / baseVersion := "1.0"
3232

3333
ThisBuild / organization := "org.scala-js"
3434
ThisBuild / organizationName := "Scala.js (https://www.scala-js.org/)"
@@ -46,7 +46,7 @@ ThisBuild / scmInfo := Some(
4646

4747
// build and matrix configuration
4848

49-
ThisBuild / crossScalaVersions := Seq("2.11.12", "2.12.14", "2.13.6", "3.0.2")
49+
ThisBuild / crossScalaVersions := Seq("2.11.12", "2.12.15", "2.13.7", "3.0.2")
5050

5151
val PrimaryOS = "ubuntu-latest"
5252
val MacOS = "macos-latest"
@@ -177,7 +177,7 @@ lazy val webworker = project
177177
name := "scala-js-macrotask-executor-webworker",
178178
scalaJSUseMainModuleInitializer := true,
179179
libraryDependencies ++= Seq(
180-
("org.scala-js" %%% "scalajs-dom" % "1.2.0").cross(CrossVersion.for3Use2_13),
180+
"org.scala-js" %%% "scalajs-dom" % "2.0.0",
181181
"org.scalameta" %%% "munit" % MUnitVersion % Test,
182182
),
183183
(Test / test) := (Test / test).dependsOn(Compile / fastOptJS).value,

project/plugins.sbt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1"
22
libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0"
33

4-
addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.22.0")
5-
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0")
4+
addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.23.0")
5+
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1")
66
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0")

webworker/src/main/scala/org/scalajs/macrotaskexecutor/MacrotaskExecutorSuiteRunner.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import munit.MUnitRunner
2020
import org.junit.runner.Description
2121
import org.junit.runner.notification.Failure
2222
import org.junit.runner.notification.RunNotifier
23-
import org.scalajs.dom.webworkers.DedicatedWorkerGlobalScope
23+
import org.scalajs.dom.DedicatedWorkerGlobalScope
2424

2525
import scala.scalajs.js
2626

webworker/src/test/scala/org/scalajs/macrotaskexecutor/WebWorkerMacrotaskSuite.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package org.scalajs.macrotaskexecutor
1818

1919
import munit.FunSuite
2020
import org.scalajs.dom.MessageEvent
21-
import org.scalajs.dom.webworkers.Worker
21+
import org.scalajs.dom.Worker
2222

2323
import scala.concurrent.Promise
2424

0 commit comments

Comments
 (0)