Benchmarks.
Per-cell history for causl-ts — the default pure-TypeScript
engine — against Jotai, Redux Toolkit, and MobX across the canonical
scenario taxonomy in causljs/causl-bench. Each card shows one
(library × scenario × scale) cell — median wall-time as the
solid line, p95 as the translucent band, run-by-run, with a
regression verdict computed from the latest two runs. The
causl-wasm Rust production engine Enterprise
is benchmarked separately under a two-engine comparison; that data
and its analysis ship with the Enterprise
wasm-performance guide.
Per-cell median + p95 + verdict.
The page first tries ./history.json; if the
nightly publish hasn't landed yet it falls back to
./history.sample.json, a checked-in multi-run
stub so the dashboard is never empty. The “source” cell in the
meta strip below tells you which one is showing.
Each cell carries a profile link that points at
./profiles/latest/<library-scenario-scale>/.
Those directories are populated by the nightly
perf-snapshot workflow, which is currently parked
in .github/workflows-disabled/perf-snapshot.yml
until GitHub Actions is re-enabled.
The links remain useful as discoverable placeholders so the
dashboard surface doesn't change shape when CI is restored.
New sample field: boundaryCrossings (Phase L1) Enterprise
Each causl-wasm Enterprise sample now carries a boundaryCrossings
integer counting how many times the scenario crossed the JS↔WASM bridge during
the timed region. causl-ts samples carry 0 (no bridge).
The per-cell history charts will gain a boundary-crossings sparkline in a
follow-up; today the value is queryable in
history.json directly. The full
boundary-tax analysis is an Enterprise topic — see the
wasm-performance guide.
Charting library — vanilla SVG (decision log)
D3 (~70 KB minified) and Plotly (~3.5 MB minified) were both
evaluated as upgrades over the inline-SVG MVP. We stayed on
vanilla SVG and brought the visible upgrades —
cubic-Hermite (Catmull-Rom α=0.5) line smoothing, D3-style
"nice" axis ticks at 1/2/5 × 10k, per-point hover
tooltips covering every run — directly into dashboard.js.
The trade we declined: a lazy-loaded D3 bundle would have
added ~70 KB of parse/compile cost to a page whose only
interactive surface is a small per-cell sparkline grid.
Lighthouse Performance budgets penalise any third-party JS
that touches the main thread post-paint, and Plotly is
two orders of magnitude over budget. The visible upgrades
the issue wanted were ~80 lines of inline math each, not a
framework dependency. The dashboard ships with zero external
runtime dependencies; the <noscript> block
below points no-JS readers at the raw history JSON.
Engine comparison: causl-ts vs causl-ts-wasm-engine Enterprise
Side-by-side per-scenario perf envelope under two engine
implementations. The upstream causl-ts series is the
default pure-TypeScript engine; the
causl-ts-wasm-engine Enterprise series is the
fork wired to the consolidated WASM bridge from
causljs/causl-wasm. Toggle the source per bench run with
CAUSL_TS_SOURCE=upstream|wasm-engine in
causljs/causl-bench.
The JS↔WASM crossings column is the star metric — it counts how many times each
scenario rang the WASM bridge on this run. causl-ts is always 0 (no boundary);
causl-ts-wasm-engine should be > 0 on every cell. The wall-time delta between
engines is dominated by boundary cost per crossing × number of crossings.
| Scenario | causl-ts | causl-ts-wasm-engine Enterprise | JS↔WASM crossings + payload bytes min/median/max |
wall (ms) | CPU user (ms) | CPU sys (ms) | heap peak (B) | hot path | cold path |
|---|---|---|---|---|---|---|---|---|---|
Loading dual-engine.json from causljs/causl-bench… | |||||||||
Why some (scenario × library) cells are absent.
Some cells in the comparison table are absent because of
structural limits in the comparator library, not because
the scenario was selected to disadvantage it. Where a
comparator can run a cell, its result is published as-is;
where it cannot, the structural reason is stated and the
cell renders as (skipped — …) with a
reason string after #1293.
The fairness-audit discipline
(docs/bench/2026-05-12-fairness-audit.md)
classifies every skip into one of the three buckets below.
Recursive read-path overflows V8 at linear-chain × 10000.
Affected cells: linear-chain × 10000 for
jotai, mobx, redux-toolkit.
Causl reports a real median here post-#956.
Each comparator's read path evaluates the chain via mutual
recursion through its track/get entry point, so the V8 call
stack overflows at depth 10 000. The harnesses throw
a typed RecursiveEvalStackOverflowError
up-front (see packages/bench/src/libraries/_expansion-stub.ts)
so the doomed setup never runs and the tolerant runner
records a clean skip with the per-cell reason string.
Causl's Phase-D driver is iterative end-to-end after
#956:
the registration walk and the commit-time fix-point both run
through an explicit StackFrame[] rather than
recursive calls. SPEC §5.1 Amendment 3 pins this as an
implementation-detail trail with no contract change —
every §3 atomicity and §5.1 phase invariant is
preserved byte-identically; only the depth ceiling moves.
The asymmetry is structural to the JS engine shape, not the
workload, and tracked under
#721 part 3
(mobx investigation) /
#798 /
#922 /
#926.
Scenario's acceptance gate cannot be expressed in the comparator's public surface.
Affected cells:
commit-firehose-1000-subs,
multi-fetch-race-N10,
and op-wasm-boundary-1k — each skipped for
jotai, mobx, redux-toolkit.
These scenarios require commit-metadata semantics
(subscribeReads vs subscribeCommits
split — SPEC §5.1 Phase F),
staleness-aware async (a commit-seq surface so a late fetch
result can detect that a newer write has landed — see
SPEC.async.md),
or the WASM-FFI boundary contract (SPEC §17.6) Enterprise.
None of the three comparator libraries have an equivalent
public-API surface, so the cell's acceptance gate is not
architecturally expressible.
The harness throws a typed
ExpansionScenarioNotImplementedError with a
reason string identifying the public-API gap — deferred
explicitly under #843
rather than implementing a cell that runs but cannot answer
the question the scenario was constructed to ask
(the “loud silence” failure mode).
Catch-all for typed skips that fall outside Classes 1 and 2.
Any cell where the harness throws
ExpansionScenarioNotImplementedError for a
per-cell reason that does not reduce to a structural stack
overflow or to a public-API gap. None are currently active
in the published comparison table — every entry today
falls into Class 1 or Class 2 — but the bucket exists
so future expansion scenarios with library-specific gaps
have a documented home rather than appearing as a silent
drop. The per-cell reason string in
comparison_table.md always names the gap; this
class is the residual category.
Three axes, picked because they fail in the field.
Microbenchmarks lie about real workloads, so each axis is anchored to a scenario that already exists as a runnable demo in the repository. No synthetic loops without a corresponding adopter shape.
Time-to-coherent-state.
Spreadsheet-diamond and linear-chain scenarios from
packages/bench/scenarios/ commit a single
root edit and measure wall-clock time until every
dependent has settled to its glitch-free value.
Reported as median + p95 across runs, library by library.
Full-import @causl/core.
Measured against the SPEC §14.2 budget — the 15–30 KB
band with an 18 KB working target — using the same
size-limit configuration that already gates
every PR. Surfaced via bundle-table.ts and
will land on this page in a follow-up.
Bounded enumerator under load.
The BFS frontier / traces / races caps are swept across
cap settings, plotting
states_visited versus wall-clock so adopters
can pick a cap without guessing.
For the full methodology behind these numbers — the SPEC §17.5 capability-cost residual band and how to run the bench harness against your own workload — read the Performance usage guide. Opting into the WebAssembly backend Enterprise and its benchmark story are covered in the Enterprise wasm-performance guide.