subms 0.9.4

The sub-millisecond perf harness for Rust. Zero-dependency std-only library that records timed samples per stage, computes percentiles, supports coordinated-omission correction, runs scale sweeps, and emits a stable JSON contract. Byte-equivalent to the Java sibling com.submillisecond:subms.
Documentation
//! Structured bench summary - the typed counterpart to the standard subms JSON.
//!
//! Build one with [`crate::summarize`] (or [`crate::summarize_lean`] for the
//! sample-free variant). Hand the summary to [`crate::print_summary`],
//! [`crate::assert_p99_under`], or [`crate::summary_to_json`].
//!
//! Field shape is byte-equivalent to Java's `SubMsBenchSummary` / `SubMsStageSummary`.

use std::collections::BTreeMap;

/// Per-stage summary. The counterpart of one `stages.<name>` entry in the
/// subms JSON contract.
///
/// `samples_ns` carries the downsampled chronological timeline emitted by
/// [`crate::summarize`], capped at the harness's `sample_cap` (default 500;
/// raise it to keep more points); [`crate::summarize_lean`] leaves it `None`.
///
/// Does NOT derive `Eq` because `jitter_score: f64` can be NaN. Use
/// `PartialEq` instead, and prefer comparing the integer fields explicitly
/// in tests rather than `assert_eq!`-ing the whole struct.
#[derive(Clone, Debug, PartialEq)]
pub struct SubMsStageSummary {
    pub name: String,
    pub count: usize,
    pub p50_ns: u64,
    pub p99_ns: u64,
    pub p999_ns: u64,
    pub max_ns: u64,
    pub mean_ns: u64,
    /// Sample-standard-deviation across the (post-warmup-skip) timings.
    /// `0` when count < 2. Added in subms 0.4.0; the JSON contract emits
    /// it as `stddev_ns` and older readers ignore the field.
    pub stddev_ns: u64,
    /// Log2-spaced histogram of the chronological samples. 64 buckets
    /// covering 1ns .. ~18.4s (2^0 .. 2^64 ns). Each entry is the count
    /// of samples whose value falls in `[2^i, 2^(i+1))` nanoseconds.
    /// Empty (all-zero) when the stage has no samples. The full CDF can
    /// be reconstructed by cumulative sum across the array.
    ///
    /// Added in subms 0.5.0; the JSON contract emits it as
    /// `cdf_buckets_ns` and older readers ignore the field.
    pub cdf_buckets_ns: Vec<u64>,
    /// Jitter score in `[0.0, 1.0]`: coefficient of variation of the
    /// per-window mean across non-overlapping 32-sample windows,
    /// clamped. 0 = perfectly stable measurement environment, 1+ = the
    /// underlying noise floor dominates the signal. Useful for spotting
    /// bench runs where the *measurement rig* was unstable, separate
    /// from the algorithm's own tail.
    ///
    /// Added in subms 0.5.0.
    pub jitter_score: f64,
    pub samples_ns: Option<Vec<u64>>,
}

/// Typed summary of one bench run. Mirrors the on-disk JSON downstream tooling persists per workload.
///
/// Stage order is registration order, matching the harness.
#[derive(Clone, Debug)]
pub struct SubMsBenchSummary {
    pub workload: String,
    pub lang: String,
    pub timestamp: String,
    /// CPU core the bench last executed on (Linux `/proc/self/stat` field 39),
    /// or `None` off Linux. Makes each run self-describing about core placement -
    /// pair with `cpu_affinity` to tell an isolated/pinned core from a migratable
    /// one. Added in subms 0.5.3; the JSON contract emits it under `cpu`.
    pub cpu_core: Option<u32>,
    /// The process's allowed-CPU affinity list (Linux `Cpus_allowed_list`), e.g.
    /// `"1"` (single core -> pinned/isolated) or `"0-1"` (migratable). `None`
    /// off Linux.
    pub cpu_affinity: Option<String>,
    pub inputs: BTreeMap<String, String>,
    pub meta: BTreeMap<String, String>,
    pub stages: Vec<SubMsStageSummary>,
}

impl SubMsBenchSummary {
    /// Look up a stage by name.
    pub fn stage(&self, name: &str) -> Option<&SubMsStageSummary> {
        self.stages.iter().find(|s| s.name == name)
    }
}

/// A sweep is a collection of [`SubMsBenchSummary`] runs that share a
/// workload but vary one or more input parameters - typically `entries` for
/// scale-curve studies, or `bloom_mode` for a feature-toggle comparison.
///
/// `varied_input_key` names the input that differs across runs; `None` means
/// runs are labelled by ordinal.
///
/// Java counterpart: `com.submillisecond.perf.SubMsBenchSweep`.
#[derive(Clone, Debug)]
pub struct SubMsBenchSweep {
    pub workload: String,
    pub lang: String,
    pub varied_input_key: Option<String>,
    pub runs: Vec<SubMsBenchSummary>,
}

/// One row in a per-stage diff: a baseline value, a candidate value, and the
/// deltas between them.
///
/// `delta_pct` is signed: positive means the candidate is slower (regression
/// for latency metrics). Java counterpart: `SubMsMetricDiff`.
#[derive(Clone, Debug, PartialEq)]
pub struct SubMsMetricDiff {
    pub metric: String,
    pub baseline_ns: u64,
    pub candidate_ns: u64,
    pub delta_ns: i64,
    /// `100 * delta_ns / baseline_ns`. `f64::INFINITY` when baseline = 0.
    pub delta_pct: f64,
}

/// Per-stage diff: every percentile + mean compared between two runs of the
/// same stage. `worst_regression_pct` is the most-positive `delta_pct` across
/// all metrics - the headline number a CI gate keys off.
///
/// Java counterpart: `SubMsStageDiff`.
#[derive(Clone, Debug)]
pub struct SubMsStageDiff {
    pub stage: String,
    pub metrics: Vec<SubMsMetricDiff>,
    pub worst_regression_pct: f64,
}

/// Typed diff between a baseline and a candidate [`SubMsBenchSummary`].
/// Java counterpart: `SubMsBenchDiff`.
#[derive(Clone, Debug)]
pub struct SubMsBenchDiff {
    pub baseline_workload: String,
    pub candidate_workload: String,
    pub lang: String,
    pub stages: Vec<SubMsStageDiff>,
    pub baseline_only_stages: Vec<String>,
    pub candidate_only_stages: Vec<String>,
    pub regression_threshold_pct: f64,
}

impl SubMsBenchDiff {
    /// `true` if any stage's `worst_regression_pct` exceeded
    /// `regression_threshold_pct`.
    pub fn has_regression(&self) -> bool {
        self.stages
            .iter()
            .any(|s| s.worst_regression_pct > self.regression_threshold_pct)
    }

    /// The worst-regressing stage, or `None` if all stages stayed within the threshold.
    pub fn worst_stage(&self) -> Option<&SubMsStageDiff> {
        self.stages.iter().max_by(|a, b| {
            a.worst_regression_pct
                .partial_cmp(&b.worst_regression_pct)
                .unwrap_or(std::cmp::Ordering::Equal)
        })
    }
}