stylus_trace_core/diff/
threshold.rs1use 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#[derive(Debug, Clone, Default, Deserialize, Serialize)]
16pub struct ThresholdConfig {
17 #[serde(default)]
19 pub gas: GasThresholds,
20
21 #[serde(default)]
23 pub hostio: HostIOThresholds,
24
25 #[serde(default)]
27 pub hot_paths: Option<HotPathThresholds>,
28}
29
30#[derive(Debug, Clone, Default, Deserialize, Serialize)]
32pub struct GasThresholds {
33 pub max_increase_percent: Option<f64>,
35
36 pub max_increase_absolute: Option<u64>,
38}
39
40#[derive(Debug, Clone, Default, Deserialize, Serialize)]
42pub struct HostIOThresholds {
43 pub max_total_calls_increase_percent: Option<f64>,
45
46 pub limits: Option<HashMap<String, u64>>,
48}
49
50#[derive(Debug, Clone, Default, Deserialize, Serialize)]
52pub struct HotPathThresholds {
53 pub warn_individual_increase_percent: Option<f64>,
55}
56
57pub 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
79pub fn check_thresholds(
95 diff: &mut DiffReport,
96 config: &ThresholdConfig,
97) -> Vec<ThresholdViolation> {
98 let mut violations = Vec::new();
99
100 check_gas_thresholds(&diff.deltas.gas, &config.gas, &mut violations);
102
103 check_hostio_thresholds(&diff.deltas.hostio, &config.hostio, &mut violations);
105
106 if let Some(hp_thresholds) = &config.hot_paths {
108 check_hot_path_thresholds(&diff.deltas.hot_paths, hp_thresholds, &mut violations);
109 }
110
111 diff.threshold_violations = violations.clone();
113 diff.summary = create_summary(&violations);
114
115 violations
116}
117
118pub fn check_gas_thresholds(
120 gas_delta: &super::schema::GasDelta,
121 thresholds: &GasThresholds,
122 violations: &mut Vec<ThresholdViolation>,
123) {
124 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 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
149fn check_hostio_thresholds(
151 hostio_delta: &super::schema::HostIoDelta,
152 thresholds: &HostIOThresholds,
153 violations: &mut Vec<ThresholdViolation>,
154) {
155 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 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
184fn 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
204pub 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}