simplebench_runtime/
lib.rs

1//! SimpleBench Runtime - Core library for the SimpleBench microbenchmarking framework.
2//!
3//! This crate provides the runtime components for SimpleBench:
4//! - Benchmark registration via the [`SimpleBench`] struct and `inventory` crate
5//! - Timing and measurement with warmup phases
6//! - Statistical analysis of benchmark results
7//! - Baseline storage and regression detection
8//!
9//! # Usage
10//!
11//! This crate is typically used alongside `simplebench-macros` which provides the
12//! `#[bench]` attribute for easy benchmark registration:
13//!
14//! ```rust,ignore
15//! use simplebench_macros::bench;
16//!
17//! #[bench]
18//! fn my_benchmark() {
19//!     // code to benchmark
20//! }
21//! ```
22//!
23//! The `cargo simplebench` CLI tool handles compilation and execution of benchmarks.
24
25use serde::{Deserialize, Serialize};
26use std::time::Duration;
27
28pub mod baseline;
29pub mod changepoint;
30pub mod config;
31pub mod cpu_analysis;
32pub mod cpu_monitor;
33pub mod measurement;
34pub mod output;
35pub mod statistics;
36
37pub use baseline::*;
38pub use changepoint::*;
39pub use config::*;
40pub use cpu_analysis::*;
41pub use cpu_monitor::*;
42pub use measurement::*;
43pub use output::*;
44pub use statistics::*;
45
46// Re-export inventory for use by the macro
47pub use inventory;
48
49/// Percentile statistics for a benchmark run.
50///
51/// Contains the 50th, 90th, and 99th percentile timings along with the mean.
52#[derive(Debug, Default, Clone, Serialize, Deserialize)]
53pub struct Percentiles {
54    /// 50th percentile (median) timing
55    pub p50: Duration,
56    /// 90th percentile timing
57    pub p90: Duration,
58    /// 99th percentile timing
59    pub p99: Duration,
60    /// Arithmetic mean of all timings
61    pub mean: Duration,
62}
63
64/// Comprehensive statistics for a benchmark run.
65///
66/// All timing values are in nanoseconds for precision.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct Statistics {
69    /// Arithmetic mean in nanoseconds
70    pub mean: u128,
71    /// Median (50th percentile) in nanoseconds
72    pub median: u128,
73    /// 90th percentile in nanoseconds
74    pub p90: u128,
75    /// 99th percentile in nanoseconds
76    pub p99: u128,
77    /// Standard deviation in nanoseconds
78    pub std_dev: f64,
79    /// Variance in nanoseconds squared
80    pub variance: f64,
81    /// Minimum timing in nanoseconds
82    pub min: u128,
83    /// Maximum timing in nanoseconds
84    pub max: u128,
85    /// Number of samples collected
86    pub sample_count: usize,
87}
88
89/// Complete result of a benchmark run.
90///
91/// Contains all timing data, statistics, and metadata for a single benchmark execution.
92#[derive(Debug, Default, Clone, Serialize, Deserialize)]
93pub struct BenchResult {
94    /// Benchmark function name
95    pub name: String,
96    /// Module path where the benchmark is defined
97    pub module: String,
98    /// Number of iterations per sample
99    pub iterations: usize,
100    /// Number of samples collected
101    pub samples: usize,
102    /// Percentile statistics computed from all timings
103    pub percentiles: Percentiles,
104    /// Raw timing data for each sample
105    pub all_timings: Vec<Duration>,
106    /// CPU state samples collected during the run
107    #[serde(default)]
108    pub cpu_samples: Vec<CpuSnapshot>,
109    /// Total warmup duration in milliseconds
110    #[serde(default)]
111    pub warmup_ms: Option<u128>,
112    /// Number of iterations performed during warmup
113    #[serde(default)]
114    pub warmup_iterations: Option<u64>,
115}
116
117/// Comparison between current benchmark run and baseline.
118///
119/// Contains statistical measures to determine if performance has regressed.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Comparison {
122    /// Mean timing from the current run
123    pub current_mean: Duration,
124    /// Mean timing from the baseline
125    pub baseline_mean: Duration,
126    /// Percentage change from baseline (positive = slower)
127    pub percentage_change: f64,
128    /// Number of baseline samples used for comparison
129    #[serde(default)]
130    pub baseline_count: usize,
131    /// Z-score for statistical significance
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub z_score: Option<f64>,
134    /// 95% confidence interval for the change
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub confidence_interval: Option<(f64, f64)>,
137    /// Probability that a real change occurred
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub change_probability: Option<f64>,
140}
141
142/// A registered benchmark function.
143///
144/// This struct is used by the `inventory` crate for compile-time benchmark registration.
145/// The `#[bench]` macro from `simplebench-macros` generates these registrations automatically.
146pub struct SimpleBench {
147    /// Name of the benchmark function
148    pub name: &'static str,
149    /// Module path where the benchmark is defined
150    pub module: &'static str,
151    /// The benchmark function to execute
152    pub func: fn(),
153}
154
155inventory::collect!(SimpleBench);
156
157/// Benchmark metadata for JSON listing.
158///
159/// A simplified representation of a benchmark for discovery/listing purposes.
160#[derive(Debug, Serialize, Deserialize)]
161pub struct BenchmarkInfo {
162    /// Name of the benchmark function
163    pub name: String,
164    /// Module path where the benchmark is defined
165    pub module: String,
166}
167
168/// List all registered benchmarks as JSON to stdout
169///
170/// Used by the orchestrator to discover benchmark names before execution.
171pub fn list_benchmarks_json() {
172    let benchmarks: Vec<BenchmarkInfo> = inventory::iter::<SimpleBench>()
173        .map(|b| BenchmarkInfo {
174            name: b.name.to_string(),
175            module: b.module.to_string(),
176        })
177        .collect();
178    println!("{}", serde_json::to_string(&benchmarks).unwrap());
179}
180
181/// Run a single benchmark and output JSON result to stdout
182///
183/// The benchmark to run is specified via SIMPLEBENCH_BENCH_FILTER env var (exact match).
184/// The core to pin to is specified via SIMPLEBENCH_PIN_CORE env var.
185pub fn run_single_benchmark_json(config: &crate::config::BenchmarkConfig) {
186    let bench_name = std::env::var("SIMPLEBENCH_BENCH_FILTER")
187        .expect("SIMPLEBENCH_BENCH_FILTER must be set for single benchmark execution");
188
189    let pin_core: usize = std::env::var("SIMPLEBENCH_PIN_CORE")
190        .ok()
191        .and_then(|s| s.parse().ok())
192        .unwrap_or(1); // Default to core 1, not 0 (reserved)
193
194    // Set CPU affinity
195    if let Err(e) = affinity::set_thread_affinity([pin_core]) {
196        eprintln!(
197            "Warning: Failed to set affinity to core {}: {:?}",
198            pin_core, e
199        );
200    }
201
202    // Find and run the benchmark
203    for bench in inventory::iter::<SimpleBench>() {
204        if bench.name == bench_name {
205            let result = measure_with_warmup(
206                bench.name.to_string(),
207                bench.module.to_string(),
208                bench.func,
209                config.measurement.iterations,
210                config.measurement.samples,
211                config.measurement.warmup_duration_secs,
212            );
213            println!("{}", serde_json::to_string(&result).unwrap());
214            return;
215        }
216    }
217
218    eprintln!("ERROR: Benchmark '{}' not found", bench_name);
219    std::process::exit(1);
220}
221
222pub(crate) fn calculate_percentiles(timings: &[Duration]) -> Percentiles {
223    let mut sorted_timings = timings.to_vec();
224    sorted_timings.sort();
225
226    let len = sorted_timings.len();
227    let p50_idx = (len * 50) / 100;
228    let p90_idx = (len * 90) / 100;
229    let p99_idx = (len * 99) / 100;
230
231    // Calculate mean
232    let sum_nanos: u128 = timings.iter().map(|d| d.as_nanos()).sum();
233    let mean_nanos = sum_nanos / (len as u128);
234    let mean = Duration::from_nanos(mean_nanos as u64);
235
236    Percentiles {
237        p50: sorted_timings[p50_idx.min(len - 1)],
238        p90: sorted_timings[p90_idx.min(len - 1)],
239        p99: sorted_timings[p99_idx.min(len - 1)],
240        mean,
241    }
242}
243
244/// Calculate comprehensive statistics from raw timing samples
245pub fn calculate_statistics(samples: &[u128]) -> Statistics {
246    let sample_count = samples.len();
247
248    if sample_count == 0 {
249        return Statistics {
250            mean: 0,
251            median: 0,
252            p90: 0,
253            p99: 0,
254            std_dev: 0.0,
255            variance: 0.0,
256            min: 0,
257            max: 0,
258            sample_count: 0,
259        };
260    }
261
262    // Sort for percentile calculations
263    let mut sorted = samples.to_vec();
264    sorted.sort();
265
266    // Calculate percentiles
267    let p50_idx = (sample_count * 50) / 100;
268    let p90_idx = (sample_count * 90) / 100;
269    let p99_idx = (sample_count * 99) / 100;
270
271    let median = sorted[p50_idx.min(sample_count - 1)];
272    let p90 = sorted[p90_idx.min(sample_count - 1)];
273    let p99 = sorted[p99_idx.min(sample_count - 1)];
274
275    // Calculate mean
276    let sum: u128 = samples.iter().sum();
277    let mean = sum / (sample_count as u128);
278
279    // Calculate variance and standard deviation
280    let mean_f64 = mean as f64;
281    let variance: f64 = samples
282        .iter()
283        .map(|&s| {
284            let diff = s as f64 - mean_f64;
285            diff * diff
286        })
287        .sum::<f64>()
288        / (sample_count as f64);
289
290    let std_dev = variance.sqrt();
291
292    // Min and max
293    let min = *sorted.first().unwrap();
294    let max = *sorted.last().unwrap();
295
296    Statistics {
297        mean,
298        median,
299        p90,
300        p99,
301        std_dev,
302        variance,
303        min,
304        max,
305        sample_count,
306    }
307}
308
309/// Run all benchmarks with configuration and stream results
310///
311/// This is the primary entry point for the generated runner.
312/// Prints each benchmark result immediately as it completes.
313pub fn run_and_stream_benchmarks(config: &crate::config::BenchmarkConfig) -> Vec<BenchResult> {
314    use crate::baseline::{BaselineManager, ComparisonResult};
315    use crate::output::{
316        print_benchmark_result_line, print_comparison_line, print_new_baseline_line,
317        print_streaming_summary,
318    };
319    use colored::*;
320
321    match affinity::set_thread_affinity([0]) {
322        Ok(_) => println!(
323            "{} {}\n",
324            "Set affinity to core".green().bold(),
325            "0".cyan().bold()
326        ),
327        Err(e) => println!("Failed to set core affinity {e:?}"),
328    };
329
330    // Verify benchmark environment
331    crate::cpu_monitor::verify_benchmark_environment(0);
332
333    let mut results = Vec::new();
334    let mut comparisons = Vec::new();
335
336    // Initialize baseline manager
337    let baseline_manager = match BaselineManager::new() {
338        Ok(bm) => Some(bm),
339        Err(e) => {
340            eprintln!("Warning: Could not initialize baseline manager: {}", e);
341            eprintln!("Running without baseline comparison.");
342            None
343        }
344    };
345
346    // Get benchmark filter if specified
347    let bench_filter = std::env::var("SIMPLEBENCH_BENCH_FILTER").ok();
348
349    // Count how many benchmarks match the filter
350    let total_benchmarks: usize = inventory::iter::<SimpleBench>().count();
351    let filtered_count = if let Some(ref filter) = bench_filter {
352        inventory::iter::<SimpleBench>()
353            .filter(|b| b.name.contains(filter))
354            .count()
355    } else {
356        total_benchmarks
357    };
358
359    println!(
360        "{} {} {} {} {}",
361        "Running benchmarks with".green().bold(),
362        config.measurement.samples,
363        "samples ×".green().bold(),
364        config.measurement.iterations,
365        "iterations".green().bold()
366    );
367
368    if let Some(ref filter) = bench_filter {
369        println!(
370            "{} {} ({} matched filter: \"{}\")\n",
371            "Filtering to".dimmed(),
372            filtered_count,
373            if filtered_count == 1 {
374                "benchmark"
375            } else {
376                "benchmarks"
377            },
378            filter
379        );
380    } else {
381        println!();
382    }
383
384    // Run each benchmark and print immediately
385    for bench in inventory::iter::<SimpleBench> {
386        // Apply filter if specified
387        if let Some(ref filter) = bench_filter {
388            if !bench.name.contains(filter) {
389                continue; // Skip this benchmark
390            }
391        }
392        // Run benchmark
393        let result = measure_with_warmup(
394            bench.name.to_string(),
395            bench.module.to_string(),
396            bench.func,
397            config.measurement.iterations,
398            config.measurement.samples,
399            config.measurement.warmup_duration_secs,
400        );
401
402        // Print benchmark result immediately
403        print_benchmark_result_line(&result);
404
405        // Compare with baseline using CPD and print comparison
406        if let Some(ref bm) = baseline_manager {
407            let crate_name = result.module.split("::").next().unwrap_or("unknown");
408
409            // Load recent baselines for window-based comparison
410            let mut is_regression = false;
411            if let Ok(historical) =
412                bm.load_recent_baselines(crate_name, &result.name, config.comparison.window_size)
413            {
414                if !historical.is_empty() {
415                    // Use CPD-based comparison
416                    let comparison_result = crate::baseline::detect_regression_with_cpd(
417                        &result,
418                        &historical,
419                        config.comparison.threshold,
420                        config.comparison.confidence_level,
421                        config.comparison.cp_threshold,
422                        config.comparison.hazard_rate,
423                    );
424
425                    is_regression = comparison_result.is_regression;
426
427                    if let Some(ref comparison) = comparison_result.comparison {
428                        print_comparison_line(
429                            comparison,
430                            &result.name,
431                            comparison_result.is_regression,
432                        );
433                    }
434
435                    comparisons.push(comparison_result);
436                } else {
437                    // First run - no baseline
438                    print_new_baseline_line(&result.name);
439
440                    comparisons.push(ComparisonResult {
441                        benchmark_name: result.name.clone(),
442                        comparison: None,
443                        is_regression: false,
444                    });
445                }
446            }
447
448            // Save new baseline with regression flag
449            if let Err(e) = bm.save_baseline(crate_name, &result, is_regression) {
450                eprintln!(
451                    "Warning: Failed to save baseline for {}: {}",
452                    result.name, e
453                );
454            }
455        }
456
457        results.push(result);
458        println!(); // Blank line between benchmarks
459    }
460
461    // Print summary footer
462    if !comparisons.is_empty() {
463        print_streaming_summary(&comparisons, &config.comparison);
464
465        // Show filter stats if filtering was applied
466        if let Some(ref filter) = bench_filter {
467            println!(
468                "\n{} {} of {} total benchmarks (filter: \"{}\")",
469                "Ran".dimmed(),
470                filtered_count,
471                total_benchmarks,
472                filter
473            );
474        }
475    }
476
477    results
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn test_calculate_percentiles() {
486        let timings = vec![
487            Duration::from_millis(1),
488            Duration::from_millis(2),
489            Duration::from_millis(3),
490            Duration::from_millis(4),
491            Duration::from_millis(5),
492            Duration::from_millis(6),
493            Duration::from_millis(7),
494            Duration::from_millis(8),
495            Duration::from_millis(9),
496            Duration::from_millis(10),
497        ];
498
499        let percentiles = calculate_percentiles(&timings);
500
501        // For 10 samples: p50 at index 5 (6ms), p90 at index 9 (10ms), p99 at index 9 (10ms)
502        // Mean: (1+2+3+4+5+6+7+8+9+10)/10 = 55/10 = 5.5ms
503        assert_eq!(percentiles.p50, Duration::from_millis(6));
504        assert_eq!(percentiles.p90, Duration::from_millis(10));
505        assert_eq!(percentiles.p99, Duration::from_millis(10));
506        assert_eq!(percentiles.mean, Duration::from_micros(5500));
507    }
508
509    #[test]
510    fn test_calculate_percentiles_single_element() {
511        let timings = vec![Duration::from_millis(5)];
512        let percentiles = calculate_percentiles(&timings);
513
514        assert_eq!(percentiles.p50, Duration::from_millis(5));
515        assert_eq!(percentiles.p90, Duration::from_millis(5));
516        assert_eq!(percentiles.p99, Duration::from_millis(5));
517        assert_eq!(percentiles.mean, Duration::from_millis(5));
518    }
519}