======================
Performance Benchmarks
======================
What we measure
---------------
.. table::
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| Rank | Command / artifact | Answers |
+========+============================================================================+==========================================================================+
| 1 (CI) | ``examples/cachegrind_harness.rs`` via ``scripts/run_cachegrind_bench.sh`` | Instruction-count deltas on fixed CON parse / skip / write / float paths |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| 2 (PR) | ``benchmarks/`` + ``asv.conf.json`` (ASV) → ``asv-spyglass compare`` | Python surface wall-time on PR vs base |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| 3 | ``benches/compare_readers.py`` | Same CON text vs ASE ``ase.io.eon`` and eOn-style C sscanf |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| 4 | ``benches/multiformat_traj.py`` | Equal-geometry wall times: ASE XYZ/extXYZ/CON vs readcon CON |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| 5 | ``benches/ase_traj_vs_con.py`` | ASE ``.traj`` / NetCDF / XYZ vs ``readcon.read_chemfiles`` vs native CON |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| 6 | ``benches/h5md_vs_con.py`` | MDAnalysis H5MD / h5py positions vs CON / chemfiles XYZ |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
| local | ``cargo bench`` (Criterion) | Rust microbench latency; optional PR job saves baselines |
+--------+----------------------------------------------------------------------------+--------------------------------------------------------------------------+
Regression gates in CI:
- **Cachegrind I-refs** — path-filtered + weekly job (``ci_cachegrind.yml``); commits
``docs/source/_generated/cachegrind_results.*`` on ``main`` when 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):
.. include:: _generated/cachegrind_results.rst
Reproduce (needs Valgrind):
.. code:: shell
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``):
1. Matrix builds **base** and **PR** with
``maturin develop --features python,chemfiles --release``.
2. Runs `ASV `_ on ``benchmarks/`` with
``-E existing:$(which python)`` (no re-install mid-bench).
3. ``asv-spyglass compare`` writes ``results/comparison.txt`` (job fails if empty).
4. ``ci_bench_commenter.yml`` posts 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):
.. table::
+----------------------+---------------------------------------------------------------------------+
| Class | API timed |
+======================+===========================================================================+
| ``TimeReadConTiny`` | ``read_con_string`` / ``read_con`` / ``iter_con`` on multi-frame tiny CON |
+----------------------+---------------------------------------------------------------------------+
| ``TimeReadConCuh2`` | ``read_con`` on multi-frame ``cuh2.con`` (218 atoms/frame) |
+----------------------+---------------------------------------------------------------------------+
| ``TimeChemfilesXyz`` | ``read_chemfiles`` multi-frame XYZ → ``ConFrame`` (skipped if lean) |
+----------------------+---------------------------------------------------------------------------+
| ``TimeSelectAtoms`` | ``frame.select_atoms("name Cu")`` (skipped if lean) |
+----------------------+---------------------------------------------------------------------------+
Local:
.. code:: shell
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/*/*.json .asv/results/*/*.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.
.. code:: shell
# 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_float2`` vs ``float_std_parse`` Cachegrind scenarios)
- Line views: zero-copy over the input buffer (``MemchrLines``)
- Atom vectors: sized from the CON header before filling
- File load: ``read_to_string`` below 64 KiB, mmap at/above (``MMAP_THRESHOLD`` in
``src/compression.rs``)
- Multi-frame parallel parse: Rayon when buffer ≥ 48 KiB and ``parallel`` is on
(``PARALLEL_BYTES_THRESHOLD`` in ``src/iterators.rs``)
- Skip without atoms: ``forward`` / ``forward_fast`` (Cachegrind ``forward_*``)
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.
.. figure:: img/parsing_throughput.svg
Parsing throughput (from peer JSON via ``make_plots.py``)
.. figure:: img/pareto_features_vs_speed.svg
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`` / Python ``iter_con`` / ``read_first_frame``
- Skip payload: ``count_frames`` / ``forward_fast`` when atoms are not needed
- Coordinates 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:
.. code:: rust
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`` (per type, not
per atom), pre-sized atom storage from headers, and parses floats from borrowed
line slices (no intermediate atom-line ``String``).
.. figure:: img/memory_usage.svg
Peak RSS plot (from historical peer JSON; re-run ``make_plots.py`` to refresh)
Feature matrix plot
-------------------
.. figure:: img/feature_comparison.svg
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 :doc:`spec`.
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)
---------------------
.. code:: shell
# 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