WebAssembly / js-promise-integration

JavaScript Promise Integration
Other
64 stars 17 forks source link

What happens when JS and Wasm stack frames are sandwiched? #35

Open juj opened 3 months ago

juj commented 3 months ago

I am trying to understand what happens if I want to suspend the callstack of code that from JS event handler would call into Wasm, which would call out to JS, which would call back to Wasm, which might then call back out to JS to perform an JSPI'd operation?

What would be needed to pause the whole "sandwiched" callstack with multiple JS and Wasm frames in this case? Is there some kind of interaction with async function in JS required?

fgmccabe commented 3 months ago

In general, the sandwich scenario is going to lead to traps. The reason is that we are not allowing suspended JS frames (technically, we are not allowing the capture of JS frames). OTOH, if you are entering into wasm from JS via a promising export, then that call will be run on a separate stack. In addition, that call becomes a Promising export: the wasm call will be returning a Promise to JS. In short, you can use the wasm/JS sandwich if you also following the same pattern with wasm that you would with JS: use async functions.

juj commented 3 months ago

Thanks for the update.. this is going to be quite a challenge. In a large 3D application like Unity, control flow might route via JS and Wasm on the callstack, when function callbacks from external JS APIs and SDKs are in play

I'll have to prototype out how that would work.

Is there a way to ensure pausing a full sandwiched callstack, if I can ensure that all the middle JS frames are all async functions?

E.g. JS event handler -> Wasm1 -> JS where I guarantee all functions are async functions -> Wasm2 -> async JS that returns a Promise.

Should I be able to devise a mechanism of freezing Wasm1, that middle JS, and Wasm2 until the async JS resolves? Would all these middle JS functions work if they are async? Or if I suitably wrap all the middle JS functions into a Promise mechanism?

yurydelendik commented 3 months ago

Just to add: the execution of JS may rely on main stack to work (e.g. frontend, tiering, debugging, realm, GC, etc.) Allowing JS frame to suspend will be a huge headache for JS engines with JITs.

yurydelendik commented 3 months ago

E.g. JS event handler -> Wasm1 -> JS where I guarantee all functions are async functions -> Wasm2 -> async JS that returns a Promise.

Wasm1 -> JS and Wasm2 -> async JS will switch you to the main stack, where you can "re-enter" into Wasm with other promising call.

fgmccabe commented 3 months ago

I think that the crucial point here is that JS code calling into 'suspendable' computations currently uses the async/await pattern. (Ignoring wasm at this point). With that pattern in place, if the JS call looks like: await wasmCall() where wasmCall is implemented as a call to a promising export, there should be no issue with the code being on the main thread.

fgmccabe commented 3 months ago

JSPI is already reentrant. I agree that that can cause issues for wasm code: it must be structured correctly.

hoodmane commented 3 months ago

I've found that if I have a stack with alternating wasm and JavaScript like: wasm JS wasm JS wasm

and I stack switch for each wasm segment separately, it is quite slow and expensive memory-wise. There's a hint about this in the recent blog post:

However, this is not a sustainable strategy: we would like to support applications with millions of suspended coroutines; this is not possible if each stack is 1MB in size.

Perhaps if each of these switches allocates a megabyte this can account for a decent amount of the poor performance. So once the quality of implementation improves, maybe this pattern will be okay? For now, I've been trying to replace JS trampolines with dynamically generated webassembly. This works for my use case because most of the JS trampolines in my code can be implemented with dynamic wasm. In the general case, I guess you just have to pay the price until the implementation gets faster.