Skip to main content

criterion_hypothesis_harness/
lib.rs

1//! Custom harness runtime for criterion-hypothesis
2//!
3//! This replaces criterion's default harness with an HTTP-controlled one.
4//! The harness exposes benchmark functions via HTTP endpoints, allowing
5//! external orchestration of benchmark execution.
6
7mod server;
8
9pub use server::{run_harness, run_harness_async};
10
11use std::collections::HashMap;
12use std::time::Duration;
13
14/// A benchmark function that runs `n` inner iterations and returns total elapsed.
15///
16/// The closure is expected to perform its work `n` times inside a tight loop
17/// and return the total elapsed duration. The orchestrator divides by `n` to
18/// obtain the per-iteration mean, which is the statistical unit the t-test
19/// operates on.
20///
21/// Using a per-iteration loop amortises clock-read overhead (`Instant::now` is
22/// ~20–50 ns) and gives meaningful variance estimates for fast functions.
23pub type BenchmarkFn = Box<dyn Fn(u64) -> Duration + Send + Sync>;
24
25/// Registry of discovered benchmarks.
26///
27/// This stores all benchmark functions that have been registered with the harness.
28/// Each benchmark is identified by a unique string name.
29pub struct BenchmarkRegistry {
30    benchmarks: HashMap<String, BenchmarkFn>,
31}
32
33impl BenchmarkRegistry {
34    /// Create a new empty benchmark registry.
35    pub fn new() -> Self {
36        Self {
37            benchmarks: HashMap::new(),
38        }
39    }
40
41    /// Register a benchmark function with the given name.
42    ///
43    /// The closure receives an iteration count `n` and should execute the work
44    /// `n` times before returning total elapsed.
45    ///
46    /// # Example
47    ///
48    /// ```ignore
49    /// let mut registry = BenchmarkRegistry::new();
50    /// registry.register("my_benchmark", |n| {
51    ///     let start = std::time::Instant::now();
52    ///     for _ in 0..n {
53    ///         std::hint::black_box(do_work());
54    ///     }
55    ///     start.elapsed()
56    /// });
57    /// ```
58    pub fn register<F>(&mut self, name: impl Into<String>, f: F)
59    where
60        F: Fn(u64) -> Duration + Send + Sync + 'static,
61    {
62        self.benchmarks.insert(name.into(), Box::new(f));
63    }
64
65    /// List all registered benchmark names.
66    pub fn list(&self) -> Vec<String> {
67        self.benchmarks.keys().cloned().collect()
68    }
69
70    /// Run a benchmark by name for `iterations` inner iterations.
71    ///
72    /// Returns `None` if no benchmark with the given name exists.
73    pub fn run(&self, name: &str, iterations: u64) -> Option<Duration> {
74        self.benchmarks.get(name).map(|f| f(iterations))
75    }
76
77    /// Check if a benchmark with the given name exists.
78    pub fn contains(&self, name: &str) -> bool {
79        self.benchmarks.contains_key(name)
80    }
81
82    /// Get the number of registered benchmarks.
83    pub fn len(&self) -> usize {
84        self.benchmarks.len()
85    }
86
87    /// Check if the registry is empty.
88    pub fn is_empty(&self) -> bool {
89        self.benchmarks.is_empty()
90    }
91}
92
93impl Default for BenchmarkRegistry {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_registry_new() {
105        let registry = BenchmarkRegistry::new();
106        assert!(registry.is_empty());
107        assert_eq!(registry.len(), 0);
108    }
109
110    #[test]
111    fn test_registry_register_and_list() {
112        let mut registry = BenchmarkRegistry::new();
113        registry.register("bench1", |_n| Duration::from_millis(10));
114        registry.register("bench2", |_n| Duration::from_millis(20));
115
116        assert_eq!(registry.len(), 2);
117        assert!(registry.contains("bench1"));
118        assert!(registry.contains("bench2"));
119        assert!(!registry.contains("bench3"));
120
121        let names = registry.list();
122        assert_eq!(names.len(), 2);
123        assert!(names.contains(&"bench1".to_string()));
124        assert!(names.contains(&"bench2".to_string()));
125    }
126
127    #[test]
128    fn test_registry_run_passes_iterations() {
129        use std::sync::atomic::{AtomicU64, Ordering};
130        use std::sync::Arc;
131
132        let observed = Arc::new(AtomicU64::new(0));
133        let observed_clone = Arc::clone(&observed);
134
135        let mut registry = BenchmarkRegistry::new();
136        registry.register("iter_echo", move |n| {
137            observed_clone.store(n, Ordering::SeqCst);
138            Duration::from_nanos(n * 100)
139        });
140
141        let result = registry.run("iter_echo", 42);
142        assert_eq!(result, Some(Duration::from_nanos(4200)));
143        assert_eq!(observed.load(Ordering::SeqCst), 42);
144    }
145
146    #[test]
147    fn test_registry_run_missing() {
148        let mut registry = BenchmarkRegistry::new();
149        registry.register("exists", |_n| Duration::from_millis(5));
150
151        assert!(registry.run("missing", 1).is_none());
152    }
153
154    #[test]
155    fn test_registry_default() {
156        let registry = BenchmarkRegistry::default();
157        assert!(registry.is_empty());
158    }
159}