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;
|
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:
|
/* 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
|
* when a body PARKS (asyncify suspension inside commit.Push), COROUTINE::Call
|
||||||
* RETURNS EARLY — the later asyncify rewind re-enters the fiber through the
|
* RETURNS EARLY — the later asyncify rewind re-enters the fiber through the
|
||||||
|
|
@ -118,6 +148,18 @@ inline void drainFibers()
|
||||||
|
|
||||||
while( !q.empty() )
|
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;
|
fiberBusy() = true;
|
||||||
slot.done = false;
|
slot.done = false;
|
||||||
slot.body = new std::function<void()>( std::move( q.front() ) );
|
slot.body = new std::function<void()>( std::move( q.front() ) );
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue