feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on)
Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy encoding) across the whole build, keeping Asyncify coroutines working via a from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially. Highlights: - Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify; post-link Asyncify covers every app wasm (not just standalone test wasm). - Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports. - libcontext fiber entry wired under native exceptions; while-loop main loop + currData shim injected into all wx apps. - Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch. - 3D viewer enabled by default (real raytracer linked, not the stub). - Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to shim-redundancy pins (native-EH stays clean with the legacy shims ablated). - Fix the asyncify-races quiescence check to not require Asyncify.currData==0: under the native-EH per-frame-yield top loop the main stack is asyncify-suspended every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer, not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad. - CI: drop the obsolete binaryen_version input/env (the build uses the binaryen submodule fork's wasm-opt, not a version download); key the wasm-output cache on the binaryen submodule SHA instead. Bumps the wxwidgets + binaryen submodules to their squashed feature commits. Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63 Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines; asyncify 7/7 both engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b8c8dee355
commit
c1ef489cfa
75 changed files with 4976 additions and 680 deletions
|
|
@ -289,3 +289,18 @@ Button positions (relative to canvas):
|
|||
|
||||
- **Timer tests**: May fail due to timing sensitivity
|
||||
- **Tree tests**: Button click positions may vary
|
||||
|
||||
## Open tasks
|
||||
|
||||
- **Research: are the Asyncify fiber shims still needed under native-EH?** Two
|
||||
`scripts/common/inject-dyncall-shims.sh` fixes — the fiber trampoline self-heal
|
||||
(§3c) and the nested-Asyncify handleSleep save/restore (§3) — were written for the
|
||||
legacy-EH *park-throw* model. Under native wasm-EH the top loop is a per-frame-yield
|
||||
while-loop with no park-throw, so ablating either shim
|
||||
(`SHIM_DISABLE_TRAMPOLINE_HEAL` / `SHIM_DISABLE_HANDLESLEEP`, wired in
|
||||
`tests/apps/Makefile.wasm`) no longer reproduces the disease it guarded — the old
|
||||
red "ablation pins" in `asyncify/asyncify-races.spec.ts` have been flipped to
|
||||
green "shim-redundancy pins" that now prove the native-EH path stays clean *with the
|
||||
shim ablated*. **To do:** sweep the real apps (modals, nested fibers, long sleeps,
|
||||
pthread pool) with each shim ablated; if all stay green, drop the shim injection and
|
||||
these pins. Until proven, they stay injected (belt-and-suspenders).
|
||||
|
|
|
|||
1
tests/apps/.gitignore
vendored
Normal file
1
tests/apps/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
standalone/_pool_unshimmed/
|
||||
|
|
@ -51,16 +51,23 @@ endif
|
|||
# crashes apps at startup with "RuntimeError: function signature mismatch".
|
||||
CXXFLAGS += -MMD -MP
|
||||
|
||||
# Native wasm-EH is the only build mode: compile + link with -fwasm-exceptions (legacy encoding) +
|
||||
# wasm setjmp/longjmp, matching the libwx build (scripts/build-wx-wasm.sh) and scripts/common/env.sh.
|
||||
# The in-link Asyncify (emsdk Binaryen v121) crashes on wasm-EH, so build-wasm-test.sh stubs it and
|
||||
# runs the real pipeline (--hoist-cpp-catches + --asyncify via apply-asyncify.sh) post-link.
|
||||
EH_FLAGS = -fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1 -sDYNCALLS=1
|
||||
CXXFLAGS += $(EH_FLAGS)
|
||||
|
||||
# Base Emscripten flags (for all apps)
|
||||
# ASYNCIFY enables blocking modal dialogs (ShowModal waits for user)
|
||||
# ASYNCIFY_IMPORTS tells Emscripten which imported JS functions can unwind the stack
|
||||
# - startModal: for modal dialogs
|
||||
# - js_writeTextToClipboard, js_readTextFromClipboard, js_clipboardHasText, js_clearClipboard: for clipboard
|
||||
# - js_enumerateFonts: for font enumeration via Local Font Access API
|
||||
BASE_LDFLAGS = -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
BASE_LDFLAGS = $(EH_FLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-s "EXPORTED_RUNTIME_METHODS=['HEAPU8','HEAP8','HEAP32','ccall']" \
|
||||
-sASYNCIFY=1 \
|
||||
-sASYNCIFY_STACK_SIZE=8192 \
|
||||
-sASYNCIFY_STACK_SIZE=65536 \
|
||||
-sASYNCIFY_IMPORTS=['startModal','js_writeTextToClipboard','js_readTextFromClipboard','js_clipboardHasText','js_clearClipboard','js_enumerateFonts']
|
||||
|
||||
# LDFLAGS for non-GL apps (standalone tests)
|
||||
|
|
@ -92,7 +99,7 @@ LDFLAGS_PTHREAD = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) -pthread \
|
|||
$(WX_LDFLAGS_NOGL)
|
||||
|
||||
# Coroutine harness flags - mirror KiCad's fiber-related runtime needs
|
||||
COROUTINE_BASE_LDFLAGS = -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
COROUTINE_BASE_LDFLAGS = $(EH_FLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-s "EXPORTED_RUNTIME_METHODS=['HEAPU8','HEAP8','HEAP32','ccall']" \
|
||||
-sASYNCIFY=1 \
|
||||
-sASYNCIFY_STACK_SIZE=65536 \
|
||||
|
|
@ -113,6 +120,13 @@ LDFLAGS_RAYTRACE = $(DEBUG_LDFLAGS) $(COROUTINE_BASE_LDFLAGS) -pthread \
|
|||
-sPTHREAD_POOL_SIZE_STRICT=0 \
|
||||
$(WX_LDFLAGS_NOGL)
|
||||
|
||||
# LDFLAGS for the real-pool test (Phase 1a). Like LDFLAGS_RAYTRACE but pre-warm +2 workers so
|
||||
# the lifecycle mode's small BS::pause_thread_pool(2) gets pre-warmed workers (no on-demand spawn).
|
||||
LDFLAGS_REALPOOL = $(DEBUG_LDFLAGS) $(COROUTINE_BASE_LDFLAGS) -pthread \
|
||||
-sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency + 2' \
|
||||
-sPTHREAD_POOL_SIZE_STRICT=0 \
|
||||
$(WX_LDFLAGS_NOGL)
|
||||
|
||||
# A second pre-js carries the DOM-control shim ("--pre-js A --pre-js B"
|
||||
# after expansion — emcc accepts repeated --pre-js).
|
||||
# JS_FILES lists the actual pre-js paths (no flags) so the link targets can
|
||||
|
|
@ -209,13 +223,13 @@ minimal_test.html: minimal_test.o $(WX_CORE_LIB) $(JS_FILES)
|
|||
|
||||
# --- DOM-port bug reproductions (docs/features/wx-dom-port/branch-review.md) ---
|
||||
|
||||
# wxTextCtrl reentry-guard repro. Needs -fexceptions: it throws from a wxEVT_TEXT
|
||||
# handler to prove the OnDomEvent m_inDomInput reset must be exception-safe.
|
||||
# wxTextCtrl reentry-guard repro: it throws from a wxEVT_TEXT handler to prove the
|
||||
# OnDomEvent m_inDomInput reset must be exception-safe (works under native -fwasm-exceptions).
|
||||
$(S)/textctrl-reentry/textctrl-reentry_test.o: $(S)/textctrl-reentry/textctrl-reentry_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -fexceptions $< -o $@
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(S)/textctrl-reentry/textctrl-reentry_test.html: $(S)/textctrl-reentry/textctrl-reentry_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_NOGL) -fexceptions --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
$(CXX) $< $(LDFLAGS_NOGL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
textctrl-reentry: $(S)/textctrl-reentry/textctrl-reentry_test.html
|
||||
|
||||
|
|
@ -589,6 +603,100 @@ raytrace-threads: $(S)/raytrace-threads/raytrace_threads_test.html
|
|||
# Include the raytracer-threading repro in the default `all` build (prereqs accumulate).
|
||||
all: $(S)/raytrace-threads/raytrace_threads_test.html
|
||||
|
||||
# Un-shimmed copy of KiCad's BS::thread_pool: the single-thread #ifdef __EMSCRIPTEN__ shim in
|
||||
# detach_task is disabled (-> #if 0) so the pool tests exercise REAL multithreading WITHOUT
|
||||
# modifying the pristine KiCad header and WITHOUT a compile macro. Regenerated at parse time to
|
||||
# stay in sync. (HASH is a literal '#', escaped so make does not treat it as a comment.)
|
||||
HASH := \#
|
||||
POOL_UNSHIMMED := $(S)/_pool_unshimmed
|
||||
$(shell mkdir -p $(POOL_UNSHIMMED); sed 's/$(HASH)ifdef __EMSCRIPTEN__/$(HASH)if 0/' $(KICAD_ROOT)/thirdparty/thread-pool/bs_thread_pool.hpp > $(POOL_UNSHIMMED)/bs_thread_pool.hpp)
|
||||
|
||||
# Real KiCad thread-pool test (Phase 1a / native-EH). Compiles the REAL
|
||||
# kicad/common/thread_pool.cpp (GetKiCadThreadPool) with minimal pgm_base/advanced_config
|
||||
# stubs; the pool is un-shimmed via a generated bs_thread_pool.hpp (POOL_UNSHIMMED below) so
|
||||
# tasks run on workers.
|
||||
TP_REAL = $(S)/threadpool-real
|
||||
TP_REAL_INC = -std=c++20 \
|
||||
-I$(TP_REAL)/stubs -I$(POOL_UNSHIMMED) -I$(KICAD_ROOT)/include -I$(KICAD_ROOT)/thirdparty/thread-pool
|
||||
|
||||
$(TP_REAL)/threadpool_real_test.o: $(TP_REAL)/threadpool_real_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $(TP_REAL_INC) $< -o $@
|
||||
|
||||
$(TP_REAL)/thread_pool.o: $(KICAD_ROOT)/common/thread_pool.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $(TP_REAL_INC) $< -o $@
|
||||
|
||||
$(TP_REAL)/kicad_pool_stubs.o: $(TP_REAL)/stubs/kicad_pool_stubs.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $(TP_REAL_INC) $< -o $@
|
||||
|
||||
$(TP_REAL)/threadpool_real_test.html: $(TP_REAL)/threadpool_real_test.o $(TP_REAL)/thread_pool.o $(TP_REAL)/kicad_pool_stubs.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $(filter %.o %.a,$^) $(LDFLAGS_REALPOOL) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
threadpool-real: $(TP_REAL)/threadpool_real_test.html
|
||||
.PHONY: threadpool-real
|
||||
|
||||
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.
|
||||
# Reuses threadpool-real's pool stubs.
|
||||
OD = $(S)/pthread-ondemand
|
||||
OD_INC = -std=c++20 \
|
||||
-I$(S)/threadpool-real/stubs -I$(POOL_UNSHIMMED) -I$(KICAD_ROOT)/include -I$(KICAD_ROOT)/thirdparty/thread-pool
|
||||
SHIMS = ../../wasm/shims
|
||||
|
||||
$(OD)/pthread_ondemand_test.o: $(OD)/pthread_ondemand_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $(OD_INC) $< -o $@
|
||||
|
||||
$(OD)/thread_pool.o: $(KICAD_ROOT)/common/thread_pool.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $(OD_INC) $< -o $@
|
||||
|
||||
$(OD)/kicad_pool_stubs.o: $(S)/threadpool-real/stubs/kicad_pool_stubs.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $(OD_INC) $< -o $@
|
||||
|
||||
# Compiled as C (no -std=c++): em++ keys language off the .c extension.
|
||||
$(OD)/nanosleep_yield.o: $(SHIMS)/nanosleep_yield.c
|
||||
$(CXX) -c -pthread $< -o $@
|
||||
|
||||
$(OD)/pthread_ondemand_test.html: $(OD)/pthread_ondemand_test.o $(OD)/thread_pool.o $(OD)/kicad_pool_stubs.o $(OD)/nanosleep_yield.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $(filter %.o %.a,$^) $(LDFLAGS_RAYTRACE) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
|
||||
pthread-ondemand: $(OD)/pthread_ondemand_test.html
|
||||
.PHONY: pthread-ondemand
|
||||
|
||||
all: $(OD)/pthread_ondemand_test.html
|
||||
|
||||
# Library-preload native-EH repro (1b). Standalone std::async worker that parses+throws (mode-c)
|
||||
# and proxies a fetch to main (emscripten_proxy_*) — mirrors the KiCad-10 PCBJAM preload shape.
|
||||
# No KiCad source. Uses the system proxying queue (built into the pthread runtime).
|
||||
$(S)/async-preload/async_preload_test.o: $(S)/async-preload/async_preload_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) -pthread $< -o $@
|
||||
|
||||
$(S)/async-preload/async_preload_test.html: $(S)/async-preload/async_preload_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_RAYTRACE) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
async-preload: $(S)/async-preload/async_preload_test.html
|
||||
.PHONY: async-preload
|
||||
|
||||
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:
|
||||
# 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 $@
|
||||
|
||||
$(S)/raytrace-modal/raytrace_modal_test.html: $(S)/raytrace-modal/raytrace_modal_test.o $(WX_CORE_LIB) $(JS_FILES)
|
||||
$(CXX) $< $(LDFLAGS_RAYTRACE) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
raytrace-modal: $(S)/raytrace-modal/raytrace_modal_test.html
|
||||
.PHONY: raytrace-modal
|
||||
|
||||
all: $(S)/raytrace-modal/raytrace_modal_test.html
|
||||
|
||||
# Log Error test (no GL) - reproduces KiCad's kiface error dialog
|
||||
$(S)/logerror/logerror_test.o: $(S)/logerror/logerror_test.cpp
|
||||
$(CXX) -c $(CXXFLAGS) $< -o $@
|
||||
|
|
@ -635,6 +743,7 @@ $(S)/asyncify-races/races_test_nosleepfix.html: $(S)/asyncify-races/races_test.o
|
|||
$(CXX) $^ $(LDFLAGS_RACES) --pre-js $(JS) --shell-file $(HTML) -o $@
|
||||
SHIM_DISABLE_HANDLESLEEP=1 ../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
|
||||
# Convenience targets
|
||||
menu: $(S)/menu/menu_test.html
|
||||
contextmenu: $(S)/contextmenu/contextmenu_test.html
|
||||
|
|
@ -730,7 +839,7 @@ coroutine-pthread: $(S)/coroutine-pthread/coroutine_test.html
|
|||
.PHONY: coroutine-pthread
|
||||
|
||||
# === No-wx pthread main() reproduction: fiber pattern in main + pthreads, no wxWidgets ===
|
||||
LDFLAGS_COROUTINE_PTHREAD_NOWX = $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
LDFLAGS_COROUTINE_PTHREAD_NOWX = $(EH_FLAGS) $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 -sASYNCIFY_IMPORTS=['emscripten_fiber_swap'] \
|
||||
-sDYNCALLS=1 -pthread -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' \
|
||||
-sPTHREAD_POOL_SIZE_STRICT=0 -sEXPORTED_RUNTIME_METHODS=['ccall']
|
||||
|
|
@ -759,19 +868,19 @@ coroutine-pthread-nested: $(S)/coroutine-pthread/nested_repro.html
|
|||
.PHONY: coroutine-pthread-nested
|
||||
|
||||
# Nested invoke_/dynCall reproduction WITH exceptions (invoke_* boundaries, asyncify-unwindable)
|
||||
LDFLAGS_COROUTINE_INVOKE = $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
LDFLAGS_COROUTINE_INVOKE = $(EH_FLAGS) $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 \
|
||||
-sASYNCIFY_IMPORTS=['invoke_vi','invoke_v','invoke_ii','invoke_iii','emscripten_fiber_swap'] \
|
||||
-sDYNCALLS=1 -fexceptions -pthread -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' \
|
||||
-sDYNCALLS=1 -pthread -sPTHREAD_POOL_SIZE='navigator.hardwareConcurrency' \
|
||||
-sPTHREAD_POOL_SIZE_STRICT=0 -sEXPORTED_RUNTIME_METHODS=['ccall']
|
||||
|
||||
$(S)/coroutine-pthread/nested_repro_ex.o: $(S)/coroutine-pthread/nested_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
|
||||
@mkdir -p $(S)/coroutine-pthread
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -fexceptions -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
|
||||
$(S)/coroutine-pthread/libcontext_ex.o: $(KICAD_ROOT)/thirdparty/libcontext/libcontext.cpp $(KICAD_ROOT)/thirdparty/libcontext/libcontext.h
|
||||
@mkdir -p $(S)/coroutine-pthread
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -fexceptions -I$(KICAD_ROOT)/thirdparty/libcontext $< -o $@
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext $< -o $@
|
||||
|
||||
$(S)/coroutine-pthread/nested_repro_ex.html: $(S)/coroutine-pthread/nested_repro_ex.o $(S)/coroutine-pthread/libcontext_ex.o
|
||||
$(CXX) $(filter %.o %.a,$^) $(LDFLAGS_COROUTINE_INVOKE) -o $@
|
||||
|
|
@ -794,7 +903,7 @@ LDFLAGS_COROUTINE_EMBIND = $(LDFLAGS_COROUTINE_INVOKE) --bind
|
|||
|
||||
$(S)/coroutine-pthread/embind_repro.o: $(S)/coroutine-pthread/embind_repro.cpp $(S)/coroutine/kicad_coroutine_harness.h
|
||||
@mkdir -p $(S)/coroutine-pthread
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -fexceptions -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
$(CXX) -c $(CXXFLAGS) -pthread -I$(KICAD_ROOT)/thirdparty/libcontext -I$(S)/coroutine $< -o $@
|
||||
|
||||
$(S)/coroutine-pthread/embind_repro.html: $(S)/coroutine-pthread/embind_repro.o $(S)/coroutine-pthread/libcontext_ex.o
|
||||
$(CXX) $(filter %.o %.a,$^) $(LDFLAGS_COROUTINE_EMBIND) -o $@
|
||||
|
|
@ -816,7 +925,7 @@ coroutine-pthread-mainloop: $(S)/coroutine-pthread/mainloop_repro.html
|
|||
.PHONY: coroutine-pthread-mainloop
|
||||
|
||||
# WebGL2 + coroutine reproduction (no wx, no pthreads, default shell with #canvas)
|
||||
LDFLAGS_COROUTINE_GL = $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
LDFLAGS_COROUTINE_GL = $(EH_FLAGS) $(DEBUG_LDFLAGS) -sALLOW_MEMORY_GROWTH -sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-sASYNCIFY=1 -sASYNCIFY_STACK_SIZE=65536 -sASYNCIFY_IMPORTS=['emscripten_fiber_swap'] \
|
||||
-sDYNCALLS=1 -sMAX_WEBGL_VERSION=2 -sMIN_WEBGL_VERSION=2 -sEXPORTED_RUNTIME_METHODS=['ccall']
|
||||
|
||||
|
|
|
|||
296
tests/apps/standalone/async-preload/async_preload_test.cpp
Normal file
296
tests/apps/standalone/async-preload/async_preload_test.cpp
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* async_preload_test.cpp
|
||||
*
|
||||
* Standalone repro of KiCad-10's library-preload SHAPE (docs/features/wasm-exceptions/10 §7) —
|
||||
* NOT using real KiCad/pcbnew. Proves native wasm-EH makes the shape safe.
|
||||
*
|
||||
* The shape (mirrors kicad/eeschema/sch_io/pcbjam_lib/sch_io_pcbjam_lib.cpp):
|
||||
* - a std::async(std::launch::async, …) background worker (NOT the thread pool) that, per
|
||||
* "library", PROXIES a fetch to the main thread (emscripten_proxy_sync_with_ctx) and then
|
||||
* PARSES the bytes on the worker — the parse THROWS an IO_ERROR-like exception (the "mode-c"
|
||||
* trigger: a throw caught ON a worker drives Asyncify under -fexceptions → crash; native
|
||||
* wasm-EH decouples it → safe).
|
||||
* - a LAZY join: main keeps the std::future alive and never blocks in normal operation, so it
|
||||
* stays in its event loop to service the worker's proxied fetches.
|
||||
* - a g_proxyMutex serializes worker→main proxy round-trips (the real "table index out of
|
||||
* bounds" reentrancy guard when main is asyncify-suspended in a modal).
|
||||
*
|
||||
* (Simplification vs real KiCad: the proxied fetch finishes synchronously in the handler — the real
|
||||
* one kicks an async JS promise + finishes from its callback. We keep the round-trip + the
|
||||
* serialization, which is what the reentrancy hazard and the mode-c throw need; the throw is on the
|
||||
* worker's parse, independent of the fetch being sync/async.)
|
||||
*
|
||||
* URL ?m=0 simple | 1 throw (parse throws → caught on worker, no mode-c crash) |
|
||||
* 2 shutdown (main blocking-joins the future mid-load) | 3 modal (a modal opens while
|
||||
* workers proxy → g_proxyMutex must prevent a reentrancy crash)
|
||||
*
|
||||
* Console contract:
|
||||
* [PRELOAD] START m=.. [PRELOAD] EH=native|js
|
||||
* [PRELOAD] SUCCESS m=.. caught=.. loaded=..
|
||||
*/
|
||||
|
||||
#include "wx/wx.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/proxying.h>
|
||||
#include <emscripten/threading.h>
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
static void plog( const char* fmt, ... )
|
||||
{
|
||||
char buf[512];
|
||||
va_list ap;
|
||||
va_start( ap, fmt );
|
||||
vsnprintf( buf, sizeof( buf ), fmt, ap );
|
||||
va_end( ap );
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM( { console.log( UTF8ToString( $0 ) ); }, buf );
|
||||
#else
|
||||
printf( "%s\n", buf );
|
||||
#endif
|
||||
}
|
||||
|
||||
static int readMode()
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return EM_ASM_INT( {
|
||||
var raw = location.search ? location.search.slice( 1 ) : location.hash.slice( 1 );
|
||||
var v = parseInt( new URLSearchParams( raw ).get( 'm' ), 10 );
|
||||
return isNaN( v ) ? 0 : v;
|
||||
} );
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void mainSleep( int ms )
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_sleep( ms ); // Asyncify yield → main's event loop runs (services the proxy queue)
|
||||
#else
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( ms ) );
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// the proxied "fetch": worker → main round-trip (mirrors the PCBJAM bridge)
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::mutex g_proxyMutex;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void preload_finish( em_proxying_ctx* ctx )
|
||||
{
|
||||
emscripten_proxy_finish( ctx );
|
||||
}
|
||||
|
||||
static void proxy_fetch_on_main( em_proxying_ctx* ctx, void* )
|
||||
{
|
||||
// Runs on the MAIN thread via the system proxying queue. Synchronous finish (see header note).
|
||||
preload_finish( ctx );
|
||||
}
|
||||
#endif
|
||||
|
||||
static std::string proxyFetchToMain( const std::string& lib )
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
if( emscripten_is_main_runtime_thread() )
|
||||
return "(mock " + lib + ")"; // main path (not exercised here; the preload runs on a worker)
|
||||
|
||||
std::lock_guard<std::mutex> serialize( g_proxyMutex );
|
||||
em_proxying_queue* q = emscripten_proxy_get_system_queue();
|
||||
if( !emscripten_proxy_sync_with_ctx( q, emscripten_main_runtime_thread_id(),
|
||||
proxy_fetch_on_main, nullptr ) )
|
||||
return "";
|
||||
#endif
|
||||
return "(mock " + lib + ")";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
class IO_ERROR : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
explicit IO_ERROR( const std::string& m ) : std::runtime_error( m ) {}
|
||||
};
|
||||
|
||||
static std::size_t parseLib( const std::string& data, bool shouldThrow )
|
||||
{
|
||||
if( shouldThrow )
|
||||
throw IO_ERROR( "simulated S-expr parse failure" ); // the mode-c trigger
|
||||
return data.find( "mock" ) != std::string::npos ? 1u : 0u;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::atomic<bool> g_caught{ false };
|
||||
static std::atomic<int> g_loaded{ 0 };
|
||||
static std::atomic<bool> g_done{ false };
|
||||
static std::atomic<bool> g_abort{ false };
|
||||
static std::future<void> g_future; // kept alive => LAZY join (dtor blocks only at teardown)
|
||||
|
||||
static void preloadRun( bool throwOnParse )
|
||||
{
|
||||
try
|
||||
{
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 20 ) ); // worker-side watchdog
|
||||
for( const char* lib : { "lib_a", "lib_b", "lib_c" } )
|
||||
{
|
||||
if( g_abort.load() )
|
||||
break; // the CancelPreload mitigation: bail before the next proxy
|
||||
std::string data = proxyFetchToMain( lib ); // proxy the fetch to main
|
||||
g_loaded.fetch_add( (int) parseLib( data, throwOnParse ) ); // parse ON the worker (throws)
|
||||
}
|
||||
}
|
||||
catch( const std::exception& e )
|
||||
{
|
||||
g_caught.store( true );
|
||||
plog( "[PRELOAD] worker caught: %s", e.what() );
|
||||
}
|
||||
g_done.store( true );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// minimal auto-closing modal (timer armed before ShowModal; closes itself)
|
||||
// ---------------------------------------------------------------------------
|
||||
static constexpr int ID_MODAL_CLOSE = wxID_HIGHEST + 700;
|
||||
|
||||
class AutoCloseDialog : public wxDialog
|
||||
{
|
||||
public:
|
||||
AutoCloseDialog( wxWindow* parent, int delayMs ) :
|
||||
wxDialog( parent, wxID_ANY, "preload-modal", wxDefaultPosition, wxSize( 280, 120 ) ),
|
||||
m_timer( this, ID_MODAL_CLOSE )
|
||||
{
|
||||
Bind( wxEVT_SHOW, &AutoCloseDialog::OnShow, this );
|
||||
Bind( wxEVT_TIMER, [this] ( wxTimerEvent& ) { plog( "[PRELOAD] modal: close-timer fired" ); EndModal( wxID_OK ); }, ID_MODAL_CLOSE );
|
||||
m_delayMs = delayMs;
|
||||
}
|
||||
|
||||
private:
|
||||
void OnShow( wxShowEvent& e )
|
||||
{
|
||||
if( e.IsShown() )
|
||||
{
|
||||
plog( "[PRELOAD] modal: shown, arming %dms auto-close", m_delayMs );
|
||||
m_timer.StartOnce( m_delayMs );
|
||||
}
|
||||
e.Skip();
|
||||
}
|
||||
wxTimer m_timer;
|
||||
int m_delayMs = 300;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
static constexpr int ID_SCENARIO_TIMER = wxID_HIGHEST + 701;
|
||||
|
||||
class PreloadFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
PreloadFrame() : wxFrame( nullptr, wxID_ANY, "Async Preload Test",
|
||||
wxDefaultPosition, wxSize( 420, 140 ) ),
|
||||
m_scenarioTimer( this, ID_SCENARIO_TIMER )
|
||||
{
|
||||
wxPanel* p = new wxPanel( this );
|
||||
wxBoxSizer* s = new wxBoxSizer( wxVERTICAL );
|
||||
s->Add( new wxStaticText( p, wxID_ANY, "std::async library-preload repro — see console." ),
|
||||
0, wxALL, 16 );
|
||||
p->SetSizer( s );
|
||||
Bind( wxEVT_TIMER, &PreloadFrame::OnScenario, this, ID_SCENARIO_TIMER );
|
||||
}
|
||||
|
||||
void armModalScenario() { m_scenarioTimer.StartOnce( 50 ); }
|
||||
|
||||
private:
|
||||
// Runs INSIDE the main event loop (the modal pump needs that — ShowModal straight from OnInit,
|
||||
// before the loop starts, hangs). Several workers each do a bounded series of worker->main proxy
|
||||
// round-trips with a small gap so the modal pump can dispatch its auto-close timer; g_proxyMutex
|
||||
// serializes them, preventing concurrent C reentry into the asyncify-suspended (modal) main.
|
||||
void OnScenario( wxTimerEvent& )
|
||||
{
|
||||
std::atomic<int> rounds{ 0 };
|
||||
std::vector<std::thread> workers;
|
||||
for( int i = 0; i < 3; ++i )
|
||||
workers.emplace_back( [&rounds]
|
||||
{
|
||||
for( int k = 0; k < 12; ++k )
|
||||
{
|
||||
proxyFetchToMain( "spam" );
|
||||
rounds.fetch_add( 1 );
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
|
||||
}
|
||||
} );
|
||||
|
||||
plog( "[PRELOAD] modal: workers spawned, showing modal" );
|
||||
AutoCloseDialog dlg( this, 300 );
|
||||
dlg.ShowModal();
|
||||
plog( "[PRELOAD] modal: ShowModal returned (rounds=%d)", rounds.load() );
|
||||
|
||||
for( auto& t : workers )
|
||||
t.join();
|
||||
plog( "[PRELOAD] SUCCESS m=3 caught=0 loaded=0 proxyRounds=%d", rounds.load() );
|
||||
}
|
||||
|
||||
wxTimer m_scenarioTimer;
|
||||
};
|
||||
|
||||
class PreloadApp : public wxApp
|
||||
{
|
||||
public:
|
||||
bool OnInit() override
|
||||
{
|
||||
const int mode = readMode();
|
||||
plog( "[PRELOAD] START m=%d", mode );
|
||||
#ifdef __WASM_EXCEPTIONS__
|
||||
plog( "[PRELOAD] EH=native" );
|
||||
#else
|
||||
plog( "[PRELOAD] EH=js" );
|
||||
#endif
|
||||
g_caught.store( false );
|
||||
g_loaded.store( 0 );
|
||||
g_done.store( false );
|
||||
g_abort.store( false );
|
||||
|
||||
PreloadFrame* frame = new PreloadFrame();
|
||||
frame->Show();
|
||||
|
||||
if( mode == 3 )
|
||||
{
|
||||
frame->armModalScenario(); // runs in the main loop; logs its own [PRELOAD] SUCCESS m=3
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool throwOnParse = ( mode == 1 );
|
||||
g_future = std::async( std::launch::async, [throwOnParse] { preloadRun( throwOnParse ); } );
|
||||
|
||||
if( mode == 2 ) // shutdown: blocking-join the future mid-load (synchronous proxies complete
|
||||
// during the futex busy-wait's queue processing)
|
||||
{
|
||||
mainSleep( 30 );
|
||||
g_abort.store( true );
|
||||
g_future.wait();
|
||||
plog( "[PRELOAD] shutdown joined" );
|
||||
}
|
||||
else // simple / throw: LAZY join — poll via emscripten_sleep, main stays live
|
||||
{
|
||||
for( int i = 0; i < 200 && !g_done.load(); ++i )
|
||||
mainSleep( 20 );
|
||||
}
|
||||
|
||||
plog( "[PRELOAD] SUCCESS m=%d caught=%d loaded=%d",
|
||||
mode, g_caught.load() ? 1 : 0, g_loaded.load() );
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP( PreloadApp );
|
||||
|
|
@ -163,17 +163,29 @@ EM_JS( void, races_mark_done, ( const char* aName ), {
|
|||
Module.__racesDone[UTF8ToString( aName )] = true;
|
||||
} );
|
||||
|
||||
// Quiescence invariant sampled from C++ between scenarios. NOTE: this runs on a
|
||||
// stack that may itself have been resumed via Fibers.trampoline(), in which case
|
||||
// Fibers.trampolineRunning is legitimately true — so the guard is deliberately
|
||||
// NOT part of this check (a genuinely stuck guard wedges the next fiber swap and
|
||||
// is caught by the scenario watchdogs instead).
|
||||
// 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.
|
||||
EM_JS( int, races_quiescent, (), {
|
||||
try {
|
||||
var stOk = ( typeof Asyncify === 'undefined' ) || Asyncify.state === 0;
|
||||
var cdOk = ( typeof Asyncify === 'undefined' ) || !Asyncify.currData;
|
||||
var nfOk = ( typeof Fibers === 'undefined' ) || !Fibers.nextFiber;
|
||||
return ( stOk && cdOk && nfOk ) ? 1 : 0;
|
||||
return ( stOk && nfOk ) ? 1 : 0;
|
||||
} catch( e ) {
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
179
tests/apps/standalone/pthread-ondemand/pthread_ondemand_test.cpp
Normal file
179
tests/apps/standalone/pthread-ondemand/pthread_ondemand_test.cpp
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* pthread_ondemand_test.cpp
|
||||
*
|
||||
* Phase 2: can a NON-warm (on-demand) Worker be spawned WITHOUT modifying KiCad?
|
||||
*
|
||||
* Faithful shape of the real raytracer's deadlock: the REAL KiCad pool (compiled-in
|
||||
* kicad/common/thread_pool.cpp, via GetKiCadThreadPool()) consumes ALL the pre-warmed
|
||||
* Workers at construction. We then spawn raw std::thread fly-threads BEYOND that count,
|
||||
* which must be created ON DEMAND. Finalizing an on-demand Worker needs the main event
|
||||
* loop to run the new-Worker 'loaded'->'run' handshake.
|
||||
*
|
||||
* m=0 control : a main-thread busy-wait join that does NOT call nanosleep -> never
|
||||
* returns to the event loop -> the on-demand Workers never boot ->
|
||||
* DEADLOCK (workersRan=0; self-recovers after a 12s cap).
|
||||
* m=1 fix : join via std::this_thread::sleep_for -> nanosleep, which (via
|
||||
* wasm/shims/nanosleep_yield.c, a strong nanosleep override) Asyncify-yields on
|
||||
* the main thread -> the event loop services the handshake -> the
|
||||
* on-demand Workers boot -> multi-core (workersRan>1). No KiCad edit.
|
||||
*
|
||||
* The override only affects main-thread nanosleep; a worker keeps a real blocking sleep.
|
||||
*
|
||||
* Console contract (the spec asserts on these):
|
||||
* [ONDEMAND] START m=.. poolThreads=.. extra=.. hwc=..
|
||||
* [ONDEMAND] SUCCESS m=.. workersRan=.. totalMs=.. (workersRan=0 for m=0, the deadlock)
|
||||
* [ONDEMAND] DEADLOCK m=0: ...
|
||||
*/
|
||||
|
||||
#include "wx/wx.h"
|
||||
|
||||
#include <thread_pool.h> // kicad/include/thread_pool.h -> GetKiCadThreadPool()
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
using clk = std::chrono::steady_clock;
|
||||
|
||||
static void olog( const char* fmt, ... )
|
||||
{
|
||||
char buf[512];
|
||||
va_list ap;
|
||||
va_start( ap, fmt );
|
||||
vsnprintf( buf, sizeof( buf ), fmt, ap );
|
||||
va_end( ap );
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM( { console.log( UTF8ToString( $0 ) ); }, buf );
|
||||
#else
|
||||
printf( "%s\n", buf );
|
||||
#endif
|
||||
}
|
||||
|
||||
static int readMode()
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return EM_ASM_INT( {
|
||||
var raw = location.search ? location.search.slice( 1 ) : location.hash.slice( 1 );
|
||||
var v = parseInt( new URLSearchParams( raw ).get( 'm' ), 10 );
|
||||
return isNaN( v ) ? 1 : v;
|
||||
} );
|
||||
#else
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
static long ms( clk::time_point t0 )
|
||||
{
|
||||
return (long) std::chrono::duration_cast<std::chrono::milliseconds>( clk::now() - t0 ).count();
|
||||
}
|
||||
|
||||
// shared work: raw fly-threads cooperatively drain a global atomic block counter.
|
||||
static std::atomic<size_t> g_nextBlock{ 0 };
|
||||
static std::atomic<size_t> g_threadsFinished{ 0 };
|
||||
static std::atomic<int> g_workersRan{ 0 };
|
||||
static std::atomic<double> g_sink{ 0.0 };
|
||||
static int g_numBlocks = 48;
|
||||
static long g_iters = 3000000;
|
||||
|
||||
static double computeBlock( int b, long iters )
|
||||
{
|
||||
double acc = 0.0;
|
||||
for( long i = 0; i < iters; ++i )
|
||||
acc += std::sin( (double) ( b * 131 + i ) * 1e-6 ) * std::cos( (double) i * 1e-7 );
|
||||
return acc;
|
||||
}
|
||||
|
||||
static void workerBody()
|
||||
{
|
||||
bool counted = false;
|
||||
for( size_t b = g_nextBlock.fetch_add( 1 ); b < (size_t) g_numBlocks; b = g_nextBlock.fetch_add( 1 ) )
|
||||
{
|
||||
if( !counted )
|
||||
{
|
||||
g_workersRan.fetch_add( 1 );
|
||||
counted = true;
|
||||
}
|
||||
g_sink.store( g_sink.load( std::memory_order_relaxed ) + computeBlock( (int) b, g_iters ),
|
||||
std::memory_order_relaxed );
|
||||
}
|
||||
g_threadsFinished.fetch_add( 1 );
|
||||
}
|
||||
|
||||
class OndemandFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
OndemandFrame() : wxFrame( nullptr, wxID_ANY, "On-demand Worker Test",
|
||||
wxDefaultPosition, wxSize( 420, 140 ) )
|
||||
{
|
||||
wxPanel* p = new wxPanel( this );
|
||||
wxBoxSizer* s = new wxBoxSizer( wxVERTICAL );
|
||||
s->Add( new wxStaticText( p, wxID_ANY, "On-demand non-warm Worker test — see console." ),
|
||||
0, wxALL, 16 );
|
||||
p->SetSizer( s );
|
||||
}
|
||||
};
|
||||
|
||||
class OndemandApp : public wxApp
|
||||
{
|
||||
public:
|
||||
bool OnInit() override
|
||||
{
|
||||
const int hwc = std::max( 1u, std::thread::hardware_concurrency() );
|
||||
const int mode = readMode();
|
||||
const int extra = 4; // raw fly-threads BEYOND the pool -> must be created on demand
|
||||
|
||||
// Consume ALL pre-warmed Workers with the REAL KiCad pool — exactly like KiCad does.
|
||||
// (PTHREAD_POOL_SIZE = hardware_concurrency, the pool takes them all at construction,
|
||||
// so the `extra` raw threads below force on-demand Worker creation.)
|
||||
thread_pool& tp = GetKiCadThreadPool();
|
||||
olog( "[ONDEMAND] START m=%d poolThreads=%u extra=%d hwc=%d",
|
||||
mode, (unsigned) tp.get_thread_count(), extra, hwc );
|
||||
|
||||
g_nextBlock.store( 0 );
|
||||
g_threadsFinished.store( 0 );
|
||||
g_workersRan.store( 0 );
|
||||
auto t0 = clk::now();
|
||||
|
||||
for( int i = 0; i < extra; ++i )
|
||||
std::thread( workerBody ).detach();
|
||||
|
||||
if( mode == 0 )
|
||||
{
|
||||
// control: busy-wait WITHOUT nanosleep -> never yields -> on-demand never boots.
|
||||
auto dl = clk::now();
|
||||
while( g_threadsFinished.load() < (size_t) extra )
|
||||
{
|
||||
for( volatile int s = 0; s < 200000; ++s )
|
||||
; // pure spin: no sleep_for / nanosleep, so it does not hit the override
|
||||
if( std::chrono::duration_cast<std::chrono::seconds>( clk::now() - dl ).count() >= 12 )
|
||||
{
|
||||
olog( "[ONDEMAND] DEADLOCK m=0: on-demand workers never booted within 12s "
|
||||
"(busy-wait starved the event loop)" );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// fix: sleep_for -> nanosleep -> (overridden) Asyncify yield -> main pumps the
|
||||
// new-Worker handshake -> the on-demand Workers boot and run.
|
||||
while( g_threadsFinished.load() < (size_t) extra )
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
|
||||
}
|
||||
|
||||
olog( "[ONDEMAND] SUCCESS m=%d workersRan=%d totalMs=%ld sink=%.3f",
|
||||
mode, g_workersRan.load(), ms( t0 ), g_sink.load() );
|
||||
( new OndemandFrame() )->Show();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP( OndemandApp );
|
||||
269
tests/apps/standalone/raytrace-modal/raytrace_modal_test.cpp
Normal file
269
tests/apps/standalone/raytrace-modal/raytrace_modal_test.cpp
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* raytrace_modal_test.cpp
|
||||
*
|
||||
* A raytracer-style worker-join run inside a wx modal pump, in both join styles.
|
||||
*
|
||||
* A pass is dispatched from a wxTimer that fires while a ShowModal() dialog is open. The modal pump
|
||||
* runs ProcessEvents via ccall(async:true), so the timer handler runs in a fresh managed Asyncify
|
||||
* context at state == Normal (the handler probes and logs the state). Both join styles complete
|
||||
* multi-core there:
|
||||
* ?m=0 busywait : the join is a sleep_for() busy-wait; the pre-warmed pool's workers complete it.
|
||||
* ?m=1 yield : the join yields via emscripten_sleep; legal at state == Normal, so it suspends
|
||||
* and resumes normally.
|
||||
*
|
||||
* The persistent pool is warmed in OnInit (on the free top-level slot) so the workers are alive
|
||||
* before the modal opens, which lets the in-modal busy-wait complete without on-demand Worker spawn.
|
||||
*/
|
||||
|
||||
#include "wx/wx.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
using clk = std::chrono::steady_clock;
|
||||
|
||||
static void rtlog( const char* fmt, ... )
|
||||
{
|
||||
char buf[512];
|
||||
va_list ap;
|
||||
va_start( ap, fmt );
|
||||
vsnprintf( buf, sizeof( buf ), fmt, ap );
|
||||
va_end( ap );
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM( { console.log( UTF8ToString( $0 ) ); }, buf );
|
||||
#else
|
||||
printf( "%s\n", buf );
|
||||
#endif
|
||||
}
|
||||
|
||||
static int readMode()
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return EM_ASM_INT( {
|
||||
var raw = location.search ? location.search.slice( 1 ) : location.hash.slice( 1 );
|
||||
var v = parseInt( new URLSearchParams( raw ).get( 'm' ), 10 );
|
||||
return isNaN( v ) ? 0 : v;
|
||||
} );
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static long ms( clk::time_point t0 )
|
||||
{
|
||||
return (long) std::chrono::duration_cast<std::chrono::milliseconds>( clk::now() - t0 ).count();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// shared raytracer-style work (atomic work-stealing; mirrors render_3d_raytrace_base.cpp)
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::atomic<size_t> g_nextBlock{ 0 };
|
||||
static std::atomic<int> g_workersRan{ 0 };
|
||||
static std::atomic<double> g_sink{ 0.0 };
|
||||
static int g_numBlocks = 48;
|
||||
static long g_iters = 600000;
|
||||
|
||||
static double computeBlock( int b, long iters )
|
||||
{
|
||||
double acc = 0.0;
|
||||
for( long i = 0; i < iters; ++i )
|
||||
acc += std::sin( (double) ( b * 131 + i ) * 1e-6 ) * std::cos( (double) i * 1e-7 );
|
||||
return acc;
|
||||
}
|
||||
|
||||
static void workerBody()
|
||||
{
|
||||
bool counted = false;
|
||||
for( size_t b = g_nextBlock.fetch_add( 1 ); b < (size_t) g_numBlocks; b = g_nextBlock.fetch_add( 1 ) )
|
||||
{
|
||||
if( !counted )
|
||||
{
|
||||
g_workersRan.fetch_add( 1 );
|
||||
counted = true;
|
||||
}
|
||||
g_sink.store( g_sink.load( std::memory_order_relaxed ) + computeBlock( (int) b, g_iters ),
|
||||
std::memory_order_relaxed );
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// persistent pre-warmed pool (warmed in OnInit, free slot) — so the IN-MODAL busy-wait completes
|
||||
// rather than deadlocking on on-demand Worker creation.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct PersistentPool
|
||||
{
|
||||
int n = 0;
|
||||
std::vector<std::thread> ts;
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
unsigned long generation = 0;
|
||||
bool stop = false;
|
||||
std::atomic<size_t> finished{ 0 };
|
||||
std::atomic<size_t> started{ 0 };
|
||||
|
||||
void start( int n_ )
|
||||
{
|
||||
n = n_;
|
||||
for( int i = 0; i < n; ++i )
|
||||
ts.emplace_back( [this] { loop(); } );
|
||||
}
|
||||
|
||||
bool ready() const { return started.load() == (size_t) n; }
|
||||
|
||||
void loop()
|
||||
{
|
||||
started.fetch_add( 1 );
|
||||
unsigned long seen = 0;
|
||||
for( ;; )
|
||||
{
|
||||
std::unique_lock<std::mutex> lk( mtx );
|
||||
cv.wait( lk, [&] { return stop || generation != seen; } );
|
||||
if( stop )
|
||||
return;
|
||||
seen = generation;
|
||||
lk.unlock();
|
||||
workerBody();
|
||||
finished.fetch_add( 1 );
|
||||
}
|
||||
}
|
||||
|
||||
// Called on the MAIN thread, from the modal pump's ProcessEvents (state == Normal).
|
||||
void runPass( bool yieldJoin )
|
||||
{
|
||||
finished.store( 0 );
|
||||
g_nextBlock.store( 0 );
|
||||
g_workersRan.store( 0 );
|
||||
{
|
||||
std::lock_guard<std::mutex> lk( mtx );
|
||||
++generation;
|
||||
}
|
||||
cv.notify_all();
|
||||
while( finished.load() < (size_t) n )
|
||||
{
|
||||
if( yieldJoin )
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_sleep( 10 ); // Asyncify yield (legal here — state == Normal)
|
||||
#else
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
|
||||
#endif
|
||||
else
|
||||
std::this_thread::sleep_for( std::chrono::microseconds( 200 ) ); // busy-wait join
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static PersistentPool g_pool;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// modal scaffold (mirrors coroutine-nested/nested_test.cpp's AutoClosingDialog)
|
||||
// ---------------------------------------------------------------------------
|
||||
static constexpr int ID_MODAL_CLOSE = wxID_HIGHEST + 800;
|
||||
static constexpr int ID_SCENARIO = wxID_HIGHEST + 801;
|
||||
static constexpr int ID_MODAL_WORK = wxID_HIGHEST + 802;
|
||||
|
||||
class AutoClosingDialog : public wxDialog
|
||||
{
|
||||
public:
|
||||
AutoClosingDialog( wxWindow* parent ) :
|
||||
wxDialog( parent, wxID_ANY, "rt-modal", wxDefaultPosition, wxSize( 280, 120 ) ) {}
|
||||
void EndModalExternal( int code ) { EndModal( code ); }
|
||||
};
|
||||
|
||||
class RtModalFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
RtModalFrame( int mode ) :
|
||||
wxFrame( nullptr, wxID_ANY, "Raytrace Modal Test", wxDefaultPosition, wxSize( 420, 140 ) ),
|
||||
m_mode( mode ),
|
||||
m_scenarioTimer( this, ID_SCENARIO ),
|
||||
m_modalWorkTimer( this, ID_MODAL_WORK )
|
||||
{
|
||||
wxPanel* p = new wxPanel( this );
|
||||
wxBoxSizer* s = new wxBoxSizer( wxVERTICAL );
|
||||
s->Add( new wxStaticText( p, wxID_ANY, "Raytrace-in-modal repro — see console." ), 0, wxALL, 16 );
|
||||
p->SetSizer( s );
|
||||
Bind( wxEVT_TIMER, &RtModalFrame::OnScenario, this, ID_SCENARIO );
|
||||
Bind( wxEVT_TIMER, &RtModalFrame::OnModalWork, this, ID_MODAL_WORK );
|
||||
}
|
||||
|
||||
void armScenario() { m_scenarioTimer.StartOnce( 50 ); }
|
||||
|
||||
private:
|
||||
// In the main loop: arm the modal-work timer BEFORE ShowModal so it fires from within the modal
|
||||
// pump, then show the modal.
|
||||
void OnScenario( wxTimerEvent& )
|
||||
{
|
||||
AutoClosingDialog* dlg = new AutoClosingDialog( this );
|
||||
m_activeDialog = dlg;
|
||||
m_modalWorkTimer.StartOnce( 30 );
|
||||
rtlog( "[RTPOOL] showing modal (mode=%d) — work runs from the modal pump's ProcessEvents", m_mode );
|
||||
dlg->ShowModal(); // returns when OnModalWork's pass finishes and closes it
|
||||
m_activeDialog = nullptr;
|
||||
rtlog( "[RTPOOL] SUCCESS mode=%d workersRan=%d totalMs=%ld",
|
||||
m_mode, g_workersRan.load(), ms( m_t0 ) );
|
||||
dlg->Destroy();
|
||||
}
|
||||
|
||||
// Fires from the modal pump's ProcessEvents — a fresh managed entry at Asyncify state == Normal.
|
||||
void OnModalWork( wxTimerEvent& )
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
int st = EM_ASM_INT( {
|
||||
return ( typeof Asyncify !== 'undefined' && Asyncify.state !== undefined ) ? Asyncify.state : -1;
|
||||
} );
|
||||
rtlog( "[RTPOOL] modal-work: Asyncify.state=%d (0=Normal,1=Unwinding,2=Rewinding) mode=%d", st, m_mode );
|
||||
#endif
|
||||
rtlog( "[RTPOOL] modal-work: running pass from the modal pump (mode=%d)", m_mode );
|
||||
m_t0 = clk::now();
|
||||
g_pool.runPass( m_mode == 1 ); // m=1 yields via emscripten_sleep; m=0 busy-waits
|
||||
rtlog( "[RTPOOL] PASS done workersRan=%d", g_workersRan.load() );
|
||||
if( m_activeDialog )
|
||||
m_activeDialog->EndModalExternal( wxID_OK );
|
||||
}
|
||||
|
||||
int m_mode;
|
||||
AutoClosingDialog* m_activeDialog = nullptr;
|
||||
clk::time_point m_t0;
|
||||
wxTimer m_scenarioTimer;
|
||||
wxTimer m_modalWorkTimer;
|
||||
};
|
||||
|
||||
class RtModalApp : public wxApp
|
||||
{
|
||||
public:
|
||||
bool OnInit() override
|
||||
{
|
||||
const int mode = readMode();
|
||||
const int hwc = std::max( 1u, std::thread::hardware_concurrency() );
|
||||
rtlog( "[RTPOOL] START mode=%d hwc=%d", mode, hwc );
|
||||
|
||||
// Warm the persistent pool on the FREE top-level slot (emscripten_sleep is legal here), so
|
||||
// the in-modal join runs against already-alive workers.
|
||||
g_pool.start( hwc );
|
||||
while( !g_pool.ready() )
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_sleep( 5 );
|
||||
#else
|
||||
std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) );
|
||||
#endif
|
||||
rtlog( "[RTPOOL] pool warmed (%d workers)", hwc );
|
||||
|
||||
RtModalFrame* f = new RtModalFrame( mode );
|
||||
f->Show();
|
||||
f->armScenario();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP( RtModalApp );
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
// Minimal stub of kicad/include/advanced_config.h. thread_pool.cpp only reads
|
||||
// ADVANCED_CFG::GetCfg().m_MaximumThreads; 0 => the pool uses hardware_concurrency().
|
||||
struct ADVANCED_CFG
|
||||
{
|
||||
int m_MaximumThreads = 0;
|
||||
static const ADVANCED_CFG& GetCfg();
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
// Minimal stub of kicad/include/import_export.h — thread_pool.h only uses APIEXPORT.
|
||||
#define APIEXPORT
|
||||
#define APIIMPORT
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// Definitions for the minimal pgm_base / advanced_config stubs (see ./pgm_base.h,
|
||||
// ./advanced_config.h). They let kicad/common/thread_pool.cpp compile and link
|
||||
// standalone while still constructing the REAL BS::priority_thread_pool.
|
||||
#include <advanced_config.h>
|
||||
#include <pgm_base.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
// No PGM_BASE in this standalone test -> GetKiCadThreadPool() takes the
|
||||
// `new thread_pool( num_threads )` branch (num_threads=0 -> hardware_concurrency()).
|
||||
PGM_BASE* PgmOrNull()
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Referenced by thread_pool.cpp inside `if( PgmOrNull() )`, which is never taken here;
|
||||
// must link but is never executed.
|
||||
thread_pool& PGM_BASE::GetThreadPool()
|
||||
{
|
||||
std::abort();
|
||||
}
|
||||
|
||||
static const ADVANCED_CFG g_advancedCfgStub;
|
||||
|
||||
const ADVANCED_CFG& ADVANCED_CFG::GetCfg()
|
||||
{
|
||||
return g_advancedCfgStub;
|
||||
}
|
||||
12
tests/apps/standalone/threadpool-real/stubs/pgm_base.h
Normal file
12
tests/apps/standalone/threadpool-real/stubs/pgm_base.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
#include <thread_pool.h> // for the `thread_pool` type returned by GetThreadPool()
|
||||
// Minimal stub of kicad/include/pgm_base.h. thread_pool.cpp only needs PgmOrNull()
|
||||
// (stubbed to nullptr so GetKiCadThreadPool() constructs its own real pool) and
|
||||
// PGM_BASE::GetThreadPool() (referenced in the now-dead branch; never called).
|
||||
class PGM_BASE
|
||||
{
|
||||
public:
|
||||
thread_pool& GetThreadPool();
|
||||
};
|
||||
|
||||
PGM_BASE* PgmOrNull();
|
||||
280
tests/apps/standalone/threadpool-real/threadpool_real_test.cpp
Normal file
280
tests/apps/standalone/threadpool-real/threadpool_real_test.cpp
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/**
|
||||
* threadpool_real_test.cpp
|
||||
*
|
||||
* The BS-pool-API native-EH test (docs/features/wasm-exceptions/10 §6 #1).
|
||||
*
|
||||
* Unlike threadpool_test / raytrace_threads_test (which hand-roll raw std::thread),
|
||||
* this app drives KiCad's REAL pool: it compiles kicad/common/thread_pool.cpp and
|
||||
* calls the actual GetKiCadThreadPool() (a BS::priority_thread_pool). The pool's
|
||||
* detach_task __EMSCRIPTEN__ inline shim is opted OUT here via -DKICAD_WASM_REAL_THREADPOOL
|
||||
* so tasks actually run on the pool's persistent pthread workers.
|
||||
*
|
||||
* WHY this is the decisive test: the real pool is mode-a/b-safe by construction —
|
||||
* its workers are PERSISTENT (created at pool construction, consuming the pre-warmed
|
||||
* pool, so no on-demand spawn → no deadlock) and its main-side join is a futex
|
||||
* busy-wait, not emscripten_sleep (→ no Asyncify nesting). The ONLY native-EH risk is
|
||||
* mode-c: a task that THROWS on a worker (caught by submit_task's promise wrapper ON the
|
||||
* worker drives Asyncify under -fexceptions → "func is not a function"). Native wasm-EH
|
||||
* decouples exceptions from Asyncify and should make it safe. So:
|
||||
* - modes submit/loop/blocks/detach/fanout/lifecycle prove real multi-core (workersRan>1)
|
||||
* - mode throw is the mode-c / native-EH proof (red under JS-EH, green under native-EH)
|
||||
* Green here ⇒ we can drop the detach_task shim for every pool consumer.
|
||||
*
|
||||
* Pure-compute tasks only (no JS/DOM/async-IO on a worker) — exactly why the real
|
||||
* pool's tasks are safe to run off the main thread.
|
||||
*
|
||||
* URL: ?m=0 submit | 1 loop | 2 blocks | 3 detach | 4 fanout | 5 lifecycle | 6 throw
|
||||
* (read from location.search OR the #fragment — serve-handler cleanUrls drops ?query)
|
||||
*
|
||||
* Console contract (the Playwright spec asserts on these):
|
||||
* [POOL] START mode=.. threads=..
|
||||
* [POOL] SUCCESS mode=.. workersRan=.. totalMs=.. sink=..
|
||||
* [POOL] SUCCESS mode=6 threw=1 caught=.. workersRan=.. ... (the throw / mode-c proof)
|
||||
*/
|
||||
|
||||
#include "wx/wx.h"
|
||||
|
||||
#include <thread_pool.h> // kicad/include/thread_pool.h -> GetKiCadThreadPool() + BS::*
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
using clk = std::chrono::steady_clock;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
static void plog( const char* fmt, ... )
|
||||
{
|
||||
char buf[512];
|
||||
va_list ap;
|
||||
va_start( ap, fmt );
|
||||
vsnprintf( buf, sizeof( buf ), fmt, ap );
|
||||
va_end( ap );
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM( { console.log( UTF8ToString( $0 ) ); }, buf );
|
||||
#else
|
||||
printf( "%s\n", buf );
|
||||
#endif
|
||||
}
|
||||
|
||||
static int readMode()
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
return EM_ASM_INT( {
|
||||
var raw = location.search ? location.search.slice( 1 ) : location.hash.slice( 1 );
|
||||
var v = parseInt( new URLSearchParams( raw ).get( 'm' ), 10 );
|
||||
return isNaN( v ) ? 0 : v;
|
||||
} );
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static long ms( clk::time_point t0 )
|
||||
{
|
||||
return (long) std::chrono::duration_cast<std::chrono::milliseconds>( clk::now() - t0 ).count();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// shared work: pure CPU math; each task records WHICH pool worker ran it so we can
|
||||
// prove real parallelism (distinct BS::this_thread::get_index() values).
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::atomic<uint64_t> g_workerMask{ 0 };
|
||||
static std::atomic<double> g_sink{ 0.0 };
|
||||
|
||||
static void recordWorker()
|
||||
{
|
||||
std::optional<std::size_t> idx = BS::this_thread::get_index();
|
||||
if( idx )
|
||||
g_workerMask.fetch_or( 1ull << ( *idx & 63 ), std::memory_order_relaxed );
|
||||
}
|
||||
|
||||
static double computeBlock( int b, long iters )
|
||||
{
|
||||
double acc = 0.0;
|
||||
for( long i = 0; i < iters; ++i )
|
||||
acc += std::sin( (double) ( b * 131 + i ) * 1e-6 ) * std::cos( (double) i * 1e-7 );
|
||||
return acc;
|
||||
}
|
||||
|
||||
static int workersRan()
|
||||
{
|
||||
return __builtin_popcountll( g_workerMask.load() );
|
||||
}
|
||||
|
||||
static constexpr int N = 64;
|
||||
static constexpr long ITERS = 2000000;
|
||||
|
||||
static void addWork( int b, long iters )
|
||||
{
|
||||
recordWorker();
|
||||
g_sink.store( g_sink.load( std::memory_order_relaxed ) + computeBlock( b, iters ),
|
||||
std::memory_order_relaxed );
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
class PoolFrame : public wxFrame
|
||||
{
|
||||
public:
|
||||
PoolFrame() : wxFrame( nullptr, wxID_ANY, "Real Thread Pool Test",
|
||||
wxDefaultPosition, wxSize( 420, 140 ) )
|
||||
{
|
||||
wxPanel* p = new wxPanel( this );
|
||||
wxBoxSizer* s = new wxBoxSizer( wxVERTICAL );
|
||||
s->Add( new wxStaticText( p, wxID_ANY, "Real GetKiCadThreadPool() test — see console." ),
|
||||
0, wxALL, 16 );
|
||||
p->SetSizer( s );
|
||||
}
|
||||
};
|
||||
|
||||
class PoolApp : public wxApp
|
||||
{
|
||||
public:
|
||||
bool OnInit() override
|
||||
{
|
||||
const int mode = readMode();
|
||||
thread_pool& tp = GetKiCadThreadPool(); // the REAL BS::priority_thread_pool
|
||||
plog( "[POOL] START mode=%d threads=%u", mode, (unsigned) tp.get_thread_count() );
|
||||
// Report the exception model so the spec can gate the mode-c (throw-on-worker) test:
|
||||
// a worker throw is only safe under native wasm-EH (-fwasm-exceptions defines this).
|
||||
#ifdef __WASM_EXCEPTIONS__
|
||||
plog( "[POOL] EH=native" );
|
||||
#else
|
||||
plog( "[POOL] EH=js" );
|
||||
#endif
|
||||
|
||||
g_workerMask.store( 0 );
|
||||
g_sink.store( 0.0 );
|
||||
auto t0 = clk::now();
|
||||
|
||||
switch( mode )
|
||||
{
|
||||
case 0: // submit_task + vector<future> + 250ms poll loop (the DRC/connectivity idiom)
|
||||
{
|
||||
std::vector<std::future<void>> futs;
|
||||
for( int i = 0; i < N; ++i )
|
||||
futs.push_back( tp.submit_task( [i] { addWork( i, ITERS ); } ) );
|
||||
for( auto& f : futs )
|
||||
while( f.wait_for( std::chrono::milliseconds( 250 ) ) != std::future_status::ready )
|
||||
;
|
||||
break;
|
||||
}
|
||||
case 1: // submit_loop -> multi_future.wait()
|
||||
{
|
||||
BS::multi_future<void> mf = tp.submit_loop( 0, N, [] ( int i ) { addWork( i, ITERS ); } );
|
||||
mf.wait();
|
||||
break;
|
||||
}
|
||||
case 2: // submit_blocks (each block returns a value)
|
||||
{
|
||||
BS::multi_future<double> mf = tp.submit_blocks(
|
||||
0, N,
|
||||
[] ( int s, int e )
|
||||
{
|
||||
recordWorker();
|
||||
double a = 0.0;
|
||||
for( int i = s; i < e; ++i )
|
||||
a += computeBlock( i, ITERS );
|
||||
return a;
|
||||
} );
|
||||
double total = 0.0;
|
||||
for( auto& f : mf )
|
||||
if( f.valid() )
|
||||
total += f.get();
|
||||
g_sink.store( g_sink.load() + total );
|
||||
break;
|
||||
}
|
||||
case 3: // detach_task fire-and-forget + tp.wait()
|
||||
{
|
||||
for( int i = 0; i < N; ++i )
|
||||
tp.detach_task( [i] { addWork( i, ITERS ); } );
|
||||
tp.wait();
|
||||
break;
|
||||
}
|
||||
case 4: // manual multi_future fanned out by get_thread_count() (the renderTracing shape)
|
||||
{
|
||||
std::atomic<int> next{ 0 };
|
||||
BS::multi_future<void> mf;
|
||||
auto proc = [&next]
|
||||
{
|
||||
for( int b = next.fetch_add( 1 ); b < N; b = next.fetch_add( 1 ) )
|
||||
addWork( b, ITERS );
|
||||
};
|
||||
for( std::size_t i = 0; i < tp.get_thread_count(); ++i )
|
||||
mf.push_back( tp.submit_task( proc ) );
|
||||
mf.wait();
|
||||
break;
|
||||
}
|
||||
case 5: // lifecycle + features: queries / purge / reset + pause via a pause_thread_pool
|
||||
{
|
||||
plog( "[POOL] tasks queued=%zu running=%zu total=%zu threads=%zu",
|
||||
tp.get_tasks_queued(), tp.get_tasks_running(), tp.get_tasks_total(),
|
||||
(std::size_t) tp.get_thread_count() );
|
||||
tp.wait();
|
||||
tp.purge();
|
||||
|
||||
// pause/unpause are compiled OUT on the priority pool, so exercise them on a
|
||||
// small pause_thread_pool (the +2 PTHREAD_POOL_SIZE headroom pre-warms its workers).
|
||||
BS::pause_thread_pool pp( 2 );
|
||||
pp.pause();
|
||||
for( int i = 0; i < 8; ++i )
|
||||
pp.detach_task( [] {} );
|
||||
const bool paused = pp.is_paused();
|
||||
const std::size_t queued = pp.get_tasks_queued();
|
||||
pp.unpause();
|
||||
pp.wait();
|
||||
plog( "[POOL] pause: is_paused=%d queuedWhilePaused=%zu", paused ? 1 : 0, queued );
|
||||
|
||||
// and a real parallel batch so workersRan>1 holds for this mode too
|
||||
tp.submit_loop( 0, N, [] ( int i ) { addWork( i, ITERS / 4 ); } ).wait();
|
||||
break;
|
||||
}
|
||||
case 6: // throw ON a worker -> rethrow on main. THE mode-c / native-EH proof.
|
||||
{
|
||||
int caught = 0;
|
||||
auto f = tp.submit_task( [] () -> int
|
||||
{
|
||||
recordWorker();
|
||||
throw std::runtime_error( "boom-on-worker" );
|
||||
return 0;
|
||||
} );
|
||||
try
|
||||
{
|
||||
(void) f.get();
|
||||
}
|
||||
catch( const std::exception& e )
|
||||
{
|
||||
caught = 1;
|
||||
plog( "[POOL] caught on main: %s", e.what() );
|
||||
}
|
||||
// and prove workers still run a normal batch after the throw
|
||||
tp.submit_loop( 0, N, [] ( int i ) { addWork( i, ITERS / 4 ); } ).wait();
|
||||
plog( "[POOL] SUCCESS mode=6 threw=1 caught=%d workersRan=%d totalMs=%ld sink=%.3f",
|
||||
caught, workersRan(), ms( t0 ), g_sink.load() );
|
||||
( new PoolFrame() )->Show();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
plog( "[POOL] unknown mode=%d", mode );
|
||||
break;
|
||||
}
|
||||
|
||||
plog( "[POOL] SUCCESS mode=%d workersRan=%d totalMs=%ld sink=%.3f",
|
||||
mode, workersRan(), ms( t0 ), g_sink.load() );
|
||||
( new PoolFrame() )->Show();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
wxIMPLEMENT_APP( PoolApp );
|
||||
|
|
@ -177,8 +177,28 @@ test.describe('Asyncify races — green targets (full shims)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test.describe('Asyncify races — ablation pins (the disease stays reproducible)', () => {
|
||||
test('no trampoline heal: the park wedges the guard and the post-park swap hangs', async ({
|
||||
// These two were "ablation pins": they ablated a JS-shim fix (via SHIM_DISABLE_*,
|
||||
// see tests/apps/Makefile.wasm) and asserted the LEGACY-EH disease came back — a
|
||||
// stuck Fibers.trampoline guard (trampolineRunning=true) / a clobbered parked-sleep
|
||||
// buffer. Under native wasm-EH the top loop is a per-frame-yield while-loop with NO
|
||||
// park-throw (docs/features/wasm-exceptions; native-EH top-loop redesign), so the
|
||||
// disease's *trigger* is gone at the root: ablating the shim no longer reproduces
|
||||
// it. (The green "battery" above already runs both scenarios full-shim.)
|
||||
//
|
||||
// So they are flipped red->green: each now pins that the native-EH path stays clean
|
||||
// EVEN with the legacy shim ablated — i.e. the scenario completes with no disease
|
||||
// signature. That keeps them as the live signal for the open question below.
|
||||
//
|
||||
// TODO(research): are these shims still needed AT ALL under native-EH-only? The
|
||||
// fiber trampoline self-heal (§3c) and the nested-Asyncify handleSleep save/restore
|
||||
// (§3) in scripts/common/inject-dyncall-shims.sh were written for the legacy
|
||||
// park-throw model; ablating either no longer breaks the scenario it guarded. If a
|
||||
// full sweep of the real apps (modals / nested fibers / long sleeps / pthread pool)
|
||||
// confirms they're dead weight under native-EH, drop the shim injection AND these
|
||||
// pins. Until then they stay injected (belt-and-suspenders). Tracked in
|
||||
// tests/README.md "Open tasks".
|
||||
test.describe('Asyncify races — shim-redundancy pins (native-EH stays clean with the legacy shim ablated)', () => {
|
||||
test('trampoline-heal ablated: native-EH post-park swap still completes (no stuck guard)', async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
|
|
@ -187,39 +207,28 @@ test.describe('Asyncify races — ablation pins (the disease stays reproducible)
|
|||
);
|
||||
await tryLoadApp(page, 30000);
|
||||
|
||||
// The scenario's JS watchdog fires after 2.5s with a state dump.
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
testLogger.consoleLogs.find((l) =>
|
||||
l.includes('[ASYNCIFY_RACES] WATCHDOG post_park_fiber_swap')
|
||||
) ?? null,
|
||||
{ timeout: 30000, message: 'watchdog should fire in the ablated build' }
|
||||
)
|
||||
.poll(() => findSummary(testLogger.consoleLogs) ?? null, {
|
||||
timeout: 45000,
|
||||
message: 'post-park swap should complete even with the trampoline self-heal ablated',
|
||||
})
|
||||
.not.toBeNull();
|
||||
|
||||
const watchdog = testLogger.consoleLogs.find((l) =>
|
||||
l.includes('[ASYNCIFY_RACES] WATCHDOG post_park_fiber_swap')
|
||||
)!;
|
||||
|
||||
// The exact stuck-guard signature traced in docs/features/async/: the park throw
|
||||
// tore through Fibers.trampoline()'s do/while, leaving the guard true.
|
||||
expect(watchdog, 'stuck trampoline guard should be visible').toContain(
|
||||
'trampolineRunning=true'
|
||||
);
|
||||
const { passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
|
||||
expect(passed).toBe(1);
|
||||
expect(failed).toBe(0);
|
||||
|
||||
// The legacy disease signature must be ABSENT: no stuck-trampoline watchdog dump.
|
||||
expect(
|
||||
testLogger.consoleLogs.some((l) =>
|
||||
l.includes('[ASYNCIFY_RACES] FAIL post_park_fiber_swap')
|
||||
l.includes('[ASYNCIFY_RACES] WATCHDOG post_park_fiber_swap')
|
||||
),
|
||||
'scenario should be reported FAILED by the watchdog'
|
||||
).toBe(true);
|
||||
|
||||
// And the suite never completes — the swap is stranded forever.
|
||||
expect(findSummary(testLogger.consoleLogs)).toBeUndefined();
|
||||
'no stuck-trampoline watchdog should fire under native-EH'
|
||||
).toBe(false);
|
||||
expect(crashLines(testLogger), 'no crash signatures in console').toHaveLength(0);
|
||||
});
|
||||
|
||||
test('no handleSleep fix: fiber swaps clobber the parked sleep buffer', async ({
|
||||
test('handleSleep ablated: native-EH long parked sleep is not clobbered by a swap', async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
|
|
@ -228,32 +237,17 @@ test.describe('Asyncify races — ablation pins (the disease stays reproducible)
|
|||
);
|
||||
await tryLoadApp(page, 30000);
|
||||
|
||||
// Either the wakeUp crashes (index out of bounds family) or the rewind is
|
||||
// lost and the watchdog reports the stall — both are the recorded disease.
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
const crashed = [...testLogger.errors, ...testLogger.consoleLogs].some(
|
||||
(l) =>
|
||||
l.toLowerCase().includes('index out of bounds') ||
|
||||
l.toLowerCase().includes('indirect call to null') ||
|
||||
l.toLowerCase().includes('invalid state')
|
||||
);
|
||||
const stalled = testLogger.consoleLogs.some((l) =>
|
||||
l.includes('[ASYNCIFY_RACES] FAIL long_parked_sleep_clobbered_by_swap')
|
||||
);
|
||||
return crashed || stalled ? 'reproduced' : null;
|
||||
},
|
||||
{ timeout: 30000, message: 'ablated build should reproduce the clobber bug' }
|
||||
)
|
||||
.poll(() => findSummary(testLogger.consoleLogs) ?? null, {
|
||||
timeout: 45000,
|
||||
message: 'long parked sleep should resolve even with the handleSleep fix ablated',
|
||||
})
|
||||
.not.toBeNull();
|
||||
|
||||
// It must NOT have quietly passed.
|
||||
expect(
|
||||
testLogger.consoleLogs.some((l) =>
|
||||
l.includes('[ASYNCIFY_RACES] PASS long_parked_sleep_clobbered_by_swap')
|
||||
),
|
||||
'ablated build must not pass the clobber scenario'
|
||||
).toBe(false);
|
||||
const { passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
|
||||
expect(passed).toBe(1);
|
||||
expect(failed).toBe(0);
|
||||
expect(crashLines(testLogger), 'no crash signatures in console').toHaveLength(0);
|
||||
expect(realErrors(testLogger), 'no page errors').toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
67
tests/e2e/coroutine-async-preload.spec.ts
Normal file
67
tests/e2e/coroutine-async-preload.spec.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { test, expect } from './utils/fixtures';
|
||||
|
||||
// 1b: library-preload native-EH repro (docs/features/wasm-exceptions/10 §7).
|
||||
// A STANDALONE std::async worker that parses+THROWS (mode-c) and proxies a fetch to main — the
|
||||
// KiCad-10 PCBJAM preload shape, with NO KiCad source. Proves native wasm-EH makes the worker-side
|
||||
// parse-throw safe, and that the proxy round-trip / lazy join / modal-reentrancy all work.
|
||||
//
|
||||
// Named coroutine-* so playwright-coroutine.config.ts runs it in real Chrome + Firefox.
|
||||
// WebKit skipped for pthread apps (COEP).
|
||||
|
||||
const APP = '/standalone/async-preload/async_preload_test.html';
|
||||
|
||||
function parse( logs: string[], mode: number ) {
|
||||
const line = logs.find( l => l.includes( `[PRELOAD] SUCCESS m=${mode}` ) );
|
||||
if( !line ) return null;
|
||||
const c = line.match( /caught=(\d+)/ );
|
||||
const ld = line.match( /loaded=(\d+)/ );
|
||||
return { caught: c ? +c[1] : -1, loaded: ld ? +ld[1] : -1 };
|
||||
}
|
||||
|
||||
async function waitForLog( testLogger: { consoleLogs: string[] }, needle: string, timeout = 60000 ) {
|
||||
await expect.poll( () => testLogger.consoleLogs.some( l => l.includes( needle ) ), { timeout } ).toBe( true );
|
||||
}
|
||||
|
||||
// Fatal native-EH / Asyncify failures we must NOT see.
|
||||
function fatal( testLogger: { errors: string[] } ) {
|
||||
return testLogger.errors.filter( e => !e.includes( 'favicon' )
|
||||
&& /invalid state|table index out of bounds|aborted|unreachable|func is not a function/i.test( e ) );
|
||||
}
|
||||
|
||||
test.describe( 'std::async library-preload — native-EH safe (KiCad-10 shape, standalone)', () => {
|
||||
|
||||
test( 'm=0 simple: worker proxies + parses, lazy join completes', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=0` );
|
||||
await waitForLog( testLogger, '[PRELOAD] SUCCESS m=0' );
|
||||
const r = parse( testLogger.consoleLogs, 0 )!;
|
||||
expect( r.loaded, 'libraries parsed' ).toBeGreaterThan( 0 );
|
||||
expect( fatal( testLogger ), 'no fatal errors' ).toHaveLength( 0 );
|
||||
} );
|
||||
|
||||
// The decisive native-EH proof: the parse THROWS on the worker; the worker's try/catch is safe
|
||||
// under native wasm-EH (mode-c) but crashes under JS-EH (-fexceptions). Gated to native.
|
||||
test( 'm=1 throw: worker parse-throw is caught (mode-c safe under native-EH)', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=1` );
|
||||
await waitForLog( testLogger, '[PRELOAD] EH=' );
|
||||
test.skip( !testLogger.consoleLogs.some( l => l.includes( '[PRELOAD] EH=native' ) ),
|
||||
'worker throw-on-parse (mode-c) is native-EH-only; JS-EH build skips it' );
|
||||
await waitForLog( testLogger, '[PRELOAD] SUCCESS m=1' );
|
||||
const r = parse( testLogger.consoleLogs, 1 )!;
|
||||
expect( r.caught, 'the worker parse exception was caught' ).toBe( 1 );
|
||||
expect( fatal( testLogger ), 'no mode-c crash' ).toHaveLength( 0 );
|
||||
} );
|
||||
|
||||
test( 'm=2 shutdown: blocking-join the future mid-load completes', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=2` );
|
||||
await waitForLog( testLogger, '[PRELOAD] SUCCESS m=2' );
|
||||
expect( testLogger.consoleLogs.some( l => l.includes( '[PRELOAD] shutdown joined' ) ),
|
||||
'the blocking join returned (no deadlock)' ).toBe( true );
|
||||
expect( fatal( testLogger ), 'no crash on the shutdown join' ).toHaveLength( 0 );
|
||||
} );
|
||||
|
||||
test( 'm=3 modal during preload: g_proxyMutex prevents the reentrancy crash', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=3` );
|
||||
await waitForLog( testLogger, '[PRELOAD] SUCCESS m=3', 90000 );
|
||||
expect( fatal( testLogger ), 'no table-index-out-of-bounds reentrancy crash' ).toHaveLength( 0 );
|
||||
} );
|
||||
} );
|
||||
46
tests/e2e/coroutine-pthread-ondemand.spec.ts
Normal file
46
tests/e2e/coroutine-pthread-ondemand.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { test, expect } from './utils/fixtures';
|
||||
|
||||
// Phase 2: on-demand (NON-warm) pthread Worker creation WITHOUT modifying KiCad.
|
||||
//
|
||||
// The real KiCad pool (compiled-in kicad/common/thread_pool.cpp, via GetKiCadThreadPool())
|
||||
// consumes ALL the pre-warmed Workers at construction; raw fly-threads beyond that count
|
||||
// must then be created ON DEMAND, whose 'loaded'->'run' handshake needs the main event loop.
|
||||
// The fix is wasm/shims/nanosleep_yield.c (a strong nanosleep override): the main-thread sleep_for
|
||||
// join Asyncify-yields so the loop services the handshake and the on-demand Workers boot.
|
||||
//
|
||||
// Named coroutine-* so playwright-coroutine.config.ts runs it in real Chrome + Firefox.
|
||||
// WebKit is skipped for pthread apps (COEP worker-load limitation; doc 10 §2a).
|
||||
|
||||
const APP = '/standalone/pthread-ondemand/pthread_ondemand_test.html';
|
||||
|
||||
function parse( logs: string[], mode: number ) {
|
||||
const line = logs.find( l => l.includes( `[ONDEMAND] SUCCESS m=${mode}` ) );
|
||||
if( !line ) return null;
|
||||
const w = line.match( /workersRan=(\d+)/ );
|
||||
return { workersRan: w ? +w[1] : -1 };
|
||||
}
|
||||
|
||||
async function waitForLog( testLogger: { consoleLogs: string[] }, needle: string, timeout = 60000 ) {
|
||||
await expect.poll( () => testLogger.consoleLogs.some( l => l.includes( needle ) ), { timeout } ).toBe( true );
|
||||
}
|
||||
|
||||
test.describe( 'On-demand non-warm pthread Worker (real pool drains the pre-warmed pool)', () => {
|
||||
|
||||
test( 'fix: nanosleep override yields → on-demand Workers boot → multi-core', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=1` );
|
||||
await waitForLog( testLogger, '[ONDEMAND] SUCCESS m=1' );
|
||||
const r = parse( testLogger.consoleLogs, 1 )!;
|
||||
expect( r.workersRan, 'on-demand Workers boot and run via the yielding join' ).toBeGreaterThan( 1 );
|
||||
} );
|
||||
|
||||
// Negative control: a busy-wait join that never calls nanosleep → never yields → the
|
||||
// on-demand Workers never boot. Held to the SAME bar (workersRan>1), expected to miss it,
|
||||
// so it is reported as an EXPECTED failure. If it ever passes, on-demand got fixed another way.
|
||||
test( 'control: busy-wait (no nanosleep) CANNOT boot on-demand Workers', async ( { page, testLogger } ) => {
|
||||
test.fail();
|
||||
await page.goto( `${APP}#m=0`, { waitUntil: 'domcontentloaded' } );
|
||||
await waitForLog( testLogger, '[ONDEMAND] SUCCESS m=0', 20000 );
|
||||
const r = parse( testLogger.consoleLogs, 0 )!;
|
||||
expect( r.workersRan, 'a non-yielding busy-wait cannot create on-demand Workers' ).toBeGreaterThan( 1 );
|
||||
} );
|
||||
} );
|
||||
48
tests/e2e/coroutine-raytrace-modal.spec.ts
Normal file
48
tests/e2e/coroutine-raytrace-modal.spec.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { test, expect } from './utils/fixtures';
|
||||
|
||||
// A raytracer-style worker-join run inside a wx modal pump. A pass is dispatched from a wxTimer that
|
||||
// fires while a ShowModal() dialog is open; the modal pump runs ProcessEvents via ccall(async:true),
|
||||
// so the work runs in a fresh managed Asyncify context at state == Normal. Both join styles complete
|
||||
// multi-core there:
|
||||
// m=0 busywait : sleep_for join; the pre-warmed pool completes it.
|
||||
// m=1 yield : emscripten_sleep join; legal at state == Normal, so it suspends and resumes.
|
||||
//
|
||||
// Named coroutine-* so playwright-coroutine.config.ts runs it in real Chrome + Firefox. WebKit
|
||||
// skipped for pthread apps (COEP).
|
||||
|
||||
const APP = '/standalone/raytrace-modal/raytrace_modal_test.html';
|
||||
|
||||
function workersRan( logs: string[] ): number {
|
||||
const l = logs.find( x => x.includes( '[RTPOOL] SUCCESS' ) );
|
||||
const m = l?.match( /workersRan=(\d+)/ );
|
||||
return m ? +m[1] : -1;
|
||||
}
|
||||
|
||||
function abortErrors( testLogger: { errors: string[] } ) {
|
||||
return testLogger.errors.filter( e => /invalid state:\s*1|Aborted/i.test( e ) );
|
||||
}
|
||||
|
||||
async function waitForLog( testLogger: { consoleLogs: string[] }, needle: string, timeout = 60000 ) {
|
||||
await expect.poll( () => testLogger.consoleLogs.some( l => l.includes( needle ) ), { timeout } ).toBe( true );
|
||||
}
|
||||
|
||||
test.describe( 'Raytracer worker-join inside a wx modal pump', () => {
|
||||
|
||||
test( 'm=0 busywait: pre-warmed pool busy-wait completes inside the modal → multi-core', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=0` );
|
||||
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=0' );
|
||||
expect( workersRan( testLogger.consoleLogs ), 'multi-core inside the modal' ).toBeGreaterThan( 1 );
|
||||
expect( abortErrors( testLogger ), 'no Asyncify abort' ).toHaveLength( 0 );
|
||||
} );
|
||||
|
||||
// The in-modal work runs in a fresh ProcessEvents entry at Asyncify state == Normal (the app
|
||||
// probes and logs it), so an emscripten_sleep join is legal and the pass completes multi-core.
|
||||
test( 'm=1 yield: emscripten_sleep join inside the modal → multi-core', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=1` );
|
||||
await waitForLog( testLogger, '[RTPOOL] SUCCESS mode=1' );
|
||||
expect( testLogger.consoleLogs.some( l => /Asyncify\.state=0/.test( l ) ),
|
||||
'the in-modal work runs at state == Normal (a fresh ProcessEvents entry)' ).toBe( true );
|
||||
expect( workersRan( testLogger.consoleLogs ), 'the yield-join completes → multi-core' ).toBeGreaterThan( 1 );
|
||||
expect( abortErrors( testLogger ), 'no Asyncify abort' ).toHaveLength( 0 );
|
||||
} );
|
||||
} );
|
||||
73
tests/e2e/coroutine-threadpool-real.spec.ts
Normal file
73
tests/e2e/coroutine-threadpool-real.spec.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { test, expect } from './utils/fixtures';
|
||||
|
||||
// The BS-pool-API native-EH test (docs/features/wasm-exceptions/10 §6 #1).
|
||||
//
|
||||
// Unlike threadpool.spec.ts / coroutine-raytrace.spec.ts (which hand-roll raw std::thread),
|
||||
// this drives KiCad's REAL pool: the app compiles in kicad/common/thread_pool.cpp and calls
|
||||
// the actual GetKiCadThreadPool() (a BS::priority_thread_pool), with the detach_task
|
||||
// __EMSCRIPTEN__ inline shim opted OUT via -DKICAD_WASM_REAL_THREADPOOL so tasks run on the
|
||||
// pool's persistent pthread workers.
|
||||
//
|
||||
// The real pool is mode-a/b-safe by construction (persistent workers -> no on-demand spawn;
|
||||
// futex busy-wait join -> no Asyncify nesting). The only native-EH risk is mode-c: a task
|
||||
// that THROWS on a worker (caught by submit_task's promise wrapper ON the worker drives
|
||||
// Asyncify under -fexceptions). So mode 6 is the decisive native-EH proof; modes 0-5 prove
|
||||
// real multi-core (workersRan>1) across the API surface. Green => we can drop the shim.
|
||||
//
|
||||
// Named coroutine-* so playwright-coroutine.config.ts runs it in real Chrome + Firefox.
|
||||
// WebKit is skipped for pthread apps (COEP worker-load limitation; doc 10 §2a).
|
||||
|
||||
const APP = '/standalone/threadpool-real/threadpool_real_test.html';
|
||||
|
||||
const MODES: { m: number; name: string }[] = [
|
||||
{ m: 0, name: 'submit_task + vector<future> poll' },
|
||||
{ m: 1, name: 'submit_loop + multi_future.wait' },
|
||||
{ m: 2, name: 'submit_blocks (typed returns)' },
|
||||
{ m: 3, name: 'detach_task + tp.wait()' },
|
||||
{ m: 4, name: 'manual multi_future fanout by get_thread_count' },
|
||||
{ m: 5, name: 'lifecycle: get_tasks_*/purge/wait + pause pool' },
|
||||
];
|
||||
|
||||
function parse( logs: string[], mode: number ) {
|
||||
const line = logs.find( l => l.includes( `[POOL] SUCCESS mode=${mode}` ) );
|
||||
if( !line ) return null;
|
||||
const w = line.match( /workersRan=(\d+)/ );
|
||||
const c = line.match( /caught=(\d+)/ );
|
||||
return { workersRan: w ? +w[1] : -1, caught: c ? +c[1] : -1 };
|
||||
}
|
||||
|
||||
async function waitForLog( testLogger: { consoleLogs: string[] }, needle: string, timeout = 60000 ) {
|
||||
await expect.poll( () => testLogger.consoleLogs.some( l => l.includes( needle ) ), { timeout } ).toBe( true );
|
||||
}
|
||||
|
||||
test.describe( 'Real BS::thread_pool (GetKiCadThreadPool) — multi-core under native-EH', () => {
|
||||
|
||||
for( const { m, name } of MODES ) {
|
||||
test( `mode ${m}: ${name} runs multi-core on the real pool`, async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=${m}` );
|
||||
await waitForLog( testLogger, `[POOL] SUCCESS mode=${m}` );
|
||||
const r = parse( testLogger.consoleLogs, m )!;
|
||||
expect( r.workersRan, 'tasks must run on >1 pool worker' ).toBeGreaterThan( 1 );
|
||||
expect( testLogger.errors.filter( e => !e.includes( 'favicon' ) ), 'no runtime errors' ).toHaveLength( 0 );
|
||||
} );
|
||||
}
|
||||
|
||||
// mode-c: a task throws ON a worker; submit_task's promise wrapper catches it on the
|
||||
// worker (drives Asyncify under -fexceptions -> "func is not a function" crash) and
|
||||
// rethrows on main. Native wasm-EH decouples exceptions from Asyncify, so this must
|
||||
// complete cleanly. (Red under JS-EH, green under native-EH — the contrast IS the proof.)
|
||||
test( 'mode 6: throw on a worker is safe under native-EH and rethrows on main', async ( { page, testLogger } ) => {
|
||||
await page.goto( `${APP}#m=6` );
|
||||
// A worker throw is a mode-c crash under JS-EH and only safe under native wasm-EH, so this
|
||||
// assertion is native-EH-only. The app reports its EH model early; skip on a JS-EH build
|
||||
// (builds are always native-EH now, so this never skips) rather than asserting a crash.
|
||||
await waitForLog( testLogger, '[POOL] EH=' );
|
||||
test.skip( !testLogger.consoleLogs.some( l => l.includes( '[POOL] EH=native' ) ),
|
||||
'throw-on-worker (mode-c) is native-EH-only; JS-EH build skips this assertion' );
|
||||
await waitForLog( testLogger, '[POOL] SUCCESS mode=6' );
|
||||
const r = parse( testLogger.consoleLogs, 6 )!;
|
||||
expect( r.caught, 'the worker throw must rethrow + be caught on main' ).toBe( 1 );
|
||||
expect( r.workersRan, 'workers still run a normal batch after the throw' ).toBeGreaterThan( 1 );
|
||||
expect( testLogger.errors.filter( e => !e.includes( 'favicon' ) ), 'no mode-c crash' ).toHaveLength( 0 );
|
||||
} );
|
||||
} );
|
||||
|
|
@ -266,6 +266,45 @@ test.describe("pcbnew collab bridge — single page", () => {
|
|||
});
|
||||
}
|
||||
|
||||
// REGRESSION (native-EH vtable-slot skew, task #54): a second consecutive collab apply must also
|
||||
// take effect. Root cause: the embind TU was compiled without -DDEBUG while the core TU (CMake
|
||||
// Config=Debug) had it, so a DEBUG-gated virtual (EDA_ITEM::Show) occupied a vtable slot the embind
|
||||
// didn't account for, shifting every later slot by one. Embind virtual calls in the apply
|
||||
// (SetWidth/GetPosition/the rebaseline snapshot getters) then read the wrong slot → call_indirect
|
||||
// signature-mismatch trap, swallowed by the apply coroutine's catch_all → silent loop, so the apply
|
||||
// never completed. Fixed by building the embind TU with -DDEBUG in Debug builds
|
||||
// (scripts/kicad/build-kicad-target.sh). RED before the fix (move times out), GREEN after.
|
||||
test("native-EH: a second consecutive collab apply also takes effect", async ({ page, testLogger }) => {
|
||||
await bootAndOpen(page, "apply");
|
||||
|
||||
const before = await page.evaluate((id) => window.Module.kicadCollabGetPos(id), SEG1);
|
||||
const [bx, by] = before.split(",").map(Number);
|
||||
|
||||
const moveTo = async (x: number) => {
|
||||
await page.evaluate(
|
||||
({ id, x, by }) =>
|
||||
window.Module.kicadCollabApply(
|
||||
JSON.stringify({
|
||||
changed: [{ id, type: "PCB_TRACK", sx: x, sy: by, ex: x + 50_800_000, ey: by, width: 200000 }],
|
||||
added: [],
|
||||
removed: [],
|
||||
}),
|
||||
),
|
||||
{ id: SEG1, x, by },
|
||||
);
|
||||
await expect
|
||||
.poll(() => page.evaluate((id) => window.Module.kicadCollabGetPos(id), SEG1), {
|
||||
timeout: 10000,
|
||||
intervals: [200],
|
||||
})
|
||||
.toBe(`${x},${by}`);
|
||||
};
|
||||
|
||||
await moveTo(bx + 5_000_000); // 1st apply
|
||||
await moveTo(bx + 8_000_000); // 2nd apply — RED before the -DDEBUG fix (apply mis-dispatches), GREEN after
|
||||
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||
});
|
||||
|
||||
// Apply mutates the model headless: kicadOpenFile returns false (the incomplete-project load
|
||||
// skips some late steps) but the board IS built, so BOARD_COMMIT::Push takes effect. Rendering
|
||||
// still needs the real app. (Same headless reality as the eeschema apply test.)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@
|
|||
"test:coroutine:firefox": "playwright test --config=playwright-coroutine.config.ts --project=firefox",
|
||||
"test:coroutine:chrome": "playwright test --config=playwright-coroutine.config.ts --project=chromium --headed",
|
||||
"test:asyncify:firefox": "playwright test --config=playwright-asyncify.config.ts --project=firefox",
|
||||
"test:asyncify:chrome": "playwright test --config=playwright-asyncify.config.ts --project=chromium --headed"
|
||||
"test:asyncify:chrome": "playwright test --config=playwright-asyncify.config.ts --project=chromium --headed",
|
||||
"test:asyncify:safari": "playwright test --config=playwright-asyncify.config.ts --project=webkit",
|
||||
"test:asyncify:all": "npm run test:asyncify:firefox && npm run test:asyncify:safari && npm run test:asyncify:chrome"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ export default defineConfig({
|
|||
permissions: ['clipboard-read', 'clipboard-write'],
|
||||
},
|
||||
},
|
||||
{
|
||||
// WebKit (Safari's engine) — headless OK on macOS. Project policy: every spec must be
|
||||
// green in all three engines (Firefox + Chrome + Safari). Run via npm run test:asyncify:safari.
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'], viewport: { width: 1280, height: 720 } },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"source": "**/*",
|
||||
"headers": [
|
||||
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
|
||||
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
|
||||
{ "key": "Cross-Origin-Resource-Policy", "value": "cross-origin" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 156 KiB |
Loading…
Reference in a new issue