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

API Concerns with Structured Clone for Wasm Modules #972

Closed
flagxor opened this issue Jan 30, 2017 · 55 comments
Closed

API Concerns with Structured Clone for Wasm Modules #972

flagxor opened this issue Jan 30, 2017 · 55 comments
Milestone

Comments

@flagxor
Copy link
Member

flagxor commented Jan 30, 2017

Some have expressed concerns that structured clone for wasm modules is a bad choice of contact point into the web platform.
In particular, it has these drawbacks:

  • It encourages modules with the origin in which WebAssembly.compile was done, rather than the url of the bytes came from.
  • It encourages use of IndexedDB as an application managed compilation cache, rather than leveraging the existing caching mechanisms in the browser. Requiring middleware vendors to get the caching pattern right.
  • It encourages use of postMessage for module for cross-origin shared modules.

We have casually assumed that the current mechanism would soon be augmented by support for WebAssembly.compile of a ReaderStream (for instance from fetch), to allow streaming compilation.
We've also discussed the possibility of surfacing wasm modules as es6 modules.

Have we found the right contact point?
Alternatives exist such as:

  • Expand the Cache (service worker API) to support wasm compilation and explicit caching of wasm code, and knowing when it's actually compiled.
  • Ensure Foreign fetch allows the possibility of sharing a wasm module cross-origin.
  • Pass an URL instead of an ArrayBuffer to WebAssembly.compile. Using things like blob urls for dynamic cases.

We've already implicitly assumed we'll want to support more options.
Will any of those subsume the current interface to the point we'll want to deprecate it at some point?
Do we want to consider limiting the scope of structured cloning to plan for better future patterns?

@titzer
Copy link

titzer commented Jan 30, 2017

Another option which I personally prefer would be to allow a new XmlHttpRequest response type (e.g. WebAssembly.Module), which doesn't introduce new API surface area and is an even stronger hint to the browser to start, e.g. streaming compilation while downloading.

@lukewagner
Copy link
Member

lukewagner commented Jan 30, 2017

The main two specific concerns that I've heard so far that make sense to me are:

  • window.postMessage and window.onmessage are error prone APIs that are generally discouraged and we shouldn't be encouraging use of these APIs for people wanting cross-origin caching/reuse
  • use of IDB to cache code in insecure contexts makes it easier for a one-time MITM attack (against the insecure origin) to permanently poison the site

If we all agree on these concerns, then I think both could be addressed conservatively by:

  • disallowing structured clone of a Module to a window (only allowing postMessage to workers)
  • disallowing the storing of a Module in IDB in insecure contexts (which follows from the general principle of restricting powerful new features to secure contexts)

Looking forward, and building on basic structured cloning support, we could then incrementally evolve to:

  • have WebAssembly.compile/instantiate take a stream, as has been proposed
  • integrate with Service Worker and the Cache API, leveraging Foreign-Fetch to support a safer-by-default way to cache/reuse code across origins

I think this strategy wins by supporting all the short-term caching and code-sharing needs of wasm without blocking on Service Workers (with new, to-be-defined APIs) in all browsers and also providing powerful primitives (viz., structured clone of modules) that can be used in other ways that wouldn't otherwise involve Service Workers (EWM etc).

There's also the idea to push all the caching into the browser (having all the APIs take URLs, using Blob-URLs for dynamically-generated content). Just from a predictability point of view (and taking into account all the arguments for Service Workers over the HTTP cache), I think this is not a great option. I totally agree we should make loading a wasm module as easy as a <script> tag, but I think that even in this case, caching should be handled by a Service Worker that intercepts the <script> request.

