fix(wasm): never swap to a fiber while an Asyncify context is parked
Opening a large board could kill the editor runtime outright: "index out of bounds" / "unreachable executed" / "indirect call signature mismatch", after which every later focus/key event trapped. Reported in prod against a big uploaded board, and correlated by the reporter with the moment the presence WebSocket connects. That correlation is the tell. Presence/collab work enters the wasm through runOnFiber -> COROUTINE::Call, i.e. emscripten_fiber_swap, whose stop_unwind corrupts Asyncify's single currData slot when ANOTHER context is already parked there. docs/features/async/13 pins the invariant: exactly one unwind/rewind transition in flight, and prescribes "a single shared is-a- transition-in-flight guard the pumps consult before re-driving". Normally the slot is free when fibers drain: the main loop's per-frame wxWasmYieldToBrowser completes every frame, so drainFibers runs between yields. It is NOT free when a nested/modal pump tick drives ProcessEvents while the chain that opened the modal is parked deeper down — precisely a big board open (progress dialog over a parked load). The wx dispatch interlock does not cover this: the modal parks deliberately zero its count so their own pump may dispatch. So gate the swap itself: drainFibers defers (re-CallAfter) while asyncifyInFlight(). Bodies are viewport/overlay/apply work, so waiting out the park costs latency, never correctness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8jo7zz1ZwzYpjJ64UZKN4
This commit is contained in:
parent
19a713f454
commit
32218122c3
1 changed files with 42 additions and 0 deletions
|
|
@ -61,6 +61,36 @@ inline bool& fiberBusy()
|
|||
return busy;
|
||||
}
|
||||
|
||||
/**
|
||||
* True while an Asyncify unwind/rewind or park is IN FLIGHT (the single
|
||||
* `currData` slot is taken by somebody else's suspended context).
|
||||
*
|
||||
* Starting a fiber in that window is the documented crash family
|
||||
* (docs/features/async/13, "scheduler invariant"): `emscripten_fiber_swap`
|
||||
* calls `stop_unwind`, which asserts/corrupts when another context is parked —
|
||||
* surfacing as "index out of bounds", "unreachable executed" or "indirect call
|
||||
* signature mismatch", after which the runtime is poisoned and every later
|
||||
* event traps.
|
||||
*
|
||||
* Normally the slot is FREE here: the main loop's per-frame
|
||||
* `wxWasmYieldToBrowser` completes every frame, so `drainFibers` (a
|
||||
* CallAfter, drained from ProcessEvents) runs between yields with nothing in
|
||||
* flight. It is NOT free when a nested/modal pump tick drives ProcessEvents
|
||||
* while the chain that opened the modal is parked deeper down the stack —
|
||||
* exactly what a big board open does (progress dialog over a parked load),
|
||||
* and the window in which a presence/collab body arriving off the WebSocket
|
||||
* used to swap a fiber and take the runtime down.
|
||||
*
|
||||
* The wx dispatch interlock (wx/wasm/private/dispatch.h) does not cover this:
|
||||
* the modal parks deliberately zero its count so their own pump may dispatch.
|
||||
*/
|
||||
inline bool asyncifyInFlight()
|
||||
{
|
||||
return EM_ASM_INT( {
|
||||
return ( typeof Asyncify !== "undefined" && Asyncify.currData ) ? 1 : 0;
|
||||
} ) != 0;
|
||||
}
|
||||
|
||||
/* The in-flight body. HEAP-allocated and pinned for the body's whole life:
|
||||
* when a body PARKS (asyncify suspension inside commit.Push), COROUTINE::Call
|
||||
* RETURNS EARLY — the later asyncify rewind re-enters the fiber through the
|
||||
|
|
@ -118,6 +148,18 @@ inline void drainFibers()
|
|||
|
||||
while( !q.empty() )
|
||||
{
|
||||
// Someone else's context is parked in the single Asyncify slot: swapping
|
||||
// to a fiber now corrupts it (see asyncifyInFlight). Re-queue and retry
|
||||
// on the next drained tick — bodies are viewport/overlay/apply work, so
|
||||
// waiting out the park costs latency, never correctness.
|
||||
if( asyncifyInFlight() )
|
||||
{
|
||||
if( wxEvtHandler* h = fiberHandler() )
|
||||
h->CallAfter( []() { drainFibers(); } );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fiberBusy() = true;
|
||||
slot.done = false;
|
||||
slot.body = new std::function<void()>( std::move( q.front() ) );
|
||||
|
|
|
|||
Loading…
Reference in a new issue