Support fiber API with JSPI - #27638
Conversation
The fiber API currently only supports Asyncify. JSPI allows suspending and resuming WebAssembly execution via native stack switching without the code size and performance overhead of Asyncify bytecode instrumentation. Mark asyncify_stack parameters as _Nullable in fiber.h since an Asyncify stack buffer is unnecessary under JSPI. Update the fiber test to run under both Asyncify and JSPI.
|
I'm kind of surprised we only have one test for this. I'm guessing this is not very widely used. |
| G.fibers[0].init_with_api(h1, &val); | ||
| G.fibers[1].init_with_api(h2, &val); | ||
| emscripten_fiber_swap(&G.main, &G.fibers[0].context); | ||
| printf("direct-%d-*\n", val); |
There was a problem hiding this comment.
Why do we need this new test case? Were we missing coverage of this?
There was a problem hiding this comment.
Yeah, the existing test only ever swapped back and forth between child fibers and the main fiber. There was no coverage for swapping directly between two child fibers without going through main. Added a comment to clarify this.
| #endif | ||
| var userData = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.user_data, '*') }}}; | ||
| var start = {{{ makeDynCall('vp', 'entryPoint', true) }}}; | ||
| start(userData).catch((e) => { |
There was a problem hiding this comment.
Does this mean the ``entryPoint` must return a Promise? Is that guaranteed?
There was a problem hiding this comment.
The third arg is promising = true so this will always return a promise. Added a comment.
sbc100
left a comment
There was a problem hiding this comment.
Very impressive how seemingly simple this is.
|
@Akaricchi are you still using fibers in emscripten? |
Yes, they are fundamental for Taisei Project's web port; in particular they are used by koishi as a coroutine backend. Koishi has a basic test, but it's known to be insufficient. I'll give this PR a try later. If you want to test it yourself, you can check Taisei's github actions workflows to see how to build it for emscripten. Coroutines are only used by the game logic right now, so you'd actually have to play for a bit (or idle in the main menu to trigger demo playback; mind that those desync on the master branch). |
|
@Akaricchi I remember back when you added fibre support you were pretty vigilant about getting max performance. I would be curious if you get a chance if you could confirm if the JSPI version is fast (hopefully it is) for you, and by how much? |
|
Sure, I'll run some benchmarks when/if I get this to work with Taisei. The performance of the current asyncify-based version is objectively pretty damn bad when compared to native, but it ended up being acceptable for Taisei. I expect this to be fine too, unless it does something egregious like yielding to the browser event loop for every swap. The code seemed fine at a glance, but I don't know the specifics of how JSPI works. |
JSPI does require a micro-task for every swap, that is kind of fundamentally how it works. This is not the same things as the full browser event loop though, and we would expect it to be cheaper overall than ASYNCIFY (which has its own runtime and code size impacts). |
|
|
||
| swap(oldFiber, newFiber) { | ||
| return new Promise((resolve) => { | ||
| Fibers.fiberResolvers.set(oldFiber, resolve); |
There was a problem hiding this comment.
I'm not sure I like this dependency on the fiber struct's address. The docs state:
This structure represents a Fiber context continuation. The runtime does not keep references to these objects, they only contain information needed to perform the context switch. The switch operation updates some of the contents, however.
and that is true for the asyncify version. So it's possible to, e.g. realloc() an array of fibers without breaking anything. Perhaps you can fix this by reusing the rewind_id field of asyncify_data_t (embedded into emscripten_fiber_t), e.g. allocate an integer handle for each resolve and associate that instead of the address.
What happens when a fiber is discarded and never resumed though? Is there a zombie entry stuck in the map then?
The fiber API currently only supports Asyncify. JSPI allows suspending and resuming WebAssembly execution via native stack switching without the code size and performance overhead of Asyncify bytecode instrumentation.
Mark asyncify_stack parameters as _Nullable in fiber.h since an Asyncify stack buffer is unnecessary under JSPI.
Update the fiber test to run under both Asyncify and JSPI.