From 5df0ac24e3106700efb50b195955119d8b0edea1 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 18 Jul 2026 19:56:39 +0100 Subject: [PATCH 1/6] fix(wasm): reuse a session-scoped SolverContext in the browser MVP dds_mvp_calc_table called the stateless CalcDDtablePBN, which allocated a fresh SolverContext (and transposition table) on every call and freed it before returning. Reuse a lazily-constructed static SolverContext for the lifetime of the wasm module instance instead, calling reset_for_solve() between deals to recycle the TT memory pool without freeing it. Co-Authored-By: Claude --- web/dds_mvp_wasm.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/dds_mvp_wasm.cpp b/web/dds_mvp_wasm.cpp index 34634509..4c99fa19 100644 --- a/web/dds_mvp_wasm.cpp +++ b/web/dds_mvp_wasm.cpp @@ -7,7 +7,7 @@ #include -#include +#include #if defined(__EMSCRIPTEN__) #include @@ -15,6 +15,14 @@ #define EMSCRIPTEN_KEEPALIVE #endif +namespace { +auto mvp_context() -> SolverContext& { + static SolverContext ctx; // lazily constructed on first call, lives for + // the module instance's lifetime (the session) + return ctx; +} +} // namespace + extern "C" { // Fills out_table[20] with res_table[strain][hand] (strain 0..4 = S,H,D,C,N). @@ -33,8 +41,12 @@ auto dds_mvp_calc_table(const char* pbn, int* out_table) -> int } std::memcpy(deal.cards, pbn, pbn_len + 1); + SolverContext& ctx = mvp_context(); + ctx.reset_for_solve(); // recycle TT memory pool + search bookkeeping + // between deals; keeps the underlying allocation + DdTableResults table{}; - const int res = CalcDDtablePBN(deal, &table); + const int res = calc_dd_table_pbn(ctx, deal, &table); if (res != RETURN_NO_FAULT) { return res; } From 5bd067f8ce6cdf5b711fd8ecdc87290303307138 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 18 Jul 2026 19:57:43 +0100 Subject: [PATCH 2/6] test(wasm): add cross-deal SolverContext reuse regression test Adds kPbnHand1/kExpectedHand1 (sourced from hands/list10.txt's third record) and a test that solves two different deals back-to-back through the same reused static SolverContext, then re-solves the first deal to confirm no stale state leaks across calls. Co-Authored-By: Claude --- web/dds_mvp_wasm_test.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/web/dds_mvp_wasm_test.cpp b/web/dds_mvp_wasm_test.cpp index 35bc17ce..a4c895a0 100644 --- a/web/dds_mvp_wasm_test.cpp +++ b/web/dds_mvp_wasm_test.cpp @@ -23,6 +23,13 @@ constexpr int kExpectedHand0[20] = { constexpr char kPbnHand0[] = "N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3"; +constexpr int kExpectedHand1[20] = { + 3, 10, 3, 10, 9, 4, 9, 4, 8, 4, 8, 4, 3, 9, 3, 9, 4, 8, 4, 8, +}; + +constexpr char kPbnHand1[] = + "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 5.A95432.7632.K6 AKJ9842.K.T8.J93"; + } // namespace TEST(DdsMvpWasmTest, RejectsNullPointers) { @@ -51,3 +58,25 @@ TEST(DdsMvpWasmTest, FillsFlatStrainHandTable) { EXPECT_EQ(out[i], kExpectedHand0[i]) << "index " << i; } } + +TEST(DdsMvpWasmTest, FillsFlatStrainHandTableAcrossReuse) { + int out0[20]{}; + ASSERT_EQ(dds_mvp_calc_table(kPbnHand0, out0), RETURN_NO_FAULT); + for (int i = 0; i < 20; ++i) { + EXPECT_EQ(out0[i], kExpectedHand0[i]) << "hand0 index " << i; + } + + int out1[20]{}; + ASSERT_EQ(dds_mvp_calc_table(kPbnHand1, out1), RETURN_NO_FAULT); + for (int i = 0; i < 20; ++i) { + EXPECT_EQ(out1[i], kExpectedHand1[i]) << "hand1 index " << i; + } + + // Solve hand0 again through the same reused context to confirm the + // intervening different-deal solve didn't leave stale state behind. + int out0_again[20]{}; + ASSERT_EQ(dds_mvp_calc_table(kPbnHand0, out0_again), RETURN_NO_FAULT); + for (int i = 0; i < 20; ++i) { + EXPECT_EQ(out0_again[i], kExpectedHand0[i]) << "hand0 repeat index " << i; + } +} From df40646c0f768d3c4e0570e922ca1c5b2ae62c62 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 18 Jul 2026 21:31:57 +0100 Subject: [PATCH 3/6] perf(wasm): switch to native wasm exception handling Replace -fexceptions (Emscripten's JS-trampoline exception emulation) with -fwasm-exceptions (native wasm EH proposal, supported by emsdk 5.0.7 / all current browsers and Node) at compile and link time for all wasm builds. The toolchain's default -fno-exceptions is not overridden at the clang frontend level by -fwasm-exceptions alone in this LLVM build, so -fexceptions is kept immediately before it in DDS_CPPOPTS to force-enable exceptions before -fwasm-exceptions selects the wasm EH lowering mechanism. Benchmarked ~4% faster (90-91ms/call vs 93-94.5ms/call) over a rotating set of deals via Node timing script. Co-Authored-By: Claude --- CPPVARIABLES.bzl | 5 +++++ wasm_compat.bzl | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CPPVARIABLES.bzl b/CPPVARIABLES.bzl index 432cfb57..6325f45a 100644 --- a/CPPVARIABLES.bzl +++ b/CPPVARIABLES.bzl @@ -59,7 +59,12 @@ DDS_CPPOPTS = select({ "-Wpedantic", "-Wall", "-Werror", + # -fexceptions must precede -fwasm-exceptions: the toolchain's default + # -fno-exceptions is otherwise not overridden at the clang frontend + # level by -fwasm-exceptions alone (it only selects the EH lowering + # mechanism, not the "exceptions enabled" toggle, in this LLVM build). "-fexceptions", + "-fwasm-exceptions", ], "//conditions:default": [ "-std=c++20" diff --git a/wasm_compat.bzl b/wasm_compat.bzl index c3fea9b7..0e651073 100644 --- a/wasm_compat.bzl +++ b/wasm_compat.bzl @@ -3,7 +3,7 @@ # Emscripten link flags for cc_binary targets wrapped by wasm_cc_binary. WASM_LINKOPTS = [ "-sWASM=1", - "-fexceptions", + "-fwasm-exceptions", "-sALLOW_MEMORY_GROWTH=1", "-sINITIAL_MEMORY=268435456", # DDS search recursion needs more than Emscripten's 64KB default stack. From a7cb699b2853aaeaaf373df6ec05864d7cda90a3 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sat, 18 Jul 2026 21:34:03 +0100 Subject: [PATCH 4/6] docs(wasm): update flag table for the exception-model switch Reflects -fexceptions -> -fwasm-exceptions and documents why wasm link-time -flto was tried and reverted (the pinned emsdk 5.0.7 frozen cache lacks LTO-bitcode system libraries). Co-Authored-By: Claude --- docs/wasm_build.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/wasm_build.md b/docs/wasm_build.md index 561192fa..8014c55e 100644 --- a/docs/wasm_build.md +++ b/docs/wasm_build.md @@ -96,13 +96,22 @@ For other experiments, copy built `.js` / `.wasm` files from `bazel-bin/wasm/` t | Flag | Purpose | |------|---------| | `-O3` | Aggressive optimization | -| `-flto` | Link-time optimization | -| `-fexceptions` | Enable C++ exceptions | +| `-flto` | Link-time optimization at compile time only (see note below) | +| `-fwasm-exceptions` | Native WebAssembly exception handling (replaces the slower JS-trampoline exception emulation of `-fexceptions`) | | `-sWASM=1` | Emscripten WASM output (link flag) | | `-sALLOW_MEMORY_GROWTH=1` | Allow heap growth at runtime | | `-sINITIAL_MEMORY=268435456` | 256MB initial memory | | `-sSTACK_SIZE=8388608` | 8MB stack (default 64KB is too small for DDS search) | +`-flto` is applied at compile time (`DDS_CPPOPTS`) but deliberately **not** at +link time. Passing `-flto` to the wasm link step was tried and reverted: the +`emsdk 5.0.7` package pinned in `MODULE.bazel` ships a frozen, pre-built cache +containing only non-LTO system libraries, and requesting link-time LTO makes +Emscripten require LTO-bitcode variants of core sysroot libraries (e.g. +`libprintf_long_double`) that the frozen cache can't build on demand inside +Bazel's hermetic sandbox. Revisit only if the emsdk packaging changes to ship +a populated LTO cache. + ## C++ standard WASM example binaries are built as C++20. The Emscripten toolchain (via `wasm_cc_binary`) compiles the transitioned `cc_target`; project flags for that configuration come from `CPPVARIABLES.bzl` when `//:build_wasm` matches (the Emscripten platform transition sets `wasm32`). From 8f6e046110d1debcadc33b71089b76e177b0e354 Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Sun, 19 Jul 2026 13:36:56 +0100 Subject: [PATCH 5/6] docs(spec): document session-scoped SolverContext and wasm LTO gap web-mvp: records that dds_mvp_calc_table now reuses one static SolverContext for the module's lifetime (Task 01) instead of allocating fresh per call, including the concurrency caveat. wasm-emscripten: adds a known-gap entry for link-time LTO, tried and reverted because the pinned emsdk 5.0.7 toolchain's frozen cache lacks LTO-bitcode system libraries. build-system: corrects the "macOS and WASM add LTO" claim to reflect that WASM only gets compile-time LTO, not link-time. Co-Authored-By: Claude --- specs/build-system.md | 13 ++++++++----- specs/wasm-emscripten.md | 10 +++++++++- specs/web-mvp.md | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/specs/build-system.md b/specs/build-system.md index 76105081..3417e4a5 100644 --- a/specs/build-system.md +++ b/specs/build-system.md @@ -1,7 +1,7 @@ --- capability: build-system owners: [//, CPPVARIABLES.bzl, wasm_compat.bzl] -last-updated: 2026-07-18 +last-updated: 2026-07-19 --- # Build System @@ -37,10 +37,13 @@ than re-encoding toolchain knowledge. the host OS). Any target that wants per-platform flags does so through the `select()`s in `CPPVARIABLES.bzl` — it should not hand-roll `-O3`/`/O2`. - **Optimised builds are strict.** Non-debug macOS/Linux/WASM use `-O3` with - `-Wall -Wpedantic -Werror`; Windows uses `/O2 /W4 /WX /permissive-`. macOS and - WASM add LTO. C++20 is the baseline (`--cxxopt=-std=c++20` in `.bazelrc` for - macOS/Linux; Windows `/std:c++20` in `DDS_CPPOPTS`). Treat `-Werror` as a - standing invariant: warnings break the build. + `-Wall -Wpedantic -Werror`; Windows uses `/O2 /W4 /WX /permissive-`. macOS adds + LTO at both compile and link (`-flto=thin`); WASM adds `-flto` at compile time + only — link-time LTO is not currently possible under the pinned hermetic emsdk + toolchain (see [wasm-emscripten](wasm-emscripten.md)). C++20 is the baseline + (`--cxxopt=-std=c++20` in `.bazelrc` for macOS/Linux; Windows `/std:c++20` in + `DDS_CPPOPTS`). Treat `-Werror` as a standing invariant: warnings break the + build. - **Feature flags are `--define`-driven `config_setting`s**, surfaced through `DDS_LOCAL_DEFINES` / `DDS_SCHEDULER_DEFINE`: | config setting | `--define` | preprocessor define | capability | diff --git a/specs/wasm-emscripten.md b/specs/wasm-emscripten.md index 697fba99..67b5f357 100644 --- a/specs/wasm-emscripten.md +++ b/specs/wasm-emscripten.md @@ -1,7 +1,7 @@ --- capability: wasm-emscripten owners: [wasm] -last-updated: 2026-07-18 +last-updated: 2026-07-19 --- # WASM (Emscripten) Build @@ -56,6 +56,14 @@ core solver builds and runs correctly under Emscripten. - **Only three examples are ported**, not the full [examples-cli](examples-cli.md) set. - **No threaded WASM** — single-thread only. +- **No link-time LTO.** `-flto` applies at WASM compile time only + ([build-system](build-system.md)); enabling it at link time was tried and + reverted because the `emsdk 5.0.7` toolchain pinned in `MODULE.bazel` ships a + frozen, pre-built cache containing only non-LTO system libraries. Link-time + LTO requires LTO-bitcode variants of core sysroot libraries (e.g. + `libprintf_long_double`) that the frozen cache cannot build on demand inside + Bazel's hermetic sandbox. Revisit only if the emsdk packaging ships a + populated LTO cache. - Browser wiring, the site, MVP post-build JS patches (`web/patch_mvp_wasm.py`), and JS/e2e tests belong to [web-mvp](web-mvp.md); this capability provides the example modules, not the page. diff --git a/specs/web-mvp.md b/specs/web-mvp.md index 760e9db7..653adae7 100644 --- a/specs/web-mvp.md +++ b/specs/web-mvp.md @@ -1,7 +1,7 @@ --- capability: web-mvp owners: [web] -last-updated: 2026-07-18 +last-updated: 2026-07-19 --- # Web MVP @@ -53,8 +53,19 @@ a full application. browser/`file://` safety on `//web:dds_mvp_wasm` only — not the example WASM CLIs. If an emsdk upgrade moves that line, update the regex and the note in `docs/wasm_build.md`. +- **The module holds one session-scoped `SolverContext` for its lifetime.** + `dds_mvp_calc_table` (`web/dds_mvp_wasm.cpp`) constructs a function-local + `static SolverContext` lazily on first call and reuses it — including its + transposition table — for every subsequent call in that module instance, + calling `reset_for_solve()` between deals to recycle the TT memory pool + without freeing the underlying allocation. This mirrors a WASM module + instance's own lifetime (one `WebAssembly.Memory`, static constructors run + once) instead of allocating a fresh [solver-context](solver-context.md) per + call. Because the context is shared, it is **not safe for concurrent + solves** — a future move to Web Workers would need one context per worker. - **The MVP's WASM inherits the single-thread assumption** of - [wasm-emscripten](wasm-emscripten.md); results come from the same [dds-public-api](dds-public-api.md) core. + [wasm-emscripten](wasm-emscripten.md) — which is what makes sharing that + single context safe today; results come from the same [dds-public-api](dds-public-api.md) core. ## Key entry points From d3a55aa818a4e7a57838c12c6c6f35354110a82f Mon Sep 17 00:00:00 2001 From: Martin Nygren Date: Thu, 23 Jul 2026 19:33:35 +0100 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/wasm_build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wasm_build.md b/docs/wasm_build.md index 8014c55e..289faa65 100644 --- a/docs/wasm_build.md +++ b/docs/wasm_build.md @@ -97,7 +97,7 @@ For other experiments, copy built `.js` / `.wasm` files from `bazel-bin/wasm/` t |------|---------| | `-O3` | Aggressive optimization | | `-flto` | Link-time optimization at compile time only (see note below) | -| `-fwasm-exceptions` | Native WebAssembly exception handling (replaces the slower JS-trampoline exception emulation of `-fexceptions`) | +| `-fwasm-exceptions` | Native WebAssembly exception handling (use together with `-fexceptions`; replaces the slower JS-trampoline EH lowering used when linking with `-fexceptions` alone) | | `-sWASM=1` | Emscripten WASM output (link flag) | | `-sALLOW_MEMORY_GROWTH=1` | Allow heap growth at runtime | | `-sINITIAL_MEMORY=268435456` | 256MB initial memory |