-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Initial Wasm Stack Switching support #16779
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool!
I didn't did a full review yet but this looks like a nice minimal first step.
In terms of naming I think we should do with whatever the standard clang flag is for enabling stack switching. Then we can just keep the normal -sASYNCIFY
and it will use whatever features it can internally.
BTW, how does this effect fibres. My understanding is that with fibres we can have any number of outstanding/sleeping/suspended stacks and resume them in any order. IIRC that stack switch proposal doesn't allow for multiple outstanding stacks?
emscripten.py
Outdated
@@ -840,7 +840,7 @@ def create_module(sending, receiving, invoke_funcs, metadata): | |||
module = [] | |||
|
|||
module.append('var asmLibraryArg = %s;\n' % sending) | |||
if settings.ASYNCIFY and settings.ASSERTIONS: | |||
if settings.ASYNCIFY: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this was just for debugging?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is indeed wrong, but it is for more than debugging. I'll fix + add a comment. We now need to instrument imports for either stack switching (to use the Promise API) or for assertions (to add asserts on proper import handling).
Ah, I wasn't aware clang had a flag for it. That sounds like a good idea, to match it the way we do for wasm exceptions. |
I'm not sure it does.. I was just assuming there would be one.. but maybe that won't happen. I guess if it doesn't happen when we will need our own flag .. we can bikeshed the name later. |
I think you are right. We can't support fibers naively with stack switching due to that. However, I wonder if we can perhaps create more instances of the module and each fiber would have one. A little like pthreads has one instance in each Worker, actually... Anyhow, in the long term wasm stack switching will solve this even if the Promise API does not. |
The fibers question has come up with us before. The current API will likely not allow it; however, there is a V1.1 (my terminology) of the API that would support fibers in an analogous way. This is on our roadmap. |
After more thought I'm not sure we have a better idea for the option name here. So perhaps it's better not to decide on a permanent long-term name atm, and stick with If that makes sense to people then I think this PR is ready for review. |
Ok with me. Movement is better than perfect stillness. |
Fixed some tests now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm % comments
emcc.py
Outdated
@@ -2223,6 +2225,7 @@ def check_memory_setting(setting): | |||
not settings.ASSERTIONS and \ | |||
not settings.RELOCATABLE and \ | |||
not settings.ASYNCIFY_LAZY_LOAD_CODE and \ | |||
not settings.ASYNCIFY == 2 and \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!= 2
emcc.py
Outdated
@@ -2512,6 +2515,9 @@ def get_full_import_name(name): | |||
|
|||
settings.ASYNCIFY_IMPORTS = [get_full_import_name(i) for i in settings.ASYNCIFY_IMPORTS] | |||
|
|||
if settings.ASYNCIFY == 2 and not settings.EXIT_RUNTIME: | |||
diagnostics.warning('experimental', 'ASYNCIFY with stack switching is only tested with EXIT_RUNTIME atm') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about just always emitting the warning when ASYNCIFY=2
is specified? It is experimental right now for all uses, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I'll warn all the time. I'll also add an error here with exit runtime, as it looks like that is just broken atm in d8 (which is the only place we can test so far).
// API. | ||
suspender: null, | ||
// The promise that is being suspended on in the VM atm, or null. | ||
promise: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if I make a call into the module while the is promise oustanding?
Can we at least assert if I try to do that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that should just work. What won't work is trying to suspend again IIUC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, if you try to call in to a module via a wrapped export, and there is an outstanding suspension, then the engine will/should trap. However, this will not be the case with the static API; in that case, it will be permitted to reenter a statically wrapped export. In order to support reentrancy, something will have to be done with the shadow stack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, thanks! We should probably move over to the static API here eventually, then, even before fibers, since we do want to support "small" calls back into the wasm during a suspend, e.g. to do a malloc. OTOH, maybe that's not urgent as malloc would not be wrapped, so it should just work already.
#if ASYNCIFY == 2 | ||
// TODO we could perhaps add an init function and put this there, but | ||
// this should work for now. | ||
Asyncify.suspender = new WebAssembly.Suspender(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't do $suspender: "new WebAssembly.Suspender()"
as the top level of the library object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would resolve as a string? Though there might be a way with postSet
to do this... but initing the suspender right before we use it to instrument imports seems more "local" anyhow.
src/library_async.js
Outdated
@@ -341,9 +424,11 @@ mergeInto(LibraryManager.library, { | |||
}, | |||
}, | |||
|
|||
emscripten_sleep__sig: 'vi', // TODO: others |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need that comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, we'll need to add sigs for all methods in this file eventually? We depend on that with the new wasm API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm adding a better comment for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I means is, all library functions could benefit from adding as .sig so this comment could equally well apply to all __sig
lines in the whole codebase.
In order words, that TODO to add more __sigs is more of a codebase-wide TODO.. I'm not sure it makes sense to mention it in just one specific place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is far more urgent here, though: literally it will trap without a sig, when using stack switching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats that same for all functions though. For example, if you try to call addFunction
with any JS library function that does't have a __sig
it will crash/trap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Sorry let me rephrase that: if you try to take the address of a library function within a SIDE_MODULE, and that library function doesn't have a __sig
, it will fail).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I guess to me this feels more urgent since I think users are more likely to run into it... but maybe that's wrong.
I can remove the TODO if you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, maybe elaborate a little if you want to keep it. TODO: add sigs to other functions in the files so that ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I edited it now to focus on the overall work to get this entire file to work with stack switching. The sigs are just a part of that.
src/library_async.js
Outdated
emscripten_sleep__deps: ['$safeSetTimeout'], | ||
emscripten_sleep: function(ms) { | ||
Asyncify.handleSleep((wakeUp) => safeSetTimeout(wakeUp, ms)); | ||
// TODO: add return in the others. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All other suspending methods. I'm adding a better comment now. Basically, for the new API to work sleep etc. all need to have a sig, and to return a value (the Promise).
src/runtime_functions.js
Outdated
@@ -16,6 +16,30 @@ function uleb128Encode(n) { | |||
return [(n % 128) | 128, n >> 7]; | |||
} | |||
|
|||
function getTypeDescription(sig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about sigStringToTypes
? Or sigStringToWasmTypes
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I'll rename to sigToWasmTypes
.
// 2: Depend on VM support for the wasm stack switching proposal. This allows | ||
// async operations to happen without the overhead of modifying the wasm. | ||
// TODO: document which of the following flags are still relevant in this | ||
// mode (e.g. IGNORE_INDIRECT etc. are not needed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document this as experimental? Mention where this is currently supported? Link to the SS proposal?
|
||
# stack switching does not asyncify the code, which means it is very close | ||
# to the normal "nothing" size, and much smaller than the asyncified size | ||
self.assertLess(stack_switching_size, 0.60 * asyncify_size) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would the wasm file not be 100% identical with stack switching?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor changes might exist. For example enabling asyncify with or without stack switching will force malloc to be included, to allocate the stack. We could actually remove that in stack switching eventually, at least for the non-fiber case. Also asyncify may require different dynCalls, keeping more of them alive (it does an indirect call to resume, if we paused in one).
@kripken that's an interesting idea. However, mind that fibers are intended to be "lightweight", and a program may feasibly want to create hundreds, if not thousands, of short-lived fibers. This is the case in Taisei, which was the motivating project for the fibers API. The cost of instantiating new modules can be mitigated if we can pool and reuse them, but I don't know about the memory footprint of keeping so many instances. I'd be interested in testing it if this ever gets implemented, though. |
@Akaricchi Good point, yeah, if you want hundreds or thousands than the cost of Modules can be noticeable, even if each is just 1MB or so (which I think is the right order of magnitude). So we may need to wait for full stack switching inside wasm. |
Neither of the above conclusions is necessarily warranted. The so-called static JS Promise integration API is a very small extension to what has already been done. That will allow for fibers; however, they will have to be managed by JS code. |
Sounds good @fgmccabe , we can experiment with fibers that way earlier, then. |
This is super exciting! Thanks @kripken et al! |
This is exciting news! @kripken we were wondering,
|
I'm pretty sure this is dependent on the wasm stack switching proposal: |
OK, makes sense https://twitter.com/emscripten/status/1530272023431114752 (and the below comments) answer my questions. |
Yes, this basically replaces the "userspace" Asyncify transform of the wasm with the wasm stack switching proposal. So it is 100% dependent on that support in browsers. We didn't add this to the changelog because it is still far from stable. You can test it now, if you use latest v8 and flip a flag ( |
This uses the Promise API for Wasm Stack Switching as an alternative to
Asyncify. That is, instead of modifying the wasm like Asyncify does, we have
the VM pause and resume us. When we want to pause an import returns a
Promise, and when we want to resume we resolve the Promise, basically.
There is more work to do, like getting all the tests to pass, but for now this
gets one test working + adds a code size test that shows the benefit here.
Marked as draft as while this seems to work, I'd appreciate feedback on the
API. I have it for now as
-sASYNCIFY=2
, that is, another mode of theASYNCIFY
flag. That's the smallest diff, and we do still use the AsyncifyAPI (both in C and in JS, just the impl differs), but maybe we want something like
a new flag
-sSTACK_SWITCHING
, which would be similar to how we have-sEXCEPTION_HANDLING
for wasm exceptions - however, that is aninternal flag (users do
-fwasm-exceptions
), so it's not a 100% relevantprecedent.