standards-positions icon indicating copy to clipboard operation
standards-positions copied to clipboard

JavaScript Promise Integration (JSPI)

Open fgmccabe opened this issue 1 year ago • 8 comments

WebKittens

No response

Title of the proposal

JavaScript Promise Integration Proposal

URL to the spec

https://github.com/WebAssembly/js-promise-integration/blob/main/document/js-api/index.bs

URL to the spec's repository

https://github.com/WebAssembly/js-promise-integration

Issue Tracker URL

No response

Explainer URL

https://github.com/WebAssembly/js-promise-integration/blob/main/proposals/js-promise-integration/Overview.md

TAG Design Review URL

https://github.com/w3ctag/design-reviews/issues/809

Mozilla standards-positions issue URL

https://mozilla.github.io/standards-positions/#wasm-js-promise-integration

WebKit Bugzilla URL

No response

Radar URL

No response

Description

This is an integration API between WebAssembly and JavaScript. It is following the W3C WebAssembly standards process; and is currently (10/29/2024) at phase 3: we expect to be able to transition to phase 4 in the near future.

JSPI allows a WebAssembly application that issues calls to sequential APIs to access asynchronous Promise-bearing functions. For example, in the scenario where a C program that has been written using a Posix read function the actual implementation of read will be via a WebAssembly import. However, most WebAPIs are asynchronous in nature: returning a Promise when invoked and expecting the application to attach a callback to the Promise. This is not straightforward to realize when the C original source simply expects the read to complete before continuing.

JSPI allows the C program to use the read as normal; it functions by intercepting the Promise and suspending the entire C application until the Promise is fulfilled. At which point the C code is resumed with the results of the read operation.

This represents an important capability for WebAssembly code that is based on legacy APIs: it allows the code to access the asynchronous functionality in a sequential way. In particular, this allows the code to run in the main thread without causing it to block (even though the WebAssembly code is blocked: the main thread will not be).

Apart from the core application of accessing asynchronous APIs from WebAssembly, other important functionalities enabled by JSPI include dynamic loading of WebAssembly modules (again from the main thread), dynamic access to resources (via the fetch API) and so-called code-splitting (where a module is split into smaller segments that are loaded separately and patched together at run-time).

JSPI should not be confused with parallelism or threading. An application using JSPI continues to be single threaded.

JSPI does not alter the semantics of either WebAssembly or JavaScript. In particular, JSPI does not permit JS programs to be suspended via WebAssembly. In addition, because the entire WebAssembly call is suspended, it is not possible for a WebAssembly program to respond internally when being suspended. (This is the subject of a separate activity within the W3C Wasm community.)

fgmccabe avatar Oct 29 '24 22:10 fgmccabe

@fgmccabe could you proved a link to a human-readable formatted copy of the spec, if there is one? The link provided seems to be the raw source.

https://github.com/WebAssembly/js-promise-integration/blob/main/document/js-api/index.bs

othermaciej avatar Nov 02 '24 21:11 othermaciej

Apologies. This is a more readable link:

https://webassembly.github.io/js-promise-integration/js-api/#jspi

It's a permalink: it will always reflect the current state of the JSPI spec.

fgmccabe avatar Nov 04 '24 17:11 fgmccabe

Here's a relevant WebKit bug requesting JSPI support: https://bugs.webkit.org/show_bug.cgi?id=283054

brandonpayton avatar Dec 18 '24 00:12 brandonpayton

Our status is currently against JSPI.

Our main concern is not with JSPI by itself it's that we're unconvinced that side stacks are the way core stack switching should go. If we decide that side stacks are not the way we want to go then we've locked ourselves into supporting side stacks forever anyway by shipping JSPI. In particular, we think the current side stack based core stack switching proposals have a few problems:

  1. They don't interface well with JS hosts, which as a web browser is the main host we care about. In particular, side-stacks are not suspendable with interleaved JS so you'd have to create a separate side stack for each wasm -> async JS -> wasm bridge.
  2. Side-stacks cannot efficiently represent stack-less coroutines (e.g. how async functions are implemented in JS/Swift/C++/Rust/Python/etc). Thus those languages have the same code size issues that lead to JSPI in the first place.
  3. Side-stacks probably cannot move between web Workers under future security plans we have without doing a memcpy, effectively. Our opinion is that this is prohibitively expensive.

We'd like to see an async functions proposal along the lines of what JS does. I have an outline of that in my head, which I'll hopefully be able to share soon.

Given that, I put together a PoC JSPI as a library https://github.com/kmiller68/JSPI-as-library, which I think should stopgap a lot of the uses for JSPI in the interim while the direction of the core-stack proposal is decided. The performance of that library for the example from V8's blog post is identical to JSPI and could probably be sped up by only Atomics.notifying when the other thread is actually asleep.

kmiller68 avatar Feb 10 '25 14:02 kmiller68

JSPI stands on its own

Contrary to

Our main concern is not with JSPI by itself it's that we're unconvinced that side stacks are the way core stack switching should go.

we believe that JSPI has independent justification and deserves to be considered independently of core stack switching.

