Skip to main content

stylus_trace_core/diff/
threshold.rs

1//! Threshold configuration and violation detection.
2//!
3//! Loads threshold policies from TOML and checks diff reports
4//! for violations.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10
11use super::schema::{DiffReport, DiffSummary, ThresholdViolation};
12use super::DiffError;
13
14/// Complete threshold configuration
15#[derive(Debug, Clone, Default, Deserialize, Serialize)]
16pub struct ThresholdConfig {
17    /// Gas thresholds
18    #[serde(default)]
19    pub gas: GasThresholds,
20
21    /// HostIO thresholds
22    #[serde(default)]
23    pub hostio: HostIOThresholds,
24
25    /// Hot path thresholds (optional)
26    #[serde(default)]
27    pub hot_paths: Option<HotPathThresholds>,
28}
29
30/// Gas-related thresholds
31#[derive(Debug, Clone, Default, Deserialize, Serialize)]
32pub struct GasThresholds {
33    /// Maximum allowed gas increase percentage
34    pub max_increase_percent: Option<f64>,
35
36    /// Maximum allowed absolute gas increase
37    pub max_increase_absolute: Option<u64>,
38}
39
40/// HostIO-related thresholds
41#[derive(Debug, Clone, Default, Deserialize, Serialize)]
42pub struct HostIOThresholds {
43    /// Maximum allowed percentage increase in total HostIO calls
44    pub max_total_calls_increase_percent: Option<f64>,
45
46    /// Per-type absolute limits (e.g., storage_load_max_increase: 5)
47    pub limits: Option<HashMap<String, u64>>,
48}
49
50/// Hot path thresholds
51#[derive(Debug, Clone, Default, Deserialize, Serialize)]
52pub struct HotPathThresholds {
53    /// Warn if any single hot path increases by more than this percentage
54    pub warn_individual_increase_percent: Option<f64>,
55}
56
57/// Load thresholds from a TOML file
58///
59/// # Arguments
60/// * `path` - Path to the TOML configuration file
61///
62/// # Returns
63/// Parsed ThresholdConfig
64///
65/// # Errors
66/// * `DiffError::IoError` - If file cannot be read
67/// * `DiffError::ThresholdParseFailed` - If TOML is invalid
68///
69/// # Example
70/// ```ignore
71/// let thresholds = load_thresholds("thresholds.toml")?;
72/// ```
73pub fn load_thresholds(path: impl AsRef<Path>) -> Result<ThresholdConfig, DiffError> {
74    let contents = fs::read_to_string(path)?;
75    let config: ThresholdConfig = toml::from_str(&contents)?;
76    Ok(config)
77}
78
79/// Check a diff report against thresholds and update violations
80///
81/// # Arguments
82/// * `diff` - Mutable reference to diff report to update
83/// * `config` - Threshold configuration to check against
84///
85/// # Returns
86/// Vector of violations (also updates diff.threshold_violations)
87///
88/// # Example
89/// ```ignore
90/// let mut diff = generate_diff(&baseline, &target)?;
91/// let thresholds = load_thresholds("thresholds.toml")?;
92/// check_thresholds(&mut diff, &thresholds);
93/// ```
94pub fn check_thresholds(
95    diff: &mut DiffReport,
96    config: &ThresholdConfig,
97) -> Vec<ThresholdViolation> {
98    let mut violations = Vec::new();
99
100    // Check gas thresholds
101    check_gas_thresholds(&diff.deltas.gas, &config.gas, &mut violations);
102
103    // Check HostIO thresholds
104    check_hostio_thresholds(&diff.deltas.hostio, &config.hostio, &mut violations);
105
106    // Check hot path thresholds
107    if let Some(hp_thresholds) = &config.hot_paths {
108        check_hot_path_thresholds(&diff.deltas.hot_paths, hp_thresholds, &mut violations);
109    }
110
111    // Update diff report
112    diff.threshold_violations = violations.clone();
113    diff.summary = create_summary(&violations);
114
115    violations
116}
117
118/// Check gas thresholds
119pub fn check_gas_thresholds(
120    gas_delta: &super::schema::GasDelta,
121    thresholds: &GasThresholds,
122    violations: &mut Vec<ThresholdViolation>,
123) {
124    // Check percentage increase
125    if let Some(max_percent) = thresholds.max_increase_percent {
126        if gas_delta.percent_change > max_percent {
127            violations.push(ThresholdViolation {
128                metric: "gas.max_increase_percent".to_string(),
129                threshold: max_percent,
130                actual: gas_delta.percent_change,
131                severity: "error".to_string(),
132            });
133        }
134    }
135
136    // Check absolute increase
137    if let Some(max_absolute) = thresholds.max_increase_absolute {
138        if gas_delta.absolute_change > 0 && gas_delta.absolute_change as u64 > max_absolute {
139            violations.push(ThresholdViolation {
140                metric: "gas.max_increase_absolute".to_string(),
141                threshold: max_absolute as f64,
142                actual: gas_delta.absolute_change as f64,
143                severity: "error".to_string(),
144            });
145        }
146    }
147}
148
149/// Check HostIO thresholds
150fn check_hostio_thresholds(
151    hostio_delta: &super::schema::HostIoDelta,
152    thresholds: &HostIOThresholds,
153    violations: &mut Vec<ThresholdViolation>,
154) {
155    // Check total calls percentage
156    if let Some(max_percent) = thresholds.max_total_calls_increase_percent {
157        if hostio_delta.total_calls_percent_change > max_percent {
158            violations.push(ThresholdViolation {
159                metric: "hostio.max_total_calls_increase_percent".to_string(),
160                threshold: max_percent,
161                actual: hostio_delta.total_calls_percent_change,
162                severity: "error".to_string(),
163            });
164        }
165    }
166
167    // Check per-type limits
168    if let Some(limits) = &thresholds.limits {
169        for (hostio_type, max_increase) in limits {
170            if let Some(change) = hostio_delta.by_type_changes.get(hostio_type) {
171                if change.delta > 0 && change.delta as u64 > *max_increase {
172                    violations.push(ThresholdViolation {
173                        metric: format!("hostio.limits.{}_max_increase", hostio_type),
174                        threshold: *max_increase as f64,
175                        actual: change.delta as f64,
176                        severity: "error".to_string(),
177                    });
178                }
179            }
180        }
181    }
182}
183
184/// Check hot path thresholds
185fn check_hot_path_thresholds(
186    hot_paths_delta: &super::schema::HotPathsDelta,
187    thresholds: &HotPathThresholds,
188    violations: &mut Vec<ThresholdViolation>,
189) {
190    if let Some(max_percent) = thresholds.warn_individual_increase_percent {
191        for comparison in &hot_paths_delta.common_paths {
192            if comparison.percent_change > max_percent {
193                violations.push(ThresholdViolation {
194                    metric: format!("hot_paths.{}", comparison.stack),
195                    threshold: max_percent,
196                    actual: comparison.percent_change,
197                    severity: "warning".to_string(),
198                });
199            }
200        }
201    }
202}
203
204/// Create summary based on violations
205pub fn create_summary(violations: &[ThresholdViolation]) -> DiffSummary {
206    let error_count = violations.iter().filter(|v| v.severity == "error").count();
207    let warning_count = violations
208        .iter()
209        .filter(|v| v.severity == "warning")
210        .count();
211
212    let status = if error_count > 0 {
213        "FAILED"
214    } else if warning_count > 0 {
215        "WARNING"
216    } else {
217        "PASSED"
218    };
219
220    DiffSummary {
221        has_regressions: error_count > 0,
222        violation_count: violations.len(),
223        status: status.to_string(),
224        warning: None,
225    }
226}