Skip to content

fix(web-client): cache the precompile prover preprocessed table in the ST wasm build - #319

Open
WiktorStarczewski wants to merge 6 commits into
nextfrom
wiktor/st-precompiles-prover-std
Open

WiktorStarczewski wants to merge 6 commits into
nextfrom
wiktor/st-precompiles-prover-std

Conversation

@WiktorStarczewski

@WiktorStarczewski WiktorStarczewski commented Aug 25, 2026 •

Copy link
Copy Markdown
Collaborator

The ST wasm build resolved miden-precompiles-prover without std, so every prove/verify that raises a precompile claim (e.g. an ECDSA-authenticated tx) rebuilt the preprocessed STARK bundle from scratch; a feature-only direct dep now flips std on, so the ST build caches it like the MT build already does. Closes #318.

  • Repeat ECDSA proofs in the same instance drop ~0.3–0.4s (~6–8% of a ~5s proof); first proof unchanged; Falcon control flat.
  • ST bundle size unchanged (19,877,642 → 19,867,096 bytes, −10.5KB).
  • MT resolution unchanged (cargo tree before/after diff empty); miden-processor stays without std, so the threaded trace-build wasm trap stays uncompiled.
  • Trade-off: the cached bundle is retained for the wasm instance's lifetime — ~50 MiB per populated hash-config slot, two slots reachable (Poseidon2 via the client's default prover, Blake3 via TransactionProver.newLocalProver()), so ~100 MiB worst case. Disclosed in the CHANGELOG. Wasm linear memory never shrinks, so the pre-fix per-prove transient build already grew the heap by the same ~50 MiB and browsers never returned it; only an app exercising both prover paths can see a higher peak than before.
  • A make check-wasm-features guard (run in make lint and the clippy-wasm CI job) asserts the ST graph resolves exactly one miden-precompiles-prover and that it carries std — the fix's only observable effect is latency, so without this a dep bump could silently regress it.
  • New scripts/bench-precompile-cache.mjs reproduces the measurement (in-browser mock chain, no network); not wired into CI.
  • Drive-by: the release_wasm paths filter now includes the manifests, so a features-only Cargo.toml edit can't skip the release verify job.
Measurement (headless Chromium, Apple-silicon Mac, full-optimization ST builds, 3 pages × 3 proves)
ECDSA cold median warm median cold−warm
before 5110ms 4788ms 322ms
after 5303ms 4720ms 582ms

Before, cold≈warm — every proof pays the rebuild (the 322ms is generic JIT/memory warmup; the Falcon control shows the same shape). After, warm proofs additionally skip the table build. Tracing spans agree: the precompile-session prove_stark drops 3064ms → ~2660ms on warm proofs after (−400ms) vs −200ms before, and the build_session/build_trace spans — present only when a claim is raised — confirm the ECDSA tx exercises the precompile path while Falcon runs never emit them.

Repro: node crates/web-client/scripts/bench-precompile-cache.mjs <dist-st-dir> [--spans].

Binary verification (`strings -a` counts, raw release wasm)
marker before after
OnceLock 1 6 — one new static per cached_preprocessed! hash config
parking_lot 0 435 — compiles only under the new std chain
preprocessed_cache 12 48
execute_and_build_trace_sync 0 0 — processor std stays off (acceptance criterion 4)
ace_codegen 0 0

The issue's ace_codegen proxy stays 0: mod ace is pub(crate) and nothing on the wasm path calls it, so the linker DCEs it even with the feature on. The OnceLock/parking_lot markers plus the warm-proof drop are the std-is-on evidence instead (the criterion's alternate wording).

Safety: why `miden-precompiles-prover/std` and not `miden-prover/std`

miden-prover/std implies miden-processor/std, which compiles execute_and_build_trace_sync (std::thread::scope) — the known wasm trap. miden-precompiles-prover has no processor/prover dependency, so its std cannot arm it. Everything it enables transitively (miden-ace-codegen, miden-air/std, miden-core/std→parking_lot, …) is already live in the shipped MT artifact. Cargo.lock diff is the single dep edge; no version moved.

