datalogic-bench
August 25, 2026 · View on GitHub
Dev-only benchmark harness for datalogic-rs. For the latest captured
matrix and headline numbers, see BENCHMARK.md — link
to that file from other docs rather than re-quoting cells inline.
Four binaries share a common suite loader and reporter (src/lib.rs):
| Binary | Purpose |
|---|---|
self | Times datalogic-rs alone using the fast arena path (compile once, persistent input arena, eval-arena reset). Use this to track regressions in our own engine. |
compare | Cross-library matrix: runs every suite against every available subject (datalogic-rs API tiers, gated Rust crates, JS/WASM via Node) and prints a markdown table of avg ns/op. |
boundary_core | The rust-core runner for the per-binding boundary benchmark under boundary/; emits the same JSON-lines schema as the other runtimes' runners. |
profile_macro | Sampling-profiler feeder (samply / Instruments): hammers one macro suite in a hot loop so the profile shows only that suite's evaluation path. |
A third area, boundary/, measures the opposite of the
matrix: per-binding boundary cost — what a real caller pays per
evaluation through each language binding (C ABI, Node, Python, WASM,
Go, JVM, .NET, PHP) on the three workloads from
BINDINGS-OVERHEAD.md. One runner per
runtime, one shared discipline (warmup, ~250 ms samples, median of 5),
JSON-lines output, and a renderer for that document's tables:
cd boundary && ./run.sh && python3 render.py
See boundary/README.md for runner status:
all nine runners are verified and produced the 2026-07-03 v2 capture;
go / dotnet / jvm / php sit in the extended set only because they need
their language toolchains installed. The boundary_core bin target in
this crate is the rust-core runner.
Both read JSON suites from crates/datalogic-rs/tests/suites/, and both
accept --macro to swap those for the synthesized macro suites instead.
Both write JSON reports to tools/benchmark/output/ (gitignored):
report-self-*.json for --all runs of self, report-compare-*.json
for compare, report-compare-macro-*.json for compare --macro.
self — regression baseline
# Single suite (compatible.json by default)
cargo run --release -p datalogic-bench --bin self
# All suites
cargo run --release -p datalogic-bench --bin self -- --all
# Specific suite
cargo run --release -p datalogic-bench --bin self -- arithmetic/plus.json
# Macro tier: synthesized large-payload suites (1k/10k arrays, 128-key
# objects, 48-level nesting, 10 KB strings, one eligibility rule)
cargo run --release -p datalogic-bench --bin self -- --macro
Each suite is timed three ways with the same discipline (median of 3
reps, black_box, session reset per iteration, pre-sized arena): the
whole suite (the headline, comparable with older reports), just the
rules the compiler constant-folded to a literal (Logic::is_constant),
and the rest. The per-suite line shows the split
(folded 23/32 @ 2.94 ns, rest @ 75.75 ns) and the summary reports
overall / folded-only / non-folded-only geomeans, so constant-folded
rules can't flatter the data-dependent number. The macro tier scales
its per-suite iteration count from a pilot pass so one timed rep lands
near ~250 ms; see BENCHMARK.md for the
suite list.
compare — cross-library matrix
The matrix has one row per suite and one column per subject. Cells are the median ns/op of three timed samples, each sized to hit a ~200ms wall budget. Two aggregation rows at the bottom show the arithmetic mean (familiar) and geometric mean (the right average for cross-library comparison — one slow suite doesn't dominate).
Subjects
The matrix shows one column per library / API tier that takes a
precompile-once approach — apples-to-apples cells. Convenience-API
tiers (Engine::eval_str, Session::eval_borrowed, raw
evaluate(ruleStr, dataStr, false) on the WASM) are intentionally not
in the matrix because their numbers measure API-shape costs (parse cost,
session reset cost, WASM string marshalling) rather than engine cost.
For per-API-tier numbers on datalogic-rs alone, see bin/self.rs.
Always compiled in:
| Column | What it exercises |
|---|---|
dlrs:engine | Pre-compiled Logic + caller-owned Bump, batch-style reset between iterations. The native baseline. |
Behind a Cargo feature:
| Column | Feature flag | Crate |
|---|---|---|
jsonlogic-rs | subject-jsonlogic-rs | bestowinc/json-logic-rs 0.5 — apply(&Value, &Value), no compile API |
Auto-detected at runtime (require Node + an npm install in runners/):
| Column | API exercised |
|---|---|
dlrs:wasm:compiled | @goplasmatic/datalogic-wasm new CompiledRule(ruleStr, false) once per rule, then .evaluate(dataStr) per call. WASM analog of dlrs:engine; remaining per-call cost is data marshall + parse + result stringify across the V8↔WASM boundary. |
json-logic-js | json-logic-js (jwadhams) — apply(rule, data), interpreted, no compile API. |
json-logic-engine | json-logic-engine (TotalTechGeek) — interpreted (engine.run(rule, data)). |
json-logic-engine:compiled | json-logic-engine — pre-compiled (engine.build(rule), "12.5–20× hot path" per the library's README). |
json-logic-engine and json-logic-engine:compiled share their npm
package but exercise different APIs (interpreter vs build-then-call).
One-time setup for Node subjects
# Build the WASM that the dlrs:wasm column points at:
cd bindings/wasm && ./build.sh
# Install the runner deps (json-logic-js + a file: link to the wasm pkg):
cd tools/benchmark/runners && npm install
If node isn't on PATH or runners/node_modules/ is missing, the
matrix runner hard-fails by default (the surprise of "complete-looking
matrix with silently-empty columns" is worse than an explicit error).
Pass --allow-missing-subjects to render the matrix without the
unavailable columns.
Run
# Single suite (compatible.json by default)
cargo run --release -p datalogic-bench --bin compare
# Specific suite
cargo run --release -p datalogic-bench --bin compare -- arithmetic/plus.json
# Every suite from tests/suites/index.json
cargo run --release -p datalogic-bench --bin compare -- --all
# Synthesized macro suites (large payloads) across all subjects; report
# lands in output/report-compare-macro-<timestamp>.json
cargo run --release -p datalogic-bench --bin compare -- --macro
# With the gated Rust competitor
cargo run --release -p datalogic-bench --bin compare \
--features subject-jsonlogic-rs -- --all
# Allow rendering even when Node subjects aren't installed
cargo run --release -p datalogic-bench --bin compare -- --all --allow-missing-subjects
Reading the output
=== Cross-Library Matrix — avg ns/op (median of 3, ~200ms target/cell, 50 suites) ===
| Suite | dlrs:engine | jsonlogic-rs | dlrs:wasm:compiled | json-logic-js | json-logic-engine | json-logic-engine:compiled |
|----------------------|------------:|-------------:|-------------------:|--------------:|------------------:|---------------------------:|
| arithmetic/plus.json | 2.8 | 224.4* | 518.6 | 393.4* | 73.0 | 22.6 |
...
| arithmetic mean | ... | ... | ... | ... | ... | ... |
| geometric mean | ... | ... | ... | ... | ... | ... |
* partial coverage — subject errored on some cases in this suite.
- Numbers are nanoseconds per evaluation (lower is better).
—= subject unavailable for this run (feature off, runtime missing, or precompile failed for the suite).ERR= subject ran but errored on >50% of cases in the suite.- A trailing
*on a number = subject errored on some cases in the suite but completed enough that ns/op is still meaningful. - Negative-test cases (entries with
error: {...}instead ofresult) are filtered out of compare runs — engines disagree on what "errors" and how expensive their error path is, so including them would unfairly penalise verbose-error subjects.
After the matrix, a pairwise ratio table is printed:
=== Pairwise shared-suite ratios ===
json-logic-engine:compiled 9.4x slower than dlrs:engine over 3 shared suites
...
Each line is the geomean of per-suite ns/op ratios computed only over
suites where both subjects have finite cells. The per-column mean
rows in the matrix cover different suite subsets when subjects ERR on
different suites, so quotients of column geomeans mix incomparable
sets; the pairwise ratios never do. Matrix cells, per-column means, and
these ratios are also written to output/report-compare-<timestamp>.json
(output/report-compare-macro-<timestamp>.json for --macro runs).
Native-CPU build (optional, host-only numbers)
A .cargo/config.toml inside tools/benchmark/ adds
-C target-cpu=native. Cargo only picks this up when the cwd is at or below
the benchmark crate, so it's opt-in by location:
cd tools/benchmark
cargo run --release --bin compare -- --all
Numbers from a native build are not portable across machines — keep them as a relative baseline, not an absolute publishable figure. Builds invoked from the repo root remain portable.
Adding more subjects
Native Rust crate
- Add an optional dep + a Cargo feature in
tools/benchmark/Cargo.toml:[dependencies] my-jsonlogic = { version = "X.Y", optional = true } [features] subject-my-jsonlogic = ["dep:my-jsonlogic"] - Add a
Subjectimpl insidebin/compare.rs, gated by#[cfg(feature = "subject-my-jsonlogic")]. Mirror the pattern ofJsonLogicRs— pre-parse rule and data once, timeapply()only. - Push the subject into
build_subjects()(also gated). - Run with
--features subject-my-jsonlogic.
JS / WASM library (via Node subprocess)
cd tools/benchmark/runners && npm install <pkg>.- Add a
LIBSentry inrunners/node-runner.js— one asyncsetupthat returns a callableapply(case). - In
build_subjects()insidebin/compare.rs, push a newNodeSubject::new("display-name", "<npm-pkg>")(gated onnode_dep_installed("<npm-pkg>")).
That's the entire recipe — three files each, no harness changes.
Platform support
Linux and macOS. The Node runner uses POSIX path conventions in
file:../../../bindings/wasm/pkg and the runners/ setup is
shell-coded; Windows isn't tested.
CI
Don't run compare in CI. WASM build + npm install + 3+ minutes of
matrix work makes for flaky CI runs. self is the regression-tracking
target — keep CI on cargo test --workspace --all-features plus a
single-suite self invocation if you want a perf signal.