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

Initial Wasm Stack Switching support #16779

Merged
merged 39 commits into from
May 27, 2022
Merged

Initial Wasm Stack Switching support #16779

merged 39 commits into from
May 27, 2022

Conversation

kripken
Copy link
Member

@kripken kripken commented Apr 21, 2022

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 the
ASYNCIFY flag. That's the smallest diff, and we do still use the Asyncify
API (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 an
internal flag (users do -fwasm-exceptions), so it's not a 100% relevant
precedent.

@kripken kripken requested a review from sbc100 April 21, 2022 20:14
Copy link
Collaborator

@sbc100 sbc100 left a 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:
Copy link
Collaborator

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?

Copy link
Member Author

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).

@kripken
Copy link
Member Author

kripken commented Apr 21, 2022

In terms of naming I think we should do with whatever the standard clang flag is for enabling stack switching.

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.

@sbc100
Copy link
Collaborator

sbc100 commented Apr 21, 2022

In terms of naming I think we should do with whatever the standard clang flag is for enabling stack switching.

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.

@kripken
Copy link
Member Author

kripken commented Apr 21, 2022

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?

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.

@fgmccabe
Copy link

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.

@kripken kripken marked this pull request as ready for review May 24, 2022 22:34
@kripken
Copy link
Member Author

kripken commented May 24, 2022

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 ASYNCIFY=2 (which can remain the internal option for it later, if we add a more user-friendly name).

If that makes sense to people then I think this PR is ready for review.

@fgmccabe
Copy link

Ok with me. Movement is better than perfect stillness.

@kripken
Copy link
Member Author

kripken commented May 25, 2022

Fixed some tests now.

@kripken kripken requested a review from sbc100 May 25, 2022 19:49
Copy link
Collaborator

@sbc100 sbc100 left a 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 \
Copy link
Collaborator

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')
Copy link
Collaborator

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?

Copy link
Member Author

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,
Copy link
Collaborator

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?

Copy link
Member Author

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.

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.

Copy link
Member Author

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();
Copy link
Collaborator

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?

Copy link
Member Author

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.

@@ -341,9 +424,11 @@ mergeInto(LibraryManager.library, {
},
},

emscripten_sleep__sig: 'vi', // TODO: others
Copy link
Collaborator

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?

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Collaborator

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.

Copy link
Member Author

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.

Copy link
Collaborator

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.

Copy link
Collaborator

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).

Copy link
Member Author

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.

Copy link
Collaborator

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 ...

Copy link
Member Author

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.

emscripten_sleep__deps: ['$safeSetTimeout'],
emscripten_sleep: function(ms) {
Asyncify.handleSleep((wakeUp) => safeSetTimeout(wakeUp, ms));
// TODO: add return in the others.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What others?

Copy link
Member Author

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).

@@ -16,6 +16,30 @@ function uleb128Encode(n) {
return [(n % 128) | 128, n >> 7];
}

function getTypeDescription(sig) {
Copy link
Collaborator

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?

Copy link
Member Author

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)
Copy link
Collaborator

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)
Copy link
Collaborator

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?

Copy link
Member Author

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 kripken merged commit 2ac0877 into main May 27, 2022
@kripken kripken deleted the asyncify-stack-switching branch May 27, 2022 19:35
@Akaricchi
Copy link
Contributor

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.

@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.

@kripken
Copy link
Member Author

kripken commented Jun 6, 2022

@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.

@fgmccabe
Copy link

fgmccabe commented Jun 6, 2022

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.
OTOH, having wasm-level stack switching for one proposal will make things a lot smoother

@kripken
Copy link
Member Author

kripken commented Jun 6, 2022

Sounds good @fgmccabe , we can experiment with fibers that way earlier, then.

@hoodmane
Copy link
Collaborator

hoodmane commented Jun 8, 2022

This is super exciting! Thanks @kripken et al!

@rth
Copy link
Contributor

rth commented Jun 9, 2022

This is exciting news! @kripken we were wondering,

  • what's the relationship of this PR with the WASM stack switching proposal?
  • this would work even without any browser-specific implementations for stack switching then? And if so does it mean that that proposal is no longer necessary? @hoodmane is updating the Emscripten version used in Pyodide so we can try it. Also, there is no changelog entry for this PR, was it intentional?

@hoodmane
Copy link
Collaborator

hoodmane commented Jun 9, 2022

I'm pretty sure this is dependent on the wasm stack switching proposal:
Asyncify.suspender = new WebAssembly.Suspender();
There is no such thing as WebAssembly.Suspender() in current browsers. So it's a ways off still unfortunately but it's still very exciting.

@rth
Copy link
Contributor

rth commented Jun 9, 2022

OK, makes sense https://twitter.com/emscripten/status/1530272023431114752 (and the below comments) answer my questions.

@kripken
Copy link
Member Author

kripken commented Jun 9, 2022

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 (--experimental-wasm-stack-switching; and you can get Chrome canary and run it with that flag for browser testing). I'd be curious for any findings from you or anyone else that wants to test it, but please expect that many things may just be broken atm.

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