jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove the inventory; every deletion verified by grep closure + full gates. Broken-right-now fixes: - deploy-staging.yml passed the retired opt_level input — the workflow could not even start. Removed. - env.sh carried dead exports with a live -sASYNCIFY=1 inside (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason. - docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone. Dead weight removed: - binaryen submodule (nothing builds or invokes it), wasm-opt-bench workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess scaffolding (existed to parallelize the deleted wasm-opt phase; the postprocess is a seconds-long node script and now runs inline), build-monitor's dead asyncify rows, sched-context orphan build output, dead .gitignore entries, the .jspi-assets spike dir (the two wf-result research JSONs moved to docs/features/async/migration-evidence/). - bindings: fiber_park.h + its 12 embind registrations (broken-if- called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route, main_stack_runner.h + 5 includes, the always-null context-sleep weak hook in nanosleep_yield.c. - shim: the backend field (installed-flag idempotency instead), noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the WasmTool fallback and string-dump normalize branch). - web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts (gerber-demo keeps it: it loads the deployed CDN release, which predates emscripten 6 — noted inline). Conditionals: all 'backend === jspi' checks reduced to scheduler- presence checks; races_quiescent re-keyed from Asyncify.state (vacuous) to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive, which is the probing activation's own window by definition). Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→ JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS, kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests), collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→ wasmTrapSignatures (lists byte-identical). Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused asserts re-keyed to live JSPI beacons; eeschema-load's failure message no longer sends the developer to a deleted script; wait-beacons' dead families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is unconstructible); the embind test.fail re-gated with the JSPI reason (plain embind invokers cannot suspend — verified still failing); lint-determinism now scans tests/jspi (166 files clean); eeschema-collab local-move gated to chromium (~50% flaky on FF even solo; pcbnew twin covers both engines). Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md describes the single-phase build; docs/features/async/README.md banner-marked historical and repointed at the NEW 23-jspi-runtime.md (current architecture: export census, turnstile, libcontext ownership + refusal contract, embind call shapes, the em-pthread service-wrapper trick, exception policy, known gaps). Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the quiescence-probe fix; the 3 other reds were verified contention flakes solo-green or the documented FF gate), web 76/0, jspi 18/18 both engines, vitest 295/295 + 17/17, all lints green, live-app census clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
This commit is contained in:
parent
3ee174e9b4
commit
9c475a804e
120 changed files with 1522 additions and 2974 deletions
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Shared plumbing for the per-editor collab binding TUs (eeschema_embind.cpp,
|
||||
* pcbnew_embind.cpp) — the frame-type-free half of the bridge: string/JSON
|
||||
* wire emitters to window.kicadCollab, the CallAfter+COROUTINE fiber idiom,
|
||||
* wire emitters to window.kicadCollab, the CallAfter+COROUTINE apply queue,
|
||||
* and the frame-generic test hooks. Header-only (the collab_presence_style.h
|
||||
* pattern), so the build script needs no extra objects and the merged
|
||||
* kicad_editor image links it without ODR issues.
|
||||
|
|
@ -28,112 +28,110 @@ namespace pcbjam_collab {
|
|||
inline std::string toUtf8( const wxString& s ) { return std::string( s.utf8_str() ); }
|
||||
|
||||
/**
|
||||
* Run a body on the editor's main loop AND on a libcontext fiber stack — the
|
||||
* exact context native tool edits run in. Embind ccalls / bare CallAfter
|
||||
* stacks mis-dispatch asyncify-instrumented virtual calls (invoke_* through a
|
||||
* stale table type traps, or silently no-ops); commits, GAL overlay work and
|
||||
* the s-expr formatters must therefore run through this. CallAfter queues
|
||||
* onto the app's pending-event list (drained every frame by the wasm main
|
||||
* loop, src/wasm/evtloop.cpp); COROUTINE::Call moves the body to the fiber.
|
||||
* Run a body on the editor's main loop AND inside a COROUTINE — the exact
|
||||
* context native tool edits run in. CallAfter queues onto the app's
|
||||
* pending-event list (drained every frame by the wasm main loop,
|
||||
* src/wasm/evtloop.cpp); COROUTINE::Call moves the body onto its own
|
||||
* coroutine stack. Commits, GAL overlay work and the s-expr formatters run
|
||||
* through this.
|
||||
*
|
||||
* SERIALIZED (drift-trio finding #10, standalone-hardening 0008 §10): bodies
|
||||
* run strictly one-at-a-time through a FIFO. The previous per-body
|
||||
* fire-and-forget coroutine interleaved under load: when a body PARKED
|
||||
* (asyncify suspension inside commit.Push — connectivity/GAL work), the main
|
||||
* loop kept draining pending events and started the NEXT body — a local
|
||||
* commit and a remote apply then ran interleaved on shared commit/listener
|
||||
* state (s_applyingRemote is a single global), silently losing applies on the
|
||||
* actively-editing receiver and, in the worst case, corrupting memory (fuzz
|
||||
* S10: wasm OOB on an observer). The busy flag is park-safe: an asyncify
|
||||
* suspension suspends the whole drain loop with the body and rewinds it
|
||||
* transparently, while any other drain invocation no-ops on the flag; the
|
||||
* suspended drain's own while-loop picks up whatever queued meanwhile.
|
||||
* run strictly one-at-a-time through a FIFO. A body that SUSPENDS (a JSPI
|
||||
* suspension inside commit.Push — connectivity/GAL work) returns early from
|
||||
* COROUTINE::Call while its coroutine is still in flight. Without the queue
|
||||
* the main loop kept draining pending events and started the NEXT body — a
|
||||
* local commit and a remote apply then ran interleaved on shared
|
||||
* commit/listener state (s_applyingRemote is a single global), silently
|
||||
* losing applies on the actively-editing receiver and, in the worst case,
|
||||
* corrupting memory (fuzz S10: wasm OOB on an observer). The busy flag is
|
||||
* suspension-safe: while a suspended body is in flight every other drain
|
||||
* invocation no-ops on the flag, and the body's own coroutine tail
|
||||
* re-schedules the drain when it completes.
|
||||
*/
|
||||
inline std::deque<std::function<void()>>& fiberQueue()
|
||||
inline std::deque<std::function<void()>>& applyQueue()
|
||||
{
|
||||
static std::deque<std::function<void()>> q;
|
||||
return q;
|
||||
}
|
||||
|
||||
inline bool& fiberBusy()
|
||||
inline bool& applyBusy()
|
||||
{
|
||||
static bool busy = false;
|
||||
return busy;
|
||||
}
|
||||
|
||||
/* 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
|
||||
* SAME callable at the SAME addresses (dynCall_vi → fcontext_entry →
|
||||
* callerStub → the wrapper). Stack-local cor/body (the original runOnFiber
|
||||
* AND the first serialized version) were destroyed on that early return, so
|
||||
* the rewind called through freed objects — "table index is out of bounds"
|
||||
* at rewind, memory corruption downstream (finding #10b's symbolized stack).
|
||||
* `done` is the ONLY completion signal; Call() returning is not. */
|
||||
struct FiberSlot
|
||||
* when a body SUSPENDS (a JSPI suspension inside commit.Push), COROUTINE::Call
|
||||
* RETURNS EARLY — the later resume re-enters the coroutine through the SAME
|
||||
* callable at the SAME addresses. Stack-local cor/body (the original
|
||||
* runOnCoroutine AND the first serialized version) were destroyed on that
|
||||
* early return, so the resume ran through freed objects — memory corruption
|
||||
* downstream (finding #10b's symbolized stack). `done` is the ONLY completion
|
||||
* signal; Call() returning is not. */
|
||||
struct ApplySlot
|
||||
{
|
||||
COROUTINE<int, int>* cor = nullptr;
|
||||
std::function<void()>* body = nullptr;
|
||||
bool done = false;
|
||||
};
|
||||
|
||||
inline FiberSlot& activeFiberSlot()
|
||||
inline ApplySlot& activeApplySlot()
|
||||
{
|
||||
static FiberSlot s;
|
||||
static ApplySlot s;
|
||||
return s;
|
||||
}
|
||||
|
||||
inline wxEvtHandler*& fiberHandler()
|
||||
inline wxEvtHandler*& applyHandler()
|
||||
{
|
||||
static wxEvtHandler* h = nullptr;
|
||||
return h;
|
||||
}
|
||||
|
||||
inline void drainFibers();
|
||||
inline void drainApplies();
|
||||
|
||||
inline void reapFiber()
|
||||
inline void reapApply()
|
||||
{
|
||||
FiberSlot& slot = activeFiberSlot();
|
||||
ApplySlot& slot = activeApplySlot();
|
||||
delete slot.cor;
|
||||
delete slot.body;
|
||||
slot.cor = nullptr;
|
||||
slot.body = nullptr;
|
||||
slot.done = false;
|
||||
fiberBusy() = false;
|
||||
applyBusy() = false;
|
||||
}
|
||||
|
||||
inline void drainFibers()
|
||||
inline void drainApplies()
|
||||
{
|
||||
FiberSlot& slot = activeFiberSlot();
|
||||
ApplySlot& slot = activeApplySlot();
|
||||
|
||||
if( fiberBusy() )
|
||||
if( applyBusy() )
|
||||
{
|
||||
if( !slot.done )
|
||||
return; // parked body still in flight — its tail re-drains
|
||||
return; // suspended body still in flight — its tail re-drains
|
||||
|
||||
reapFiber(); // completed via rewind since the last drain
|
||||
reapApply(); // completed since the last drain
|
||||
}
|
||||
|
||||
auto& q = fiberQueue();
|
||||
auto& q = applyQueue();
|
||||
|
||||
while( !q.empty() )
|
||||
{
|
||||
fiberBusy() = true;
|
||||
applyBusy() = true;
|
||||
slot.done = false;
|
||||
slot.body = new std::function<void()>( std::move( q.front() ) );
|
||||
q.pop_front();
|
||||
|
||||
slot.cor = new COROUTINE<int, int>( []( int ) -> int
|
||||
{
|
||||
FiberSlot& sl = activeFiberSlot();
|
||||
ApplySlot& sl = activeApplySlot();
|
||||
( *sl.body )();
|
||||
sl.done = true;
|
||||
|
||||
// If we parked, no drain is pending by the time the rewind
|
||||
// completes — schedule the reap + next body from the fiber tail
|
||||
// (CallAfter only queues; safe here).
|
||||
if( wxEvtHandler* h = fiberHandler() )
|
||||
h->CallAfter( []() { drainFibers(); } );
|
||||
// If we suspended, no drain is pending by the time the body
|
||||
// completes — schedule the reap + next body from the coroutine
|
||||
// tail (CallAfter only queues; safe here).
|
||||
if( wxEvtHandler* h = applyHandler() )
|
||||
h->CallAfter( []() { drainApplies(); } );
|
||||
|
||||
return 0;
|
||||
} );
|
||||
|
|
@ -141,17 +139,17 @@ inline void drainFibers()
|
|||
slot.cor->Call( 0 );
|
||||
|
||||
if( !slot.done )
|
||||
return; // parked — cor/body stay pinned for the rewind
|
||||
return; // suspended — cor/body stay pinned for the resume
|
||||
|
||||
reapFiber();
|
||||
reapApply();
|
||||
}
|
||||
}
|
||||
|
||||
inline void runOnFiber( wxEvtHandler* aHandler, std::function<void()> aBody )
|
||||
inline void runOnCoroutine( wxEvtHandler* aHandler, std::function<void()> aBody )
|
||||
{
|
||||
fiberHandler() = aHandler;
|
||||
fiberQueue().push_back( std::move( aBody ) );
|
||||
aHandler->CallAfter( []() { drainFibers(); } );
|
||||
applyHandler() = aHandler;
|
||||
applyQueue().push_back( std::move( aBody ) );
|
||||
aHandler->CallAfter( []() { drainApplies(); } );
|
||||
}
|
||||
|
||||
// ── C++ → JS wire emitters (no-ops without a JS listener) ───────────────────
|
||||
|
|
@ -205,14 +203,14 @@ inline void emitViewport( double aCx, double aCy, double aPxPerIu, int aW, int a
|
|||
|
||||
// ── frame-generic test hooks (ysync miss 09) ────────────────────────────────
|
||||
|
||||
/** Run Edit>Undo exactly like the UI would (main-loop + fiber stack) —
|
||||
/** Run Edit>Undo exactly like the UI would (main loop + apply coroutine) —
|
||||
* exercises the local-ops-only undo policy and the stale-picker UUID guard. */
|
||||
inline bool testUndo( EDA_BASE_FRAME* aFrame )
|
||||
{
|
||||
if( !aFrame )
|
||||
return false;
|
||||
|
||||
runOnFiber( aFrame, [aFrame]() { aFrame->GetToolManager()->RunAction( ACTIONS::undo ); } );
|
||||
runOnCoroutine( aFrame, [aFrame]() { aFrame->GetToolManager()->RunAction( ACTIONS::undo ); } );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ struct CORE
|
|||
* (center + half-extents, IU) into this canvas — contain, never crop:
|
||||
* the follower's zoom is derived from ITS OWN canvas size, so leaders
|
||||
* and followers on different monitors see the same world region.
|
||||
* Fiber like every other view mutation from JS. */
|
||||
* Apply coroutine like every other view mutation from JS. */
|
||||
void fitViewport( double aCx, double aCy, double aHalfW, double aHalfH )
|
||||
{
|
||||
EDA_DRAW_FRAME* fr = frame();
|
||||
|
|
@ -258,7 +258,7 @@ struct CORE
|
|||
if( !fr || aHalfW <= 0 || aHalfH <= 0 )
|
||||
return;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [this, fr, aCx, aCy, aHalfW, aHalfH]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [this, fr, aCx, aCy, aHalfW, aHalfH]() {
|
||||
KIGFX::VIEW* view = fr->GetCanvas()->GetView();
|
||||
const VECTOR2I& sz = view->GetScreenPixelSize();
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ struct CORE
|
|||
}
|
||||
|
||||
/** kicadCollabSetViewport (0005): pan to a world position (comment panel
|
||||
* "jump to pin"). Fiber like every other view mutation from JS. */
|
||||
* "jump to pin"). Apply coroutine like every other view mutation from JS. */
|
||||
void panTo( double aCx, double aCy )
|
||||
{
|
||||
EDA_DRAW_FRAME* fr = frame();
|
||||
|
|
@ -287,7 +287,7 @@ struct CORE
|
|||
if( !fr )
|
||||
return;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [this, fr, aCx, aCy]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [this, fr, aCx, aCy]() {
|
||||
fr->GetCanvas()->GetView()->SetCenter( VECTOR2D( aCx, aCy ) );
|
||||
fr->GetCanvas()->ForceRefresh();
|
||||
emitViewportIfChanged();
|
||||
|
|
@ -311,7 +311,8 @@ struct CORE
|
|||
return;
|
||||
|
||||
// Screen→world via the non-virtual VIEW::ToWorld (the virtual
|
||||
// VIEW_CONTROLS::GetMousePosition is an asyncify dispatch risk here).
|
||||
// VIEW_CONTROLS::GetMousePosition mis-dispatched here under the
|
||||
// retired asyncify runtime; the direct call stays).
|
||||
wxPoint p = aEvt.GetPosition();
|
||||
VECTOR2D world = fr->GetCanvas()->GetView()->ToWorld( VECTOR2D( p.x, p.y ), true );
|
||||
|
||||
|
|
@ -327,9 +328,9 @@ struct CORE
|
|||
|
||||
// ── remote render ─────────────────────────────────────────────────────
|
||||
|
||||
// Repaint the remote-peers overlay. Runs in CallAfter + COROUTINE: the
|
||||
// first MakeOverlay() view->Add and the items' virtual ViewBBox() need the
|
||||
// fiber stack (asyncify virtual dispatch — same constraint as the apply).
|
||||
// Repaint the remote-peers overlay. Runs in CallAfter + COROUTINE via the
|
||||
// apply queue — serialized with the applies, same constraint as every
|
||||
// other view mutation from JS.
|
||||
void redrawOverlay()
|
||||
{
|
||||
redrawScheduled = false;
|
||||
|
|
@ -407,7 +408,7 @@ struct CORE
|
|||
return;
|
||||
|
||||
redrawScheduled = true;
|
||||
pcbjam_collab::runOnFiber( fr, [this]() { redrawOverlay(); } );
|
||||
pcbjam_collab::runOnCoroutine( fr, [this]() { redrawOverlay(); } );
|
||||
}
|
||||
|
||||
// ── JS entry-point bodies ─────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@
|
|||
#include <pcbjam_remote_lock.h>
|
||||
#include "collab_common.h"
|
||||
#include "open_gate.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "collab_presence_core.h"
|
||||
#include "collab_presence_style.h"
|
||||
#include "pcbjam_theme.h"
|
||||
|
|
@ -85,7 +84,7 @@ using json = nlohmann::json;
|
|||
#ifndef KICAD_MERGED_EMBIND
|
||||
bool kicadOpenFile( std::string path )
|
||||
{
|
||||
// Held across every Asyncify park of the load; see open_gate.h.
|
||||
// Held across every suspension of the load; see open_gate.h.
|
||||
pcbjam_open::BusyGuard busy;
|
||||
|
||||
if( pcbjam_open::testParkMs() > 0 )
|
||||
|
|
@ -302,10 +301,10 @@ SCH_ITEM* makeItem( const json& j )
|
|||
else if( type == "SCH_SHAPE" )
|
||||
{
|
||||
// Reconstruct from the geometry itemToJson emits. Committing a *new* SCH_SHAPE used to
|
||||
// trap in SCH_COMMIT::Push's CHT_ADD path (GAL view->Add → an asyncify invoke_viii
|
||||
// mis-dispatch, "memory access out of bounds") because doApply ran off a fiber stack;
|
||||
// doApply now runs inside a COROUTINE (kicadCollabApply) so the add dispatches correctly,
|
||||
// exactly as a native draw does. (0006/0007.) NB FILL_T::NO_FILL == 1, not 0.
|
||||
// trap in SCH_COMMIT::Push's CHT_ADD path under the retired asyncify runtime because
|
||||
// doApply ran off the coroutine context; doApply runs inside a COROUTINE
|
||||
// (kicadCollabApply), serialized with local edits, exactly as a native draw does.
|
||||
// (0006/0007.) NB FILL_T::NO_FILL == 1, not 0.
|
||||
SHAPE_T st = (SHAPE_T) j.value( "stype", (int) SHAPE_T::RECTANGLE );
|
||||
int layer = j.value( "layer", (int) LAYER_NOTES );
|
||||
int width = j.value( "width", 0 );
|
||||
|
|
@ -577,10 +576,11 @@ void flushDiff()
|
|||
}
|
||||
|
||||
// Coalesce all the listener callbacks of one commit (and any other edits in the same loop
|
||||
// turn) into a single post-settle diff. flushDiff runs inside a COROUTINE: its v2 items
|
||||
// emit serializes items via SCH_IO_KICAD_SEXPR::Format (itemBlob), whose virtual dispatch
|
||||
// is only reliable on the libcontext fiber stack — on the bare CallAfter stack it traps
|
||||
// and silently kills the whole flush, legacy emit included (same lesson as doApply, 0007).
|
||||
// turn) into a single post-settle diff. flushDiff runs inside a COROUTINE via the apply
|
||||
// queue: its v2 items emit serializes items via SCH_IO_KICAD_SEXPR::Format (itemBlob), and
|
||||
// queueing it with the applies keeps emits from interleaving with a suspended apply body
|
||||
// (under the retired asyncify runtime the bare CallAfter stack additionally trapped here —
|
||||
// same lesson as doApply, 0007).
|
||||
void scheduleFlush()
|
||||
{
|
||||
if( g_flushScheduled )
|
||||
|
|
@ -589,7 +589,7 @@ void scheduleFlush()
|
|||
g_flushScheduled = true;
|
||||
|
||||
if( SCH_EDIT_FRAME* fr = schFrame() )
|
||||
pcbjam_collab::runOnFiber( fr, []() { flushDiff(); } );
|
||||
pcbjam_collab::runOnCoroutine( fr, []() { flushDiff(); } );
|
||||
else
|
||||
flushDiff();
|
||||
}
|
||||
|
|
@ -598,9 +598,9 @@ void scheduleFlush()
|
|||
// to the child .kicad_sch file and tell the standalone (window.kicadCollab.onSheetCreated),
|
||||
// so the child is persisted/registered the moment it's created — without waiting for the
|
||||
// user to enter it or save the project. Otherwise the parent's `(sheet … child)` reference
|
||||
// dangles for peers / on reload. Deferred onto the fiber stack (CallAfter + COROUTINE):
|
||||
// SCH_IO_KICAD_SEXPR::Format's virtual dispatch traps on the bare listener/CallAfter stack,
|
||||
// same as flushDiff/doApply. The sheet is re-resolved by uuid in the deferred body so a
|
||||
// dangles for peers / on reload. Deferred onto the apply coroutine (CallAfter + COROUTINE),
|
||||
// same as flushDiff/doApply, so the save serializes with any in-flight apply.
|
||||
// The sheet is re-resolved by uuid in the deferred body so a
|
||||
// since-deleted sheet (e.g. an immediate undo) is a no-op rather than a dangling pointer.
|
||||
void scheduleSheetSave( SCH_SHEET* aSheet )
|
||||
{
|
||||
|
|
@ -616,7 +616,7 @@ void scheduleSheetSave( SCH_SHEET* aSheet )
|
|||
std::string childAbs = toUtf8( childFn.GetFullPath() );
|
||||
std::string uuid = toUtf8( aSheet->m_Uuid.AsString() );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, childAbs, uuid]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, childAbs, uuid]() {
|
||||
KIID kid( wxString::FromUTF8( uuid.c_str() ) );
|
||||
SCH_ITEM* item = fr->Schematic().ResolveItem( kid, nullptr, /*allowNull*/ true );
|
||||
|
||||
|
|
@ -834,24 +834,23 @@ void schedulePresenceSelCheck()
|
|||
namespace {
|
||||
|
||||
// SCH_SYMBOL::Move() / SCH_LABEL_BASE::Move() move their child fields (reference, value, …) via
|
||||
// an inner `field.Move()` — itself a virtual call that mis-dispatches in the apply context, so
|
||||
// the field text is left behind at its old position while the body moves. Re-move the fields
|
||||
// with a devirtualized call so the labels follow the symbol on the peer. (The inner call is a
|
||||
// harmless no-op when it mis-dispatches — the fields stay put — so this doesn't double-move.)
|
||||
// an inner `field.Move()`. Under the retired asyncify runtime that inner call silently no-oped
|
||||
// in the apply context — the field text was left behind at its old position while the body
|
||||
// moved — so the fields are re-moved here with a devirtualized call. Kept as-is through the
|
||||
// JSPI migration; the collab move/drift suites pin the peer's field positions.
|
||||
void moveFields( std::vector<SCH_FIELD>& aFields, const VECTOR2I& aDelta )
|
||||
{
|
||||
for( SCH_FIELD& field : aFields )
|
||||
field.SCH_FIELD::Move( aDelta );
|
||||
}
|
||||
|
||||
// Move an item to an absolute position for the `changed` path. SCH_ITEM::Move() is virtual;
|
||||
// dispatching it through the vtable from the apply/CallAfter context hits the asyncify
|
||||
// call_indirect mis-dispatch and silently NO-OPS (so symbols/junctions/labels never moved on
|
||||
// the peer — only SCH_LINE worked, via its direct SetStart/EndPoint path). GetPosition() reads
|
||||
// fine (it's a plain virtual read; see 0003 / eeschema_collab_asyncify_apply). The fix:
|
||||
// devirtualize Move() with an explicit class-qualified call, which is statically bound — a
|
||||
// plain wasm `call`, not an instrumented call_indirect — so it actually executes. Composite
|
||||
// items additionally need their child fields moved (see moveFields).
|
||||
// Move an item to an absolute position for the `changed` path. Under the retired asyncify
|
||||
// runtime the virtual SCH_ITEM::Move(), dispatched from the apply/CallAfter context, silently
|
||||
// NO-OPED (symbols/junctions/labels never moved on the peer — only SCH_LINE worked, via its
|
||||
// direct SetStart/EndPoint path; see 0003 / eeschema_collab_asyncify_apply). Move() is
|
||||
// therefore devirtualized with explicit class-qualified calls — statically bound plain wasm
|
||||
// calls, correct on any runtime, so they stay. Composite items additionally need their child
|
||||
// fields moved (see moveFields).
|
||||
void moveItemTo( SCH_ITEM* aItem, const VECTOR2I& aNewPos )
|
||||
{
|
||||
VECTOR2I delta = aNewPos - aItem->GetPosition();
|
||||
|
|
@ -887,9 +886,8 @@ void moveItemTo( SCH_ITEM* aItem, const VECTOR2I& aNewPos )
|
|||
}
|
||||
|
||||
// The actual model mutation, via SCH_COMMIT so connectivity/ERC recompute as for a UI
|
||||
// edit. (Editor write ops like SCH_ITEM::Move are called through invoke_vii, whose
|
||||
// asyncify-instrumented dynCall trampoline traps on a stale type — fixed at the JS shim
|
||||
// layer in scripts/common/shims/dyncall-binding.js.tmpl; see 0003.)
|
||||
// edit. (0003 decoded an asyncify-era dynCall trap on this path; the shim that healed
|
||||
// it retired with that runtime.)
|
||||
void doApply( SCH_EDIT_FRAME* aFrame, const json& aDelta )
|
||||
{
|
||||
SCHEMATIC& sch = aFrame->Schematic();
|
||||
|
|
@ -1140,17 +1138,16 @@ void collabTestMove( SCH_EDIT_FRAME* aFrame, SCH_ITEM* aItem, SCH_SCREEN* aScree
|
|||
// JS → C++. Apply a remote per-item delta by uuid, through SCH_COMMIT so connectivity/
|
||||
// ERC/hierarchy recompute the same way a UI edit would (0003 §apply).
|
||||
//
|
||||
// SCH_COMMIT must run in the editor's Asyncify-rooted main loop — invoking it from this
|
||||
// embind ccall, or from an emscripten_async_call/setTimeout callback, traps with an
|
||||
// "indirect call signature mismatch" because those are not the asyncify root (0001 §5).
|
||||
// wxEvtHandler::CallAfter queues onto the app's pending-event list, which the wasm main
|
||||
// loop drains every frame via ProcessPendingEvents() (src/wasm/evtloop.cpp) — i.e. the
|
||||
// exact context real UI edits run in. So defer the whole mutation there.
|
||||
// SCH_COMMIT must run on the editor's main loop, not on this embind ccall or an
|
||||
// emscripten_async_call/setTimeout callback: wxEvtHandler::CallAfter queues onto the
|
||||
// app's pending-event list, which the wasm main loop drains every frame via
|
||||
// ProcessPendingEvents() (src/wasm/evtloop.cpp) — i.e. the exact context real UI edits
|
||||
// run in. So defer the whole mutation there.
|
||||
void schCollabApply( std::string aJson )
|
||||
{
|
||||
// Open-in-flight guard (open_gate.h): never touch the model while a
|
||||
// kicadOpenFile Asyncify chain is parked mid-load — commits/virtuals on a
|
||||
// half-built schematic mis-dispatch ("indirect call signature mismatch").
|
||||
// kicadOpenFile chain is suspended mid-load — commits/virtuals would walk
|
||||
// a half-built schematic mid-mutation.
|
||||
// Callers gate on kicadOpenFileBusy; fuzzed by tests/kicad/collab-load-fuzz.spec.ts.
|
||||
if( pcbjam_open::busy() )
|
||||
return;
|
||||
|
|
@ -1165,12 +1162,11 @@ void schCollabApply( std::string aJson )
|
|||
if( !fr )
|
||||
return;
|
||||
|
||||
// Defer to the editor's main-loop context + fiber stack (runOnFiber) so SCH_COMMIT runs
|
||||
// like a normal edit: SCH_COMMIT::Push's CHT_ADD of a *new* SCH_SHAPE/SCH_SYMBOL
|
||||
// dispatches GAL virtuals (view->Add → ViewGetLayers) through asyncify-instrumented
|
||||
// invoke_*; off the fiber stack those mis-dispatch and trap inside KiCad core, which the
|
||||
// bridge can't devirtualize. On the fiber stack they dispatch correctly. (0007.)
|
||||
pcbjam_collab::runOnFiber( fr, [fr, delta]() { doApply( fr, delta ); } );
|
||||
// Defer to the editor's main loop + apply coroutine (runOnCoroutine) so SCH_COMMIT runs
|
||||
// like a normal edit: a commit body that suspends (connectivity/GAL work) returns early
|
||||
// from COROUTINE::Call, so applies must serialize with each other and with local edits
|
||||
// or they interleave on shared commit/listener state. (0007, drift-trio #10.)
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, delta]() { doApply( fr, delta ); } );
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1211,7 +1207,7 @@ void schCollabApplyItems( std::string aJson )
|
|||
if( !fr )
|
||||
return;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, wire]() { doApplyItems( fr, wire ); } );
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, wire]() { doApplyItems( fr, wire ); } );
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1353,7 +1349,7 @@ bool schCollabTestRemoveItem( std::string aId )
|
|||
|
||||
SCH_SCREEN* screen = path.LastScreen();
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, item, screen]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, item, screen]() {
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Remove( item, screen );
|
||||
commit.Push( wxT( "Collab test remove" ) );
|
||||
|
|
@ -1382,7 +1378,7 @@ bool schCollabTestRotateItem( std::string aId, double aDeg )
|
|||
SCH_SCREEN* screen = path.LastScreen();
|
||||
int steps = ( (int) ( aDeg / 90.0 + ( aDeg >= 0 ? 0.5 : -0.5 ) ) % 4 + 4 ) % 4;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, item, screen, steps]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, item, screen, steps]() {
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Modify( item, screen );
|
||||
|
||||
|
|
@ -1427,7 +1423,7 @@ bool schCollabTestSetFieldText( std::string aId, std::string aText )
|
|||
SCH_SCREEN* screen = path.LastScreen();
|
||||
wxString text = wxString::FromUTF8( aText.c_str() );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, sym, screen, text]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, sym, screen, text]() {
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Modify( sym, screen );
|
||||
sym->SetValueFieldText( text );
|
||||
|
|
@ -1440,7 +1436,7 @@ bool schCollabTestSetFieldText( std::string aId, std::string aText )
|
|||
|
||||
// ── drift-trio phase B action hooks (standalone-hardening 0008 §5) ───────────
|
||||
// Creation/mutation primitives for the trio harness's action catalog. Each
|
||||
// drives a REAL SCH_COMMIT on the fiber stack, so the SCHEMATIC_LISTENER →
|
||||
// drives a REAL SCH_COMMIT on the apply coroutine, so the SCHEMATIC_LISTENER →
|
||||
// flushDiff emit path runs exactly as for a UI edit. Names are tool-unique
|
||||
// (registered outside the KICAD_MERGED_EMBIND guard — same convention as
|
||||
// kicadCollabTestSetFieldText), so the merged image needs no dispatcher.
|
||||
|
|
@ -1467,7 +1463,7 @@ static std::string schCollabTestCommitAdd( SCH_ITEM* aItem, const wxChar* aMsg )
|
|||
std::string id = toUtf8( aItem->m_Uuid.AsString() );
|
||||
wxString msg( aMsg );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aItem, screen, msg]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aItem, screen, msg]() {
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Add( aItem, screen );
|
||||
commit.Push( msg );
|
||||
|
|
@ -1549,7 +1545,7 @@ std::string schCollabTestAddSymbol( std::string aLibId, int aX, int aY, std::str
|
|||
|
||||
std::string id = toUtf8( sym->m_Uuid.AsString() );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, sym, screen]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, sym, screen]() {
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Add( sym, screen );
|
||||
commit.Push( wxT( "Collab test add symbol" ) );
|
||||
|
|
@ -1600,7 +1596,7 @@ bool schCollabTestMirrorSchItem( std::string aId, bool aHorizontal )
|
|||
if( !item )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aId, aHorizontal]() { // re-resolve on the fiber (S4)
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aId, aHorizontal]() { // re-resolve on the coroutine (S4)
|
||||
SCH_SHEET_PATH path;
|
||||
SCH_ITEM* live = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||
&path, /*allowNull*/ true );
|
||||
|
|
@ -1643,7 +1639,7 @@ std::string schCollabTestDuplicateSchItem( std::string aId, int aDx, int aDy )
|
|||
|
||||
std::string id = toUtf8( dup->m_Uuid.AsString() );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, dup, screen]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, dup, screen]() {
|
||||
SCH_COMMIT commit( fr );
|
||||
commit.Add( dup, screen );
|
||||
commit.Push( wxT( "Collab test duplicate" ) );
|
||||
|
|
@ -2038,9 +2034,9 @@ void kicadSaveSchematic( std::string path )
|
|||
}
|
||||
|
||||
|
||||
static bool kicadCollabFiberBusyProbe()
|
||||
static bool kicadCollabBusyProbe()
|
||||
{
|
||||
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
|
||||
return pcbjam_collab::applyBusy() || !pcbjam_collab::applyQueue().empty();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(eeschema) {
|
||||
|
|
@ -2065,7 +2061,7 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
|||
function("kicadOpenFile", &kicadOpenFile PCBJAM_PARKER_POLICY);
|
||||
function("kicadOpenFileBusy", &kicadOpenFileBusy);
|
||||
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
||||
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
|
||||
function("kicadCollabBusy", &kicadCollabBusyProbe);
|
||||
// Read-only viewer lock (read-only-viewer).
|
||||
function("kicadSetReadOnly", &kicadSetReadOnly);
|
||||
// Yjs collaborative bridge entry points (same contract as pl_editor).
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
/*
|
||||
* Test-only repro lever for the DECODED production board-load trap
|
||||
* (docs/features/async/15-timer-park-repro.md round 3, 2026-07-31):
|
||||
* resuming a KiCad coroutine while its body is asyncify-parked inside
|
||||
* handleSleep.
|
||||
*
|
||||
* Two suspension protocols share one context on wasm. A coroutine suspended
|
||||
* by a real yield (fiber_swap) has valid rewind data in its fiber struct; a
|
||||
* coroutine whose body parked via handleSleep (lib-bridge wait,
|
||||
* emscripten_sleep) does NOT — its live state is in the sleep's buffer,
|
||||
* invisible to the fiber machinery and to TOOL_MANAGER. Resume() then swaps
|
||||
* into the STALE fiber data: finishContextSwitch → doRewind → "unreachable
|
||||
* executed", and every later entry reads poisoned Asyncify state ("index out
|
||||
* of bounds"). Impossible natively — a coroutine cannot be suspended without
|
||||
* yielding.
|
||||
*
|
||||
* The lever stages the prod state machine exactly:
|
||||
* start(parkMs): Call() a coroutine that immediately KiYield()s — this
|
||||
* writes VALID suspension data once and clears the fresh-entry path, the
|
||||
* state every long-lived tool loop is in.
|
||||
* prime(): Resume() it legitimately — the body then emscripten_sleep()s
|
||||
* (the internal park; prime's Resume ghost-returns per the epoch
|
||||
* machinery) and afterwards KiYield()s again.
|
||||
* poke(): Resume() DURING the sleep — the fatal prod operation. Unfixed
|
||||
* runtime: rewinds the stale suspension → the exact prod trap. Fixed
|
||||
* runtime: the jump is refused (null INVOCATION_ARGS, same contract as
|
||||
* jump-ghost) and the body completes undisturbed; a later poke() after
|
||||
* the second KiYield resumes it for real.
|
||||
*
|
||||
* Production is inert: nothing runs unless start() is called.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <emscripten.h>
|
||||
#include <tool/coroutine.h>
|
||||
|
||||
namespace pcbjam_fiber_park
|
||||
{
|
||||
|
||||
struct State
|
||||
{
|
||||
// 0 idle · 1 yielded-once (primed suspension) · 2 in the sleep park ·
|
||||
// 3 woke, yielded again · 4 resumed past second yield · 5 body returned
|
||||
int phase = 0;
|
||||
int parkMs = 0;
|
||||
int pokes = 0;
|
||||
// Second coroutine (poisoned-attribution scenario): 0 idle · 1 yielded ·
|
||||
// 2 completed. Starting it while the FIRST body is asyncify-parked makes
|
||||
// libcontext attribute the jump's old side to that parked fiber
|
||||
// (g_current_context is stale), writing a fresh suspension into its
|
||||
// struct — the exact laundering that let the prod resume bypass the
|
||||
// swap_suspended guard.
|
||||
int phase2 = 0;
|
||||
};
|
||||
|
||||
inline State& state()
|
||||
{
|
||||
static State s_state;
|
||||
return s_state;
|
||||
}
|
||||
|
||||
inline COROUTINE<int, int>*& co()
|
||||
{
|
||||
static COROUTINE<int, int>* s_co = nullptr;
|
||||
return s_co;
|
||||
}
|
||||
|
||||
inline int fiberBody( int )
|
||||
{
|
||||
state().phase = 1;
|
||||
co()->KiYield();
|
||||
|
||||
state().phase = 2;
|
||||
if( state().parkMs > 0 )
|
||||
emscripten_sleep( state().parkMs );
|
||||
|
||||
state().phase = 3;
|
||||
co()->KiYield();
|
||||
|
||||
state().phase = 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Call() + first KiYield: coroutine now has VALID fiber suspension data. */
|
||||
inline bool start( int aParkMs )
|
||||
{
|
||||
if( co() && co()->Running() )
|
||||
return false; // one in flight; the spec drives one cycle at a time
|
||||
|
||||
delete co();
|
||||
state() = State();
|
||||
state().parkMs = aParkMs;
|
||||
co() = new COROUTINE<int, int>( fiberBody );
|
||||
co()->Call( 0 );
|
||||
return state().phase == 1;
|
||||
}
|
||||
|
||||
/** Legitimate Resume into the primed yield; the body then parks. Ghost-returns. */
|
||||
inline bool prime()
|
||||
{
|
||||
if( !co() )
|
||||
return false;
|
||||
|
||||
return co()->Resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume() regardless of the body's suspension state — what TOOL_MANAGER does
|
||||
* on the next event, unaware the body is asyncify-parked. Counted so the spec
|
||||
* can correlate pokes with phases.
|
||||
*/
|
||||
inline bool poke()
|
||||
{
|
||||
if( !co() )
|
||||
return false;
|
||||
|
||||
++state().pokes;
|
||||
return co()->Resume();
|
||||
}
|
||||
|
||||
inline COROUTINE<int, int>*& co2()
|
||||
{
|
||||
static COROUTINE<int, int>* s_co2 = nullptr;
|
||||
return s_co2;
|
||||
}
|
||||
|
||||
inline int fiberBody2( int )
|
||||
{
|
||||
state().phase2 = 1;
|
||||
co2()->KiYield();
|
||||
state().phase2 = 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a SECOND coroutine while the first body is asyncify-parked. Because
|
||||
* g_current_context still points at the parked fiber, libcontext attributes
|
||||
* this jump's old side to it: the swap writes a fresh (foreign) suspension
|
||||
* into the PARKED fiber's struct and re-marks it swap_suspended — the
|
||||
* laundering that lets a later Resume bypass the C++ guard. The JS
|
||||
* stale-rewind guard (handlesleep.js) must still quarantine it.
|
||||
*/
|
||||
inline bool startSecond()
|
||||
{
|
||||
if( !co() )
|
||||
return false; // scenario needs the first coroutine in flight
|
||||
|
||||
if( co2() && co2()->Running() )
|
||||
return false;
|
||||
|
||||
delete co2();
|
||||
state().phase2 = 0;
|
||||
co2() = new COROUTINE<int, int>( fiberBody2 );
|
||||
co2()->Call( 0 );
|
||||
return state().phase2 == 1;
|
||||
}
|
||||
|
||||
/** Resume the second coroutine past its yield (cleanup / completion). */
|
||||
inline bool pokeSecond()
|
||||
{
|
||||
if( !co2() )
|
||||
return false;
|
||||
|
||||
return co2()->Resume();
|
||||
}
|
||||
|
||||
inline std::string stateJson()
|
||||
{
|
||||
char buf[144];
|
||||
snprintf( buf, sizeof( buf ),
|
||||
"{\"phase\":%d,\"pokes\":%d,\"parkMs\":%d,\"running\":%s,\"phase2\":%d}",
|
||||
state().phase, state().pokes, state().parkMs,
|
||||
( co() && co()->Running() ) ? "true" : "false", state().phase2 );
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace pcbjam_fiber_park
|
||||
|
|
@ -25,7 +25,6 @@
|
|||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include "open_gate.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "pcbjam_async_policy.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
|
@ -38,8 +37,8 @@ static GERBVIEW_FRAME* gerbFrame()
|
|||
|
||||
static bool openFileSet( const std::vector<wxString>& aFiles )
|
||||
{
|
||||
// Held across every Asyncify park of the load (open_gate.h): the layer load
|
||||
// parks, and a wx timer dispatched into a half-built layer set traps.
|
||||
// Held across every suspension of the load (open_gate.h): the layer load
|
||||
// suspends, and a wx timer dispatched into a half-built layer set traps.
|
||||
pcbjam_open::BusyGuard busy;
|
||||
|
||||
GERBVIEW_FRAME* frame = gerbFrame();
|
||||
|
|
|
|||
|
|
@ -43,9 +43,7 @@
|
|||
#include "pcbjam_libs_reload.h"
|
||||
#include "pcbjam_async_policy.h"
|
||||
#include "open_gate.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "timer_park.h"
|
||||
#include "fiber_park.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
|
|
@ -141,7 +139,7 @@ bool schCollabTestClearSelection();
|
|||
// standalone bundles compile from their own binding TU.
|
||||
static bool kicadOpenFile( std::string path )
|
||||
{
|
||||
// Held across every Asyncify park of the load; see open_gate.h.
|
||||
// Held across every suspension of the load; see open_gate.h.
|
||||
pcbjam_open::BusyGuard busy;
|
||||
|
||||
if( pcbjam_open::testParkMs() > 0 )
|
||||
|
|
@ -167,43 +165,6 @@ static bool kicadOpenFile( std::string path )
|
|||
return ok;
|
||||
}
|
||||
|
||||
// Phase F (docs/features/async/22 §10, the awaited-ccall entry class): the
|
||||
// open body above, driven from a DISPATCH CONTEXT instead of the main stack.
|
||||
// Run there, every wait inside the load parks the context through the
|
||||
// registry — the main stack never parks in place, which is the last
|
||||
// production member of the overlapped-wake class the D-on beacon sweep named.
|
||||
//
|
||||
// THE TOKEN IS PASSED IN, NOT RETURNED. Running the job on a dispatch context
|
||||
// Asyncify-suspends THIS embind frame while the load parks, so any return
|
||||
// value is delivered as an unwind PLACEHOLDER (0) into a rewind JS discards —
|
||||
// the same gotcha the fiber-park levers document. So the shim wrapper mints
|
||||
// the wait token in pure JS (no swap), hands it in here, and awaits its
|
||||
// promise; this starter returns void and its own placeholder return is
|
||||
// harmless. The job resolves the token when the load completes.
|
||||
extern "C" void wxWasmRunOnDispatchContext( void ( *fn )( void* ), void* arg );
|
||||
extern "C" void wxWasmResolveWait( int aToken, int aResult );
|
||||
|
||||
namespace
|
||||
{
|
||||
struct OPEN_JOB
|
||||
{
|
||||
std::string path;
|
||||
int token;
|
||||
};
|
||||
|
||||
void kicadOpenFileJob( void* aArg )
|
||||
{
|
||||
std::unique_ptr<OPEN_JOB> job( static_cast<OPEN_JOB*>( aArg ) );
|
||||
const bool ok = kicadOpenFile( job->path );
|
||||
wxWasmResolveWait( job->token, ok ? 1 : 0 );
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static void kicadOpenFileStart( int token, std::string path )
|
||||
{
|
||||
wxWasmRunOnDispatchContext( &kicadOpenFileJob, new OPEN_JOB{ std::move( path ), token } );
|
||||
}
|
||||
|
||||
// JS-pollable open-in-flight probe (open_gate.h): the web shell defers the
|
||||
// collab/presence attach until the open chain has truly completed.
|
||||
static bool kicadOpenFileBusy()
|
||||
|
|
@ -218,7 +179,7 @@ static void kicadTestSetOpenPark( int aMs )
|
|||
}
|
||||
|
||||
// Test-only (timer-park repro, timer_park.h): a one-shot wx timer whose
|
||||
// Notify() Asyncify-parks — the deterministic concurrent-park window.
|
||||
// Notify() suspends — the deterministic concurrent-suspension window.
|
||||
static bool kicadTestArmTimerPark( int aDelayMs, int aParkMs )
|
||||
{
|
||||
return pcbjam_timer_park::arm( aDelayMs, aParkMs );
|
||||
|
|
@ -229,44 +190,6 @@ static std::string kicadTestTimerParkState()
|
|||
return pcbjam_timer_park::stateJson();
|
||||
}
|
||||
|
||||
// Test-only (fiber_park.h): Resume() into a foreign-parked coroutine — the
|
||||
// decoded prod board-load trap family. NOTE: these are sync embind bindings
|
||||
// for MANUAL probing on Chromium only. Do not build specs on them: the
|
||||
// mutating levers suspend, and no embind shape delivers that correctly
|
||||
// (plain registration throws on strict-JSPI Firefox; emscripten::async()
|
||||
// re-executes its invoker when the awaited promise settles). The runtime
|
||||
// contracts they staged are pinned by the jspi-coroutine harness (18 cases)
|
||||
// and tests/kicad/coroutine-lifecycle.spec.ts instead.
|
||||
static bool kicadTestFiberParkStart( int aParkMs )
|
||||
{
|
||||
return pcbjam_fiber_park::start( aParkMs );
|
||||
}
|
||||
|
||||
static bool kicadTestFiberParkPrime()
|
||||
{
|
||||
return pcbjam_fiber_park::prime();
|
||||
}
|
||||
|
||||
static bool kicadTestFiberParkPoke()
|
||||
{
|
||||
return pcbjam_fiber_park::poke();
|
||||
}
|
||||
|
||||
static std::string kicadTestFiberParkState()
|
||||
{
|
||||
return pcbjam_fiber_park::stateJson();
|
||||
}
|
||||
|
||||
static bool kicadTestFiberParkStartSecond()
|
||||
{
|
||||
return pcbjam_fiber_park::startSecond();
|
||||
}
|
||||
|
||||
static bool kicadTestFiberParkPokeSecond()
|
||||
{
|
||||
return pcbjam_fiber_park::pokeSecond();
|
||||
}
|
||||
|
||||
|
||||
// Canvas-only chrome toggle (features/mobile): hide/show every AUI pane
|
||||
// except the central draw canvas, plus the menubar and status bar, so the GAL
|
||||
|
|
@ -607,29 +530,23 @@ static bool collabTestClearSelection()
|
|||
}
|
||||
|
||||
|
||||
static bool kicadCollabFiberBusyProbe()
|
||||
static bool kicadCollabBusyProbe()
|
||||
{
|
||||
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
|
||||
return pcbjam_collab::applyBusy() || !pcbjam_collab::applyQueue().empty();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(kicad_editor) {
|
||||
// Fiber-queue idle probe (drift-trio finding #10b): a bare-embind-stack
|
||||
// save during a parked apply fiber mis-dispatches (table index OOB) — the
|
||||
// JS side must defer scratch saves while collab fiber work is in flight.
|
||||
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
|
||||
// Apply-queue idle probe (drift-trio finding #10b): a scratch save taken
|
||||
// while a collab apply is in flight (queued, or suspended mid-commit)
|
||||
// would serialize a half-mutated model — the JS side must defer scratch
|
||||
// saves until this reads false.
|
||||
function("kicadCollabBusy", &kicadCollabBusyProbe);
|
||||
// Programmatic file open (preferred over UI automation from the web app).
|
||||
function("kicadOpenFile", &kicadOpenFile PCBJAM_PARKER_POLICY);
|
||||
function("kicadOpenFileStart", &kicadOpenFileStart);
|
||||
function("kicadOpenFileBusy", &kicadOpenFileBusy);
|
||||
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
||||
function("kicadTestArmTimerPark", &kicadTestArmTimerPark);
|
||||
function("kicadTestTimerParkState", &kicadTestTimerParkState);
|
||||
function("kicadTestFiberParkStart", &kicadTestFiberParkStart);
|
||||
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
|
||||
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
|
||||
function("kicadTestFiberParkState", &kicadTestFiberParkState);
|
||||
function("kicadTestFiberParkStartSecond", &kicadTestFiberParkStartSecond);
|
||||
function("kicadTestFiberParkPokeSecond", &kicadTestFiberParkPokeSecond);
|
||||
|
||||
// Canvas-only mobile mode (features/mobile).
|
||||
function("kicadSetChrome", &kicadSetChrome);
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Main-stack runner: the KiCad half of wx's nested-loop bounce
|
||||
* (pcbjam docs/features/async/19, 20 D3).
|
||||
*
|
||||
* A quasi-modal's nested event loop parks its whole stack for the dialog's
|
||||
* lifetime. When that stack is a TOOL coroutine's, the park suspends the
|
||||
* fiber's body where the fiber layer cannot see it: the stale-fiber guard
|
||||
* quarantines the fiber, then REFUSES its own resume, and the dialog stops
|
||||
* responding to clicks (only the titlebar x still works, because that path is
|
||||
* ungated). That is the Symbol Properties hang.
|
||||
*
|
||||
* wx detects "this nested loop is about to park on a non-main stack" — it can,
|
||||
* cheaply and exactly, by comparing a frame address against
|
||||
* emscripten_stack_get_base()/end() — but it must not know what a coroutine
|
||||
* is. So it calls this runner, and KiCad's TOOL_MANAGER moves the loop onto
|
||||
* the main stack via its own RunMainStack mechanism, which suspends the
|
||||
* coroutine the legitimate way: a fiber swap the layer records
|
||||
* (swap_suspended = true), so no quarantine, no refused resume.
|
||||
*
|
||||
* This lives in pcbjam's binding layer rather than in KiCad or wx precisely
|
||||
* because it is the only place that may know about both.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <wx/app.h>
|
||||
|
||||
#include <eda_base_frame.h>
|
||||
#include <tool/tool_manager.h>
|
||||
|
||||
#include <wx/wasm/private/mainstack.h>
|
||||
|
||||
namespace pcbjam_main_stack
|
||||
{
|
||||
|
||||
/**
|
||||
* Run aFunc on the main stack if a tool coroutine is currently active.
|
||||
*
|
||||
* Returns 1 when the body has been run (bounced, or run inline because there
|
||||
* was no coroutine to bounce off), 0 when there was nothing to run it with —
|
||||
* no frame or no tool manager — in which case wx parks in place exactly as it
|
||||
* did before this hook existed.
|
||||
*/
|
||||
inline int run_on_main_stack( void ( *aFunc )( void* ), void* aArg )
|
||||
{
|
||||
if( !wxTheApp )
|
||||
return 0;
|
||||
|
||||
auto* frame = dynamic_cast<EDA_BASE_FRAME*>( wxTheApp->GetTopWindow() );
|
||||
|
||||
if( !frame )
|
||||
return 0;
|
||||
|
||||
TOOL_MANAGER* toolMgr = frame->GetToolManager();
|
||||
|
||||
if( !toolMgr )
|
||||
return 0;
|
||||
|
||||
// RunOnMainStackIfActiveTool runs the body inline when no coroutine is
|
||||
// running. Either way the body HAS run, so report it as handled — letting
|
||||
// wx fall through would run the nested loop a second time.
|
||||
bool ran = false;
|
||||
|
||||
toolMgr->RunOnMainStackIfActiveTool(
|
||||
[aFunc, aArg, &ran]()
|
||||
{
|
||||
ran = true;
|
||||
aFunc( aArg );
|
||||
} );
|
||||
|
||||
return ran ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs at static-init time. Only stores a function pointer, so it is safe
|
||||
* before wx exists; every lookup above happens lazily per call, since frames
|
||||
* come and go. Included by more than one binding TU — setting the same pointer
|
||||
* twice is idempotent.
|
||||
*/
|
||||
struct INSTALLER
|
||||
{
|
||||
INSTALLER() { wxWasmSetMainStackRunner( &run_on_main_stack ); }
|
||||
};
|
||||
|
||||
inline INSTALLER g_installer;
|
||||
|
||||
} // namespace pcbjam_main_stack
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
/*
|
||||
* Truthful "kicadOpenFile in flight" signal for the web shell.
|
||||
*
|
||||
* kicadOpenFile runs OpenProjectFiles under Asyncify: the embind call unwinds
|
||||
* back to JS long before the load finishes, and the chain stays parked (and
|
||||
* resumes, and parks again) across the whole multi-second load. Any bare
|
||||
* embind entry that walks the model while that chain is parked mid-mutation
|
||||
* (collab snapshot, presence bind) can virtual-dispatch through a half-built
|
||||
* item and trap ("indirect call signature mismatch" — same class as the wx
|
||||
* dispatch interlock and the drift-trio #10b fiber-busy probe, but through a
|
||||
* JS entry neither of those covers).
|
||||
* kicadOpenFile runs OpenProjectFiles as a suspending (promising) export: the
|
||||
* embind call hands JS a Promise long before the load finishes, and the chain
|
||||
* stays suspended (and resumes, and suspends again) across the whole
|
||||
* multi-second load. Any bare embind entry that walks the model while that
|
||||
* chain is suspended mid-mutation (collab snapshot, presence bind) reads a
|
||||
* half-built item graph and can trap — same class as the wx dispatch
|
||||
* interlock and the collab apply-busy probe, but through a JS entry neither
|
||||
* of those covers.
|
||||
*
|
||||
* The guard is RAII on the open's C++ stack frame: an Asyncify unwind does not
|
||||
* run destructors and a rewind resumes past the constructor, so the count is
|
||||
* held for the park's entire lifetime and drops exactly when OpenProjectFiles
|
||||
* The guard is RAII on the open's C++ stack frame: a suspension keeps the
|
||||
* whole C++ stack (and with it this frame) alive, so the count is held for
|
||||
* the suspension's entire lifetime and drops exactly when OpenProjectFiles
|
||||
* truly returns (the same primitive as wxWasmDispatchGuard). A trap escaping
|
||||
* the open leaves the count stuck — the JS poll times out and degrades.
|
||||
*/
|
||||
|
|
@ -30,21 +30,22 @@ inline int& busyCount()
|
|||
}
|
||||
|
||||
/**
|
||||
* Held for the whole open. Two counters, same Asyncify-RAII trick:
|
||||
* Held for the whole open. Two counters, same suspension-RAII trick:
|
||||
*
|
||||
* - `busyCount` is OURS: it answers kicadOpenFileBusy() for the web shell and
|
||||
* gates the collab entries (JS → embind reentry).
|
||||
* - `wxWasmDispatchGuard` enrolls the open in the WX DISPATCH INTERLOCK. This
|
||||
* matters because `kicadOpenFile` enters through embind, not through a wx
|
||||
* dispatch entry point, so without it `wxWasmDispatchParked()` reads FALSE
|
||||
* for the entire load: every park (progress pump, thread-pool futex wait,
|
||||
* lib bridge) lets the pump dispatch a QUEUED WX TIMER into the half-built
|
||||
* board — src/wasm/timer.cpp fires it because nothing looks parked — and
|
||||
* the handler walks half-mutated widget/board state ("index out of bounds",
|
||||
* the same signature as the symbol-chooser crash the interlock was built
|
||||
* for). Holding the guard makes those timers defer (retry 17 ms later)
|
||||
* until the load truly completes. Paints keep running; the progress
|
||||
* dialog's own pump is the designed exception (it zeroes the count).
|
||||
* for the entire load: every suspension (progress pump, thread-pool futex
|
||||
* wait, lib bridge) lets the pump dispatch a QUEUED WX TIMER into the
|
||||
* half-built board — src/wasm/timer.cpp fires it because nothing looks
|
||||
* parked — and the handler walks half-mutated widget/board state ("index
|
||||
* out of bounds", the same signature as the symbol-chooser crash the
|
||||
* interlock was built for). Holding the guard makes those timers defer
|
||||
* (retry 17 ms later) until the load truly completes. Paints keep running;
|
||||
* the progress dialog's own pump is the designed exception (it zeroes the
|
||||
* count).
|
||||
*/
|
||||
struct BusyGuard
|
||||
{
|
||||
|
|
@ -63,11 +64,12 @@ inline bool busy()
|
|||
|
||||
/**
|
||||
* Test-only deterministic park (tests/kicad/collab-load-fuzz.spec.ts): with a
|
||||
* nonzero value, kicadOpenFile Asyncify-parks for this many ms on entry and
|
||||
* again after OpenProjectFiles returns — busy guard held, model fully loaded.
|
||||
* Natural in-load parks (thread-pool futex waits) are scheduler-dependent and
|
||||
* never happen on a fast idle machine, so the guard would be untestable in CI
|
||||
* without this window. 0 (the default) is a no-op in production.
|
||||
* nonzero value, kicadOpenFile suspends for this many ms on entry and again
|
||||
* after OpenProjectFiles returns — busy guard held, model fully loaded.
|
||||
* Natural in-load suspensions (thread-pool futex waits) are
|
||||
* scheduler-dependent and never happen on a fast idle machine, so the guard
|
||||
* would be untestable in CI without this window. 0 (the default) is a no-op
|
||||
* in production.
|
||||
*/
|
||||
inline int& testParkMs()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
*
|
||||
* Header-only (the collab_common.h pattern); common-code includes only, so the
|
||||
* merged kicad_editor TU (deliberately eeschema/pcbnew-header-free) can use it.
|
||||
* Runs on the fiber stack: LoadLibraryEntry Asyncify-suspends in the JS bridge,
|
||||
* and the tree sync dispatches GAL/tree virtuals that trap off-fiber.
|
||||
* Runs on the apply coroutine: LoadLibraryEntry suspends in the JS bridge, so
|
||||
* the reload must serialize with the collab applies and local edits.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
|
@ -52,7 +52,7 @@ inline void reloadLibrary( std::string aKind, std::string aNickname )
|
|||
const bool fp = aKind == "footprint";
|
||||
const wxString nick = wxString::FromUTF8( aNickname.c_str() );
|
||||
|
||||
pcbjam_collab::runOnFiber( top, [top, fp, nick]()
|
||||
pcbjam_collab::runOnCoroutine( top, [top, fp, nick]()
|
||||
{
|
||||
LIBRARY_MANAGER& mgr = Pgm().GetLibraryManager();
|
||||
const LIBRARY_TABLE_TYPE type =
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ namespace pcbjam_theme {
|
|||
* which have no chrome API. */
|
||||
inline void ( *g_afterThemeApplied )() = nullptr;
|
||||
|
||||
/** Set the chrome appearance FLAG only — no widget traffic, no fiber. Safe
|
||||
/** Set the chrome appearance FLAG only — no widget traffic, no coroutine. Safe
|
||||
* from the browser main thread at any point (it writes one bool in shared
|
||||
* wasm memory); the embedder calls it at onRuntimeInitialized, BEFORE main()
|
||||
* spawns on the KiCad pthread, so the first widget paint is already themed.
|
||||
|
|
@ -79,7 +79,7 @@ inline void syncChromeAppearance( bool aDark )
|
|||
}
|
||||
|
||||
/** Apply `aTheme` ("pcbjam-dark", "_builtin_default", …) to one frame. Runs
|
||||
* on the frame's fiber: CommonSettingsChanged reaches tool/view internals
|
||||
* on the apply coroutine: CommonSettingsChanged reaches tool/view internals
|
||||
* that must not run from a bare JS callback. Null frame no-ops (the merged
|
||||
* dispatcher calls every editor, open or not). */
|
||||
inline void setColorTheme( EDA_DRAW_FRAME* aFrame, const std::string& aTheme )
|
||||
|
|
@ -87,7 +87,7 @@ inline void setColorTheme( EDA_DRAW_FRAME* aFrame, const std::string& aTheme )
|
|||
if( !aFrame )
|
||||
return;
|
||||
|
||||
pcbjam_collab::runOnFiber( aFrame, [aFrame, aTheme]() {
|
||||
pcbjam_collab::runOnCoroutine( aFrame, [aFrame, aTheme]() {
|
||||
// The shell only ever sends our dark theme name or the builtin
|
||||
// default, so the chrome appearance rides on that distinction.
|
||||
const bool dark = aTheme != "_builtin_default";
|
||||
|
|
@ -116,7 +116,7 @@ inline void setColorTheme( EDA_DRAW_FRAME* aFrame, const std::string& aTheme )
|
|||
// unconditionally reloads colors and recaches the view.
|
||||
aFrame->CommonSettingsChanged( 0 );
|
||||
|
||||
// Queued from INSIDE the fiber body, AFTER CommonSettingsChanged: the
|
||||
// Queued from INSIDE the coroutine body, AFTER CommonSettingsChanged: the
|
||||
// menubar rebuild it triggers is itself a CallAfter on this same
|
||||
// handler, so FIFO puts the re-assert behind the rebuilt (shown)
|
||||
// menubar.
|
||||
|
|
|
|||
|
|
@ -53,10 +53,8 @@
|
|||
#include "collab_common.h"
|
||||
#include "collab_presence_core.h"
|
||||
#include "open_gate.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "pcbjam_async_policy.h"
|
||||
#include "timer_park.h"
|
||||
#include "fiber_park.h"
|
||||
#include "collab_presence_style.h"
|
||||
#include "pcbjam_theme.h"
|
||||
#include "pcbjam_libs_reload.h"
|
||||
|
|
@ -89,7 +87,7 @@ using json = nlohmann::json;
|
|||
#ifndef KICAD_MERGED_EMBIND
|
||||
bool kicadOpenFile( std::string path )
|
||||
{
|
||||
// Held across every Asyncify park of the load; see open_gate.h.
|
||||
// Held across every suspension of the load; see open_gate.h.
|
||||
pcbjam_open::BusyGuard busy;
|
||||
|
||||
if( pcbjam_open::testParkMs() > 0 )
|
||||
|
|
@ -129,7 +127,7 @@ void kicadTestSetOpenPark( int aMs )
|
|||
}
|
||||
|
||||
// Test-only (timer-park repro, timer_park.h): a one-shot wx timer whose
|
||||
// Notify() Asyncify-parks — the deterministic concurrent-park window.
|
||||
// Notify() suspends — the deterministic concurrent-suspension window.
|
||||
bool kicadTestArmTimerPark( int aDelayMs, int aParkMs )
|
||||
{
|
||||
return pcbjam_timer_park::arm( aDelayMs, aParkMs );
|
||||
|
|
@ -140,38 +138,6 @@ std::string kicadTestTimerParkState()
|
|||
return pcbjam_timer_park::stateJson();
|
||||
}
|
||||
|
||||
// Test-only (fiber-resume-park repro, fiber_park.h): Resume() into an
|
||||
// asyncify-parked coroutine — the decoded prod board-load trap.
|
||||
bool kicadTestFiberParkStart( int aParkMs )
|
||||
{
|
||||
return pcbjam_fiber_park::start( aParkMs );
|
||||
}
|
||||
|
||||
bool kicadTestFiberParkPrime()
|
||||
{
|
||||
return pcbjam_fiber_park::prime();
|
||||
}
|
||||
|
||||
bool kicadTestFiberParkPoke()
|
||||
{
|
||||
return pcbjam_fiber_park::poke();
|
||||
}
|
||||
|
||||
std::string kicadTestFiberParkState()
|
||||
{
|
||||
return pcbjam_fiber_park::stateJson();
|
||||
}
|
||||
|
||||
bool kicadTestFiberParkStartSecond()
|
||||
{
|
||||
return pcbjam_fiber_park::startSecond();
|
||||
}
|
||||
|
||||
bool kicadTestFiberParkPokeSecond()
|
||||
{
|
||||
return pcbjam_fiber_park::pokeSecond();
|
||||
}
|
||||
|
||||
// Read-only viewer lock (read-only-viewer): flips the process-global
|
||||
// PCBJAM_READ_ONLY flag consumed by TOOL_MANAGER (view-only action allowlist)
|
||||
// and the selection tools (nothing selectable), and mirrors it onto the
|
||||
|
|
@ -205,9 +171,9 @@ bool kicadSetReadOnly( bool aReadOnly )
|
|||
// trigger; the real change set is a diff of the full model taken after the
|
||||
// edit's BOARD_COMMIT::Push — connectivity cleanup included — has returned,
|
||||
// so peers converge by re-applying already-clean geometry). See eeschema 0007.
|
||||
// - apply = BOARD_COMMIT run inside a CallAfter + COROUTINE fiber stack, the exact
|
||||
// context native tool edits run in, so GAL view->Add of a freshly-constructed
|
||||
// item dispatches its asyncify-instrumented virtuals correctly (eeschema 0007).
|
||||
// - apply = BOARD_COMMIT run inside a CallAfter + COROUTINE, the exact context native
|
||||
// tool edits run in, serialized with every other apply/local edit through the
|
||||
// collab_common.h apply queue (eeschema 0007).
|
||||
//
|
||||
// Scope of this first commit (0004 §"first PoC", matching eeschema commit-3's first cut):
|
||||
// position/geometry sync of existing items — changed (move/reshape) and removed work for
|
||||
|
|
@ -232,12 +198,12 @@ bool isTrackType( KICAD_T t )
|
|||
return t == PCB_TRACE_T || t == PCB_ARC_T || t == PCB_VIA_T;
|
||||
}
|
||||
|
||||
// Read an item's layer WITHOUT the virtual GetLayer(). That virtual mis-dispatches in the
|
||||
// non-coroutine emit/snapshot context — it returns 0 (F_Cu) for EVERY item (the same asyncify
|
||||
// call_indirect class as eeschema's Move()), which silently put every collab-added item/track/
|
||||
// text on the top copper layer on the peer. A class-qualified `BOARD_ITEM::GetLayer()` is a
|
||||
// statically-bound (direct) call that just reads m_layer, bypassing call_indirect. Zones keep
|
||||
// their layer in m_layerSet (not m_layer), so use their non-virtual GetFirstLayer().
|
||||
// Read an item's layer WITHOUT the virtual GetLayer(). Under the retired asyncify runtime
|
||||
// that virtual mis-dispatched in the non-coroutine emit/snapshot context — it returned 0
|
||||
// (F_Cu) for EVERY item, silently putting every collab-added item/track/text on the top
|
||||
// copper layer on the peer. The class-qualified `BOARD_ITEM::GetLayer()` is a statically-
|
||||
// bound (direct) call that just reads m_layer — correct on any runtime, so it stays. Zones
|
||||
// keep their layer in m_layerSet (not m_layer), so use their non-virtual GetFirstLayer().
|
||||
int itemLayer( BOARD_ITEM* aItem )
|
||||
{
|
||||
if( aItem->Type() == PCB_ZONE_T )
|
||||
|
|
@ -314,8 +280,9 @@ json itemToJson( BOARD_ITEM* aItem )
|
|||
}
|
||||
|
||||
// Vias and zones reconstruct NATIVELY on `added` (the s-expr clipboard blob's `(kicad_pcb …)`
|
||||
// envelope parse — used for footprints — is asyncify-fragile in wasm for these, the same wall
|
||||
// that deferred the eeschema symbol blob). So emit the geometry their makeItem needs.
|
||||
// envelope parse — used for footprints — proved fragile for these under the retired asyncify
|
||||
// runtime, the same wall that deferred the eeschema symbol blob; the native path stays as the
|
||||
// simpler, pinned-by-tests route). So emit the geometry their makeItem needs.
|
||||
if( aItem->Type() == PCB_VIA_T )
|
||||
{
|
||||
auto* via = static_cast<PCB_VIA*>( aItem );
|
||||
|
|
@ -342,7 +309,7 @@ json itemToJson( BOARD_ITEM* aItem )
|
|||
|
||||
j["poly"] = pts;
|
||||
}
|
||||
// A board-level graphic text (Place→Text) also reconstructs NATIVELY (same asyncify reason as
|
||||
// A board-level graphic text (Place→Text) also reconstructs NATIVELY (same reason as
|
||||
// via/zone): emit its size / stroke / angle so makeItem can rebuild it. Footprint child text
|
||||
// is synced by move, not `added`, so this is only the board PCB_TEXT case.
|
||||
else if( aItem->Type() == PCB_TEXT_T )
|
||||
|
|
@ -632,7 +599,7 @@ BOARD_ITEM* makeItem( BOARD& aBoard, const json& j )
|
|||
item = tr;
|
||||
}
|
||||
// Via / zone: reconstruct natively from emitted geometry (the envelope-blob parse is
|
||||
// asyncify-fragile for these — see itemToJson). The blob is still emitted as a fallback.
|
||||
// skipped for these — see itemToJson). The blob is still emitted as a fallback.
|
||||
else if( type == "PCB_VIA" && j.contains( "drill" ) )
|
||||
{
|
||||
auto* via = new PCB_VIA( &aBoard );
|
||||
|
|
@ -714,7 +681,7 @@ BOARD_ITEM* makeItem( BOARD& aBoard, const json& j )
|
|||
// Set an existing item's geometry from a `changed` delta. Tracks reshape via their endpoints
|
||||
// (independent — like an eeschema wire); everything else moves to an absolute position.
|
||||
// SetStart/SetEnd/SetPosition run inside the apply COROUTINE (see kicadCollabApply), the same
|
||||
// fiber context native edits use, so the virtual dispatch resolves correctly.
|
||||
// context native edits use, serialized through the apply queue.
|
||||
void applyChanged( BOARD_ITEM* aItem, const json& j )
|
||||
{
|
||||
if( isTrackType( aItem->Type() ) && j.contains( "sx" ) )
|
||||
|
|
@ -915,7 +882,7 @@ void flushDiff()
|
|||
// Attach an s-expr clipboard blob ONLY for types makeItem reconstructs from it
|
||||
// (footprints, board graphics, …). Tracks/vias/zones/text rebuild NATIVELY from the
|
||||
// fields itemToJson already emitted, so they need no blob — and skipping it avoids a
|
||||
// wasted SaveSelection plus the asyncify-fragile envelope parse for those.
|
||||
// wasted SaveSelection plus the envelope parse for those.
|
||||
if( live && !isTrackType( live->Type() ) && live->Type() != PCB_ZONE_T
|
||||
&& live->Type() != PCB_TEXT_T )
|
||||
withBlob["sexpr"] = blobForItem( board, live );
|
||||
|
|
@ -971,10 +938,11 @@ void flushDiff()
|
|||
|
||||
// Coalesce all the listener callbacks of one commit (and any other edits in the same loop
|
||||
// turn) into a single post-settle diff.
|
||||
// flushDiff runs inside a COROUTINE: the v2 items emit serializes ROOT items via
|
||||
// CLIPBOARD_IO Format (blobForItem), whose virtual dispatch is only reliable on the
|
||||
// libcontext fiber stack — on the bare CallAfter stack it can trap and silently kill
|
||||
// the whole flush, legacy emit included (same lesson as doApply / eeschema 0007).
|
||||
// flushDiff runs inside a COROUTINE via the apply queue: the v2 items emit serializes
|
||||
// ROOT items via CLIPBOARD_IO Format (blobForItem), and running it in the same queue
|
||||
// as the applies keeps emits from interleaving with a suspended apply body (under the
|
||||
// retired asyncify runtime the bare CallAfter stack additionally trapped here — same
|
||||
// lesson as doApply / eeschema 0007).
|
||||
void scheduleFlush()
|
||||
{
|
||||
if( g_flushScheduled )
|
||||
|
|
@ -983,7 +951,7 @@ void scheduleFlush()
|
|||
g_flushScheduled = true;
|
||||
|
||||
if( PCB_EDIT_FRAME* fr = pcbFrame() )
|
||||
pcbjam_collab::runOnFiber( fr, []() { flushDiff(); } );
|
||||
pcbjam_collab::runOnCoroutine( fr, []() { flushDiff(); } );
|
||||
else
|
||||
flushDiff();
|
||||
}
|
||||
|
|
@ -1246,10 +1214,9 @@ void doApplyItems( PCB_EDIT_FRAME* aFrame, const json& aWire )
|
|||
}
|
||||
|
||||
// Test/PoC move (the BOARD_COMMIT body for kicadCollabTestMoveFirst). Run inside a COROUTINE by
|
||||
// the caller: `BOARD_ITEM::Move` is virtual, and dispatched off the app main stack (a bare
|
||||
// CallAfter) it hits the asyncify call_indirect mis-dispatch and silently NO-OPS — the commit
|
||||
// dirties the board but the item doesn't move. On the fiber stack (where native tool edits and
|
||||
// doApply run) it dispatches correctly. (Same lesson as eeschema's devirtualized move.)
|
||||
// the caller — the context native tool edits and doApply run in, serialized through the apply
|
||||
// queue. (Under the retired asyncify runtime the virtual `BOARD_ITEM::Move` additionally
|
||||
// mis-dispatched off that context; same lesson as eeschema's devirtualized move.)
|
||||
void collabTestMove( PCB_EDIT_FRAME* aFrame, BOARD_ITEM* aItem, int aDx, int aDy )
|
||||
{
|
||||
BOARD_COMMIT commit( aFrame );
|
||||
|
|
@ -1344,8 +1311,8 @@ pcbjam_presence::CORE& presenceCore()
|
|||
|
||||
// Draw ONE item's selection box under the given style. Exact-geometry
|
||||
// outline (style shape 5): footprints hug their bounding hull,
|
||||
// everything else its transformed shape. Runs on the coroutine fiber
|
||||
// (virtual dispatch), falls back to the bbox on anything that can't
|
||||
// everything else its transformed shape. Runs on the apply
|
||||
// coroutine, falls back to the bbox on anything that can't
|
||||
// produce a polygon.
|
||||
auto drawItem = [&]( BOARD_ITEM* item, const std::string& name,
|
||||
const KIGFX::COLOR4D& itemColor,
|
||||
|
|
@ -1447,19 +1414,18 @@ void schedulePresenceSelCheck()
|
|||
// JS → C++. Apply a remote per-item delta by uuid, through BOARD_COMMIT so connectivity/ratsnest
|
||||
// recompute the same way a UI edit would (0004 §apply).
|
||||
//
|
||||
// BOARD_COMMIT must run in the editor's Asyncify-rooted main loop — invoking it from this embind
|
||||
// ccall, or from a setTimeout callback, traps with an "indirect call signature mismatch" (those
|
||||
// aren't the asyncify root). wxEvtHandler::CallAfter queues onto the app's pending-event list,
|
||||
// drained every frame by the wasm main loop (src/wasm/evtloop.cpp) — the exact context real UI
|
||||
// edits run in. Additionally run the mutation inside a COROUTINE so it executes on a libcontext
|
||||
// fiber stack: BOARD_COMMIT::Push's CHT_ADD of a freshly-built item dispatches GAL virtuals
|
||||
// (view->Add → ViewGetLayers) through asyncify-instrumented invoke_*; off the fiber stack those
|
||||
// mis-dispatch and trap inside KiCad core, on it they dispatch correctly (eeschema 0007).
|
||||
// BOARD_COMMIT must run on the editor's main loop, not on this embind ccall or a setTimeout
|
||||
// callback: wxEvtHandler::CallAfter queues onto the app's pending-event list, drained every
|
||||
// frame by the wasm main loop (src/wasm/evtloop.cpp) — the exact context real UI edits run in.
|
||||
// Additionally run the mutation inside a COROUTINE via the collab_common.h apply queue: a
|
||||
// commit body that suspends (connectivity/GAL work) returns early from COROUTINE::Call, so
|
||||
// applies must serialize with each other and with local edits or they interleave on shared
|
||||
// commit/listener state (eeschema 0007, drift-trio #10).
|
||||
void pcbCollabApply( std::string aJson )
|
||||
{
|
||||
// Open-in-flight guard (open_gate.h): never touch the model while a
|
||||
// kicadOpenFile Asyncify chain is parked mid-load — commits/virtuals on a
|
||||
// half-built board mis-dispatch ("indirect call signature mismatch").
|
||||
// kicadOpenFile chain is suspended mid-load — commits/virtuals would walk
|
||||
// a half-built board mid-mutation.
|
||||
// Callers gate on kicadOpenFileBusy; fuzzed by tests/kicad/collab-load-fuzz.spec.ts.
|
||||
if( pcbjam_open::busy() )
|
||||
return;
|
||||
|
|
@ -1474,7 +1440,7 @@ void pcbCollabApply( std::string aJson )
|
|||
if( !fr )
|
||||
return;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, delta]() { doApply( fr, delta ); } );
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, delta]() { doApply( fr, delta ); } );
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1495,7 +1461,7 @@ void pcbCollabApplyItems( std::string aJson )
|
|||
if( !fr )
|
||||
return;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, wire]() { doApplyItems( fr, wire ); } );
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, wire]() { doApplyItems( fr, wire ); } );
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1631,9 +1597,9 @@ std::string pcbCollabTestMoveFirst( int aDx, int aDy )
|
|||
return;
|
||||
|
||||
movedId = toUtf8( item->m_Uuid.AsString() );
|
||||
// Main stack + fiber (runOnFiber), so the virtual Move()
|
||||
// dispatches instead of no-opping — same wrapping as doApply.
|
||||
pcbjam_collab::runOnFiber( fr, [fr, item, aDx, aDy]() {
|
||||
// Main loop + apply coroutine (runOnCoroutine) — same
|
||||
// wrapping as doApply.
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, item, aDx, aDy]() {
|
||||
collabTestMove( fr, item, aDx, aDy );
|
||||
} );
|
||||
} );
|
||||
|
|
@ -1997,9 +1963,8 @@ std::string kicadCollabTestItemBlob( std::string aId )
|
|||
// ── ysync-review repro hooks ─────────────────────────────────────────────────
|
||||
// Local-edit test hooks for the ysync-review repro e2e (docs/features/
|
||||
// ysync-review on the ysync-review branch): each drives a REAL BOARD_COMMIT on
|
||||
// the app main stack inside a COROUTINE fiber (the collabTestMove wrapping —
|
||||
// virtual item mutators mis-dispatch off the fiber stack), so the
|
||||
// COLLAB_LISTENER → flushDiff emit path runs exactly as for a UI edit. Each
|
||||
// the app main loop inside the apply COROUTINE (the collabTestMove wrapping),
|
||||
// so the COLLAB_LISTENER → flushDiff emit path runs exactly as for a UI edit. Each
|
||||
// returns false when the uuid doesn't resolve, letting the spec distinguish
|
||||
// "hook missed the item" from "differ missed the edit" (bug 04).
|
||||
|
||||
|
|
@ -2035,7 +2000,7 @@ bool pcbCollabTestRemoveItem( std::string aId )
|
|||
if( !item )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, item]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, item]() {
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Remove( item );
|
||||
commit.Push( wxT( "Collab test remove" ) );
|
||||
|
|
@ -2055,7 +2020,7 @@ bool pcbCollabTestRotateItem( std::string aId, double aDeg )
|
|||
if( !item )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, item, aDeg]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, item, aDeg]() {
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Modify( item );
|
||||
item->Rotate( item->GetPosition(), EDA_ANGLE( aDeg, DEGREES_T ) );
|
||||
|
|
@ -2077,7 +2042,7 @@ bool pcbCollabTestSetPadSize( std::string aId, int aW, int aH )
|
|||
|
||||
PAD* pad = static_cast<PAD*>( item );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, pad, aW, aH]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, pad, aW, aH]() {
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Modify( pad );
|
||||
pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( aW, aH ) );
|
||||
|
|
@ -2098,7 +2063,7 @@ bool pcbCollabTestMoveEndpoint( std::string aId, int aDx, int aDy )
|
|||
if( !item || ( !isTrackType( item->Type() ) && item->Type() != PCB_SHAPE_T ) )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, item, aDx, aDy]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, item, aDx, aDy]() {
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Modify( item );
|
||||
|
||||
|
|
@ -2121,7 +2086,7 @@ bool pcbCollabTestMoveEndpoint( std::string aId, int aDx, int aDy )
|
|||
|
||||
// ── drift-trio phase B action hooks (standalone-hardening 0008 §5) ───────────
|
||||
// Creation/mutation primitives for the trio harness's action catalog. Each
|
||||
// drives a REAL BOARD_COMMIT on the fiber stack, so the BOARD_LISTENER →
|
||||
// drives a REAL BOARD_COMMIT on the apply coroutine, so the BOARD_LISTENER →
|
||||
// flushDiff emit path runs exactly as for a UI edit. Names are tool-unique
|
||||
// (registered outside the KICAD_MERGED_EMBIND guard — same convention as
|
||||
// kicadCollabTestSetPadSize), so the merged image needs no dispatcher.
|
||||
|
|
@ -2140,7 +2105,7 @@ static std::string pcbCollabTestCommitAdd( BOARD_ITEM* aItem, const wxChar* aMsg
|
|||
std::string id = toUtf8( aItem->m_Uuid.AsString() );
|
||||
wxString msg( aMsg );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aItem, msg]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aItem, msg]() {
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Add( aItem );
|
||||
commit.Push( msg );
|
||||
|
|
@ -2221,7 +2186,7 @@ bool pcbCollabTestFlipBoardItem( std::string aId )
|
|||
if( !testResolve( fr, aId ) )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aId]() { // re-resolve on the fiber (S4)
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aId]() { // re-resolve on the coroutine (S4)
|
||||
BOARD_ITEM* item = testResolve( fr, aId );
|
||||
|
||||
if( !item )
|
||||
|
|
@ -2251,7 +2216,7 @@ bool pcbCollabTestSetFootprintField( std::string aId, std::string aField, std::s
|
|||
if( !isRef && aField != "Value" )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aId, text, isRef]() { // re-resolve on the fiber (S4)
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aId, text, isRef]() { // re-resolve on the coroutine (S4)
|
||||
BOARD_ITEM* live = testResolve( fr, aId );
|
||||
|
||||
if( !live || live->Type() != PCB_FOOTPRINT_T )
|
||||
|
|
@ -2279,7 +2244,7 @@ bool pcbCollabTestSetBoardItemLocked( std::string aId, bool aLocked )
|
|||
if( !testResolve( fr, aId ) )
|
||||
return false;
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aId, aLocked]() { // re-resolve on the fiber (S4)
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aId, aLocked]() { // re-resolve on the coroutine (S4)
|
||||
BOARD_ITEM* item = testResolve( fr, aId );
|
||||
|
||||
if( !item )
|
||||
|
|
@ -2294,7 +2259,7 @@ bool pcbCollabTestSetBoardItemLocked( std::string aId, bool aLocked )
|
|||
return true;
|
||||
}
|
||||
|
||||
// By-uuid variant of MoveFirst (same fiber + BOARD_COMMIT body).
|
||||
// By-uuid variant of MoveFirst (same coroutine + BOARD_COMMIT body).
|
||||
bool pcbCollabTestMoveBoardItem( std::string aId, int aDx, int aDy )
|
||||
{
|
||||
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||
|
|
@ -2302,11 +2267,11 @@ bool pcbCollabTestMoveBoardItem( std::string aId, int aDx, int aDy )
|
|||
if( !testResolve( fr, aId ) )
|
||||
return false;
|
||||
|
||||
// Re-resolve ON the fiber: a remote remove can apply between scheduling
|
||||
// Re-resolve ON the coroutine: a remote remove can apply between scheduling
|
||||
// and running, and doApplyItems FREES removed items — a captured pointer
|
||||
// would be dangling and the commit would resurrect a deleted item
|
||||
// (drift-trio S4 move-vs-delete). Vanished => the move loses, silently.
|
||||
pcbjam_collab::runOnFiber( fr, [fr, aId, aDx, aDy]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, aId, aDx, aDy]() {
|
||||
if( BOARD_ITEM* item = testResolve( fr, aId ) )
|
||||
collabTestMove( fr, item, aDx, aDy );
|
||||
} );
|
||||
|
|
@ -2330,7 +2295,7 @@ std::string pcbCollabTestDuplicateBoardItem( std::string aId, int aDx, int aDy )
|
|||
|
||||
std::string id = toUtf8( dup->m_Uuid.AsString() );
|
||||
|
||||
pcbjam_collab::runOnFiber( fr, [fr, dup]() {
|
||||
pcbjam_collab::runOnCoroutine( fr, [fr, dup]() {
|
||||
BOARD_COMMIT commit( fr );
|
||||
commit.Add( dup );
|
||||
commit.Push( wxT( "Collab test duplicate" ) );
|
||||
|
|
@ -2389,9 +2354,9 @@ std::string Pad_GetPinFunction(PAD* pad) {
|
|||
return pad->GetPinFunction().ToStdString();
|
||||
}
|
||||
|
||||
static bool kicadCollabFiberBusyProbe()
|
||||
static bool kicadCollabBusyProbe()
|
||||
{
|
||||
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
|
||||
return pcbjam_collab::applyBusy() || !pcbjam_collab::applyQueue().empty();
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(pcbnew) {
|
||||
|
|
@ -2436,13 +2401,7 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
|||
function("kicadTestSetOpenPark", &kicadTestSetOpenPark);
|
||||
function("kicadTestArmTimerPark", &kicadTestArmTimerPark);
|
||||
function("kicadTestTimerParkState", &kicadTestTimerParkState);
|
||||
function("kicadTestFiberParkStart", &kicadTestFiberParkStart);
|
||||
function("kicadTestFiberParkPrime", &kicadTestFiberParkPrime);
|
||||
function("kicadTestFiberParkPoke", &kicadTestFiberParkPoke);
|
||||
function("kicadTestFiberParkState", &kicadTestFiberParkState);
|
||||
function("kicadTestFiberParkStartSecond", &kicadTestFiberParkStartSecond);
|
||||
function("kicadTestFiberParkPokeSecond", &kicadTestFiberParkPokeSecond);
|
||||
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
|
||||
function("kicadCollabBusy", &kicadCollabBusyProbe);
|
||||
// Read-only viewer lock (read-only-viewer).
|
||||
function("kicadSetReadOnly", &kicadSetReadOnly);
|
||||
// Yjs collaborative bridge entry points (same contract as pl_editor / eeschema).
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
#include <wx/window.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "open_gate.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "pcbjam_async_policy.h"
|
||||
#include <eda_draw_frame.h>
|
||||
#include <kiid.h>
|
||||
|
|
@ -40,7 +39,7 @@ using json = nlohmann::json;
|
|||
// File→Open.
|
||||
bool kicadOpenFile( std::string path )
|
||||
{
|
||||
// Held across every Asyncify park of the load; see open_gate.h.
|
||||
// Held across every suspension of the load; see open_gate.h.
|
||||
pcbjam_open::BusyGuard busy;
|
||||
|
||||
if( pcbjam_open::testParkMs() > 0 )
|
||||
|
|
@ -315,7 +314,7 @@ void addBlob( DS_DATA_MODEL& aModel, const json& j )
|
|||
void kicadCollabApply( std::string aJson )
|
||||
{
|
||||
// Open-in-flight guard (open_gate.h): never touch the model while a
|
||||
// kicadOpenFile Asyncify chain is parked mid-load; see kicadOpenFileBusy.
|
||||
// kicadOpenFile chain is suspended mid-load; see kicadOpenFileBusy.
|
||||
if( pcbjam_open::busy() )
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,24 @@
|
|||
/*
|
||||
* Test-only deterministic repro lever for the production board-load trap
|
||||
* family ("index out of bounds" / "unreachable executed" in doRewind —
|
||||
* gal-refresh-timer investigation, docs/features/async/14-open-settle-gate.md
|
||||
* lineage).
|
||||
* Test-only deterministic lever: a SUSPENDING wx-timer handler running
|
||||
* concurrently with the main loop's per-frame suspension (gal-refresh-timer
|
||||
* investigation, docs/features/async/14-open-settle-gate.md lineage).
|
||||
*
|
||||
* The surviving hypothesis after the v0.1.19 crash log (2026-07-31): a wx
|
||||
* timer callback is a FRESH JS→wasm entry (emscripten_async_call →
|
||||
* TimerCallbackFunc::Run → Notify). The main loop is Asyncify-parked in
|
||||
* wxWasmYieldToBrowser for most of wall-clock time, so a timer handler that
|
||||
* itself parks creates TWO live Asyncify contexts over the single-slot
|
||||
* Asyncify.currData — the emscripten #9153 family the handlesleep.js shim
|
||||
* silently repairs. Add a fiber swap (the collab entries run on
|
||||
* TOOL_MANAGER coroutines — emscripten_fiber_swap, which bypasses the shim's
|
||||
* allocateData accounting entirely) and the prod trap's exact second stack
|
||||
* (doRewind → finishContextSwitch under __asyncjs__wxWasmYieldToBrowser)
|
||||
* becomes constructible on demand.
|
||||
* A wx timer callback is a FRESH JS→wasm entry (emscripten_async_call →
|
||||
* TimerCallbackFunc::Run → Notify). The main loop spends most wall-clock time
|
||||
* suspended in wxWasmYieldToBrowser, so a timer handler that itself suspends
|
||||
* puts a second suspended activation in flight over the shared scheduler
|
||||
* state — the concurrency shape the scheduler's turnstile and the dispatch
|
||||
* interlock must absorb. While the handler is suspended,
|
||||
* wxWasmDispatchParked() reads true and every other due timer spins the 17 ms
|
||||
* retry loop — the [wx-timer] storm diagnostic window this lever opens on
|
||||
* demand.
|
||||
*
|
||||
* Natural occurrences need a timer handler that parks mid-paint (GAL init /
|
||||
* lib bridge) — scheduler-dependent, never reproduced locally in 10+
|
||||
* Natural occurrences need a timer handler that suspends mid-paint (GAL init
|
||||
* / lib bridge) — scheduler-dependent, never reproduced locally in 10+
|
||||
* attempts. This lever makes the window deterministic: arm a one-shot wx
|
||||
* timer whose Notify() emscripten_sleep()s for a fixed time; the e2e then
|
||||
* hammers fiber-based collab entries through the window and asserts the
|
||||
* runtime SURVIVES (red on a build where the hypothesis holds).
|
||||
* hammers coroutine-based collab entries through the window and asserts the
|
||||
* runtime SURVIVES.
|
||||
*
|
||||
* Production is unaffected: nothing fires unless kicadTestArmTimerPark is
|
||||
* called.
|
||||
|
|
|
|||
Loading…
Reference in a new issue