-
Notifications
You must be signed in to change notification settings - Fork 13
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
Motivation #2
Comments
I believe the purpose of async/await is to efficiently interop with JavaScript Promises, which isn't possible for a compiler (it requires integration with the host). And similarly for JavaScript generators/iterators. |
(emphasis mine) We see around 50% size and speed overhead in practice when doing it in the toolchain. This is even with a whole-program analysis to instrument as little code as possible. It is very hard to avoid that overhead. Control flow has far more branches, and suspend/resume points save/load all locals which extends live ranges in very bad ways. And that goes all the way up the stack from any possible pause (and even more so with indirect calls). A 50% size overhead is, in practice, preventing a significant amount of users from using this feature atm. Fixing that is a goal here. |
I think I should clarify something. I believe the three goals above describe two very different things: stack switching and futures/promises/generators/however-you-like-to-call-them (I'll just call them promises from now on).
You are talking here about stack switching, which indeed is very expensive today in WebAssembly and requires advanced transformations to be sort-of usable. Asyncify is certainly a great piece of work, but it still is a workaround. My proposal is to fix just that, and ignore promises for now. What I'm talking about in the quote ("as there are already ways to efficiently support them in WebAssembly") are what I call promises. Many languages implement them and they generally don't need special support from the instruction set.
Isn't this just an issue of WebAssembly<->JavaScript interaction? Existing compilers prove that it's possible to efficiently implement async/await on WebAssembly (examples: C++ coroutines and Rust async/await, I'm not talking about asyncify). But JavaScript doesn't know about them and so can't directly interact with promises written in these languages. I believe it's best to leave this JavaScript<->WebAssembly interaction out of this stack switching proposal, perhaps it's a better fit for the interface types proposal. |
The problem is that in order to run a Promise in user-land you need some sort of closure (e.g. The compiler can convert async/await into
Interface types will not fix this at all, because the problem is that WebAssembly needs a way to wait for a Promise to finish without using closures. That's completely outside of the scope of interface types. |
Sorry, I may have misunderstood you, then. Yes, the goal Asyncify solves is to allow a wasm module to wait on a JS Promise. As you said, that requires stack switching in wasm. And I agree that is the one thing this proposal should do. Perhaps the use cases are not described well enough in the overview. I understood async/await there to mean awaiting a JS Promise, and also, to allow wasm to implement async/await using stack switching, etc. I agree the overview could be clearer. |
Right. I think I better understand the issue now. So the goal is to be able to interact from WebAssembly with JavaScript APIs that use promises and are thus asynchronous? If so, it becomes a bit more complicated and needs to interact with the JavaScript event loop somehow. I have a design in mind that might work for this purpose. I'll make a strawman proposal. |
@aykevl Yes, the JS interaction is not trivial. We've actually done a lot of discussion on this in the stack switching meetings, https://github.com/WebAssembly/meetings/tree/main/stack/2021 Have you attended those? If not, there are notes for them in that link. Regardless, more ideas and designs would of course be welcome! |
Oh I'm sorry, I wasn't aware of that. I must have missed quite a bit! |
Ok, I've quickly skimmed most of the notest. It would have been useful if there was a link at the top of the README to that list of notes (and possibly to the agenda as well for future meetings). Unfortunately I can't be at the next meeting. Indeed there has been quite a bit of discussion already! So now I feel like my design idea might not be as great as I originally thought. Context: I've written TinyGo, which is a Go implementation. It has a custom runtime and thus scheduler. For most platforms, it relies on stack switching to switch between goroutines (if you don't know Go: goroutines are a kind of green threads and behave much like regular threads but are somewhat lighter weight). Unfortunately, stack switching is not currently supported on WebAssembly. There is asyncify, but I didn't want to have it as a dependency. Instead, I've worked around this in a somewhat similar way to asyncify using LLVM coroutines. My idea is basically to provide only the low-level stack switching primitives, essentially making stacks a kind of object that can be interacted with:
I believe this is enough for interacting with JavaScript promises. Essentially, I realized that there is no way to wait for a JS promise to finish from WebAssembly, you have to return all the way back to JavaScript and let the event loop run until the promise is settled. Therefore, I think it's unavoidable that such a WebAssembly program will require some sort of scheduler. In pseudocode, this is what it would look like from JavaScript: var instance;
WebAssembly.instantiate(...).then((result) => {
instance = result.instance;
instance.start();
})
function fetchWrapper(url) {
fetch(url).then(response => {
// resume the blocked task
instance.notifyFetchResult(response.json());
// run scheduler until there are no more runnable tasks
instance.scheduler();
})
// not bothering with error handling here
} And from WebAssembly: // this is part of the runtime
func start() {
var id = stack.new(main);
runqueue.add(new Task(id));
scheduler()
}
func scheduler() {
while tasks in runqueue {
var task = runqueue.pop();
stack.switch(task.id)
}
// all runnable tasks have run, so return now
}
// this is the user code
func main() {
result = doFetch(some url);
print(result) // do something with it, seemingly synchronously
}
// this is library code
// globals only used for ease of understanding
var fetchingTask;
var fetchResult;
func doFetch(url) {
// Start the fetch, and suspend the current task.
fetchingTask = getCurrentTask();
call JavaScript fetchWrapper(url);
stack.switch(0); // switch back to the main stack (which contains the scheduler)
return fetchResult
}
func notifyFetchResult(result) {
// Store the fetch result and queue the task that did the fetch in the runqueue.
// Don't run it yet.
fetchResult = result;
runqueue.push(fetchingTask);
} I hope this makes sense. In this implementation, only the raw stack switching primitives are exposed to WebAssembly and the language/compiler/runtime take care of switching between these stacks. This is roughly what I already use for TinyGo except that it needs an asyncify-like transform to be usable in a JS context. In this example, the behavior is as follows:
Possible issues:
If this makes sense and is any good, I can work on a proper (more formal) proposal. I've relatively quickly written this comment to roughly explain what I had in mind. |
I would encourage you to attend the subgroup meetings. |
I will try to be at a meeting but I should note that monday evenings (in my timezone, GMT+2) don't work well for me. But maybe I can make a short presentation what I would like to see from a TinyGo perspective (which might be slightly different from what upstream Go would want to see). |
Merge typed-func-refs proposal
I just wanted to add this here; within the Embedded world, we're looking at ways to interrupt a running WASM application and invoke an event handler; think a traditional interrupt event handler, or even a Unix / Linux signal handler, This would entail the WASM VM being interrupted, forcing a stack switch and a specific function being invoked. I'm not sure if there are similar requirements within the browser world too? |
You can do something like this today: use a designated location in linear memory that the host writes into. The wasm code has to poll it of course. |
At the moment, the overview gives these goals:
Of these, only green threads require any changes to WebAssembly:
While I've only mentioned LLVM above, other compilers do something similar. I believe Rust has a separate implementation of async/await (it doesn't appear to use LLVM coroutines). But an easier way to understand why async/await and yield-style generators do not need WebAssembly support is by realizing that they can all be implemented manually, by hand. And if it can be written by hand, a compiler can perform that transformation for you as a compiler pass.
What cannot be written by hand is stack switching. In particular, this is a problem in Go which uses stack switching extensively for goroutines (every blocking operation potentially results in a stack switch).
Therefore, I would propose that this proposal only focuses on the green threads use case, that requires stack switching (as evident from the name:
stack-switching
). Of course you could potentially use stack switching for async/await and yield-style generators, but I do not think we should focus on them as there are already ways to efficiently support them in WebAssembly.The text was updated successfully, but these errors were encountered: