Skip to content
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

Synchronously instantiate WebAssembly modules #76

Merged
merged 3 commits into from
Nov 15, 2023

Conversation

nicolo-ribaudo
Copy link
Collaborator

@nicolo-ribaudo nicolo-ribaudo commented Oct 27, 2023

WebAssembly modules "phases" are mapped to ES modules "phases" as follows:

JavaScript WebAssembly
Fetch Fetch + compile
Execute Instantiate
  • The fetch (and fetch+compile 1) phases are currently asynchronous/off-thread: on the web you cannot obviously block the main thread when fetching dependencies, and being able to compile Wasm asynchronously has benefits for the same reason.
  • The execute phase for JS can currently be either asynchronous or synchronous, depending on whether the module uses top-level await or not.
  • The instantiate phase for Wasm is currently asynchronous, as if it was implemented using an ES module that contains top-level await.

We are currently working on a JavaScript proposal that allows deferring the "Execute" phase to when it's actually needed, rather than at startup time:

import defer * as myMod from "./mod.js"; // mod.js is not executed
later(() => {
  use(myMod.someValue); // mod.js is executed now
});

However, we can only defer synchronous execution, because we will synchronously execute on property access on the module namespace object. If users were ok with having asynchronous deferred execution, they would just be using dynamic imports.

This means that this feature does not compose with top-level await, and as a consequence it does not compose with the current version of this wasm-esm integration proposal. Instantiating Wasm modules synchronously would make it possible to defer their instantiation until when it's actually needed, rather than increasing startup time:

import defer * as myMod from "./mod.wasm"; // mod.wasm is not instantiated
later(() => {
  return myMod.compute(1, 2, 3) // mod.wasm is instantiated now
});

This example shows a direct deferred import of a wasm module, but the common case would probably be a deferred import of an ES module that transitively imports a wasm module.

My understanding is that instantiation is done asynchronously due to requirements from earlier implementations of WebAssembly that deferred most of the compilation work to the "instantiate" phase, making it excessively costly to do synchronously. Wasm compilers have changed in the meanwhile, and as such the original concerns may not apply anymore.

  • TODO: check if the HTML PR needs to be updated due to this change

Footnotes

  1. while in this spec it may look as if compilation was done synchronously, the integration in the HTML spec runs the "parse a WebAssembly module" algorithm asynchronously: https://github.com/whatwg/html/pull/4372/files#diff-41cf6794ba4200b839c53531555f0f3998df4cbb01a4d5cb0b94e3ca5e23947dR88707

@nicolo-ribaudo nicolo-ribaudo marked this pull request as draft October 27, 2023 11:27
@Pauan
Copy link

Pauan commented Oct 27, 2023

However, we can only defer synchronous execution, because we will synchronously execute on property access on the module namespace object. If users were ok with having asynchronous deferred execution, they would just be using dynamic imports.

Any JavaScript module can potentially use top-level await. In the real world it's very common to use npm packages, so a large portion of your application is third-party packages.

Even if those packages are currently synchronous, at any point in time they can add in a top-level await without your knowledge, which will then cause your deferred proposal to break.

Top-level await is an official first-class concept in JS, and it is important, it should be properly supported. I don't think it's a good idea to create an artificial barrier for top-level await.

Packages should be able to add in top-level await without worrying about breaking downstream consumers. So consumers should always assume that modules are asynchronous.

@nicolo-ribaudo
Copy link
Collaborator Author

nicolo-ribaudo commented Oct 27, 2023

Note that "break" means simply that the module using top-level await is not deferred. They do not compose (you cannot have a module that is both deferred and asynchronous), but they still "fail gracefully" (ref: https://github.com/tc39/proposal-defer-import-eval/#top-level-await).

@Pauan
Copy link

Pauan commented Oct 27, 2023

but they still "fail gracefully"

I see, that is far more reasonable.

@devsnek
Copy link
Member

devsnek commented Oct 27, 2023

is this suggesting that importing wasm with esm will force blocking compilation? that kinda sucks :/

@guybedford
Copy link
Collaborator

@devsnek compile is a separate step, this is only instantiation.

Copy link
Collaborator

@guybedford guybedford left a comment

Choose a reason for hiding this comment

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

To give some background here - this came out of discussion with @littledan who previously worked on this behaviour, based on the realization that the constraints that originally motivated the async work no longer apply due to engine compilation updates.

1. Perform ! [=Call=](|promiseCapability|.\[[Resolve]], undefined, « undefined »).
1. [=Upon rejection=] of |instancePromise| with reason |r|:
1. Perform ! [=Call=](|promiseCapability|.\[[Reject]], undefined, « |r| »).
1. Let |instance| be the result of [=Instantiate a WebAssembly module|instantiating the WebAssembly module=] |module| with imports |importsObject|.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we still need to be explicit about the error handling here - do we need to say that any errors get rethrown? Alternatively should we explicitly set the error on the module?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, errors here need to be rethrown. However, isn't it implicit? For example, Instantiate a WebAssembly Module does not explicitly rehthrow instantiation errors, that are generated in Instantaite the core of a WebAssembly module.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, if it's implicit that's fine - just wanted to verify that the throw completion is being properly propagated in the move from async to sync.

@nicolo-ribaudo
Copy link
Collaborator Author

For reference, Node.js's experimental implementation of this proposal does async compile and sync instantiation: https://github.com/nodejs/node/blob/1392538d1b91672360d402b358e40537691b67c2/lib/internal/modules/esm/translators.js#L513-L542

Copy link
Collaborator

@guybedford guybedford left a comment

Choose a reason for hiding this comment

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

This seems like the right direction to me. @littledan would be great to have your review on this.

Copy link
Collaborator

@littledan littledan left a comment

Choose a reason for hiding this comment

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

LGTM (with Apple and WasmCG's approval) but it'd be nice to do some further cleanups on the js-api, in particular remove this algorithm: https://webassembly.github.io/spec/js-api/index.html#asynchronously-instantiate-a-webassembly-module

@guybedford
Copy link
Collaborator

guybedford commented Nov 15, 2023

This has been open for over two weeks now, so I'll aim to land it tomorrow. Discussing this with @nicolo-ribaudo we agreed further refactorings can be made as follow-up PRs as necessary.

@littledan littledan merged commit 9f6136d into WebAssembly:main Nov 15, 2023
4 checks passed
@nicolo-ribaudo nicolo-ribaudo deleted the sync-instantiate branch November 15, 2023 06:51
@ajklein
Copy link

ajklein commented Apr 1, 2024

To give some background here - this came out of discussion with @littledan who previously worked on this behaviour, based on the realization that the constraints that originally motivated the async work no longer apply due to engine compilation updates.

@littledan, can you provide more background on which engine compilation updates referred to here?

(I found this thread via @guybedford's slides presented at the 2024-02-27 Wasm CG meeting)

@nicolo-ribaudo
Copy link
Collaborator Author

nicolo-ribaudo commented Apr 1, 2024

Some browser implementations of Wasm (maybe just WebKit?) originally didn't have a Wasm jit compiler, so all the compilation was done in WebAssembly.instantiate. Now all major web JS/Wasm engines support jit for Wasm, so the work that needs to happen during instantiation is much cheaper.

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.

6 participants