Performance Benchmarks¶
What we measure¶
Rank |
Command / artifact |
Answers |
|---|---|---|
1 (CI) |
|
Instruction-count deltas on fixed CON parse / skip / write / float paths |
2 (PR) |
|
Python surface wall-time on PR vs base |
3 |
|
Same CON text vs ASE |
4 |
|
Equal-geometry wall times: ASE XYZ/extXYZ/CON vs readcon CON |
5 |
|
ASE |
6 |
|
MDAnalysis H5MD / h5py positions vs CON / chemfiles XYZ |
local |
|
Rust microbench latency; optional PR job saves baselines |
Regression gates in CI:
Cachegrind I-refs — path-filtered + weekly job (
ci_cachegrind.yml); commitsdocs/source/_generated/cachegrind_results.*onmainwhen counts change.Python ASV + spyglass — every PR (
ci_benchmark.yml); comment via asv-perch.
Peer scripts under benches/ are for local re-runs. JSON under
benches/results/ is what you produce when you run them — this page does not
embed host wall-clock tables.
CI Cachegrind (instruction-count gate)¶
Workflow ci_cachegrind.yml runs Valgrind Cachegrind on
examples/cachegrind_harness.rs when CON-path sources (or this doc) change on
main, on a weekly schedule, or on manual dispatch. With
CACHEGRIND_FEATURES=chemfiles it also records conversion scenarios.
Current committed include (refreshed by that job):
Scenario |
I refs |
Notes |
|---|---|---|
|
6,442,260 |
2-frame multi CON (50×) |
|
4,919,716 |
forward() skip (50×) |
|
7,270,084 |
coords + velocities (50×) |
|
29,049,995 |
100× tiny_cuh2 (10×) |
|
6,891,164 |
skip 100 frames (10×) |
|
12,678,002 |
218-atom frame (20×) |
|
20,002,480 |
5-col fast-float2 (10k) |
|
32,902,970 |
5-col str::parse (10k) |
|
35,128,770 |
buffer writer (10×) |
|
8,758,445 |
XYZ path → ConFrame (50×) |
|
8,535,762 |
XYZ memory → ConFrame (50×) |
|
6,044,440 |
selection |
Generated 2026-08-15T20:57:20Z from commit 2bd9cda
(Cargo features: chemfiles-from-sources). Metric: Valgrind Cachegrind I refs.
Lower is better for the same scenario. Comparable across commits on the same CI image/Valgrind.
Wall-clock Criterion tables elsewhere on this page are illustrative unless re-run for a release.
PR workflow Benchmark PR still uses Criterion + critcmp for latency deltas.
Reproduce (needs Valgrind):
scripts/run_cachegrind_bench.sh
# writes docs/source/_generated/cachegrind_results.{json,rst}
I-refs for a fixed binary are stable across noisy shared runners; wall-clock medians on GHA are not. That is why Cachegrind is the commit-stable gate and ASV is the PR Python surface compare.
Python ASV (PR surface)¶
Workflow Benchmark PR (ci_benchmark.yml):
Matrix builds base and PR with
maturin develop --features python,chemfiles --release.Runs ASV on
benchmarks/with-E existing:$(which python)(no re-install mid-bench).asv-spyglass comparewritesresults/comparison.txt(job fails if empty).ci_bench_commenter.ymlposts that file via asv-perch.
A separate criterion-benchmark job may save Criterion baselines; it uses
continue-on-error: true so a Rust microbench miss does not fail the workflow
conclusion that asv-perch requires.
Suite (calls shipped readcon only):
Class |
API timed |
|---|---|
|
|
|
|
|
|
|
|
Local:
maturin develop --features python,chemfiles --release
# uv pip install asv numpy ase
asv machine --yes
asv run -E "existing:$(which python)" --set-commit-hash "$(git rev-parse HEAD)" \
--record-samples --quick
uvx --from "git+https://github.com/airspeed-velocity/asv_spyglass.git" \
asv-spyglass compare .asv/results/*/<base>*.json .asv/results/*/<pr>*.json \
.asv/results/benchmarks.json \
--label-before main --label-after pr
Config: asv.conf.json. Results: .asv/ (gitignored).
Peer scripts (local)¶
Run on your machine; write JSON under benches/results/ if you want a citeable
artifact. Ordering vs ASE / C sscanf / MDA is a property of that run, not a
global ranking.
# Same CON text vs ASE ase.io.eon and eOn-style C sscanf
uv run --with matplotlib --with numpy --with ase python benches/compare_readers.py
# Equal-geometry multi-frame: ASE XYZ/extXYZ/CON vs readcon CON
python benches/multiformat_traj.py --fixtures cuh2 --ladder 100 --repeats 5 \
--out benches/results/multiformat_traj.json
# ASE .traj / NetCDF / multi-frame XYZ vs chemfiles→CON vs native CON
maturin develop --features python,chemfiles --release
python benches/ase_traj_vs_con.py --frames 100 --repeats 5
# H5MD via MDAnalysis / h5py vs CON (uncompressed + gzip + optional zstd size/load)
uv pip install MDAnalysis h5py ase numpy
python benches/h5md_vs_con.py --frames 100 --repeats 5
# writes h5md_size_bytes, con_size_bytes, con_gz_size_bytes, con_zst_size_bytes,
# readcon_con_ms, readcon_con_gz_ms, readcon_con_zst_ms (null if zstd feature off)
Implementation details that show up in Cachegrind and peer scripts (facts about the code, not a promise about every host):
Atom floats: fast-float2 (
float_fast_float2vsfloat_std_parseCachegrind scenarios)Line views: zero-copy over the input buffer (
MemchrLines)Atom vectors: sized from the CON header before filling
File load:
read_to_stringbelow 64 KiB, mmap at/above (MMAP_THRESHOLDinsrc/compression.rs)Multi-frame parallel parse: Rayon when buffer ≥ 48 KiB and
parallelis on (PARALLEL_BYTES_THRESHOLDinsrc/iterators.rs)Skip without atoms:
forward/forward_fast(Cachegrindforward_*)
h5py position/value alone loads a coordinate array (not cell / constraints /
atom_id / JSON). benches/h5md_vs_con.py times MDAnalysis H5MD and h5py as
peer load paths on equal geometry, and reports **CON.gz / optional CON.zst**
sizes and load times next to uncompressed CON. Gzip of multi-frame CON is often
the same order as positions-only H5MD for near-repeated rare-event frames; that
is a size fact, not advice to store campaigns as H5MD instead of CON +
readcon-db. Compressed CON always fully decompresses before parse (no mmap);
uncompressed ≥ 64 KiB uses memmap2.
Plots under docs/orgmode/img/ are produced by benches/make_plots.py from
JSON you generate; regenerate after peer runs if you need them current.
Parsing throughput (from peer JSON via make_plots.py)¶
Feature coverage vs parse speed (from peer JSON via make_plots.py)¶
Public API model and hot path¶
**Load (full frames)**
read_all_frames/ConFrameIterator/ Pythoniter_con/read_first_frameSkip payload:
count_frames/forward_fastwhen atoms are not neededCoordinates on a loaded frame: SoA / Python
coords_array()No separate public “coords-only” trajectory load
**Python**
Multi-frame parse paths release the GIL via py.detach (src/python.rs) around
the Rust work; iter_con yields full frames.
Criterion (local Rust latency)¶
cargo bench runs benches/iterator_bench.rs. Prefer Cachegrind for I-ref
regressions and ASV for the Python PR surface. The PR Criterion job is optional
artifact collection only; the posted comment is ASV/spyglass.
Memory¶
Peak RSS depends on host and whether all frames are materialised. Streaming:
let iter = ConFrameIterator::new(&contents);
for result in iter {
let frame = result?;
// process, then drop
}
Per frame the library keeps type symbols as shared Arc<str> (per type, not
per atom), pre-sized atom storage from headers, and parses floats from borrowed
line slices (no intermediate atom-line String).
Peak RSS plot (from historical peer JSON; re-run make_plots.py to refresh)¶
Feature matrix plot¶
CON v2 feature matrix vs common formats (make_plots.py)¶
CON v2 on the wire can carry positions, velocities, forces, unit cell,
per-direction constraints, atom_id, structured JSON, compression, multi-frame
concatenation, and streaming iteration — see The CON File Format Specification.
Statistical analysis (optional)¶
bayescomp can fit Gamma-family models
to Criterion JSON and compare_readers.py timings when you want credible
intervals; it is not part of the PR gate.
Reproduce (one place)¶
# Python ASV (PR surface)
maturin develop --features python,chemfiles --release
asv machine --yes
asv run -E "existing:$(which python)" --set-commit-hash "$(git rev-parse HEAD)" \
--record-samples --quick
# CON peers / multi-format / traj / H5MD — see Peer scripts above
# Plots from your JSON
uv run --with matplotlib --with numpy python benches/make_plots.py
# Cachegrind I-refs (needs Valgrind)
scripts/run_cachegrind_bench.sh
# Rust Criterion
cargo bench