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:
Viktor Vaczi 2026-08-14 09:25:32 +02:00
commit 9c475a804e
120 changed files with 1522 additions and 2974 deletions

View file

@ -57,14 +57,14 @@ CXXFLAGS += -MMD -MP
# whole Asyncify/binaryen post-link pipeline are retired).
EH_FLAGS = -fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1
# Promising entry exports: every JS->wasm entry that can transitively suspend
# (census of EMSCRIPTEN_KEEPALIVE in wxwidgets/src/wasm; the Sched*/abandon
# probes never suspend and stay plain). pcbjam_libctx_entry is libcontext's
# (census of EMSCRIPTEN_KEEPALIVE in wxwidgets/src/wasm; the non-suspending
# wx_dispatch_abandon probe stays plain). pcbjam_libctx_entry is libcontext's
# coroutine entry (only present in apps that link libcontext — emscripten
# warns-and-ignores absent names).
WX_JSPI_EXPORTS = main,wx_dom_event,wx_dom_mouse,wx_window_close,wx_window_move,wx_window_resize,ProcessEvents,wxWasmMailboxTick,wxWasmTopLevelTick,wxWasmJobTick,pcbjam_libctx_entry
# Per-app suspending test levers (fresh-stack ccalls that park or swap a
# coroutine). Appended as a LAST -sJSPI_EXPORTS on the app's link line, which
# wins over the one inside ASYNC_LDFLAGS (emcc last-wins). The non-parking
# wins over the one inside JSPI_LDFLAGS (emcc last-wins). The non-parking
# levers (races_end_active_modal — the answer-synchronously resolve path)
# deliberately stay plain.
RACES_EXTRA_LDFLAGS = -sJSPI_EXPORTS=$(WX_JSPI_EXPORTS),races_swap_once,races_park_token2,races_wdt_park_b
@ -73,12 +73,12 @@ JSPI_SHIM = $(abspath ../../scripts/common/shims/jspi-scheduler.js)
# through it; without forced inclusion the reference resolves to emscripten's
# throwing stub and the FIRST throwing wx handler aborts the whole runtime
# ("native code called abort()" -> pthread mutex deadlock storm).
ASYNC_LDFLAGS = -sJSPI \
JSPI_LDFLAGS = -sJSPI \
-sJSPI_EXPORTS=$(WX_JSPI_EXPORTS) \
-s "EXPORTED_RUNTIME_METHODS=['HEAPU8','HEAP8','HEAP32','ccall','stackSave','stackRestore']" \
-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$$stringToNewUTF8' \
--pre-js $(JSPI_SHIM)
ASYNC_CORO_LDFLAGS = $(ASYNC_LDFLAGS)
JSPI_CORO_LDFLAGS = $(JSPI_LDFLAGS)
CXXFLAGS += $(EH_FLAGS)
# Base Emscripten flags (for all apps)
@ -86,7 +86,7 @@ CXXFLAGS += $(EH_FLAGS)
# - js_writeTextToClipboard, js_readTextFromClipboard, js_clipboardHasText, js_clearClipboard: for clipboard
# - js_enumerateFonts: for font enumeration via Local Font Access API
BASE_LDFLAGS = $(EH_FLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
$(ASYNC_LDFLAGS)
$(JSPI_LDFLAGS)
# LDFLAGS for non-GL apps (standalone tests)
LDFLAGS_NOGL = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(WX_LDFLAGS_NOGL)
@ -116,9 +116,9 @@ LDFLAGS_PTHREAD = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) -pthread \
-sPTHREAD_POOL_SIZE_STRICT=0 \
$(WX_LDFLAGS_NOGL)
# Coroutine harness flags - mirror KiCad's fiber-related runtime needs
# Coroutine harness flags - mirror KiCad's coroutine runtime needs
COROUTINE_BASE_LDFLAGS = $(EH_FLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
$(ASYNC_CORO_LDFLAGS)
$(JSPI_CORO_LDFLAGS)
LDFLAGS_COROUTINE = $(DEBUG_LDFLAGS) $(COROUTINE_BASE_LDFLAGS) $(WX_LDFLAGS_NOGL)
# The suspend-races harness must match PRODUCTION suspension semantics: the KiCad
@ -129,7 +129,7 @@ LDFLAGS_RACES = $(DEBUG_LDFLAGS) $(COROUTINE_BASE_LDFLAGS) -sASSERTIONS=0 $(WX_L
# LDFLAGS for the raytracer thread-deadlock repro. Same pthread + pool config as
# LDFLAGS_PTHREAD, but built on COROUTINE_BASE_LDFLAGS for the
# stack KiCad actually ships (emscripten_sleep needs Asyncify to yield).
# stack KiCad actually ships (emscripten_sleep suspends via JSPI).
LDFLAGS_RAYTRACE = $(DEBUG_LDFLAGS) $(COROUTINE_BASE_LDFLAGS) -pthread \
-sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' \
-sPTHREAD_POOL_SIZE_STRICT=0 \
@ -148,7 +148,7 @@ LDFLAGS_REALPOOL = $(DEBUG_LDFLAGS) $(COROUTINE_BASE_LDFLAGS) -pthread \
# depend on them — otherwise editing a shim wouldn't trigger a relink.
JS = $(TOOLS_ROOT)/wx.js --pre-js $(TOOLS_ROOT)/wx-dom.js
JS_FILES = $(TOOLS_ROOT)/wx.js $(TOOLS_ROOT)/wx-dom.js
# The scheduler shim is a pre-js on every link (ASYNC_LDFLAGS): apps whose
# The scheduler shim is a pre-js on every link (JSPI_LDFLAGS): apps whose
# objects are up to date must still relink when it changes.
JS_FILES += $(JSPI_SHIM)
HTML = $(TOOLS_ROOT)/template.html
@ -677,7 +677,7 @@ all: $(TP_REAL)/threadpool_real_test.html
# On-demand non-warm Worker test (Phase 2). The real pool (compiled-in thread_pool.cpp)
# consumes the pre-warmed Workers, then raw fly-threads force on-demand creation;
# wasm/shims/nanosleep_yield.c (a strong nanosleep override; -Wl,--wrap crashes wasm-ld)
# makes the main-thread sleep_for join Asyncify-yield so the on-demand Workers boot.
# makes the main-thread sleep_for join yield (a JSPI suspension) so the on-demand Workers boot.
# Reuses threadpool-real's pool stubs.
OD = $(S)/pthread-ondemand
OD_INC = -std=c++20 \
@ -720,7 +720,7 @@ async-preload: $(S)/async-preload/async_preload_test.html
all: $(S)/async-preload/async_preload_test.html
# A raytracer worker-join run inside a wx modal pump. The pump dispatches the work via
# ProcessEvents (ccall async:true) at Asyncify state==Normal, so both joins complete:
# ProcessEvents (ccall async:true) between suspensions, so both joins complete:
# m=0 sleep_for busy-wait, m=1 emscripten_sleep yield.
$(S)/raytrace-modal/raytrace_modal_test.o: $(S)/raytrace-modal/raytrace_modal_test.cpp
$(CXX) -c $(CXXFLAGS) -pthread $< -o $@
@ -750,8 +750,9 @@ $(S)/coroutine/libcontext.o: $(KICAD_ROOT)/thirdparty/libcontext/libcontext.cpp
$(S)/coroutine/coroutine_test.html: $(S)/coroutine/coroutine_test.o $(S)/coroutine/libcontext.o $(WX_CORE_LIB) $(JS_FILES)
$(CXX) $(filter %.o %.a,$^) $(LDFLAGS_COROUTINE) --pre-js $(JS) --shell-file $(HTML) -o $@
# Nested coroutine+modal interaction harness - reproduces Asyncify rewind corruption
# when fiber swaps happen inside a wxDialog::ShowModal event loop (Issue #9153).
# Nested coroutine+modal interaction harness - historically reproduced asyncify rewind
# corruption when coroutine swaps happened inside a wxDialog::ShowModal event loop
# (Issue #9153); now pins the same topologies on the JSPI runtime.
$(S)/coroutine-nested/nested_test.o: $(S)/coroutine-nested/nested_test.cpp $(S)/coroutine/kicad_coroutine_harness.h
$(CXX) -c $(CXXFLAGS) -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
@ -851,7 +852,7 @@ clean:
.PHONY: all clean menu contextmenu scrollbar clipboard filedialog layout aui toolbar grid dialog timer tree dataview htmlwin stc print dnd propgrid pickers collapsible listctrl infobar dataviewvirtual auinotebook wizard gridedit calendar gridrenderers printpreview bitmapbuttons specialized validators ownerdrawn popup xml wasmedge fontenum textdecor bitmask regions maximize earlysize selectheight uipolish threadpool logerror retinascale coroutine coroutine-nested asyncify-races notebook radiogroups collapse-relayout
# === Coroutine pthread variant — reproduces the KiCad Asyncify-fiber x pthreads crash ===
# === Coroutine pthread variant — historically reproduced the KiCad coroutine x pthreads crash ===
# Same modal-free harness as `coroutine`, but compiled/linked with pthreads to match
# KiCad's runtime (-pthread + PTHREAD_POOL_SIZE). The single-threaded `coroutine` build
# passes in system Chrome; KiCad (pthreads) crashes. This isolates that difference.

View file

@ -1,34 +1,36 @@
// races_test.cpp - Asyncify race-condition red-green harness.
// races_test.cpp - suspension race-condition red-green harness.
//
// Reproduces the KiCad-WASM Asyncify failure modes deterministically so the shim
// fixes stay pinned by tests (see features/async/ research dossier):
// Reproduces the KiCad-WASM suspension failure modes deterministically so the
// scheduler-shim fixes stay pinned by tests (see features/async/ research
// dossier). The scenarios were decoded on the retired asyncify runtime; the
// topologies they stage are runtime-agnostic and now pin the JSPI scheduler's
// turnstile/window contracts:
//
// - The app performs a fiber swap during OnInit BEFORE the main loop parks.
// This is the load-bearing topology detail: it means main() is resumed via
// Fibers.trampoline() when wxGUIEventLoop::DoRun() executes the
// emscripten_set_main_loop(...,1) `throw "unwind"` park, so the throw tears
// through the live trampoline do/while. Without the trampoline self-heal
// shim that wedges Fibers.trampolineRunning=true forever and the FIRST
// post-park fiber swap hangs (the KiCad schematic/PCB tool hang).
// - The app performs a coroutine swap during OnInit BEFORE the main loop
// parks. This is the load-bearing topology detail: the park then lands on
// a stack that already completed a suspension chain (the startup shape
// that historically wedged the asyncify fiber trampoline — the KiCad
// schematic/PCB tool hang).
// coroutine-nested/nested_test.cpp does NOT do a pre-park swap, which is
// why it never reproduced that hang.
//
// - EM_ASYNC_JS sleeps (modal dialogs, token waits) overlapping fiber swaps
// reproduce the single-slot Asyncify.currData clobber family (the KiCad
// clipboard "index out of bounds" crash).
// - EM_ASYNC_JS sleeps (modal dialogs, token waits) overlapping coroutine
// swaps — historically the single-slot Asyncify.currData clobber family
// (the KiCad clipboard "index out of bounds" crash), now the concurrent
// multi-suspension bookkeeping the scheduler's turnstile serializes.
//
// URL parameters:
// ?only=<scenario> run a single scenario instead of the default battery
// (used for scenarios that intentionally wedge/crash)
// ?mode=sleep-park make the LAST pre-park suspension a sleep instead of a
// fiber swap: the park throw then escapes through the
// sleep's wakeUp promise reaction as an unhandled
// "unwind" rejection (scenario unwind_through_promise)
// coroutine swap, so the park arrives out of a sleep
// resume rather than a swap (scenario
// unwind_through_promise)
//
// Output protocol (polled by tests/asyncify/asyncify-races.spec.ts):
// Output protocol (polled by tests/jspi/suspend-races.spec.ts):
// [ASYNCIFY_RACES] CASE <name>
// [ASYNCIFY_RACES] PASS <name> / FAIL <name> :: <detail>
// [ASYNCIFY_RACES] WATCHDOG <name> state=.. currData=.. trampolineRunning=..
// [ASYNCIFY_RACES] WATCHDOG <name> windowLive=.. resumeReady=..
// [ASYNCIFY_RACES] SUMMARY total=N passed=N failed=N
#include "wx/wx.h"
@ -110,7 +112,7 @@ EM_ASYNC_JS( int, races_await_token, ( int aToken ), {
// a raw await's engine-level resume bypasses the SP discipline and never
// ends the current window (windowLive wedges the pump).
var S = globalThis.__wxScheduler;
if( S && S.backend === 'jspi' )
if( S )
return await S.promiseYield( p, 'races-token' );
return await p;
} );
@ -128,7 +130,7 @@ EM_JS( void, races_resolve_token_after, ( int aToken, int aValue, int aDelayMs )
// Plain parked sleep.
EM_ASYNC_JS( int, races_sleep_ms, ( int aMs ), {
var S = globalThis.__wxScheduler;
if( S && S.backend === 'jspi' ) {
if( S ) {
await S.sleepYield( aMs ); // turnstile-routed (see races_await_token)
return 1;
}
@ -152,19 +154,19 @@ EM_JS( void, races_schedule_ccall, ( const char* aFunc, int aDelayMs ), {
}, aDelayMs );
} );
// Watchdog: if the scenario hasn't marked itself done in aMs, dump the Asyncify
// state and emit a FAIL line. JS-side, so it fires even when C++ is wedged.
// Watchdog: if the scenario hasn't marked itself done in aMs, dump the
// scheduler state and emit a FAIL line. JS-side, so it fires even when C++ is
// wedged.
EM_JS( void, races_arm_watchdog, ( const char* aName, int aMs ), {
var name = UTF8ToString( aName );
Module.__racesDone = Module.__racesDone || {};
setTimeout( function() {
if( !Module.__racesDone[name] ) {
var st = ( typeof Asyncify !== 'undefined' ) ? Asyncify.state : 'n/a';
var cd = ( typeof Asyncify !== 'undefined' ) ? ( Asyncify.currData || 0 ) : 'n/a';
var tr = ( typeof Fibers !== 'undefined' ) ? Fibers.trampolineRunning : 'n/a';
var nf = ( typeof Fibers !== 'undefined' ) ? Fibers.nextFiber : 'n/a';
console.log( '[ASYNCIFY_RACES] WATCHDOG ' + name + ' state=' + st + ' currData=' + cd
+ ' trampolineRunning=' + tr + ' nextFiber=' + nf );
var S = globalThis.__wxScheduler;
var wl = S ? !!S._windowLive : 'n/a';
var rr = ( S && S._resumeReady ) ? S._resumeReady.length : 'n/a';
console.log( '[ASYNCIFY_RACES] WATCHDOG ' + name + ' windowLive=' + wl
+ ' resumeReady=' + rr );
console.log( '[ASYNCIFY_RACES] FAIL ' + name + ' :: watchdog timeout (suspension never completed)' );
}
}, aMs );
@ -175,34 +177,28 @@ EM_JS( void, races_mark_done, ( const char* aName ), {
Module.__racesDone[UTF8ToString( aName )] = true;
} );
// Quiescence invariant sampled from C++ between scenarios.
//
// Two things are deliberately NOT checked:
// * Fibers.trampolineRunning — this can run on a stack itself resumed via
// Fibers.trampoline(), in which case the guard is legitimately true.
// * Asyncify.currData — under native wasm-EH the top-level event loop is a
// per-frame-yield while-loop (wxWasmYieldToBrowser, an EM_ASYNC_JS rAF
// suspend that re-arms every frame; see wxwidgets/src/wasm/evtloop.cpp). So
// the main stack is asyncify-suspended between frames and currData is
// legitimately churning — it is non-zero while a frame yield is pending, and
// can momentarily hold a freed-but-not-yet-nulled buffer right after a
// concurrent suspension resumes. That is a transient bookkeeping value, NOT a
// leak (the buffers are _malloc/_free'd each frame — addresses are reused),
// so requiring currData==0 here is a stale legacy assumption from the old
// throw-to-park loop. A genuinely stuck suspension is caught by state != 0
// (Suspending/Rewinding never clearing) and by the scenario watchdogs.
// What's left is the real invariant: the asyncify machine is back to Normal and
// no fiber is queued.
// Quiescence invariant sampled from C++ between scenarios, keyed to the JSPI
// scheduler (globalThis.__wxScheduler): between scenarios no resume window may
// still be live (_windowLive) and no resume may sit queued (_resumeReady).
// The main loop's own per-frame suspension (wxWasmYieldToBrowser) does not
// count against either — its window closes when the frame yield's suspension
// completes, before the next C++ code runs. A genuinely stuck suspension is
// additionally caught by the scenario watchdogs. Without a scheduler (the
// raw-await harness builds) there is no shared state to wedge — quiescent by
// construction.
EM_JS( int, races_quiescent, (), {
try {
// JSPI glue still defines an Asyncify object (shared library file)
// but with no state machine - Asyncify.state is undefined there, and
// that is quiescent-by-construction (suspensions are engine-native).
var stOk = ( typeof Asyncify === 'undefined' )
|| Asyncify.state === undefined
|| Asyncify.state === 0;
var nfOk = ( typeof Fibers === 'undefined' ) || !Fibers.nextFiber;
return ( stOk && nfOk ) ? 1 : 0;
var S = globalThis.__wxScheduler;
if( !S )
return 1;
// NOTE: _windowLive is NOT part of quiescence here — this probe runs
// from INSIDE a tracked activation, whose own window is live by
// definition. A wedge manifests as backlog: queued-but-unarmed
// resumes or a stuck mutator FIFO (the 2s force-clear watchdog keys
// on the same signal).
var rrOk = !S._resumeReady || S._resumeReady.length === 0;
var mqOk = !S.mutatorQueue || S.mutatorQueue.length === 0;
return ( rrOk && mqOk ) ? 1 : 0;
} catch( e ) {
return 0;
}
@ -211,12 +207,11 @@ EM_JS( int, races_quiescent, (), {
EM_JS( void, races_log_state, ( const char* aTag ), {
try {
var tag = UTF8ToString( aTag );
var st = ( typeof Asyncify !== 'undefined' ) ? Asyncify.state : 'n/a';
var cd = ( typeof Asyncify !== 'undefined' ) ? ( Asyncify.currData || 0 ) : 'n/a';
var tr = ( typeof Fibers !== 'undefined' ) ? Fibers.trampolineRunning : 'n/a';
var nf = ( typeof Fibers !== 'undefined' ) ? Fibers.nextFiber : 'n/a';
console.log( '[ASYNCIFY_RACES] STATE ' + tag + ' state=' + st + ' currData=' + cd
+ ' trampolineRunning=' + tr + ' nextFiber=' + nf );
var S = globalThis.__wxScheduler;
var wl = S ? !!S._windowLive : 'n/a';
var rr = ( S && S._resumeReady ) ? S._resumeReady.length : 'n/a';
console.log( '[ASYNCIFY_RACES] STATE ' + tag + ' windowLive=' + wl
+ ' resumeReady=' + rr );
} catch( e ) {}
} );
@ -380,8 +375,8 @@ private:
{
#ifdef __EMSCRIPTEN__
aCtx.Expect( races_quiescent() == 1,
"asyncify machine not quiescent " + aWhere
+ " (state/currData/trampolineRunning/nextFiber - see STATE log)" );
"scheduler not quiescent " + aWhere
+ " (windowLive/resumeReady - see STATE log)" );
if( races_quiescent() != 1 )
races_log_state( ( "non-quiescent-" + aWhere ).c_str() );
@ -870,10 +865,10 @@ public:
} );
#endif
// THE LOAD-BEARING TOPOLOGY: complete a fiber swap cycle during OnInit.
// From here on, main() runs inside Fibers.trampoline()'s do/while; the
// upcoming emscripten_set_main_loop(...,1) park throw will tear through
// that live frame (exactly what KiCad's startup tool burst does).
// THE LOAD-BEARING TOPOLOGY: complete a coroutine swap cycle during
// OnInit, before the main loop parks (exactly what KiCad's startup
// tool burst does). Historically this put main() inside the asyncify
// fiber trampoline when the park throw tore through it.
{
TestCoroutine co( []( TestCoroutine& self ) { self.Yield( 1 ); } );
co.Call( 1 );
@ -884,9 +879,9 @@ public:
#ifdef __EMSCRIPTEN__
if( sleepPark )
{
// Make the LAST pre-park suspension a sleep: main is then resumed
// from the sleep's wakeUp (trampoline frame already closed), and the
// park throw escapes through the wakeUp promise reaction instead.
// Make the LAST pre-park suspension a sleep: main then reaches the
// park out of a sleep resume rather than a swap (historically the
// park throw escaped through the sleep's wakeUp promise reaction).
races_sleep_ms( 30 );
LogLine( "[ASYNCIFY_RACES] PRE-PARK-SLEEP done (sleep-park mode)" );
}

View file

@ -92,33 +92,6 @@ void LogLine( const std::string& aLine )
}
void LogAsyncifyState( const char* aTag )
{
#ifdef __EMSCRIPTEN__
EM_ASM( {
try {
var tag = UTF8ToString( $0 );
var state = ( typeof Asyncify !== 'undefined' ) ? Asyncify.state : 'N/A';
var stackLen = ( typeof Asyncify !== 'undefined' && Asyncify.exportCallStack )
? Asyncify.exportCallStack.length : 'N/A';
var currData = ( typeof Asyncify !== 'undefined' && Asyncify.currData )
? Asyncify.currData : 'null';
var tableLen = ( typeof wasmTable !== 'undefined' && wasmTable )
? wasmTable.length : 'N/A';
console.log( '[COROUTINE_TEST] ASYNCIFY ' + tag +
' state=' + state +
' stackLen=' + stackLen +
' currData=' + currData +
' tableLen=' + tableLen );
} catch (e) {
console.log( '[COROUTINE_TEST] ASYNCIFY ' + UTF8ToString( $0 ) + ' error=' + e );
}
}, aTag );
#else
(void) aTag;
#endif
}
} // namespace
@ -155,7 +128,6 @@ private:
if( aEvent.IsShown() )
{
LogLine( "[COROUTINE_TEST] MODAL-SHOW " + m_tag );
LogAsyncifyState( ( "modal-shown-" + m_tag ).c_str() );
if( !m_externalClose )
m_timer.StartOnce( m_delayMs );
@ -193,7 +165,7 @@ public:
panel,
wxID_ANY,
"Tests the interaction between wxDialog::ShowModal (EM_ASYNC_JS / startModal) and\n"
"libcontext fibers (emscripten_fiber_swap). Reproduces nested Asyncify crashes.\n"
"libcontext coroutines (JSPI). Historically reproduced nested asyncify crashes.\n"
"The suite runs automatically on startup and reports PASS/FAIL per scenario."
);
sizer->Add( description, 0, wxEXPAND | wxALL, 8 );
@ -276,7 +248,6 @@ private:
Log( wxString::Format( "[COROUTINE_TEST] CASE %s", caseName ) );
CaseContext ctx;
LogAsyncifyState( "A-pre-modal" );
{
AutoClosingDialog dlg( this, "baselineA", 50 );
@ -284,7 +255,6 @@ private:
ctx.Expect( result == wxID_OK, "modal should return wxID_OK" );
}
LogAsyncifyState( "A-post-modal" );
FinalizeCase( caseName, std::move( ctx ) );
}
@ -296,7 +266,6 @@ private:
Log( wxString::Format( "[COROUTINE_TEST] CASE %s", caseName ) );
CaseContext ctx;
LogAsyncifyState( "B-pre-fiber" );
TestCoroutine coroutine( []( TestCoroutine& self ) {
self.Yield( 42 );
@ -309,7 +278,6 @@ private:
running = coroutine.Resume( 2 );
ctx.Expect( !running, "fiber should finish on resume" );
LogAsyncifyState( "B-post-fiber" );
FinalizeCase( caseName, std::move( ctx ) );
}
@ -323,7 +291,6 @@ private:
m_currentCaseName = caseName;
m_currentCtx = std::make_unique<CaseContext>();
LogAsyncifyState( "S3-pre-modal" );
auto dlg = std::make_unique<AutoClosingDialog>( this, "S3", 0 );
dlg->UseExternalClose();
@ -337,7 +304,6 @@ private:
int result = dlg->ShowModal();
m_activeDialog = nullptr;
LogAsyncifyState( "S3-post-modal" );
m_currentCtx->Expect( result == wxID_OK,
"modal should return wxID_OK (actual: " + std::to_string( result ) + ")" );
@ -350,7 +316,6 @@ private:
void RunScenario3FiberWork()
{
LogAsyncifyState( "S3-timer-enter" );
{
TestCoroutine co( []( TestCoroutine& self ) {
@ -361,16 +326,13 @@ private:
m_currentCtx->Expect( running, "S3: fiber should yield on first call" );
m_currentCtx->Expect( co.LastReturnValue() == 100, "S3: yield value should be 100" );
LogAsyncifyState( "S3-after-call" );
running = co.Resume( 2 );
m_currentCtx->Expect( !running, "S3: fiber should finish on resume" );
LogAsyncifyState( "S3-after-resume" );
}
// Fiber destroyed here
LogAsyncifyState( "S3-after-destroy" );
if( m_activeDialog )
m_activeDialog->EndModalExternal( wxID_OK );
@ -385,7 +347,6 @@ private:
m_currentCaseName = caseName;
m_currentCtx = std::make_unique<CaseContext>();
LogAsyncifyState( "S4-pre-modal" );
auto dlg = std::make_unique<AutoClosingDialog>( this, "S4", 0 );
dlg->UseExternalClose();
@ -397,7 +358,6 @@ private:
int result = dlg->ShowModal();
m_activeDialog = nullptr;
LogAsyncifyState( "S4-post-modal" );
m_currentCtx->Expect( result == wxID_OK, "S4: modal should return wxID_OK" );
FinalizeCase( caseName, std::move( *m_currentCtx ) );
@ -408,7 +368,6 @@ private:
void RunScenario4MultiSwap()
{
LogAsyncifyState( "S4-timer-enter" );
{
TestCoroutine co( []( TestCoroutine& self ) {
@ -433,7 +392,6 @@ private:
m_currentCtx->Expect( !running, "S4: fiber should finish" );
}
LogAsyncifyState( "S4-after-fiber" );
if( m_activeDialog )
m_activeDialog->EndModalExternal( wxID_OK );
@ -449,7 +407,6 @@ private:
m_currentCaseName = caseName;
m_currentCtx = std::make_unique<CaseContext>();
LogAsyncifyState( "S5-pre-modal" );
m_s5Fiber = std::make_unique<TestCoroutine>( []( TestCoroutine& self ) {
self.Yield( 501 );
@ -466,7 +423,6 @@ private:
int result = dlg->ShowModal();
m_activeDialog = nullptr;
LogAsyncifyState( "S5-post-modal" );
// After modal, resume the fiber
if( m_s5Fiber && m_s5Fiber->Running() )
@ -489,13 +445,11 @@ private:
void RunScenario5Yield()
{
LogAsyncifyState( "S5-timer-enter" );
bool running = m_s5Fiber->Call( 1 );
m_currentCtx->Expect( running, "S5: fiber should yield in modal" );
m_currentCtx->Expect( m_s5Fiber->LastReturnValue() == 501, "S5: yield 501" );
LogAsyncifyState( "S5-fiber-yielded" );
// Do NOT resume; leave the fiber suspended across the modal close.
@ -504,7 +458,8 @@ private:
}
// --- Case 6: fiber_deep_yield_loop_inside_modal ---
// Deep recursive stack with many yields inside a modal. Stresses asyncify buffers.
// Deep recursive stack with many yields inside a modal. Stresses the stack-capture
// machinery under deep frames.
void StartCase_FiberDeepYieldLoop()
{
const std::string caseName = "fiber_deep_yield_loop_inside_modal";
@ -512,7 +467,6 @@ private:
m_currentCaseName = caseName;
m_currentCtx = std::make_unique<CaseContext>();
LogAsyncifyState( "S6-pre-modal" );
auto dlg = std::make_unique<AutoClosingDialog>( this, "S6", 0 );
dlg->UseExternalClose();
@ -524,7 +478,6 @@ private:
int result = dlg->ShowModal();
m_activeDialog = nullptr;
LogAsyncifyState( "S6-post-modal" );
m_currentCtx->Expect( result == wxID_OK, "S6: modal should return wxID_OK" );
FinalizeCase( caseName, std::move( *m_currentCtx ) );
@ -535,7 +488,6 @@ private:
void RunScenario6DeepYield()
{
LogAsyncifyState( "S6-timer-enter" );
{
TestCoroutine co( [ctx = m_currentCtx.get()]( TestCoroutine& self ) {
@ -571,7 +523,6 @@ private:
m_currentCtx->Expect( !running, "S6: deep fiber should finish" );
}
LogAsyncifyState( "S6-after-fiber" );
if( m_activeDialog )
m_activeDialog->EndModalExternal( wxID_OK );
@ -585,7 +536,6 @@ private:
Log( wxString::Format( "[COROUTINE_TEST] CASE %s", caseName ) );
CaseContext ctx;
LogAsyncifyState( "S7-pre-modal-A" );
// Modal A (auto-close)
{
@ -594,7 +544,6 @@ private:
ctx.Expect( resultA == wxID_OK, "S7: modal A should return wxID_OK" );
}
LogAsyncifyState( "S7-post-modal-A" );
// Fiber work between modals
{
@ -609,7 +558,6 @@ private:
ctx.Expect( !running, "S7: inter-modal fiber should finish" );
}
LogAsyncifyState( "S7-mid" );
// Modal B (auto-close)
{
@ -618,7 +566,6 @@ private:
ctx.Expect( resultB == wxID_OK, "S7: modal B should return wxID_OK" );
}
LogAsyncifyState( "S7-post-modal-B" );
FinalizeCase( "modal_fiber_modal_sequence", std::move( ctx ) );
@ -634,7 +581,6 @@ private:
m_currentCaseName = caseName;
m_currentCtx = std::make_unique<CaseContext>();
LogAsyncifyState( "S8-pre-modal" );
auto dlg = std::make_unique<AutoClosingDialog>( this, "S8", 0 );
dlg->UseExternalClose();
@ -646,7 +592,6 @@ private:
int result = dlg->ShowModal();
m_activeDialog = nullptr;
LogAsyncifyState( "S8-post-modal" );
m_currentCtx->Expect( result == wxID_OK, "S8: modal should return wxID_OK" );
FinalizeCase( caseName, std::move( *m_currentCtx ) );
@ -658,7 +603,6 @@ private:
void RunScenario8NestedFibers()
{
LogAsyncifyState( "S8-timer-enter" );
{
auto ctx = m_currentCtx.get();
@ -692,7 +636,6 @@ private:
"S8: unexpected sequence: " + JoinVector( sequence ) );
}
LogAsyncifyState( "S8-after-fiber" );
if( m_activeDialog )
m_activeDialog->EndModalExternal( wxID_OK );

View file

@ -1,5 +1,6 @@
// jspi-coroutine — validates the JSPI libcontext backend (PCBJAM_JSPI) through
// the EXACT protocol coroutine.h drives it with, without linking wx or KiCad.
// jspi-coroutine — validates the JSPI libcontext backend (the __EMSCRIPTEN__
// build of kicad/thirdparty/libcontext) through the EXACT protocol
// coroutine.h drives it with, without linking wx or KiCad.
//
// MiniCoro below is a compact transcription of COROUTINE<>'s libcontext
// mechanics (doCall/jumpIn/jumpOut/callerStub, INVOCATION_ARGS, the
@ -109,7 +110,7 @@ struct MiniCoro
cor->m_body( *cor );
cor->m_running = false;
// the 3-line JSPI hook coroutine.h carries under PCBJAM_JSPI
// the completion hook coroutine.h carries under __EMSCRIPTEN__
libcontext::finish_fcontext( cor->m_callee.ctx );
cor->jumpOut();