Join GitHub today
Use WebAssembly.instantiateStreaming instead of WebAssembly.compileStreaming #6433
Unfortunately there are performance issues with
If I understand correctly, for the JSC engine
Because of this, we plan to recommend that all bundlers use
yes there are multiple issues with that:
webpack tries to treat WASM like ESM.
This means the
WASM modules also have a
In the WASM JS API imports are passed via
The ESM spec specifies multiple phases. One phase is the
When WASM are in the module graph this means:
This behavior is not possible when using the Promise-version of
It's only possible when using a sync version of
Note: Technically there could be a WASM without
webpack wants to download and compile the WASM in parallel to downloading the JS code. Using
I also want to quote the WebAssembly spec. It states that compilation happens in a background thread in
It's not clearly stated but in my optinion JSC's behavior is not spec-comform.
WASM also lacks the ability to use live bindings of imported identifiers. Instead the
It would be great to support getters in the
This is worth discussing at the next CG meeting. @TheLarkInn, would you mind adding to the agenda (once it's posted... we just had the meeting today so I haven't created the agenda yet)?
To answer a few points:
Interesting points on the spec. It's not final and I don't think this wording is accurate. It cannot mandate parallelism (unless parallel == 1 is OK). It does not mandate that all compilation be performed (disallowing any future compilation), and cannot either because that would prevent having an interpreter or tiering (so Chakra and SpiderMonkey would then not conform either).
referenced this issue
Feb 13, 2018
To discourage devs from using the sync
I wasn't aware of this spec requirement, but you seem to be quite right!
So given these two constraints, could WebPack:
To break most cycles involving functions, I would imagine
The main problems with this are:
While there are future JS API extensions we've discussed that could address some of these cases, I think it might be ok for a bundler to simply reject these cases (i.e., issue a build error) for now:
However, if these cases are or become important, I think they could be made to work, with some effort; happy to talk more about that.
I also thought about that, but that only works great when WASM has no dependencies.
Shim function would really defeat future performance improvements in JS engines. I think wasm-to-wasm calls will get optimized in engines soon. The benefit of not going through the JS stack is big (afaik at least for v8). (cc @mstarzinger)
This would break a lot of good stuff in WASM. No memory sharing, no tables...
In my opinion this is not a good way to go. I guess I'll stay with the sync Instance call for now. (Maybe using instantiateStreaming for WASM without start and only function imports from JS.) I hope browsers will allow sync
Are there any problems in addition to what is already being discussed?
When I mentioned shim functions, this was for JS↔wasm imports; and even then they weren't always necessary; just to break cycles. I gave some reasons above why I don't expect wasm↔wasm imports to be common at the bundler stage; have you seen any indications otherwise?
By the time wasm↔wasm imports have become common, we can have increased the expressiveness of JS API (in ways already proposed, e.g., allowing
Similarly, until dynamic linking starts being used, every module (or .wasm+.js glue pair) will have its own memory/table, so I don't think we need to worry about multiple .wasm's sharing the same memory/table in the initial support.
That being said, if you wanted to do extra work and support this, it would be possible with another simple .wasm rewrite: convert the module that defines the memory/table to instead import and then create the memory/table in the JS glue code so they can be imported.
Then in practice WebPack wasm integration won't work on Chrome and will have worst-case load-time performance on Safari; that seems pretty unattractive. I think it's important not to let the perfect be the enemy of the good here: we're talking about very simple configurations of .wasm modules for the first few years that could easily and optimally use today's
For my use case I need the synchronous form for CJS to allow migrating to the asynchronous ESM form. But without the synchronous option I can't really offer the ESM form any time soon since there's no migration path.
The 4 kB limit seems odd since the file is already downloaded, so not blocking for network. It's just CPU crunching which could be done in something like a web worker.
Grabbing some larger wasm payloads, 15 kB and 41 kB, I was able to get output from:
Which I'm guessing means there is no restriction there (a good thing).
@jdalton, a web worker only helps if it can run concurrently, which necessitates an asynchronous API.
Still, not everybody is happy with the 4kB limit for synchronous compilation. Web platform folks felt very strongly about it, though. I don't actually know if V8 activates it on Node as well.
A web worker can run the synchronous
I'm not going to bikeshed various web worker implementations. You get the gist, it's gravy when you can offload to another thread for crunching.
cycles are not the problem in this case (they are another problem).
Consider a part of you application is loaded on demand and contains WASM (this is always the case with webpack when using WASM). Assuming the WASM module depends on some other JS module in that part of the application.
When the user requests that part of the application I want to start downloading the JS and the WASM. I also want to prepare the WASM to be used. This could happen in parallel.
Now the problem with instanciateStreaming. You need to pass all imports to instanciateStreaming. But these imports do come from the JS module which should be loaded in parallel. I could solve this with shim functions, but I always need them.
I my opinion this it not uncommon. I imagine the WASM future as many small WASM modules interleaved with JS modules. WASM to WASM is possible and great, because you could pass complex binary objects between them instead of converting to JS values. Many WASM modules can share a memory WASM module (which gives you alloc and free) and pass pointer around. WASM-to-WASM is great and we shouldn't limit or prevent it.
Everything for dynamic linking with imports and exports are in place. Any performance issues left with this could be easily solved. In my option this is the way to go.
I favor dynamic linking over static linking because:
Dynamic linking should be the way you author WASM.
You may use WASM from CommonJS, conditionally:
if(Math.random() < 0.5) require("./something.wasm");
This must execute sync. In this case webpack calls
The ESM spec requires sync evaluation, that's only possible with
Either the ESM spec is changed to allow async evaluation, which is don't recommend.
Or the WASM implementations are changed to allow and support sync instantiate. The WASM spec doesn't have to be changed, it already allows sync instantiate.
Or we give up on the approach to make WASM like ESM.
We have discussed generalising the instantiate function to allow its import object to be a collection of promises of individual objects (or even allow the individual entities to be promises). That should solve the parallel compilation use case without requiring shims.
Ah, ok, I was imagining the JS file that called
Agreed with @rossberg here, using shims as a polyfill.
It's not possible to take two arbitrary wasm modules and have them share memory and tables; they have to be specifically compiled (probably with the same toolchain for the same language) to work this way. This is because these wasm modules must carefully coordinate use of linear memory and table index space to avoid clobbering each other and also coordinate and basic impl details like malloc/free. Right now, there is not a non-experimental toolchain that does this that I know of.
Moreover, even when this is possible, I don't think it will be common for multiple wasm ESMs to share memory because it is, I think, a quite good thing for unrelated modules to have separate memories/tables: otherwise there will be subtle corruption bugs that only surface when particular modules are used together, leaks will be hard to track down, and OOMs on 32-bit devices will be more likely due to larger memories. It will probably be a good idea in the future to use dynamic linking to factor out common runtime code (e.g., libc, libm), but in that case it is only the
Not in the toolchain: it may be possible at the bundler level but, right now, there is not a non-experimental toolchain that will actually emit dynamically-linked wasm code.
Why can't we go with the shims? I think they'll work fine w/ very little overhead (probably zero overhead once the
Ignoring multi lang modules compiled to wasm, I don't see why a narrowerer scope of single lang compiled WASM modules sharing memory being a bad idea.…
On Mon, Feb 19, 2018, 1:16 PM Luke Wagner ***@***.***> wrote: Ah, ok, I was imagining the JS file that called instantiateStreaming also contained all the JS code (it had already been downloaded), but it sounds like these are in separate files, so I see why function shims would always be needed. Now the problem with instanciateStreaming. You need to pass all imports to instanciateStreaming. But these imports do come from the JS module which should be loaded in parallel. I could solve this > with shim functions, but I always need them. Agreed with @rossberg <https://github.com/rossberg> here, using shims as a polyfill. I my opinion this it not uncommon. I imagine the WASM future as many small WASM modules interleaved with JS modules. WASM to WASM is possible and great, because you could pass complex binary objects between them instead of converting to JS values. Many WASM modules can share a memory WASM module (which gives you alloc and free) and pass pointer around. It's not possible to take two arbitrary wasm modules and have them share memory and tables; they have to be specifically compiled (probably with the same toolchain for the same language) to work this way. This is because these wasm modules must carefully coordinate use of linear memory and table index space to avoid clobbering each other and also coordinate and basic impl details like malloc/free. Right now, there is not a non-experimental toolchain that does this that I know of. Moreover, even when this is possible, I don't think it will be common for multiple wasm ESMs to share memory because it is, I think, a quite good thing for unrelated modules to have separate memories/tables: otherwise there will be subtle corruption bugs that only surface when particular modules are used together, leaks will be hard to track down, and OOMs on 32-bit devices will be more likely due to larger memories. It will probably be a good idea in the future to use dynamic linking to factor out common runtime code (e.g., libc, libm), but in that case it is only the Module being shared between Instances, not the Memory or Table. Everything for dynamic linking with imports and exports are in place. Not in the toolchain: it may be possible at the bundler level but, right now, there is not a non-experimental toolchain that will actually emit dynamically-linked wasm code. ------------------------------ Why can't we go with the shims? I think they'll work fine w/ very little overhead (probably zero overhead once the Promise-taking instantiateStreaming is present). — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#6433 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/ADQBMPV7ecdBl1w7wld8zOKPdiazKgIqks5tWeSXgaJpZM4R2Qni> .
Practically speaking, dynamic linking of unrelated modules (i.e., modules that aren't compiled all together as part of the same program/app/system) requires a stable source-language ABI which doesn't exist for any of compile-to-wasm language I've seen (and probably shouldn't until ABI-affecting wasm features like multiple return values and exception handling land). So I think that's the entire story for the next 1-2 years unless something totally new pops up.
But even after that, I think modules should default to being standalone and defining their own memory. The benefits are basically the same as those for having a microkernel OS architecture:
We can definitely hypothesize situations where it would be more efficient for modules to share memory, but there are a couple different ways to go about fixing that and it's not clear that a brute-force type of dynamic linking is the right way. It's be useful to have the use cases present to help guide us through this design space since it's inherently in optimization territory.
Ok I created an extra challenge.
Here is a test case which I believe is impossible to solve without sync Instance.
The dependency graph looks like this:
a -> tracker -> b -> tracker -> wasm -> tracker -> c
Which results in the following evaluation order according to the ESM: tracker, b, c, wasm, a (all in the same tick)
Feel free to "compile" the test case anyway you like and try using
But maybe you surprise me with a clever approach and I was wrong.
added a commit
Mar 7, 2018
referenced this issue
Mar 7, 2018
added a commit
Mar 8, 2018
I'm working on a PR to fix this and i'm trying to find the best solution.
The one describe here forces us to shift:
My idea is to inject a runtime func that Webpack will call first (and eventually call the start func).
(module (import "env" "global" (global i32)) (func (export "t") (result i32) (get_global 0) ) )
(module (global (mut i32) (i32.const 0)) (func (export "t") (result i32) (get_global 0) ) (func (export "init") (param i32) (get_local 0) (set_global 0) ) )
Each global will add an argument and two instructions, according to v8's limits that should cover most use-cases, but if not we can inject another init function.
Since you are the experts here, I would like to know what do you think about that? And if i'm not missing obvious things.
It's also trivial to import an i64 global when standardized even if the host doesn't support it.
Thanks for asking! Talking with @linclark and @rossberg a bit more over the last week about how native wasm-ESM integration would be defined in terms of core wasm semantics, I think the natural thing for wasm would be to have wasm instantiation take a snapshot of the values of its imported live bindings at the point in the Instantiation-phase post-order traversal where the wasm module gets instantiated. This reflects the existing design of the wasm core spec's embedding interface, which takes a list of values. (WebAssembly module exports could still be live bindings which would allow a degree of cyclicy with JS.)
Unfortunately, IIUC, the only live bindings with non-
With the mutable globals and an
But for now, this means that global imports (constant and mutable) are in the same category as memory/table imports. Note: global imports exist in wasm v.1 solely for dynamic linking (specifically, to pipe in a dynamic value for the
So my (tentative, obviously this is all in flux and open for discussion :) recommendation would be, for an MVP, to simply not support bundling memory/table/global imports, or to support it by serializing
I was thinking about the implementation specifically for Webpack but it would be more useful to integrate it in the standard, I agree.
Since in Webpack we can resolve the dependencies AOT, we can optimize the loading of the modules (like
I like that solution, it would simplify what we're trying to do here.
I'm just wondering, if you have a cyclic dependency it will end up in a deadlock (just the concept nothing to do with lock etc)? It seems less flexible that functions.
To sum up and from what I understand we should leave the
One idea behind the init function from my PR is that we can already think about polyfilling future features (like the i64 global import).
It would lead to an instantiation-time error when one wasm module's instantiation would read
In the short term, it would still be best to switch to
referenced this issue
Mar 15, 2018
This was referenced
Mar 23, 2018
@mathiasbynens I think that's great for general consumption, however I believe the root issue here is that webpack itself is using synchronous APIs in a more responsible manner (in workers?) and is only being blocked by an arbitrary 4kB limit imposed by Chrome browser specifically. (It's been a while so things might have shifted)
Related: #6475 (comment).
Interesting... @sokra spinning into a worker would mix a few features that we can/want to land.…
On Fri, Apr 13, 2018, 9:27 AM gahaas ***@***.***> wrote: Synchronous compilation should work in Chrome 66 on workers. There was indeed an issue but it got fixed. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#6433 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/ADQBMEQoz00APeilDbQy5CX4WebROt5Tks5toNH1gaJpZM4R2Qni> .
@jdalton I mixed up issues. For me, already in Chrome 65
FYI, I mixed it up with WebAssembly.compiledStreaming, which is only supported in Chrome 66 on workers.
The opposite is true: the 4 KB size limits only apply on the main thread. There are no limits in workers. https://cs.chromium.org/chromium/src/third_party/blink/renderer/bindings/core/v8/v8_initializer.cc?l=437 (I guess you meant