(cc'ing @annevk @bzbarsky @wanderview @asutherland for any comments)

@bzbarsky
Copy link

window.postMessage and window.onmessage are error prone APIs that are generally discouraged

This is the first I've heard about this so would appreciate concrete pointers. In particular, postMessage is supposed to be the API for cross-site communication, so if it's being dicouraged, what is being encouraged instead?

@lukewagner
Copy link
Member

(Mostly forwarding @jeisinger's comments, so feel free to chime in)
@bzbarsky Maybe I overstated the case, but just these Security Concerns about needing to verify the sender's origin.

@bzbarsky
Copy link

Well, right, you have to do that. :)

@titzer
Copy link

titzer commented Jan 31, 2017

Other advantages of using an XHR response type are:

  • the browser gets to prepare for streaming compilation as soon as possible, e.g. even as it starts network requests
  • WebAssembly.compile() is nominally implemented in the JS engine and it'd be weird for it to either have an API out to its embedder to handle URLs, or for it itself to be implemented by the embedder
  • XHRs inherently have origin URLs attached to them, whereas Streams do not necessarily.

@esprehn
Copy link

esprehn commented Jan 31, 2017

As discussed in the other thread, I don't think postMessage of wasm modules makes sense. It seems to be addressing the issue that .compile() doesn't accept a URL like JS/CSS/etc. so the underlying system can't implement streaming and caching for you like we do for other resources.

In general I don't see a difference between WASM, JS, CSS, Images, and the other resource types. They all are:

  • Expensive to parse and compile.
  • Used in multiple frames on the same page.
  • Need async and stream processing.
  • Need caching.

So far the platform has handled the resource caching implicitly. Having the platform handle it has been hugely beneficial to us as we've evolved the underlying system to use different caching strategies over the past several years. Implementing an effective strategy yourself in script is very difficult because you don't have access to things like the system memory limits, pressure signals, number of open tabs and frames, what CPU state the device is in, if it's plugged in, etc.

Separately given the security issues of sending compiled code across the origin boundary, I don't think we can allow sending pre-compiled code cross origin. Anything you send will need to be recompiled on the other side. It's basically the same as sending a string and asking the other end to call eval() on it in JS today.

I think what's missing here is a .compile(url) => Promise<WebAssembly.Module> API, integration with <script src>, <link rel=preload>, integrity hashes, etc.

@titzer Having .compile() take a URL which is implemented in the embedder is no more weird than ES modules having import statements that need to be fetched by the embedder.

@esprehn
Copy link

esprehn commented Jan 31, 2017

@drufball @bfgeek

@ghost
Copy link

ghost commented Jan 31, 2017

@esprehn The performance of wasm code depends on baking in information that depends on the target features and the resources allocated or promised. Many use cases have been deferred to being implemented in user code in a translation stage between the data streaming in and the web browser wasm compiler. For example: user defined code compression strategies; dealing with differences between web browsers in their wasm implementations; taking advantage for performance enhancements that might have spotty deployment; etc. Perhaps this strategy of deferring these use cases was just wishful thinking and not practical, but it might have a lot of benefits if practical.

This translation stage might practically run in a separate context if that helps, a context with limited inputs so that it could be re-run with only those inputs to generate the same output, but it still needs input of the target features and some of the allocated or promised resources.

This translation stage might need to re-run even between instances on the client side, for example to deal with feature changes between browser versions, or even just changes in the available resources. E.g. The first instance of a game might get a lot or fast memory, and the second only a limited amount of slower memory, both needing separately compiled binary code to run.

@jeisinger
Copy link

I would like to see an API that allows for strongly tying a WebAssembly module to an origin. We should require CORS for exceptions throwing inside a cross origin module to bubble to the embedder, and we should be able to do mixed content checking.

I think that once we tie modules to origins, all use cases that currently require structural cloning would naturally work.

You wouldn't need the other origin postMsg'ing a module to you, you can just request it by specifying the URL. You also don't need to have a message handler with proper origin checking, the browser does it for you. You don't need to store the module in an IndexedDB, the browser can do that for you as well. You can have a service worker that handles upgrading in the background. Once we have foreign fetch, you can support stuff like sharing an game engine on one origin with many other origins only hosting the actual game content.

My comment about message handler being suboptimal from a security point of view is that I don't that loading a cross origin script should require cross origin messages at all, and then not requiring that is better than requiring a message handler hoping that every site gets the origin checks right.

Also, if we add new response types, let's add them to the fetch api, not XHR :)

@jayphelps
Copy link

jayphelps commented Jan 31, 2017

Also, if we add new response types, let's add them to the fetch api, not XHR :)

fetch does not support cancellation, and since the latest proposal was withdrawn I would much prefer that any response types supported by fetch were also supported by XHR so we can optionally cancel them. Pretty please 😄

We already have an awkward situation where Service Workers don't have access to XHR so they can't make requests that are cancellable.

@flagxor
Copy link
Member Author

flagxor commented Jan 31, 2017

I believe using the collection of in-progress standards described here will make sense for the more common use cases once these APIs are finalized and available in all browsers. This was already the longer term direction we had intended to go, including rationalizing wasm modules in some way with ES6 modules.

However, I'm also skeptical that we can get the proper caching behavior right, in one go, in all the browsers in a fashion sufficiently coordinated to make the Web an appealing target for this kind of content.
A large part of the motivation for WebAssembly is to provide lower level mechanisms, because the high level mechanisms of the Web haven't consistently allowed developers to do what they need to be performant, even when they are highly motivated to do so (recall for this content the bar is identical content running as a native app).

Structured clone is definitely more low level than what we ideally want folks to use long term. But it is also expressed at a level that allows one to quickly determine that it exhaustively covers all the ways you might want to use it.

I have a hard time readily assessing what exactly the union of Service Workers, Streaming Fetch, Foreign Fetch, some kind of explicit/implicit code caching extension to the Cache API / XHR / Fetch, permits or denies developers. Can I cache content I generated synchronously? It also seems especially unclear what level of functionality this will make available in various browsers (as not all of them support the same set).

It's also useful to keep in perspective that the majority of content initially targeting wasm will come thru emscripten or Unity, which prescribes JS boilerplate we can vet for generality.

Besides the case of allowing storage to indexeddb in insecure origins (which I'm fine with disallowing), I'm not sure a concrete security concern has been raised. That said, I believe limiting things to same origin + secure origins for indexeddb storage is a reasonable starting point, as folks seem very worried bad patterns will get baked in.

Failing that, I would hope those suggesting other API surfaces can clarify whether those surfaces have support in all the major browsers, and what the polyfill options are when they don't (i.e. what do browsers without cache api do?, most other use cases can be polyfilled with indexedb + XHR). I'd be more comfortable with the suggestion of other contact points with the web platform, if it was clear that contact point can work additively in most forthcoming browser versions with only the addition of webassembly.

@pizlonator
Copy link
Contributor

@esprehn link to separate thread discussing passing modules in postMessage?

@lukewagner I think I agree with what you're proposing. Specific thoughts:

The main two specific concerns that I've heard so far that make sense to me are:

window.postMessage and window.onmessage are error prone APIs that are generally discouraged and we shouldn't be encouraging use of these APIs for people wanting cross-origin caching/reuse
use of IDB to cache code in insecure contexts makes it easier for a one-time MITM attack (against the insecure origin) to permanently poison the site
If we all agree on these concerns, then I think both could be addressed conservatively by:

disallowing structured clone of a Module to a window (only allowing postMessage to workers)

Makes sense.

disallowing the storing of a Module in IDB in insecure contexts (which follows from the general principle of restricting powerful new features to secure contexts)

I think I agree with this.

Looking forward, and building on basic structured cloning support, we could then incrementally evolve to:

have WebAssembly.compile/instantiate take a stream, as has been proposed
integrate with Service Worker and the Cache API, leveraging Foreign-Fetch to support a safer-by-default way to cache/reuse code across origins
I think this strategy wins by supporting all the short-term caching and code-sharing needs of wasm without blocking on Service Workers (with new, to-be-defined APIs) in all browsers and also providing powerful primitives (viz., structured clone of modules) that can be used in other ways that wouldn't otherwise involve Service Workers (EWM etc).

I agree with not gating WebAssembly on other things. WebAssembly is a complex beast, so it shouldn't be too coupled to other complex beasts.

I think that if you enable WebAssembly in JSC then you will get it via the Objective-C API, too. That seems pretty neat, but in native apps there is no pre-existing caching engine to fall back to and it makes sense to let the client just do it however they like.

There's also the idea to push all the caching into the browser (having all the APIs take URLs, using Blob-URLs for dynamically-generated content). Just from a predictability point of view (and taking into account all the arguments for Service Workers over the HTTP cache), I think this is not a great option. I totally agree we should make loading a wasm module as easy as a <script> tag, but I think that even in this case, caching should be handled by a Service Worker that intercepts the <script> request.

Totally. Just as WebAssembly is engineered to get around all the wild dynamic JITing we do, it should probably get around the wild caching, too. Any claim otherwise has to answer: exactly how does the cache guarantee the same determinism as the current API?

@flagxor You said:

Will any of those subsume the current interface to the point we'll want to deprecate it at some point?

I'm not convinced that any of them subsume the current interface, but that may just be that I'm missing some context.

If I want the compiling megapause to happen once in a multi-worker app, how do I do it using the current interface and the alternate interfaces?

@flagxor
Copy link
Member Author

flagxor commented Jan 31, 2017

Agreed I'm not sure it does cover all the cases.

As for your multi worker use case:

For the current api you compile once in one worker or via the async method on the main thread, then post it around.

With the other, presumably you need to post around a message to let everyone know the cache has been prime. One worker would have to compile and added things to the cache (through the cache API which isn't available everywhere), and also check that compilation is done (through some as yet unspeced API).

I suppose if we imagined some clarification of how wasm is rationalized as an es6 import, importScripts in each worker could arrange for a single compile to be shared.

@pizlonator
Copy link
Contributor

@flagxor Thanks for that explanation.

For the current api you compile once in one worker or via the async method on the main thread, then post it around.

That is what I guessed. This feels very natural.

With the other, presumably you need to post around a message to let everyone know the cache has been prime. One worker would have to compile and added things to the cache (through the cache API which isn't available everywhere), and also check that compilation is done (through some as yet unspeced API).

I suppose if we imagined some clarification of how wasm is rationalized as an es6 import, importScripts in each worker could arrange for a single compile to be shared.

This feels less intuitive.

The compiling megapause is huge, and the result it produces could also be large - as big as an app binary. We want the client to be able to easily tell when the megapause happens and how long the result lives. Our current API gives us this, so we should stick with it.

Is anyone proposing any security-related extension to the current API beyond what @lukewagner proposed? Does everyone concur that @lukewagner's fix is sufficient?

Also, I don't understand how @titzer's XHR idea relates to this. Is this related to the security and API consistency concerns, or is this an unrelated optimization?

@ghost
Copy link

ghost commented Jan 31, 2017

For the current api you compile once in one worker or via the async method on the main thread, then post it around.

Except that this will not work in general without re-compilation, and if a user translator is in that pipeline than it would need to be part of the module posted plus the original input data.

Also the compiled code will be specific to allocated or promised resources and it does not appear that the sender JS code can practically know the details of the resources that the receiver will have? So the strategy of posting a compiled module from one context to another has rather limited applicability.

What would seem practical is for a number of threads to share the same compiled code as they could all be working with the same resources, but perhaps this could be a fork style operation and a separate matter.

I repeat my suggestion that a pipeline be defined that includes the user translator and the inputs which will include the allocated or promised resources, so that the compile code can be generated from these inputs. The web browser would then be responsible for managing the caching and compilation based on these inputs. Some of the inputs might be URLs, and others internal details of the implementation that get baked into the compiled code.

This also requires resource management, to obtain the promised resources. In particular the promised linear memory will be needed and some characteristics of this will need to be exposed to the user translator, such as the size, but other details might be internal. If the promised linear memory ends up not being fulfilled then it might even be necessary to re-try this process.

For features this might mean that the model of generating and attempting to validate code to do feature detection is not a good fit, rather the features requested or used might need to be inputs exposed to the user translator and used as keys in the cache. The cache keys would be best no more specific than the features and specific inputs used.

@annevk
Copy link
Member

annevk commented Jan 31, 2017

I was asked to chime in by a couple of folks and only have a basic understanding of the setup, so please bear with me.

Could someone explain what the message passing of wasm code enables? Is the purpose here to compile the code once and then execute the compiled code in multiple workers? Meaning that the compiled code would effectively be shared (readonly I hope)?

And the angst is because this is introducing a new primitive that doesn't exist for (JavaScript) code on the web today?

More minor things: 1) @jeisinger, if you have an object you can pass around, there's no need for origins. Message passing effectively has CORS builtin. 2) XMLHttpRequest won't see any extension. Any extensions will be to fetch(). 3) Is there a story for wasm and <script type=module>/new Worker(url, { type: "module" })?

@ghost
Copy link

ghost commented Jan 31, 2017

@annevk I'll let other people explain their question, but I understand they do want to use message passing and the IndexedDB to enable management of the compilation and caching in JS.

I don't think their approach will work well, but could I explore an alternative as a separate thread of discussion rather than having an absolute position, and so ask how might fetch() work with the technical fact that for best performance the code needs to bake in code specific to the resources at hand which might depend on how the linear memory is allocated? Could fetch support a user defined translation stage between the content streaming in and the compilation of that content, and could it support inputs into this pipeline that depend on resources allocated or promised?

I am not sure how valid the analogy is, but it might be similar to an image resource being fetched for which part of the decoder is user defined code (which might decode from a compressed format to a simpler bit map image format) and this decoder might need to know the target device pixel encoding as an input and this might change depending on the resources a tab has and the target dpi etc which might change if the user scales the display or moves a window to another monitor or casts it to another device etc. How would you recommend abstracting this?

@annevk
Copy link
Member

annevk commented Jan 31, 2017

@Wllang fetch() returns a promise for a response that settles once all the headers are in (but the body is not). I would recommend passing the body (a stream) of that response to your custom decoder that does the translation.

@lukewagner
Copy link
Member

@annevk Sorry for lack of context; here's some: So we have a JS object introduced by wasm, WebAssembly.Module, that we've currently defined/implemented to be structured cloneable because it is stateless (so not a JS function object, more like an unevaluated function literal). A Module abstractly represents "code I've already compiled" (at some considerable expense) and so intuitively postMessageing it to workers is a way to share already-compiled code with workers and storing a Module in IDB is a way to serialize the compiled code. (It's up to the browser to implement these optimizations, of course.)

@annevk
Copy link
Member

annevk commented Jan 31, 2017

Given the concerns raised so far with that approach I'm sympathetic to @esprehn's concerns. It does indeed seem rather novel and not much existing infrastructure is being reused. The deduplication offered with <script type=module> could be used here potentially, although for workers you would need more since you want to deduplicate compilation and code across workers, whereas the module map is currently global-scoped. Supporting something like new Worker(..., {type:"wasm"}) doesn't seem out of the question however. Has that been explored?

(The security concern with postMessage() seems arbitrary. If you allow non-trusted ArrayBuffers or Modules to be posted, what is to say you won't allow non-trusted URLs (but nevertheless secure, since they are attacker-controlled) to be posted?)

@lukewagner
Copy link
Member

@annevk postMessage and IndexedDB are existing infrastructure that are being reused, and they're also in all browsers :) The question isn't whether we should integrate with <script type=module> and other ways to load ES6 modules -- I think we should -- the question is whether there are any hard reason to stop supporting structured clone of Module and only allow sharing via URL (and effectively blocking predictable wasm code sharing/caching on SW + new SW APIs). So far, I don't see any new hard reasons.

@pizlonator
Copy link
Contributor

I completely agree with @lukewagner's point. Module is a thing that can be passed around with postMessage and doing so has clear semantics, so forbidding it would be silly.

@annevk

The deduplication offered with <script type=module> could be used here potentially, although for workers you would need more since you want to deduplicate compilation and code across workers, whereas the module map is currently global-scoped. Supporting something like new Worker(..., {type:"wasm"}) doesn't seem out of the question however. Has that been explored?

WebAssembly can be used for simple things like "load code from URL and run code" but it exposes something more powerful: you can programmatically generate WebAssembly code in JS or in WebAssembly, call instantiate or whatever to compile it, and then you can run the thing you compiled. In that world, the user want to think of the code that was generated as just an object - not a named resource.

@sunfishcode sunfishcode modified the milestone: MVP Jan 31, 2017
@annevk
Copy link
Member

annevk commented Jan 31, 2017

postMessage and IndexedDB are existing infrastructure that are being reused, and they're also in all browsers :)

The way you are using them is not a way that any other new resource type is using them. Not reusing existing fetch infrastructure is also rather novel. I'm not sure ignoring @esprehn's post is helping.

In that world, the user want to think of the code that was generated as just an object - not a named resource.

Yeah, understood. Mostly trying to mediate at the moment. (I do think that if we were to offer such functionality we should also have it for JavaScript. Not sure why it would have to be specific to WebAssembly. Compilation is a bottleneck for JavaScript too.)

@flagxor
Copy link
Member Author

flagxor commented Jan 31, 2017

The way you are using them is not a way that any other new resource type is using them. Not reusing existing fetch infrastructure is also rather novel. I'm not sure ignoring @esprehn's post is helping.

JS and other resources with large decode costs would also benefit from explicit caching / passing of compiled code in a small subset of use cases. This probably has not happened (yet) because JS also suffers from having a context dependent meaning, and because the majority of these resources are small.

The normal case for JS is that a script tag just works for most things. Adding a basic service worker makes offline work. By comparison, with 50MB modules being common, even a "normal" Wasm module confronts what would be the advanced case for JS, i.e. the need to show download progress, background the compile, and ideally manage incremental updates and sharing more carefully.

Providing direct low-level mechanisms (as long as they're secure) is a good way to avoid baking-out use cases.
WebAssembly for example intentionally avoided describing its dynamic linking in terms of ES6 modules, instead providing a lower level mechanism, so as not to exclude things that C/posix style dynamic linking allows. In fact, we picked a mechanism even lower level than C, so that non-C dynamic binding wouldn't be ruled out.
Similarly SharedArrayBuffers avoided only providing high level synchronization primitives like mutex/condition vars/queues, as futex + atomics are even more expressive, and because the others can be implemented in terms of them.

As we start to see small chunks of Wasm code in JS frameworks, we'll surely want the implicit mechanisms to also be present for good interoperability. But I'm unconvinced it's wise to rule out more general mechanisms, as Wasm is explicitly intended to fill in the 10% case not well covered by JS currently.

@pizlonator
Copy link
Contributor

@annevk

First of all, @esprehn's post cites another discussion that he hasn't provided a link to. I'd like him to either describe that discussion or show a link. Otherwise, it's hard to follow.

You said:

Yeah, understood. Mostly trying to mediate at the moment. (I do think that if we were to offer such functionality we should also have it for JavaScript. Not sure why it would have to be specific to WebAssembly. Compilation is a bottleneck for JavaScript too.)

In JS, the compilation happens whenever the VM feels like doing it because the compilation is coupled to the dynamic type inference, which requires a profiling stage. We're not going to be providing a compiling API for JS anytime soon, because that would mean locking in the timing of when it happens and we have found empirically that we want this to be tunable in the VM. For example, if we slow down our compiler by adding a new kind of optimization then we would want to change when the compiler runs to offset the increased cost.

On the other hand, WebAssembly compilation is a well-understood procedure. We can specify compilation for WebAssembly because the entire WebAssembly language was designed from the ground up in order to enable us to define what it means to compile it. No such thing exists in JS.

Therefore, I don't think it's useful to assume that the way JS gets compiled should have anything at all to do with how WebAssembly is compiled.

@ghost
Copy link

ghost commented Jan 31, 2017

@annevk So fetch returns a stream for the body of the deployed blob, and I presume manages the caching of that blob. However the use case being discussed is far more general, and includes the caching and reuse of the compiled code generated in part from that blob, and just having a stream does not solve the use case that posting modules and storing them in the indexdb is intended to address.

I would also note that people keep characterising what they are proposing as "predictable wasm code sharing/caching on SW + new SW API", however this does not fit the technical realities. The fact is that two separate contexts might allocate linear memories with different characteristics which require different compiled code and the sender will not know this so that approach will just not work without more design work.

For example, an implementation could restrict all wasm linear memories to having the same capabilities so either all run in separate processes or all use particular sized guard zones or all use inline bounds checks. Flags might be added to wasm compilation that hints at a preference to optimize for one of the cases and that might be used as a cache key, etc.

For example, it might be possible abstract these contexts so the requesting context might first obtain a promise for a linear memory (a somewhat opaque object that is a descriptor of the linear memory) and post it to the sender and the sender might then compile the wasm module for that specific descriptor (looking up it's indexdb cache and using that descriptor as a key) and then send a speculatively compiled module back, and retry if needed.

People are not giving you all these details of such a plan and there might be very significant constraints.

Some other good points were made, such as wanting to support the use case of code generated without a pipeline that is based on a URL. Some of these usescases would be compiling in an already running wasm context with already allocated resources so might well be able to obtain a descriptor of this as an input to compilation and a cache. Some of these use cases might be able to build a provenance descriptor that allows the input to be regenerated, and in other case the only practical cache strategy for these might be to use the dynamically generated input as the cache key.

There are lots of precedents outside the JS sphere, for example scientific computing architectures do a lot of caching and manage large resources, so it's not new computer science. These are also not recent issues for wasm and were raised long ago such as #378 and #376

I just don't think people have pushed the design far enough to be asking for the merits to be judged, it's a WIP, but it would be good to have more people pondering a good path.

@lukewagner
Copy link
Member

@annevk The basic symmetry argument seems reasonable but I think it shouldn't prevent us from starting at a separate point from JS and incrementally moving both wasm and JS towards each other.

So on the wasm side that means the things we've mentioned above and were already planning to do (that's the nature of an MVP; all sorts of good stuff is left out in the first iteration).

And, actually, on the JS side, while I agree with @pizlonator that we're not going to have much success caching/sharing fully-optimized machine code, I think a new, opaque, stateless, structured-cloneable uninstantiated JS module object could potentially have major perf benefits and be worth considering in the future. At the very least, an engine could store the chars in an ideal format (the ideal encoding + compression, noting function boundaries and maybe upvar aliasing, and noting the absence of early syntax errors) so that there was zero up-front scanning/syntax-error-only-parsing required when instantiating that JS module.

But I don't mean to get off on a JS-load-time tangent: I just mean to make the point that this isn't the end of the story and we should expect continued evolution based on our experience shipping v.1. What's important is that we have something that works now, with relatively predictable performance, multiple implementations, and an evolution story. I'd like to avoid increasing our scope if it's not absolutely necessary.

@annevk
Copy link
Member

annevk commented Feb 2, 2017

That's a single fetch and compile inside browsers.

Is that actually true? We don't define such deduplication in Fetch. Servers could return unique resources each time. And I'm pretty sure there are some cases where such deduplication would break things. If that is a thing you think should work (or does already), please open an issue at https://github.com/whatwg/fetch/issues/new.

As for resource integrity and preloading, those can be used for resources acquired through fetch() too. But I guess the argument is that if you know it's wasm, you can compile the incoming stream (hoping the integrity ends up matching)? Requiring CSP unsafe-eval seems totally reasonable. It doesn't seem that much different from eval() indeed.

@lukewagner
Copy link
Member

In fact, Chrome decided not to ship structural cloning of WebAssembly modules for now.

This is surprising and frustrating given that, until recently, experimental wasm support in Chrome has included structured clone to the degree that various middleware vendors have already tested interoperable caching between Chrome and Firefox (with the expected massive improvement over a cold load). Many of these same apps are already caching assets in IDB so caching the wasm alongside is natural.

At least in FF, we haven't seen any compelling security reasons to disable structured cloning and do not wish to regress users so will not be disabling structured clone (unless everyone else agrees to remove it from the spec). Thus, tools can just try { store.put(wasm) } catch(e) {} and then Chrome will just end up never successfully caching. I think this will (further) hurt the impression of overall (cross-browser) wasm load time, though.

@jeisinger
Copy link

I understand that this is disappointing, but I think it's important to stop treating this discussion as a security concern that might or might not exist, but as a discussion about an API surface that we'd be able to ship from a web platform perspective.

@lukewagner
Copy link
Member

@jeisinger I totally get that SW is the shiny future here and could ultimately offer ideal performance and ergonomics for a majority (but not all!) use cases. But it feels like there is an implicit assumption that using structured clone to persist and share things is being considered deprecated as a part of the platform. If they are, that's one conversation. But if they're not, then how is structured clone not purely additive to a platform that already has these concepts? I know it's code, but so is a string of JS chars or a Bob of wasm bytes.

Is the underlying worry that adding structured clone will take away the motivation to follow through with future work (caching and SW additions) and thus you're trying to preserve a sense of urgency?

@ghost
Copy link

ghost commented Feb 2, 2017

@annevk

We don't define such deduplication in Fetch. Servers could return unique resources each time. And I'm pretty sure there are some cases where such deduplication would break things.

So what happens if a html pages uses an images many times. Would web browsers have some sensible strategy of speculating that it was the same on each request and delaying launching multiple requests until at least the response headers were received.

please open an issue at

Is that a hint that this is going nowhere and that you need something far more concrete to consider?

But I guess the argument is that if you know it's wasm, you can compile the incoming stream (hoping the integrity ends up matching)?

Great point. Would the web browser know if it is a dynamic response after receiving the response headers, and couldn't it use a HEAD method to probe this first. Would that be enough to have good confidence to start streaming translation/compilation. Being able to version the resources seems a requirement for cache management.

Requiring CSP unsafe-eval seems totally reasonable. It doesn't seem that much different from eval() indeed.

That would exclude some potentially great use cases for wasm in security critical web software. Wasm uses a much simpler compiler than JS, and offloads a lot of this to the producer, and implements a simple sandbox, so it might have been a great platform for running secure code. It is also expected to run some really big blocks of code and have a small set of operators, so the test coverage might be much higher. Could you ponder a solution that allows wasm to run in security sensitive contexts. Obviously the wasm JS API could not accept a buffer of code to compile without unsafe-eval, but can we still have a good solution that is secure.

@lukewagner

Is the underlying worry that adding structured clone will take away the motivation to follow through with future work (caching and SW additions) and thus you're trying to preserve a sense of urgency?

If code is deployed using this then that will create a legacy problem, and that's not a good start. It would be hard if not impossible to fill for a clone/indexdb custom solution. However if you could take a look at the proposal to use a description of the pipeline and adapt that to meet the use case you might be representing then this might be something trivial to fill in future.

@slightlyoff
Copy link

Hey all,

Late to the thread. Apologies for making it longer.

Speaking from the perspective of the web platform and loading in browsers, it seems important to note a few things:

  • Using Response and bodies does not require Service Workers. Doesn't even require the Cache API. You can continue to use IDB for code storage if you like.
  • Having a separate (more manual) loading path for your feature is a huge red flag. I'm going to file an @w3ctag issue on this to get review started. As a part of that, I'd like to better understand the use-cases for the fully manual loading and cloning.
  • The notion of moving all WASM code into memory is...sub-optimal. One of the best reasons to re-use the fetch() types is the ability to load and store resources without extra copies and the ability to transparently take advantage of streams.

Those of us who feel the weight of web platform consistency really do want to understand the arguments for fully-custom behavior better and try to come up with some middle ground that meets your needs without creating parallel (WASM-only) behavior and types. For instance, there are proposals brewing for code optimisation hints for JS as well. Unifying them should be treated by everyone as a goal. If it's not possible, so be it, but we should evaluate the needs and work towards consistency.

Regards

@ghost
Copy link

ghost commented Feb 7, 2017

@slightlyoff Thank you, some more input would be very welcome. Speaking for my own understand of the challenges:

  • The resources (the encoded code) can be very large and it is likely that it is all needed before the web app can start, in contrast to small image resources or a streaming videos. Perhaps some new strategies are needed due to this difference.

  • Personally I see great value in having a user defined decompression/translation stage in the pipeline. This could allow web developers to explore their own encodings independent of the web browser vendors and their agreement. It would also allow web developers to deal with differences between web browsers and to take advantage of enhancements that have spotty deployment etc. We could live without one wasm standard if there were such a layer that worked well, so this might be a critical component, and it might set the stage for web browsers innovating for years to come. There are a lot of enhancements planned for wasm.

  • The output of a decompression/translation stage might be many times larger than the source, a significant blob to cache. Web browsers may well want to not even store it rather just generate it in a stream and regenerate it as needed. It might not be practical to keep this around as an input key to the compiled code cache.

  • The code needs to be compiled before running in general. Some approaches might work around this somewhat, such as starting with an interpreter etc, but there is a practical use case for being able to compile it as it streams in and with the translation stage it is compiling the output of that stage. The compiled code could also be a significant sized blob.

  • There are use cases in which the same code can be re-used. Some might use exactly the same code, and some might use it in different contexts that need a new translation and compilation pipeline to run.

  • Given the above, I believe that practically some resource management is needed. One approach is to allow manual management of this in custom code on the client. So the manual code would be responsible for managing the upstream versions and re-compiling as necessary and would ultimately be passing around an object representing this compiled code, and hence the structured-clone/indexdb proposal.

  • Personally I see technical problems with the 'manual' structured-clone/indexdb proposal approach, that the compiled code will depend on the target context so may well need to be recompiled when reused. Further the management of the resources such as CPU cycles and memory can be better managed by the web browser which has more global knowledge of these than it would appear practical to give to manual user code running in a web origin context. Compiling a large blob of wasm could tax the resource of a device, it may well need to schedule it, and it might help to be able to compile before allocating the linear memory (but the characteristics of the linear memory may be an input to the compilation stage).

  • The wasm code runs in a new sandbox, and could potentially run without JS, or with a barrier between the two. It could even have API's that allow it to generate new code to be compiled and linked back into itself without exposing that potential to the larger JS code. There appear to be some compelling use cases for secure code here, and if the compilation were managed by manual JS then this separation might not be possible (unless the wasm code could have a restricted API for this all).

Personally I think borrowing some ideas from the scientific computing would help, so build a description of the tasks that need to be done and the resources needed so that the web browser can plan their loading and execution and optimize it and manage the cache. If getting this integrated into the web is going to take a big effort then wasm might use a somewhat throw-away description for now that might be easily subsumed by something much more comprehensive in future.

@ghost
Copy link

ghost commented Feb 7, 2017

@slightlyoff There were no responses to my input about making the case for provisioning for recompiling code for the specific context, but here is some quick technical support for this position. These are run times for the zlib benchmark (a compression algorithm) running in wasm code. You can see that even my quick attempts at this using a 'pointer-masking' strategy and other approaches that might be specific to the context can give some useful performance improvements over that achieved so far by the web browser vendors. I presume they will want to catch up and it seems prudent to set the stage for this in coming years.

gcc x86 32-bit: 9.4 sec
custom pointer-masking wip x86 32-bit: 9.8 sec
firefox wasm x86-32: 15.3
v8 x86-32: 15.3

@jfbastien
Copy link
Member

TL;DR: I think this will be addressed by #1048.

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

No branches or pull requests