In particular, JSPI enables applications built with a substantial amount of legacy code – written in languages like C/C++ – that were written assuming largely synchronous API (e.g. Posix file read) – to participate in an environment where most APIs are asynchronous (e.g. fetch). Notably, JSPI alone is enough to address this use case; core stack switching is not necessary, and we believe JSPI would be well-motivated even if core stack switching were never shipped.

This has been borne out in practice, with over 200 registrants to the JSPI origin trial. Highlighted are registrants who wish to access WebGPU APIs (which, unlike WebGL, are all asynchronous). As a late stage Phase 3 proposal, JSPI has already gotten significant positive feedback both in terms of usability and performance from application developers that find it immediately useful.

To address the other particular concerns raised:

They don't interface well with JS hosts, which as a web browser is the main host we care about. In particular, side-stacks are not suspendable with interleaved JS so you'd have to create a separate side stack for each wasm -> async JS -> wasm bridge.

The design of JSPI does, indeed, require a separate stack for each entry via the async export wrapper. Furthermore, we do not permit a suspended stack to contain JS frames (or host frames). This was chosen for a few separate reasons:

  • We wanted to ‘derisk’ JSPI as much as possible. This included not permitting JS computations to be suspended except by existing async/await mechanisms. Not permitting JS suspension represents a key policy choice. By returning a Promise to JS frames, JSPI integrates perfectly with existing async/await mechanisms in JS.

  • An earlier design did permit the suspension of a JSPI call stack containing multiple secondary stacks (assuming that no JS sandwiched in between). However, we arrived at the current design based on feedback from toolchain providers and users. Suspending a single JSPI stack at a time is simpler to tool for and accounts for the vast majority of the usages encountered.

  • Creating secondary stacks is not an expensive operation. It has approximately the same cost as creating any JS object, so it is fine if interleaved JS frames require allocating multiple stacks. Used stack memories themselves can be kept in a pool to further reduce the cost of allocation. Overall, the design of secondary stacks is intended to support applications with many hundreds of thousands of suspended computations.

Side-stacks cannot efficiently represent stackless coroutines (e.g. how async functions are implemented in JS/Swift/C++/Rust/Python/etc). Thus those languages have the same code size issues that lead to JSPI in the first place.

This mistakes the motivation for JSPI, which was never the code size or performance overhead of implementing source-level stackless coroutines. The motivation was rather the performance and code size overhead of the Wasm-to-Wasm Asyncify transformation, which without JSPI is the only way to provide a synchronous view of asynchronous Web APIs to a WebAssembly program without other architectural compromises.

In fact, the Chrome implementation of JSPI has consistently outperformed Asyncify on both code size and run-time execution, as we had anticipated.

Side-stacks probably cannot move between web Workers under future security plans we have without doing a memcpy, effectively. Our opinion is that this is prohibitively expensive.

JSPI does not require stacks to move between Workers. Furthermore, since all usages of JSPI are from JavaScript, moving a JSPI stack would imply moving a JavaScript computation between workers: this is not under consideration at the moment. Could you elaborate what the future security plans this would interfere with, and what the timeline for implementing them would be?

Using Workers to emulate JSPI

The APIs for moving work to other threads implemented on top of Workers and synchronously waiting for it to complete have been available for a long time. Developers have not found these APIs to be sufficient to solve the problems addressed by JSPI. Problems with this approach include:

  • Worker threads performing asynchronous work cannot access program state kept in JS on the main application thread and vice versa.

  • Coordination between threads requires either message passing or shared memory. The former has unacceptable performance compared to JSPI and the latter requires COOP and COEP headers to be deployed properly, which is not always possible or under the control of library or application developers.

  • Creating a worker thread is much more resource intensive than creating even very many side stacks. It is common practice to tie the number of workers to the number of CPU cores. Furthermore, the main browser thread can only wait for worker threads by spinning, which ties up the thread and can introduce jank.

  • Languages compiled to WasmGC cannot yet use threads, so this solution is not readily available to them.

JSPI suffers none of these problems, and because it is so much more expressive than the solution of moving asynchronous work to other threads, performance comparisons between them are not very meaningful.

fgmccabe avatar Feb 11 '25 18:02 fgmccabe

Quite disappointed that JSPI is not considered as important for WebKit team. Our product uses 2 engines, for rendering and ai. Both engines are supporting webgpu as fastest path, which requires jspi to work. But webkit doesn‘t have webgpu by default yet. Moreover it will never support jspi?

Honya2000 avatar May 29 '25 15:05 Honya2000

WebKit is lagging behind with all new (and super useful) apis. Wasm Apis, Ai apis, and a lot more (Navigation Api!!). Thats not good. If this continues like this i will have to tell my users to install chrome instead (as much as i dont want to, but at least they're doing the best they can especially around web ai apis)

ApoloApps avatar Jun 13 '25 16:06 ApoloApps

Short status update here, we've withdrawn our objections to this API. We still have concerns with the core-stack switching API but our belief is that those concerns no longer necessitate blocking this API.

kmiller68 avatar Sep 22 '25 19:09 kmiller68