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/// Get the CPU core this thread is pinned to (if any)
6fn get_pinned_core() -> usize {
7    // Check env var set by orchestrator
8    std::env::var("SIMPLEBENCH_PIN_CORE")
9        .ok()
10        .and_then(|s| s.parse().ok())
11        .unwrap_or(0)
12}
13
14/// Warmup using a closure (generic version for new measurement functions)
15fn warmup_closure<F>(func: &mut F, duration: Duration, bench_name: &str) -> (u128, u64)
16where
17    F: FnMut(),
18{
19    let start = Instant::now();
20    let mut total_iterations = 0u64;
21    let mut last_report = Instant::now();
22    let target_ms = duration.as_millis() as u64;
23
24    while start.elapsed() < duration {
25        func();
26        total_iterations += 1;
27
28        // Emit progress every 100ms
29        if last_report.elapsed() >= Duration::from_millis(100) {
30            emit_progress(&ProgressMessage {
31                bench: bench_name,
32                phase: ProgressPhase::Warmup {
33                    elapsed_ms: start.elapsed().as_millis() as u64,
34                    target_ms,
35                },
36            });
37            last_report = Instant::now();
38        }
39    }
40
41    (start.elapsed().as_millis(), total_iterations)
42}
43
44/// Measure a closure, collecting timing samples with CPU monitoring
45fn measure_closure<F>(
46    func: &mut F,
47    samples: usize,
48    bench_name: &str,
49) -> (Vec<Duration>, Vec<CpuSnapshot>)
50where
51    F: FnMut(),
52{
53    let mut all_timings = Vec::with_capacity(samples);
54    let mut cpu_samples = Vec::with_capacity(samples);
55
56    // Initialize CPU monitor for the pinned core
57    let cpu_core = get_pinned_core();
58    let monitor = CpuMonitor::new(cpu_core);
59
60    // Report progress every ~1% of samples (minimum every sample for small counts)
61    let report_interval = (samples / 100).max(1);
62
63    for sample_idx in 0..samples {
64        // Emit progress BEFORE timing (so we don't affect measurements)
65        if sample_idx % report_interval == 0 {
66            emit_progress(&ProgressMessage {
67                bench: bench_name,
68                phase: ProgressPhase::Samples {
69                    current: sample_idx as u32,
70                    total: samples as u32,
71                },
72            });
73        }
74
75        // Read CPU frequency BEFORE measurement (while CPU is active)
76        let freq_before = monitor.read_frequency();
77
78        let start = Instant::now();
79        func();
80        let elapsed = start.elapsed();
81        all_timings.push(elapsed);
82
83        // Read frequency after as well, use the higher of the two
84        let freq_after = monitor.read_frequency();
85        let frequency_khz = match (freq_before, freq_after) {
86            (Some(before), Some(after)) => Some(before.max(after)),
87            (Some(f), None) | (None, Some(f)) => Some(f),
88            (None, None) => None,
89        };
90
91        let snapshot = CpuSnapshot {
92            timestamp: Instant::now(),
93            frequency_khz,
94            temperature_millic: monitor.read_temperature(),
95        };
96        cpu_samples.push(snapshot);
97    }
98
99    // Emit completion message
100    emit_progress(&ProgressMessage {
101        bench: bench_name,
102        phase: ProgressPhase::Complete,
103    });
104
105    (all_timings, cpu_samples)
106}
107
108/// Measure a simple benchmark (no setup) using the new architecture.
109///
110/// This function is called by the generated benchmark wrapper for benchmarks
111/// without setup code. The config is passed in, and a complete BenchResult is returned.
112pub fn measure_simple<F>(
113    config: &BenchmarkConfig,
114    name: &str,
115    module: &str,
116    mut func: F,
117) -> BenchResult
118where
119    F: FnMut(),
120{
121    // Warmup
122    let (warmup_ms, warmup_iters) = warmup_closure(
123        &mut func,
124        Duration::from_secs(config.measurement.warmup_duration_secs),
125        name,
126    );
127
128    // Measurement
129    let (all_timings, cpu_samples) = measure_closure(&mut func, config.measurement.samples, name);
130
131    let percentiles = calculate_percentiles(&all_timings);
132
133    BenchResult {
134        name: name.to_string(),
135        module: module.to_string(),
136        samples: config.measurement.samples,
137        percentiles,
138        all_timings,
139        cpu_samples,
140        warmup_ms: Some(warmup_ms),
141        warmup_iterations: Some(warmup_iters),
142    }
143}
144
145/// Measure a benchmark with setup code that runs once before measurement.
146///
147/// This function is called by the generated benchmark wrapper for benchmarks
148/// with the `setup` attribute. Setup runs exactly once, then the benchmark
149/// function receives a reference to the setup data for each iteration.
150pub fn measure_with_setup<T, S, B>(
151    config: &BenchmarkConfig,
152    name: &str,
153    module: &str,
154    setup: S,
155    mut bench: B,
156) -> BenchResult
157where
158    S: FnOnce() -> T,
159    B: FnMut(&T),
160{
161    // Run setup ONCE before any measurement
162    let data = setup();
163
164    // Create closure that borrows the setup data
165    let mut func = || bench(&data);
166
167    // Warmup
168    let (warmup_ms, warmup_iters) = warmup_closure(
169        &mut func,
170        Duration::from_secs(config.measurement.warmup_duration_secs),
171        name,
172    );
173
174    // Measurement
175    let (all_timings, cpu_samples) = measure_closure(&mut func, config.measurement.samples, name);
176
177    let percentiles = calculate_percentiles(&all_timings);
178
179    BenchResult {
180        name: name.to_string(),
181        module: module.to_string(),
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/// Warmup with setup running before each call (for setup_each benchmarks)
192fn warmup_with_setup<T, S, B>(
193    setup: &mut S,
194    bench: &mut B,
195    duration: Duration,
196    bench_name: &str,
197) -> (u128, u64)
198where
199    S: FnMut() -> T,
200    B: FnMut(T),
201{
202    let start = Instant::now();
203    let mut total_iterations = 0u64;
204    let mut last_report = Instant::now();
205    let target_ms = duration.as_millis() as u64;
206
207    while start.elapsed() < duration {
208        let data = setup();
209        bench(data);
210        total_iterations += 1;
211
212        // Emit progress every 100ms
213        if last_report.elapsed() >= Duration::from_millis(100) {
214            emit_progress(&ProgressMessage {
215                bench: bench_name,
216                phase: ProgressPhase::Warmup {
217                    elapsed_ms: start.elapsed().as_millis() as u64,
218                    target_ms,
219                },
220            });
221            last_report = Instant::now();
222        }
223    }
224
225    (start.elapsed().as_millis(), total_iterations)
226}
227
228/// Warmup with setup running before each call, borrowing version
229fn warmup_with_setup_ref<T, S, B>(
230    setup: &mut S,
231    bench: &mut B,
232    duration: Duration,
233    bench_name: &str,
234) -> (u128, u64)
235where
236    S: FnMut() -> T,
237    B: FnMut(&T),
238{
239    let start = Instant::now();
240    let mut total_iterations = 0u64;
241    let mut last_report = Instant::now();
242    let target_ms = duration.as_millis() as u64;
243
244    while start.elapsed() < duration {
245        let data = setup();
246        bench(&data);
247        total_iterations += 1;
248
249        // Emit progress every 100ms
250        if last_report.elapsed() >= Duration::from_millis(100) {
251            emit_progress(&ProgressMessage {
252                bench: bench_name,
253                phase: ProgressPhase::Warmup {
254                    elapsed_ms: start.elapsed().as_millis() as u64,
255                    target_ms,
256                },
257            });
258            last_report = Instant::now();
259        }
260    }
261
262    (start.elapsed().as_millis(), total_iterations)
263}
264
265/// Measure a benchmark where setup runs before every sample (owning version).
266///
267/// The benchmark function takes ownership of the data produced by setup.
268/// This allows benchmarking operations that consume or mutate their input.
269pub fn measure_with_setup_each<T, S, B>(
270    config: &BenchmarkConfig,
271    name: &str,
272    module: &str,
273    mut setup: S,
274    mut bench: B,
275) -> BenchResult
276where
277    S: FnMut() -> T,
278    B: FnMut(T),
279{
280    // Warmup: run setup + bench together
281    let (warmup_ms, warmup_iters) = warmup_with_setup(
282        &mut setup,
283        &mut bench,
284        Duration::from_secs(config.measurement.warmup_duration_secs),
285        name,
286    );
287
288    // Measurement
289    let samples = config.measurement.samples;
290    let mut all_timings = Vec::with_capacity(samples);
291    let mut cpu_samples = Vec::with_capacity(samples);
292
293    // Initialize CPU monitor for the pinned core
294    let cpu_core = get_pinned_core();
295    let monitor = CpuMonitor::new(cpu_core);
296
297    // Report progress every ~1% of samples
298    let report_interval = (samples / 100).max(1);
299
300    for sample_idx in 0..samples {
301        // Emit progress BEFORE timing
302        if sample_idx % report_interval == 0 {
303            emit_progress(&ProgressMessage {
304                bench: name,
305                phase: ProgressPhase::Samples {
306                    current: sample_idx as u32,
307                    total: samples as u32,
308                },
309            });
310        }
311
312        // Setup runs before each sample
313        let data = setup();
314
315        // Read CPU frequency BEFORE measurement
316        let freq_before = monitor.read_frequency();
317
318        let start = Instant::now();
319        bench(data); // Consumes data
320        let elapsed = start.elapsed();
321        all_timings.push(elapsed);
322
323        // Read frequency after as well
324        let freq_after = monitor.read_frequency();
325        let frequency_khz = match (freq_before, freq_after) {
326            (Some(before), Some(after)) => Some(before.max(after)),
327            (Some(f), None) | (None, Some(f)) => Some(f),
328            (None, None) => None,
329        };
330
331        let snapshot = CpuSnapshot {
332            timestamp: Instant::now(),
333            frequency_khz,
334            temperature_millic: monitor.read_temperature(),
335        };
336        cpu_samples.push(snapshot);
337    }
338
339    // Emit completion message
340    emit_progress(&ProgressMessage {
341        bench: name,
342        phase: ProgressPhase::Complete,
343    });
344
345    let percentiles = calculate_percentiles(&all_timings);
346
347    BenchResult {
348        name: name.to_string(),
349        module: module.to_string(),
350        samples,
351        percentiles,
352        all_timings,
353        cpu_samples,
354        warmup_ms: Some(warmup_ms),
355        warmup_iterations: Some(warmup_iters),
356    }
357}
358
359/// Measure a benchmark where setup runs before every sample (borrowing version).
360///
361/// The benchmark function borrows the data produced by setup.
362/// Use this when you need fresh data each sample but don't consume it.
363pub fn measure_with_setup_each_ref<T, S, B>(
364    config: &BenchmarkConfig,
365    name: &str,
366    module: &str,
367    mut setup: S,
368    mut bench: B,
369) -> BenchResult
370where
371    S: FnMut() -> T,
372    B: FnMut(&T),
373{
374    // Warmup: run setup + bench together
375    let (warmup_ms, warmup_iters) = warmup_with_setup_ref(
376        &mut setup,
377        &mut bench,
378        Duration::from_secs(config.measurement.warmup_duration_secs),
379        name,
380    );
381
382    // Measurement
383    let samples = config.measurement.samples;
384    let mut all_timings = Vec::with_capacity(samples);
385    let mut cpu_samples = Vec::with_capacity(samples);
386
387    // Initialize CPU monitor for the pinned core
388    let cpu_core = get_pinned_core();
389    let monitor = CpuMonitor::new(cpu_core);
390
391    // Report progress every ~1% of samples
392    let report_interval = (samples / 100).max(1);
393
394    for sample_idx in 0..samples {
395        // Emit progress BEFORE timing
396        if sample_idx % report_interval == 0 {
397            emit_progress(&ProgressMessage {
398                bench: name,
399                phase: ProgressPhase::Samples {
400                    current: sample_idx as u32,
401                    total: samples as u32,
402                },
403            });
404        }
405
406        // Setup runs before each sample
407        let data = setup();
408
409        // Read CPU frequency BEFORE measurement
410        let freq_before = monitor.read_frequency();
411
412        let start = Instant::now();
413        bench(&data); // Borrows data
414        let elapsed = start.elapsed();
415        all_timings.push(elapsed);
416
417        // Read frequency after as well
418        let freq_after = monitor.read_frequency();
419        let frequency_khz = match (freq_before, freq_after) {
420            (Some(before), Some(after)) => Some(before.max(after)),
421            (Some(f), None) | (None, Some(f)) => Some(f),
422            (None, None) => None,
423        };
424
425        let snapshot = CpuSnapshot {
426            timestamp: Instant::now(),
427            frequency_khz,
428            temperature_millic: monitor.read_temperature(),
429        };
430        cpu_samples.push(snapshot);
431
432        drop(data); // Explicit drop (happens anyway)
433    }
434
435    // Emit completion message
436    emit_progress(&ProgressMessage {
437        bench: name,
438        phase: ProgressPhase::Complete,
439    });
440
441    let percentiles = calculate_percentiles(&all_timings);
442
443    BenchResult {
444        name: name.to_string(),
445        module: module.to_string(),
446        samples,
447        percentiles,
448        all_timings,
449        cpu_samples,
450        warmup_ms: Some(warmup_ms),
451        warmup_iterations: Some(warmup_iters),
452    }
453}
454
455pub fn measure_single_iteration<F>(func: F) -> Duration
456where
457    F: FnOnce(),
458{
459    let start = Instant::now();
460    func();
461    start.elapsed()
462}
463
464pub fn validate_measurement_params(samples: usize) -> Result<(), String> {
465    if samples == 0 {
466        return Err("Samples must be greater than 0".to_string());
467    }
468    if samples > 1_000_000 {
469        return Err(
470            "Samples should not exceed 1,000,000 for reasonable execution time".to_string(),
471        );
472    }
473    Ok(())
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use std::thread;
480
481    #[test]
482    fn test_measure_single_iteration() {
483        let duration = measure_single_iteration(|| {
484            thread::sleep(Duration::from_millis(1));
485        });
486
487        assert!(duration >= Duration::from_millis(1));
488        assert!(duration < Duration::from_millis(10)); // Should be close to 1ms
489    }
490
491    #[test]
492    fn test_validate_measurement_params() {
493        assert!(validate_measurement_params(100).is_ok());
494        assert!(validate_measurement_params(0).is_err());
495        assert!(validate_measurement_params(1_000_001).is_err());
496        assert!(validate_measurement_params(100_000).is_ok());
497    }
498
499    #[test]
500    fn test_measure_simple_basic() {
501        let config = BenchmarkConfig {
502            measurement: crate::config::MeasurementConfig {
503                samples: 10,
504                warmup_duration_secs: 0, // Skip warmup for test speed
505            },
506            ..Default::default()
507        };
508
509        let result = measure_simple(&config, "test_bench", "test_module", || {
510            // Simple work
511            let _ = (0..100).sum::<i32>();
512        });
513
514        assert_eq!(result.name, "test_bench");
515        assert_eq!(result.module, "test_module");
516        assert_eq!(result.samples, 10);
517        assert_eq!(result.all_timings.len(), 10);
518
519        // All measurements should be reasonable (not zero, not extremely large)
520        for timing in &result.all_timings {
521            assert!(*timing > Duration::from_nanos(0));
522            assert!(*timing < Duration::from_secs(1));
523        }
524    }
525}