simplebench_runtime/
measurement.rs

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