Skip to content

Try improve WASM performance.#247

Merged
zzcgumn merged 6 commits into
developfrom
fix/try_improve_wasm_performance
Jul 23, 2026
Merged

Try improve WASM performance.#247
zzcgumn merged 6 commits into
developfrom
fix/try_improve_wasm_performance

Conversation

@zzcgumn

@zzcgumn zzcgumn commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Investigates and partially fixes the WASM performance regression flagged in
claude/analysis/clang_wasm.md. The analysis's primary hypothesis
(per-call SolverContext allocation) turned out not to be the actual
bottleneck once measured — but the fix is architecturally correct and kept
regardless. The secondary factor (Emscripten's legacy exception emulation)
turned out to be the one that actually moved the needle.

  • Session-scoped SolverContext in the browser MVPdds_mvp_calc_table
    used to construct and tear down a fresh SolverContext (and its
    transposition table) on every call. It now reuses one static SolverContext
    for the module's lifetime, calling reset_for_solve() between deals.
    Measured: no significant change (~93-94ms/call before and after) — the
    double-dummy search itself dominates the workload, not the per-call
    allocation. Kept anyway since it matches the previous session-singleton
    design and is the architecturally correct behavior for a long-lived module.
  • Native WASM exception handling — switched -fexceptions (JS-trampoline
    exception emulation) to -fwasm-exceptions (native wasm EH proposal) at
    compile and link time. Measured ~4% faster (90-91ms/call vs.
    93-94.5ms/call), consistent across runs.
  • -msimd128 was tried and reverted — no improvement (slight regression,
    ~92-93ms/call vs. 90-91ms/call without it).
  • Link-time -flto for WASM was tried and reverted — the pinned emsdk 5.0.7 toolchain ships a frozen, pre-built cache with only non-LTO system
    libraries; requesting link-time LTO needs LTO-bitcode variants of core
    sysroot libs (e.g. libprintf_long_double) that the frozen cache can't
    build inside Bazel's hermetic sandbox. Documented as a known gap rather than
    worked around.
  • Specs (web-mvp, wasm-emscripten, build-system) and docs/wasm_build.md
    updated to reflect the above.

What did not work (documented, not silently dropped)

Attempted Result
Session-scoped SolverContext reuse No measurable speedup on its own
-msimd128 Slight regression, reverted
WASM link-time -flto Blocked by hermetic toolchain cache, reverted

Test plan

  • bazel test //web:dds_mvp_wasm_test — includes a new cross-deal
    context-reuse regression test (FillsFlatStrainHandTableAcrossReuse)
  • bazel test //web:web_tests //web:web_system_tests
  • bazel test --test_tag_filters=e2e //web:dds_mvp_e2e_test (Playwright)
  • bazel test //wasm:all
  • bazel test //library/tests/... (sanity — no native API changes)
  • bazel build //...

@zzcgumn zzcgumn self-assigned this Jul 19, 2026
@zzcgumn
zzcgumn requested a review from tameware July 19, 2026 12:50
@zzcgumn

zzcgumn commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

@tzimnoch , this looks similar to your results. Small improvement which I think is worth merging.

@tameware tameware left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a good way to benchmark? Shall I have Cursor code one up, perhaps by adding a --wasm option to dtest?

Never mind - I had Cursor create a new wasm-dtest utility that wraps node calls. I have a PR out for that. When I have a chance I'll cherry pick this PR in and see what difference it makes.

@zzcgumn

zzcgumn commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

@tameware, I think it makes sense to merge the wasm-dtest utility first and rebase this on top of it. Hoping we can get some better data than what Claude Code was able to report.

@tameware

Copy link
Copy Markdown
Collaborator

Initial benchmarks show a 4.5% performance improvement over 100 calcs (2,000 solves). I'll run against 1,000 calcs next, to make sure.

@tameware

Copy link
Copy Markdown
Collaborator

Interesting! 100 calcs comparison was 9815 / 10274 ms, for 0.9553. For 1,000 calcs it's 126298 / 126993 = 0.99452726. Still faster, but only by 0.5%. I'll run a few more to make sure this is not just noise. More important, I've been comparing against develop head. That's wrong - I should be comparing the code before and after this commit.

@tameware

Copy link
Copy Markdown
Collaborator

Proper comparison now:

          100     1000   
Before:  9796   120840
After:   9685   119398

Speedup: 1.1%     1.9%

I'll run one more with a larger sample size, then approve if the results hold up.

@tameware

Copy link
Copy Markdown
Collaborator

Disappointing. Back to the drawing board?

          100     1000    10000
Before:  9796   120840  1267727
After:   9685   119398  1301136

Speedup: 1.1%     1.9%  -2.6%

I'll run a few more times to confirm.

@tameware

Copy link
Copy Markdown
Collaborator

I found and fixed an int32 overflow bug in the WASM dtest code, but it did not affect the results.

list10000.txt contains more than half duplicated deals, and it's possible that affected the results. I switched to running through list1000.txt 10 times. Clearly that also has duplicates, but they are not batched the way they are in list10000.txt.

My first run with the new setup was favorable to this PR:

Before  1245.39 seconds
After   1217.46 seconds
Speedup     2.2 %

zzcgumn and others added 5 commits July 23, 2026 08:32
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@zzcgumn
zzcgumn force-pushed the fix/try_improve_wasm_performance branch from 7fe479b to 8f6e046 Compare July 23, 2026 07:47
@tzimnoch

Copy link
Copy Markdown
Collaborator

Do the changes in the compared branches differ by the code to sort hands by difficulty? (#216) Is there any possibility that the cause of slowdown with increasing numbers of hands is that sorting the hands becomes more expensive than the threading gains as the number of hands increases?

          100     1000    10000
Before:  9796   120840  1267727
After:   9685   119398  1301136

Speedup: 1.1%     1.9%  -2.6%

I'd be surprised if this explains the difference, but it's not out of the realm of possibility. The results imply there's some non-linear-time operation happening in the After branch. Sorting is usually O(n log n).

Was there any evaluation of the cost to sort?

If it is expensive, a complete ordering might not be necessary. Setting a threshold such that the the top ~20% most difficult hands are solved before the bottom 80% (in any order) may be sufficient, which would be an O(n) operation.

@tameware

Copy link
Copy Markdown
Collaborator

Do the changes in the compared branches differ by the code to sort hands by difficulty? (#216) Is there any possibility that the cause of slowdown with increasing numbers of hands is that sorting the hands becomes more expensive than the threading gains as the number of hands increases?

Both branches I'm testing contain PR 216. The only difference is the changes in this PR.

I'd be surprised if this explains the difference, but it's not out of the realm of possibility. The results imply there's some non-linear-time operation happening in the After branch. Sorting is usually O(n log n).

The code works in batches of 40 deals. I cannot think the sort cost is significant, but it would be easy to measure.

Was there any evaluation of the cost to sort?

No. I benchmarked throughput before and after the change. Note that the difficulty sort replaced Scheduler code that was present in 2.9 but not used in 3.0.

If it is expensive, a complete ordering might not be necessary. Setting a threshold such that the the top ~20% most difficult hands are solved before the bottom 80% (in any order) may be sufficient, which would be an O(n) operation.

Agreed - if sorting is expensive then a partial order would be worth considering.

@tameware

Copy link
Copy Markdown
Collaborator

A second extensive test with no duplicates within batches shows similar improvement to the first. I'd say this PR is good to go.

Before  1267.73 seconds
After   1223.72 seconds
Speedup     3.5 %

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets a measured WASM performance regression by (1) making the Web MVP bridge reuse a long-lived SolverContext instead of re-allocating per call and (2) switching the Emscripten build to native WebAssembly exception handling, with accompanying documentation/spec updates to reflect the current WASM constraints (notably the lack of link-time LTO under the pinned hermetic emsdk).

Changes:

  • Reuse a module-lifetime static SolverContext in dds_mvp_calc_table, resetting between deals to recycle TT memory without reallocating.
  • Enable native WASM exception handling by using -fwasm-exceptions (and ensuring exceptions are enabled at compile-time via -fexceptions in WASM copts).
  • Add a regression test for cross-deal context reuse and update specs/docs to document the new behavior and toolchain limitations.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
web/dds_mvp_wasm.cpp Reuses a session-scoped SolverContext and routes table computation through the context-aware C++ API.
web/dds_mvp_wasm_test.cpp Adds a regression test verifying correct results across multiple solves using the reused context.
wasm_compat.bzl Switches shared Emscripten link flags from legacy exception handling to native wasm EH.
CPPVARIABLES.bzl Ensures WASM builds enable exceptions and select wasm EH lowering (-fexceptions + -fwasm-exceptions).
specs/web-mvp.md Documents the session-scoped context behavior and its concurrency implications.
specs/wasm-emscripten.md Documents the current “no link-time LTO” constraint under the pinned hermetic emsdk cache model.
specs/build-system.md Clarifies platform-specific optimization/LTO behavior, especially for WASM vs macOS.
docs/wasm_build.md Updates user-facing WASM flag documentation and adds the link-time LTO limitation note.

Comment thread docs/wasm_build.md
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@zzcgumn

zzcgumn commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

@tzimnoch or @tameware, could I please get an approval from either of you if you are happy for this to be merged.

@tameware

Copy link
Copy Markdown
Collaborator

I thought I had approved. The only active button I see now is "Merge pull request".

@zzcgumn
zzcgumn merged commit 7b4a6c7 into develop Jul 23, 2026
7 checks passed
@zzcgumn
zzcgumn deleted the fix/try_improve_wasm_performance branch July 23, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants