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