simplebench_runtime/
measurement.rs

1use crate::{calculate_percentiles, BenchResult, CpuMonitor, CpuSnapshot};
2use std::time::{Duration, Instant};
3
4/// Warmup benchmark using time-based exponential doubling (Criterion-style)
5/// Returns (elapsed_ms, total_iterations) for reporting
6fn warmup_benchmark<F>(bench_fn: &F, warmup_duration: Duration, iterations: usize) -> (u128, u64)
7where
8    F: Fn(),
9{
10    let start = Instant::now();
11    let mut total_iterations = 0u64;
12    let mut batch_size = 1u64;
13
14    while start.elapsed() < warmup_duration {
15        // Run benchmark function batch_size times
16        for _ in 0..batch_size {
17            for _ in 0..iterations {
18                bench_fn();
19            }
20        }
21
22        total_iterations += batch_size * (iterations as u64);
23        batch_size *= 2; // Exponential doubling
24    }
25
26    (start.elapsed().as_millis(), total_iterations)
27}
28
29/// Get the CPU core this thread is pinned to (if any)
30fn get_pinned_core() -> usize {
31    // Check env var set by orchestrator
32    std::env::var("SIMPLEBENCH_PIN_CORE")
33        .ok()
34        .and_then(|s| s.parse().ok())
35        .unwrap_or(0)
36}
37
38pub fn measure_with_warmup<F>(
39    name: String,
40    module: String,
41    func: F,
42    iterations: usize,
43    samples: usize,
44    warmup_duration_secs: u64,
45) -> BenchResult
46where
47    F: Fn(),
48{
49    // Perform time-based warmup and store stats
50    let (warmup_ms, warmup_iters) =
51        warmup_benchmark(&func, Duration::from_secs(warmup_duration_secs), iterations);
52
53    let mut result = measure_function_impl(name, module, func, iterations, samples);
54
55    // Store warmup stats in result for later printing
56    result.warmup_ms = Some(warmup_ms);
57    result.warmup_iterations = Some(warmup_iters);
58
59    result
60}
61
62pub fn measure_function_impl<F>(
63    name: String,
64    module: String,
65    func: F,
66    iterations: usize,
67    samples: usize,
68) -> BenchResult
69where
70    F: Fn(),
71{
72    let mut all_timings = Vec::with_capacity(samples);
73    let mut cpu_samples = Vec::with_capacity(samples);
74
75    // Initialize CPU monitor for the pinned core
76    let cpu_core = get_pinned_core();
77    let monitor = CpuMonitor::new(cpu_core);
78
79    for _ in 0..samples {
80        // Read CPU frequency BEFORE measurement (while CPU is active)
81        let freq_before = monitor.read_frequency();
82
83        let start = Instant::now();
84        for _ in 0..iterations {
85            func();
86        }
87        let elapsed = start.elapsed();
88        all_timings.push(elapsed);
89
90        // Read frequency after as well, use the higher of the two
91        let freq_after = monitor.read_frequency();
92        let frequency_khz = match (freq_before, freq_after) {
93            (Some(before), Some(after)) => Some(before.max(after)),
94            (Some(f), None) | (None, Some(f)) => Some(f),
95            (None, None) => None,
96        };
97
98        let snapshot = CpuSnapshot {
99            timestamp: Instant::now(),
100            frequency_khz,
101            temperature_millic: monitor.read_temperature(),
102        };
103        cpu_samples.push(snapshot);
104    }
105
106    let percentiles = calculate_percentiles(&all_timings);
107
108    BenchResult {
109        name,
110        module,
111        iterations,
112        samples,
113        percentiles,
114        all_timings,
115        cpu_samples,
116        warmup_ms: None,
117        warmup_iterations: None,
118    }
119}
120
121pub fn measure_single_iteration<F>(func: F) -> Duration
122where
123    F: FnOnce(),
124{
125    let start = Instant::now();
126    func();
127    start.elapsed()
128}
129
130pub fn validate_measurement_params(iterations: usize, samples: usize) -> Result<(), String> {
131    if iterations == 0 {
132        return Err("Iterations must be greater than 0".to_string());
133    }
134    if samples == 0 {
135        return Err("Samples must be greater than 0".to_string());
136    }
137    if samples > 1_000_000 {
138        return Err(
139            "Samples should not exceed 1,000,000 for reasonable execution time".to_string(),
140        );
141    }
142    Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use std::thread;
149
150    #[test]
151    fn test_measure_single_iteration() {
152        let duration = measure_single_iteration(|| {
153            thread::sleep(Duration::from_millis(1));
154        });
155
156        assert!(duration >= Duration::from_millis(1));
157        assert!(duration < Duration::from_millis(10)); // Should be close to 1ms
158    }
159
160    #[test]
161    fn test_validate_measurement_params() {
162        assert!(validate_measurement_params(100, 100).is_ok());
163        assert!(validate_measurement_params(0, 100).is_err());
164        assert!(validate_measurement_params(100, 0).is_err());
165        assert!(validate_measurement_params(100, 1_000_001).is_err());
166        assert!(validate_measurement_params(5, 100_000).is_ok());
167    }
168
169    #[test]
170    fn test_measure_function_basic() {
171        let result = measure_function_impl(
172            "test_bench".to_string(),
173            "test_module".to_string(),
174            || {
175                // Simple work
176                let _ = (0..100).sum::<i32>();
177            },
178            100,
179            10,
180        );
181
182        assert_eq!(result.name, "test_bench");
183        assert_eq!(result.module, "test_module");
184        assert_eq!(result.iterations, 100);
185        assert_eq!(result.samples, 10);
186        assert_eq!(result.all_timings.len(), 10);
187
188        // All measurements should be reasonable (not zero, not extremely large)
189        for timing in &result.all_timings {
190            assert!(*timing > Duration::from_nanos(0));
191            assert!(*timing < Duration::from_secs(1));
192        }
193    }
194}