ssb 0.1.1

Simple benchmarking for Rust, with hierarchical call tree, based on fastrace.
Documentation
use serde::{Deserialize, Serialize};

/// Aggregated timing statistics for a single named span across all iterations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanStats {
    pub name: String,
    /// Number of iterations that contained this span.
    pub count: usize,
    pub min_ns: u64,
    pub max_ns: u64,
    pub mean_ns: f64,
    pub median_ns: f64,
    pub p95_ns: f64,
    pub p99_ns: f64,
    pub stddev_ns: f64,
    /// Name of the parent span, if any. Used to reconstruct the call tree.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent: Option<String>,
}

impl SpanStats {
    pub(crate) fn compute(name: String, mut samples: Vec<u64>, parent: Option<String>) -> Self {
        assert!(!samples.is_empty(), "cannot compute stats for zero samples");
        samples.sort_unstable();

        let n = samples.len();
        let min_ns = samples[0];
        let max_ns = samples[n - 1];
        // Use u128 accumulator to avoid overflow for large iteration counts.
        let sum: u128 = samples.iter().map(|&x| x as u128).sum();
        let mean_ns = sum as f64 / n as f64;
        let median_ns = lerp_percentile(&samples, 0.50);
        let p95_ns = lerp_percentile(&samples, 0.95);
        let p99_ns = lerp_percentile(&samples, 0.99);
        let variance = samples
            .iter()
            .map(|&x| {
                let d = x as f64 - mean_ns;
                d * d
            })
            .sum::<f64>()
            / n as f64;

        SpanStats {
            name,
            count: n,
            min_ns,
            max_ns,
            mean_ns,
            median_ns,
            p95_ns,
            p99_ns,
            stddev_ns: variance.sqrt(),
            parent,
        }
    }
}

/// Linear-interpolated percentile on a pre-sorted slice.
fn lerp_percentile(sorted: &[u64], p: f64) -> f64 {
    let n = sorted.len();
    if n == 1 {
        return sorted[0] as f64;
    }
    let pos = p * (n - 1) as f64;
    let lo = pos.floor() as usize;
    let hi = (lo + 1).min(n - 1);
    let frac = pos - lo as f64;
    sorted[lo] as f64 * (1.0 - frac) + sorted[hi] as f64 * frac
}

/// Format a nanosecond duration for human display.
pub fn fmt_ns(ns: f64) -> String {
    if ns < 1_000.0 {
        format!("{:.1} ns", ns)
    } else if ns < 1_000_000.0 {
        format!("{:.2} µs", ns / 1_000.0)
    } else if ns < 1_000_000_000.0 {
        format!("{:.2} ms", ns / 1_000_000.0)
    } else {
        format!("{:.3} s", ns / 1_000_000_000.0)
    }
}