Skip to content

Node 26 and enable Scala.js stress tests#178

Merged
natsukagami merged 3 commits into
lampepfl:mainfrom
tanishiking:node26
May 7, 2026
Merged

Node 26 and enable Scala.js stress tests#178
natsukagami merged 3 commits into
lampepfl:mainfrom
tanishiking:node26

Conversation

@tanishiking
Copy link
Copy Markdown
Contributor

@tanishiking tanishiking commented May 6, 2026

Update to Node 26 that updated to 14.6.202.33, that fixed V8 issue discussed in #165.

edit: now StressTest doesn't fail with stack overflow, but it is still extremely slow on V8 🤔 -> It looks like loop over Long generates NumericRange[Long] that takes so long. Using Integer instead, and now tests finishes approximately 6min. maybe worth investigating on scala-js side.

Update to Node 26 that updated to 14.6.202.33, that fixed V8 issue discussed in lampepfl#165.
`1L to total` produce `NumericRange[Long]` instead of an Int `Range`.
`NumericRange[Long]` iterates through boxed values, and that makes
async-heavy loop even slower.
@tanishiking tanishiking marked this pull request as ready for review May 6, 2026 06:48
Copy link
Copy Markdown
Member

@bishabosha bishabosha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

@natsukagami
Copy link
Copy Markdown
Contributor

Looks like there's an OOM error 🤔

@tanishiking
Copy link
Copy Markdown
Contributor Author

tanishiking commented May 7, 2026

Oh, StressTest takes approx 3.5GB memory on my local computer... will take a look what's going on.

Previously, a `WasmAsyncSupport.scheduleBoundary` created
2-nested `js.async` where outer `js.async` does nothing.

`WasmAsyncSupport.scheduleBoundary` ran schelduled
bodies by `execute(() => boundary(body))` inherited from
the implementation from `AsyncSupport`.

`JSAsyncScheduler` implements `execute` using `js.async`:

```scala
object JsAsyncScheduler extends Scheduler:
  def execute(body: Runnable) = js.async(body.run())
```

and, on the other hand, boundary also call `js.async`

```scala
override def boundary[T](body: Label[T] ?=> T): T =
  val label = WasmLabel[T]()
  js.async:
    val r = body(using label)
    label.resolve(r) // see [[WasmLabel]]
  js.await(label.promise)
```

As a result, each `Future` body was started as something like:

```scala
js.async {
  val label = WasmLabel[T]()
  js.async {
    val r = body(using label)
    label.resolve(r)
  }
  js.await(label.promise)
}
```

The outer async block almost do nothing: it only waits until the
inner body first suspends or completes, and then exists.

In stress tests that create thousands of `Future`s, and this
async boundary creates many Promise/JSPI continuations and JS-Wasm
boundary, which makes significant slow-down and memory consumption.

**fix**

This commit fixes the problem by removing the unnecessary outer
`js.async` block. Now `scheduleBoundary` is implemented like:

```scala
val label = WasmLabel[Unit]()
s.execute: () =>
  body(using label)
  label.resolve(())
```

so the `Future` would be

```scala
val label = WasmLabel[Unit]()
js.async {
  body(using label)
  label.resolve(())
}
```

Now, stress test consumes around 900MB of memory (previously it takes
around 3.5GB~).
@tanishiking
Copy link
Copy Markdown
Contributor Author

tanishiking commented May 7, 2026

I found that WasmAsyncSupport.scheduleBoundary created two-nested js.async and outer js.async seemed unnecessary. Removing outer js.async significantly reduce the memory consumption and it makes StressTest much faster on JS :)
c9ab495

* this assumes that the root context is **already** under `js.async`.
*/
trait WasmJSPISuspend(using AsyncToken) extends SuspendSupport:
trait WasmJSPISuspend(using AsyncToken) extends AsyncSupport:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it makes sense, because JSPI anyway supports Async wether you like it or not ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense yeah! No reason to split this in the case of JSPI

@tanishiking
Copy link
Copy Markdown
Contributor Author

Now, c220e96 is negligible on my local environment, maybe we can revert that.

Copy link
Copy Markdown
Contributor

@natsukagami natsukagami left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool!

@natsukagami
Copy link
Copy Markdown
Contributor

Now, c220e96 is negligible on my local environment, maybe we can revert that.

I think we can keep it, it makes sense to use ints for these loops anyway

@natsukagami natsukagami merged commit 84de9cf into lampepfl:main May 7, 2026
4 checks passed
@tanishiking tanishiking deleted the node26 branch May 7, 2026 13:26
@tanishiking
Copy link
Copy Markdown
Contributor Author

Wow, now test-js finishes in 1m30s, which previously took 3m30s~4m 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants