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
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)" );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue