simplebench_runtime/
config.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::Path;
4
5/// Configuration for benchmark measurement parameters
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct MeasurementConfig {
8    /// Number of timing samples to collect per benchmark
9    #[serde(default = "default_samples")]
10    pub samples: usize,
11
12    /// Warmup duration in seconds (default: 3 seconds, matching Criterion)
13    #[serde(default = "default_warmup_duration")]
14    pub warmup_duration_secs: u64,
15}
16
17fn default_samples() -> usize {
18    1000
19}
20fn default_warmup_duration() -> u64 {
21    3 // 3 seconds, matching Criterion's default
22}
23
24impl Default for MeasurementConfig {
25    fn default() -> Self {
26        Self {
27            samples: default_samples(),
28            warmup_duration_secs: default_warmup_duration(),
29        }
30    }
31}
32
33/// Configuration for baseline comparison
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ComparisonConfig {
36    /// Regression threshold percentage
37    #[serde(default = "default_threshold")]
38    pub threshold: f64,
39
40    /// CI mode: fail on regressions
41    #[serde(default)]
42    pub ci_mode: bool,
43
44    /// Window size for historical comparison (default: 10)
45    #[serde(default = "default_window_size")]
46    pub window_size: usize,
47
48    /// Statistical confidence level (default: 0.95 = 95%)
49    #[serde(default = "default_confidence_level")]
50    pub confidence_level: f64,
51
52    /// Change point probability threshold (default: 0.8 = 80%)
53    #[serde(default = "default_cp_threshold")]
54    pub cp_threshold: f64,
55
56    /// Bayesian hazard rate (default: 0.1 = change every 10 runs)
57    #[serde(default = "default_hazard_rate")]
58    pub hazard_rate: f64,
59}
60
61fn default_threshold() -> f64 {
62    5.0
63}
64
65fn default_window_size() -> usize {
66    10
67}
68
69fn default_confidence_level() -> f64 {
70    0.95
71}
72
73fn default_cp_threshold() -> f64 {
74    0.8
75}
76
77fn default_hazard_rate() -> f64 {
78    0.1
79}
80
81impl Default for ComparisonConfig {
82    fn default() -> Self {
83        Self {
84            threshold: default_threshold(),
85            ci_mode: false,
86            window_size: default_window_size(),
87            confidence_level: default_confidence_level(),
88            cp_threshold: default_cp_threshold(),
89            hazard_rate: default_hazard_rate(),
90        }
91    }
92}
93
94/// Complete SimpleBench configuration
95#[derive(Debug, Clone, Serialize, Deserialize, Default)]
96pub struct BenchmarkConfig {
97    #[serde(default)]
98    pub measurement: MeasurementConfig,
99
100    #[serde(default)]
101    pub comparison: ComparisonConfig,
102}
103
104impl BenchmarkConfig {
105    /// Load configuration with priority: env vars > config file > defaults
106    ///
107    /// This is called by the generated runner at startup.
108    pub fn load() -> Self {
109        // Start with defaults
110        let mut config = Self::default();
111
112        // Try to load from config file
113        if let Ok(file_config) = Self::from_file("simplebench.toml") {
114            config = file_config;
115        }
116
117        // Override with environment variables
118        config.apply_env_overrides();
119
120        config
121    }
122
123    /// Load configuration from a TOML file
124    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
125        let contents = fs::read_to_string(path)?;
126        let config: BenchmarkConfig = toml::from_str(&contents)?;
127        Ok(config)
128    }
129
130    /// Apply environment variable overrides
131    ///
132    /// This allows CLI args (passed via env vars) to override config file values.
133    pub fn apply_env_overrides(&mut self) {
134        // Measurement overrides
135        if let Ok(samples) = std::env::var("SIMPLEBENCH_SAMPLES") {
136            if let Ok(val) = samples.parse() {
137                self.measurement.samples = val;
138            }
139        }
140
141        if let Ok(warmup) = std::env::var("SIMPLEBENCH_WARMUP_DURATION") {
142            if let Ok(val) = warmup.parse() {
143                self.measurement.warmup_duration_secs = val;
144            }
145        }
146
147        // Comparison overrides
148        if std::env::var("SIMPLEBENCH_CI").is_ok() {
149            self.comparison.ci_mode = true;
150        }
151
152        if let Ok(threshold) = std::env::var("SIMPLEBENCH_THRESHOLD") {
153            if let Ok(val) = threshold.parse() {
154                self.comparison.threshold = val;
155            }
156        }
157
158        // CPD-specific overrides
159        if let Ok(window) = std::env::var("SIMPLEBENCH_WINDOW") {
160            if let Ok(val) = window.parse() {
161                self.comparison.window_size = val;
162            }
163        }
164
165        if let Ok(confidence) = std::env::var("SIMPLEBENCH_CONFIDENCE") {
166            if let Ok(val) = confidence.parse() {
167                self.comparison.confidence_level = val;
168            }
169        }
170
171        if let Ok(cp_threshold) = std::env::var("SIMPLEBENCH_CP_THRESHOLD") {
172            if let Ok(val) = cp_threshold.parse() {
173                self.comparison.cp_threshold = val;
174            }
175        }
176
177        if let Ok(hazard_rate) = std::env::var("SIMPLEBENCH_HAZARD_RATE") {
178            if let Ok(val) = hazard_rate.parse() {
179                self.comparison.hazard_rate = val;
180            }
181        }
182    }
183
184    /// Save configuration to a TOML file
185    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
186        let toml = toml::to_string_pretty(self)?;
187        fs::write(path, toml)?;
188        Ok(())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use std::env;
196    use tempfile::NamedTempFile;
197
198    #[test]
199    fn test_default_config() {
200        let config = BenchmarkConfig::default();
201        assert_eq!(config.measurement.samples, 1000);
202        assert_eq!(config.measurement.warmup_duration_secs, 3);
203        assert_eq!(config.comparison.threshold, 5.0);
204        assert!(!config.comparison.ci_mode);
205    }
206
207    #[test]
208    fn test_save_and_load_config() {
209        let config = BenchmarkConfig::default();
210        let temp_file = NamedTempFile::new().unwrap();
211
212        config.save(temp_file.path()).unwrap();
213        let loaded = BenchmarkConfig::from_file(temp_file.path()).unwrap();
214
215        assert_eq!(loaded.measurement.samples, 1000);
216        assert_eq!(loaded.measurement.warmup_duration_secs, 3);
217    }
218
219    #[test]
220    fn test_env_overrides() {
221        env::set_var("SIMPLEBENCH_SAMPLES", "300");
222        env::set_var("SIMPLEBENCH_WARMUP_DURATION", "5");
223        env::set_var("SIMPLEBENCH_CI", "1");
224        env::set_var("SIMPLEBENCH_THRESHOLD", "10.0");
225
226        let mut config = BenchmarkConfig::default();
227        config.apply_env_overrides();
228
229        assert_eq!(config.measurement.samples, 300);
230        assert_eq!(config.measurement.warmup_duration_secs, 5);
231        assert!(config.comparison.ci_mode);
232        assert_eq!(config.comparison.threshold, 10.0);
233
234        // Clean up
235        env::remove_var("SIMPLEBENCH_SAMPLES");
236        env::remove_var("SIMPLEBENCH_WARMUP_DURATION");
237        env::remove_var("SIMPLEBENCH_CI");
238        env::remove_var("SIMPLEBENCH_THRESHOLD");
239    }
240
241    #[test]
242    fn test_partial_config_file() {
243        let toml_content = r#"
244            [measurement]
245            samples = 150
246
247            [comparison]
248            threshold = 7.5
249        "#;
250
251        let temp_file = NamedTempFile::new().unwrap();
252        fs::write(temp_file.path(), toml_content).unwrap();
253
254        let config = BenchmarkConfig::from_file(temp_file.path()).unwrap();
255
256        // Specified values
257        assert_eq!(config.measurement.samples, 150);
258        assert_eq!(config.comparison.threshold, 7.5);
259
260        // Default values for unspecified fields
261        assert_eq!(config.measurement.warmup_duration_secs, 3);
262        assert!(!config.comparison.ci_mode);
263    }
264}