simplebench_runtime/
measurement.rs

1use crate::{calculate_percentiles, config::BenchmarkConfig, 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
38/// Warmup using a closure (generic version for new measurement functions)
39fn warmup_closure<F>(func: &mut F, duration: Duration, iterations: usize) -> (u128, u64)
40where
41    F: FnMut(),
42{
43    let start = Instant::now();
44    let mut total_iterations = 0u64;
45    let mut batch_size = 1u64;
46
47    while start.elapsed() < duration {
48        for _ in 0..batch_size {
49            for _ in 0..iterations {
50                func();
51            }
52        }
53        total_iterations += batch_size * (iterations as u64);
54        batch_size *= 2;
55    }
56
57    (start.elapsed().as_millis(), total_iterations)
58}
59
60/// Measure a closure, collecting timing samples with CPU monitoring
61fn measure_closure<F>(
62    func: &mut F,
63    iterations: usize,
64    samples: usize,
65) -> (Vec<Duration>, Vec<CpuSnapshot>)
66where
67    F: FnMut(),
68{
69    let mut all_timings = Vec::with_capacity(samples);
70    let mut cpu_samples = Vec::with_capacity(samples);
71
72    // Initialize CPU monitor for the pinned core
73    let cpu_core = get_pinned_core();
74    let monitor = CpuMonitor::new(cpu_core);
75
76    for _ in 0..samples {
77        // Read CPU frequency BEFORE measurement (while CPU is active)
78        let freq_before = monitor.read_frequency();
79
80        let start = Instant::now();
81        for _ in 0..iterations {
82            func();
83        }
84        let elapsed = start.elapsed();
85        all_timings.push(elapsed);
86
87        // Read frequency after as well, use the higher of the two
88        let freq_after = monitor.read_frequency();
89        let frequency_khz = match (freq_before, freq_after) {
90            (Some(before), Some(after)) => Some(before.max(after)),
91            (Some(f), None) | (None, Some(f)) => Some(f),
92            (None, None) => None,
93        };
94
95        let snapshot = CpuSnapshot {
96            timestamp: Instant::now(),
97            frequency_khz,
98            temperature_millic: monitor.read_temperature(),
99        };
100        cpu_samples.push(snapshot);
101    }
102
103    (all_timings, cpu_samples)
104}
105
106/// Measure a simple benchmark (no setup) using the new architecture.
107///
108/// This function is called by the generated benchmark wrapper for benchmarks
109/// without setup code. The config is passed in, and a complete BenchResult is returned.
110pub fn measure_simple<F>(
111    config: &BenchmarkConfig,
112    name: &str,
113    module: &str,
114    mut func: F,
115) -> BenchResult
116where
117    F: FnMut(),
118{
119    // Warmup
120    let (warmup_ms, warmup_iters) = warmup_closure(
121        &mut func,
122        Duration::from_secs(config.measurement.warmup_duration_secs),
123        config.measurement.iterations,
124    );
125
126    // Measurement
127    let (all_timings, cpu_samples) = measure_closure(
128        &mut func,
129        config.measurement.iterations,
130        config.measurement.samples,
131    );
132
133    let percentiles = calculate_percentiles(&all_timings);
134
135    BenchResult {
136        name: name.to_string(),
137        module: module.to_string(),
138        iterations: config.measurement.iterations,
139        samples: config.measurement.samples,
140        percentiles,
141        all_timings,
142        cpu_samples,
143        warmup_ms: Some(warmup_ms),
144        warmup_iterations: Some(warmup_iters),
145    }
146}
147
148/// Measure a benchmark with setup code that runs once before measurement.
149///
150/// This function is called by the generated benchmark wrapper for benchmarks
151/// with the `setup` attribute. Setup runs exactly once, then the benchmark
152/// function receives a reference to the setup data for each iteration.
153pub fn measure_with_setup<T, S, B>(
154    config: &BenchmarkConfig,
155    name: &str,
156    module: &str,
157    setup: S,
158    mut bench: B,
159) -> BenchResult
160where
161    S: FnOnce() -> T,
162    B: FnMut(&T),
163{
164    // Run setup ONCE before any measurement
165    let data = setup();
166
167    // Create closure that borrows the setup data
168    let mut func = || bench(&data);
169
170    // Warmup
171    let (warmup_ms, warmup_iters) = warmup_closure(
172        &mut func,
173        Duration::from_secs(config.measurement.warmup_duration_secs),
174        config.measurement.iterations,
175    );
176
177    // Measurement
178    let (all_timings, cpu_samples) = measure_closure(
179        &mut func,
180        config.measurement.iterations,
181        config.measurement.samples,
182    );
183
184    let percentiles = calculate_percentiles(&all_timings);
185
186    BenchResult {
187        name: name.to_string(),
188        module: module.to_string(),
189        iterations: config.measurement.iterations,
190        samples: config.measurement.samples,
191        percentiles,
192        all_timings,
193        cpu_samples,
194        warmup_ms: Some(warmup_ms),
195        warmup_iterations: Some(warmup_iters),
196    }
197}
198
199pub fn measure_with_warmup<F>(
200    name: String,
201    module: String,
202    func: F,
203    iterations: usize,
204    samples: usize,
205    warmup_duration_secs: u64,
206) -> BenchResult
207where
208    F: Fn(),
209{
210    // Perform time-based warmup and store stats
211    let (warmup_ms, warmup_iters) =
212        warmup_benchmark(&func, Duration::from_secs(warmup_duration_secs), iterations);
213
214    let mut result = measure_function_impl(name, module, func, iterations, samples);
215
216    // Store warmup stats in result for later printing
217    result.warmup_ms = Some(warmup_ms);
218    result.warmup_iterations = Some(warmup_iters);
219
220    result
221}
222
223pub fn measure_function_impl<F>(
224    name: String,
225    module: String,
226    func: F,
227    iterations: usize,
228    samples: usize,
229) -> BenchResult
230where
231    F: Fn(),
232{
233    let mut all_timings = Vec::with_capacity(samples);
234    let mut cpu_samples = Vec::with_capacity(samples);
235
236    // Initialize CPU monitor for the pinned core
237    let cpu_core = get_pinned_core();
238    let monitor = CpuMonitor::new(cpu_core);
239
240    for _ in 0..samples {
241        // Read CPU frequency BEFORE measurement (while CPU is active)
242        let freq_before = monitor.read_frequency();
243
244        let start = Instant::now();
245        for _ in 0..iterations {
246            func();
247        }
248        let elapsed = start.elapsed();
249        all_timings.push(elapsed);
250
251        // Read frequency after as well, use the higher of the two
252        let freq_after = monitor.read_frequency();
253        let frequency_khz = match (freq_before, freq_after) {
254            (Some(before), Some(after)) => Some(before.max(after)),
255            (Some(f), None) | (None, Some(f)) => Some(f),
256            (None, None) => None,
257        };
258
259        let snapshot = CpuSnapshot {
260            timestamp: Instant::now(),
261            frequency_khz,
262            temperature_millic: monitor.read_temperature(),
263        };
264        cpu_samples.push(snapshot);
265    }
266
267    let percentiles = calculate_percentiles(&all_timings);
268
269    BenchResult {
270        name,
271        module,
272        iterations,
273        samples,
274        percentiles,
275        all_timings,
276        cpu_samples,
277        warmup_ms: None,
278        warmup_iterations: None,
279    }
280}
281
282pub fn measure_single_iteration<F>(func: F) -> Duration
283where
284    F: FnOnce(),
285{
286    let start = Instant::now();
287    func();
288    start.elapsed()
289}
290
291pub fn validate_measurement_params(iterations: usize, samples: usize) -> Result<(), String> {
292    if iterations == 0 {
293        return Err("Iterations must be greater than 0".to_string());
294    }
295    if samples == 0 {
296        return Err("Samples must be greater than 0".to_string());
297    }
298    if samples > 1_000_000 {
299        return Err(
300            "Samples should not exceed 1,000,000 for reasonable execution time".to_string(),
301        );
302    }
303    Ok(())
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use std::thread;
310
311    #[test]
312    fn test_measure_single_iteration() {
313        let duration = measure_single_iteration(|| {
314            thread::sleep(Duration::from_millis(1));
315        });
316
317        assert!(duration >= Duration::from_millis(1));
318        assert!(duration < Duration::from_millis(10)); // Should be close to 1ms
319    }
320
321    #[test]
322    fn test_validate_measurement_params() {
323        assert!(validate_measurement_params(100, 100).is_ok());
324        assert!(validate_measurement_params(0, 100).is_err());
325        assert!(validate_measurement_params(100, 0).is_err());
326        assert!(validate_measurement_params(100, 1_000_001).is_err());
327        assert!(validate_measurement_params(5, 100_000).is_ok());
328    }
329
330    #[test]
331    fn test_measure_function_basic() {
332        let result = measure_function_impl(
333            "test_bench".to_string(),
334            "test_module".to_string(),
335            || {
336                // Simple work
337                let _ = (0..100).sum::<i32>();
338            },
339            100,
340            10,
341        );
342
343        assert_eq!(result.name, "test_bench");
344        assert_eq!(result.module, "test_module");
345        assert_eq!(result.iterations, 100);
346        assert_eq!(result.samples, 10);
347        assert_eq!(result.all_timings.len(), 10);
348
349        // All measurements should be reasonable (not zero, not extremely large)
350        for timing in &result.all_timings {
351            assert!(*timing > Duration::from_nanos(0));
352            assert!(*timing < Duration::from_secs(1));
353        }
354    }
355}