From 8dcddf093beec496fc7d293d755127cd75977532 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Thu, 20 Aug 2026 20:20:04 -0700 Subject: [PATCH] chore(release): release v0.4.5 - Bump Rust and Python package metadata and release links to v0.4.5 - Generate the changelog and retain only the Zenodo concept DOI - Add idempotent release-version automation with prior-release inference - Publish README comparisons from retained performance measurements - Align release and benchmarking documentation with the new workflow --- AGENTS.md | 8 +- CHANGELOG.md | 117 +++ CITATION.cff | 8 +- Cargo.lock | 6 +- Cargo.toml | 2 +- README.md | 50 +- docs/BENCHMARKING.md | 75 +- docs/PERFORMANCE.md | 533 ++++++-------- docs/RELEASING.md | 337 +++------ docs/archive/performance/README.md | 1 + docs/archive/performance/v0.4.4-vs-v0.4.3.md | 383 ++++++++++ .../bench/vs_linalg_lu_solve_median.csv | 16 +- .../vs_linalg_lu_solve_median.provenance.json | 76 +- .../bench/vs_linalg_lu_solve_median.svg | 148 ++-- justfile | 66 +- pyproject.toml | 6 +- scripts/README.md | 42 +- scripts/archive_performance.py | 8 + scripts/benchmark_contract.py | 48 ++ scripts/check_docs_version_sync.py | 89 ++- scripts/criterion_dim_plot.py | 696 +++++++++++------- scripts/performance_artifacts.py | 12 + scripts/tests/test_archive_performance.py | 1 + scripts/tests/test_check_docs_version_sync.py | 13 + scripts/tests/test_criterion_dim_plot.py | 627 ++++++++-------- scripts/tests/test_update_release_version.py | 226 ++++++ scripts/update_release_version.py | 521 +++++++++++++ uv.lock | 2 +- 28 files changed, 2755 insertions(+), 1362 deletions(-) create mode 100644 docs/archive/performance/v0.4.4-vs-v0.4.3.md create mode 100644 scripts/benchmark_contract.py create mode 100644 scripts/tests/test_update_release_version.py create mode 100644 scripts/update_release_version.py diff --git a/AGENTS.md b/AGENTS.md index c8e8df1..69df9d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -296,6 +296,7 @@ just test # Lib + doc tests (fast) just test-all # All tests (Rust, benchmark inputs, and Python) just examples # Run all examples just update # Update dependency locks and repository-owned Cargo tools +just update-version vX.Y.Z # Update release metadata without upgrading dependencies ``` ### Detailed Command Reference @@ -305,9 +306,9 @@ just update # Update dependency locks and repository-owned Cargo tools - Benchmarks: `cargo bench --locked --features bench` (or `just bench`) - Benchmarks (exact arithmetic): `just bench-exact` - Benchmarks (la-stack vs nalgebra/faer): `just bench-vs-linalg [filter]` (full run) or `just bench-vs-linalg-quick [filter]` (reduced) -- Benchmarks (plot vs_linalg CSV/SVG/JSON provenance): `just plot-vs-linalg [metric] [stat] [sample] [log_y]`; - publish a freshly gated full run to README with - `just plot-vs-linalg-readme [metric] [stat] [sample] [log_y]` +- Benchmarks (plot exploratory vs_linalg CSV/SVG/JSON provenance): `just plot-vs-linalg [metric] [stat] [sample] [log_y]`; + after `just performance-release`, publish its retained measurements to README + with `just performance-readme [metric] [stat] [sample] [log_y]` - Benchmarks (save baseline): `just bench-save-baseline v0.4.1` - Build (debug): `cargo build` (or `just build`) - Build (release): `cargo build --release` (or `just build-release`) @@ -326,6 +327,7 @@ just update # Update dependency locks and repository-owned Cargo tools - Lint/validate: `just check` - Cargo manifest/lockfile synchronization: `just cargo-lock-check` - Unused dependency check: `just unused-deps` (uses `cargo-machete`) +- Update release metadata: `just update-version vX.Y.Z` (infers the previous stable published GitHub release) - Pre-commit validation / CI simulation: `just ci` (lint + tests + examples + bench compile) - Python setup from the lockfile: `uv sync --locked --group dev` (or `just python-sync`) - Python tests: `just test-python` diff --git a/CHANGELOG.md b/CHANGELOG.md index 038d39a..00f1d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,117 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.4.5] - 2026-08-21 + +### ⚠️ Breaking Changes + +- Unify local and release performance workflows +- Require Rust 1.97.1 +- Require Rust 1.98.0 + +### Merged Pull Requests + +- Bump the dependencies group with 3 updates [#211](https://github.com/acgetchell/la-stack/pull/211) +- Bump the github-actions group with 7 updates [#200](https://github.com/acgetchell/la-stack/pull/200) + +### Added + +- Persist release report artifacts for rerendering [`600445c`](https://github.com/acgetchell/la-stack/commit/600445c325ade517631d50a5f625923f56387f1c) + + - Store deterministic comparison CSV with an adjacent schema-versioned provenance sidecar before temporary worktrees are removed. + - Render and promote reports only from validated artifact reloads, with fail-closed path checks and transactional rollback. + - Add performance-rerender and document artifact retention, GitHub assets, and the release workflow. + - Refresh Rust, Python, contributor-tool, and GitHub Action pins, and separate Dependabot security update groups. + +### Dependencies + +- Bump the github-actions group with 7 updates [#200](https://github.com/acgetchell/la-stack/pull/200) + [`9728daf`](https://github.com/acgetchell/la-stack/commit/9728dafc026bb2a849bae49feaca6144c5598698) + +### Fixed + +- Enforce pinned uv in local recipe guards [`938aff6`](https://github.com/acgetchell/la-stack/commit/938aff62517d5055d1db49bc7734e4069ef74d72) + + - Reject missing or mismatched uv versions with actionable diagnostics. + - Reuse the canonical version guard across uv-backed validation helpers. +- Harden release artifact promotion [`8b8537b`](https://github.com/acgetchell/la-stack/commit/8b8537b86226285a343976ea987cf2759bf3dd7d) + - Treat CRLF and LF archive reports as equivalent while preserving stored bytes. + - Share comparison selection and report-update guidance across generation paths. + - Isolate artifact path resolution and retained-artifact rendering. + - Clarify that durable benchmark downloads are GitHub Release assets. +- Harden numerical and release evidence [`c878701`](https://github.com/acgetchell/la-stack/commit/c8787017122c3e94958e82d327fdab8a305999b8) + - Surface determinant overflow even when error-bound terms underflow. + - Measure complete benchmark operations symmetrically and bind retained reports to atomic, validated provenance. + - Fail closed on inconsistent release metadata, tags, changelog sections, Semgrep annotations, and recorded CPU provenance. + - Align docs.rs feature annotations, citation identifiers, all-target linting, and configurable property-test runs with documented contracts. +- Harden release and validation edge cases [`260afb8`](https://github.com/acgetchell/la-stack/commit/260afb85fdc8eaa873bdb64b6d7eae76ab6b9abc) + - Preserve archived changelog bytes during rollback and reject malformed Unreleased headings. + - Match overlapping Semgrep fixture spans deterministically and centralize immutable benchmark provenance handling. + - Document exact matrix/RHS scaling and the power-of-two factor that preserves linear systems. +- [**breaking**] Unify local and release performance workflows + [`c601c41`](https://github.com/acgetchell/la-stack/commit/c601c4146b01868a29288b7221e413706e3e01c7) + - Retain local Markdown, CSV, and provenance artifacts while allowing same-version comparisons of tracked changes. + - Exclude untracked files explicitly and isolate the narrowed non-exact comparison bundle. + - Promote distinct-release artifacts through performance-doc and keep performance-release atomic and rollback-capable. + - Render one consolidated table per benchmark suite and reject invalid release publication states. +- Isolate algebraic float fixture scans [`56e848f`](https://github.com/acgetchell/la-stack/commit/56e848fb3dbbc93f2fd4c0b566d71489c96dc857) + - exclude intentional algebraic float violations from production Semgrep scans while preserving direct fixture validation + - cover f64 associated-call syntax alongside f32 and receiver forms + - forbid dead-code suppressions and remove blanket allowances from static-analysis fixtures + +### Maintenance + +- Gate auto-merge on CodeRabbit approval [`2a37961`](https://github.com/acgetchell/la-stack/commit/2a37961ee8c95f2482541910de78680af22bb968) + + - Request one SHA-scoped CodeRabbit review for each Dependabot update. + - Enable squash auto-merge only for the triggering head after required checks pass. + - Group GitHub Actions updates, remove autopep8, and refresh pinned tooling. + - Print an explicit release title in tag-release follow-up guidance. +- [**breaking**] Require Rust 1.97.1 [`99bac26`](https://github.com/acgetchell/la-stack/commit/99bac263ae773cc8cb42c418630c8e7dd7d023b8) + - align contributor, CI, release, and Clippy toolchain baselines + - compile benchmarks with Cargo's warning policy to preserve cache reuse + - publish CodeRabbit's canonical review check for ruleset enforcement +- Restore ruleset-compatible status [`5235365`](https://github.com/acgetchell/la-stack/commit/5235365efaf38a0d4035d74a46a7468ad10921d0) + - publish the legacy CodeRabbit commit status required by the main ruleset + - keep review progress disabled until CodeRabbit can publish the required check run +- Standardize validation and centralize tool pins [`54a7e3a`](https://github.com/acgetchell/la-stack/commit/54a7e3a8e8a640fa5069d5c07fffbf1c1dfab903) + - Compose CI from orthogonal leaf validators with one release-profile nextest pass and separate doctests. + - Resolve workflow tooling through the justfile and shared setup action. + - Document focused validation, notebook scope, and Rust 1.97 tooling policy. + - Refresh development tools, security actions, Dependabot schedules, and lockfiles. +- [**breaking**] Require Rust 1.98.0 [`2f9e0a8`](https://github.com/acgetchell/la-stack/commit/2f9e0a83c4fd974f34f87f29409cf1eac4da5343) + - align the MSRV, contributor toolchain, dependency locks, and documented maintenance baseline + - forbid algebraic float operations in numerical kernels to preserve IEEE-754 error and reproducibility contracts + - add a scoped just update workflow with atomic Cargo tool-pin reconciliation + - clean failed changelog and performance staging files without disturbing published artifacts +- Bump the dependencies group with 3 updates [#211](https://github.com/acgetchell/la-stack/pull/211) + [`493068a`](https://github.com/acgetchell/la-stack/commit/493068ac653724959f7083f34d5c909a496314bd) + + Updates the requirements on [ruff](https://github.com/astral-sh/ruff) , [ty](https://github.com/astral-sh/ty) and + [setuptools](https://github.com/pypa/setuptools) to permit the latest version. + + Updates `ruff` from 0.16.1 to 0.16.2 + - [Release notes](https://github.com/astral-sh/ruff/releases) + - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ruff/compare/0.16.1...0.16.2) + + Updates `ty` from 0.0.66 to 0.0.69 + - [Release notes](https://github.com/astral-sh/ty/releases) + - [Changelog](https://github.com/astral-sh/ty/blob/main/CHANGELOG.md) + - [Commits](https://github.com/astral-sh/ty/compare/0.0.66...0.0.69) + + Updates `setuptools` to 84.0.0 + - [Release notes](https://github.com/pypa/setuptools/releases) + - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) + - [Commits](https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0) + +### Performance + +- Reduce checked determinant query overhead [`2bdf1d3`](https://github.com/acgetchell/la-stack/commit/2bdf1d3dc4b4a27928a888be678b4c360916e94c) + + - Aggregate D≥5 range proofs before a single cold scaled replay. + - Preserve immediate small-dimension fallback and range-safe determinant behavior. + ## [0.4.4] - 2026-07-13 ### ⚠️ Breaking Changes @@ -67,6 +178,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - fail benchmark publication closed on invalid samples or mismatched provenance - make release and changelog tooling transactional, path-safe, and Windows-portable - align benchmark CI with pinned local tools and least-privilege publishing +- Harden changelog and version generation [`2e1eb2e`](https://github.com/acgetchell/la-stack/commit/2e1eb2e965333fab51934c2b605fe3465e4b57e6) + - Reject tag prefixes embedded in longer non-semver revisions + - Preserve fenced examples when stripping Dependabot metadata + - Document the conditional exact-solve scaling invariant + - Regenerate the v0.4.4 changelog through the release workflow ### Maintenance @@ -775,6 +891,7 @@ Older releases are archived by minor series: - [0.2.x](docs/archive/changelog/0.2.md) - [0.1.x](docs/archive/changelog/0.1.md) +[0.4.5]: https://github.com/acgetchell/la-stack/compare/v0.4.4...v0.4.5 [0.4.4]: https://github.com/acgetchell/la-stack/compare/v0.4.3...v0.4.4 [0.4.3]: https://github.com/acgetchell/la-stack/compare/v0.4.2...v0.4.3 [0.4.2]: https://github.com/acgetchell/la-stack/compare/v0.4.1...v0.4.2 diff --git a/CITATION.cff b/CITATION.cff index 457ec19..05441db 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,15 +2,11 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." type: software title: "la-stack: Fast, stack-allocated linear algebra for fixed dimensions in Rust" -version: 0.4.4 -date-released: 2026-07-13 +version: 0.4.5 +date-released: 2026-08-21 url: "https://github.com/acgetchell/la-stack" repository-code: "https://github.com/acgetchell/la-stack" doi: "10.5281/zenodo.18158926" -identifiers: - - description: "Zenodo DOI for version 0.4.4" - type: doi - value: "10.5281/zenodo.21331524" authors: - family-names: "Getchell" given-names: "Adam" diff --git a/Cargo.lock b/Cargo.lock index a6c8ca7..0831134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,9 +264,9 @@ checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "enum-as-inner" @@ -639,7 +639,7 @@ dependencies = [ [[package]] name = "la-stack" -version = "0.4.4" +version = "0.4.5" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index bbaa418..091d521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "la-stack" -version = "0.4.4" +version = "0.4.5" edition = "2024" rust-version = "1.98.0" license = "BSD-3-Clause" diff --git a/README.md b/README.md index b5618d3..dd7f90a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.18158926.svg)](https://doi.org/10.5281/zenodo.18158926) [![Crates.io](https://badgen.net/crates/v/la-stack)](https://crates.io/crates/la-stack) [![Downloads](https://badgen.net/crates/d/la-stack)](https://crates.io/crates/la-stack) -[![License](https://badgen.net/github/license/acgetchell/la-stack)](https://github.com/acgetchell/la-stack/blob/v0.4.4/LICENSE) +[![License](https://badgen.net/github/license/acgetchell/la-stack)](https://github.com/acgetchell/la-stack/blob/v0.4.5/LICENSE) [![Docs.rs](https://docs.rs/la-stack/badge.svg)](https://docs.rs/la-stack) [![CI](https://github.com/acgetchell/la-stack/actions/workflows/ci.yml/badge.svg)](https://github.com/acgetchell/la-stack/actions/workflows/ci.yml) [![rust-clippy analyze][clippy-badge]][clippy-workflow] @@ -60,9 +60,9 @@ for the algorithms, validity boundaries, and supporting references. exact values allocate as required - ✅ `unsafe` forbidden -See [CHANGELOG.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/CHANGELOG.md) +See [CHANGELOG.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/CHANGELOG.md) for release history and -[docs/roadmap.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/roadmap.md) +[docs/roadmap.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/roadmap.md) for current release planning. ## 🚫 Anti-goals @@ -102,7 +102,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -la-stack = "0.4.4" +la-stack = "0.4.5" ``` ### Feature flags @@ -256,7 +256,7 @@ rationals (this pulls in `num-bigint`, `num-rational`, and `num-traits` for ```toml [dependencies] -la-stack = { version = "0.4.4", features = ["exact"] } +la-stack = { version = "0.4.5", features = ["exact"] } ``` These routines are exact with respect to the finite binary64 values stored in @@ -501,7 +501,7 @@ breaking callers. ![LU solve (factor + solve): median time vs dimension][lu-solve-benchmark] Raw data: -[docs/assets/bench/vs_linalg_lu_solve_median.csv](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/assets/bench/vs_linalg_lu_solve_median.csv) +[docs/assets/bench/vs_linalg_lu_solve_median.csv](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/assets/bench/vs_linalg_lu_solve_median.csv) Measurement provenance: [docs/assets/bench/vs_linalg_lu_solve_median.provenance.json][benchmark-provenance] @@ -523,22 +523,22 @@ README table, and provenance together. For the full per-kernel comparison methodology, input construction, and release-comparison workflow details, see -[docs/BENCHMARKING.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/BENCHMARKING.md). +[docs/BENCHMARKING.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/BENCHMARKING.md). For the current release-to-release performance snapshot, see -[docs/PERFORMANCE.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/PERFORMANCE.md). +[docs/PERFORMANCE.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/PERFORMANCE.md). | D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | reduction vs nalgebra (point est.) | reduction vs faer (point est.) | |---:|--------------------:|--------------------:|----------------:|---------------------:|----------------:| -| 2 | 2.051 | 4.609 | 149.537 | +55.5% | +98.6% | -| 3 | 10.032 | 23.094 | 185.935 | +56.6% | +94.6% | -| 4 | 21.806 | 53.542 | 218.921 | +59.3% | +90.0% | -| 5 | 43.787 | 70.437 | 282.262 | +37.8% | +84.5% | -| 8 | 128.337 | 167.505 | 414.791 | +23.4% | +69.1% | -| 16 | 672.680 | 581.273 | 875.411 | -15.7% | +23.2% | -| 32 | 2,873.720 | 2,470.435 | 2,861.209 | -16.3% | -0.4% | -| 64 | 18,165.369 | 15,021.737 | 12,225.703 | -20.9% | -48.6% | +| 2 | 2.044 | 4.601 | 151.939 | +55.6% | +98.7% | +| 3 | 9.989 | 23.513 | 196.357 | +57.5% | +94.9% | +| 4 | 21.865 | 54.716 | 223.910 | +60.0% | +90.2% | +| 5 | 44.510 | 71.219 | 293.420 | +37.5% | +84.8% | +| 8 | 145.405 | 188.352 | 381.872 | +22.8% | +61.9% | +| 16 | 672.491 | 585.261 | 897.236 | -14.9% | +25.0% | +| 32 | 2,777.707 | 2,501.361 | 2,952.778 | -11.0% | +5.9% | +| 64 | 17,357.785 | 13,878.401 | 12,199.761 | -25.1% | -42.3% | @@ -593,14 +593,14 @@ CI runs `just ci` on Ubuntu, macOS, and Windows to keep platform coverage aligned with the local comprehensive validation path. For coverage commands and report locations, see -[`docs/COVERAGE.md`](https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/COVERAGE.md). +[`docs/COVERAGE.md`](https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/COVERAGE.md). For the full contributor workflow, see -[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/CONTRIBUTING.md). +[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/CONTRIBUTING.md). ## 📝 Citation If you use this library in academic work, please cite it using -[CITATION.cff](https://github.com/acgetchell/la-stack/blob/v0.4.4/CITATION.cff) +[CITATION.cff](https://github.com/acgetchell/la-stack/blob/v0.4.5/CITATION.cff) (or GitHub's "Cite this repository" feature). Tagged releases are archived on Zenodo under the [all-versions concept DOI](https://doi.org/10.5281/zenodo.18158926). @@ -608,23 +608,23 @@ Zenodo under the ## 📚 References For canonical references to the algorithms used by this crate, see -[REFERENCES.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/REFERENCES.md). +[REFERENCES.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/REFERENCES.md). ## 🤖 AI Agents AI coding assistants should read -[AGENTS.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/AGENTS.md) +[AGENTS.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/AGENTS.md) before proposing or applying changes. See -[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.4/CONTRIBUTING.md) +[CONTRIBUTING.md](https://github.com/acgetchell/la-stack/blob/v0.4.5/CONTRIBUTING.md) for the repository's AI-assisted development note. ## 📄 License -BSD 3-Clause License. See [LICENSE](https://github.com/acgetchell/la-stack/blob/v0.4.4/LICENSE). +BSD 3-Clause License. See [LICENSE](https://github.com/acgetchell/la-stack/blob/v0.4.5/LICENSE). [audit-badge]: https://github.com/acgetchell/la-stack/actions/workflows/audit.yml/badge.svg [audit-workflow]: https://github.com/acgetchell/la-stack/actions/workflows/audit.yml -[benchmark-provenance]: https://github.com/acgetchell/la-stack/blob/v0.4.4/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json +[benchmark-provenance]: https://github.com/acgetchell/la-stack/blob/v0.4.5/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json [clippy-badge]: https://github.com/acgetchell/la-stack/actions/workflows/rust-clippy.yml/badge.svg [clippy-workflow]: https://github.com/acgetchell/la-stack/actions/workflows/rust-clippy.yml -[lu-solve-benchmark]: https://raw.githubusercontent.com/acgetchell/la-stack/v0.4.4/docs/assets/bench/vs_linalg_lu_solve_median.svg +[lu-solve-benchmark]: https://raw.githubusercontent.com/acgetchell/la-stack/v0.4.5/docs/assets/bench/vs_linalg_lu_solve_median.svg diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md index 77bd4db..08be44c 100644 --- a/docs/BENCHMARKING.md +++ b/docs/BENCHMARKING.md @@ -25,21 +25,23 @@ the commands measure and where their outputs go. | Goal | Recipe | |------|--------| | Latest-release local audit | `just performance-local` | -| Non-exact release-signal check against tags | `just performance-local-non-exact v0.4.4 v0.4.3` | +| Non-exact release-signal check against tags | `just performance-local-non-exact v0.4.5 v0.4.4` | | Fast saved-baseline loop | `just bench-save-baseline ` then `just bench-compare all-benches` | | Full crate comparison | `just bench-vs-linalg` | -| README table and plot | `just plot-vs-linalg-readme` | -| Release report | `just performance-release v0.4.4 v0.4.3` | +| README table and plot | `just performance-release` then `just performance-readme` | +| Release report | `just performance-release v0.4.5 v0.4.4` | | Build docs from retained release inputs | `just performance-doc` | -| Published-asset comparison | `just performance-github-assets v0.4.4 v0.4.3` | +| Published-asset comparison | `just performance-github-assets v0.4.5 v0.4.4` | Rule of thumb: - Use `performance-local*` for clean, self-contained answers. - Use `bench-save-*` plus `bench-compare` for tight local optimization loops. -- Use `bench-vs-linalg` plus plotting when updating README crate-to-crate - comparisons. +- Use `bench-vs-linalg` plus `plot-vs-linalg` for exploratory crate-to-crate + plots. - Use `performance-release` only when preparing committed release artifacts. +- After `performance-release`, use `performance-readme` to publish its + retained crate-to-crate measurements without benchmarking again. - Use `performance-doc` for report-format changes after a valid, promotable comparison dataset has already been retained. @@ -152,10 +154,10 @@ distinct release identifiers. For a narrower non-exact check against a known release pair, run: ```bash -just performance-local-non-exact v0.4.4 v0.4.3 +just performance-local-non-exact v0.4.5 v0.4.4 ``` -This generates a local `v0.4.3` `vs_linalg` baseline, measures the current +This generates a local `v0.4.4` `vs_linalg` baseline, measures the current la-stack `vs_linalg` rows, and renders a `vs_linalg` report. The report includes saved baseline nalgebra/faer timings as context where matching peer rows exist, without rerunning current peer crates. @@ -210,28 +212,41 @@ revision-to-revision comparison. ### Update The README nalgebra/faer Table -The README benchmark table and SVG plot are crate-to-crate comparisons from the -current checkout: +The README benchmark table and SVG plot are derived from the retained release +comparison. Generate and validate that comparison first, then publish its +crate-to-crate measurements: ```bash -just plot-vs-linalg-readme +just performance-release +just performance-readme ``` -This publication recipe validates the benchmark fixtures, runs a fresh benchmark -for the selected metric only, and requires la-stack, nalgebra, and faer results -for every canonical dimension (D=2, 3, 4, 5, 8, 16, 32, and 64) before updating: +`performance-readme` does not run benchmarks. It loads the canonical +`target/bench-reports/performance.csv` and adjacent provenance JSON retained by +`performance-release`, then uses the current la-stack result and the peer +nalgebra/faer results measured by that same shared current harness. It requires +all three timings for every canonical dimension (D=2, 3, 4, 5, 8, 16, 32, and +64) before updating: - `README.md` - `docs/assets/bench/vs_linalg_lu_solve_median.csv` - `docs/assets/bench/vs_linalg_lu_solve_median.svg` - `docs/assets/bench/vs_linalg_lu_solve_median.provenance.json` -The provenance sidecar records the measured source state, CPU, operating system, -Rust toolchain, dependency lock and harness digests, Criterion dependency and -selection, dimensions, benchmark command, and correctness-gate result. Missing -coverage or provenance aborts publication. Use `--allow-partial` only for -exploratory CSV/SVG output; it cannot update README and its sidecar explicitly -marks measurement provenance unavailable. +The publisher verifies the retained artifact digest and schema, release version, +measured source state, commit, dependency lock, complete peer coverage, and +recorded measurement provenance before writing anything. When the retained +provenance includes a benchmark-contract digest, it also verifies the benchmark +code, inputs, dependencies, and toolchain against the current checkout. Legacy +retained artifacts without that field remain publishable but are labeled +`legacy-retained-artifact` rather than contract-matched. The derived provenance +sidecar preserves the measurement commands and environment and records the +retained CSV/JSON digests. Missing, stale, or inconsistent input aborts +publication atomically. + +For exploratory CSV/SVG output, run `bench-vs-linalg` and `plot-vs-linalg`. +That path still reads raw Criterion output; `--allow-partial` remains +exploratory-only and cannot update README. See `uv run --locked criterion-dim-plot --help` for plotting options. @@ -241,7 +256,7 @@ Release PRs promote one curated release-to-release comparison into committed docs: ```bash -just performance-release v0.4.4 v0.4.3 +just performance-release v0.4.5 v0.4.4 ``` With no arguments, `just performance-release` infers the current release tag @@ -252,10 +267,12 @@ This command creates temporary worktrees, validates the complete comparison, and writes the exact report inputs to `target/bench-reports/performance.csv` with adjacent `performance.provenance.json`. The CSV records deterministic benchmark keys, -coverage status and notes, and baseline/current median estimates with complete -confidence intervals in nanoseconds. The JSON sidecar binds the CSV digest and -row count to the release pair, source states, commands, toolchain, Criterion -version, harness/configuration digests, host, and schema version. +coverage status and notes, baseline/current median estimates, and +same-current-harness nalgebra/faer peer estimates with complete confidence +intervals in nanoseconds. Those peer fields are the source for +`performance-readme`. The JSON sidecar binds the CSV digest and row count to +the release pair, source states, commands, toolchain, Criterion version, +harness/configuration digests, host, and schema version. Before creating worktrees or running either benchmark revision, structured local and release-report workflows require an identifiable CPU model. Raw @@ -297,7 +314,7 @@ requirement applies even when both release tags are supplied explicitly because the recipe still downloads their GitHub Release assets: ```bash -just performance-github-assets v0.4.4 v0.4.3 +just performance-github-assets v0.4.5 v0.4.4 ``` With no arguments, the recipe discovers the latest and previous stable @@ -316,15 +333,15 @@ shared-harness workflow before attributing a difference solely to library code. |------|------------|----------|---------| | `target/criterion/` | No | `cargo bench`, `bench-save-*` | Local Criterion measurements and named baselines. | | `target/bench-reports/performance.md` | No | `bench-compare`, `performance-local`, `performance-release`, `performance-doc` | Canonical local comparison report. | -| `target/bench-reports/performance.csv` | No | `performance-local`, `performance-release` | Validated tabular inputs for the canonical comparison. | -| `target/bench-reports/performance.provenance.json` | No | `performance-local`, `performance-release` | Schema, package identifiers, source, command, toolchain, host, digest, and harness provenance. | +| `target/bench-reports/performance.csv` | No | `performance-local`, `performance-release` | Validated tabular inputs for the canonical comparison and README publisher. | +| `target/bench-reports/performance.provenance.json` | No | `performance-local`, `performance-release` | Schema, package identifiers, source, command, toolchain, host, digest, and harness provenance consumed by the README publisher. | | `target/bench-reports/performance-non-exact.*` | No | `performance-local-non-exact` | Narrowed non-exact report and retained peer-context comparison inputs. | | `target/bench-reports/github-assets-performance.md` | No | `performance-github-assets` | Local report from published release artifacts. | | `target/bench-reports/github-assets-performance.csv` | No | `performance-github-assets` | Tabular inputs derived from published native archives. | | `target/bench-reports/github-assets-performance.provenance.json` | No | `performance-github-assets` | Provenance for the published-asset report inputs. | | `docs/PERFORMANCE.md` | Yes | `performance-release`, `performance-doc` | Latest curated release-to-release comparison. | | `docs/archive/performance/` | Yes | `performance-release`, `performance-doc` | Older curated release-to-release comparisons. | -| `docs/assets/bench/` | Yes | `plot-vs-linalg-readme` | README benchmark CSV/SVG assets and JSON provenance. | +| `docs/assets/bench/` | Yes | `performance-readme` | README benchmark CSV/SVG assets and JSON provenance. | | GitHub Release | Remote | `.github/workflows/release-benchmarks.yml` | Criterion baseline archive. | Published baseline assets use the filename diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 9009a30..3a6c756 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -1,7 +1,7 @@ # Benchmark Performance -**la-stack** v0.4.4 · `e736c5f` (HEAD) -**Source revision timestamp**: 2026-07-12 17:11:09 UTC (deterministic report metadata; not the benchmark measurement time) +**la-stack** v0.4.5 · `99d3392` (HEAD) +**Source revision timestamp**: 2026-08-20 21:16:55 UTC (deterministic report metadata; not the benchmark measurement time) **Benchmark measurement timestamp**: not recorded by Criterion; use the provenance below to identify the measured revisions and environment. **Statistic**: median **Suite**: all @@ -9,7 +9,7 @@ ## Benchmark Results -Comparison against baseline **v0.4.3**: +Comparison against baseline **v0.4.4**: Negative point-estimate change means the current point estimate is smaller; a baseline/current point-estimate ratio above 1.00 has the same meaning. The CI-relation column reports only whether the two marginal Criterion intervals overlap. These are not paired confidence intervals @@ -19,335 +19,238 @@ for the change, so the report makes no statistical-significance or performance-i **Measurement environment**: recorded for both samples under one shared current harness. -- CPU: unavailable (legacy report recorded architecture only: `arm`) -- OS: `Darwin 25.5.0 arm64` -- rustc: `rustc 1.97.0 (2d8144b78 2026-07-07)` -- Current commit: `e736c5fda155ef23c8712f89ae15bf5369ff3787` +- CPU: `Apple M4 Max (arm64)` +- OS: `Darwin 25.6.0 arm64` +- rustc: `rustc 1.98.0 (88d9e12ae 2026-08-18)` +- Current commit: `99d33927e389018c599c009603a7eafffcc25089` - Current Git clean: `false` -- Current source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2` -- Baseline commit: `dd4ad192a42e28d9cc72b336b40802fe65cea4f0` +- Current source-state SHA-256: `7d72086128b12e2a89d5ca78f0e8cc0cb393a44f1062d027a069722ff9dae007` +- Baseline commit: `03a6dc751b7bf7c69b833aeb4e20e2acb6da2e4c` - Baseline Git clean: `false` -- Baseline source-state SHA-256: `1d77b9640c3fe8dce45b486052177fe4d9a44b840605661da0717b8cb0cec9b8` -- Cargo.lock SHA-256: `0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd` -- Benchmark harness SHA-256: `c2cc116cf2d77d415f12f30a60bcf3e1b9e0d553c884bc3c57ce6c953124bc08` +- Baseline source-state SHA-256: `227a2780c989352be2eacf3f32ca67032cc69fc843e596acc386a7c4f7d459f0` +- Cargo.lock SHA-256: `72b99b4f7f3917d668bb71448198d5d496334684097eac282c859d08d8cf4492` +- Benchmark harness SHA-256: `164d6bbac2e6cb19b85c81677e4ab4428c3abb130dcfd38a2f5f162a5dd8a905` **Publication and validation environment**: -- Publication CPU: unavailable (legacy report recorded architecture only: `arm`) -- Publication OS: `Darwin 25.5.0 arm64` -- Publication rustc: `rustc 1.97.0 (2d8144b78 2026-07-07)` -- Publication commit: `e736c5fda155ef23c8712f89ae15bf5369ff3787` +- Publication CPU: `Apple M4 Max (arm64)` +- Publication OS: `Darwin 25.6.0 arm64` +- Publication rustc: `rustc 1.98.0 (88d9e12ae 2026-08-18)` +- Publication commit: `99d33927e389018c599c009603a7eafffcc25089` - Publication Git clean: `false` -- Publication source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2` -- Publication Cargo.lock SHA-256: `0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd` -- Publication harness SHA-256: `c2cc116cf2d77d415f12f30a60bcf3e1b9e0d553c884bc3c57ce6c953124bc08` +- Publication source-state SHA-256: `7d72086128b12e2a89d5ca78f0e8cc0cb393a44f1062d027a069722ff9dae007` +- Publication Cargo.lock SHA-256: `72b99b4f7f3917d668bb71448198d5d496334684097eac282c859d08d8cf4492` +- Publication harness SHA-256: `164d6bbac2e6cb19b85c81677e4ab4428c3abb130dcfd38a2f5f162a5dd8a905` - Criterion suite/scope: `all` / `release-signal` - Criterion statistic/sample: `median` / `new` - Criterion dependency version: `0.8.2` -- Baseline command: `just bench-save-baseline v0.4.3` +- Baseline command: `just bench-save-baseline v0.4.4` - Current command: `just bench-latest` - Correctness gate: `just test-bench-inputs` passed against both the current and baseline revisions using the shared current fixture harness. -- Validated current revision: `e736c5fda155ef23c8712f89ae15bf5369ff3787` (Git clean: `false`; - source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2`) -- Validated baseline revision: `dd4ad192a42e28d9cc72b336b40802fe65cea4f0` (Git clean: `false`; - source-state SHA-256: `1d77b9640c3fe8dce45b486052177fe4d9a44b840605661da0717b8cb0cec9b8`) -- Baseline API compatibility: `la_stack_v0_4_3_api` selects only source-compatible benchmark calls; - rows outside the baseline's correctness domain remain explicitly unavailable. -- Baseline-unavailable rows: `d8/la_stack_det_from_lu_balanced_range` and - `d8/la_stack_det_from_ldlt_balanced_range` were not timed because v0.4.3 returns zero for a - fixture whose exact determinant is one; current samples remain required, but no speedup is claimed. -- Baseline-unavailable rows: `exact_d2/det_direct_with_errbound`, - `exact_d3/det_direct_with_errbound`, and `exact_d4/det_direct_with_errbound` were not timed - because v0.4.3 predates the paired API; the comparable `det_errbound` baselines remain required. +- Validated current revision: `99d33927e389018c599c009603a7eafffcc25089` (Git clean: `false`; + source-state SHA-256: `7d72086128b12e2a89d5ca78f0e8cc0cb393a44f1062d027a069722ff9dae007`) +- Validated baseline revision: `03a6dc751b7bf7c69b833aeb4e20e2acb6da2e4c` (Git clean: `false`; + source-state SHA-256: `227a2780c989352be2eacf3f32ca67032cc69fc843e596acc386a7c4f7d459f0`) ## Exact arithmetic -### D=2 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -11.0% | faster point estimate; marginal CIs separated | 1.12x | -| det_direct | 0.6 ns [0.6 ns, 0.6 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -28.6% | faster point estimate; marginal CIs separated | 1.40x | -| det_errbound | 0.7 ns [0.7 ns, 0.7 ns] | 1.6 ns [1.6 ns, 1.6 ns] | +128.0% | slower point estimate; marginal CIs separated | 0.44x | -| det_exact | 95.7 ns [95.5 ns, 95.8 ns] | 89.3 ns [89.2 ns, 89.4 ns] | -6.7% | faster point estimate; marginal CIs separated | 1.07x | -| det_exact_f64_result | 77.0 ns [76.9 ns, 77.1 ns] | 65.5 ns [65.4 ns, 65.5 ns] | -15.0% | faster point estimate; marginal CIs separated | 1.18x | -| det_exact_rounded_f64 | 245.4 ns [244.9 ns, 245.9 ns] | 66.5 ns [66.5 ns, 66.6 ns] | -72.9% | faster point estimate; marginal CIs separated | 3.69x | -| det_sign_exact | 2.0 ns [2.0 ns, 2.0 ns] | 2.8 ns [2.8 ns, 2.8 ns] | +41.2% | slower point estimate; marginal CIs separated | 0.71x | -| solve_exact | 6.73 µs [6.72 µs, 6.74 µs] | 7.33 µs [7.32 µs, 7.34 µs] | +8.9% | slower point estimate; marginal CIs separated | 0.92x | -| solve_exact_f64_result | 6.89 µs [6.88 µs, 6.91 µs] | 8.38 µs [8.37 µs, 8.39 µs] | +21.6% | slower point estimate; marginal CIs separated | 0.82x | -| solve_exact_rounded_f64 | 7.08 µs [7.07 µs, 7.10 µs] | 7.66 µs [7.64 µs, 7.68 µs] | +8.1% | slower point estimate; marginal CIs separated | 0.93x | - -### D=3 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -17.2% | faster point estimate; marginal CIs separated | 1.21x | -| det_direct | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -16.6% | faster point estimate; marginal CIs separated | 1.20x | -| det_errbound | 1.6 ns [1.6 ns, 1.6 ns] | 3.4 ns [3.4 ns, 3.4 ns] | +112.5% | slower point estimate; marginal CIs separated | 0.47x | -| det_exact | 334.2 ns [333.8 ns, 335.3 ns] | 320.7 ns [320.2 ns, 321.0 ns] | -4.1% | faster point estimate; marginal CIs separated | 1.04x | -| det_exact_f64_result | 311.4 ns [310.4 ns, 313.1 ns] | 294.9 ns [294.0 ns, 295.3 ns] | -5.3% | faster point estimate; marginal CIs separated | 1.06x | -| det_exact_rounded_f64 | 505.9 ns [503.2 ns, 507.1 ns] | 294.9 ns [294.2 ns, 295.5 ns] | -41.7% | faster point estimate; marginal CIs separated | 1.72x | -| det_sign_exact | 3.7 ns [3.7 ns, 3.7 ns] | 4.7 ns [4.7 ns, 4.7 ns] | +26.9% | slower point estimate; marginal CIs separated | 0.79x | -| solve_exact | 29.70 µs [29.65 µs, 29.77 µs] | 31.46 µs [31.37 µs, 31.51 µs] | +5.9% | slower point estimate; marginal CIs separated | 0.94x | -| solve_exact_f64_result | 29.88 µs [29.80 µs, 30.01 µs] | 33.01 µs [32.96 µs, 33.11 µs] | +10.5% | slower point estimate; marginal CIs separated | 0.90x | -| solve_exact_rounded_f64 | 30.48 µs [30.41 µs, 30.58 µs] | 31.82 µs [31.76 µs, 31.84 µs] | +4.4% | slower point estimate; marginal CIs separated | 0.96x | - -### D=4 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det | 4.5 ns [4.5 ns, 4.5 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -47.7% | faster point estimate; marginal CIs separated | 1.91x | -| det_direct | 4.3 ns [4.3 ns, 4.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -47.0% | faster point estimate; marginal CIs separated | 1.89x | -| det_errbound | 6.9 ns [6.9 ns, 6.9 ns] | 6.7 ns [6.7 ns, 6.7 ns] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | -| det_exact | 1.10 µs [1.10 µs, 1.10 µs] | 994.3 ns [990.6 ns, 998.2 ns] | -9.5% | faster point estimate; marginal CIs separated | 1.11x | -| det_exact_f64_result | 1.09 µs [1.09 µs, 1.10 µs] | 959.7 ns [955.9 ns, 961.8 ns] | -12.1% | faster point estimate; marginal CIs separated | 1.14x | -| det_exact_rounded_f64 | 1.28 µs [1.27 µs, 1.29 µs] | 963.6 ns [962.2 ns, 964.4 ns] | -24.6% | faster point estimate; marginal CIs separated | 1.33x | -| det_sign_exact | 11.3 ns [11.3 ns, 11.3 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -31.5% | faster point estimate; marginal CIs separated | 1.46x | -| solve_exact | 77.25 µs [77.10 µs, 77.40 µs] | 79.77 µs [79.57 µs, 79.95 µs] | +3.3% | slower point estimate; marginal CIs separated | 0.97x | -| solve_exact_f64_result | 76.96 µs [76.80 µs, 77.13 µs] | 83.27 µs [83.05 µs, 83.39 µs] | +8.2% | slower point estimate; marginal CIs separated | 0.92x | -| solve_exact_rounded_f64 | 77.73 µs [77.58 µs, 77.82 µs] | 81.50 µs [81.39 µs, 81.62 µs] | +4.8% | slower point estimate; marginal CIs separated | 0.95x | - -### D=5 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det | 22.0 ns [22.0 ns, 22.1 ns] | 25.5 ns [25.4 ns, 25.6 ns] | +16.0% | slower point estimate; marginal CIs separated | 0.86x | -| det_exact | 3.08 µs [3.08 µs, 3.09 µs] | 2.93 µs [2.93 µs, 2.94 µs] | -5.0% | faster point estimate; marginal CIs separated | 1.05x | -| det_exact_f64_result | 3.07 µs [3.07 µs, 3.07 µs] | 2.88 µs [2.88 µs, 2.89 µs] | -6.2% | faster point estimate; marginal CIs separated | 1.07x | -| det_exact_rounded_f64 | 3.30 µs [3.29 µs, 3.30 µs] | 2.90 µs [2.89 µs, 2.90 µs] | -12.1% | faster point estimate; marginal CIs separated | 1.14x | -| det_sign_exact | 3.06 µs [3.05 µs, 3.07 µs] | 2.99 µs [2.98 µs, 3.00 µs] | -2.4% | faster point estimate; marginal CIs separated | 1.03x | -| solve_exact | 151.99 µs [151.61 µs, 152.39 µs] | 159.24 µs [158.84 µs, 159.40 µs] | +4.8% | slower point estimate; marginal CIs separated | 0.95x | -| solve_exact_f64_result | 151.95 µs [151.73 µs, 152.26 µs] | 162.98 µs [162.62 µs, 163.59 µs] | +7.3% | slower point estimate; marginal CIs separated | 0.93x | -| solve_exact_rounded_f64 | 153.08 µs [152.82 µs, 153.19 µs] | 159.84 µs [159.58 µs, 160.22 µs] | +4.4% | slower point estimate; marginal CIs separated | 0.96x | - -### Random corpus D=2 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 99.8 ns [99.2 ns, 100.4 ns] | 136.1 ns [136.0 ns, 136.7 ns] | +36.4% | slower point estimate; marginal CIs separated | 0.73x | -| det_exact | 3.22 µs [3.22 µs, 3.23 µs] | 3.06 µs [3.05 µs, 3.06 µs] | -5.2% | faster point estimate; marginal CIs separated | 1.05x | -| solve_exact | 65.74 µs [65.66 µs, 65.83 µs] | 67.56 µs [67.42 µs, 67.79 µs] | +2.8% | slower point estimate; marginal CIs separated | 0.97x | -| solve_exact_f64_result | 66.67 µs [66.58 µs, 66.75 µs] | 77.80 µs [77.55 µs, 77.91 µs] | +16.7% | slower point estimate; marginal CIs separated | 0.86x | -| solve_exact_rounded_f64 | 66.67 µs [66.58 µs, 66.69 µs] | 69.23 µs [69.09 µs, 69.35 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | - -### Random corpus D=3 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 174.3 ns [174.2 ns, 174.5 ns] | 225.1 ns [225.0 ns, 225.3 ns] | +29.1% | slower point estimate; marginal CIs separated | 0.77x | -| det_exact | 8.43 µs [8.39 µs, 8.47 µs] | 7.76 µs [7.72 µs, 7.81 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | -| solve_exact | 212.03 µs [211.85 µs, 212.21 µs] | 216.24 µs [216.02 µs, 216.47 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | -| solve_exact_f64_result | 213.65 µs [213.11 µs, 214.14 µs] | 229.74 µs [229.51 µs, 230.18 µs] | +7.5% | slower point estimate; marginal CIs separated | 0.93x | -| solve_exact_rounded_f64 | 213.73 µs [213.55 µs, 214.10 µs] | 218.02 µs [217.72 µs, 218.45 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | - -### Random corpus D=4 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 534.8 ns [534.5 ns, 535.0 ns] | 426.5 ns [425.8 ns, 427.4 ns] | -20.3% | faster point estimate; marginal CIs separated | 1.25x | -| det_exact | 28.60 µs [28.34 µs, 28.91 µs] | 24.12 µs [24.09 µs, 24.16 µs] | -15.7% | faster point estimate; marginal CIs separated | 1.19x | -| solve_exact | 486.14 µs [485.47 µs, 487.12 µs] | 495.70 µs [494.68 µs, 496.46 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | -| solve_exact_f64_result | 487.15 µs [486.57 µs, 487.66 µs] | 512.02 µs [511.01 µs, 513.36 µs] | +5.1% | slower point estimate; marginal CIs separated | 0.95x | -| solve_exact_rounded_f64 | 489.02 µs [487.59 µs, 489.93 µs] | 497.20 µs [495.79 µs, 498.14 µs] | +1.7% | slower point estimate; marginal CIs separated | 0.98x | - -### Random corpus D=5 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 51.48 µs [51.17 µs, 51.83 µs] | 48.07 µs [47.59 µs, 48.89 µs] | -6.6% | faster point estimate; marginal CIs separated | 1.07x | -| det_exact | 52.09 µs [51.90 µs, 52.27 µs] | 47.94 µs [47.67 µs, 48.20 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | -| solve_exact | 963.73 µs [961.57 µs, 964.57 µs] | 978.88 µs [977.33 µs, 979.71 µs] | +1.6% | slower point estimate; marginal CIs separated | 0.98x | -| solve_exact_f64_result | 960.79 µs [958.85 µs, 962.50 µs] | 995.01 µs [993.26 µs, 997.47 µs] | +3.6% | slower point estimate; marginal CIs separated | 0.97x | -| solve_exact_rounded_f64 | 964.92 µs [963.08 µs, 966.28 µs] | 982.14 µs [980.67 µs, 983.66 µs] | +1.8% | slower point estimate; marginal CIs separated | 0.98x | - -### Near-singular 3x3 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 350.2 ns [348.8 ns, 352.5 ns] | 349.4 ns [348.4 ns, 350.5 ns] | -0.2% | marginal CIs overlap | 1.00x | -| det_exact | 306.5 ns [305.5 ns, 307.4 ns] | 304.5 ns [303.9 ns, 305.1 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | -| solve_exact | 2.30 µs [2.29 µs, 2.30 µs] | 2.32 µs [2.31 µs, 2.32 µs] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | -| solve_exact_f64_result | 2.30 µs [2.30 µs, 2.31 µs] | 2.42 µs [2.41 µs, 2.42 µs] | +5.0% | slower point estimate; marginal CIs separated | 0.95x | -| solve_exact_rounded_f64 | 2.32 µs [2.31 µs, 2.32 µs] | 2.32 µs [2.32 µs, 2.33 µs] | +0.3% | marginal CIs overlap | 1.00x | - -### Large entries 3x3 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 283.6 ns [283.2 ns, 286.8 ns] | 281.7 ns [280.8 ns, 282.9 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | -| det_exact | 286.3 ns [285.5 ns, 288.1 ns] | 295.7 ns [294.0 ns, 296.9 ns] | +3.3% | slower point estimate; marginal CIs separated | 0.97x | -| solve_exact | 90.64 µs [90.57 µs, 90.79 µs] | 94.07 µs [94.01 µs, 94.20 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | -| solve_exact_f64_result | 91.18 µs [91.03 µs, 91.25 µs] | 95.07 µs [94.88 µs, 95.21 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | -| solve_exact_rounded_f64 | 91.51 µs [91.41 µs, 91.63 µs] | 94.47 µs [94.34 µs, 94.60 µs] | +3.2% | slower point estimate; marginal CIs separated | 0.97x | - -### Hilbert 4x4 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 11.3 ns [11.3 ns, 11.3 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -31.5% | faster point estimate; marginal CIs separated | 1.46x | -| det_exact | 1.24 µs [1.24 µs, 1.25 µs] | 1.09 µs [1.09 µs, 1.09 µs] | -12.3% | faster point estimate; marginal CIs separated | 1.14x | -| solve_exact | 57.23 µs [57.13 µs, 57.41 µs] | 59.81 µs [59.71 µs, 59.94 µs] | +4.5% | slower point estimate; marginal CIs separated | 0.96x | -| solve_exact_f64_result | 57.31 µs [57.18 µs, 57.62 µs] | 61.72 µs [61.51 µs, 61.85 µs] | +7.7% | slower point estimate; marginal CIs separated | 0.93x | -| solve_exact_rounded_f64 | 58.02 µs [57.92 µs, 58.14 µs] | 60.30 µs [60.18 µs, 60.43 µs] | +3.9% | slower point estimate; marginal CIs separated | 0.96x | - -### Hilbert 5x5 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | -|-----------|-------:|-------:|-------:|:-----------|--------:| -| det_sign_exact | 3.23 µs [3.22 µs, 3.24 µs] | 3.23 µs [3.23 µs, 3.24 µs] | +0.1% | marginal CIs overlap | 1.00x | -| det_exact | 3.26 µs [3.25 µs, 3.27 µs] | 3.00 µs [2.99 µs, 3.01 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | -| solve_exact | 119.87 µs [119.55 µs, 120.21 µs] | 124.37 µs [123.95 µs, 124.57 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | -| solve_exact_f64_result | 119.44 µs [119.30 µs, 119.59 µs] | 127.21 µs [126.96 µs, 127.34 µs] | +6.5% | slower point estimate; marginal CIs separated | 0.94x | -| solve_exact_rounded_f64 | 120.33 µs [120.13 µs, 120.48 µs] | 125.50 µs [125.24 µs, 125.78 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | +| Case | Benchmark | v0.4.4 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|:-----|:----------|-------:|-------:|-------:|:-----------|--------:| +| D=2 | det | 0.4 ns [0.4 ns, 0.4 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | +| D=2 | det_direct | 0.4 ns [0.4 ns, 0.4 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -1.5% | marginal CIs overlap | 1.02x | +| D=2 | det_direct_with_errbound | 1.7 ns [1.7 ns, 1.7 ns] | 1.7 ns [1.7 ns, 1.7 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | +| D=2 | det_errbound | 1.6 ns [1.6 ns, 1.6 ns] | 1.6 ns [1.6 ns, 1.6 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | +| D=2 | det_exact | 91.7 ns [91.6 ns, 91.9 ns] | 91.3 ns [91.2 ns, 91.5 ns] | -0.4% | faster point estimate; marginal CIs separated | 1.00x | +| D=2 | det_exact_f64_result | 65.6 ns [65.3 ns, 65.8 ns] | 64.7 ns [64.6 ns, 64.7 ns] | -1.3% | faster point estimate; marginal CIs separated | 1.01x | +| D=2 | det_exact_rounded_f64 | 66.7 ns [66.6 ns, 66.8 ns] | 66.6 ns [66.5 ns, 66.7 ns] | -0.1% | marginal CIs overlap | 1.00x | +| D=2 | det_sign_exact | 2.8 ns [2.7 ns, 2.8 ns] | 2.7 ns [2.7 ns, 2.7 ns] | -1.1% | faster point estimate; marginal CIs separated | 1.01x | +| D=2 | solve_exact | 6.90 µs [6.88 µs, 6.91 µs] | 6.78 µs [6.77 µs, 6.79 µs] | -1.8% | faster point estimate; marginal CIs separated | 1.02x | +| D=2 | solve_exact_f64_result | 8.14 µs [8.13 µs, 8.16 µs] | 7.91 µs [7.89 µs, 7.92 µs] | -2.9% | faster point estimate; marginal CIs separated | 1.03x | +| D=2 | solve_exact_rounded_f64 | 7.25 µs [7.23 µs, 7.27 µs] | 7.10 µs [7.10 µs, 7.11 µs] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | +| D=3 | det | 0.8 ns [0.8 ns, 0.8 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -1.1% | faster point estimate; marginal CIs separated | 1.01x | +| D=3 | det_direct | 0.8 ns [0.8 ns, 0.8 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | +| D=3 | det_direct_with_errbound | 3.3 ns [3.3 ns, 3.4 ns] | 3.3 ns [3.3 ns, 3.3 ns] | -0.0% | marginal CIs overlap | 1.00x | +| D=3 | det_errbound | 3.3 ns [3.3 ns, 3.3 ns] | 3.3 ns [3.3 ns, 3.3 ns] | -0.6% | faster point estimate; marginal CIs separated | 1.01x | +| D=3 | det_exact | 341.9 ns [340.5 ns, 343.5 ns] | 323.7 ns [323.2 ns, 324.3 ns] | -5.3% | faster point estimate; marginal CIs separated | 1.06x | +| D=3 | det_exact_f64_result | 300.9 ns [300.5 ns, 301.6 ns] | 293.7 ns [293.2 ns, 293.9 ns] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | +| D=3 | det_exact_rounded_f64 | 300.5 ns [300.1 ns, 301.0 ns] | 293.7 ns [293.5 ns, 294.0 ns] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | +| D=3 | det_sign_exact | 4.7 ns [4.7 ns, 4.7 ns] | 4.7 ns [4.7 ns, 4.7 ns] | -0.6% | faster point estimate; marginal CIs separated | 1.01x | +| D=3 | solve_exact | 30.23 µs [30.12 µs, 30.31 µs] | 29.93 µs [29.89 µs, 29.97 µs] | -1.0% | faster point estimate; marginal CIs separated | 1.01x | +| D=3 | solve_exact_f64_result | 32.20 µs [32.13 µs, 32.26 µs] | 31.88 µs [31.83 µs, 31.95 µs] | -1.0% | faster point estimate; marginal CIs separated | 1.01x | +| D=3 | solve_exact_rounded_f64 | 31.46 µs [31.19 µs, 31.53 µs] | 30.49 µs [30.44 µs, 30.52 µs] | -3.1% | faster point estimate; marginal CIs separated | 1.03x | +| D=4 | det | 3.3 ns [3.3 ns, 3.3 ns] | 3.2 ns [3.2 ns, 3.2 ns] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | +| D=4 | det_direct | 2.4 ns [2.4 ns, 2.4 ns] | 2.4 ns [2.4 ns, 2.4 ns] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | det_direct_with_errbound | 6.9 ns [6.9 ns, 6.9 ns] | 6.8 ns [6.8 ns, 6.8 ns] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | det_errbound | 6.8 ns [6.8 ns, 6.8 ns] | 6.7 ns [6.7 ns, 6.7 ns] | -1.3% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | det_exact | 1.02 µs [1.02 µs, 1.03 µs] | 1.01 µs [1.01 µs, 1.01 µs] | -1.1% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | det_exact_f64_result | 993.0 ns [990.5 ns, 996.0 ns] | 976.7 ns [974.5 ns, 978.7 ns] | -1.6% | faster point estimate; marginal CIs separated | 1.02x | +| D=4 | det_exact_rounded_f64 | 991.3 ns [989.1 ns, 993.3 ns] | 973.8 ns [973.0 ns, 976.3 ns] | -1.8% | faster point estimate; marginal CIs separated | 1.02x | +| D=4 | det_sign_exact | 7.8 ns [7.8 ns, 7.8 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -1.2% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | solve_exact | 78.81 µs [78.73 µs, 78.97 µs] | 77.91 µs [77.80 µs, 77.99 µs] | -1.1% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | solve_exact_f64_result | 81.82 µs [81.72 µs, 81.92 µs] | 80.71 µs [80.62 µs, 80.79 µs] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | +| D=4 | solve_exact_rounded_f64 | 79.66 µs [79.57 µs, 79.83 µs] | 78.66 µs [78.57 µs, 78.76 µs] | -1.3% | faster point estimate; marginal CIs separated | 1.01x | +| D=5 | det | 25.1 ns [25.1 ns, 25.2 ns] | 25.0 ns [24.9 ns, 25.1 ns] | -0.6% | marginal CIs overlap | 1.01x | +| D=5 | det_exact | 3.08 µs [3.06 µs, 3.12 µs] | 2.94 µs [2.94 µs, 2.95 µs] | -4.4% | faster point estimate; marginal CIs separated | 1.05x | +| D=5 | det_exact_f64_result | 3.02 µs [3.01 µs, 3.03 µs] | 2.91 µs [2.90 µs, 2.91 µs] | -3.7% | faster point estimate; marginal CIs separated | 1.04x | +| D=5 | det_exact_rounded_f64 | 2.98 µs [2.98 µs, 2.99 µs] | 2.96 µs [2.95 µs, 2.97 µs] | -0.6% | faster point estimate; marginal CIs separated | 1.01x | +| D=5 | det_sign_exact | 3.15 µs [3.14 µs, 3.17 µs] | 3.05 µs [3.04 µs, 3.05 µs] | -3.4% | faster point estimate; marginal CIs separated | 1.04x | +| D=5 | solve_exact | 156.87 µs [156.58 µs, 157.10 µs] | 155.34 µs [155.10 µs, 155.58 µs] | -1.0% | faster point estimate; marginal CIs separated | 1.01x | +| D=5 | solve_exact_f64_result | 159.75 µs [159.55 µs, 160.17 µs] | 158.29 µs [158.09 µs, 158.43 µs] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | +| D=5 | solve_exact_rounded_f64 | 159.08 µs [158.45 µs, 160.00 µs] | 156.37 µs [156.22 µs, 156.57 µs] | -1.7% | faster point estimate; marginal CIs separated | 1.02x | +| Hilbert 4x4 | det_exact | 1.13 µs [1.12 µs, 1.13 µs] | 1.10 µs [1.09 µs, 1.10 µs] | -2.8% | faster point estimate; marginal CIs separated | 1.03x | +| Hilbert 4x4 | det_sign_exact | 8.0 ns [7.9 ns, 8.0 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -3.2% | faster point estimate; marginal CIs separated | 1.03x | +| Hilbert 4x4 | solve_exact | 59.49 µs [59.37 µs, 59.59 µs] | 58.24 µs [58.12 µs, 58.35 µs] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | +| Hilbert 4x4 | solve_exact_f64_result | 61.76 µs [61.70 µs, 61.84 µs] | 60.89 µs [60.80 µs, 60.95 µs] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | +| Hilbert 4x4 | solve_exact_rounded_f64 | 60.17 µs [60.12 µs, 60.28 µs] | 58.88 µs [58.75 µs, 59.03 µs] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | +| Hilbert 5x5 | det_exact | 3.13 µs [3.12 µs, 3.13 µs] | 3.05 µs [3.03 µs, 3.05 µs] | -2.6% | faster point estimate; marginal CIs separated | 1.03x | +| Hilbert 5x5 | det_sign_exact | 3.33 µs [3.32 µs, 3.34 µs] | 3.27 µs [3.26 µs, 3.28 µs] | -1.9% | faster point estimate; marginal CIs separated | 1.02x | +| Hilbert 5x5 | solve_exact | 122.74 µs [122.59 µs, 122.86 µs] | 120.11 µs [119.87 µs, 120.28 µs] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | +| Hilbert 5x5 | solve_exact_f64_result | 125.85 µs [125.61 µs, 126.01 µs] | 123.42 µs [123.24 µs, 123.55 µs] | -1.9% | faster point estimate; marginal CIs separated | 1.02x | +| Hilbert 5x5 | solve_exact_rounded_f64 | 124.06 µs [123.87 µs, 124.28 µs] | 122.12 µs [121.79 µs, 122.49 µs] | -1.6% | faster point estimate; marginal CIs separated | 1.02x | +| Large entries 3x3 | det_exact | 309.9 ns [308.3 ns, 310.7 ns] | 290.4 ns [290.0 ns, 291.4 ns] | -6.3% | faster point estimate; marginal CIs separated | 1.07x | +| Large entries 3x3 | det_sign_exact | 291.0 ns [289.7 ns, 292.2 ns] | 282.1 ns [281.0 ns, 283.3 ns] | -3.0% | faster point estimate; marginal CIs separated | 1.03x | +| Large entries 3x3 | solve_exact | 95.06 µs [94.85 µs, 95.29 µs] | 92.60 µs [92.46 µs, 92.76 µs] | -2.6% | faster point estimate; marginal CIs separated | 1.03x | +| Large entries 3x3 | solve_exact_f64_result | 96.49 µs [96.09 µs, 97.15 µs] | 93.61 µs [93.42 µs, 93.81 µs] | -3.0% | faster point estimate; marginal CIs separated | 1.03x | +| Large entries 3x3 | solve_exact_rounded_f64 | 95.16 µs [94.97 µs, 95.34 µs] | 93.36 µs [93.20 µs, 93.55 µs] | -1.9% | faster point estimate; marginal CIs separated | 1.02x | +| Near-singular 3x3 | det_exact | 322.6 ns [321.3 ns, 325.5 ns] | 303.0 ns [302.2 ns, 303.5 ns] | -6.1% | faster point estimate; marginal CIs separated | 1.06x | +| Near-singular 3x3 | det_sign_exact | 372.6 ns [370.5 ns, 374.2 ns] | 345.8 ns [344.2 ns, 347.4 ns] | -7.2% | faster point estimate; marginal CIs separated | 1.08x | +| Near-singular 3x3 | solve_exact | 2.42 µs [2.41 µs, 2.43 µs] | 2.34 µs [2.34 µs, 2.35 µs] | -3.0% | faster point estimate; marginal CIs separated | 1.03x | +| Near-singular 3x3 | solve_exact_f64_result | 2.51 µs [2.49 µs, 2.52 µs] | 2.46 µs [2.45 µs, 2.47 µs] | -1.9% | faster point estimate; marginal CIs separated | 1.02x | +| Near-singular 3x3 | solve_exact_rounded_f64 | 2.42 µs [2.42 µs, 2.43 µs] | 2.35 µs [2.35 µs, 2.36 µs] | -2.9% | faster point estimate; marginal CIs separated | 1.03x | +| Random corpus D=2 | det_exact | 3.20 µs [3.19 µs, 3.20 µs] | 3.13 µs [3.13 µs, 3.13 µs] | -2.0% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=2 | det_sign_exact | 141.6 ns [140.8 ns, 143.3 ns] | 136.5 ns [135.7 ns, 137.2 ns] | -3.6% | faster point estimate; marginal CIs separated | 1.04x | +| Random corpus D=2 | solve_exact | 69.25 µs [69.09 µs, 69.50 µs] | 68.00 µs [67.75 µs, 68.99 µs] | -1.8% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=2 | solve_exact_f64_result | 80.02 µs [79.86 µs, 80.24 µs] | 78.04 µs [77.88 µs, 78.24 µs] | -2.5% | faster point estimate; marginal CIs separated | 1.03x | +| Random corpus D=2 | solve_exact_rounded_f64 | 70.51 µs [70.05 µs, 70.90 µs] | 67.94 µs [67.89 µs, 68.00 µs] | -3.6% | faster point estimate; marginal CIs separated | 1.04x | +| Random corpus D=3 | det_exact | 9.03 µs [8.95 µs, 9.10 µs] | 8.81 µs [8.24 µs, 8.89 µs] | -2.5% | faster point estimate; marginal CIs separated | 1.03x | +| Random corpus D=3 | det_sign_exact | 228.6 ns [228.3 ns, 228.8 ns] | 225.0 ns [224.8 ns, 225.1 ns] | -1.6% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=3 | solve_exact | 219.99 µs [219.73 µs, 220.21 µs] | 216.21 µs [215.83 µs, 216.36 µs] | -1.7% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=3 | solve_exact_f64_result | 235.73 µs [234.74 µs, 236.39 µs] | 230.14 µs [229.96 µs, 230.31 µs] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=3 | solve_exact_rounded_f64 | 222.81 µs [222.46 µs, 223.31 µs] | 217.76 µs [217.35 µs, 218.01 µs] | -2.3% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=4 | det_exact | 25.74 µs [25.66 µs, 25.87 µs] | 24.87 µs [24.84 µs, 24.98 µs] | -3.4% | faster point estimate; marginal CIs separated | 1.03x | +| Random corpus D=4 | det_sign_exact | 434.0 ns [432.4 ns, 435.8 ns] | 424.5 ns [423.9 ns, 425.6 ns] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=4 | solve_exact | 511.24 µs [509.70 µs, 513.54 µs] | 493.20 µs [492.65 µs, 493.60 µs] | -3.5% | faster point estimate; marginal CIs separated | 1.04x | +| Random corpus D=4 | solve_exact_f64_result | 531.44 µs [528.39 µs, 535.08 µs] | 513.06 µs [512.52 µs, 513.31 µs] | -3.5% | faster point estimate; marginal CIs separated | 1.04x | +| Random corpus D=4 | solve_exact_rounded_f64 | 502.60 µs [502.00 µs, 503.33 µs] | 494.39 µs [493.41 µs, 495.23 µs] | -1.6% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=5 | det_exact | 47.95 µs [47.47 µs, 48.29 µs] | 46.58 µs [46.42 µs, 46.65 µs] | -2.9% | faster point estimate; marginal CIs separated | 1.03x | +| Random corpus D=5 | det_sign_exact | 48.52 µs [47.83 µs, 48.94 µs] | 51.72 µs [50.78 µs, 52.35 µs] | +6.6% | slower point estimate; marginal CIs separated | 0.94x | +| Random corpus D=5 | solve_exact | 994.36 µs [993.76 µs, 995.50 µs] | 972.74 µs [971.25 µs, 974.13 µs] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | +| Random corpus D=5 | solve_exact_f64_result | 1.02 ms [1.02 ms, 1.02 ms] | 992.23 µs [990.24 µs, 993.69 µs] | -2.9% | faster point estimate; marginal CIs separated | 1.03x | +| Random corpus D=5 | solve_exact_rounded_f64 | 1.03 ms [1.03 ms, 1.04 ms] | 972.11 µs [970.46 µs, 973.74 µs] | -6.0% | faster point estimate; marginal CIs separated | 1.06x | ## vs_linalg -### D=2 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 1.6 ns [1.6 ns, 1.6 ns] | 2.7 ns [2.7 ns, 2.7 ns] | +67.9% | slower point estimate; marginal CIs separated | 0.60x | 0.8 ns [0.8 ns, 0.8 ns] | 99.0 ns [97.7 ns, 99.4 ns] | -| la_stack_det | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -16.0% | faster point estimate; marginal CIs separated | 1.19x | — | — | -| la_stack_lu | 1.3 ns [1.3 ns, 1.4 ns] | 1.9 ns [1.9 ns, 1.9 ns] | +40.5% | slower point estimate; marginal CIs separated | 0.71x | 1.6 ns [1.6 ns, 1.6 ns] | 87.9 ns [87.6 ns, 88.2 ns] | -| la_stack_ldlt | 2.1 ns [2.1 ns, 2.1 ns] | 6.7 ns [6.6 ns, 6.8 ns] | +225.3% | slower point estimate; marginal CIs separated | 0.31x | 1.7 ns [1.7 ns, 1.7 ns] | 79.3 ns [78.8 ns, 79.7 ns] | -| la_stack_lu_solve | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | -0.6% | faster point estimate; marginal CIs separated | 1.01x | 4.5 ns [4.5 ns, 4.5 ns] | 147.8 ns [147.2 ns, 148.8 ns] | -| la_stack_ldlt_solve | 4.0 ns [4.0 ns, 4.0 ns] | 10.0 ns [9.9 ns, 10.0 ns] | +146.2% | slower point estimate; marginal CIs separated | 0.41x | 2.8 ns [2.8 ns, 2.8 ns] | 122.8 ns [122.2 ns, 123.1 ns] | -| la_stack_solve_from_lu | 1.2 ns [1.2 ns, 1.2 ns] | 1.3 ns [1.2 ns, 1.3 ns] | +0.7% | slower point estimate; marginal CIs separated | 0.99x | 2.7 ns [2.7 ns, 2.7 ns] | 47.2 ns [47.1 ns, 47.4 ns] | -| la_stack_solve_from_ldlt | 1.2 ns [1.2 ns, 1.2 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +3.4% | slower point estimate; marginal CIs separated | 0.97x | 1.3 ns [1.3 ns, 1.3 ns] | 38.6 ns [38.0 ns, 39.0 ns] | -| la_stack_det_from_lu | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +4.7% | slower point estimate; marginal CIs separated | 0.95x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -| la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +0.4% | slower point estimate; marginal CIs separated | 1.00x | 0.4 ns [0.4 ns, 0.4 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -| la_stack_dot | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -14.0% | faster point estimate; marginal CIs separated | 1.16x | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -| la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -20.2% | faster point estimate; marginal CIs separated | 1.25x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.2 ns, 4.2 ns] | -| la_stack_inf_norm | 0.8 ns [0.8 ns, 0.8 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -24.2% | faster point estimate; marginal CIs separated | 1.32x | 0.5 ns [0.5 ns, 0.5 ns] | 0.8 ns [0.8 ns, 0.8 ns] | - -### D=3 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 7.5 ns [7.5 ns, 7.5 ns] | 10.6 ns [10.6 ns, 10.6 ns] | +41.0% | slower point estimate; marginal CIs separated | 0.71x | 16.7 ns [16.6 ns, 16.8 ns] | 137.9 ns [137.1 ns, 138.9 ns] | -| la_stack_det | 1.5 ns [1.5 ns, 1.5 ns] | 2.2 ns [2.2 ns, 2.2 ns] | +46.4% | slower point estimate; marginal CIs separated | 0.68x | — | — | -| la_stack_lu | 9.1 ns [9.1 ns, 9.1 ns] | 8.5 ns [8.4 ns, 8.5 ns] | -7.0% | faster point estimate; marginal CIs separated | 1.08x | 15.2 ns [15.1 ns, 15.4 ns] | 127.3 ns [126.3 ns, 128.1 ns] | -| la_stack_ldlt | 7.1 ns [7.1 ns, 7.1 ns] | 14.0 ns [14.0 ns, 14.1 ns] | +97.2% | slower point estimate; marginal CIs separated | 0.51x | 4.1 ns [4.1 ns, 4.1 ns] | 93.6 ns [93.2 ns, 94.0 ns] | -| la_stack_lu_solve | 9.6 ns [9.6 ns, 9.7 ns] | 9.9 ns [9.9 ns, 9.9 ns] | +2.8% | slower point estimate; marginal CIs separated | 0.97x | 23.0 ns [22.9 ns, 23.2 ns] | 191.2 ns [190.5 ns, 191.8 ns] | -| la_stack_ldlt_solve | 8.6 ns [8.6 ns, 8.7 ns] | 15.4 ns [15.3 ns, 15.4 ns] | +78.5% | slower point estimate; marginal CIs separated | 0.56x | 8.2 ns [8.2 ns, 8.2 ns] | 137.6 ns [137.3 ns, 138.2 ns] | -| la_stack_solve_from_lu | 2.1 ns [2.1 ns, 2.1 ns] | 2.1 ns [2.1 ns, 2.1 ns] | -0.3% | faster point estimate; marginal CIs separated | 1.00x | 4.4 ns [4.4 ns, 4.4 ns] | 49.5 ns [49.3 ns, 49.7 ns] | -| la_stack_solve_from_ldlt | 1.8 ns [1.8 ns, 1.8 ns] | 1.8 ns [1.8 ns, 1.8 ns] | +0.0% | marginal CIs overlap | 1.00x | 2.9 ns [2.9 ns, 2.9 ns] | 38.2 ns [37.8 ns, 38.5 ns] | -| la_stack_det_from_lu | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +28.1% | slower point estimate; marginal CIs separated | 0.78x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | -| la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +40.3% | slower point estimate; marginal CIs separated | 0.71x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -| la_stack_dot | 0.8 ns [0.8 ns, 0.8 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -12.8% | faster point estimate; marginal CIs separated | 1.15x | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | -| la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -18.4% | faster point estimate; marginal CIs separated | 1.22x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.1 ns, 4.2 ns] | -| la_stack_inf_norm | 1.7 ns [1.7 ns, 1.7 ns] | 1.3 ns [1.3 ns, 1.3 ns] | -24.1% | faster point estimate; marginal CIs separated | 1.32x | 1.1 ns [1.1 ns, 1.1 ns] | 1.3 ns [1.3 ns, 1.3 ns] | - -### D=4 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 12.6 ns [12.6 ns, 12.7 ns] | 17.0 ns [16.9 ns, 17.0 ns] | +34.3% | slower point estimate; marginal CIs separated | 0.74x | 30.6 ns [30.5 ns, 30.6 ns] | 162.0 ns [161.2 ns, 164.0 ns] | -| la_stack_det | 6.1 ns [6.1 ns, 6.1 ns] | 4.9 ns [4.9 ns, 4.9 ns] | -20.1% | faster point estimate; marginal CIs separated | 1.25x | — | — | -| la_stack_lu | 13.7 ns [13.6 ns, 13.7 ns] | 13.8 ns [13.8 ns, 13.8 ns] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | 29.6 ns [29.5 ns, 29.7 ns] | 148.2 ns [147.6 ns, 148.9 ns] | -| la_stack_ldlt | 12.6 ns [12.6 ns, 12.7 ns] | 21.5 ns [21.4 ns, 21.5 ns] | +69.8% | slower point estimate; marginal CIs separated | 0.59x | 10.0 ns [10.0 ns, 10.1 ns] | 115.8 ns [115.2 ns, 116.2 ns] | -| la_stack_lu_solve | 22.2 ns [22.1 ns, 22.2 ns] | 21.6 ns [21.6 ns, 21.6 ns] | -2.8% | faster point estimate; marginal CIs separated | 1.03x | 51.9 ns [51.8 ns, 52.0 ns] | 217.5 ns [216.4 ns, 218.8 ns] | -| la_stack_ldlt_solve | 17.5 ns [17.5 ns, 17.6 ns] | 26.8 ns [26.8 ns, 26.8 ns] | +52.7% | slower point estimate; marginal CIs separated | 0.65x | 14.9 ns [14.8 ns, 14.9 ns] | 159.4 ns [159.2 ns, 159.8 ns] | -| la_stack_solve_from_lu | 4.0 ns [4.0 ns, 4.0 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -0.2% | faster point estimate; marginal CIs separated | 1.00x | 5.1 ns [5.1 ns, 5.1 ns] | 51.8 ns [51.7 ns, 51.9 ns] | -| la_stack_solve_from_ldlt | 2.5 ns [2.5 ns, 2.5 ns] | 2.5 ns [2.5 ns, 2.5 ns] | +0.1% | marginal CIs overlap | 1.00x | 5.5 ns [5.5 ns, 5.5 ns] | 39.0 ns [38.7 ns, 39.5 ns] | -| la_stack_det_from_lu | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | +21.1% | slower point estimate; marginal CIs separated | 0.83x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | -| la_stack_det_from_ldlt | 0.7 ns [0.7 ns, 0.7 ns] | 1.0 ns [0.9 ns, 1.0 ns] | +47.9% | slower point estimate; marginal CIs separated | 0.68x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | -| la_stack_dot | 1.0 ns [1.0 ns, 1.0 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -29.3% | faster point estimate; marginal CIs separated | 1.41x | 0.7 ns [0.7 ns, 0.7 ns] | 1.2 ns [1.2 ns, 1.2 ns] | -| la_stack_norm2_sq | 0.7 ns [0.7 ns, 0.7 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -32.7% | faster point estimate; marginal CIs separated | 1.49x | 0.5 ns [0.5 ns, 0.5 ns] | 4.2 ns [4.1 ns, 4.2 ns] | -| la_stack_inf_norm | 3.0 ns [3.0 ns, 3.0 ns] | 2.2 ns [2.2 ns, 2.2 ns] | -25.4% | faster point estimate; marginal CIs separated | 1.34x | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | - -### D=5 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 26.1 ns [26.0 ns, 26.1 ns] | 37.8 ns [37.8 ns, 38.0 ns] | +45.0% | slower point estimate; marginal CIs separated | 0.69x | 55.6 ns [55.4 ns, 55.7 ns] | 203.2 ns [201.2 ns, 206.1 ns] | -| la_stack_det | 26.5 ns [26.4 ns, 26.5 ns] | 39.6 ns [39.6 ns, 39.9 ns] | +49.6% | slower point estimate; marginal CIs separated | 0.67x | — | — | -| la_stack_lu | 26.8 ns [26.7 ns, 26.9 ns] | 32.3 ns [32.2 ns, 32.5 ns] | +20.6% | slower point estimate; marginal CIs separated | 0.83x | 54.7 ns [54.6 ns, 54.9 ns] | 194.4 ns [191.2 ns, 196.0 ns] | -| la_stack_ldlt | 19.8 ns [19.7 ns, 19.9 ns] | 42.2 ns [42.1 ns, 42.2 ns] | +113.0% | slower point estimate; marginal CIs separated | 0.47x | 14.7 ns [14.6 ns, 14.9 ns] | 137.3 ns [136.8 ns, 137.7 ns] | -| la_stack_lu_solve | 45.7 ns [45.6 ns, 46.1 ns] | 45.6 ns [44.9 ns, 46.1 ns] | -0.2% | marginal CIs overlap | 1.00x | 68.9 ns [68.8 ns, 69.0 ns] | 323.2 ns [302.8 ns, 329.6 ns] | -| la_stack_ldlt_solve | 24.7 ns [24.7 ns, 24.8 ns] | 54.9 ns [46.3 ns, 55.5 ns] | +121.7% | slower point estimate; marginal CIs separated | 0.45x | 61.5 ns [61.0 ns, 61.9 ns] | 219.5 ns [215.2 ns, 229.6 ns] | -| la_stack_solve_from_lu | 6.2 ns [6.2 ns, 6.3 ns] | 6.4 ns [6.4 ns, 6.4 ns] | +2.5% | slower point estimate; marginal CIs separated | 0.98x | 8.0 ns [8.0 ns, 8.1 ns] | 89.3 ns [88.2 ns, 89.9 ns] | -| la_stack_solve_from_ldlt | 3.9 ns [3.9 ns, 3.9 ns] | 3.9 ns [3.9 ns, 3.9 ns] | +1.4% | slower point estimate; marginal CIs separated | 0.99x | 9.2 ns [9.1 ns, 9.3 ns] | 66.4 ns [65.7 ns, 66.8 ns] | -| la_stack_det_from_lu | 0.9 ns [0.9 ns, 0.9 ns] | 1.7 ns [1.7 ns, 1.7 ns] | +98.3% | slower point estimate; marginal CIs separated | 0.50x | 0.7 ns [0.7 ns, 0.7 ns] | 1.5 ns [1.5 ns, 1.5 ns] | -| la_stack_det_from_ldlt | 0.8 ns [0.8 ns, 0.8 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +51.4% | slower point estimate; marginal CIs separated | 0.66x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | -| la_stack_dot | 1.2 ns [1.2 ns, 1.2 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -32.9% | faster point estimate; marginal CIs separated | 1.49x | 0.7 ns [0.7 ns, 0.7 ns] | 1.4 ns [1.4 ns, 1.4 ns] | -| la_stack_norm2_sq | 0.9 ns [0.9 ns, 0.9 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -38.6% | faster point estimate; marginal CIs separated | 1.63x | 0.6 ns [0.6 ns, 0.6 ns] | 4.3 ns [4.2 ns, 4.3 ns] | -| la_stack_inf_norm | 4.7 ns [4.7 ns, 4.7 ns] | 3.4 ns [3.4 ns, 3.4 ns] | -26.9% | faster point estimate; marginal CIs separated | 1.37x | 3.2 ns [3.2 ns, 3.2 ns] | 3.2 ns [3.2 ns, 3.2 ns] | - -### D=8 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 83.5 ns [83.1 ns, 83.7 ns] | 88.3 ns [88.1 ns, 88.7 ns] | +5.7% | slower point estimate; marginal CIs separated | 0.95x | 137.9 ns [137.5 ns, 138.5 ns] | 280.8 ns [279.8 ns, 281.8 ns] | -| la_stack_det | 81.9 ns [81.6 ns, 82.1 ns] | 90.3 ns [90.1 ns, 91.1 ns] | +10.3% | slower point estimate; marginal CIs separated | 0.91x | — | — | -| la_stack_lu | 83.7 ns [82.9 ns, 84.6 ns] | 82.5 ns [82.2 ns, 82.6 ns] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | 123.5 ns [123.2 ns, 124.5 ns] | 265.6 ns [263.8 ns, 266.7 ns] | -| la_stack_ldlt | 100.9 ns [100.7 ns, 101.1 ns] | 92.0 ns [90.4 ns, 92.3 ns] | -8.8% | faster point estimate; marginal CIs separated | 1.10x | 97.7 ns [97.4 ns, 97.8 ns] | 207.9 ns [205.9 ns, 209.1 ns] | -| la_stack_lu_solve | 127.2 ns [126.9 ns, 127.5 ns] | 142.9 ns [141.7 ns, 143.7 ns] | +12.3% | slower point estimate; marginal CIs separated | 0.89x | 165.3 ns [164.7 ns, 166.2 ns] | 367.7 ns [367.0 ns, 368.7 ns] | -| la_stack_ldlt_solve | 119.7 ns [119.5 ns, 119.8 ns] | 105.1 ns [104.8 ns, 105.4 ns] | -12.2% | faster point estimate; marginal CIs separated | 1.14x | 148.2 ns [147.6 ns, 148.3 ns] | 272.8 ns [272.0 ns, 273.8 ns] | -| la_stack_solve_from_lu | 13.5 ns [13.5 ns, 13.5 ns] | 13.5 ns [13.5 ns, 13.5 ns] | -0.2% | faster point estimate; marginal CIs separated | 1.00x | 13.4 ns [13.3 ns, 13.4 ns] | 94.9 ns [94.7 ns, 95.6 ns] | -| la_stack_solve_from_ldlt | 8.3 ns [8.3 ns, 8.3 ns] | 8.2 ns [8.2 ns, 8.2 ns] | -0.5% | faster point estimate; marginal CIs separated | 1.00x | 21.9 ns [21.9 ns, 22.0 ns] | 69.5 ns [69.4 ns, 69.7 ns] | -| la_stack_det_from_lu | 1.3 ns [1.3 ns, 1.3 ns] | 2.2 ns [2.2 ns, 2.5 ns] | +69.6% | slower point estimate; marginal CIs separated | 0.59x | 1.0 ns [1.0 ns, 1.0 ns] | 2.2 ns [2.2 ns, 2.2 ns] | -| la_stack_det_from_ldlt | 1.3 ns [1.3 ns, 1.3 ns] | 2.5 ns [2.4 ns, 2.5 ns] | +94.9% | slower point estimate; marginal CIs separated | 0.51x | 0.9 ns [0.9 ns, 0.9 ns] | 2.1 ns [2.0 ns, 2.1 ns] | -| la_stack_dot | 1.5 ns [1.5 ns, 1.6 ns] | 1.0 ns [1.0 ns, 1.0 ns] | -33.3% | faster point estimate; marginal CIs separated | 1.50x | 1.1 ns [1.1 ns, 1.1 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -| la_stack_norm2_sq | 1.3 ns [1.3 ns, 1.3 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -48.8% | faster point estimate; marginal CIs separated | 1.95x | 0.7 ns [0.7 ns, 0.7 ns] | 4.1 ns [4.1 ns, 4.1 ns] | -| la_stack_inf_norm | 12.4 ns [12.4 ns, 12.5 ns] | 8.4 ns [8.4 ns, 8.4 ns] | -32.2% | faster point estimate; marginal CIs separated | 1.48x | 8.0 ns [8.0 ns, 8.1 ns] | 8.1 ns [8.1 ns, 8.1 ns] | -| la_stack_lu_pivoting | 103.7 ns [103.4 ns, 104.0 ns] | 91.3 ns [90.9 ns, 91.6 ns] | -12.0% | faster point estimate; marginal CIs separated | 1.14x | — | — | -| la_stack_lu_ill_conditioned | 96.7 ns [96.5 ns, 96.9 ns] | 82.5 ns [82.4 ns, 82.7 ns] | -14.7% | faster point estimate; marginal CIs separated | 1.17x | — | — | -| la_stack_ldlt_ill_conditioned | 100.4 ns [100.2 ns, 100.5 ns] | 92.0 ns [90.2 ns, 92.5 ns] | -8.4% | faster point estimate; marginal CIs separated | 1.09x | — | — | - -### D=16 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 398.9 ns [396.4 ns, 402.7 ns] | 427.7 ns [426.4 ns, 428.5 ns] | +7.2% | slower point estimate; marginal CIs separated | 0.93x | 460.8 ns [458.2 ns, 466.5 ns] | 662.5 ns [659.9 ns, 664.7 ns] | -| la_stack_det | 393.5 ns [392.8 ns, 395.5 ns] | 457.5 ns [444.9 ns, 461.4 ns] | +16.3% | slower point estimate; marginal CIs separated | 0.86x | — | — | -| la_stack_lu | 367.8 ns [367.3 ns, 368.7 ns] | 399.6 ns [398.4 ns, 404.0 ns] | +8.6% | slower point estimate; marginal CIs separated | 0.92x | 468.6 ns [468.4 ns, 469.5 ns] | 642.3 ns [637.6 ns, 646.4 ns] | -| la_stack_ldlt | 449.6 ns [448.6 ns, 450.4 ns] | 393.0 ns [392.5 ns, 393.5 ns] | -12.6% | faster point estimate; marginal CIs separated | 1.14x | 405.6 ns [405.2 ns, 405.8 ns] | 416.8 ns [415.5 ns, 418.1 ns] | -| la_stack_lu_solve | 638.4 ns [636.2 ns, 639.9 ns] | 654.7 ns [653.6 ns, 656.0 ns] | +2.6% | slower point estimate; marginal CIs separated | 0.98x | 577.2 ns [576.7 ns, 577.9 ns] | 894.6 ns [891.7 ns, 896.5 ns] | -| la_stack_ldlt_solve | 518.9 ns [517.5 ns, 520.2 ns] | 451.7 ns [451.4 ns, 452.3 ns] | -12.9% | faster point estimate; marginal CIs separated | 1.15x | 649.4 ns [647.5 ns, 651.8 ns] | 590.8 ns [588.9 ns, 593.7 ns] | -| la_stack_solve_from_lu | 196.9 ns [196.4 ns, 197.5 ns] | 197.2 ns [196.3 ns, 198.0 ns] | +0.2% | marginal CIs overlap | 1.00x | 93.4 ns [93.3 ns, 93.5 ns] | 239.7 ns [239.3 ns, 240.2 ns] | -| la_stack_solve_from_ldlt | 28.2 ns [28.1 ns, 28.3 ns] | 27.7 ns [27.7 ns, 27.7 ns] | -1.7% | faster point estimate; marginal CIs separated | 1.02x | 123.6 ns [123.3 ns, 123.8 ns] | 177.5 ns [177.1 ns, 177.8 ns] | -| la_stack_det_from_lu | 2.6 ns [2.6 ns, 2.6 ns] | 3.7 ns [3.7 ns, 3.7 ns] | +42.5% | slower point estimate; marginal CIs separated | 0.70x | 1.8 ns [1.8 ns, 1.8 ns] | 4.8 ns [4.7 ns, 4.9 ns] | -| la_stack_det_from_ldlt | 2.5 ns [2.5 ns, 2.6 ns] | 3.3 ns [3.2 ns, 3.5 ns] | +30.0% | slower point estimate; marginal CIs separated | 0.77x | 1.7 ns [1.7 ns, 1.7 ns] | 4.4 ns [4.4 ns, 4.4 ns] | -| la_stack_dot | 3.1 ns [3.0 ns, 3.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -26.3% | faster point estimate; marginal CIs separated | 1.36x | 1.9 ns [1.9 ns, 1.9 ns] | 4.6 ns [4.5 ns, 4.6 ns] | -| la_stack_norm2_sq | 2.7 ns [2.6 ns, 2.7 ns] | 2.1 ns [2.0 ns, 2.1 ns] | -23.1% | faster point estimate; marginal CIs separated | 1.30x | 1.5 ns [1.5 ns, 1.5 ns] | 4.1 ns [4.1 ns, 4.1 ns] | -| la_stack_inf_norm | 49.8 ns [49.5 ns, 50.0 ns] | 32.7 ns [32.6 ns, 32.7 ns] | -34.3% | faster point estimate; marginal CIs separated | 1.52x | 31.7 ns [31.7 ns, 31.7 ns] | 32.6 ns [32.5 ns, 32.6 ns] | - -### D=32 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 2.11 µs [2.10 µs, 2.11 µs] | 2.21 µs [2.21 µs, 2.22 µs] | +5.0% | slower point estimate; marginal CIs separated | 0.95x | 2.51 µs [2.50 µs, 2.52 µs] | 2.30 µs [2.28 µs, 2.31 µs] | -| la_stack_det | 2.01 µs [2.00 µs, 2.02 µs] | 2.60 µs [2.60 µs, 2.61 µs] | +29.6% | slower point estimate; marginal CIs separated | 0.77x | — | — | -| la_stack_lu | 2.20 µs [2.18 µs, 2.21 µs] | 2.07 µs [2.06 µs, 2.07 µs] | -6.1% | faster point estimate; marginal CIs separated | 1.07x | 2.16 µs [2.16 µs, 2.16 µs] | 2.26 µs [2.25 µs, 2.27 µs] | -| la_stack_ldlt | 2.81 µs [2.81 µs, 2.82 µs] | 2.53 µs [2.52 µs, 2.54 µs] | -9.9% | faster point estimate; marginal CIs separated | 1.11x | 2.11 µs [2.10 µs, 2.11 µs] | 1.42 µs [1.42 µs, 1.42 µs] | -| la_stack_lu_solve | 2.68 µs [2.68 µs, 2.68 µs] | 2.79 µs [2.79 µs, 2.80 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | 2.45 µs [2.45 µs, 2.46 µs] | 2.86 µs [2.86 µs, 2.87 µs] | -| la_stack_ldlt_solve | 3.27 µs [3.26 µs, 3.27 µs] | 2.94 µs [2.92 µs, 2.95 µs] | -10.1% | faster point estimate; marginal CIs separated | 1.11x | 2.79 µs [2.79 µs, 2.79 µs] | 1.91 µs [1.91 µs, 1.91 µs] | -| la_stack_solve_from_lu | 688.9 ns [686.2 ns, 692.0 ns] | 674.0 ns [672.6 ns, 674.9 ns] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | 331.4 ns [331.1 ns, 331.8 ns] | 619.2 ns [617.2 ns, 622.7 ns] | -| la_stack_solve_from_ldlt | 303.6 ns [302.9 ns, 304.0 ns] | 307.3 ns [306.8 ns, 307.7 ns] | +1.2% | slower point estimate; marginal CIs separated | 0.99x | 566.9 ns [564.8 ns, 570.2 ns] | 463.7 ns [463.0 ns, 464.2 ns] | -| la_stack_det_from_lu | 7.0 ns [7.0 ns, 7.0 ns] | 13.9 ns [13.4 ns, 14.0 ns] | +98.2% | slower point estimate; marginal CIs separated | 0.50x | 3.1 ns [3.1 ns, 3.1 ns] | 8.8 ns [8.8 ns, 8.8 ns] | -| la_stack_det_from_ldlt | 6.3 ns [6.3 ns, 6.4 ns] | 12.9 ns [12.9 ns, 13.0 ns] | +105.4% | slower point estimate; marginal CIs separated | 0.49x | 3.0 ns [3.0 ns, 3.0 ns] | 8.5 ns [8.5 ns, 8.5 ns] | -| la_stack_dot | 7.5 ns [7.5 ns, 7.6 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -46.4% | faster point estimate; marginal CIs separated | 1.87x | 4.7 ns [4.7 ns, 4.7 ns] | 15.1 ns [15.1 ns, 15.1 ns] | -| la_stack_norm2_sq | 7.5 ns [7.5 ns, 7.5 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -46.9% | faster point estimate; marginal CIs separated | 1.88x | 3.9 ns [3.8 ns, 3.9 ns] | 4.2 ns [4.2 ns, 4.2 ns] | -| la_stack_inf_norm | 202.4 ns [202.2 ns, 202.7 ns] | 128.9 ns [128.6 ns, 129.1 ns] | -36.3% | faster point estimate; marginal CIs separated | 1.57x | 159.6 ns [157.6 ns, 161.3 ns] | 163.8 ns [163.5 ns, 164.1 ns] | - -### D=64 - -| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | -|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| -| la_stack_det_via_lu | 15.18 µs [15.16 µs, 15.21 µs] | 15.60 µs [15.58 µs, 15.65 µs] | +2.7% | slower point estimate; marginal CIs separated | 0.97x | 15.01 µs [14.78 µs, 15.10 µs] | 10.70 µs [10.68 µs, 10.71 µs] | -| la_stack_det | 15.15 µs [15.13 µs, 15.17 µs] | 15.48 µs [15.44 µs, 15.51 µs] | +2.2% | slower point estimate; marginal CIs separated | 0.98x | — | — | -| la_stack_lu | 15.34 µs [15.31 µs, 15.37 µs] | 14.66 µs [14.64 µs, 14.69 µs] | -4.4% | faster point estimate; marginal CIs separated | 1.05x | 13.67 µs [13.65 µs, 13.70 µs] | 10.50 µs [10.48 µs, 10.56 µs] | -| la_stack_ldlt | 21.53 µs [21.49 µs, 21.58 µs] | 20.42 µs [20.39 µs, 20.49 µs] | -5.2% | faster point estimate; marginal CIs separated | 1.05x | 11.41 µs [10.97 µs, 11.50 µs] | 8.86 µs [8.85 µs, 8.87 µs] | -| la_stack_lu_solve | 17.48 µs [17.44 µs, 17.50 µs] | 17.24 µs [17.21 µs, 17.41 µs] | -1.3% | faster point estimate; marginal CIs separated | 1.01x | 14.84 µs [14.80 µs, 14.86 µs] | 12.20 µs [12.18 µs, 12.23 µs] | -| la_stack_ldlt_solve | 24.60 µs [24.49 µs, 24.76 µs] | 22.70 µs [22.63 µs, 22.84 µs] | -7.7% | faster point estimate; marginal CIs separated | 1.08x | 14.06 µs [14.02 µs, 14.10 µs] | 10.12 µs [10.11 µs, 10.12 µs] | -| la_stack_solve_from_lu | 2.64 µs [2.63 µs, 2.65 µs] | 2.55 µs [2.54 µs, 2.56 µs] | -3.4% | faster point estimate; marginal CIs separated | 1.04x | 786.0 ns [778.1 ns, 788.8 ns] | 1.66 µs [1.66 µs, 1.66 µs] | -| la_stack_solve_from_ldlt | 1.10 µs [1.10 µs, 1.10 µs] | 1.07 µs [1.07 µs, 1.07 µs] | -2.7% | faster point estimate; marginal CIs separated | 1.03x | 1.29 µs [1.29 µs, 1.29 µs] | 1.25 µs [1.24 µs, 1.25 µs] | -| la_stack_det_from_lu | 27.0 ns [27.0 ns, 27.1 ns] | 31.9 ns [31.8 ns, 32.5 ns] | +17.9% | slower point estimate; marginal CIs separated | 0.85x | 8.7 ns [8.7 ns, 8.7 ns] | 21.6 ns [21.6 ns, 21.7 ns] | -| la_stack_det_from_ldlt | 27.5 ns [27.0 ns, 28.7 ns] | 31.6 ns [31.6 ns, 31.6 ns] | +14.8% | slower point estimate; marginal CIs separated | 0.87x | 8.5 ns [8.5 ns, 8.5 ns] | 21.0 ns [20.9 ns, 21.1 ns] | -| la_stack_dot | 38.4 ns [38.4 ns, 38.5 ns] | 11.0 ns [10.9 ns, 11.0 ns] | -71.5% | faster point estimate; marginal CIs separated | 3.51x | 9.0 ns [9.0 ns, 9.0 ns] | 36.6 ns [36.6 ns, 36.7 ns] | -| la_stack_norm2_sq | 38.2 ns [38.2 ns, 38.3 ns] | 10.8 ns [10.8 ns, 10.8 ns] | -71.8% | faster point estimate; marginal CIs separated | 3.54x | 7.4 ns [7.4 ns, 7.4 ns] | 6.2 ns [6.2 ns, 6.2 ns] | -| la_stack_inf_norm | 1.92 µs [1.92 µs, 1.92 µs] | 621.3 ns [620.2 ns, 621.7 ns] | -67.6% | faster point estimate; marginal CIs separated | 3.09x | 1.14 µs [1.13 µs, 1.15 µs] | 1.57 µs [1.57 µs, 1.57 µs] | +| Case | Benchmark | v0.4.4 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.4 nalgebra | v0.4.4 faer | +|:-----|:----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| D=16 | la_stack_det | 432.1 ns [428.1 ns, 433.9 ns] | 436.8 ns [435.3 ns, 438.7 ns] | +1.1% | slower point estimate; marginal CIs separated | 0.99x | — | — | +| D=16 | la_stack_det_from_ldlt | 3.5 ns [3.4 ns, 3.6 ns] | 2.9 ns [2.9 ns, 2.9 ns] | -17.7% | faster point estimate; marginal CIs separated | 1.21x | 1.7 ns [1.7 ns, 1.7 ns] | 4.5 ns [4.5 ns, 4.6 ns] | +| D=16 | la_stack_det_from_lu | 4.2 ns [4.1 ns, 4.3 ns] | 3.3 ns [3.3 ns, 3.3 ns] | -22.4% | faster point estimate; marginal CIs separated | 1.29x | 1.8 ns [1.8 ns, 1.8 ns] | 5.0 ns [4.9 ns, 5.0 ns] | +| D=16 | la_stack_det_via_lu | 423.2 ns [418.2 ns, 427.0 ns] | 422.8 ns [420.8 ns, 424.7 ns] | -0.1% | marginal CIs overlap | 1.00x | 458.2 ns [457.0 ns, 458.6 ns] | 679.3 ns [672.2 ns, 683.9 ns] | +| D=16 | la_stack_dot | 2.4 ns [2.4 ns, 2.4 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -1.0% | faster point estimate; marginal CIs separated | 1.01x | 1.9 ns [1.9 ns, 1.9 ns] | 4.6 ns [4.6 ns, 4.6 ns] | +| D=16 | la_stack_inf_norm | 33.5 ns [33.3 ns, 33.6 ns] | 32.7 ns [32.7 ns, 32.7 ns] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | 33.4 ns [33.2 ns, 33.6 ns] | 33.4 ns [33.3 ns, 33.5 ns] | +| D=16 | la_stack_ldlt | 394.2 ns [392.7 ns, 395.9 ns] | 390.6 ns [389.5 ns, 391.2 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | 408.4 ns [407.3 ns, 410.1 ns] | 446.8 ns [443.5 ns, 448.3 ns] | +| D=16 | la_stack_ldlt_solve | 433.6 ns [432.5 ns, 434.6 ns] | 436.1 ns [435.2 ns, 437.4 ns] | +0.6% | slower point estimate; marginal CIs separated | 0.99x | 638.5 ns [637.9 ns, 640.1 ns] | 624.6 ns [623.7 ns, 626.6 ns] | +| D=16 | la_stack_lu | 398.6 ns [397.3 ns, 400.9 ns] | 403.5 ns [401.3 ns, 405.9 ns] | +1.2% | slower point estimate; marginal CIs separated | 0.99x | 465.6 ns [464.7 ns, 466.9 ns] | 658.8 ns [654.2 ns, 665.1 ns] | +| D=16 | la_stack_lu_solve | 667.8 ns [665.2 ns, 672.5 ns] | 672.5 ns [670.9 ns, 674.0 ns] | +0.7% | marginal CIs overlap | 0.99x | 585.3 ns [584.6 ns, 586.3 ns] | 897.2 ns [895.5 ns, 900.9 ns] | +| D=16 | la_stack_norm2_sq | 2.1 ns [2.1 ns, 2.1 ns] | 2.1 ns [2.1 ns, 2.1 ns] | -3.5% | faster point estimate; marginal CIs separated | 1.04x | 1.5 ns [1.5 ns, 1.5 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| D=16 | la_stack_solve_from_ldlt | 28.0 ns [28.0 ns, 28.1 ns] | 27.7 ns [27.7 ns, 27.8 ns] | -1.0% | faster point estimate; marginal CIs separated | 1.01x | 126.5 ns [126.1 ns, 127.1 ns] | 179.6 ns [178.8 ns, 180.2 ns] | +| D=16 | la_stack_solve_from_lu | 204.7 ns [204.0 ns, 205.2 ns] | 203.1 ns [202.5 ns, 204.0 ns] | -0.8% | faster point estimate; marginal CIs separated | 1.01x | 95.3 ns [95.2 ns, 95.5 ns] | 245.0 ns [244.1 ns, 247.1 ns] | +| D=2 | la_stack_det | 0.6 ns [0.6 ns, 0.6 ns] | 0.6 ns [0.6 ns, 0.6 ns] | +2.3% | slower point estimate; marginal CIs separated | 0.98x | — | — | +| D=2 | la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -0.4% | faster point estimate; marginal CIs separated | 1.00x | 0.4 ns [0.4 ns, 0.4 ns] | 0.6 ns [0.6 ns, 0.6 ns] | +| D=2 | la_stack_det_from_lu | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -0.1% | marginal CIs overlap | 1.00x | 0.5 ns [0.5 ns, 0.5 ns] | 0.8 ns [0.8 ns, 0.8 ns] | +| D=2 | la_stack_det_via_lu | 1.8 ns [1.8 ns, 1.8 ns] | 1.8 ns [1.8 ns, 1.8 ns] | +0.6% | slower point estimate; marginal CIs separated | 0.99x | 0.8 ns [0.8 ns, 0.8 ns] | 98.3 ns [97.9 ns, 99.0 ns] | +| D=2 | la_stack_dot | 0.6 ns [0.6 ns, 0.6 ns] | 0.6 ns [0.6 ns, 0.6 ns] | +5.3% | slower point estimate; marginal CIs separated | 0.95x | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| D=2 | la_stack_inf_norm | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -8.9% | faster point estimate; marginal CIs separated | 1.10x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| D=2 | la_stack_ldlt | 6.8 ns [6.8 ns, 6.9 ns] | 7.1 ns [7.1 ns, 7.1 ns] | +3.7% | slower point estimate; marginal CIs separated | 0.96x | 1.7 ns [1.7 ns, 1.7 ns] | 81.6 ns [81.4 ns, 81.8 ns] | +| D=2 | la_stack_ldlt_solve | 9.9 ns [9.9 ns, 9.9 ns] | 10.1 ns [10.0 ns, 10.1 ns] | +2.1% | slower point estimate; marginal CIs separated | 0.98x | 2.7 ns [2.7 ns, 2.7 ns] | 124.6 ns [124.2 ns, 125.1 ns] | +| D=2 | la_stack_lu | 1.6 ns [1.6 ns, 1.6 ns] | 1.6 ns [1.6 ns, 1.6 ns] | +2.4% | slower point estimate; marginal CIs separated | 0.98x | 1.6 ns [1.6 ns, 1.6 ns] | 95.9 ns [95.2 ns, 96.3 ns] | +| D=2 | la_stack_lu_solve | 2.1 ns [2.0 ns, 2.1 ns] | 2.0 ns [2.0 ns, 2.0 ns] | -0.6% | marginal CIs overlap | 1.01x | 4.6 ns [4.6 ns, 4.6 ns] | 151.9 ns [151.7 ns, 152.5 ns] | +| D=2 | la_stack_norm2_sq | 0.4 ns [0.4 ns, 0.4 ns] | 0.4 ns [0.4 ns, 0.4 ns] | +1.0% | slower point estimate; marginal CIs separated | 0.99x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| D=2 | la_stack_solve_from_ldlt | 1.2 ns [1.2 ns, 1.2 ns] | 1.3 ns [1.3 ns, 1.3 ns] | +1.3% | slower point estimate; marginal CIs separated | 0.99x | 1.3 ns [1.3 ns, 1.3 ns] | 37.6 ns [37.4 ns, 37.9 ns] | +| D=2 | la_stack_solve_from_lu | 1.3 ns [1.3 ns, 1.3 ns] | 1.3 ns [1.3 ns, 1.3 ns] | +2.6% | slower point estimate; marginal CIs separated | 0.98x | 2.9 ns [2.9 ns, 2.9 ns] | 48.8 ns [48.6 ns, 49.1 ns] | +| D=3 | la_stack_det | 1.4 ns [1.3 ns, 1.4 ns] | 1.3 ns [1.3 ns, 1.3 ns] | -0.8% | faster point estimate; marginal CIs separated | 1.01x | — | — | +| D=3 | la_stack_det_from_ldlt | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -19.6% | faster point estimate; marginal CIs separated | 1.24x | 0.5 ns [0.5 ns, 0.5 ns] | 0.8 ns [0.8 ns, 0.8 ns] | +| D=3 | la_stack_det_from_lu | 0.7 ns [0.7 ns, 0.7 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -6.0% | faster point estimate; marginal CIs separated | 1.06x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | +| D=3 | la_stack_det_via_lu | 8.8 ns [8.7 ns, 8.8 ns] | 8.8 ns [8.8 ns, 8.9 ns] | +0.7% | slower point estimate; marginal CIs separated | 0.99x | 16.9 ns [16.8 ns, 17.0 ns] | 141.6 ns [140.9 ns, 142.3 ns] | +| D=3 | la_stack_dot | 0.7 ns [0.7 ns, 0.7 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +0.1% | marginal CIs overlap | 1.00x | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | +| D=3 | la_stack_inf_norm | 1.3 ns [1.3 ns, 1.3 ns] | 1.3 ns [1.3 ns, 1.3 ns] | -0.4% | faster point estimate; marginal CIs separated | 1.00x | 1.1 ns [1.1 ns, 1.1 ns] | 1.3 ns [1.3 ns, 1.3 ns] | +| D=3 | la_stack_ldlt | 14.8 ns [14.5 ns, 15.1 ns] | 14.1 ns [14.1 ns, 14.2 ns] | -4.5% | faster point estimate; marginal CIs separated | 1.05x | 4.0 ns [4.0 ns, 4.0 ns] | 97.8 ns [97.6 ns, 98.2 ns] | +| D=3 | la_stack_ldlt_solve | 11.6 ns [11.6 ns, 11.7 ns] | 11.7 ns [11.7 ns, 11.8 ns] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | 6.0 ns [6.0 ns, 6.0 ns] | 142.4 ns [141.9 ns, 143.0 ns] | +| D=3 | la_stack_lu | 8.5 ns [8.4 ns, 8.5 ns] | 8.4 ns [8.4 ns, 8.4 ns] | -1.2% | faster point estimate; marginal CIs separated | 1.01x | 15.4 ns [15.3 ns, 15.5 ns] | 138.0 ns [137.4 ns, 138.6 ns] | +| D=3 | la_stack_lu_solve | 9.9 ns [9.9 ns, 10.0 ns] | 10.0 ns [10.0 ns, 10.0 ns] | +0.5% | marginal CIs overlap | 1.00x | 23.5 ns [23.4 ns, 23.6 ns] | 196.4 ns [194.9 ns, 198.8 ns] | +| D=3 | la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -1.5% | faster point estimate; marginal CIs separated | 1.01x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.1 ns, 4.2 ns] | +| D=3 | la_stack_solve_from_ldlt | 1.8 ns [1.8 ns, 1.8 ns] | 1.8 ns [1.8 ns, 1.8 ns] | +0.8% | slower point estimate; marginal CIs separated | 0.99x | 2.9 ns [2.9 ns, 2.9 ns] | 40.6 ns [40.1 ns, 41.0 ns] | +| D=3 | la_stack_solve_from_lu | 2.1 ns [2.1 ns, 2.1 ns] | 2.1 ns [2.1 ns, 2.1 ns] | +0.5% | marginal CIs overlap | 1.00x | 4.6 ns [4.6 ns, 4.6 ns] | 48.6 ns [48.5 ns, 48.7 ns] | +| D=32 | la_stack_det | 2.22 µs [2.21 µs, 2.23 µs] | 2.17 µs [2.17 µs, 2.18 µs] | -2.3% | faster point estimate; marginal CIs separated | 1.02x | — | — | +| D=32 | la_stack_det_from_ldlt | 14.1 ns [14.1 ns, 14.1 ns] | 7.9 ns [7.9 ns, 7.9 ns] | -44.0% | faster point estimate; marginal CIs separated | 1.79x | 3.0 ns [3.0 ns, 3.0 ns] | 8.7 ns [8.7 ns, 8.8 ns] | +| D=32 | la_stack_det_from_lu | 14.1 ns [14.0 ns, 14.1 ns] | 9.1 ns [9.1 ns, 9.1 ns] | -35.2% | faster point estimate; marginal CIs separated | 1.54x | 3.1 ns [3.1 ns, 3.1 ns] | 9.0 ns [8.9 ns, 9.0 ns] | +| D=32 | la_stack_det_via_lu | 2.23 µs [2.21 µs, 2.23 µs] | 2.04 µs [2.03 µs, 2.04 µs] | -8.4% | faster point estimate; marginal CIs separated | 1.09x | 2.53 µs [2.52 µs, 2.54 µs] | 2.37 µs [2.36 µs, 2.38 µs] | +| D=32 | la_stack_dot | 4.1 ns [4.1 ns, 4.1 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -1.2% | faster point estimate; marginal CIs separated | 1.01x | 4.8 ns [4.8 ns, 4.8 ns] | 15.2 ns [15.2 ns, 15.2 ns] | +| D=32 | la_stack_inf_norm | 131.6 ns [131.4 ns, 131.8 ns] | 130.4 ns [130.0 ns, 130.7 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | 157.7 ns [157.2 ns, 158.1 ns] | 167.1 ns [166.9 ns, 167.3 ns] | +| D=32 | la_stack_ldlt | 2.47 µs [2.46 µs, 2.47 µs] | 2.52 µs [2.51 µs, 2.52 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | 2.13 µs [2.12 µs, 2.13 µs] | 1.50 µs [1.49 µs, 1.51 µs] | +| D=32 | la_stack_ldlt_solve | 2.83 µs [2.82 µs, 2.84 µs] | 2.84 µs [2.83 µs, 2.86 µs] | +0.4% | marginal CIs overlap | 1.00x | 2.78 µs [2.77 µs, 2.79 µs] | 1.97 µs [1.97 µs, 1.98 µs] | +| D=32 | la_stack_lu | 2.15 µs [2.14 µs, 2.15 µs] | 2.07 µs [2.07 µs, 2.08 µs] | -3.6% | faster point estimate; marginal CIs separated | 1.04x | 2.21 µs [2.20 µs, 2.22 µs] | 2.30 µs [2.29 µs, 2.30 µs] | +| D=32 | la_stack_lu_solve | 2.87 µs [2.85 µs, 2.89 µs] | 2.78 µs [2.77 µs, 2.78 µs] | -3.3% | faster point estimate; marginal CIs separated | 1.03x | 2.50 µs [2.49 µs, 2.51 µs] | 2.95 µs [2.95 µs, 2.96 µs] | +| D=32 | la_stack_norm2_sq | 4.0 ns [4.0 ns, 4.1 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -1.2% | faster point estimate; marginal CIs separated | 1.01x | 3.9 ns [3.9 ns, 3.9 ns] | 4.3 ns [4.3 ns, 4.3 ns] | +| D=32 | la_stack_solve_from_ldlt | 316.1 ns [315.9 ns, 316.5 ns] | 313.6 ns [313.2 ns, 314.1 ns] | -0.8% | faster point estimate; marginal CIs separated | 1.01x | 568.0 ns [566.1 ns, 569.7 ns] | 466.2 ns [464.8 ns, 467.1 ns] | +| D=32 | la_stack_solve_from_lu | 728.4 ns [724.4 ns, 732.7 ns] | 686.0 ns [684.4 ns, 687.3 ns] | -5.8% | faster point estimate; marginal CIs separated | 1.06x | 335.2 ns [334.7 ns, 336.0 ns] | 620.9 ns [619.3 ns, 621.8 ns] | +| D=4 | la_stack_det | 2.6 ns [2.5 ns, 2.6 ns] | 2.6 ns [2.6 ns, 2.6 ns] | +1.3% | slower point estimate; marginal CIs separated | 0.99x | — | — | +| D=4 | la_stack_det_from_ldlt | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -20.3% | faster point estimate; marginal CIs separated | 1.25x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | +| D=4 | la_stack_det_from_lu | 1.0 ns [1.0 ns, 1.0 ns] | 0.9 ns [0.9 ns, 0.9 ns] | -13.1% | faster point estimate; marginal CIs separated | 1.15x | 0.6 ns [0.6 ns, 0.6 ns] | 1.3 ns [1.3 ns, 1.3 ns] | +| D=4 | la_stack_det_via_lu | 14.3 ns [14.2 ns, 14.3 ns] | 14.4 ns [14.4 ns, 14.4 ns] | +0.8% | slower point estimate; marginal CIs separated | 0.99x | 31.3 ns [31.2 ns, 31.4 ns] | 172.0 ns [169.9 ns, 177.8 ns] | +| D=4 | la_stack_dot | 0.7 ns [0.7 ns, 0.7 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -0.2% | marginal CIs overlap | 1.00x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| D=4 | la_stack_inf_norm | 2.3 ns [2.2 ns, 2.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | +0.3% | slower point estimate; marginal CIs separated | 1.00x | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | +| D=4 | la_stack_ldlt | 22.2 ns [22.2 ns, 22.2 ns] | 22.0 ns [22.0 ns, 22.1 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | 8.0 ns [8.0 ns, 8.0 ns] | 121.3 ns [121.0 ns, 122.0 ns] | +| D=4 | la_stack_ldlt_solve | 24.1 ns [24.1 ns, 24.2 ns] | 24.0 ns [24.0 ns, 24.1 ns] | -0.4% | marginal CIs overlap | 1.00x | 11.6 ns [11.6 ns, 11.6 ns] | 169.0 ns [168.3 ns, 169.4 ns] | +| D=4 | la_stack_lu | 14.0 ns [13.9 ns, 14.0 ns] | 13.9 ns [13.8 ns, 13.9 ns] | -0.8% | faster point estimate; marginal CIs separated | 1.01x | 30.0 ns [29.9 ns, 30.1 ns] | 165.5 ns [164.6 ns, 168.1 ns] | +| D=4 | la_stack_lu_solve | 22.0 ns [21.9 ns, 22.0 ns] | 21.9 ns [21.8 ns, 21.9 ns] | -0.5% | faster point estimate; marginal CIs separated | 1.00x | 54.7 ns [54.6 ns, 54.9 ns] | 223.9 ns [222.6 ns, 225.8 ns] | +| D=4 | la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +0.5% | marginal CIs overlap | 1.00x | 0.5 ns [0.5 ns, 0.5 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| D=4 | la_stack_solve_from_ldlt | 2.6 ns [2.5 ns, 2.6 ns] | 2.5 ns [2.5 ns, 2.5 ns] | -2.5% | faster point estimate; marginal CIs separated | 1.03x | 5.9 ns [5.9 ns, 5.9 ns] | 41.5 ns [41.0 ns, 41.9 ns] | +| D=4 | la_stack_solve_from_lu | 4.0 ns [4.0 ns, 4.1 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -1.9% | faster point estimate; marginal CIs separated | 1.02x | 5.9 ns [5.9 ns, 5.9 ns] | 59.5 ns [57.6 ns, 61.1 ns] | +| D=5 | la_stack_det | 41.0 ns [40.8 ns, 41.2 ns] | 62.2 ns [41.0 ns, 62.6 ns] | +51.6% | marginal CIs overlap | 0.66x | — | — | +| D=5 | la_stack_det_from_ldlt | 1.3 ns [1.3 ns, 1.3 ns] | 1.0 ns [1.0 ns, 1.0 ns] | -24.3% | faster point estimate; marginal CIs separated | 1.32x | 0.6 ns [0.6 ns, 0.6 ns] | 1.3 ns [1.3 ns, 1.3 ns] | +| D=5 | la_stack_det_from_lu | 2.0 ns [1.9 ns, 2.0 ns] | 1.1 ns [1.1 ns, 1.1 ns] | -42.9% | faster point estimate; marginal CIs separated | 1.75x | 0.7 ns [0.7 ns, 0.7 ns] | 1.5 ns [1.5 ns, 1.5 ns] | +| D=5 | la_stack_det_via_lu | 33.3 ns [33.2 ns, 33.5 ns] | 32.6 ns [32.6 ns, 32.8 ns] | -2.0% | faster point estimate; marginal CIs separated | 1.02x | 57.6 ns [57.5 ns, 57.8 ns] | 206.8 ns [206.4 ns, 207.7 ns] | +| D=5 | la_stack_dot | 0.9 ns [0.8 ns, 0.9 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | 0.7 ns [0.7 ns, 0.8 ns] | 1.5 ns [1.5 ns, 1.5 ns] | +| D=5 | la_stack_inf_norm | 3.5 ns [3.5 ns, 3.5 ns] | 3.5 ns [3.5 ns, 3.5 ns] | +1.1% | slower point estimate; marginal CIs separated | 0.99x | 3.3 ns [3.3 ns, 3.3 ns] | 3.2 ns [3.2 ns, 3.2 ns] | +| D=5 | la_stack_ldlt | 43.8 ns [43.7 ns, 43.9 ns] | 43.5 ns [43.4 ns, 43.5 ns] | -0.8% | faster point estimate; marginal CIs separated | 1.01x | 27.7 ns [26.9 ns, 28.7 ns] | 150.0 ns [149.1 ns, 150.8 ns] | +| D=5 | la_stack_ldlt_solve | 47.2 ns [47.1 ns, 47.3 ns] | 47.1 ns [47.0 ns, 47.1 ns] | -0.2% | marginal CIs overlap | 1.00x | 42.3 ns [42.1 ns, 42.4 ns] | 210.3 ns [209.8 ns, 210.9 ns] | +| D=5 | la_stack_lu | 32.8 ns [32.7 ns, 32.9 ns] | 33.0 ns [32.6 ns, 33.2 ns] | +0.6% | marginal CIs overlap | 0.99x | 57.7 ns [57.5 ns, 57.9 ns] | 199.0 ns [198.0 ns, 200.1 ns] | +| D=5 | la_stack_lu_solve | 44.9 ns [44.4 ns, 45.9 ns] | 44.5 ns [44.3 ns, 44.7 ns] | -0.8% | marginal CIs overlap | 1.01x | 71.2 ns [71.1 ns, 71.3 ns] | 293.4 ns [290.1 ns, 306.4 ns] | +| D=5 | la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -1.1% | faster point estimate; marginal CIs separated | 1.01x | 0.6 ns [0.6 ns, 0.6 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| D=5 | la_stack_solve_from_ldlt | 3.9 ns [3.9 ns, 3.9 ns] | 4.0 ns [3.9 ns, 4.0 ns] | +1.2% | slower point estimate; marginal CIs separated | 0.99x | 9.7 ns [9.7 ns, 9.8 ns] | 66.3 ns [63.8 ns, 67.5 ns] | +| D=5 | la_stack_solve_from_lu | 6.1 ns [6.1 ns, 6.1 ns] | 6.1 ns [6.1 ns, 6.1 ns] | -0.0% | marginal CIs overlap | 1.00x | 9.4 ns [9.4 ns, 9.4 ns] | 89.2 ns [88.9 ns, 89.5 ns] | +| D=64 | la_stack_det | 14.99 µs [14.95 µs, 15.03 µs] | 14.95 µs [14.88 µs, 14.98 µs] | -0.3% | marginal CIs overlap | 1.00x | — | — | +| D=64 | la_stack_det_from_ldlt | 32.5 ns [32.4 ns, 32.5 ns] | 23.4 ns [23.4 ns, 23.4 ns] | -27.9% | faster point estimate; marginal CIs separated | 1.39x | 8.7 ns [8.7 ns, 8.7 ns] | 21.6 ns [21.5 ns, 21.6 ns] | +| D=64 | la_stack_det_from_lu | 33.5 ns [33.1 ns, 33.5 ns] | 23.6 ns [23.6 ns, 23.7 ns] | -29.3% | faster point estimate; marginal CIs separated | 1.41x | 8.9 ns [8.9 ns, 9.0 ns] | 22.4 ns [22.4 ns, 22.5 ns] | +| D=64 | la_stack_det_via_lu | 15.03 µs [15.01 µs, 15.06 µs] | 14.86 µs [14.84 µs, 14.88 µs] | -1.1% | faster point estimate; marginal CIs separated | 1.01x | 13.17 µs [13.14 µs, 13.19 µs] | 10.70 µs [10.68 µs, 10.71 µs] | +| D=64 | la_stack_dot | 11.2 ns [11.2 ns, 11.3 ns] | 11.0 ns [11.0 ns, 11.0 ns] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | 9.1 ns [9.1 ns, 9.1 ns] | 38.2 ns [38.1 ns, 38.3 ns] | +| D=64 | la_stack_inf_norm | 643.8 ns [642.5 ns, 645.5 ns] | 630.3 ns [628.7 ns, 631.3 ns] | -2.1% | faster point estimate; marginal CIs separated | 1.02x | 1.31 µs [1.30 µs, 1.31 µs] | 1.59 µs [1.58 µs, 1.59 µs] | +| D=64 | la_stack_ldlt | 21.88 µs [21.82 µs, 22.01 µs] | 19.83 µs [19.80 µs, 19.85 µs] | -9.3% | faster point estimate; marginal CIs separated | 1.10x | 10.93 µs [10.92 µs, 10.94 µs] | 9.03 µs [9.02 µs, 9.04 µs] | +| D=64 | la_stack_ldlt_solve | 24.52 µs [24.39 µs, 24.60 µs] | 22.45 µs [22.22 µs, 22.49 µs] | -8.5% | faster point estimate; marginal CIs separated | 1.09x | 12.91 µs [12.89 µs, 12.94 µs] | 10.37 µs [10.35 µs, 10.40 µs] | +| D=64 | la_stack_lu | 14.45 µs [14.42 µs, 14.52 µs] | 14.75 µs [14.74 µs, 14.76 µs] | +2.1% | slower point estimate; marginal CIs separated | 0.98x | 12.98 µs [12.94 µs, 13.01 µs] | 10.56 µs [10.55 µs, 10.58 µs] | +| D=64 | la_stack_lu_solve | 17.51 µs [17.47 µs, 17.55 µs] | 17.36 µs [17.26 µs, 17.41 µs] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | 13.88 µs [13.85 µs, 13.90 µs] | 12.20 µs [12.18 µs, 12.22 µs] | +| D=64 | la_stack_norm2_sq | 11.2 ns [11.2 ns, 11.3 ns] | 11.0 ns [10.9 ns, 11.0 ns] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | 7.4 ns [7.4 ns, 7.4 ns] | 6.3 ns [6.3 ns, 6.3 ns] | +| D=64 | la_stack_solve_from_ldlt | 1.11 µs [1.11 µs, 1.12 µs] | 1.09 µs [1.09 µs, 1.10 µs] | -1.8% | faster point estimate; marginal CIs separated | 1.02x | 1.32 µs [1.31 µs, 1.32 µs] | 1.24 µs [1.24 µs, 1.24 µs] | +| D=64 | la_stack_solve_from_lu | 2.72 µs [2.72 µs, 2.74 µs] | 2.76 µs [2.70 µs, 2.79 µs] | +1.3% | marginal CIs overlap | 0.99x | 796.6 ns [795.2 ns, 797.9 ns] | 1.65 µs [1.65 µs, 1.66 µs] | +| D=8 | la_stack_det | 94.9 ns [94.2 ns, 95.9 ns] | 94.7 ns [94.4 ns, 95.1 ns] | -0.2% | marginal CIs overlap | 1.00x | — | — | +| D=8 | la_stack_det_from_ldlt | 2.7 ns [2.7 ns, 2.7 ns] | 1.3 ns [1.3 ns, 1.3 ns] | -51.7% | faster point estimate; marginal CIs separated | 2.07x | 0.9 ns [0.9 ns, 0.9 ns] | 2.0 ns [2.0 ns, 2.0 ns] | +| D=8 | la_stack_det_from_ldlt_balanced_range | 9.3 ns [9.3 ns, 9.4 ns] | 8.8 ns [8.8 ns, 8.8 ns] | -6.0% | faster point estimate; marginal CIs separated | 1.06x | — | — | +| D=8 | la_stack_det_from_lu | 2.8 ns [2.8 ns, 2.9 ns] | 1.5 ns [1.5 ns, 1.5 ns] | -47.4% | faster point estimate; marginal CIs separated | 1.90x | 1.0 ns [1.0 ns, 1.0 ns] | 2.3 ns [2.3 ns, 2.3 ns] | +| D=8 | la_stack_det_from_lu_balanced_range | 9.3 ns [9.3 ns, 9.3 ns] | 9.3 ns [9.3 ns, 9.4 ns] | +0.3% | marginal CIs overlap | 1.00x | — | — | +| D=8 | la_stack_det_via_lu | 88.0 ns [87.4 ns, 88.7 ns] | 85.2 ns [84.9 ns, 85.3 ns] | -3.2% | faster point estimate; marginal CIs separated | 1.03x | 143.3 ns [143.0 ns, 143.7 ns] | 295.2 ns [293.3 ns, 297.7 ns] | +| D=8 | la_stack_dot | 1.0 ns [1.0 ns, 1.0 ns] | 0.9 ns [0.9 ns, 0.9 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | 1.1 ns [1.1 ns, 1.1 ns] | 2.3 ns [2.3 ns, 2.3 ns] | +| D=8 | la_stack_inf_norm | 8.5 ns [8.5 ns, 8.5 ns] | 8.5 ns [8.5 ns, 8.5 ns] | -0.3% | faster point estimate; marginal CIs separated | 1.00x | 8.1 ns [8.1 ns, 8.1 ns] | 8.2 ns [8.1 ns, 8.2 ns] | +| D=8 | la_stack_ldlt | 94.1 ns [93.1 ns, 94.7 ns] | 92.3 ns [91.4 ns, 93.2 ns] | -1.8% | marginal CIs overlap | 1.02x | 107.4 ns [106.7 ns, 107.9 ns] | 230.5 ns [226.0 ns, 233.4 ns] | +| D=8 | la_stack_ldlt_ill_conditioned | 93.3 ns [91.8 ns, 94.6 ns] | 91.9 ns [90.9 ns, 92.7 ns] | -1.5% | marginal CIs overlap | 1.02x | — | — | +| D=8 | la_stack_ldlt_solve | 103.4 ns [103.1 ns, 103.8 ns] | 100.8 ns [100.6 ns, 101.1 ns] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | 147.7 ns [141.5 ns, 150.9 ns] | 289.2 ns [288.4 ns, 290.9 ns] | +| D=8 | la_stack_lu | 85.6 ns [85.3 ns, 86.0 ns] | 83.5 ns [83.1 ns, 83.7 ns] | -2.5% | faster point estimate; marginal CIs separated | 1.03x | 143.1 ns [141.4 ns, 145.1 ns] | 278.4 ns [277.5 ns, 280.4 ns] | +| D=8 | la_stack_lu_ill_conditioned | 86.4 ns [85.9 ns, 87.0 ns] | 83.1 ns [82.9 ns, 83.2 ns] | -3.9% | faster point estimate; marginal CIs separated | 1.04x | — | — | +| D=8 | la_stack_lu_pivoting | 94.6 ns [94.4 ns, 94.8 ns] | 90.5 ns [90.4 ns, 90.7 ns] | -4.3% | faster point estimate; marginal CIs separated | 1.05x | — | — | +| D=8 | la_stack_lu_solve | 149.5 ns [149.1 ns, 150.1 ns] | 145.4 ns [138.0 ns, 146.6 ns] | -2.7% | faster point estimate; marginal CIs separated | 1.03x | 188.4 ns [187.4 ns, 189.0 ns] | 381.9 ns [380.5 ns, 383.1 ns] | +| D=8 | la_stack_norm2_sq | 0.7 ns [0.7 ns, 0.7 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +0.1% | marginal CIs overlap | 1.00x | 0.7 ns [0.7 ns, 0.7 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| D=8 | la_stack_solve_from_ldlt | 8.1 ns [8.1 ns, 8.2 ns] | 8.1 ns [8.1 ns, 8.2 ns] | +0.3% | marginal CIs overlap | 1.00x | 23.5 ns [23.4 ns, 23.7 ns] | 71.9 ns [71.4 ns, 72.2 ns] | +| D=8 | la_stack_solve_from_lu | 13.8 ns [13.7 ns, 13.8 ns] | 13.7 ns [13.6 ns, 13.7 ns] | -0.9% | faster point estimate; marginal CIs separated | 1.01x | 16.8 ns [16.8 ns, 16.8 ns] | 96.9 ns [96.7 ns, 97.4 ns] | ## How to Update diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 3c0dcd2..0f10470 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,344 +1,237 @@ # Releasing la-stack -This guide documents the release flow for `vX.Y.Z`: prepare a dedicated -release PR, merge it, create the final annotated tag from the generated -changelog, publish to crates.io, and create the GitHub release. +Prepare each `vX.Y.Z` release in a dedicated PR. After that PR is merged, +create the annotated tag, publish to crates.io, and create the GitHub release. -The release changelog is generated with `git-cliff --tag` through -`just changelog-unreleased`, so no temporary local tag is needed. +The changelog is generated for the target tag before the tag exists, so the +release process does not require a temporary local tag. -Applies to versions vX.Y.Z. Prefer updating documentation before publishing -to crates.io. +Release recipes are content-idempotent where their inputs permit it. Repeating +`update-version` on the same UTC date or repeating `performance-readme` for the +same retained bundle produces no file changes. Crossing UTC midnight +intentionally advances the release date, and repeated `performance-release` +runs can produce different measurements. ---- +## Prepare the environment -## Conventions and environment - -The current MSRV and pinned release-validation toolchain are Rust 1.98.0. Keep -`Cargo.toml`, `rust-toolchain.toml`, and `clippy.toml` aligned whenever a future -release deliberately changes that baseline. - -Set these variables to avoid repeating the version string: +Set the target tag once: ```bash -# tag has the leading v, version does not TAG=vX.Y.Z -VERSION=${TAG#v} -PREVIOUS_TAG=vA.B.C ``` -Verify your git remotes: +Verify the repository remotes and synchronize `main`: ```bash +gh auth status git remote -v +git switch main +git pull --ff-only ``` -Ensure your local `main` is up to date before beginning: +Refresh dependency requirements, lockfiles, and repository-owned Cargo tool +pins before creating the release branch: ```bash -git switch main -git pull --ff-only +just update ``` ---- - -## Step 1: Create a clean release PR +Review any tracked changes and land them separately before continuing with the +release PR, then synchronize `main` again. This keeps dependency and tool +upgrades independently reviewable; `just update-version` deliberately does not +run `just update`. -This PR should primarily include version bumps, changelog updates, benchmark -comparison updates, and documentation updates. All major code changes should -already be on `main`. +`update-version` and the release performance recipes use GitHub's published +stable releases as the authoritative release history. -Finalize release-facing metadata and documentation in this dedicated release -PR. Ordinary feature, fix, review, and hygiene work should not preemptively -bump versions or prepare release artifacts. +## Step 1: Prepare the release PR -Small, critical fixes discovered during the release process may be included, -but keep them minimal and release-critical. +Keep the release PR focused on version metadata, the generated changelog, +benchmark artifacts, and release documentation. Major code changes should +already be on `main`. -1. Create the release branch +### 1. Create the release branch ```bash git switch -c "release/$TAG" ``` -2. Bump versions - -Preferred, if `cargo-edit` is installed: +### 2. Update release metadata ```bash -cargo set-version "$VERSION" +just update-version "$TAG" ``` -Alternative: edit `Cargo.toml` manually and update `version = "..."` under -`[package]`. - -Update release metadata to match the crate version: - -- `CITATION.cff`: update `version`, the version-specific DOI identifier, and - `date-released`. Use the UTC calendar date written into the generated - changelog heading; keep the all-versions concept DOI as the primary `doi`. -- `pyproject.toml`: update `[project] version` for the Python utility package +The recipe requires a stable `vX.Y.Z` target that is not older than any +published stable GitHub release and that has at least one earlier published +stable release. It infers the previous release from GitHub and updates the Rust +and Python package metadata and lockfiles, `CITATION.cff`, README dependency and +non-artifact links, and active benchmark examples. `date-released` uses the +current UTC date; if the target changelog section already exists, its date is +updated in the same transaction. `CITATION.cff` retains the Zenodo all-versions +concept DOI. -Review the citation identity fields at the same time: author name and contact, -ORCID, repository URL, and license. Preserve la-stack's Zenodo concept DOI -(`all versions`) as the primary `doi` unless the archival policy is deliberately -changed; retain the release DOI as a version-specific identifier. +The recipe validates the synchronized references but does not update dependency +requirements, generate the changelog, or run benchmarks. Review its diff before +continuing. -Refresh both committed lockfiles after those manual metadata edits: +### 3. Generate the release changelog ```bash -cargo metadata --format-version 1 --no-deps > /dev/null -uv lock +just changelog-unreleased "$TAG" ``` -Review version references in documentation: +This generates `CHANGELOG.md` as though the target tag already existed, archives +completed minor series under `docs/archive/changelog/`, and synchronizes the +changelog heading with the UTC preparation date recorded in `CITATION.cff`. +Review the generated changelog and any archive changes. -```bash -just docs-version-check -``` - -The automated check covers package metadata, lockfiles, README dependency -snippets, release-pinned README links, and current-tag arguments in the active -benchmark workflow examples. Historical prose, compatibility notes, archived -reports, baseline arguments, and tool versions intentionally remain independent -of the current package version. +### 4. Generate the release performance comparison -3. Generate the release changelog +Run this after the package version has been updated: ```bash -# Generates CHANGELOG.md as though TAG already exists, then applies -# markdown hygiene and archives completed minor release series. -just changelog-unreleased "$TAG" +just performance-release ``` -`just changelog-unreleased` runs -`GIT_CLIFF_OFFLINE=true git-cliff --tag "$TAG" -o CHANGELOG.md`, then -`postprocess-changelog`, then `archive-changelog`. The generated tagged-release -changelog may begin directly with the active minor series; when git-cliff emits -an Unreleased block, the archiver preserves it. Older completed minor series -live under `docs/archive/changelog/`. +The no-argument form compares the current package version with the previous +stable published release. Review `docs/PERFORMANCE.md`, any archived comparison +under `docs/archive/performance/`, and the retained CSV and provenance JSON under +`target/bench-reports/`. -4. Run benchmarks and update the README comparison table +The temporary current worktree includes staged and unstaged changes to tracked +files, but excludes untracked files. Stage any new benchmark-relevant file before +running the comparison. Do not run `just clean` or `cargo clean` until the +retained report inputs have been reviewed. + +### 5. Refresh the README benchmark comparison ```bash -# Validate inputs, run a fresh complete vs_linalg benchmark, and atomically -# update the README table plus CSV/SVG/JSON-provenance assets -just plot-vs-linalg-readme +just performance-readme ``` -Review the updated table in `README.md`, the plot and CSV in `docs/assets/`, and -the adjacent provenance JSON. The publication command fails if the independent -correctness gate, canonical-dimension coverage, or required provenance is -incomplete. +This consumes the validated CSV and provenance JSON retained by +`just performance-release`; it does not run benchmarks again. It atomically +updates the table and tag-pinned benchmark links in `README.md` with the CSV, +SVG, and provenance JSON under `docs/assets/bench/`. Until it succeeds, those +README links continue to reference the previous published artifacts. + +See `docs/BENCHMARKING.md` for repair commands, local comparison modes, artifact +ownership, and provenance details. -5. Update the release performance comparison +### 6. Validate the release branch ```bash -# Infers TAG from Cargo.toml, compares it against the previous stable published -# release, writes docs/PERFORMANCE.md, and archives the previous docs/PERFORMANCE.md -# under docs/archive/performance/. -just performance-release +just ci +cargo publish --locked --allow-dirty --dry-run ``` -Run this only after the package version has been bumped. The no-argument form -uses the `Cargo.toml` version as the current release and discovers the previous -stable published release. For an explicit repair, the supplied current tag must -match the package version in the `HEAD` checkout; mismatches are rejected before -tag fetching, worktree creation, or benchmarking. Release report publication -also requires distinct current and baseline release identifiers. +`just ci` includes the lockfile, citation, documentation, test, and benchmark +compile checks. -Review `docs/PERFORMANCE.md` for the latest release-to-release comparison. Older -committed comparisons are archived under `docs/archive/performance/` with -lexicographically sorted filenames such as `v0.4.2-vs-v0.4.1.md`. Iterative -local reports still live under `target/bench-reports/`. +### 7. Review, stage, and commit the release artifacts -The temporary current worktree includes staged and unstaged changes to tracked -files. Untracked files are excluded, so stage every new benchmark-relevant file -before running the release comparison. - -The release command retains `target/bench-reports/performance.csv` and -`performance.provenance.json` before its temporary worktrees are removed, then -renders and promotes Markdown from a validated reload of that pair. Review the -CSV coverage/timing rows and JSON release, revision, command, toolchain, host, -harness, and digest metadata alongside the Markdown. For a presentation-only -correction, run `just performance-doc`; it reproduces and promotes the -report from those files without invoking Cargo or creating worktrees. The same -command can consume a pair retained by `performance-local` after the version -bump. It rejects same-version local artifacts because committed performance docs -must compare distinct releases. The command updates `docs/PERFORMANCE.md` and the -performance archive, so review and stage those changes as release artifacts. -The retained pair is local scratch and may be removed by `just clean` or -`cargo clean`, so do not clean `target/` until the release report review is -complete. - -For an explicit measurement repair, run -`just performance-release `. To compare the stored -GitHub Release assets instead of running Cargo locally, use -`just performance-github-assets`. The local release workflow validates and then -compiles both library revisions with the current checkout's hashed benchmark -harness, recording source-state, environment, toolchain, dependency, Criterion, -and validation provenance. Stored release assets retain their original -per-release harnesses; unavailable historical measurement metadata is labelled -explicitly rather than treated as an isolated library-code comparison. - -`just performance-local` is the non-promoting half of the release workflow: it -measures and writes `target/bench-reports/performance.md` plus the adjacent -retained CSV/JSON pair. The local report may compare revisions with the same -package version because commit/ref and source-state provenance distinguish them. -For a distinct pair, following it with `performance-doc` is equivalent to -`performance-release`; the latter performs fresh measurement, retention, and -rollback-capable promotion atomically. `performance-local-non-exact` uses the -same metric and report model for the narrowed peer-context view but writes a -separate `performance-non-exact.*` scratch bundle. - -After the GitHub Release is published, the `Release Benchmarks` workflow checks -out the release tag, runs the independent benchmark-input tests, saves a full -Criterion baseline, and attaches -`la-stack-$TAG-criterion-baseline.tar.gz` to the release. That release asset is -the durable native Criterion archive for historical baseline comparisons. The -compact release-report CSV is an analysis and report-reproduction layer, not a -replacement for that raw archive. The workflow also uploads a short-lived -Actions artifact for debugging the run. - -See `docs/BENCHMARKING.md` for local saved-baseline workflows and the full -comparison command reference. - -6. Validate the release branch +Inspect all changes before staging: ```bash -just ci -just cargo-lock-check -just citation-check -cargo publish --locked --allow-dirty --dry-run +git status --short +git --no-pager diff ``` -7. Stage and commit release artifacts +Expected release artifacts include package metadata and lockfiles, +`CITATION.cff`, `CHANGELOG.md`, `README.md`, `docs/PERFORMANCE.md`, and generated +files under `docs/archive/` and `docs/assets/bench/`. Stage only the reviewed +paths that were intentionally changed; do not stage the entire `docs/` tree. +Then inspect the staged diff and commit it: ```bash -git add Cargo.toml Cargo.lock CITATION.cff pyproject.toml uv.lock CHANGELOG.md README.md docs/ +git --no-pager diff --cached git commit -m "chore(release): release $TAG - Bump version to $TAG - Update citation and utility package metadata -- Update changelog with latest changes -- Update benchmark comparison table and release performance report -- Update documentation for release" +- Generate the release changelog +- Update benchmark and performance artifacts +- Update release documentation" ``` -8. Push the branch and open a PR +### 8. Push the branch and open the PR ```bash git push -u origin "release/$TAG" ``` -PR metadata: +Use `chore(release): release $TAG` as the PR title and describe the PR as a +focused release preparation without feature work. -- Title: chore(release): release $TAG -- Description: Clean release PR with version bump, changelog, and - documentation updates. No feature work. +### Handling fixes found during preparation -### Handling fixes discovered during the release process +For a critical fix that must be included, make and commit the fix, rerun +`just changelog-unreleased "$TAG"`, review and stage only the resulting changelog +files, and commit that generated update separately. -If you discover issues after generating the changelog: +For a non-critical fix, file an issue and defer it to a later release. Do not +hand-edit the generated changelog to add a known-issue note. -1. For critical fixes that must be in this release, make and commit the fix, - then regenerate the release changelog: +## Step 2: Publish after the PR is merged - ```bash - just changelog-unreleased "$TAG" - git add CHANGELOG.md docs/archive/changelog/ - git commit -m "docs: update changelog with release fixes" - ``` - -2. For non-critical fixes, document them as known issues in the release notes - or include them in the next release. - ---- - -## Step 2: After the PR is merged into main - -1. Sync your local `main` to the merge commit +### 1. Synchronize `main` ```bash git switch main git pull --ff-only ``` -2. Create the final annotated tag using the changelog content +### 2. Create and verify the annotated tag ```bash -# Creates the annotated tag from the matching CHANGELOG.md section. -# Archived versions are read from docs/archive/changelog/ automatically. -# For large changelogs (>125KB), the tag message points to the changelog -# section instead of embedding the full content. just tag "$TAG" +git --no-pager tag -l --format='%(contents)' "$TAG" ``` -3. Optional: verify the tag message content +`just tag` builds the annotation from the matching active or archived changelog +section. For a changelog larger than 125 kB, the annotation points to that +section instead of embedding it. -```bash -git tag -l --format='%(contents)' "$TAG" -``` - -4. Push the tag +### 3. Push the tag ```bash git push origin "$TAG" ``` -5. Publish to crates.io +### 4. Publish to crates.io ```bash -# Publish the crate (ensure docs are already updated on main via the PR) cargo publish --locked ``` -6. Create the GitHub release with notes from the tag annotation +### 5. Create the GitHub release ```bash -# Requires GitHub CLI (gh) and authenticated session gh release create "$TAG" --title "$TAG" --notes-from-tag ``` -Always set the GitHub release title to the exact tag string, including the -leading `v`. +Keep the release title identical to the tag, including its leading `v`. -7. Verify the durable Criterion baseline asset +### 6. Verify the durable Criterion baseline -After the `Release Benchmarks` workflow completes, verify that the GitHub -release contains the expected long-lived baseline archive: +After the `Release Benchmarks` workflow completes, verify that the release +contains its long-lived baseline archive: ```bash gh release view "$TAG" --json assets \ --jq ".assets[] | select(.name == \"la-stack-$TAG-criterion-baseline.tar.gz\") | .name" | cat ``` -The command must print `la-stack-$TAG-criterion-baseline.tar.gz`. An Actions -artifact alone is not a durable release baseline. +The command must print `la-stack-$TAG-criterion-baseline.tar.gz`. A short-lived +Actions artifact is not a substitute for this release asset. -8. Clean up the merged release branch +### 7. Remove the merged release branch -After publishing and asset verification succeed, remove the release branch -locally and on the remote: +After publication and baseline verification succeed: ```bash git branch -d "release/$TAG" git push origin --delete "release/$TAG" ``` - ---- - -## Notes and tips - -- Do not create a temporary local release tag for changelog generation; use - `just changelog-unreleased "$TAG"`. -- Keep the release PR scoped to version, changelog, archive, benchmark - comparison, and documentation changes. -- `just changelog` regenerates the current changelog from existing tags and may - update `docs/archive/changelog/`. -- `just changelog-unreleased "$TAG"` is for release PR preparation before the - final tag exists. -- `just tag "$TAG"` is for the final post-merge annotated tag. -- If multiple crates or files reference the version, confirm all of them are - updated consistently. diff --git a/docs/archive/performance/README.md b/docs/archive/performance/README.md index 51696c5..286cec9 100644 --- a/docs/archive/performance/README.md +++ b/docs/archive/performance/README.md @@ -6,3 +6,4 @@ Older release-to-release benchmark comparisons are archived here. - [v0.4.1-vs-v0.4.0](v0.4.1-vs-v0.4.0.md) - [v0.4.2-vs-v0.4.1](v0.4.2-vs-v0.4.1.md) - [v0.4.3-vs-v0.4.2](v0.4.3-vs-v0.4.2.md) +- [v0.4.4-vs-v0.4.3](v0.4.4-vs-v0.4.3.md) diff --git a/docs/archive/performance/v0.4.4-vs-v0.4.3.md b/docs/archive/performance/v0.4.4-vs-v0.4.3.md new file mode 100644 index 0000000..9009a30 --- /dev/null +++ b/docs/archive/performance/v0.4.4-vs-v0.4.3.md @@ -0,0 +1,383 @@ +# Benchmark Performance + +**la-stack** v0.4.4 · `e736c5f` (HEAD) +**Source revision timestamp**: 2026-07-12 17:11:09 UTC (deterministic report metadata; not the benchmark measurement time) +**Benchmark measurement timestamp**: not recorded by Criterion; use the provenance below to identify the measured revisions and environment. +**Statistic**: median +**Suite**: all +**Scope**: release-signal + +## Benchmark Results + +Comparison against baseline **v0.4.3**: + +Negative point-estimate change means the current point estimate is smaller; a baseline/current point-estimate ratio above 1.00 has the same meaning. +The CI-relation column reports only whether the two marginal Criterion intervals overlap. These are not paired confidence intervals +for the change, so the report makes no statistical-significance or performance-improvement claim from interval separation. + +### Reproducibility Provenance + +**Measurement environment**: recorded for both samples under one shared current harness. + +- CPU: unavailable (legacy report recorded architecture only: `arm`) +- OS: `Darwin 25.5.0 arm64` +- rustc: `rustc 1.97.0 (2d8144b78 2026-07-07)` +- Current commit: `e736c5fda155ef23c8712f89ae15bf5369ff3787` +- Current Git clean: `false` +- Current source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2` +- Baseline commit: `dd4ad192a42e28d9cc72b336b40802fe65cea4f0` +- Baseline Git clean: `false` +- Baseline source-state SHA-256: `1d77b9640c3fe8dce45b486052177fe4d9a44b840605661da0717b8cb0cec9b8` +- Cargo.lock SHA-256: `0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd` +- Benchmark harness SHA-256: `c2cc116cf2d77d415f12f30a60bcf3e1b9e0d553c884bc3c57ce6c953124bc08` + +**Publication and validation environment**: + +- Publication CPU: unavailable (legacy report recorded architecture only: `arm`) +- Publication OS: `Darwin 25.5.0 arm64` +- Publication rustc: `rustc 1.97.0 (2d8144b78 2026-07-07)` +- Publication commit: `e736c5fda155ef23c8712f89ae15bf5369ff3787` +- Publication Git clean: `false` +- Publication source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2` +- Publication Cargo.lock SHA-256: `0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd` +- Publication harness SHA-256: `c2cc116cf2d77d415f12f30a60bcf3e1b9e0d553c884bc3c57ce6c953124bc08` +- Criterion suite/scope: `all` / `release-signal` +- Criterion statistic/sample: `median` / `new` +- Criterion dependency version: `0.8.2` +- Baseline command: `just bench-save-baseline v0.4.3` +- Current command: `just bench-latest` +- Correctness gate: `just test-bench-inputs` passed against both the current and baseline revisions using the shared current fixture harness. +- Validated current revision: `e736c5fda155ef23c8712f89ae15bf5369ff3787` (Git clean: `false`; + source-state SHA-256: `fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2`) +- Validated baseline revision: `dd4ad192a42e28d9cc72b336b40802fe65cea4f0` (Git clean: `false`; + source-state SHA-256: `1d77b9640c3fe8dce45b486052177fe4d9a44b840605661da0717b8cb0cec9b8`) +- Baseline API compatibility: `la_stack_v0_4_3_api` selects only source-compatible benchmark calls; + rows outside the baseline's correctness domain remain explicitly unavailable. +- Baseline-unavailable rows: `d8/la_stack_det_from_lu_balanced_range` and + `d8/la_stack_det_from_ldlt_balanced_range` were not timed because v0.4.3 returns zero for a + fixture whose exact determinant is one; current samples remain required, but no speedup is claimed. +- Baseline-unavailable rows: `exact_d2/det_direct_with_errbound`, + `exact_d3/det_direct_with_errbound`, and `exact_d4/det_direct_with_errbound` were not timed + because v0.4.3 predates the paired API; the comparable `det_errbound` baselines remain required. + +## Exact arithmetic + +### D=2 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -11.0% | faster point estimate; marginal CIs separated | 1.12x | +| det_direct | 0.6 ns [0.6 ns, 0.6 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -28.6% | faster point estimate; marginal CIs separated | 1.40x | +| det_errbound | 0.7 ns [0.7 ns, 0.7 ns] | 1.6 ns [1.6 ns, 1.6 ns] | +128.0% | slower point estimate; marginal CIs separated | 0.44x | +| det_exact | 95.7 ns [95.5 ns, 95.8 ns] | 89.3 ns [89.2 ns, 89.4 ns] | -6.7% | faster point estimate; marginal CIs separated | 1.07x | +| det_exact_f64_result | 77.0 ns [76.9 ns, 77.1 ns] | 65.5 ns [65.4 ns, 65.5 ns] | -15.0% | faster point estimate; marginal CIs separated | 1.18x | +| det_exact_rounded_f64 | 245.4 ns [244.9 ns, 245.9 ns] | 66.5 ns [66.5 ns, 66.6 ns] | -72.9% | faster point estimate; marginal CIs separated | 3.69x | +| det_sign_exact | 2.0 ns [2.0 ns, 2.0 ns] | 2.8 ns [2.8 ns, 2.8 ns] | +41.2% | slower point estimate; marginal CIs separated | 0.71x | +| solve_exact | 6.73 µs [6.72 µs, 6.74 µs] | 7.33 µs [7.32 µs, 7.34 µs] | +8.9% | slower point estimate; marginal CIs separated | 0.92x | +| solve_exact_f64_result | 6.89 µs [6.88 µs, 6.91 µs] | 8.38 µs [8.37 µs, 8.39 µs] | +21.6% | slower point estimate; marginal CIs separated | 0.82x | +| solve_exact_rounded_f64 | 7.08 µs [7.07 µs, 7.10 µs] | 7.66 µs [7.64 µs, 7.68 µs] | +8.1% | slower point estimate; marginal CIs separated | 0.93x | + +### D=3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -17.2% | faster point estimate; marginal CIs separated | 1.21x | +| det_direct | 1.0 ns [1.0 ns, 1.0 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -16.6% | faster point estimate; marginal CIs separated | 1.20x | +| det_errbound | 1.6 ns [1.6 ns, 1.6 ns] | 3.4 ns [3.4 ns, 3.4 ns] | +112.5% | slower point estimate; marginal CIs separated | 0.47x | +| det_exact | 334.2 ns [333.8 ns, 335.3 ns] | 320.7 ns [320.2 ns, 321.0 ns] | -4.1% | faster point estimate; marginal CIs separated | 1.04x | +| det_exact_f64_result | 311.4 ns [310.4 ns, 313.1 ns] | 294.9 ns [294.0 ns, 295.3 ns] | -5.3% | faster point estimate; marginal CIs separated | 1.06x | +| det_exact_rounded_f64 | 505.9 ns [503.2 ns, 507.1 ns] | 294.9 ns [294.2 ns, 295.5 ns] | -41.7% | faster point estimate; marginal CIs separated | 1.72x | +| det_sign_exact | 3.7 ns [3.7 ns, 3.7 ns] | 4.7 ns [4.7 ns, 4.7 ns] | +26.9% | slower point estimate; marginal CIs separated | 0.79x | +| solve_exact | 29.70 µs [29.65 µs, 29.77 µs] | 31.46 µs [31.37 µs, 31.51 µs] | +5.9% | slower point estimate; marginal CIs separated | 0.94x | +| solve_exact_f64_result | 29.88 µs [29.80 µs, 30.01 µs] | 33.01 µs [32.96 µs, 33.11 µs] | +10.5% | slower point estimate; marginal CIs separated | 0.90x | +| solve_exact_rounded_f64 | 30.48 µs [30.41 µs, 30.58 µs] | 31.82 µs [31.76 µs, 31.84 µs] | +4.4% | slower point estimate; marginal CIs separated | 0.96x | + +### D=4 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 4.5 ns [4.5 ns, 4.5 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -47.7% | faster point estimate; marginal CIs separated | 1.91x | +| det_direct | 4.3 ns [4.3 ns, 4.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -47.0% | faster point estimate; marginal CIs separated | 1.89x | +| det_errbound | 6.9 ns [6.9 ns, 6.9 ns] | 6.7 ns [6.7 ns, 6.7 ns] | -2.4% | faster point estimate; marginal CIs separated | 1.02x | +| det_exact | 1.10 µs [1.10 µs, 1.10 µs] | 994.3 ns [990.6 ns, 998.2 ns] | -9.5% | faster point estimate; marginal CIs separated | 1.11x | +| det_exact_f64_result | 1.09 µs [1.09 µs, 1.10 µs] | 959.7 ns [955.9 ns, 961.8 ns] | -12.1% | faster point estimate; marginal CIs separated | 1.14x | +| det_exact_rounded_f64 | 1.28 µs [1.27 µs, 1.29 µs] | 963.6 ns [962.2 ns, 964.4 ns] | -24.6% | faster point estimate; marginal CIs separated | 1.33x | +| det_sign_exact | 11.3 ns [11.3 ns, 11.3 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -31.5% | faster point estimate; marginal CIs separated | 1.46x | +| solve_exact | 77.25 µs [77.10 µs, 77.40 µs] | 79.77 µs [79.57 µs, 79.95 µs] | +3.3% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact_f64_result | 76.96 µs [76.80 µs, 77.13 µs] | 83.27 µs [83.05 µs, 83.39 µs] | +8.2% | slower point estimate; marginal CIs separated | 0.92x | +| solve_exact_rounded_f64 | 77.73 µs [77.58 µs, 77.82 µs] | 81.50 µs [81.39 µs, 81.62 µs] | +4.8% | slower point estimate; marginal CIs separated | 0.95x | + +### D=5 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det | 22.0 ns [22.0 ns, 22.1 ns] | 25.5 ns [25.4 ns, 25.6 ns] | +16.0% | slower point estimate; marginal CIs separated | 0.86x | +| det_exact | 3.08 µs [3.08 µs, 3.09 µs] | 2.93 µs [2.93 µs, 2.94 µs] | -5.0% | faster point estimate; marginal CIs separated | 1.05x | +| det_exact_f64_result | 3.07 µs [3.07 µs, 3.07 µs] | 2.88 µs [2.88 µs, 2.89 µs] | -6.2% | faster point estimate; marginal CIs separated | 1.07x | +| det_exact_rounded_f64 | 3.30 µs [3.29 µs, 3.30 µs] | 2.90 µs [2.89 µs, 2.90 µs] | -12.1% | faster point estimate; marginal CIs separated | 1.14x | +| det_sign_exact | 3.06 µs [3.05 µs, 3.07 µs] | 2.99 µs [2.98 µs, 3.00 µs] | -2.4% | faster point estimate; marginal CIs separated | 1.03x | +| solve_exact | 151.99 µs [151.61 µs, 152.39 µs] | 159.24 µs [158.84 µs, 159.40 µs] | +4.8% | slower point estimate; marginal CIs separated | 0.95x | +| solve_exact_f64_result | 151.95 µs [151.73 µs, 152.26 µs] | 162.98 µs [162.62 µs, 163.59 µs] | +7.3% | slower point estimate; marginal CIs separated | 0.93x | +| solve_exact_rounded_f64 | 153.08 µs [152.82 µs, 153.19 µs] | 159.84 µs [159.58 µs, 160.22 µs] | +4.4% | slower point estimate; marginal CIs separated | 0.96x | + +### Random corpus D=2 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 99.8 ns [99.2 ns, 100.4 ns] | 136.1 ns [136.0 ns, 136.7 ns] | +36.4% | slower point estimate; marginal CIs separated | 0.73x | +| det_exact | 3.22 µs [3.22 µs, 3.23 µs] | 3.06 µs [3.05 µs, 3.06 µs] | -5.2% | faster point estimate; marginal CIs separated | 1.05x | +| solve_exact | 65.74 µs [65.66 µs, 65.83 µs] | 67.56 µs [67.42 µs, 67.79 µs] | +2.8% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact_f64_result | 66.67 µs [66.58 µs, 66.75 µs] | 77.80 µs [77.55 µs, 77.91 µs] | +16.7% | slower point estimate; marginal CIs separated | 0.86x | +| solve_exact_rounded_f64 | 66.67 µs [66.58 µs, 66.69 µs] | 69.23 µs [69.09 µs, 69.35 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | + +### Random corpus D=3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 174.3 ns [174.2 ns, 174.5 ns] | 225.1 ns [225.0 ns, 225.3 ns] | +29.1% | slower point estimate; marginal CIs separated | 0.77x | +| det_exact | 8.43 µs [8.39 µs, 8.47 µs] | 7.76 µs [7.72 µs, 7.81 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | +| solve_exact | 212.03 µs [211.85 µs, 212.21 µs] | 216.24 µs [216.02 µs, 216.47 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | +| solve_exact_f64_result | 213.65 µs [213.11 µs, 214.14 µs] | 229.74 µs [229.51 µs, 230.18 µs] | +7.5% | slower point estimate; marginal CIs separated | 0.93x | +| solve_exact_rounded_f64 | 213.73 µs [213.55 µs, 214.10 µs] | 218.02 µs [217.72 µs, 218.45 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | + +### Random corpus D=4 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 534.8 ns [534.5 ns, 535.0 ns] | 426.5 ns [425.8 ns, 427.4 ns] | -20.3% | faster point estimate; marginal CIs separated | 1.25x | +| det_exact | 28.60 µs [28.34 µs, 28.91 µs] | 24.12 µs [24.09 µs, 24.16 µs] | -15.7% | faster point estimate; marginal CIs separated | 1.19x | +| solve_exact | 486.14 µs [485.47 µs, 487.12 µs] | 495.70 µs [494.68 µs, 496.46 µs] | +2.0% | slower point estimate; marginal CIs separated | 0.98x | +| solve_exact_f64_result | 487.15 µs [486.57 µs, 487.66 µs] | 512.02 µs [511.01 µs, 513.36 µs] | +5.1% | slower point estimate; marginal CIs separated | 0.95x | +| solve_exact_rounded_f64 | 489.02 µs [487.59 µs, 489.93 µs] | 497.20 µs [495.79 µs, 498.14 µs] | +1.7% | slower point estimate; marginal CIs separated | 0.98x | + +### Random corpus D=5 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 51.48 µs [51.17 µs, 51.83 µs] | 48.07 µs [47.59 µs, 48.89 µs] | -6.6% | faster point estimate; marginal CIs separated | 1.07x | +| det_exact | 52.09 µs [51.90 µs, 52.27 µs] | 47.94 µs [47.67 µs, 48.20 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | +| solve_exact | 963.73 µs [961.57 µs, 964.57 µs] | 978.88 µs [977.33 µs, 979.71 µs] | +1.6% | slower point estimate; marginal CIs separated | 0.98x | +| solve_exact_f64_result | 960.79 µs [958.85 µs, 962.50 µs] | 995.01 µs [993.26 µs, 997.47 µs] | +3.6% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact_rounded_f64 | 964.92 µs [963.08 µs, 966.28 µs] | 982.14 µs [980.67 µs, 983.66 µs] | +1.8% | slower point estimate; marginal CIs separated | 0.98x | + +### Near-singular 3x3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 350.2 ns [348.8 ns, 352.5 ns] | 349.4 ns [348.4 ns, 350.5 ns] | -0.2% | marginal CIs overlap | 1.00x | +| det_exact | 306.5 ns [305.5 ns, 307.4 ns] | 304.5 ns [303.9 ns, 305.1 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | +| solve_exact | 2.30 µs [2.29 µs, 2.30 µs] | 2.32 µs [2.31 µs, 2.32 µs] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | +| solve_exact_f64_result | 2.30 µs [2.30 µs, 2.31 µs] | 2.42 µs [2.41 µs, 2.42 µs] | +5.0% | slower point estimate; marginal CIs separated | 0.95x | +| solve_exact_rounded_f64 | 2.32 µs [2.31 µs, 2.32 µs] | 2.32 µs [2.32 µs, 2.33 µs] | +0.3% | marginal CIs overlap | 1.00x | + +### Large entries 3x3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 283.6 ns [283.2 ns, 286.8 ns] | 281.7 ns [280.8 ns, 282.9 ns] | -0.7% | faster point estimate; marginal CIs separated | 1.01x | +| det_exact | 286.3 ns [285.5 ns, 288.1 ns] | 295.7 ns [294.0 ns, 296.9 ns] | +3.3% | slower point estimate; marginal CIs separated | 0.97x | +| solve_exact | 90.64 µs [90.57 µs, 90.79 µs] | 94.07 µs [94.01 µs, 94.20 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_f64_result | 91.18 µs [91.03 µs, 91.25 µs] | 95.07 µs [94.88 µs, 95.21 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_rounded_f64 | 91.51 µs [91.41 µs, 91.63 µs] | 94.47 µs [94.34 µs, 94.60 µs] | +3.2% | slower point estimate; marginal CIs separated | 0.97x | + +### Hilbert 4x4 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 11.3 ns [11.3 ns, 11.3 ns] | 7.7 ns [7.7 ns, 7.7 ns] | -31.5% | faster point estimate; marginal CIs separated | 1.46x | +| det_exact | 1.24 µs [1.24 µs, 1.25 µs] | 1.09 µs [1.09 µs, 1.09 µs] | -12.3% | faster point estimate; marginal CIs separated | 1.14x | +| solve_exact | 57.23 µs [57.13 µs, 57.41 µs] | 59.81 µs [59.71 µs, 59.94 µs] | +4.5% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_f64_result | 57.31 µs [57.18 µs, 57.62 µs] | 61.72 µs [61.51 µs, 61.85 µs] | +7.7% | slower point estimate; marginal CIs separated | 0.93x | +| solve_exact_rounded_f64 | 58.02 µs [57.92 µs, 58.14 µs] | 60.30 µs [60.18 µs, 60.43 µs] | +3.9% | slower point estimate; marginal CIs separated | 0.96x | + +### Hilbert 5x5 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | +|-----------|-------:|-------:|-------:|:-----------|--------:| +| det_sign_exact | 3.23 µs [3.22 µs, 3.24 µs] | 3.23 µs [3.23 µs, 3.24 µs] | +0.1% | marginal CIs overlap | 1.00x | +| det_exact | 3.26 µs [3.25 µs, 3.27 µs] | 3.00 µs [2.99 µs, 3.01 µs] | -8.0% | faster point estimate; marginal CIs separated | 1.09x | +| solve_exact | 119.87 µs [119.55 µs, 120.21 µs] | 124.37 µs [123.95 µs, 124.57 µs] | +3.8% | slower point estimate; marginal CIs separated | 0.96x | +| solve_exact_f64_result | 119.44 µs [119.30 µs, 119.59 µs] | 127.21 µs [126.96 µs, 127.34 µs] | +6.5% | slower point estimate; marginal CIs separated | 0.94x | +| solve_exact_rounded_f64 | 120.33 µs [120.13 µs, 120.48 µs] | 125.50 µs [125.24 µs, 125.78 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | + +## vs_linalg + +### D=2 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 1.6 ns [1.6 ns, 1.6 ns] | 2.7 ns [2.7 ns, 2.7 ns] | +67.9% | slower point estimate; marginal CIs separated | 0.60x | 0.8 ns [0.8 ns, 0.8 ns] | 99.0 ns [97.7 ns, 99.4 ns] | +| la_stack_det | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -16.0% | faster point estimate; marginal CIs separated | 1.19x | — | — | +| la_stack_lu | 1.3 ns [1.3 ns, 1.4 ns] | 1.9 ns [1.9 ns, 1.9 ns] | +40.5% | slower point estimate; marginal CIs separated | 0.71x | 1.6 ns [1.6 ns, 1.6 ns] | 87.9 ns [87.6 ns, 88.2 ns] | +| la_stack_ldlt | 2.1 ns [2.1 ns, 2.1 ns] | 6.7 ns [6.6 ns, 6.8 ns] | +225.3% | slower point estimate; marginal CIs separated | 0.31x | 1.7 ns [1.7 ns, 1.7 ns] | 79.3 ns [78.8 ns, 79.7 ns] | +| la_stack_lu_solve | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | -0.6% | faster point estimate; marginal CIs separated | 1.01x | 4.5 ns [4.5 ns, 4.5 ns] | 147.8 ns [147.2 ns, 148.8 ns] | +| la_stack_ldlt_solve | 4.0 ns [4.0 ns, 4.0 ns] | 10.0 ns [9.9 ns, 10.0 ns] | +146.2% | slower point estimate; marginal CIs separated | 0.41x | 2.8 ns [2.8 ns, 2.8 ns] | 122.8 ns [122.2 ns, 123.1 ns] | +| la_stack_solve_from_lu | 1.2 ns [1.2 ns, 1.2 ns] | 1.3 ns [1.2 ns, 1.3 ns] | +0.7% | slower point estimate; marginal CIs separated | 0.99x | 2.7 ns [2.7 ns, 2.7 ns] | 47.2 ns [47.1 ns, 47.4 ns] | +| la_stack_solve_from_ldlt | 1.2 ns [1.2 ns, 1.2 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +3.4% | slower point estimate; marginal CIs separated | 0.97x | 1.3 ns [1.3 ns, 1.3 ns] | 38.6 ns [38.0 ns, 39.0 ns] | +| la_stack_det_from_lu | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +4.7% | slower point estimate; marginal CIs separated | 0.95x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.5 ns [0.5 ns, 0.5 ns] | +0.4% | slower point estimate; marginal CIs separated | 1.00x | 0.4 ns [0.4 ns, 0.4 ns] | 0.6 ns [0.6 ns, 0.6 ns] | +| la_stack_dot | 0.7 ns [0.7 ns, 0.7 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -14.0% | faster point estimate; marginal CIs separated | 1.16x | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -20.2% | faster point estimate; marginal CIs separated | 1.25x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| la_stack_inf_norm | 0.8 ns [0.8 ns, 0.8 ns] | 0.6 ns [0.6 ns, 0.6 ns] | -24.2% | faster point estimate; marginal CIs separated | 1.32x | 0.5 ns [0.5 ns, 0.5 ns] | 0.8 ns [0.8 ns, 0.8 ns] | + +### D=3 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 7.5 ns [7.5 ns, 7.5 ns] | 10.6 ns [10.6 ns, 10.6 ns] | +41.0% | slower point estimate; marginal CIs separated | 0.71x | 16.7 ns [16.6 ns, 16.8 ns] | 137.9 ns [137.1 ns, 138.9 ns] | +| la_stack_det | 1.5 ns [1.5 ns, 1.5 ns] | 2.2 ns [2.2 ns, 2.2 ns] | +46.4% | slower point estimate; marginal CIs separated | 0.68x | — | — | +| la_stack_lu | 9.1 ns [9.1 ns, 9.1 ns] | 8.5 ns [8.4 ns, 8.5 ns] | -7.0% | faster point estimate; marginal CIs separated | 1.08x | 15.2 ns [15.1 ns, 15.4 ns] | 127.3 ns [126.3 ns, 128.1 ns] | +| la_stack_ldlt | 7.1 ns [7.1 ns, 7.1 ns] | 14.0 ns [14.0 ns, 14.1 ns] | +97.2% | slower point estimate; marginal CIs separated | 0.51x | 4.1 ns [4.1 ns, 4.1 ns] | 93.6 ns [93.2 ns, 94.0 ns] | +| la_stack_lu_solve | 9.6 ns [9.6 ns, 9.7 ns] | 9.9 ns [9.9 ns, 9.9 ns] | +2.8% | slower point estimate; marginal CIs separated | 0.97x | 23.0 ns [22.9 ns, 23.2 ns] | 191.2 ns [190.5 ns, 191.8 ns] | +| la_stack_ldlt_solve | 8.6 ns [8.6 ns, 8.7 ns] | 15.4 ns [15.3 ns, 15.4 ns] | +78.5% | slower point estimate; marginal CIs separated | 0.56x | 8.2 ns [8.2 ns, 8.2 ns] | 137.6 ns [137.3 ns, 138.2 ns] | +| la_stack_solve_from_lu | 2.1 ns [2.1 ns, 2.1 ns] | 2.1 ns [2.1 ns, 2.1 ns] | -0.3% | faster point estimate; marginal CIs separated | 1.00x | 4.4 ns [4.4 ns, 4.4 ns] | 49.5 ns [49.3 ns, 49.7 ns] | +| la_stack_solve_from_ldlt | 1.8 ns [1.8 ns, 1.8 ns] | 1.8 ns [1.8 ns, 1.8 ns] | +0.0% | marginal CIs overlap | 1.00x | 2.9 ns [2.9 ns, 2.9 ns] | 38.2 ns [37.8 ns, 38.5 ns] | +| la_stack_det_from_lu | 0.6 ns [0.6 ns, 0.6 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +28.1% | slower point estimate; marginal CIs separated | 0.78x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | +| la_stack_det_from_ldlt | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +40.3% | slower point estimate; marginal CIs separated | 0.71x | 0.5 ns [0.5 ns, 0.5 ns] | 0.7 ns [0.7 ns, 0.7 ns] | +| la_stack_dot | 0.8 ns [0.8 ns, 0.8 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -12.8% | faster point estimate; marginal CIs separated | 1.15x | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | +| la_stack_norm2_sq | 0.5 ns [0.5 ns, 0.5 ns] | 0.4 ns [0.4 ns, 0.4 ns] | -18.4% | faster point estimate; marginal CIs separated | 1.22x | 0.4 ns [0.4 ns, 0.4 ns] | 4.2 ns [4.1 ns, 4.2 ns] | +| la_stack_inf_norm | 1.7 ns [1.7 ns, 1.7 ns] | 1.3 ns [1.3 ns, 1.3 ns] | -24.1% | faster point estimate; marginal CIs separated | 1.32x | 1.1 ns [1.1 ns, 1.1 ns] | 1.3 ns [1.3 ns, 1.3 ns] | + +### D=4 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 12.6 ns [12.6 ns, 12.7 ns] | 17.0 ns [16.9 ns, 17.0 ns] | +34.3% | slower point estimate; marginal CIs separated | 0.74x | 30.6 ns [30.5 ns, 30.6 ns] | 162.0 ns [161.2 ns, 164.0 ns] | +| la_stack_det | 6.1 ns [6.1 ns, 6.1 ns] | 4.9 ns [4.9 ns, 4.9 ns] | -20.1% | faster point estimate; marginal CIs separated | 1.25x | — | — | +| la_stack_lu | 13.7 ns [13.6 ns, 13.7 ns] | 13.8 ns [13.8 ns, 13.8 ns] | +0.9% | slower point estimate; marginal CIs separated | 0.99x | 29.6 ns [29.5 ns, 29.7 ns] | 148.2 ns [147.6 ns, 148.9 ns] | +| la_stack_ldlt | 12.6 ns [12.6 ns, 12.7 ns] | 21.5 ns [21.4 ns, 21.5 ns] | +69.8% | slower point estimate; marginal CIs separated | 0.59x | 10.0 ns [10.0 ns, 10.1 ns] | 115.8 ns [115.2 ns, 116.2 ns] | +| la_stack_lu_solve | 22.2 ns [22.1 ns, 22.2 ns] | 21.6 ns [21.6 ns, 21.6 ns] | -2.8% | faster point estimate; marginal CIs separated | 1.03x | 51.9 ns [51.8 ns, 52.0 ns] | 217.5 ns [216.4 ns, 218.8 ns] | +| la_stack_ldlt_solve | 17.5 ns [17.5 ns, 17.6 ns] | 26.8 ns [26.8 ns, 26.8 ns] | +52.7% | slower point estimate; marginal CIs separated | 0.65x | 14.9 ns [14.8 ns, 14.9 ns] | 159.4 ns [159.2 ns, 159.8 ns] | +| la_stack_solve_from_lu | 4.0 ns [4.0 ns, 4.0 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -0.2% | faster point estimate; marginal CIs separated | 1.00x | 5.1 ns [5.1 ns, 5.1 ns] | 51.8 ns [51.7 ns, 51.9 ns] | +| la_stack_solve_from_ldlt | 2.5 ns [2.5 ns, 2.5 ns] | 2.5 ns [2.5 ns, 2.5 ns] | +0.1% | marginal CIs overlap | 1.00x | 5.5 ns [5.5 ns, 5.5 ns] | 39.0 ns [38.7 ns, 39.5 ns] | +| la_stack_det_from_lu | 0.7 ns [0.7 ns, 0.7 ns] | 0.9 ns [0.9 ns, 0.9 ns] | +21.1% | slower point estimate; marginal CIs separated | 0.83x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| la_stack_det_from_ldlt | 0.7 ns [0.7 ns, 0.7 ns] | 1.0 ns [0.9 ns, 1.0 ns] | +47.9% | slower point estimate; marginal CIs separated | 0.68x | 0.5 ns [0.5 ns, 0.5 ns] | 1.0 ns [1.0 ns, 1.0 ns] | +| la_stack_dot | 1.0 ns [1.0 ns, 1.0 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -29.3% | faster point estimate; marginal CIs separated | 1.41x | 0.7 ns [0.7 ns, 0.7 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| la_stack_norm2_sq | 0.7 ns [0.7 ns, 0.7 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -32.7% | faster point estimate; marginal CIs separated | 1.49x | 0.5 ns [0.5 ns, 0.5 ns] | 4.2 ns [4.1 ns, 4.2 ns] | +| la_stack_inf_norm | 3.0 ns [3.0 ns, 3.0 ns] | 2.2 ns [2.2 ns, 2.2 ns] | -25.4% | faster point estimate; marginal CIs separated | 1.34x | 2.0 ns [2.0 ns, 2.0 ns] | 2.0 ns [2.0 ns, 2.0 ns] | + +### D=5 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 26.1 ns [26.0 ns, 26.1 ns] | 37.8 ns [37.8 ns, 38.0 ns] | +45.0% | slower point estimate; marginal CIs separated | 0.69x | 55.6 ns [55.4 ns, 55.7 ns] | 203.2 ns [201.2 ns, 206.1 ns] | +| la_stack_det | 26.5 ns [26.4 ns, 26.5 ns] | 39.6 ns [39.6 ns, 39.9 ns] | +49.6% | slower point estimate; marginal CIs separated | 0.67x | — | — | +| la_stack_lu | 26.8 ns [26.7 ns, 26.9 ns] | 32.3 ns [32.2 ns, 32.5 ns] | +20.6% | slower point estimate; marginal CIs separated | 0.83x | 54.7 ns [54.6 ns, 54.9 ns] | 194.4 ns [191.2 ns, 196.0 ns] | +| la_stack_ldlt | 19.8 ns [19.7 ns, 19.9 ns] | 42.2 ns [42.1 ns, 42.2 ns] | +113.0% | slower point estimate; marginal CIs separated | 0.47x | 14.7 ns [14.6 ns, 14.9 ns] | 137.3 ns [136.8 ns, 137.7 ns] | +| la_stack_lu_solve | 45.7 ns [45.6 ns, 46.1 ns] | 45.6 ns [44.9 ns, 46.1 ns] | -0.2% | marginal CIs overlap | 1.00x | 68.9 ns [68.8 ns, 69.0 ns] | 323.2 ns [302.8 ns, 329.6 ns] | +| la_stack_ldlt_solve | 24.7 ns [24.7 ns, 24.8 ns] | 54.9 ns [46.3 ns, 55.5 ns] | +121.7% | slower point estimate; marginal CIs separated | 0.45x | 61.5 ns [61.0 ns, 61.9 ns] | 219.5 ns [215.2 ns, 229.6 ns] | +| la_stack_solve_from_lu | 6.2 ns [6.2 ns, 6.3 ns] | 6.4 ns [6.4 ns, 6.4 ns] | +2.5% | slower point estimate; marginal CIs separated | 0.98x | 8.0 ns [8.0 ns, 8.1 ns] | 89.3 ns [88.2 ns, 89.9 ns] | +| la_stack_solve_from_ldlt | 3.9 ns [3.9 ns, 3.9 ns] | 3.9 ns [3.9 ns, 3.9 ns] | +1.4% | slower point estimate; marginal CIs separated | 0.99x | 9.2 ns [9.1 ns, 9.3 ns] | 66.4 ns [65.7 ns, 66.8 ns] | +| la_stack_det_from_lu | 0.9 ns [0.9 ns, 0.9 ns] | 1.7 ns [1.7 ns, 1.7 ns] | +98.3% | slower point estimate; marginal CIs separated | 0.50x | 0.7 ns [0.7 ns, 0.7 ns] | 1.5 ns [1.5 ns, 1.5 ns] | +| la_stack_det_from_ldlt | 0.8 ns [0.8 ns, 0.8 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +51.4% | slower point estimate; marginal CIs separated | 0.66x | 0.6 ns [0.6 ns, 0.6 ns] | 1.2 ns [1.2 ns, 1.2 ns] | +| la_stack_dot | 1.2 ns [1.2 ns, 1.2 ns] | 0.8 ns [0.8 ns, 0.8 ns] | -32.9% | faster point estimate; marginal CIs separated | 1.49x | 0.7 ns [0.7 ns, 0.7 ns] | 1.4 ns [1.4 ns, 1.4 ns] | +| la_stack_norm2_sq | 0.9 ns [0.9 ns, 0.9 ns] | 0.5 ns [0.5 ns, 0.5 ns] | -38.6% | faster point estimate; marginal CIs separated | 1.63x | 0.6 ns [0.6 ns, 0.6 ns] | 4.3 ns [4.2 ns, 4.3 ns] | +| la_stack_inf_norm | 4.7 ns [4.7 ns, 4.7 ns] | 3.4 ns [3.4 ns, 3.4 ns] | -26.9% | faster point estimate; marginal CIs separated | 1.37x | 3.2 ns [3.2 ns, 3.2 ns] | 3.2 ns [3.2 ns, 3.2 ns] | + +### D=8 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 83.5 ns [83.1 ns, 83.7 ns] | 88.3 ns [88.1 ns, 88.7 ns] | +5.7% | slower point estimate; marginal CIs separated | 0.95x | 137.9 ns [137.5 ns, 138.5 ns] | 280.8 ns [279.8 ns, 281.8 ns] | +| la_stack_det | 81.9 ns [81.6 ns, 82.1 ns] | 90.3 ns [90.1 ns, 91.1 ns] | +10.3% | slower point estimate; marginal CIs separated | 0.91x | — | — | +| la_stack_lu | 83.7 ns [82.9 ns, 84.6 ns] | 82.5 ns [82.2 ns, 82.6 ns] | -1.4% | faster point estimate; marginal CIs separated | 1.01x | 123.5 ns [123.2 ns, 124.5 ns] | 265.6 ns [263.8 ns, 266.7 ns] | +| la_stack_ldlt | 100.9 ns [100.7 ns, 101.1 ns] | 92.0 ns [90.4 ns, 92.3 ns] | -8.8% | faster point estimate; marginal CIs separated | 1.10x | 97.7 ns [97.4 ns, 97.8 ns] | 207.9 ns [205.9 ns, 209.1 ns] | +| la_stack_lu_solve | 127.2 ns [126.9 ns, 127.5 ns] | 142.9 ns [141.7 ns, 143.7 ns] | +12.3% | slower point estimate; marginal CIs separated | 0.89x | 165.3 ns [164.7 ns, 166.2 ns] | 367.7 ns [367.0 ns, 368.7 ns] | +| la_stack_ldlt_solve | 119.7 ns [119.5 ns, 119.8 ns] | 105.1 ns [104.8 ns, 105.4 ns] | -12.2% | faster point estimate; marginal CIs separated | 1.14x | 148.2 ns [147.6 ns, 148.3 ns] | 272.8 ns [272.0 ns, 273.8 ns] | +| la_stack_solve_from_lu | 13.5 ns [13.5 ns, 13.5 ns] | 13.5 ns [13.5 ns, 13.5 ns] | -0.2% | faster point estimate; marginal CIs separated | 1.00x | 13.4 ns [13.3 ns, 13.4 ns] | 94.9 ns [94.7 ns, 95.6 ns] | +| la_stack_solve_from_ldlt | 8.3 ns [8.3 ns, 8.3 ns] | 8.2 ns [8.2 ns, 8.2 ns] | -0.5% | faster point estimate; marginal CIs separated | 1.00x | 21.9 ns [21.9 ns, 22.0 ns] | 69.5 ns [69.4 ns, 69.7 ns] | +| la_stack_det_from_lu | 1.3 ns [1.3 ns, 1.3 ns] | 2.2 ns [2.2 ns, 2.5 ns] | +69.6% | slower point estimate; marginal CIs separated | 0.59x | 1.0 ns [1.0 ns, 1.0 ns] | 2.2 ns [2.2 ns, 2.2 ns] | +| la_stack_det_from_ldlt | 1.3 ns [1.3 ns, 1.3 ns] | 2.5 ns [2.4 ns, 2.5 ns] | +94.9% | slower point estimate; marginal CIs separated | 0.51x | 0.9 ns [0.9 ns, 0.9 ns] | 2.1 ns [2.0 ns, 2.1 ns] | +| la_stack_dot | 1.5 ns [1.5 ns, 1.6 ns] | 1.0 ns [1.0 ns, 1.0 ns] | -33.3% | faster point estimate; marginal CIs separated | 1.50x | 1.1 ns [1.1 ns, 1.1 ns] | 2.3 ns [2.3 ns, 2.3 ns] | +| la_stack_norm2_sq | 1.3 ns [1.3 ns, 1.3 ns] | 0.7 ns [0.7 ns, 0.7 ns] | -48.8% | faster point estimate; marginal CIs separated | 1.95x | 0.7 ns [0.7 ns, 0.7 ns] | 4.1 ns [4.1 ns, 4.1 ns] | +| la_stack_inf_norm | 12.4 ns [12.4 ns, 12.5 ns] | 8.4 ns [8.4 ns, 8.4 ns] | -32.2% | faster point estimate; marginal CIs separated | 1.48x | 8.0 ns [8.0 ns, 8.1 ns] | 8.1 ns [8.1 ns, 8.1 ns] | +| la_stack_lu_pivoting | 103.7 ns [103.4 ns, 104.0 ns] | 91.3 ns [90.9 ns, 91.6 ns] | -12.0% | faster point estimate; marginal CIs separated | 1.14x | — | — | +| la_stack_lu_ill_conditioned | 96.7 ns [96.5 ns, 96.9 ns] | 82.5 ns [82.4 ns, 82.7 ns] | -14.7% | faster point estimate; marginal CIs separated | 1.17x | — | — | +| la_stack_ldlt_ill_conditioned | 100.4 ns [100.2 ns, 100.5 ns] | 92.0 ns [90.2 ns, 92.5 ns] | -8.4% | faster point estimate; marginal CIs separated | 1.09x | — | — | + +### D=16 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 398.9 ns [396.4 ns, 402.7 ns] | 427.7 ns [426.4 ns, 428.5 ns] | +7.2% | slower point estimate; marginal CIs separated | 0.93x | 460.8 ns [458.2 ns, 466.5 ns] | 662.5 ns [659.9 ns, 664.7 ns] | +| la_stack_det | 393.5 ns [392.8 ns, 395.5 ns] | 457.5 ns [444.9 ns, 461.4 ns] | +16.3% | slower point estimate; marginal CIs separated | 0.86x | — | — | +| la_stack_lu | 367.8 ns [367.3 ns, 368.7 ns] | 399.6 ns [398.4 ns, 404.0 ns] | +8.6% | slower point estimate; marginal CIs separated | 0.92x | 468.6 ns [468.4 ns, 469.5 ns] | 642.3 ns [637.6 ns, 646.4 ns] | +| la_stack_ldlt | 449.6 ns [448.6 ns, 450.4 ns] | 393.0 ns [392.5 ns, 393.5 ns] | -12.6% | faster point estimate; marginal CIs separated | 1.14x | 405.6 ns [405.2 ns, 405.8 ns] | 416.8 ns [415.5 ns, 418.1 ns] | +| la_stack_lu_solve | 638.4 ns [636.2 ns, 639.9 ns] | 654.7 ns [653.6 ns, 656.0 ns] | +2.6% | slower point estimate; marginal CIs separated | 0.98x | 577.2 ns [576.7 ns, 577.9 ns] | 894.6 ns [891.7 ns, 896.5 ns] | +| la_stack_ldlt_solve | 518.9 ns [517.5 ns, 520.2 ns] | 451.7 ns [451.4 ns, 452.3 ns] | -12.9% | faster point estimate; marginal CIs separated | 1.15x | 649.4 ns [647.5 ns, 651.8 ns] | 590.8 ns [588.9 ns, 593.7 ns] | +| la_stack_solve_from_lu | 196.9 ns [196.4 ns, 197.5 ns] | 197.2 ns [196.3 ns, 198.0 ns] | +0.2% | marginal CIs overlap | 1.00x | 93.4 ns [93.3 ns, 93.5 ns] | 239.7 ns [239.3 ns, 240.2 ns] | +| la_stack_solve_from_ldlt | 28.2 ns [28.1 ns, 28.3 ns] | 27.7 ns [27.7 ns, 27.7 ns] | -1.7% | faster point estimate; marginal CIs separated | 1.02x | 123.6 ns [123.3 ns, 123.8 ns] | 177.5 ns [177.1 ns, 177.8 ns] | +| la_stack_det_from_lu | 2.6 ns [2.6 ns, 2.6 ns] | 3.7 ns [3.7 ns, 3.7 ns] | +42.5% | slower point estimate; marginal CIs separated | 0.70x | 1.8 ns [1.8 ns, 1.8 ns] | 4.8 ns [4.7 ns, 4.9 ns] | +| la_stack_det_from_ldlt | 2.5 ns [2.5 ns, 2.6 ns] | 3.3 ns [3.2 ns, 3.5 ns] | +30.0% | slower point estimate; marginal CIs separated | 0.77x | 1.7 ns [1.7 ns, 1.7 ns] | 4.4 ns [4.4 ns, 4.4 ns] | +| la_stack_dot | 3.1 ns [3.0 ns, 3.3 ns] | 2.3 ns [2.3 ns, 2.3 ns] | -26.3% | faster point estimate; marginal CIs separated | 1.36x | 1.9 ns [1.9 ns, 1.9 ns] | 4.6 ns [4.5 ns, 4.6 ns] | +| la_stack_norm2_sq | 2.7 ns [2.6 ns, 2.7 ns] | 2.1 ns [2.0 ns, 2.1 ns] | -23.1% | faster point estimate; marginal CIs separated | 1.30x | 1.5 ns [1.5 ns, 1.5 ns] | 4.1 ns [4.1 ns, 4.1 ns] | +| la_stack_inf_norm | 49.8 ns [49.5 ns, 50.0 ns] | 32.7 ns [32.6 ns, 32.7 ns] | -34.3% | faster point estimate; marginal CIs separated | 1.52x | 31.7 ns [31.7 ns, 31.7 ns] | 32.6 ns [32.5 ns, 32.6 ns] | + +### D=32 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 2.11 µs [2.10 µs, 2.11 µs] | 2.21 µs [2.21 µs, 2.22 µs] | +5.0% | slower point estimate; marginal CIs separated | 0.95x | 2.51 µs [2.50 µs, 2.52 µs] | 2.30 µs [2.28 µs, 2.31 µs] | +| la_stack_det | 2.01 µs [2.00 µs, 2.02 µs] | 2.60 µs [2.60 µs, 2.61 µs] | +29.6% | slower point estimate; marginal CIs separated | 0.77x | — | — | +| la_stack_lu | 2.20 µs [2.18 µs, 2.21 µs] | 2.07 µs [2.06 µs, 2.07 µs] | -6.1% | faster point estimate; marginal CIs separated | 1.07x | 2.16 µs [2.16 µs, 2.16 µs] | 2.26 µs [2.25 µs, 2.27 µs] | +| la_stack_ldlt | 2.81 µs [2.81 µs, 2.82 µs] | 2.53 µs [2.52 µs, 2.54 µs] | -9.9% | faster point estimate; marginal CIs separated | 1.11x | 2.11 µs [2.10 µs, 2.11 µs] | 1.42 µs [1.42 µs, 1.42 µs] | +| la_stack_lu_solve | 2.68 µs [2.68 µs, 2.68 µs] | 2.79 µs [2.79 µs, 2.80 µs] | +4.3% | slower point estimate; marginal CIs separated | 0.96x | 2.45 µs [2.45 µs, 2.46 µs] | 2.86 µs [2.86 µs, 2.87 µs] | +| la_stack_ldlt_solve | 3.27 µs [3.26 µs, 3.27 µs] | 2.94 µs [2.92 µs, 2.95 µs] | -10.1% | faster point estimate; marginal CIs separated | 1.11x | 2.79 µs [2.79 µs, 2.79 µs] | 1.91 µs [1.91 µs, 1.91 µs] | +| la_stack_solve_from_lu | 688.9 ns [686.2 ns, 692.0 ns] | 674.0 ns [672.6 ns, 674.9 ns] | -2.2% | faster point estimate; marginal CIs separated | 1.02x | 331.4 ns [331.1 ns, 331.8 ns] | 619.2 ns [617.2 ns, 622.7 ns] | +| la_stack_solve_from_ldlt | 303.6 ns [302.9 ns, 304.0 ns] | 307.3 ns [306.8 ns, 307.7 ns] | +1.2% | slower point estimate; marginal CIs separated | 0.99x | 566.9 ns [564.8 ns, 570.2 ns] | 463.7 ns [463.0 ns, 464.2 ns] | +| la_stack_det_from_lu | 7.0 ns [7.0 ns, 7.0 ns] | 13.9 ns [13.4 ns, 14.0 ns] | +98.2% | slower point estimate; marginal CIs separated | 0.50x | 3.1 ns [3.1 ns, 3.1 ns] | 8.8 ns [8.8 ns, 8.8 ns] | +| la_stack_det_from_ldlt | 6.3 ns [6.3 ns, 6.4 ns] | 12.9 ns [12.9 ns, 13.0 ns] | +105.4% | slower point estimate; marginal CIs separated | 0.49x | 3.0 ns [3.0 ns, 3.0 ns] | 8.5 ns [8.5 ns, 8.5 ns] | +| la_stack_dot | 7.5 ns [7.5 ns, 7.6 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -46.4% | faster point estimate; marginal CIs separated | 1.87x | 4.7 ns [4.7 ns, 4.7 ns] | 15.1 ns [15.1 ns, 15.1 ns] | +| la_stack_norm2_sq | 7.5 ns [7.5 ns, 7.5 ns] | 4.0 ns [4.0 ns, 4.0 ns] | -46.9% | faster point estimate; marginal CIs separated | 1.88x | 3.9 ns [3.8 ns, 3.9 ns] | 4.2 ns [4.2 ns, 4.2 ns] | +| la_stack_inf_norm | 202.4 ns [202.2 ns, 202.7 ns] | 128.9 ns [128.6 ns, 129.1 ns] | -36.3% | faster point estimate; marginal CIs separated | 1.57x | 159.6 ns [157.6 ns, 161.3 ns] | 163.8 ns [163.5 ns, 164.1 ns] | + +### D=64 + +| Benchmark | v0.4.3 (point + CI) | Latest (point + CI) | Point-estimate change | CI relation | Point-estimate ratio | v0.4.3 nalgebra | v0.4.3 faer | +|-----------|-------:|-------:|-------:|:-----------|--------:|-------:|-------:| +| la_stack_det_via_lu | 15.18 µs [15.16 µs, 15.21 µs] | 15.60 µs [15.58 µs, 15.65 µs] | +2.7% | slower point estimate; marginal CIs separated | 0.97x | 15.01 µs [14.78 µs, 15.10 µs] | 10.70 µs [10.68 µs, 10.71 µs] | +| la_stack_det | 15.15 µs [15.13 µs, 15.17 µs] | 15.48 µs [15.44 µs, 15.51 µs] | +2.2% | slower point estimate; marginal CIs separated | 0.98x | — | — | +| la_stack_lu | 15.34 µs [15.31 µs, 15.37 µs] | 14.66 µs [14.64 µs, 14.69 µs] | -4.4% | faster point estimate; marginal CIs separated | 1.05x | 13.67 µs [13.65 µs, 13.70 µs] | 10.50 µs [10.48 µs, 10.56 µs] | +| la_stack_ldlt | 21.53 µs [21.49 µs, 21.58 µs] | 20.42 µs [20.39 µs, 20.49 µs] | -5.2% | faster point estimate; marginal CIs separated | 1.05x | 11.41 µs [10.97 µs, 11.50 µs] | 8.86 µs [8.85 µs, 8.87 µs] | +| la_stack_lu_solve | 17.48 µs [17.44 µs, 17.50 µs] | 17.24 µs [17.21 µs, 17.41 µs] | -1.3% | faster point estimate; marginal CIs separated | 1.01x | 14.84 µs [14.80 µs, 14.86 µs] | 12.20 µs [12.18 µs, 12.23 µs] | +| la_stack_ldlt_solve | 24.60 µs [24.49 µs, 24.76 µs] | 22.70 µs [22.63 µs, 22.84 µs] | -7.7% | faster point estimate; marginal CIs separated | 1.08x | 14.06 µs [14.02 µs, 14.10 µs] | 10.12 µs [10.11 µs, 10.12 µs] | +| la_stack_solve_from_lu | 2.64 µs [2.63 µs, 2.65 µs] | 2.55 µs [2.54 µs, 2.56 µs] | -3.4% | faster point estimate; marginal CIs separated | 1.04x | 786.0 ns [778.1 ns, 788.8 ns] | 1.66 µs [1.66 µs, 1.66 µs] | +| la_stack_solve_from_ldlt | 1.10 µs [1.10 µs, 1.10 µs] | 1.07 µs [1.07 µs, 1.07 µs] | -2.7% | faster point estimate; marginal CIs separated | 1.03x | 1.29 µs [1.29 µs, 1.29 µs] | 1.25 µs [1.24 µs, 1.25 µs] | +| la_stack_det_from_lu | 27.0 ns [27.0 ns, 27.1 ns] | 31.9 ns [31.8 ns, 32.5 ns] | +17.9% | slower point estimate; marginal CIs separated | 0.85x | 8.7 ns [8.7 ns, 8.7 ns] | 21.6 ns [21.6 ns, 21.7 ns] | +| la_stack_det_from_ldlt | 27.5 ns [27.0 ns, 28.7 ns] | 31.6 ns [31.6 ns, 31.6 ns] | +14.8% | slower point estimate; marginal CIs separated | 0.87x | 8.5 ns [8.5 ns, 8.5 ns] | 21.0 ns [20.9 ns, 21.1 ns] | +| la_stack_dot | 38.4 ns [38.4 ns, 38.5 ns] | 11.0 ns [10.9 ns, 11.0 ns] | -71.5% | faster point estimate; marginal CIs separated | 3.51x | 9.0 ns [9.0 ns, 9.0 ns] | 36.6 ns [36.6 ns, 36.7 ns] | +| la_stack_norm2_sq | 38.2 ns [38.2 ns, 38.3 ns] | 10.8 ns [10.8 ns, 10.8 ns] | -71.8% | faster point estimate; marginal CIs separated | 3.54x | 7.4 ns [7.4 ns, 7.4 ns] | 6.2 ns [6.2 ns, 6.2 ns] | +| la_stack_inf_norm | 1.92 µs [1.92 µs, 1.92 µs] | 621.3 ns [620.2 ns, 621.7 ns] | -67.6% | faster point estimate; marginal CIs separated | 3.09x | 1.14 µs [1.13 µs, 1.15 µs] | 1.57 µs [1.57 µs, 1.57 µs] | + +## How to Update + +Local performance reports are generated in isolated temporary worktrees: + +```bash +# Local development: compare the current tree with the latest release +just performance-local + +# Release PR: update docs/PERFORMANCE.md and archive the previous report +just performance-release + +# Build release docs from retained CSV/JSON inputs (no benchmarks) +just performance-doc + +# GitHub Actions release assets +just performance-github-assets + +# Explicit repair +just performance-release +``` + +`just performance-local` writes `performance.md` plus retained `performance.csv` and +`performance.provenance.json` comparison inputs under `target/bench-reports/` without promoting documentation. +It applies staged and unstaged tracked changes; untracked files are excluded. +`just performance-github-assets` writes `target/bench-reports/github-assets-performance.md`. +`just performance-release` performs the same measurement and retention work, then promotes distinct-release documentation. +`just performance-doc` consumes the retained pair from either workflow without benchmarking and promotes it when the package versions differ. +For a distinct pair, `performance-local` followed by `performance-doc` is equivalent to the atomic `performance-release` workflow. + +Older curated release-to-release reports are archived in `docs/archive/performance/`. + +See `docs/BENCHMARKING.md` for the full comparison workflow. diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.csv b/docs/assets/bench/vs_linalg_lu_solve_median.csv index 9da270f..ddb7cac 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.csv +++ b/docs/assets/bench/vs_linalg_lu_solve_median.csv @@ -1,9 +1,9 @@ D,la_stack,la_lo,la_hi,nalgebra,na_lo,na_hi,faer,fa_lo,fa_hi -2,2.0511167827781702,2.0508010652319073,2.0514799503238637,4.608511834479354,4.588541113174011,4.679159823823008,149.53700192152104,149.28626396895226,149.86909100717443 -3,10.031816164052708,10.012870984386646,10.042108916260883,23.094065508706453,23.050967573914953,23.12253053765827,185.93474278358173,185.4274152626928,186.52338345968678 -4,21.806265246500374,21.78844796496967,21.840814592483063,53.54247145040914,53.343918684571506,53.583225701852626,218.92100858066146,217.53978259902593,220.91673823920112 -5,43.78667308084254,43.725648916200925,43.8375145276915,70.43721993909745,70.40138994288745,70.46838014608551,282.26162962383944,281.1318190503487,283.00841946602355 -8,128.33741381271173,128.16537417133148,128.44380372544757,167.50543379849552,167.2076897070214,167.94225473214073,414.79144610503465,385.1675329566855,418.10038292529816 -16,672.6803584206948,669.32087973114,675.0099138907773,581.2733321011165,581.0081202884301,581.4932153392331,875.4114698625715,871.4328720696911,877.1246563118169 -32,2873.719688592829,2864.5208484738373,2883.4553890876564,2470.43527521921,2465.696974484398,2475.3848495644006,2861.2088220918868,2857.038328349756,2865.5225755003344 -64,18165.369153911564,18049.00228937729,18230.65507518797,15021.7373313609,15007.658290233809,15057.123188405796,12225.703395061728,12218.95586419753,12243.655761316872 +2,2.043762626268518,2.0422827651419597,2.0447790116664555,4.601323421841004,4.595197301082463,4.608615047162932,151.9390190878473,151.6931342810127,152.54018630708097 +3,9.988548111179517,9.954187559495038,10.007125510422096,23.5131208253108,23.43355332339501,23.614648949741298,196.35702867954777,194.94348729609408,198.75049539724168 +4,21.864804649962238,21.84202072931614,21.880570636063183,54.71614688761136,54.62726895942409,54.87756778849946,223.91031993204984,222.61421792794988,225.8474926387316 +5,44.51008358188431,44.275702584061186,44.717608725166315,71.21900306190079,71.11478152577844,71.30045978657881,293.419765051395,290.0661591010662,306.3860360233699 +8,145.40528452059866,137.96532664767477,146.62531265833474,188.35193148966346,187.44649013949015,188.9754735930736,381.871856125649,380.45259170993273,383.1446190133525 +16,672.490775828649,670.9416553595659,674.0398009950248,585.26069324284,584.5897428943372,586.349779695245,897.2363083497698,895.5233830096844,900.8699633699633 +32,2777.70695399973,2774.6190957703316,2782.4841693051158,2501.3607828952654,2494.2493848743848,2506.2687459411595,2952.7779305246913,2948.077163491427,2959.1092519128833 +64,17357.784731525782,17261.484363636362,17414.59595959596,13878.401216108454,13851.678987455198,13898.104803240742,12199.761458333334,12175.740131578947,12222.186667448404 diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json b/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json index 01c1749..a42a47d 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json +++ b/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json @@ -1,18 +1,16 @@ { "artifact": "README vs_linalg dimension plot", "criterion": { - "benchmark_command": [ - "cargo", - "bench", - "--locked", - "--features", - "bench", - "--bench", - "vs_linalg", - "--", - "(la_stack_lu_solve|nalgebra_lu_solve|faer_lu_solve)$" + "baseline_command": [ + "just", + "bench-save-baseline", + "v0.4.4" ], "criterion_dependency": "0.8.2", + "current_command": [ + "just", + "bench-latest" + ], "dimensions": [ 2, 3, @@ -29,34 +27,40 @@ "statistic": "median" }, "measurement": { - "cargo_lock_sha256": "0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd", - "commit": "e736c5fda155ef23c8712f89ae15bf5369ff3787", - "cpu": "unavailable", - "cpu_architecture": "arm", - "git_clean": false, - "git_status_sha256": "a367ed1608bcdea33eb781794fde643f01a6a280f154ffacbd02e25506cf9f8b", - "harness_sha256": "7915a36e12d9895170323aee46910b688492dc6aca704ac7f1daaefae644a9ea", - "missing_harness_files": [], - "os": "Darwin 25.5.0 arm64", - "rustc": "rustc 1.97.0 (2d8144b78 2026-07-07)", - "source_missing": false, - "source_state_sha256": "fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2", + "baseline_api_compatibility": "none", + "baseline_commit": "03a6dc751b7bf7c69b833aeb4e20e2acb6da2e4c", + "baseline_git_clean": false, + "baseline_source_state_sha256": "227a2780c989352be2eacf3f32ca67032cc69fc843e596acc386a7c4f7d459f0", + "cargo_lock_sha256": "72b99b4f7f3917d668bb71448198d5d496334684097eac282c859d08d8cf4492", + "cpu": "Apple M4 Max (arm64)", + "current_commit": "99d33927e389018c599c009603a7eafffcc25089", + "current_git_clean": false, + "current_source_state_sha256": "7d72086128b12e2a89d5ca78f0e8cc0cb393a44f1062d027a069722ff9dae007", + "harness_sha256": "164d6bbac2e6cb19b85c81677e4ab4428c3abb130dcfd38a2f5f162a5dd8a905", + "la_stack_sample": "current", + "os": "Darwin 25.6.0 arm64", + "peer_release_context": "v0.4.4", + "peer_sample": "baseline phase under shared current harness", + "rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)", + "source": "retained performance-release artifact", "status": "recorded" }, + "performance_artifact": { + "csv": "target/bench-reports/performance.csv", + "csv_sha256": "4d4f02188ce78414d6018fe0ce23dcc7da07a23b05026843698c2ad32be7d0c8", + "provenance": "target/bench-reports/performance.provenance.json", + "provenance_sha256": "fec902d3b444dd3be67255637f56bdb53b7b6a5368a8b2410c963a4e40b6ba5c", + "release": { + "baseline": "v0.4.4", + "current": "v0.4.5" + } + }, "publication": { - "cargo_lock_sha256": "0c275998d6fe18f8b4def36611598860e96c250303ba459da280ed64e2afd3cd", - "commit": "e736c5fda155ef23c8712f89ae15bf5369ff3787", - "correctness_gate": "passed", - "cpu": "unavailable", - "cpu_architecture": "arm", - "git_clean": false, - "git_status_sha256": "a367ed1608bcdea33eb781794fde643f01a6a280f154ffacbd02e25506cf9f8b", - "harness_sha256": "7915a36e12d9895170323aee46910b688492dc6aca704ac7f1daaefae644a9ea", - "missing_harness_files": [], - "os": "Darwin 25.5.0 arm64", - "rustc": "rustc 1.97.0 (2d8144b78 2026-07-07)", - "source_missing": false, - "source_state_sha256": "fb4e2675e4c50d859a2fe358f9dfd8fd7a4e3a31fcb9f6094bf1c43492138be2" + "benchmark_contract": "legacy-retained-artifact", + "cargo_lock_sha256": "72b99b4f7f3917d668bb71448198d5d496334684097eac282c859d08d8cf4492", + "commit": "99d33927e389018c599c009603a7eafffcc25089", + "correctness_gate": "validated-performance-release-artifact", + "source_state_sha256": "7d72086128b12e2a89d5ca78f0e8cc0cb393a44f1062d027a069722ff9dae007" }, - "schema": 1 + "schema": 2 } diff --git a/docs/assets/bench/vs_linalg_lu_solve_median.svg b/docs/assets/bench/vs_linalg_lu_solve_median.svg index 890ce89..c3cc6f0 100644 --- a/docs/assets/bench/vs_linalg_lu_solve_median.svg +++ b/docs/assets/bench/vs_linalg_lu_solve_median.svg @@ -7,7 +7,7 @@ > Gnuplot -Produced by GNUPLOT 6.0 patchlevel 4 +Produced by GNUPLOT 6.0 patchlevel 5 @@ -244,51 +244,51 @@ - la-stack v0.4.4 + la-stack v0.4.5 - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -302,47 +302,47 @@ - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + @@ -356,47 +356,47 @@ - + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + diff --git a/justfile b/justfile index b15e359..6bdc62d 100644 --- a/justfile +++ b/justfile @@ -103,6 +103,11 @@ _ensure-dprint: exit 1 fi +_ensure-gh: + #!/usr/bin/env bash + set -euo pipefail + command -v gh >/dev/null || { echo "❌ 'gh' not found. Install GitHub CLI and re-run this command."; exit 1; } + _ensure-git-cliff: #!/usr/bin/env bash set -euo pipefail @@ -339,6 +344,7 @@ changelog-unreleased version: _ensure-git-cliff _ensure-rumdl python-sync GIT_CLIFF_OFFLINE=true git-cliff --tag {{ quote(version) }} -o CHANGELOG.md uv run --locked postprocess-changelog uv run --locked archive-changelog + uv run --locked update-release-version {{ quote(version) }} --sync-changelog-date archive_files=() if [ -d docs/archive/changelog ]; then while IFS= read -r -d '' file; do @@ -468,26 +474,27 @@ help-workflows: @echo " just bench-compile # Compile benches with warnings-as-errors" @echo " just bench-latest # Run cheap latest measurements" @echo " just bench-latest-vs-last # Run latest and compare against last" + @echo " just bench-save-last # Save full baseline as 'last'" + @echo " just bench-vs-linalg # Run vs_linalg bench (optional filter)" + @echo " just bench-vs-linalg-la-stack # Run la-stack rows from vs_linalg" @echo " just bench-vs-linalg-latest-vs # Run non-exact latest and compare against last" + @echo " just bench-vs-linalg-quick # Quick vs_linalg bench (reduced samples)" + @echo " just performance-doc # Build release docs from retained CSV/JSON" @echo " just performance-github-assets # Compare stored GitHub Actions release assets" @echo " just performance-local # Compare current tree against latest release locally" @echo " just performance-local-non-exact # Compare current non-exact kernels locally" + @echo " just performance-readme # Publish retained release data to README assets/table" @echo " just performance-release # Measure, retain, and publish release docs" - @echo " just performance-doc # Build release docs from retained CSV/JSON" - @echo " just bench-save-last # Save full baseline as 'last'" - @echo " just bench-vs-linalg # Run vs_linalg bench (optional filter)" - @echo " just bench-vs-linalg-la-stack # Run la-stack rows from vs_linalg" - @echo " just bench-vs-linalg-quick # Quick vs_linalg bench (reduced samples)" @echo "" @echo "Benchmark plotting:" @echo " just plot-vs-linalg # Plot Criterion results (CSV + SVG + provenance)" - @echo " just plot-vs-linalg-readme # Gate, rerun, and publish canonical README assets/table" @echo "" @echo "Changelog & releases:" @echo " just changelog # Regenerate CHANGELOG.md from full history" @echo " just changelog-unreleased # Prepend unreleased changes for a version" @echo " just tag # Create annotated tag from CHANGELOG.md" @echo " just tag-force # Recreate an existing tag" + @echo " just update-version # Update release metadata and infer the previous tag" @echo "" @echo "Setup:" @echo " just setup # Setup project environment (depends on setup-tools)" @@ -587,8 +594,12 @@ markdown-fix: _ensure-rumdl markdown-lint: markdown-check +# Build and promote release performance docs from retained report inputs. +performance-doc: python-sync + uv run --locked archive-performance --promote-artifacts + # Compare stored GitHub Actions release benchmark assets without local cargo runs. -performance-github-assets current_tag="" baseline_tag="": python-sync +performance-github-assets current_tag="" baseline_tag="": _ensure-gh python-sync #!/usr/bin/env bash set -euo pipefail current_tag={{ quote(current_tag) }} @@ -604,11 +615,11 @@ performance-github-assets current_tag="" baseline_tag="": python-sync fi # Compare the current tree against the latest release; untracked files are excluded. -performance-local: python-sync +performance-local: _ensure-gh python-sync uv run --locked archive-performance --current-vs-latest --generate-in-temp-worktree --output-only --local-report --output target/bench-reports/performance.md # Compare current non-exact kernels locally without rerunning current peer crates. -performance-local-non-exact current_tag="" baseline_tag="": python-sync +performance-local-non-exact current_tag="" baseline_tag="": _ensure-gh python-sync #!/usr/bin/env bash set -euo pipefail current_tag={{ quote(current_tag) }} @@ -623,8 +634,18 @@ performance-local-non-exact current_tag="" baseline_tag="": python-sync uv run --locked archive-performance --current-vs-latest --suite vs_linalg --generate-in-temp-worktree --output-only --local-report --output target/bench-reports/performance-non-exact.md --artifact-csv target/bench-reports/performance-non-exact.csv --artifact-provenance target/bench-reports/performance-non-exact.provenance.json fi +# Validate retained release measurements and atomically publish the canonical README assets/table. +performance-readme metric="lu_solve" stat="median" sample="new" log_y="true": python-sync + #!/usr/bin/env bash + set -euo pipefail + args=(--metric {{ quote(metric) }} --stat {{ quote(stat) }} --sample {{ quote(sample) }} --update-readme) + if [ {{ quote(log_y) }} = "true" ]; then + args+=(--log-y) + fi + uv run --locked criterion-dim-plot "${args[@]}" + # Generate local release-signal measurements in a temp worktree, then promote/archive docs. -performance-release current_tag="" baseline_tag="": python-sync +performance-release current_tag="" baseline_tag="": _ensure-gh python-sync #!/usr/bin/env bash set -euo pipefail current_tag={{ quote(current_tag) }} @@ -639,10 +660,6 @@ performance-release current_tag="" baseline_tag="": python-sync uv run --locked archive-performance --infer-release --generate-in-temp-worktree --worktree-ref HEAD fi -# Build and promote release performance docs from retained report inputs. -performance-doc: python-sync - uv run --locked archive-performance --promote-artifacts - # Plot: generate a single time-vs-dimension SVG from Criterion results. plot-vs-linalg metric="lu_solve" stat="median" sample="new" log_y="false" allow_partial="false": python-sync #!/usr/bin/env bash @@ -656,16 +673,6 @@ plot-vs-linalg metric="lu_solve" stat="median" sample="new" log_y="false" allow_ fi uv run --locked criterion-dim-plot "${args[@]}" -# Validate fixtures, rerun the selected metric across all peers and dimensions, and atomically publish the canonical README assets/table. -plot-vs-linalg-readme metric="lu_solve" stat="median" sample="new" log_y="true": python-sync - #!/usr/bin/env bash - set -euo pipefail - args=(--metric {{ quote(metric) }} --stat {{ quote(stat) }} --sample {{ quote(sample) }} --update-readme) - if [ {{ quote(log_y) }} = "true" ]; then - args+=(--log-y) - fi - uv run --locked criterion-dim-plot "${args[@]}" - # Python tooling (uv) python-check: python-format-check python-lint python-typecheck @@ -773,6 +780,10 @@ setup-tools: echo "❌ 'jq' not found. Install jq and re-run: just setup-tools" >&2 exit 1 fi + if ! have gh; then + echo "❌ 'gh' not found. Install GitHub CLI and re-run: just setup-tools" >&2 + exit 1 + fi echo "Ensuring Rust components..." if ! have rustup; then @@ -1081,6 +1092,13 @@ update-dependencies: _ensure-uv _ensure-cargo-edit uv lock --upgrade uv sync --locked --group dev +# Update deterministic release metadata, inferring the previous stable published GitHub release. +[doc('Update package, citation, lockfile, and non-artifact documentation release versions.')] +update-version tag: _ensure-gh _ensure-uv + uv run --locked update-release-version {{ quote(tag) }} + cargo metadata --locked --format-version 1 --no-deps > /dev/null + uv run --locked check-docs-version-sync + validate-json: _ensure-jq #!/usr/bin/env bash set -euo pipefail diff --git a/pyproject.toml b/pyproject.toml index 492bcee..26b6cd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "la-stack-scripts" -version = "0.4.4" +version = "0.4.5" description = "Python utility scripts for the la-stack Rust library" readme = "scripts/README.md" requires-python = ">=3.14" @@ -41,12 +41,13 @@ criterion-dim-plot = "criterion_dim_plot:main" postprocess-changelog = "postprocess_changelog:main" tag-release = "tag_release:main" check-docs-version-sync = "check_docs_version_sync:main" +update-release-version = "update_release_version:main" update-cargo-tool-pins = "update_cargo_tool_pins:main" # Configure setuptools to find modules in scripts/ directory. [tool.setuptools] package-dir = { "" = "scripts" } -py-modules = [ "archive_changelog", "archive_performance", "bench_compare", "check_docs_version_sync", "check_semgrep_fixtures", "criterion_dim_plot", "performance_artifacts", "postprocess_changelog", "subprocess_utils", "tag_release", "update_cargo_tool_pins" ] +py-modules = [ "archive_changelog", "archive_performance", "bench_compare", "benchmark_contract", "check_docs_version_sync", "check_semgrep_fixtures", "criterion_dim_plot", "performance_artifacts", "postprocess_changelog", "subprocess_utils", "tag_release", "update_cargo_tool_pins", "update_release_version" ] [tool.ruff] line-length = 160 @@ -90,6 +91,7 @@ known-first-party = [ "archive_changelog", "archive_performance", "bench_compare", + "benchmark_contract", "check_semgrep_fixtures", "criterion_dim_plot", "performance_artifacts", diff --git a/scripts/README.md b/scripts/README.md index 9f3096d..7c2bc64 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -103,9 +103,9 @@ See `docs/BENCHMARKING.md` for the current command matrix, local saved-baseline workflow, explicit tag arguments, output locations, and release-artifact comparison details. -### Plotting Criterion benchmarks (la-stack vs nalgebra/faer) +### Plotting la-stack vs nalgebra/faer benchmarks -The plotter reads Criterion output under: +The exploratory plotter reads Criterion output under: - `target/criterion/d{D}/{benchmark}/{new|base}/estimates.json` @@ -123,7 +123,7 @@ By default, the benchmark suite runs for dimensions 2–5, 8, 16, 32, and 64. `target/criterion/...`): ```bash -# full run (takes longer, better for README plots) +# full run (takes longer, better for exploratory plots) just bench-vs-linalg lu_solve # or quick run (fast sanity check; still produces estimates.json) @@ -140,23 +140,30 @@ just plot-vs-linalg lu_solve median new true just plot-vs-linalg lu_solve mean new true ``` -Use the dedicated publication path to update README's benchmark table (between -`BENCH_TABLE` markers): +For release publication, first retain the validated release comparison, then +update README's benchmark table (between `BENCH_TABLE` markers): ```bash -just plot-vs-linalg-readme lu_solve median new true +just performance-release +just performance-readme lu_solve median new true ``` -That recipe runs the benchmark-input gate and a fresh full `vs_linalg` benchmark, -requires la-stack/nalgebra/faer results at every canonical dimension, and then -publishes CSV, SVG, JSON provenance, and README together. Partial dimensions are -available only through the plotter's explicit `--allow-partial` exploratory +The README recipe consumes `target/bench-reports/performance.csv` and its +adjacent provenance JSON; it does not run the benchmark-input gate or Criterion +again. It uses the current la-stack timing and retained same-current-harness +nalgebra/faer timings, requires all three at every canonical dimension, and then +publishes CSV, SVG, derived JSON provenance, the README table, and its tag-pinned +benchmark artifact links together. Until publication succeeds, those links keep +referencing the previous published artifacts. Partial dimensions are available +only through the raw-Criterion plotter's explicit `--allow-partial` exploratory option and cannot update README. This writes: - `docs/assets/bench/vs_linalg_lu_solve_median.csv` - `docs/assets/bench/vs_linalg_lu_solve_median.svg` (requires `gnuplot`) +- `docs/assets/bench/vs_linalg_lu_solve_median.provenance.json` +- the benchmark table and tag-pinned asset links in `README.md` (For `stat=mean`, the filenames end in `_mean` instead of `_median`.) @@ -222,6 +229,20 @@ This repo has been tested with `gnuplot 6.0 patchlevel 3` (Homebrew `gnuplot 6.0 ## Changelog and release tooling +### Updating release metadata + +```bash +just update-version vX.Y.Z +``` + +The updater infers the previous stable release from published GitHub releases, +updates package, lockfile, citation, non-artifact README, and active +benchmark workflow version references transactionally, and records the current +UTC date in `CITATION.cff`. It leaves README benchmark artifact links for +`performance-readme`, and it does not upgrade dependencies. If the target +changelog heading already exists, the updater advances its date atomically with +the citation date. + ### Generating the changelog ```bash @@ -261,5 +282,6 @@ validates SemVer, and handles GitHub's 125KB tag-annotation size limit. | `postprocess_changelog.py` | Normalize and reflow generated git-cliff Markdown safely | | `subprocess_utils.py` | Safe subprocess wrappers for git commands | | `update_cargo_tool_pins.py` | Reconcile repository-owned Cargo tool pins with installed versions | +| `update_release_version.py` | Transactionally update deterministic release-version metadata | See `docs/RELEASING.md` for the full release workflow. diff --git a/scripts/archive_performance.py b/scripts/archive_performance.py index a7ddf79..c613148 100644 --- a/scripts/archive_performance.py +++ b/scripts/archive_performance.py @@ -35,6 +35,7 @@ from typing import Any, Literal, cast from bench_compare import HOW_TO_UPDATE_SECTION, render_release_artifacts +from benchmark_contract import benchmark_contract_digest from performance_artifacts import ArtifactPaths, ensure_distinct_paths, load_bundle, publish_bundle from subprocess_utils import ExecutableNotFoundError, cpu_description, run_git_command, run_git_command_with_input, run_safe_command @@ -320,6 +321,11 @@ def _published_stable_releases(repo_root: Path) -> list[PublishedRelease]: return _stable_published_releases(_github_release_list(repo_root)) +def published_stable_release_tags(repo_root: Path) -> list[str]: + """Return stable published GitHub release tags for release tooling.""" + return [release.tag for release in _published_stable_releases(repo_root)] + + def _latest_published_release(repo_root: Path) -> PublishedRelease: stable_releases = _published_stable_releases(repo_root) if not stable_releases: @@ -699,6 +705,7 @@ def _environment_metadata(checkout: Path, *, harness_sha256: str) -> dict[str, o cpu = cpu_description() os_description = " ".join(part for part in (platform.system(), platform.release(), platform.machine()) if part).strip() return { + "benchmark_contract_sha256": benchmark_contract_digest(checkout), "cargo_lock_sha256": _sha256_file(checkout / "Cargo.lock"), "commit": _checkout_commit(checkout), "correctness_gate": "passed", @@ -781,6 +788,7 @@ def _write_local_run_provenance( measurement = { "baseline_api_compatibility": baseline_run.api_compatibility or "none", "baseline_commit": baseline_run.commit, + "benchmark_contract_sha256": publication["benchmark_contract_sha256"], "cargo_lock_sha256": publication["cargo_lock_sha256"], "cpu": publication["cpu"], "current_commit": publication["commit"], diff --git a/scripts/benchmark_contract.py b/scripts/benchmark_contract.py new file mode 100644 index 0000000..de93ac4 --- /dev/null +++ b/scripts/benchmark_contract.py @@ -0,0 +1,48 @@ +"""Stable content identity for release benchmark measurements.""" + +import hashlib +from typing import TYPE_CHECKING, Final + +if TYPE_CHECKING: + from pathlib import Path + +BENCHMARK_CONTRACT_DIRS: Final[tuple[str, ...]] = ("benches",) +BENCHMARK_CONTRACT_FILES: Final[tuple[str, ...]] = ( + ".config/nextest.toml", + "Cargo.toml", + "Cargo.lock", + "rust-toolchain.toml", + "tests/exact_bench_config.rs", + "tests/vs_linalg_inputs.rs", +) + + +def benchmark_contract_files(checkout: Path) -> list[Path]: + """Return files that determine benchmark code, inputs, and dependencies.""" + files: list[Path] = [] + for relative in BENCHMARK_CONTRACT_FILES: + path = checkout / relative + if not path.is_file(): + msg = f"benchmark contract file is missing: {path}" + raise FileNotFoundError(msg) + files.append(path) + for relative in BENCHMARK_CONTRACT_DIRS: + directory = checkout / relative + if not directory.is_dir(): + msg = f"benchmark contract directory is missing: {directory}" + raise FileNotFoundError(msg) + files.extend(path for path in directory.rglob("*") if path.is_file()) + return sorted(files, key=lambda path: path.relative_to(checkout).as_posix()) + + +def benchmark_contract_digest(checkout: Path) -> str: + """Hash benchmark code, inputs, dependency resolution, and toolchain.""" + digest = hashlib.sha256() + for path in benchmark_contract_files(checkout): + relative = path.relative_to(checkout).as_posix().encode() + payload = path.read_bytes() + digest.update(len(relative).to_bytes(8, "big")) + digest.update(relative) + digest.update(len(payload).to_bytes(8, "big")) + digest.update(payload) + return digest.hexdigest() diff --git a/scripts/check_docs_version_sync.py b/scripts/check_docs_version_sync.py index 9a59b0c..8aca452 100644 --- a/scripts/check_docs_version_sync.py +++ b/scripts/check_docs_version_sync.py @@ -110,7 +110,8 @@ class VersionMismatch: package: PackageInfo -def _read_cargo_package_info(cargo_toml: Path) -> PackageInfo: +def read_cargo_package_info(cargo_toml: Path) -> PackageInfo: + """Read the authoritative Cargo package name and version.""" package = _require_table(_read_toml(cargo_toml), "package", cargo_toml) return PackageInfo( name=_require_string(package, "name", f"{cargo_toml} [package]"), @@ -118,7 +119,8 @@ def _read_cargo_package_info(cargo_toml: Path) -> PackageInfo: ) -def _read_python_project_info(pyproject_toml: Path) -> PythonProjectInfo: +def read_python_project_info(pyproject_toml: Path) -> PythonProjectInfo: + """Read the Python support-package name and version.""" project = _require_table(_read_toml(pyproject_toml), "project", pyproject_toml) return PythonProjectInfo( name=_require_string(project, "name", f"{pyproject_toml} [project]"), @@ -126,7 +128,8 @@ def _read_python_project_info(pyproject_toml: Path) -> PythonProjectInfo: ) -def _toml_table_key_line(path: Path, table_name: str, key: str) -> int: +def toml_table_key_line(path: Path, table_name: str, key: str) -> int: + """Return the unique key line within one TOML table.""" current_table: str | None = None key_re = re.compile(rf"^{re.escape(key)}\s*=") for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): @@ -187,18 +190,21 @@ def _single_package_reference( return _version_reference(path, line, version, kind) -def _cargo_lock_reference(path: Path, package: PackageInfo) -> VersionReference: +def cargo_lock_reference(path: Path, package: PackageInfo) -> VersionReference: + """Return the local root-package version reference from Cargo.lock.""" entries = _package_entries(path) candidate_indices = [index for index, entry in enumerate(entries) if entry.get("name") == package.name and "source" not in entry] return _single_package_reference(path, entries, candidate_indices, package.name, ReferenceKind.CARGO_LOCK) -def _pyproject_reference(path: Path, project: PythonProjectInfo) -> VersionReference: - line = _toml_table_key_line(path, "project", "version") +def pyproject_reference(path: Path, project: PythonProjectInfo) -> VersionReference: + """Return the Python project version reference from pyproject.toml.""" + line = toml_table_key_line(path, "project", "version") return _version_reference(path, line, project.version, ReferenceKind.PYPROJECT) -def _uv_lock_reference(path: Path, project: PythonProjectInfo) -> VersionReference: +def uv_lock_reference(path: Path, project: PythonProjectInfo) -> VersionReference: + """Return the editable support-package version reference from uv.lock.""" entries = _package_entries(path) candidate_indices: list[int] = [] for index, entry in enumerate(entries): @@ -212,7 +218,8 @@ def _uv_lock_reference(path: Path, project: PythonProjectInfo) -> VersionReferen _CITATION_DATE_RE = re.compile(r"^date-released:\s*(?P['\"]?)(?P\d{4}-\d{2}-\d{2})(?P=quote)\s*(?:#.*)?$") -def _citation_reference(path: Path) -> VersionReference: +def citation_reference(path: Path) -> VersionReference: + """Return the top-level CFF software version reference.""" references: list[VersionReference] = [] for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): if not line.startswith("version:"): @@ -228,7 +235,8 @@ def _citation_reference(path: Path) -> VersionReference: return references[0] -def _release_date(path: Path) -> tuple[int, str]: +def citation_release_date(path: Path) -> tuple[int, str]: + """Return the unique top-level CFF release date and its line.""" matches: list[tuple[int, str]] = [] for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): if not line.startswith("date-released:"): @@ -250,25 +258,33 @@ def _release_date(path: Path) -> tuple[int, str]: return matches[0] -def _validate_release_date_sync(root: Path, package: PackageInfo) -> None: - """Require CFF and generated changelog to use the same UTC release date.""" - changelog = root / "CHANGELOG.md" - if not changelog.is_file(): - return - heading_re = re.compile(rf"^## \[v?{re.escape(package.version)}\] - (?P\d{{4}}-\d{{2}}-\d{{2}})$") +def changelog_release_date(path: Path, version: str) -> tuple[int, str] | None: + """Return the unique generated release-heading date for *version*, if present.""" + if not path.is_file(): + return None + heading_re = re.compile(rf"^## \[v?{re.escape(version)}\] - (?P\d{{4}}-\d{{2}}-\d{{2}})$") changelog_matches: list[tuple[int, str]] = [] - for line_number, line in enumerate(changelog.read_text(encoding="utf-8").splitlines(), start=1): + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): match = heading_re.fullmatch(line) if match is not None: changelog_matches.append((line_number, match.group("date"))) if not changelog_matches: - return + return None if len(changelog_matches) != 1: - msg = f"{changelog} must contain exactly one release heading for {package.version}; found {len(changelog_matches)}" + msg = f"{path} must contain exactly one release heading for {version}; found {len(changelog_matches)}" raise TypeError(msg) + return changelog_matches[0] + + +def _validate_release_date_sync(root: Path, package: PackageInfo) -> None: + """Require CFF and generated changelog to use the same UTC release date.""" + changelog = root / "CHANGELOG.md" + changelog_match = changelog_release_date(changelog, package.version) + if changelog_match is None: + return citation = root / "CITATION.cff" - citation_line, citation_date = _release_date(citation) - changelog_line, changelog_date = changelog_matches[0] + citation_line, citation_date = citation_release_date(citation) + changelog_line, changelog_date = changelog_match if citation_date != changelog_date: msg = ( f"release date mismatch: {citation}:{citation_line} has {citation_date}, " @@ -277,7 +293,8 @@ def _validate_release_date_sync(root: Path, package: PackageInfo) -> None: raise TypeError(msg) -def _iter_markdown_files(root: Path) -> list[Path]: +def iter_active_markdown_files(root: Path) -> list[Path]: + """Return active Markdown files, excluding archives, fixtures, and generated history.""" markdown_files: list[Path] = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [dirname for dirname in dirnames if not (set((Path(dirpath) / dirname).relative_to(root).parts) & SKIP_DIRS)] @@ -285,13 +302,14 @@ def _iter_markdown_files(root: Path) -> list[Path]: return sorted(markdown_files) -def _dependency_regex(package_name: str) -> re.Pattern[str]: +def dependency_regex(package_name: str) -> re.Pattern[str]: + """Build the dependency-snippet matcher for one Cargo package.""" escaped_name = re.escape(package_name) return re.compile(rf'(?[^"]+)"|\{{[^}}]*version\s*=\s*"(?P[^"]+)"[^}}]*\}})') def _dependency_references(path: Path, package_name: str) -> list[VersionReference]: - dependency_re = _dependency_regex(package_name) + dependency_re = dependency_regex(package_name) references: list[VersionReference] = [] for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): for match in dependency_re.finditer(line): @@ -308,11 +326,17 @@ def _dependency_references(path: Path, package_name: str) -> list[VersionReferen return references -_README_TAG_LINK_RE = re.compile( +README_TAG_LINK_RE = re.compile( r"https://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/)" r"(?:v(?P[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)" r"|(?P[0-9a-f]{7,40}))(?=/|$|[^0-9A-Za-z._+-])" ) +README_BENCHMARK_ASSET_PATH_PREFIX = "/docs/assets/bench/" + + +def readme_tag_link_is_benchmark_asset(match: re.Match[str]) -> bool: + """Return true when a tag-pinned README URL names a generated benchmark asset.""" + return match.string.startswith(README_BENCHMARK_ASSET_PATH_PREFIX, match.end()) def _readme_tag_references(path: Path) -> list[VersionReference]: @@ -326,7 +350,8 @@ def _readme_tag_references(path: Path) -> list[VersionReference]: ReferenceKind.README_TAG_LINK, line.strip(), ) - for match in _README_TAG_LINK_RE.finditer(line) + for match in README_TAG_LINK_RE.finditer(line) + if not readme_tag_link_is_benchmark_asset(match) ) return references @@ -349,14 +374,14 @@ def _benchmark_current_tag_references(path: Path) -> list[VersionReference]: def _version_references(root: Path, package: PackageInfo) -> list[VersionReference]: pyproject_path = root / "pyproject.toml" - project = _read_python_project_info(pyproject_path) + project = read_python_project_info(pyproject_path) references = [ - _cargo_lock_reference(root / "Cargo.lock", package), - _pyproject_reference(pyproject_path, project), - _uv_lock_reference(root / "uv.lock", project), - _citation_reference(root / "CITATION.cff"), + cargo_lock_reference(root / "Cargo.lock", package), + pyproject_reference(pyproject_path, project), + uv_lock_reference(root / "uv.lock", project), + citation_reference(root / "CITATION.cff"), ] - for path in _iter_markdown_files(root): + for path in iter_active_markdown_files(root): references.extend(_dependency_references(path, package.name)) references.extend(_benchmark_current_tag_references(path)) references.extend(_readme_tag_references(root / "README.md")) @@ -366,7 +391,7 @@ def _version_references(root: Path, package: PackageInfo) -> list[VersionReferen def find_version_mismatches(root: Path) -> list[VersionMismatch]: """Return release-version references that differ from Cargo.toml.""" - package = _read_cargo_package_info(root / "Cargo.toml") + package = read_cargo_package_info(root / "Cargo.toml") _validate_release_date_sync(root, package) return [VersionMismatch(reference=reference, package=package) for reference in _version_references(root, package) if reference.version != package.version] diff --git a/scripts/criterion_dim_plot.py b/scripts/criterion_dim_plot.py index 488a9b5..9b9ebdf 100644 --- a/scripts/criterion_dim_plot.py +++ b/scripts/criterion_dim_plot.py @@ -23,11 +23,13 @@ import sys import tempfile import tomllib -from dataclasses import dataclass, field as dataclass_field +from collections.abc import Mapping +from dataclasses import dataclass from pathlib import Path from typing import Final, Protocol, TypeGuard, cast -from performance_artifacts import ensure_distinct_paths +from benchmark_contract import benchmark_contract_digest +from performance_artifacts import ArtifactPaths, PerformanceBundle, TimingEstimate, ensure_distinct_paths, load_bundle from subprocess_utils import ExecutableNotFoundError, cpu_description, find_project_root, run_git_command, run_safe_command @@ -64,6 +66,7 @@ class PlotCliArgs: stat: str sample: str criterion_dir: str + performance_csv: str out: str | None csv: str | None log_y: bool @@ -73,6 +76,15 @@ class PlotCliArgs: allow_partial: bool +@dataclass(frozen=True, slots=True) +class _ValidatedPerformance: + """One retained bundle bound to the checkout used for publication.""" + + bundle: PerformanceBundle + validation: dict[str, object] + paths: ArtifactPaths + + @dataclass(frozen=True, slots=True) class Row: """Validated timing estimates for one benchmark dimension.""" @@ -110,51 +122,6 @@ def __post_init__(self) -> None: _require_confidence_interval(self.fa_lo, self.fa_hi, "faer row") -@dataclass(slots=True) -class _CriterionSampleTransaction: - """Move stale samples aside and restore them atomically on timing failure.""" - - criterion_dir: Path - backup_root: Path - moved: list[Path] = dataclass_field(default_factory=list) - - def stage(self) -> None: - """Move all existing vs_linalg `new` samples into the backup tree.""" - for sample in _vs_linalg_new_samples(self.criterion_dir): - relative = sample.relative_to(self.criterion_dir) - backup = self.backup_root / relative - backup.parent.mkdir(parents=True, exist_ok=True) - sample.replace(backup) - self.moved.append(relative) - - def rollback(self, *, remove_fresh: bool) -> None: - """Restore moved samples, retaining backups when any step fails.""" - errors: list[str] = [] - if remove_fresh: - for sample in _vs_linalg_new_samples(self.criterion_dir): - try: - shutil.rmtree(sample) - except OSError as exc: - errors.append(f"could not remove fresh sample {sample}: {exc}") - - for relative in reversed(self.moved): - source = self.backup_root / relative - destination = self.criterion_dir / relative - if not source.exists(): - continue - if destination.exists(): - errors.append(f"could not restore {destination}: destination already exists") - continue - try: - destination.parent.mkdir(parents=True, exist_ok=True) - source.replace(destination) - except OSError as exc: - errors.append(f"could not restore {destination}: {exc}") - - if errors: - raise RuntimeError("; ".join(errors)) - - class ReadmeMarkerError(ValueError): """Base error for invalid README BENCH_TABLE markers.""" @@ -167,6 +134,10 @@ class MarkerOrderError(ReadmeMarkerError): """Raised when README markers are out of order.""" +class ReadmeBenchmarkLinkError(ValueError): + """Raised when canonical README benchmark artifact links are incomplete.""" + + class PublicationRollbackError(RuntimeError): """Raised when artifact publication fails and rollback is incomplete.""" @@ -249,18 +220,10 @@ def no_plot(self) -> bool: ... } CANONICAL_DIMS: Final[tuple[int, ...]] = (2, 3, 4, 5, 8, 16, 32, 64) -_PUBLICATION_GATE: Final[tuple[str, ...]] = ("just", "test-bench-inputs") -_PUBLICATION_BENCHMARK_BASE: Final[tuple[str, ...]] = ( - "cargo", - "bench", - "--locked", - "--features", - "bench", - "--bench", - "vs_linalg", -) -_COMMAND_TIMEOUT_SECONDS: Final[int] = 7200 +_DEFAULT_PERFORMANCE_CSV: Final[str] = "target/bench-reports/performance.csv" +_RELEASE_VERSION_PATTERN: Final[str] = r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?" _PROVENANCE_HARNESS_FILES: Final[tuple[str, ...]] = ( + ".config/nextest.toml", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", @@ -502,6 +465,28 @@ def _update_readme_table(readme_path: Path, marker_begin: str, marker_end: str, return True +def _replace_readme_benchmark_asset_versions(text: str, *, metric: str, stat: str, version: str) -> str: + """Update the selected README benchmark links only after its artifacts exist.""" + if re.fullmatch(_RELEASE_VERSION_PATTERN, version) is None: + msg = f"Cargo package version is not valid for a release-pinned README link: {version!r}" + raise ReadmeBenchmarkLinkError(msg) + + asset_stem = f"/docs/assets/bench/vs_linalg_{metric}_{stat}" + expected_assets = sorted((f"{asset_stem}.csv", f"{asset_stem}.provenance.json", f"{asset_stem}.svg")) + pattern = re.compile( + r"(?Phttps://(?:github\.com/acgetchell/la-stack/(?:blob|raw|tree)/|raw\.githubusercontent\.com/acgetchell/la-stack/))" + rf"v(?P{_RELEASE_VERSION_PATTERN})" + rf"(?P{re.escape(asset_stem)}(?:\.csv|\.provenance\.json|\.svg))" + ) + matches = list(pattern.finditer(text)) + actual_assets = sorted(match.group("asset") for match in matches) + if actual_assets != expected_assets: + msg = f"README must contain exactly one tag-pinned link for each published benchmark asset; expected {expected_assets}, found {actual_assets}" + raise ReadmeBenchmarkLinkError(msg) + + return pattern.sub(lambda match: f"{match.group('prefix')}v{version}{match.group('asset')}", text) + + def _gp_quote(s: str) -> str: # gnuplot supports single-quoted strings; escape backslashes and single quotes. return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'" @@ -573,7 +558,14 @@ def _parse_args(argv: list[str]) -> PlotCliArgs: parser.add_argument( "--criterion-dir", default="target/criterion", - help="Criterion output directory (default: target/criterion).", + help="Criterion output directory for exploratory rendering (default: target/criterion).", + ) + parser.add_argument( + "--performance-csv", + default=_DEFAULT_PERFORMANCE_CSV, + help=( + "Retained performance-release CSV used by --update-readme; the provenance JSON must be adjacent (default: target/bench-reports/performance.csv)." + ), ) parser.add_argument( "--out", @@ -617,6 +609,7 @@ def _parse_args(argv: list[str]) -> PlotCliArgs: stat=_required_str_attr(args, "stat"), sample=_required_str_attr(args, "sample"), criterion_dir=_required_str_attr(args, "criterion_dir"), + performance_csv=_required_str_attr(args, "performance_csv"), out=_optional_str_attr(args, "out"), csv=_optional_str_attr(args, "csv"), log_y=_required_bool_attr(args, "log_y"), @@ -668,6 +661,12 @@ def _resolve_output_paths(root: Path, metric: str, stat: str, out_svg: str | Non return (svg, csv) +def _resolve_performance_paths(root: Path, performance_csv: str) -> ArtifactPaths: + """Resolve a retained performance CSV and its adjacent provenance JSON.""" + csv = _resolve_under_root(root, performance_csv) + return ArtifactPaths(csv=csv, provenance=csv.with_suffix(".provenance.json")) + + def _collect_rows(criterion_dir: Path, dims: list[int], metric: Metric, stat: str, sample: str) -> tuple[list[Row], list[str]]: rows: list[Row] = [] skipped: list[str] = [] @@ -712,95 +711,6 @@ def _collect_rows(criterion_dir: Path, dims: list[int], metric: Metric, stat: st return (rows, skipped) -def _publication_benchmark_command(metric_name: str) -> tuple[str, ...]: - """Return the focused Criterion command for one published metric.""" - metric = METRICS[metric_name] - benchmark_filter = "(" + "|".join(re.escape(name) for name in (metric.la_bench, metric.na_bench, metric.fa_bench)) + ")$" - return (*_PUBLICATION_BENCHMARK_BASE, "--", benchmark_filter) - - -def _run_publication_benchmarks(root: Path, metric_name: str) -> None: - """Validate fixtures, then measure one complete README metric.""" - _run_publication_command(root, _PUBLICATION_GATE) - criterion_dir = root / "target" / "criterion" - criterion_dir.parent.mkdir(parents=True, exist_ok=True) - backup_root = Path(tempfile.mkdtemp(prefix="la-stack-stale-criterion-", dir=criterion_dir.parent)) - transaction = _CriterionSampleTransaction(criterion_dir=criterion_dir, backup_root=backup_root) - preserve_backup = False - try: - try: - transaction.stage() - except OSError as primary: - try: - transaction.rollback(remove_fresh=False) - except RuntimeError as rollback: - preserve_backup = True - msg = f"could not stage Criterion samples and rollback failed: {rollback}; backups preserved at {backup_root}" - raise RuntimeError(msg) from primary - msg = f"could not stage existing Criterion samples: {primary}" - raise RuntimeError(msg) from primary - - try: - _run_publication_command(root, _publication_benchmark_command(metric_name)) - except RuntimeError as primary: - try: - transaction.rollback(remove_fresh=True) - except RuntimeError as rollback: - preserve_backup = True - msg = f"{primary}\nCriterion sample rollback failed: {rollback}; backups preserved at {backup_root}" - raise RuntimeError(msg) from primary - raise - finally: - if not preserve_backup: - try: - shutil.rmtree(backup_root) - except OSError as exc: - print(f"Warning: could not remove Criterion sample backup {backup_root}: {exc}", file=sys.stderr) - - -def _run_publication_command(root: Path, command: tuple[str, ...]) -> None: - """Run one publication command with complete failure context.""" - try: - run_safe_command( - command[0], - list(command[1:]), - cwd=root, - timeout=_COMMAND_TIMEOUT_SECONDS, - ) - except subprocess.CalledProcessError as exc: - stderr = exc.stderr.strip() if isinstance(exc.stderr, str) else "" - detail = f"\nstderr:\n{stderr}" if stderr else "" - msg = f"publication command failed ({exc.returncode}): {' '.join(command)}{detail}" - raise RuntimeError(msg) from exc - except ExecutableNotFoundError as exc: - msg = f"publication command could not start: {' '.join(command)}: {exc}" - raise RuntimeError(msg) from exc - except subprocess.TimeoutExpired as exc: - stderr = exc.stderr.strip() if isinstance(exc.stderr, str) else "" - detail = f"\nstderr:\n{stderr}" if stderr else "" - msg = f"publication command timed out after {exc.timeout} seconds: {' '.join(command)}{detail}" - raise RuntimeError(msg) from exc - except OSError as exc: - msg = f"publication command could not start: {' '.join(command)}: {exc}" - raise RuntimeError(msg) from exc - - -def _vs_linalg_new_samples(criterion_dir: Path) -> list[Path]: - """Return every current vs_linalg sample in deterministic order.""" - if not criterion_dir.is_dir(): - return [] - return sorted( - ( - sample - for group in criterion_dir.iterdir() - if group.is_dir() and _dim_from_group_dir(group.name) is not None - for sample in group.glob("*/new") - if sample.is_dir() - ), - key=lambda path: path.relative_to(criterion_dir).as_posix(), - ) - - def _git_value(root: Path, args: list[str]) -> str: """Return deterministic Git provenance or an explicit unavailable label.""" try: @@ -912,14 +822,8 @@ def _rustc_version(root: Path) -> str: return value or "unavailable" -def _capture_provenance( - root: Path, - *, - args: PlotCliArgs, - dims: list[int], - measurement_recorded: bool, -) -> dict[str, object]: - """Capture deterministic provenance for CSV/SVG and README publication.""" +def _capture_environment(root: Path) -> dict[str, object]: + """Capture the current source, harness, toolchain, host, and Git state.""" harness_sha256, missing_harness_files = _provenance_harness_digest(root) cargo_lock = root / "Cargo.lock" cargo_lock_sha256 = hashlib.sha256(cargo_lock.read_bytes()).hexdigest() if cargo_lock.is_file() else "unavailable" @@ -927,7 +831,7 @@ def _capture_provenance( os_description = " ".join(part for part in (platform.system(), platform.release(), platform.machine()) if part).strip() or "unavailable" git_clean, git_status_sha256 = _git_status_metadata(root) source_state_sha256, source_missing = _source_state_digest(root) - environment: dict[str, object] = { + return { "cargo_lock_sha256": cargo_lock_sha256, "commit": _git_value(root, ["--no-pager", "rev-parse", "HEAD"]), "cpu": cpu, @@ -940,19 +844,16 @@ def _capture_provenance( "source_missing": source_missing, "source_state_sha256": source_state_sha256, } - measurement: dict[str, object] - if measurement_recorded: - measurement = {"status": "recorded", **environment} - else: - measurement = { - "reason": "the exploratory renderer did not run the benchmark command that produced these Criterion samples", - "status": "unavailable", - } + + +def _capture_exploratory_provenance(root: Path, *, args: PlotCliArgs, dims: list[int]) -> dict[str, object]: + """Capture provenance for rendering unverified local Criterion samples.""" + environment = _capture_environment(root) criterion_version = _read_cargo_dependency_versions(root / "Cargo.toml", {"criterion"}).get("criterion", "unavailable") return { - "artifact": "README vs_linalg dimension plot" if args.update_readme else "exploratory vs_linalg dimension plot", + "artifact": "exploratory vs_linalg dimension plot", "criterion": { - "benchmark_command": list(_publication_benchmark_command(args.metric)) if measurement_recorded else "unavailable", + "benchmark_command": "unavailable", "criterion_dependency": criterion_version, "dimensions": dims, "log_y": args.log_y, @@ -960,44 +861,266 @@ def _capture_provenance( "sample": args.sample, "statistic": args.stat, }, - "measurement": measurement, + "measurement": { + "reason": "the exploratory renderer did not run the benchmark command that produced these Criterion samples", + "status": "unavailable", + }, "publication": { **environment, - "correctness_gate": "passed" if measurement_recorded else "not-run-exploratory", + "correctness_gate": "not-run-exploratory", }, "schema": 1, } +def _required_mapping(value: object, context: str) -> Mapping[str, object]: + """Parse one already-validated frozen provenance object.""" + if not isinstance(value, Mapping) or not all(isinstance(key, str) for key in value): + msg = f"retained performance provenance requires a {context} object" + raise TypeError(msg) + return cast("Mapping[str, object]", value) + + +def _json_compatible(value: object) -> object: + """Convert frozen retained-provenance containers into JSON containers.""" + if isinstance(value, Mapping): + return {str(key): _json_compatible(item) for key, item in value.items()} + if isinstance(value, tuple): + return [_json_compatible(item) for item in value] + return value + + +def _validate_performance_selection(root: Path, args: PlotCliArgs, bundle: PerformanceBundle) -> Mapping[str, object]: + """Validate the retained report selection and return recorded measurement metadata.""" + context = bundle.context + if context.suite not in {"all", "vs_linalg"}: + msg = f"retained performance data does not include vs_linalg: suite={context.suite!r}" + raise ValueError(msg) + if context.scope != "release-signal": + msg = f"README publication requires release-signal performance data, got {context.scope!r}" + raise ValueError(msg) + if context.statistic != args.stat: + msg = f"README statistic {args.stat!r} does not match retained performance statistic {context.statistic!r}" + raise ValueError(msg) + if context.release.current == context.release.baseline: + msg = "README publication requires a distinct release pair retained by `just performance-release`" + raise ValueError(msg) + + package_version = _read_cargo_package_version(root / "Cargo.toml") + if package_version is None: + msg = f"{root / 'Cargo.toml'} has no string package version" + raise ValueError(msg) + expected_release = f"v{package_version.removeprefix('v')}" + if context.release.current != expected_release: + msg = ( + f"retained performance data is for {context.release.current}, but Cargo.toml is {expected_release}; " + "run `just performance-release` for the current release" + ) + raise ValueError(msg) + + benchmark_provenance = context.benchmark_provenance + if benchmark_provenance.get("mode") != "shared-current-harness": + msg = "README publication requires locally recorded shared-current-harness performance data" + raise ValueError(msg) + measurement = _required_mapping(benchmark_provenance.get("measurement"), "measurement") + if measurement.get("status") != "recorded": + msg = "README publication requires recorded performance measurements" + raise ValueError(msg) + return measurement + + +def _validate_current_measurement(root: Path, measurement: Mapping[str, object]) -> dict[str, object]: + """Require retained measurements to match stable current checkout inputs.""" + cargo_lock = root / "Cargo.lock" + current_source_state, source_missing = _source_state_digest(root) + if source_missing: + msg = "current measured source directory is missing: src/" + raise ValueError(msg) + current = { + "cargo_lock_sha256": hashlib.sha256(cargo_lock.read_bytes()).hexdigest() if cargo_lock.is_file() else "unavailable", + "commit": _git_value(root, ["--no-pager", "rev-parse", "HEAD"]), + "source_state_sha256": current_source_state, + } + for retained_field, current_field in ( + ("cargo_lock_sha256", "cargo_lock_sha256"), + ("current_commit", "commit"), + ("current_source_state_sha256", "source_state_sha256"), + ): + retained = measurement.get(retained_field) + current_value = current.get(current_field) + if retained != current_value: + msg = ( + f"retained performance {retained_field} {retained!r} does not match the current checkout {current_field} {current_value!r}; " + "run `just performance-release` again" + ) + raise ValueError(msg) + + retained_contract = measurement.get("benchmark_contract_sha256") + contract_status = "legacy-retained-artifact" + if retained_contract is not None: + current_contract = benchmark_contract_digest(root) + if retained_contract != current_contract: + msg = ( + f"retained performance benchmark_contract_sha256 {retained_contract!r} does not match " + f"the current checkout benchmark contract {current_contract!r}; run `just performance-release` again" + ) + raise ValueError(msg) + contract_status = "matched" + return { + **current, + "benchmark_contract": contract_status, + } + + +def _validate_performance_bundle(root: Path, args: PlotCliArgs, bundle: PerformanceBundle) -> dict[str, object]: + """Bind retained release measurements to the current release checkout.""" + measurement = _validate_performance_selection(root, args, bundle) + return _validate_current_measurement(root, measurement) + + +def _row_from_estimates( + *, + dim: int, + la_stack: TimingEstimate, + nalgebra: TimingEstimate, + faer: TimingEstimate, +) -> Row: + return Row( + dim=dim, + la_time=la_stack.median_ns, + la_lo=la_stack.ci_lower_ns, + la_hi=la_stack.ci_upper_ns, + na_time=nalgebra.median_ns, + na_lo=nalgebra.ci_lower_ns, + na_hi=nalgebra.ci_upper_ns, + fa_time=faer.median_ns, + fa_lo=faer.ci_lower_ns, + fa_hi=faer.ci_upper_ns, + ) + + +def _collect_performance_rows(bundle: PerformanceBundle, metric: Metric) -> list[Row]: + """Extract current la-stack and retained same-harness peer timings.""" + rows_by_key = {(row.group, row.benchmark): row for row in bundle.rows if row.suite == "vs_linalg"} + rows: list[Row] = [] + for dim in CANONICAL_DIMS: + key = (f"d{dim}", metric.la_bench) + performance_row = rows_by_key.get(key) + if performance_row is None: + msg = f"retained performance data is missing {key[0]}/{key[1]}" + raise ValueError(msg) + missing = [ + name + for name, estimate in ( + ("current la-stack", performance_row.current), + ("nalgebra peer", performance_row.baseline_nalgebra), + ("faer peer", performance_row.baseline_faer), + ) + if estimate is None + ] + if missing: + msg = f"retained performance data for {key[0]}/{key[1]} is missing {', '.join(missing)} timing" + raise ValueError(msg) + if performance_row.current is None or performance_row.baseline_nalgebra is None or performance_row.baseline_faer is None: + msg = "retained performance timing presence invariant violated" + raise AssertionError(msg) + rows.append( + _row_from_estimates( + dim=dim, + la_stack=performance_row.current, + nalgebra=performance_row.baseline_nalgebra, + faer=performance_row.baseline_faer, + ) + ) + return rows + + +def _display_path(root: Path, path: Path) -> str: + try: + return path.resolve().relative_to(root.resolve()).as_posix() + except ValueError: + return str(path.resolve()) + + +def _capture_performance_provenance( + root: Path, + *, + args: PlotCliArgs, + dims: list[int], + retained: _ValidatedPerformance, +) -> dict[str, object]: + """Derive README artifact provenance from a validated performance bundle.""" + bundle = retained.bundle + paths = retained.paths + benchmark_provenance = bundle.context.benchmark_provenance + criterion = _required_mapping(benchmark_provenance.get("criterion"), "criterion") + measurement = _required_mapping(benchmark_provenance.get("measurement"), "measurement") + return { + "artifact": "README vs_linalg dimension plot", + "criterion": { + "baseline_command": _json_compatible(criterion.get("baseline_command")), + "criterion_dependency": criterion.get("criterion_version"), + "current_command": _json_compatible(criterion.get("current_command")), + "dimensions": dims, + "log_y": args.log_y, + "metric": args.metric, + "sample": args.sample, + "statistic": args.stat, + }, + "measurement": { + **cast("dict[str, object]", _json_compatible(measurement)), + "la_stack_sample": "current", + "peer_release_context": bundle.context.release.baseline, + "peer_sample": "baseline phase under shared current harness", + "source": "retained performance-release artifact", + }, + "performance_artifact": { + "csv": _display_path(root, paths.csv), + "csv_sha256": hashlib.sha256(paths.csv.read_bytes()).hexdigest(), + "provenance": _display_path(root, paths.provenance), + "provenance_sha256": hashlib.sha256(paths.provenance.read_bytes()).hexdigest(), + "release": { + "baseline": bundle.context.release.baseline, + "current": bundle.context.release.current, + }, + }, + "publication": { + **retained.validation, + "correctness_gate": "validated-performance-release-artifact", + }, + "schema": 2, + } + + def _write_provenance(path: Path, provenance: dict[str, object]) -> None: """Write stable, sorted JSON provenance beside generated benchmark assets.""" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(provenance, indent=2, sort_keys=True) + "\n", encoding="utf-8") -def _validate_readme_target(root: Path, args: PlotCliArgs) -> int: # noqa: C901, PLR0911 - """Validate publication-only CLI invariants and README markers before timing.""" +def _validate_readme_target(root: Path, args: PlotCliArgs) -> int: # noqa: C901, PLR0911, PLR0912 + """Validate publication-only CLI invariants and README markers before rendering.""" if not args.update_readme: return 0 if args.allow_partial: print("--allow-partial is exploratory-only and cannot be combined with --update-readme", file=sys.stderr) return 2 if args.sample != "new": - print("README publication requires --sample new so the gated timing run is the data being published", file=sys.stderr) + print("README publication requires --sample new to identify the retained current-release measurements", file=sys.stderr) + return 2 + if args.stat != "median": + print("README publication requires --stat median to match performance-release artifacts", file=sys.stderr) return 2 if args.no_plot: print("README publication requires SVG rendering; --no-plot is exploratory-only", file=sys.stderr) return 2 - criterion_dir = _resolve_under_root(root, args.criterion_dir).resolve() - expected_criterion_dir = (root / "target" / "criterion").resolve() - if criterion_dir != expected_criterion_dir: - print( - f"README publication requires Criterion output at {expected_criterion_dir}; got {criterion_dir}", - file=sys.stderr, - ) - return 2 readme_path = _resolve_under_root(root, args.readme) canonical_readme = (root / "README.md").resolve() + try: + readme_text = readme_path.read_text(encoding="utf-8") + except OSError as exc: + print(str(exc), file=sys.stderr) + return 2 if readme_path.resolve() == canonical_readme: expected_svg, expected_csv = _resolve_output_paths(root, args.metric, args.stat, None, None) selected_svg, selected_csv = _resolve_output_paths(root, args.metric, args.stat, args.out, args.csv) @@ -1007,12 +1130,30 @@ def _validate_readme_target(root: Path, args: PlotCliArgs) -> int: # noqa: C901 file=sys.stderr, ) return 2 + expected_performance = _resolve_performance_paths(root, _DEFAULT_PERFORMANCE_CSV) + selected_performance = _resolve_performance_paths(root, args.performance_csv) + if selected_performance.csv.resolve() != expected_performance.csv.resolve(): + print( + f"README publication requires the canonical performance-release input {expected_performance.csv}; got {selected_performance.csv}", + file=sys.stderr, + ) + return 2 + try: + package_version = _read_cargo_package_version(root / "Cargo.toml") + if package_version is None: + msg = f"{root / 'Cargo.toml'} has no string package version" + raise ReadmeBenchmarkLinkError(msg) + _replace_readme_benchmark_asset_versions( + readme_text, + metric=args.metric, + stat=args.stat, + version=package_version, + ) + except (OSError, TypeError, ValueError, tomllib.TOMLDecodeError) as exc: + print(str(exc), file=sys.stderr) + return 2 marker_begin, marker_end = _readme_table_markers(args.metric, args.stat, args.sample) - try: - lines = readme_path.read_text(encoding="utf-8").splitlines() - except OSError as exc: - print(str(exc), file=sys.stderr) - return 2 + lines = readme_text.splitlines() begin_count = sum(line.strip() == marker_begin for line in lines) end_count = sum(line.strip() == marker_end for line in lines) if begin_count != 1 or end_count != 1: @@ -1027,7 +1168,7 @@ def _validate_readme_target(root: Path, args: PlotCliArgs) -> int: # noqa: C901 def _validate_publication_paths(root: Path, args: PlotCliArgs, *, out_svg: Path, out_csv: Path) -> int: - """Reject output aliases before benchmarks or publication can begin.""" + """Reject input/output aliases before publication can begin.""" paths = { "CSV output": out_csv, "provenance output": out_csv.with_suffix(".provenance.json"), @@ -1036,6 +1177,9 @@ def _validate_publication_paths(root: Path, args: PlotCliArgs, *, out_svg: Path, paths["SVG output"] = out_svg if args.update_readme: paths["README output"] = _resolve_under_root(root, args.readme) + performance = _resolve_performance_paths(root, args.performance_csv) + paths["performance input CSV"] = performance.csv + paths["performance input provenance"] = performance.provenance try: ensure_distinct_paths(paths) except (OSError, ValueError) as exc: @@ -1044,10 +1188,21 @@ def _validate_publication_paths(root: Path, args: PlotCliArgs, *, out_svg: Path, return 0 +def _changed_staged_files(pairs: list[tuple[Path, Path]]) -> list[tuple[Path, Path]]: + """Return staged files whose destination bytes differ or do not exist.""" + changed_pairs: list[tuple[Path, Path]] = [] + for staged, destination in pairs: + if destination.is_file() and staged.read_bytes() == destination.read_bytes(): + continue + changed_pairs.append((staged, destination)) + return changed_pairs + + def _replace_staged_files(pairs: list[tuple[Path, Path]], backup_dir: Path) -> None: """Replace a group of publication files and roll back on any failure.""" + changed_pairs = _changed_staged_files(pairs) backups: dict[Path, Path | None] = {} - for index, (_staged, destination) in enumerate(pairs): + for index, (_staged, destination) in enumerate(changed_pairs): destination.parent.mkdir(parents=True, exist_ok=True) if destination.is_file(): backup = backup_dir / f"backup-{index}" @@ -1061,7 +1216,7 @@ def _replace_staged_files(pairs: list[tuple[Path, Path]], backup_dir: Path) -> N replaced: list[Path] = [] try: - for staged, destination in pairs: + for staged, destination in changed_pairs: staged.replace(destination) replaced.append(destination) except OSError as primary: @@ -1103,6 +1258,34 @@ def _publish_staged_files(pairs: list[tuple[Path, Path]], root: Path) -> bool: return True +def _update_staged_readme_publication( + *, + root: Path, + args: PlotCliArgs, + rows: list[Row], + staged_readme: Path, +) -> None: + """Stage the README table and canonical artifact links as one publication.""" + marker_begin, marker_end = _readme_table_markers(args.metric, args.stat, args.sample) + _update_readme_table(staged_readme, marker_begin, marker_end, _markdown_table(rows, args.stat)) + + readme_path = _resolve_under_root(root, args.readme) + if readme_path.resolve() != (root / "README.md").resolve(): + return + + package_version = _read_cargo_package_version(root / "Cargo.toml") + if package_version is None: + msg = f"{root / 'Cargo.toml'} has no string package version" + raise ReadmeBenchmarkLinkError(msg) + updated_readme = _replace_readme_benchmark_asset_versions( + staged_readme.read_text(encoding="utf-8"), + metric=args.metric, + stat=args.stat, + version=package_version, + ) + staged_readme.write_text(updated_readme, encoding="utf-8") + + def _stage_and_publish_outputs( # noqa: PLR0913 *, root: Path, @@ -1150,9 +1333,8 @@ def _stage_and_publish_outputs( # noqa: PLR0913 readme_path = _resolve_under_root(root, args.readme) staged_readme = stage_dir / "README.md" shutil.copy2(readme_path, staged_readme) - marker_begin, marker_end = _readme_table_markers(args.metric, args.stat, args.sample) try: - _update_readme_table(staged_readme, marker_begin, marker_end, _markdown_table(rows, args.stat)) + _update_staged_readme_publication(root=root, args=args, rows=rows, staged_readme=staged_readme) except (OSError, ValueError) as exc: print(str(exc), file=sys.stderr) print("No benchmark publication files were changed.", file=sys.stderr) @@ -1171,7 +1353,7 @@ def _stage_and_publish_outputs( # noqa: PLR0913 print(f"Wrote SVG: {req.out_svg}") print(f"Wrote provenance: {final_provenance}") if args.update_readme: - print(f"Updated README table: {_resolve_under_root(root, args.readme)}") + print(f"Updated README benchmark publication: {_resolve_under_root(root, args.readme)}") return 0 @@ -1231,13 +1413,6 @@ def main(argv: list[str] | None = None) -> int: # noqa: C901, PLR0911, PLR0912, rc = _validate_publication_paths(root, args, out_svg=out_svg, out_csv=out_csv) if rc != 0: return rc - if args.update_readme: - try: - _run_publication_benchmarks(root, args.metric) - except (FileNotFoundError, RuntimeError) as exc: - print(str(exc), file=sys.stderr) - return 2 - versions = _detect_versions(root) _print_versions(versions) @@ -1245,73 +1420,70 @@ def main(argv: list[str] | None = None) -> int: # noqa: C901, PLR0911, PLR0912, na_label = _format_legend_label("nalgebra", versions.get("nalgebra", "unknown")) fa_label = _format_legend_label("faer", versions.get("faer", "unknown")) - criterion_dir = _resolve_under_root(root, args.criterion_dir) - - discovered_dims = _discover_dims(criterion_dir) if criterion_dir.exists() else [] - dims = discovered_dims if args.allow_partial else list(CANONICAL_DIMS) - if not args.allow_partial and not discovered_dims: - dims = [] - if not dims: - print( - f"No Criterion results found under {criterion_dir}.\n\nRun benchmarks first, e.g.:\n cargo bench --bench vs_linalg\n", - file=sys.stderr, - ) - return 2 - metric = METRICS[args.metric] - - try: - rows, skipped = _collect_rows(criterion_dir, dims, metric, args.stat, args.sample) - except (OSError, KeyError, TypeError, ValueError) as exc: - print(f"Invalid Criterion estimate data: {exc}", file=sys.stderr) - return 2 - if not rows: - print( - "No benchmark results found to plot for the selected metric/stat.\n" - f"Expected files like:\n {criterion_dir}/d32/{metric.la_bench}/{args.sample}/estimates.json\n", - file=sys.stderr, - ) - if skipped: - print("Skipped groups:", *skipped, sep="\n - ", file=sys.stderr) - return 2 - - if not args.allow_partial and skipped: - print( - "Canonical benchmark coverage is incomplete; no CSV, SVG, provenance, or README file was written.", - file=sys.stderr, - ) - print("Required dimensions: " + ", ".join(f"D={dim}" for dim in CANONICAL_DIMS), file=sys.stderr) - print("Coverage gaps:", *skipped, sep="\n - ", file=sys.stderr) - return 2 + if args.update_readme: + performance_paths = _resolve_performance_paths(root, args.performance_csv) + try: + performance_bundle = load_bundle(performance_paths) + validation = _validate_performance_bundle(root, args, performance_bundle) + rows = _collect_performance_rows(performance_bundle, metric) + retained = _ValidatedPerformance(bundle=performance_bundle, validation=validation, paths=performance_paths) + provenance = _capture_performance_provenance( + root, + args=args, + dims=[row.dim for row in rows], + retained=retained, + ) + except (FileNotFoundError, OSError, TypeError, ValueError, AssertionError, tomllib.TOMLDecodeError) as exc: + print(f"Invalid retained performance-release data: {exc}", file=sys.stderr) + return 2 + skipped: list[str] = [] + else: + criterion_dir = _resolve_under_root(root, args.criterion_dir) + discovered_dims = _discover_dims(criterion_dir) if criterion_dir.exists() else [] + dims = discovered_dims if args.allow_partial else list(CANONICAL_DIMS) + if not args.allow_partial and not discovered_dims: + dims = [] + if not dims: + print( + f"No Criterion results found under {criterion_dir}.\n\nRun benchmarks first, e.g.:\n cargo bench --bench vs_linalg\n", + file=sys.stderr, + ) + return 2 + try: + rows, skipped = _collect_rows(criterion_dir, dims, metric, args.stat, args.sample) + except (OSError, KeyError, TypeError, ValueError) as exc: + print(f"Invalid Criterion estimate data: {exc}", file=sys.stderr) + return 2 + if not rows: + print( + "No benchmark results found to plot for the selected metric/stat.\n" + f"Expected files like:\n {criterion_dir}/d32/{metric.la_bench}/{args.sample}/estimates.json\n", + file=sys.stderr, + ) + if skipped: + print("Skipped groups:", *skipped, sep="\n - ", file=sys.stderr) + return 2 + if not args.allow_partial and skipped: + print( + "Canonical benchmark coverage is incomplete; no CSV, SVG, provenance, or README file was written.", + file=sys.stderr, + ) + print("Required dimensions: " + ", ".join(f"D={dim}" for dim in CANONICAL_DIMS), file=sys.stderr) + print("Coverage gaps:", *skipped, sep="\n - ", file=sys.stderr) + return 2 + provenance = _capture_exploratory_provenance(root, args=args, dims=[row.dim for row in rows]) dims_present = [row.dim for row in rows] - provenance = _capture_provenance( - root, - args=args, - dims=dims_present, - measurement_recorded=args.update_readme, - ) - publication = provenance.get("publication") - if not isinstance(publication, dict): - msg = "publication provenance invariant violated" - raise TypeError(msg) - missing_harness_files = publication.get("missing_harness_files") - if not isinstance(missing_harness_files, list) or not all(isinstance(path, str) for path in missing_harness_files): - msg = "publication missing_harness_files invariant violated" - raise AssertionError(msg) - provenance_gaps = list(cast("list[str]", missing_harness_files)) - if publication.get("source_missing") is True: - provenance_gaps.append("src/") - provenance_gaps.extend(field for field in ("cargo_lock_sha256", "commit", "cpu", "os", "rustc") if publication.get(field) == "unavailable") - if publication.get("git_clean") is None: - provenance_gaps.append("git status") - if args.update_readme and provenance_gaps: - print( - "Publication provenance is incomplete; no CSV, SVG, provenance, or README file was written because required fields are unavailable: " - + ", ".join(provenance_gaps), - file=sys.stderr, - ) - return 2 + if not args.update_readme: + publication = provenance.get("publication") + if not isinstance(publication, dict): + msg = "publication provenance invariant violated" + raise TypeError(msg) + missing_harness_files = publication.get("missing_harness_files") + if not isinstance(missing_harness_files, list) or not all(isinstance(path, str) for path in missing_harness_files): + msg = "publication missing_harness_files invariant violated" + raise AssertionError(msg) title = f"{metric.title}: {args.stat} time vs dimension" req = PlotRequest( diff --git a/scripts/performance_artifacts.py b/scripts/performance_artifacts.py index 0b10767..5bbee61 100644 --- a/scripts/performance_artifacts.py +++ b/scripts/performance_artifacts.py @@ -339,6 +339,8 @@ def _validate_environment_provenance(data: Mapping[str, object], *, context: str _required_provenance_string(data, field, context=context) for field in ("cargo_lock_sha256", "harness_sha256", "source_state_sha256"): _required_provenance_sha256(data, field, context=context) + if "benchmark_contract_sha256" in data: + _required_provenance_sha256(data, "benchmark_contract_sha256", context=context) _required_provenance_bool(data, "git_clean", context=context) gate = _required_provenance_string(data, "correctness_gate", context=context) if gate != "passed": @@ -395,6 +397,8 @@ def _validate_measurement_provenance(measurement: Mapping[str, object], *, mode: "baseline_source_state_sha256", ): _required_provenance_sha256(measurement, field, context="measurement") + if "benchmark_contract_sha256" in measurement: + _required_provenance_sha256(measurement, "benchmark_contract_sha256", context="measurement") for field in ("current_git_clean", "baseline_git_clean"): _required_provenance_bool(measurement, field, context="measurement") elif measurement_status == "unavailable": @@ -465,6 +469,14 @@ def _validate_recorded_measurement_consistency( first_context="measurement", second_context="publication", ) + if "benchmark_contract_sha256" in measurement or "benchmark_contract_sha256" in publication: + _require_matching_provenance( + measurement, + publication, + "benchmark_contract_sha256", + first_context="measurement", + second_context="publication", + ) for measurement_field, publication_field in ( ("current_commit", "commit"), ("current_git_clean", "git_clean"), diff --git a/scripts/tests/test_archive_performance.py b/scripts/tests/test_archive_performance.py index 294bca0..692fc72 100644 --- a/scripts/tests/test_archive_performance.py +++ b/scripts/tests/test_archive_performance.py @@ -1632,6 +1632,7 @@ def fake_run_safe(command: str, args: Sequence[str], cwd: Path | None = None, ** assert metadata["mode"] == "shared-current-harness" assert metadata["schema"] == 2 assert metadata["measurement"]["harness_sha256"] == archive_performance._benchmark_harness_digest(report_root) + assert metadata["measurement"]["benchmark_contract_sha256"] == archive_performance.benchmark_contract_digest(report_root) assert metadata["measurement"]["current_source_state_sha256"] == archive_performance._source_state_digest(report_root) assert metadata["criterion"]["criterion_version"] == "manifest requirement 0.7.0" assert metadata["validation"]["baseline_revision"] == "passed" diff --git a/scripts/tests/test_check_docs_version_sync.py b/scripts/tests/test_check_docs_version_sync.py index 3629ef9..35fba90 100644 --- a/scripts/tests/test_check_docs_version_sync.py +++ b/scripts/tests/test_check_docs_version_sync.py @@ -119,6 +119,19 @@ def test_find_version_mismatches_reports_readme_tag_links(tmp_path: Path) -> Non assert [mismatch.reference.version for mismatch in mismatches] == ["1.2.2", "1.2.1", "abc1234"] +def test_find_version_mismatches_ignores_plot_owned_readme_benchmark_asset_links(tmp_path: Path) -> None: + _write_project( + tmp_path, + readme=( + "[csv](https://github.com/acgetchell/la-stack/blob/v1.2.2/docs/assets/bench/result.csv)\n" + "[provenance](https://github.com/acgetchell/la-stack/blob/v1.2.2/docs/assets/bench/result.provenance.json)\n" + "[svg](https://raw.githubusercontent.com/acgetchell/la-stack/v1.2.2/docs/assets/bench/result.svg)\n" + ), + ) + + assert check_docs_version_sync.find_version_mismatches(tmp_path) == [] + + @pytest.mark.parametrize("tag", ["v1.2.3.4", "v1.2.3.extra", "v1.2.3_suffix"]) def test_readme_tag_references_reject_longer_non_semver_tags(tmp_path: Path, tag: str) -> None: readme = tmp_path / "README.md" diff --git a/scripts/tests/test_criterion_dim_plot.py b/scripts/tests/test_criterion_dim_plot.py index fbfc8a5..fb6a28e 100644 --- a/scripts/tests/test_criterion_dim_plot.py +++ b/scripts/tests/test_criterion_dim_plot.py @@ -1,6 +1,7 @@ """Tests for Criterion dimension-report generation and README updates.""" import argparse +import hashlib import json import re import subprocess @@ -12,6 +13,16 @@ import pytest import criterion_dim_plot +from performance_artifacts import ( + ArtifactContext, + ArtifactPaths, + PerformanceBundle, + PerformanceRow, + ReleasePair, + ReportSource, + TimingEstimate, + write_bundle, +) if TYPE_CHECKING: from pathlib import Path @@ -106,24 +117,6 @@ def test_markdown_table_formats_values_and_pct() -> None: assert "| 64 | 1,000.000 | 900.000 | 800.000 | -11.1% | -25.0% |" in table -@pytest.mark.parametrize( - ("metric_name", "expected_filter"), - [ - ("lu_solve", "(la_stack_lu_solve|nalgebra_lu_solve|faer_lu_solve)$"), - ("dot", "(la_stack_dot|nalgebra_dot|faer_dot)$"), - ("inf_norm", "(la_stack_inf_norm|nalgebra_inf_norm|faer_inf_norm)$"), - ], -) -def test_publication_benchmark_command_selects_only_requested_metric( - metric_name: str, - expected_filter: str, -) -> None: - command = criterion_dim_plot._publication_benchmark_command(metric_name) - - assert command[:-2] == criterion_dim_plot._PUBLICATION_BENCHMARK_BASE - assert command[-2:] == ("--", expected_filter) - - def test_row_rejects_zero_peer_time_before_markdown_rendering() -> None: with pytest.raises(ValueError, match="na_time must be finite and positive"): criterion_dim_plot.Row( @@ -237,12 +230,52 @@ def test_update_readme_table_errors_on_non_unique_markers(tmp_path: Path) -> Non criterion_dim_plot._update_readme_table(readme, marker_begin, marker_end, "| x |") +def _canonical_benchmark_readme(version: str) -> str: + begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") + return ( + "[docs](https://github.com/acgetchell/la-stack/blob/v0.0.9/README.md)\n" + f"[csv](https://github.com/acgetchell/la-stack/blob/v{version}/docs/assets/bench/vs_linalg_lu_solve_median.csv)\n" + f"[provenance](https://github.com/acgetchell/la-stack/blob/v{version}/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json)\n" + f"[svg](https://raw.githubusercontent.com/acgetchell/la-stack/v{version}/docs/assets/bench/vs_linalg_lu_solve_median.svg)\n" + f"{begin}\nold table\n{end}\n" + ) + + +def test_replace_readme_benchmark_asset_versions_updates_only_complete_selected_asset_set() -> None: + updated = criterion_dim_plot._replace_readme_benchmark_asset_versions( + _canonical_benchmark_readme("1.2.2"), + metric="lu_solve", + stat="median", + version="1.2.3", + ) + + assert updated.count("v1.2.3/docs/assets/bench/") == 3 + assert "blob/v0.0.9/README.md" in updated + assert "old table" in updated + + +def test_replace_readme_benchmark_asset_versions_rejects_incomplete_asset_set() -> None: + readme = _canonical_benchmark_readme("1.2.2").replace( + "[provenance](https://github.com/acgetchell/la-stack/blob/v1.2.2/docs/assets/bench/vs_linalg_lu_solve_median.provenance.json)\n", + "", + ) + + with pytest.raises(criterion_dim_plot.ReadmeBenchmarkLinkError, match="exactly one tag-pinned link"): + criterion_dim_plot._replace_readme_benchmark_asset_versions( + readme, + metric="lu_solve", + stat="median", + version="1.2.3", + ) + + def _publication_args() -> criterion_dim_plot.PlotCliArgs: return criterion_dim_plot.PlotCliArgs( metric="lu_solve", stat="median", sample="new", criterion_dir="target/criterion", + performance_csv=criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, out=None, csv=None, log_y=True, @@ -253,7 +286,7 @@ def _publication_args() -> criterion_dim_plot.PlotCliArgs: ) -def test_readme_publication_rejects_noncanonical_data_and_asset_paths_before_timing( +def test_readme_publication_rejects_noncanonical_data_and_asset_paths_before_rendering( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: @@ -263,11 +296,11 @@ def test_readme_publication_rejects_noncanonical_data_and_asset_paths_before_tim assert ( criterion_dim_plot._validate_readme_target( tmp_path, - replace(_publication_args(), criterion_dir="stale-results"), + replace(_publication_args(), performance_csv="stale-results.csv"), ) == 2 ) - assert "requires Criterion output" in capsys.readouterr().err + assert "canonical performance-release input" in capsys.readouterr().err assert ( criterion_dim_plot._validate_readme_target( @@ -279,7 +312,7 @@ def test_readme_publication_rejects_noncanonical_data_and_asset_paths_before_tim assert "canonical CSV/SVG destinations" in capsys.readouterr().err -def test_readme_publication_rejects_no_plot_before_timing( +def test_readme_publication_rejects_no_plot_before_rendering( tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: @@ -315,69 +348,168 @@ def test_fixture_readme_may_use_custom_asset_destinations(tmp_path: Path) -> Non ) -def test_main_update_readme_happy_path( # noqa: PLR0915 +def test_readme_publication_rejects_incomplete_benchmark_links_before_rendering( tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], ) -> None: - # Create a minimal Criterion directory structure for lu_solve. - criterion_dir = tmp_path / "target" / "criterion" + (tmp_path / "Cargo.toml").write_text('[package]\nname = "fixture"\nversion = "1.2.3"\n', encoding="utf-8") + readme = _canonical_benchmark_readme("1.2.2").replace( + "[svg](https://raw.githubusercontent.com/acgetchell/la-stack/v1.2.2/docs/assets/bench/vs_linalg_lu_solve_median.svg)\n", + "", + ) + (tmp_path / "README.md").write_text(readme, encoding="utf-8") - def write_estimates(path: Path, median: float) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - json.dumps( - { - "median": { - "point_estimate": median, - "confidence_interval": {"lower_bound": median * 0.9, "upper_bound": median * 1.1}, - } - } - ), - encoding="utf-8", - ) + assert criterion_dim_plot._validate_readme_target(tmp_path, _publication_args()) == 2 + assert "exactly one tag-pinned link" in capsys.readouterr().err - for d in criterion_dim_plot.CANONICAL_DIMS: - la, na, fa = (float(d * 5), float(d * 10), float(d * 20)) - base = criterion_dir / f"d{d}" - write_estimates(base / "la_stack_lu_solve" / "new" / "estimates.json", la) - write_estimates(base / "nalgebra_lu_solve" / "new" / "estimates.json", na) - write_estimates(base / "faer_lu_solve" / "new" / "estimates.json", fa) - readme = tmp_path / "README.fixture.md" - marker_begin, marker_end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - readme.write_text(f"# Bench\n{marker_begin}\nplaceholder\n{marker_end}\n", encoding="utf-8") +_TEST_COMMIT = "0123456789abcdef0123456789abcdef01234567" - (tmp_path / "Cargo.toml").write_text( - '[package]\nname = "fixture"\nversion = "0.1.0"\n[dev-dependencies]\ncriterion = "0.7"\nnalgebra = "0.34"\nfaer = "0.22"\n', + +def _timing(value: float) -> TimingEstimate: + return TimingEstimate(median_ns=value, ci_lower_ns=value * 0.9, ci_upper_ns=value * 1.1) + + +def _write_benchmark_checkout(root: Path, *, version: str = "0.1.0") -> None: + (root / "Cargo.toml").write_text( + f'[package]\nname = "fixture"\nversion = "{version}"\n[dev-dependencies]\ncriterion = "0.7"\nnalgebra = "0.34"\nfaer = "0.22"\n', encoding="utf-8", ) - (tmp_path / "Cargo.lock").write_text("version = 4\n", encoding="utf-8") - (tmp_path / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "1.88.0"\n', encoding="utf-8") - (tmp_path / "justfile").write_text("test-bench-inputs:\n", encoding="utf-8") - for relative in ("tests/exact_bench_config.rs", "tests/vs_linalg_inputs.rs", "benches/vs_linalg.rs", "src/lib.rs"): - path = tmp_path / relative + (root / "Cargo.lock").write_text("version = 4\n", encoding="utf-8") + (root / "rust-toolchain.toml").write_text('[toolchain]\nchannel = "1.88.0"\n', encoding="utf-8") + (root / "justfile").write_text("test-bench-inputs:\n", encoding="utf-8") + for relative in ( + ".config/nextest.toml", + "tests/exact_bench_config.rs", + "tests/vs_linalg_inputs.rs", + "benches/vs_linalg.rs", + "src/lib.rs", + ): + path = root / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_text("// fixture\n", encoding="utf-8") - calls: list[tuple[str, tuple[str, ...]]] = [] - def fake_run_safe(command: str, args: list[str], **_kwargs: object) -> SimpleNamespace: - calls.append((command, tuple(args))) - if command == "cargo": - for dimension in criterion_dim_plot.CANONICAL_DIMS: - base = criterion_dir / f"d{dimension}" - write_estimates(base / "la_stack_lu_solve" / "new" / "estimates.json", float(dimension * 5)) - write_estimates(base / "nalgebra_lu_solve" / "new" / "estimates.json", float(dimension * 10)) - write_estimates(base / "faer_lu_solve" / "new" / "estimates.json", float(dimension * 20)) - return SimpleNamespace(stdout="rustc 1.88.0\n" if command == "rustc" else "") +def _write_performance_bundle( + root: Path, + *, + version: str = "0.1.0", + omit_dim: int | None = None, + omit_peer_dim: int | None = None, + include_contract: bool = True, +) -> ArtifactPaths: + harness_sha256, missing = criterion_dim_plot._provenance_harness_digest(root) + source_sha256, source_missing = criterion_dim_plot._source_state_digest(root) + assert missing == [] + assert source_missing is False + cargo_lock_sha256 = hashlib.sha256((root / "Cargo.lock").read_bytes()).hexdigest() + baseline_commit = "fedcba9876543210fedcba9876543210fedcba98" + baseline_source_sha256 = "b" * 64 + environment: dict[str, object] = { + "cargo_lock_sha256": cargo_lock_sha256, + "commit": _TEST_COMMIT, + "correctness_gate": "passed", + "cpu": "test-cpu", + "git_clean": False, + "harness_sha256": harness_sha256, + "os": "TestOS 1 arm64", + "rustc": "rustc 1.88.0", + "source_state_sha256": source_sha256, + } + if include_contract: + environment["benchmark_contract_sha256"] = criterion_dim_plot.benchmark_contract_digest(root) + current_tag = f"v{version}" + baseline_tag = "v0.0.9" + bundle = PerformanceBundle( + context=ArtifactContext( + release=ReleasePair(current=current_tag, baseline=baseline_tag), + statistic="median", + suite="all", + scope="release-signal", + source=ReportSource( + version=version, + commit=_TEST_COMMIT, + ref="HEAD", + revision_timestamp="2026-08-20 12:00:00 UTC", + ), + benchmark_provenance={ + "baseline": baseline_tag, + "criterion": { + "baseline_command": ["cargo", "bench", "--baseline", baseline_tag], + "criterion_version": "0.7.0", + "current_command": ["cargo", "bench", "--save-baseline", "current"], + "sample": "new", + "scope": "release-signal", + "statistic": "median", + "suite": "all", + }, + "measurement": { + "baseline_api_compatibility": "none", + "baseline_commit": baseline_commit, + "baseline_git_clean": False, + "baseline_source_state_sha256": baseline_source_sha256, + "current_commit": _TEST_COMMIT, + "current_git_clean": False, + "current_source_state_sha256": source_sha256, + "status": "recorded", + **{key: environment[key] for key in ("cargo_lock_sha256", "cpu", "harness_sha256", "os", "rustc")}, + **({"benchmark_contract_sha256": environment["benchmark_contract_sha256"]} if "benchmark_contract_sha256" in environment else {}), + }, + "mode": "shared-current-harness", + "publication": environment, + "schema": 2, + "validation": { + "baseline_api_compatibility": "none", + "baseline_commit": baseline_commit, + "baseline_git_clean": False, + "baseline_revision": "passed", + "baseline_source_state_sha256": baseline_source_sha256, + "command": ["just", "test-bench-inputs"], + "current_commit": _TEST_COMMIT, + "current_git_clean": False, + "current_revision": "passed", + "current_source_state_sha256": source_sha256, + "harness": "shared-current", + }, + }, + ), + rows=tuple( + PerformanceRow( + suite="vs_linalg", + scope="release-signal", + benchmark_id=f"d{dim}/la_stack_lu_solve", + group=f"d{dim}", + benchmark="la_stack_lu_solve", + baseline_benchmark="la_stack_lu_solve", + coverage_status="comparable", + coverage_note="", + baseline=_timing(float(dim * 6)), + current=_timing(float(dim * 5)), + baseline_nalgebra=None if dim == omit_peer_dim else _timing(float(dim * 10)), + baseline_faer=_timing(float(dim * 20)), + ) + for dim in criterion_dim_plot.CANONICAL_DIMS + if dim != omit_dim + ), + ) + paths = ArtifactPaths( + csv=root / criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, + provenance=root / "target/bench-reports/performance.provenance.json", + ) + write_bundle(paths, bundle) + return paths + + +def test_main_update_readme_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _write_benchmark_checkout(tmp_path) + performance_paths = _write_performance_bundle(tmp_path) + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") def fake_run_git(args: list[str], **_kwargs: object) -> SimpleNamespace: - if "status" in args: - return SimpleNamespace(stdout=" M src/lib.rs\n") - return SimpleNamespace(stdout="0123456789abcdef\n") + return SimpleNamespace(stdout=f"{_TEST_COMMIT}\n") monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fake_run_safe) monkeypatch.setattr(criterion_dim_plot, "run_git_command", fake_run_git) def fake_render(request: criterion_dim_plot.PlotRequest) -> None: @@ -385,8 +517,8 @@ def fake_render(request: criterion_dim_plot.PlotRequest) -> None: monkeypatch.setattr(criterion_dim_plot, "_render_svg_with_gnuplot", fake_render) - out_csv = tmp_path / "out.csv" - out_svg = tmp_path / "out.svg" + out_csv = tmp_path / "docs" / "assets" / "bench" / "vs_linalg_lu_solve_median.csv" + out_svg = tmp_path / "docs" / "assets" / "bench" / "vs_linalg_lu_solve_median.svg" rc = criterion_dim_plot.main( [ @@ -396,35 +528,11 @@ def fake_render(request: criterion_dim_plot.PlotRequest) -> None: "median", "--sample", "new", - "--criterion-dir", - str(criterion_dir), - "--csv", - str(out_csv), - "--out", - str(out_svg), "--update-readme", - "--readme", - str(readme), ] ) assert rc == 0 assert out_svg.read_text(encoding="utf-8") == "\n" - assert calls[:2] == [ - ("just", ("test-bench-inputs",)), - ( - "cargo", - ( - "bench", - "--locked", - "--features", - "bench", - "--bench", - "vs_linalg", - "--", - "(la_stack_lu_solve|nalgebra_lu_solve|faer_lu_solve)$", - ), - ), - ] # CSV written csv_text = out_csv.read_text(encoding="utf-8") @@ -434,17 +542,31 @@ def fake_render(request: criterion_dim_plot.PlotRequest) -> None: # README updated with computed table readme_text = readme.read_text(encoding="utf-8") - assert "placeholder" not in readme_text + assert "old table" not in readme_text assert "| 2 | 10.000 | 20.000 | 40.000 | +50.0% | +75.0% |" in readme_text assert "| 64 | 320.000 | 640.000 | 1,280.000 | +50.0% | +75.0% |" in readme_text + assert readme_text.count("v0.1.0/docs/assets/bench/") == 3 + assert "blob/v0.0.9/README.md" in readme_text provenance = json.loads(out_csv.with_suffix(".provenance.json").read_text(encoding="utf-8")) assert provenance["measurement"]["status"] == "recorded" - assert provenance["publication"]["correctness_gate"] == "passed" - assert provenance["publication"]["git_clean"] is False - assert provenance["criterion"]["benchmark_command"][:3] == ["cargo", "bench", "--locked"] + assert provenance["measurement"]["source"] == "retained performance-release artifact" + assert provenance["measurement"]["la_stack_sample"] == "current" + assert provenance["measurement"]["peer_release_context"] == "v0.0.9" + assert provenance["measurement"]["peer_sample"] == "baseline phase under shared current harness" + assert provenance["publication"]["correctness_gate"] == "validated-performance-release-artifact" + assert provenance["publication"]["benchmark_contract"] == "matched" + assert "git_clean" not in provenance["publication"] + assert provenance["criterion"]["current_command"][:2] == ["cargo", "bench"] + assert provenance["performance_artifact"]["csv"] == "target/bench-reports/performance.csv" + assert provenance["performance_artifact"]["csv_sha256"] == hashlib.sha256(performance_paths.csv.read_bytes()).hexdigest() assert re.fullmatch(r"[0-9a-f]{64}", provenance["publication"]["source_state_sha256"]) + published = (readme, out_csv, out_svg, out_csv.with_suffix(".provenance.json")) + first = {path: (path.read_bytes(), path.stat().st_mtime_ns) for path in published} + assert criterion_dim_plot.main(["--metric", "lu_solve", "--stat", "median", "--sample", "new", "--update-readme"]) == 0 + assert {path: (path.read_bytes(), path.stat().st_mtime_ns) for path in published} == first + def test_dim_parsing_and_discovery(tmp_path: Path) -> None: assert criterion_dim_plot._dim_from_group_dir("d2") == 2 @@ -986,251 +1108,136 @@ def fail_command(*_args: object, **_kwargs: object) -> SimpleNamespace: assert criterion_dim_plot._rustc_version(tmp_path) == "unavailable" -def test_main_publication_fails_closed_when_provenance_tool_is_unavailable( +def _mock_publication_environment(root: Path, monkeypatch: pytest.MonkeyPatch) -> None: + def fake_run_git(args: list[str], **_kwargs: object) -> SimpleNamespace: + return SimpleNamespace(stdout=f"{_TEST_COMMIT}\n") + + monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: root) + monkeypatch.setattr(criterion_dim_plot, "run_git_command", fake_run_git) + + +def test_main_publication_fails_closed_when_git_commit_is_unavailable( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: - criterion_dir = tmp_path / "target" / "criterion" - criterion_dir.mkdir(parents=True) - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - readme = tmp_path / "README.fixture.md" - readme.write_text(f"{begin}\nold table\n{end}\n", encoding="utf-8") - output = tmp_path / "out.csv" - row = criterion_dim_plot.Row(2, 1.0, 0.9, 1.1, 2.0, 1.9, 2.1, 3.0, 2.9, 3.1) - + _write_benchmark_checkout(tmp_path) + _write_performance_bundle(tmp_path) + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - monkeypatch.setattr(criterion_dim_plot, "_run_publication_benchmarks", lambda _root, _metric: None) - monkeypatch.setattr(criterion_dim_plot, "_detect_versions", lambda _root: {}) - monkeypatch.setattr(criterion_dim_plot, "_discover_dims", lambda _criterion_dir: [2]) - monkeypatch.setattr(criterion_dim_plot, "_collect_rows", lambda *_args: ([row], [])) - monkeypatch.setattr( - criterion_dim_plot, - "_capture_provenance", - lambda *_args, **_kwargs: { - "publication": { - "cargo_lock_sha256": "a" * 64, - "commit": "commit", - "cpu": "cpu", - "git_clean": True, - "missing_harness_files": [], - "os": "os", - "rustc": "unavailable", - "source_missing": False, - } - }, - ) - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(criterion_dir), - "--csv", - str(output), - "--out", - str(tmp_path / "out.svg"), - "--update-readme", - "--readme", - str(readme), - ] - ) + def unavailable_git(*_args: object, **_kwargs: object) -> SimpleNamespace: + message = "git unavailable" + raise OSError(message) - assert rc == 2 - assert "required fields are unavailable: rustc" in capsys.readouterr().err - assert not output.exists() - assert "old table" in readme.read_text(encoding="utf-8") - - -def test_publication_gate_failure_stops_before_timing(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - calls: list[tuple[str, tuple[str, ...]]] = [] + monkeypatch.setattr(criterion_dim_plot, "run_git_command", unavailable_git) - def fail_gate(command: str, args: list[str], **_kwargs: object) -> SimpleNamespace: - calls.append((command, tuple(args))) - raise subprocess.CalledProcessError(1, [command, *args], stderr="fixture failure") + assert criterion_dim_plot.main(["--update-readme"]) == 2 + assert "current checkout commit 'unavailable'" in capsys.readouterr().err + assert "old table" in readme.read_text(encoding="utf-8") + assert not (tmp_path / "docs/assets/bench/vs_linalg_lu_solve_median.csv").exists() - monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fail_gate) - with pytest.raises(RuntimeError, match="just test-bench-inputs"): - criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") +def test_main_publication_rejects_missing_performance_bundle( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + _write_benchmark_checkout(tmp_path) + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") + _mock_publication_environment(tmp_path, monkeypatch) - assert calls == [("just", ("test-bench-inputs",))] + assert criterion_dim_plot.main(["--update-readme"]) == 2 + assert "artifact pair is incomplete" in capsys.readouterr().err + assert "old table" in readme.read_text(encoding="utf-8") @pytest.mark.parametrize( - ("failure_kind", "expected_details", "cause_type"), + "case", [ - ("process", ("timing failed",), subprocess.CalledProcessError), - ("missing", ("Required executable 'cargo' not found in PATH",), criterion_dim_plot.ExecutableNotFoundError), - ("timeout", ("timed out after 17 seconds", "timing stalled"), subprocess.TimeoutExpired), - ("os-error", ("could not start", "working directory unavailable"), OSError), + (3, None, "missing d3/la_stack_lu_solve"), + (None, 5, "missing nalgebra peer timing"), ], ) -def test_failed_timing_restores_staged_new_samples( - monkeypatch: pytest.MonkeyPatch, +def test_main_publication_rejects_incomplete_retained_coverage( tmp_path: Path, - failure_kind: str, - expected_details: tuple[str, ...], - cause_type: type[Exception], -) -> None: - old_estimate = tmp_path / "target" / "criterion" / "d2" / "la_stack_lu" / "new" / "estimates.json" - old_estimate.parent.mkdir(parents=True) - old_estimate.write_text("old\n", encoding="utf-8") - - def fail_timing(command: str, args: list[str], **_kwargs: object) -> SimpleNamespace: - if command == "cargo": - old_estimate.parent.mkdir(parents=True, exist_ok=True) - old_estimate.write_text("partial\n", encoding="utf-8") - if failure_kind == "process": - raise subprocess.CalledProcessError(1, [command, *args], stderr="timing failed") - if failure_kind == "missing": - msg = "Required executable 'cargo' not found in PATH" - raise criterion_dim_plot.ExecutableNotFoundError(msg) - if failure_kind == "os-error": - msg = "working directory unavailable" - raise OSError(msg) - raise subprocess.TimeoutExpired([command, *args], 17, stderr="timing stalled") - return SimpleNamespace(stdout="") - - monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fail_timing) - - with pytest.raises(RuntimeError, match="cargo bench") as exc_info: - criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") - - assert old_estimate.read_text(encoding="utf-8") == "old\n" - assert all(detail in str(exc_info.value) for detail in expected_details) - assert isinstance(exc_info.value.__cause__, cause_type) - - -def test_partial_staging_failure_restores_every_moved_sample( monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, + capsys: pytest.CaptureFixture[str], + case: tuple[int | None, int | None, str], ) -> None: - criterion_dir = tmp_path / "target" / "criterion" - first = criterion_dir / "d2" / "a_bench" / "new" / "estimates.json" - second = criterion_dir / "d2" / "b_bench" / "new" / "estimates.json" - for path, text in ((first, "first\n"), (second, "second\n")): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(text, encoding="utf-8") - - backup_root = tmp_path / "criterion-backup" - backup_root.mkdir() - original_replace = criterion_dim_plot.Path.replace - - def fail_second_move(source: Path, destination: Path) -> Path: - if source == second.parent and backup_root in destination.parents: - msg = "simulated second staging failure" - raise OSError(msg) - return original_replace(source, destination) - - def fake_mkdtemp(*, prefix: str, **kwargs: object) -> str: - assert prefix == "la-stack-stale-criterion-" - assert kwargs == {"dir": criterion_dir.parent} - return str(backup_root) - - monkeypatch.setattr(criterion_dim_plot.tempfile, "mkdtemp", fake_mkdtemp) - monkeypatch.setattr(criterion_dim_plot.Path, "replace", fail_second_move) - monkeypatch.setattr(criterion_dim_plot, "run_safe_command", lambda *_args, **_kwargs: SimpleNamespace(stdout="")) - - with pytest.raises(RuntimeError, match="could not stage existing Criterion samples"): - criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") + omit_dim, omit_peer_dim, expected = case + _write_benchmark_checkout(tmp_path) + _write_performance_bundle(tmp_path, omit_dim=omit_dim, omit_peer_dim=omit_peer_dim) + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") + _mock_publication_environment(tmp_path, monkeypatch) - assert first.read_text(encoding="utf-8") == "first\n" - assert second.read_text(encoding="utf-8") == "second\n" - assert not backup_root.exists() + assert criterion_dim_plot.main(["--update-readme"]) == 2 + assert expected in capsys.readouterr().err + assert "old table" in readme.read_text(encoding="utf-8") -def test_failed_timing_preserves_backup_when_rollback_fails( - monkeypatch: pytest.MonkeyPatch, +def test_main_publication_rejects_stale_harness_without_writing( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], ) -> None: - estimate = tmp_path / "target" / "criterion" / "d2" / "la_stack_lu" / "new" / "estimates.json" - estimate.parent.mkdir(parents=True) - estimate.write_text("old\n", encoding="utf-8") - backup_root = tmp_path / "criterion-backup" - backup_root.mkdir() - - def fail_timing(command: str, args: list[str], **_kwargs: object) -> SimpleNamespace: - if command == "cargo": - estimate.parent.mkdir(parents=True, exist_ok=True) - estimate.write_text("partial\n", encoding="utf-8") - raise subprocess.CalledProcessError(1, [command, *args], stderr="timing failed") - return SimpleNamespace(stdout="") - - original_rmtree = criterion_dim_plot.shutil.rmtree - - def fail_fresh_removal(path: Path) -> None: - if path == estimate.parent: - msg = "simulated rollback removal failure" - raise OSError(msg) - original_rmtree(path) - - monkeypatch.setattr(criterion_dim_plot.tempfile, "mkdtemp", lambda **_kwargs: str(backup_root)) - monkeypatch.setattr(criterion_dim_plot.shutil, "rmtree", fail_fresh_removal) - monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fail_timing) - - with pytest.raises(RuntimeError, match="backups preserved") as exc_info: - criterion_dim_plot._run_publication_benchmarks(tmp_path, "lu_solve") + _write_benchmark_checkout(tmp_path) + _write_performance_bundle(tmp_path) + (tmp_path / "benches/vs_linalg.rs").write_text("// changed after measurement\n", encoding="utf-8") + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") + _mock_publication_environment(tmp_path, monkeypatch) - assert str(backup_root) in str(exc_info.value) - preserved = backup_root / "d2" / "la_stack_lu" / "new" / "estimates.json" - assert preserved.read_text(encoding="utf-8") == "old\n" - assert estimate.read_text(encoding="utf-8") == "partial\n" + assert criterion_dim_plot.main(["--update-readme"]) == 2 + assert "benchmark_contract_sha256" in capsys.readouterr().err + assert "old table" in readme.read_text(encoding="utf-8") -def test_readme_publication_cannot_reuse_stale_new_samples( +@pytest.mark.parametrize( + ("include_contract", "expected_status"), + [(True, "matched"), (False, "legacy-retained-artifact")], +) +def test_main_publication_ignores_justfile_only_changes( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + include_contract: bool, + expected_status: str, ) -> None: - criterion_dir = tmp_path / "target" / "criterion" - metric = criterion_dim_plot.METRICS["lu_solve"] - - def write_dimension(dimension: int) -> None: - for bench in (metric.la_bench, metric.na_bench, metric.fa_bench): - estimates = criterion_dir / f"d{dimension}" / bench / "new" / "estimates.json" - estimates.parent.mkdir(parents=True, exist_ok=True) - estimates.write_text( - json.dumps( - { - "median": { - "point_estimate": 1.0, - "confidence_interval": {"lower_bound": 0.9, "upper_bound": 1.1}, - } - } - ), - encoding="utf-8", - ) + _write_benchmark_checkout(tmp_path) + _write_performance_bundle(tmp_path, include_contract=include_contract) + (tmp_path / "justfile").write_text("test-bench-inputs:\n\n# publication help changed\n", encoding="utf-8") + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") + _mock_publication_environment(tmp_path, monkeypatch) + monkeypatch.setattr( + criterion_dim_plot, + "_render_svg_with_gnuplot", + lambda request: request.out_svg.write_text("\n", encoding="utf-8"), + ) - for dimension in criterion_dim_plot.CANONICAL_DIMS: - write_dimension(dimension) - begin, end = criterion_dim_plot._readme_table_markers("lu_solve", "median", "new") - readme = tmp_path / "README.stale-fixture.md" - readme.write_text(f"{begin}\nstale\n{end}\n", encoding="utf-8") - output = tmp_path / "benchmark.csv" + assert criterion_dim_plot.main(["--update-readme"]) == 0 + provenance = json.loads((tmp_path / "docs/assets/bench/vs_linalg_lu_solve_median.provenance.json").read_text(encoding="utf-8")) + assert provenance["publication"]["benchmark_contract"] == expected_status - def fake_run_safe(command: str, _args: list[str], **_kwargs: object) -> SimpleNamespace: - if command == "cargo": - write_dimension(2) - return SimpleNamespace(stdout="") - monkeypatch.setattr(criterion_dim_plot, "_repo_root", lambda: tmp_path) - monkeypatch.setattr(criterion_dim_plot, "run_safe_command", fake_run_safe) - - rc = criterion_dim_plot.main( - [ - "--criterion-dir", - str(criterion_dir), - "--csv", - str(output), - "--update-readme", - "--readme", - str(readme), - ] - ) +def test_main_publication_rejects_tampered_performance_csv( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + _write_benchmark_checkout(tmp_path) + paths = _write_performance_bundle(tmp_path) + paths.csv.write_text(paths.csv.read_text(encoding="utf-8") + "tampered\n", encoding="utf-8") + readme = tmp_path / "README.md" + readme.write_text(_canonical_benchmark_readme("0.0.8"), encoding="utf-8") + _mock_publication_environment(tmp_path, monkeypatch) - assert rc == 2 - assert not output.exists() - assert "stale" in readme.read_text(encoding="utf-8") - assert not (criterion_dir / "d3" / metric.la_bench / "new" / "estimates.json").exists() + assert criterion_dim_plot.main(["--update-readme"]) == 2 + assert "CSV digest mismatch" in capsys.readouterr().err + assert "old table" in readme.read_text(encoding="utf-8") def test_staged_publication_leaves_existing_assets_unchanged_when_render_fails( @@ -1259,6 +1266,7 @@ def fail_render(_request: criterion_dim_plot.PlotRequest) -> None: stat="median", sample="new", criterion_dir="target/criterion", + performance_csv=criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, out=str(svg_path), csv=str(csv_path), log_y=False, @@ -1351,6 +1359,7 @@ def test_publication_paths_reject_output_aliases(tmp_path: Path, capsys: pytest. stat="median", sample="new", criterion_dir="target/criterion", + performance_csv=criterion_dim_plot._DEFAULT_PERFORMANCE_CSV, out=str(output), csv=str(output), log_y=False, diff --git a/scripts/tests/test_update_release_version.py b/scripts/tests/test_update_release_version.py new file mode 100644 index 0000000..57dd42f --- /dev/null +++ b/scripts/tests/test_update_release_version.py @@ -0,0 +1,226 @@ +"""Tests for transactional release-version updates.""" + +from typing import TYPE_CHECKING + +import pytest + +import check_docs_version_sync +import update_release_version + +if TYPE_CHECKING: + from pathlib import Path + + +def _write_project(root: Path, *, metadata_version: str = "1.2.2", dependency_version: str = "1.2.2") -> None: + files = { + "Cargo.toml": f'[package]\nname = "other-crate"\nversion = "{metadata_version}"\n', + "Cargo.lock": ( + f'version = 4\n\n[[package]]\nname = "either"\nversion = "1.17.0"\n\n[[package]]\nname = "other-crate"\nversion = "{metadata_version}"\n' + ), + "pyproject.toml": f'[project]\nname = "other-crate-scripts"\nversion = "{metadata_version}"\n', + "uv.lock": (f'version = 1\n\n[[package]]\nname = "other-crate-scripts"\nversion = "{metadata_version}"\nsource = {{ editable = "." }}\n'), + "CITATION.cff": (f'cff-version: 1.2.0\nversion: {metadata_version}\ndate-released: 2026-07-13\ndoi: "10.5281/zenodo.12345"\n'), + "README.md": ( + f'other-crate = "{dependency_version}"\n' + f'other-crate = {{ version = "{dependency_version}", features = ["exact"] }}\n' + f"[doc](https://github.com/acgetchell/la-stack/blob/v{metadata_version}/README.md)\n" + f"[raw](https://raw.githubusercontent.com/acgetchell/la-stack/v{metadata_version}/README.md)\n" + f"[csv](https://github.com/acgetchell/la-stack/blob/v{metadata_version}/docs/assets/bench/result.csv)\n" + f"[provenance](https://github.com/acgetchell/la-stack/blob/v{metadata_version}/docs/assets/bench/result.provenance.json)\n" + f"[svg](https://raw.githubusercontent.com/acgetchell/la-stack/v{metadata_version}/docs/assets/bench/result.svg)\n" + "\n" + "| unchanged benchmark comparison |\n" + "\n" + ), + "CHANGELOG.md": "# Changelog\n\n## [1.2.2] - 2026-07-13\n", + } + for filename, content in files.items(): + (root / filename).write_text(content, encoding="utf-8") + docs = root / "docs" + docs.mkdir() + (docs / "BENCHMARKING.md").write_text( + "just performance-release v1.2.2 v1.2.1\nThis generates a local `v1.2.1` `vs_linalg` baseline.\nHistorical v1.2.1 behavior remains documented.\n", + encoding="utf-8", + ) + + +def _previous() -> update_release_version.ReleaseTag: + return update_release_version.parse_release_tag("v1.2.2") + + +def test_update_release_version_updates_all_current_surfaces_without_dependency_upgrades(tmp_path: Path) -> None: + _write_project(tmp_path) + + summary = update_release_version.update_release_version( + tmp_path, + "v1.2.3", + previous=_previous(), + release_date="2026-08-20", + ) + + assert summary.target.tag == "v1.2.3" + assert summary.previous.tag == "v1.2.2" + assert summary.release_date == "2026-08-20" + assert summary.changed_paths + assert 'name = "either"\nversion = "1.17.0"' in (tmp_path / "Cargo.lock").read_text(encoding="utf-8") + assert 'name = "other-crate"\nversion = "1.2.3"' in (tmp_path / "Cargo.lock").read_text(encoding="utf-8") + assert 'name = "other-crate-scripts"\nversion = "1.2.3"' in (tmp_path / "uv.lock").read_text(encoding="utf-8") + assert 'version = "1.2.3"' in (tmp_path / "Cargo.toml").read_text(encoding="utf-8") + assert 'version = "1.2.3"' in (tmp_path / "pyproject.toml").read_text(encoding="utf-8") + citation = (tmp_path / "CITATION.cff").read_text(encoding="utf-8") + assert "version: 1.2.3" in citation + assert "date-released: 2026-08-20" in citation + assert 'doi: "10.5281/zenodo.12345"' in citation + readme = (tmp_path / "README.md").read_text(encoding="utf-8") + assert 'other-crate = "1.2.3"' in readme + assert 'version = "1.2.3"' in readme + assert readme.count("v1.2.3") == 2 + assert readme.count("v1.2.2/docs/assets/bench/") == 3 + assert "| unchanged benchmark comparison |" in readme + benchmarking = (tmp_path / "docs" / "BENCHMARKING.md").read_text(encoding="utf-8") + assert "just performance-release v1.2.3 v1.2.2" in benchmarking + assert "This generates a local `v1.2.2` `vs_linalg` baseline." in benchmarking + assert "Historical v1.2.1 behavior remains documented." in benchmarking + assert "## [1.2.2] - 2026-07-13" in (tmp_path / "CHANGELOG.md").read_text(encoding="utf-8") + assert check_docs_version_sync.find_version_mismatches(tmp_path) == [] + + +def test_update_release_version_is_idempotent(tmp_path: Path) -> None: + _write_project(tmp_path) + kwargs = {"previous": _previous(), "release_date": "2026-08-20"} + + first = update_release_version.update_release_version(tmp_path, "v1.2.3", **kwargs) + second = update_release_version.update_release_version(tmp_path, "v1.2.3", **kwargs) + + assert first.changed_paths + assert second.changed_paths == () + + +def test_update_release_version_advances_existing_release_dates_together(tmp_path: Path) -> None: + _write_project(tmp_path, metadata_version="1.2.3", dependency_version="1.2.3") + citation = tmp_path / "CITATION.cff" + citation.write_text(citation.read_text(encoding="utf-8").replace("2026-07-13", "2026-08-20"), encoding="utf-8") + changelog = tmp_path / "CHANGELOG.md" + changelog.write_text("# Changelog\n\n## [1.2.3] - 2026-08-20\n", encoding="utf-8") + benchmarking = tmp_path / "docs" / "BENCHMARKING.md" + benchmarking.write_text( + "just performance-release v1.2.3 v1.2.2\nThis generates a local `v1.2.2` `vs_linalg` baseline.\n", + encoding="utf-8", + ) + + summary = update_release_version.update_release_version( + tmp_path, + "v1.2.3", + previous=_previous(), + release_date="2026-08-21", + ) + + assert summary.changed_paths == (changelog, citation) + assert "date-released: 2026-08-21" in citation.read_text(encoding="utf-8") + assert "## [1.2.3] - 2026-08-21" in changelog.read_text(encoding="utf-8") + + +def test_select_previous_release_tag_uses_latest_stable_published_tag() -> None: + target = update_release_version.parse_release_tag("v1.3.0") + + previous = update_release_version.select_previous_release_tag( + ["v1.1.9", "v1.2.0-rc.1", "not-a-release", "v1.2.0"], + target, + ) + + assert previous.tag == "v1.2.0" + + +def test_select_previous_release_tag_ignores_already_published_target() -> None: + target = update_release_version.parse_release_tag("v1.3.0") + + previous = update_release_version.select_previous_release_tag(["v1.2.0", "v1.3.0"], target) + + assert previous.tag == "v1.2.0" + + +@pytest.mark.parametrize("target", ["1.2.3", "v1.2", "v01.2.3", "v1.2.3-rc.1"]) +def test_parse_release_tag_rejects_non_stable_tag_forms(target: str) -> None: + with pytest.raises(ValueError, match=r"stable tag in vX\.Y\.Z form"): + update_release_version.parse_release_tag(target) + + +def test_select_previous_release_tag_rejects_target_older_than_a_published_release() -> None: + target = update_release_version.parse_release_tag("v1.2.3") + + with pytest.raises(ValueError, match="older than published"): + update_release_version.select_previous_release_tag(["v1.2.3", "v1.3.0"], target) + + +def test_infer_previous_release_tag_uses_published_github_releases(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(update_release_version, "published_stable_release_tags", lambda root: ["v1.2.1", "v1.2.2"] if root == tmp_path else []) + + previous = update_release_version.infer_previous_release_tag(tmp_path, update_release_version.parse_release_tag("v1.2.3")) + + assert previous.tag == "v1.2.2" + + +def test_unexpected_version_fails_before_writing(tmp_path: Path) -> None: + _write_project(tmp_path, dependency_version="1.0.0") + originals = {path: path.read_text(encoding="utf-8") for path in tmp_path.rglob("*") if path.is_file()} + + with pytest.raises(ValueError, match="unexpected other-crate dependency version"): + update_release_version.update_release_version( + tmp_path, + "v1.2.3", + previous=_previous(), + release_date="2026-08-20", + ) + + assert {path: path.read_text(encoding="utf-8") for path in originals} == originals + + +def test_validation_failure_rolls_back_every_changed_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _write_project(tmp_path) + originals = {path: path.read_text(encoding="utf-8") for path in tmp_path.rglob("*") if path.is_file()} + + def fail_validation(*_args: object) -> None: + msg = "simulated validation failure" + raise ValueError(msg) + + monkeypatch.setattr(update_release_version, "_validate_updated_root", fail_validation) + + with pytest.raises(ValueError, match="simulated validation failure"): + update_release_version.update_release_version( + tmp_path, + "v1.2.3", + previous=_previous(), + release_date="2026-08-20", + ) + + assert {path: path.read_text(encoding="utf-8") for path in originals} == originals + + +def test_sync_changelog_release_date_uses_citation_date(tmp_path: Path) -> None: + _write_project(tmp_path) + update_release_version.update_release_version( + tmp_path, + "v1.2.3", + previous=_previous(), + release_date="2026-08-20", + ) + changelog = tmp_path / "CHANGELOG.md" + changelog.write_text("# Changelog\n\n## [1.2.3] - 2026-08-21\n", encoding="utf-8") + + changed, release_date = update_release_version.sync_changelog_release_date( + tmp_path, + "v1.2.3", + previous=_previous(), + ) + + assert changed == (changelog,) + assert release_date == "2026-08-20" + assert "## [1.2.3] - 2026-08-20" in changelog.read_text(encoding="utf-8") + assert check_docs_version_sync.find_version_mismatches(tmp_path) == [] + + +def test_main_supports_help(capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit, match="0"): + update_release_version.main(["--help"]) + + assert "Target stable release tag" in capsys.readouterr().out diff --git a/scripts/update_release_version.py b/scripts/update_release_version.py new file mode 100644 index 0000000..ddb7f02 --- /dev/null +++ b/scripts/update_release_version.py @@ -0,0 +1,521 @@ +"""Update deterministic release-version references from one target Git tag.""" + +import argparse +import os +import re +import subprocess +import sys +import tempfile +import tomllib +from dataclasses import dataclass, field +from datetime import UTC, date, datetime +from pathlib import Path +from typing import TYPE_CHECKING + +import check_docs_version_sync as version_sync +from archive_performance import published_stable_release_tags +from subprocess_utils import ExecutableNotFoundError + +if TYPE_CHECKING: + from collections.abc import Callable + +_STABLE_TAG_RE = re.compile(r"^v(?P0|[1-9][0-9]*)\.(?P0|[1-9][0-9]*)\.(?P0|[1-9][0-9]*)$") +_TOML_VERSION_RE = re.compile(r'^(?P\s*version\s*=\s*")(?P[^"]+)(?P"\s*(?:#.*)?)$') +_CITATION_VERSION_RE = re.compile( + r"^(?Pversion:\s*(?P['\"]?))" + r"(?P[0-9A-Za-z][0-9A-Za-z.+-]*)" + r"(?P(?P=quote)\s*(?:#.*)?)$" +) +_CITATION_DATE_RE = re.compile( + r"^(?Pdate-released:\s*(?P['\"]?))" + r"(?P\d{4}-\d{2}-\d{2})" + r"(?P(?P=quote)\s*(?:#.*)?)$" +) +_BENCHMARK_TAG_PAIR_RE = re.compile( + r"(?Pjust performance-(?:github-assets|local-non-exact|release)[ \t]+)" + r"v(?P[0-9]+\.[0-9]+\.[0-9]+)" + r"(?P[ \t]+)" + r"v(?P[0-9]+\.[0-9]+\.[0-9]+)" + r"(?=[ \t]|`|$)", + re.MULTILINE, +) +_BENCHMARK_BASELINE_PROSE_RE = re.compile( + r"(?PThis generates a local `v)" + r"(?P[0-9]+\.[0-9]+\.[0-9]+)" + r"(?P` `vs_linalg` baseline)" +) + + +@dataclass(frozen=True, order=True, slots=True) +class ReleaseTag: + """A stable release tag with SemVer ordering.""" + + major: int + minor: int + patch: int + tag: str = field(compare=False) + + @property + def version(self) -> str: + """Return the package version without the leading ``v``.""" + return self.tag.removeprefix("v") + + +@dataclass(frozen=True, slots=True) +class UpdateSummary: + """Files and release identities produced by an update.""" + + target: ReleaseTag + previous: ReleaseTag + changed_paths: tuple[Path, ...] + release_date: str + + +@dataclass(frozen=True, slots=True) +class LineReplacement: + """One fail-closed scalar replacement on a known source line.""" + + line_number: int + pattern: re.Pattern[str] + group: str + replacement: str + allowed: frozenset[str] + context: str + + +def parse_release_tag(value: str, *, label: str = "release tag") -> ReleaseTag: + """Parse one stable ``vX.Y.Z`` release tag.""" + match = _STABLE_TAG_RE.fullmatch(value) + if match is None: + msg = f"{label} must be a stable tag in vX.Y.Z form, got {value!r}" + raise ValueError(msg) + return ReleaseTag( + major=int(match.group("major")), + minor=int(match.group("minor")), + patch=int(match.group("patch")), + tag=value, + ) + + +def select_previous_release_tag(tag_names: list[str], target: ReleaseTag) -> ReleaseTag: + """Select the newest published stable release before *target*.""" + stable_tags = [parse_release_tag(tag) for tag in tag_names if _STABLE_TAG_RE.fullmatch(tag) is not None] + if not stable_tags: + msg = "repository has no published stable vX.Y.Z GitHub releases" + raise ValueError(msg) + newer = [tag for tag in stable_tags if tag > target] + if newer: + latest = max(newer) + msg = f"target {target.tag} is older than published stable GitHub release {latest.tag}" + raise ValueError(msg) + previous = [tag for tag in stable_tags if tag < target] + if not previous: + msg = f"could not find a published stable GitHub release before {target.tag}" + raise ValueError(msg) + return max(previous) + + +def infer_previous_release_tag(root: Path, target: ReleaseTag) -> ReleaseTag: + """Infer the previous release from published stable GitHub releases.""" + return select_previous_release_tag(published_stable_release_tags(root), target) + + +def _current_utc_date() -> str: + """Return today's UTC calendar date for release preparation.""" + return datetime.now(UTC).date().isoformat() + + +def _validated_date(value: str) -> str: + """Require one real ISO calendar date and return it unchanged.""" + try: + parsed = date.fromisoformat(value) + except ValueError as error: + msg = f"release date must use YYYY-MM-DD, got {value!r}" + raise ValueError(msg) from error + if parsed.isoformat() != value: + msg = f"release date must use canonical YYYY-MM-DD form, got {value!r}" + raise ValueError(msg) + return value + + +def _replace_line_group(text: str, edit: LineReplacement) -> str: + lines = text.splitlines(keepends=True) + if not 1 <= edit.line_number <= len(lines): + msg = f"{edit.context} has no line {edit.line_number}" + raise ValueError(msg) + original_line = lines[edit.line_number - 1] + body = original_line.rstrip("\r\n") + ending = original_line[len(body) :] + match = edit.pattern.fullmatch(body) + if match is None: + msg = f"{edit.context}:{edit.line_number} has an unsupported version assignment: {body!r}" + raise ValueError(msg) + current = match.group(edit.group) + if current not in edit.allowed: + msg = f"{edit.context}:{edit.line_number} has unexpected version {current!r}; expected one of {sorted(edit.allowed)}" + raise ValueError(msg) + start, end = match.span(edit.group) + lines[edit.line_number - 1] = f"{body[:start]}{edit.replacement}{body[end:]}{ending}" + return "".join(lines) + + +def _replace_match_groups(match: re.Match[str], replacements: dict[str, str]) -> str: + updated = match.group(0) + spans = sorted(((match.start(group) - match.start(), match.end(group) - match.start(), value) for group, value in replacements.items()), reverse=True) + for start, end, value in spans: + updated = f"{updated[:start]}{value}{updated[end:]}" + return updated + + +def _replace_dependency_versions(text: str, package_name: str, target: ReleaseTag, previous: ReleaseTag, path: Path) -> str: + allowed = frozenset({target.version, previous.version}) + pattern = version_sync.dependency_regex(package_name) + + def replace(match: re.Match[str]) -> str: + group = "plain" if match.group("plain") is not None else "table" + current = match.group(group) + if current not in allowed: + msg = f"{path} has unexpected {package_name} dependency version {current!r}; expected one of {sorted(allowed)}" + raise ValueError(msg) + return _replace_match_groups(match, {group: target.version}) + + return pattern.sub(replace, text) + + +def _replace_readme_links(text: str, target: ReleaseTag, previous: ReleaseTag, path: Path) -> str: + allowed = frozenset({target.version, previous.version}) + + def replace(match: re.Match[str]) -> str: + if version_sync.readme_tag_link_is_benchmark_asset(match): + return match.group(0) + version = match.group("version") + if version is not None and version not in allowed: + msg = f"{path} has unexpected release-pinned link version {version!r}; expected one of {sorted(allowed)}" + raise ValueError(msg) + group = "version" if version is not None else "revision" + replacement = target.version if version is not None else target.tag + return _replace_match_groups(match, {group: replacement}) + + return version_sync.README_TAG_LINK_RE.sub(replace, text) + + +def _replace_benchmark_tag_pairs(text: str, target: ReleaseTag, previous: ReleaseTag, path: Path) -> str: + allowed_current = frozenset({target.version, previous.version}) + + def replace(match: re.Match[str]) -> str: + current = match.group("current") + if current not in allowed_current: + msg = f"{path} has unexpected benchmark current tag v{current}; expected {target.tag} or {previous.tag}" + raise ValueError(msg) + return _replace_match_groups(match, {"current": target.version, "baseline": previous.version}) + + return _BENCHMARK_TAG_PAIR_RE.sub(replace, text) + + +def _replace_benchmark_baseline_prose(text: str, previous: ReleaseTag) -> str: + """Keep the active specific-release explanation aligned with its command.""" + return _BENCHMARK_BASELINE_PROSE_RE.sub( + lambda match: _replace_match_groups(match, {"baseline": previous.version}), + text, + ) + + +def _read_text(path: Path) -> str: + return path.read_text(encoding="utf-8") + + +def _metadata_updates(root: Path, target: ReleaseTag, previous: ReleaseTag, release_date: str) -> dict[Path, str]: + allowed = frozenset({target.version, previous.version}) + cargo_toml = root / "Cargo.toml" + cargo_lock = root / "Cargo.lock" + pyproject = root / "pyproject.toml" + uv_lock = root / "uv.lock" + citation = root / "CITATION.cff" + + package = version_sync.read_cargo_package_info(cargo_toml) + project = version_sync.read_python_project_info(pyproject) + cargo_toml_line = version_sync.toml_table_key_line(cargo_toml, "package", "version") + cargo_lock_reference = version_sync.cargo_lock_reference(cargo_lock, package) + pyproject_reference = version_sync.pyproject_reference(pyproject, project) + uv_lock_reference = version_sync.uv_lock_reference(uv_lock, project) + citation_reference = version_sync.citation_reference(citation) + + updates = { + cargo_toml: _replace_line_group( + _read_text(cargo_toml), + LineReplacement( + line_number=cargo_toml_line, + pattern=_TOML_VERSION_RE, + group="version", + replacement=target.version, + allowed=allowed, + context=str(cargo_toml), + ), + ), + cargo_lock: _replace_line_group( + _read_text(cargo_lock), + LineReplacement( + line_number=cargo_lock_reference.line, + pattern=_TOML_VERSION_RE, + group="version", + replacement=target.version, + allowed=allowed, + context=str(cargo_lock), + ), + ), + pyproject: _replace_line_group( + _read_text(pyproject), + LineReplacement( + line_number=pyproject_reference.line, + pattern=_TOML_VERSION_RE, + group="version", + replacement=target.version, + allowed=allowed, + context=str(pyproject), + ), + ), + uv_lock: _replace_line_group( + _read_text(uv_lock), + LineReplacement( + line_number=uv_lock_reference.line, + pattern=_TOML_VERSION_RE, + group="version", + replacement=target.version, + allowed=allowed, + context=str(uv_lock), + ), + ), + citation: _replace_line_group( + _read_text(citation), + LineReplacement( + line_number=citation_reference.line, + pattern=_CITATION_VERSION_RE, + group="version", + replacement=target.version, + allowed=allowed, + context=str(citation), + ), + ), + } + citation_line, current_date = version_sync.citation_release_date(citation) + updates[citation] = _replace_line_group( + updates[citation], + LineReplacement( + line_number=citation_line, + pattern=_CITATION_DATE_RE, + group="date", + replacement=release_date, + allowed=frozenset({current_date, release_date}), + context=str(citation), + ), + ) + return updates + + +def _prepare_updates(root: Path, target: ReleaseTag, previous: ReleaseTag, release_date: str) -> dict[Path, str]: + updates = _metadata_updates(root, target, previous, release_date) + changelog = root / "CHANGELOG.md" + changelog_match = version_sync.changelog_release_date(changelog, target.version) + if changelog_match is not None: + changelog_line, changelog_date = changelog_match + updates[changelog] = _replace_changelog_release_date( + changelog, + target, + line=changelog_line, + current_date=changelog_date, + release_date=release_date, + ) + package = version_sync.read_cargo_package_info(root / "Cargo.toml") + for path in version_sync.iter_active_markdown_files(root): + original = _read_text(path) + updated = _replace_dependency_versions(original, package.name, target, previous, path) + updated = _replace_benchmark_tag_pairs(updated, target, previous, path) + updated = _replace_benchmark_baseline_prose(updated, previous) + if path == root / "README.md": + updated = _replace_readme_links(updated, target, previous, path) + updates[path] = updated + return updates + + +def _replace_changelog_release_date( + changelog: Path, + target: ReleaseTag, + *, + line: int, + current_date: str, + release_date: str, +) -> str: + """Return a changelog with one target release heading date synchronized.""" + heading_re = re.compile( + rf"^(?P## \[v?{re.escape(target.version)}\] - )" + r"(?P\d{4}-\d{2}-\d{2})$" + ) + return _replace_line_group( + _read_text(changelog), + LineReplacement( + line_number=line, + pattern=heading_re, + group="date", + replacement=release_date, + allowed=frozenset({current_date, release_date}), + context=str(changelog), + ), + ) + + +def _write_text_atomic(path: Path, text: str) -> None: + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent, text=True) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as stream: + stream.write(text) + temporary.chmod(path.stat().st_mode) + temporary.replace(path) + except BaseException: + temporary.unlink(missing_ok=True) + raise + + +def _validate_updated_root(root: Path, target: ReleaseTag, previous: ReleaseTag) -> None: + mismatches = version_sync.find_version_mismatches(root) + if mismatches: + details = "; ".join( + f"{mismatch.reference.path.relative_to(root)}:{mismatch.reference.line} has {mismatch.reference.version}" for mismatch in mismatches + ) + msg = f"release-version validation failed after updating to {target.tag}: {details}" + raise ValueError(msg) + for path in version_sync.iter_active_markdown_files(root): + for match in _BENCHMARK_TAG_PAIR_RE.finditer(_read_text(path)): + if match.group("current") != target.version or match.group("baseline") != previous.version: + msg = f"{path} contains a benchmark tag pair that does not match {target.tag} against {previous.tag}" + raise ValueError(msg) + for match in _BENCHMARK_BASELINE_PROSE_RE.finditer(_read_text(path)): + if match.group("baseline") != previous.version: + msg = f"{path} contains active baseline prose that does not match {previous.tag}" + raise ValueError(msg) + + +def _publish_transaction(updates: dict[Path, str], validate: Callable[[], None]) -> tuple[Path, ...]: + originals = {path: _read_text(path) for path in updates} + changed = tuple(sorted((path for path, text in updates.items() if text != originals[path]), key=str)) + replaced: list[Path] = [] + try: + for path in changed: + _write_text_atomic(path, updates[path]) + replaced.append(path) + validate() + except BaseException as primary: + rollback_errors: list[str] = [] + for path in reversed(replaced): + try: + _write_text_atomic(path, originals[path]) + except OSError as error: + rollback_errors.append(f"{path}: {error}") + if rollback_errors: + msg = f"release-version update failed ({primary}); rollback also failed: {'; '.join(rollback_errors)}" + raise RuntimeError(msg) from primary + raise + return changed + + +def update_release_version( + root: Path, + tag: str, + *, + previous: ReleaseTag | None = None, + release_date: str | None = None, +) -> UpdateSummary: + """Update release references transactionally and return a summary.""" + resolved_root = root.resolve() + target = parse_release_tag(tag, label="target tag") + previous_release = previous or infer_previous_release_tag(resolved_root, target) + if previous_release >= target: + msg = f"previous release {previous_release.tag} must be older than target {target.tag}" + raise ValueError(msg) + prepared_date = _validated_date(release_date or _current_utc_date()) + updates = _prepare_updates(resolved_root, target, previous_release, prepared_date) + changed = _publish_transaction(updates, lambda: _validate_updated_root(resolved_root, target, previous_release)) + return UpdateSummary(target=target, previous=previous_release, changed_paths=changed, release_date=prepared_date) + + +def sync_changelog_release_date( + root: Path, + tag: str, + *, + previous: ReleaseTag | None = None, +) -> tuple[tuple[Path, ...], str]: + """Synchronize a generated changelog heading from ``CITATION.cff``.""" + resolved_root = root.resolve() + target = parse_release_tag(tag, label="target tag") + previous_release = previous or infer_previous_release_tag(resolved_root, target) + package = version_sync.read_cargo_package_info(resolved_root / "Cargo.toml") + if package.version != target.version: + msg = f"Cargo.toml version {package.version} does not match target {target.tag}" + raise ValueError(msg) + citation = resolved_root / "CITATION.cff" + _, citation_date = version_sync.citation_release_date(citation) + changelog = resolved_root / "CHANGELOG.md" + changelog_match = version_sync.changelog_release_date(changelog, target.version) + if changelog_match is None: + msg = f"{changelog} has no generated release heading for {target.tag}" + raise ValueError(msg) + changelog_line, changelog_date = changelog_match + updated = _replace_changelog_release_date( + changelog, + target, + line=changelog_line, + current_date=changelog_date, + release_date=citation_date, + ) + changed = _publish_transaction( + {changelog: updated}, + lambda: _validate_updated_root( + resolved_root, + target, + previous_release, + ), + ) + return changed, citation_date + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("tag", help="Target stable release tag in vX.Y.Z form") + parser.add_argument("--root", type=Path, default=Path.cwd(), help="Repository root to update (default: current directory)") + parser.add_argument( + "--sync-changelog-date", + action="store_true", + help="Synchronize the generated changelog heading from CITATION.cff instead of updating release metadata", + ) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Update deterministic release metadata with fail-closed diagnostics.""" + args = parse_args(argv) + try: + if args.sync_changelog_date: + changed_paths, release_date = sync_changelog_release_date(args.root, args.tag) + if changed_paths: + print(f"Synchronized CHANGELOG.md release date to {release_date}.") + else: + print(f"CHANGELOG.md release date already matches {release_date}.") + return 0 + summary = update_release_version(args.root, args.tag) + except (ExecutableNotFoundError, OSError, RuntimeError, subprocess.SubprocessError, TypeError, ValueError, tomllib.TOMLDecodeError) as error: + print(f"failed to update release version: {error}", file=sys.stderr) + return 1 + + if summary.changed_paths: + for path in summary.changed_paths: + print(f"Updated {path.relative_to(args.root.resolve())}") + else: + print(f"Release-version references already match {summary.target.tag}.") + print(f"Previous release: {summary.previous.tag}") + print(f"CITATION.cff release date: {summary.release_date} (UTC update date)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/uv.lock b/uv.lock index 6b22d5b..86ad300 100644 --- a/uv.lock +++ b/uv.lock @@ -453,7 +453,7 @@ wheels = [ [[package]] name = "la-stack-scripts" -version = "0.4.4" +version = "0.4.5" source = { editable = "." } [package.dev-dependencies]