Full chromium suite green against the fixed dist (266 passed). The one failure, no_wasm_reentry_via_tojson, fails identically on an unmodified next build locally and is green on next's CI — pre-existing local-env quirk, unrelated.

Upstream option 2 (cache in the no_std branch too, via miden-utils-sync::OnceLockCompat) remains open and complementary — this PR doesn't block it.

Reviewers: the measurement table and the retained-memory trade-off are the parts worth your time — the win is real but modest (~0.3–0.4s per repeat proof at zero size cost); if you judge that not worth a dependency edge, closing per the issue's "negligible → say so" clause is a legitimate call.

F-001 P1  the rc.3 heading was renamed rather than superseded, reattributing
          five shipped entries to the unreleased rc.4 and erasing rc.3
F-002 P1  the new bench script's `/index.js` import failed the knip CI job
F-003 P1  unvalidated bench flags exited 0 having measured nothing, or crashed
          in the summary after completing every proof
F-004 P1  any variant but exactly "ecdsa" silently benchmarked the Falcon
          control while keeping the caller's label
F-005 P2  release_wasm paths filter still missed manifests that change the
          shipped WASM without moving Cargo.lock
F-006 P2  bench server leaked stream errors, lost post-startup server errors,
          and let teardown mask the benchmark's own failure
F-007 P2  manifest comment understated the std flip: it reaches nine more
          crates and links parking_lot into the no-atomics ST build
F-009 P2  dropped the unmeasured "bundle size is unchanged" claim
F-012 P3  page errors were logged but still produced a recorded result
F-018 P2  add `make check-wasm-features`: nothing failed if the std feature
          silently came back off, since the change's only effect is latency.
          Asserts the ST wasm graph resolves exactly one
          miden-precompiles-prover and that it carries `std`. Verified to fail
          against the pre-fix tree, not just to pass against this one.
F-020 P1  the changelog published "~0.3-0.4s (~6-8%)" from a cold-vs-warm
          measurement that cannot separate the cache from first-prove warm-up.
          A controlled before/after build shows warm ECDSA proofs at 5009ms vs
          4989ms and 4836ms vs 4988ms across two runs — inside run-to-run
          variance — and the Falcon control, which raises no precompile claim,
          shows the same cold-warm delta. Replaced with a device-dependent
          statement and the measured memory cost.
F-014 P2  document the retained cost: ~50 MiB per proving hash function, one
          slot in practice, with no eviction API
F-019 P2  wire the benchmark into a `bench-precompile-cache` Make target; it
          was referenced from nothing
F-016 P3  close keep-alive sockets so bench teardown cannot stall
…en the feature guard

F-023 P2  manifest and CHANGELOG named Blake3_256 as the cached slot for the
          common case; the zero-config submitNewTransaction path proves with
          None and falls through to miden-client's LocalTransactionProver::
          default(), which is ProvingOptions::new(Poseidon2). Blake3 is only
          the explicit newLocalProver() path, so an app doing both retains
          two caches (~100 MiB), not one
F-024 P2  check-wasm-features called plain `cargo`, which resolves to the
          rust-toolchain.toml pin and would make CI download a second
          toolchain the clippy-wasm job does not otherwise install
F-025 P2  per-iteration context.close() could throw and replace the error the
          iteration was already unwinding with
F-026 P2  a misspelled flag was silently ignored, so the bench reported the
          default config as if it were what was asked for
F-027 P2  bench header still presented same-instance cold-vs-warm as the
          cache measurement, contradicting the Makefile and CHANGELOG
F-028 P2  CHANGELOG's "per proving hash function" read as up to five caches
F-029 P3  workspace stanza comment claimed to flip `std`; it does not
F-030 P3  `|| exit 1` was dead after a command substitution
F-031 P3  miden-prover/std vs miden-processor/std attribution was imprecise
          and omitted that MT already ships both
F-032 P3  dropped a speculative "matters most on slower hardware" claim
F-034 P3  guard now checks the real ST feature set, and the bench fails fast
          on a missing dist instead of after a ten-second browser launch
F-037 P3  path-filter comment implied js-export-macro compiles into the WASM
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.

1 participant