1use indexmap::{IndexMap, IndexSet};
3
4use crate::api::{self, EventKind};
5use crate::runner::metrics::{Metric, MetricKind, MetricsSummary};
6use crate::runner::summary::ToolRegression;
7use crate::runner::tool::regression::RegressionConfig;
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct CallgrindRegressionConfig {
12 pub fail_fast: bool,
14 pub hard_limits: Vec<(EventKind, Metric)>,
16 pub soft_limits: Vec<(EventKind, f64)>,
18}
19
20impl Default for CallgrindRegressionConfig {
21 fn default() -> Self {
22 Self {
23 soft_limits: vec![(EventKind::Ir, 10f64)],
24 hard_limits: Vec::default(),
25 fail_fast: Default::default(),
26 }
27 }
28}
29
30impl RegressionConfig<EventKind> for CallgrindRegressionConfig {
31 fn check(&self, metrics_summary: &MetricsSummary) -> Vec<ToolRegression> {
35 self.check_regressions(metrics_summary)
36 .into_iter()
37 .map(|regressions| ToolRegression::with(MetricKind::Callgrind, regressions))
38 .collect()
39 }
40
41 fn get_soft_limits(&self) -> &[(EventKind, f64)] {
42 &self.soft_limits
43 }
44
45 fn get_hard_limits(&self) -> &[(EventKind, Metric)] {
46 &self.hard_limits
47 }
48}
49
50impl TryFrom<api::CallgrindRegressionConfig> for CallgrindRegressionConfig {
51 type Error = String;
52
53 fn try_from(value: api::CallgrindRegressionConfig) -> Result<Self, Self::Error> {
54 let api::CallgrindRegressionConfig {
55 soft_limits,
56 hard_limits,
57 fail_fast,
58 } = value;
59
60 let (soft_limits, hard_limits) = if soft_limits.is_empty() && hard_limits.is_empty() {
61 (IndexMap::from([(EventKind::Ir, 10f64)]), IndexMap::new())
62 } else {
63 let hard_limits = hard_limits
64 .into_iter()
65 .flat_map(|(callgrind_metrics, metric)| {
66 IndexSet::from(callgrind_metrics)
67 .into_iter()
68 .map(move |metric_kind| {
69 Metric::from(metric)
70 .try_convert(metric_kind)
71 .ok_or_else(|| {
72 format!(
73 "Invalid hard limit for \
74 '{metric_kind:?}/{callgrind_metrics:?}': Expected a \
75 'Int' but found '{metric:?}'"
76 )
77 })
78 })
79 })
80 .collect::<Result<IndexMap<EventKind, Metric>, String>>()?;
81
82 let soft_limits = soft_limits
83 .into_iter()
84 .flat_map(|(m, l)| IndexSet::from(m).into_iter().map(move |e| (e, l)))
85 .collect::<IndexMap<_, _>>();
86
87 (soft_limits, hard_limits)
88 };
89
90 Ok(Self {
91 soft_limits: soft_limits.into_iter().collect(),
92 hard_limits: hard_limits.into_iter().collect(),
93 fail_fast: fail_fast.unwrap_or(false),
94 })
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use rstest::rstest;
101 use EventKind::*;
102
103 use super::*;
104 use crate::api::{CallgrindMetrics, Limit};
105 use crate::runner::callgrind::model::Metrics;
106 use crate::runner::metrics::{Metric, TypeChecker};
107 use crate::util::EitherOrBoth;
108
109 fn cachesim_costs(costs: [u64; 9]) -> Metrics {
110 Metrics::with_metric_kinds([
111 (Ir, Metric::Int(costs[0])),
112 (Dr, Metric::Int(costs[1])),
113 (Dw, Metric::Int(costs[2])),
114 (I1mr, Metric::Int(costs[3])),
115 (D1mr, Metric::Int(costs[4])),
116 (D1mw, Metric::Int(costs[5])),
117 (ILmr, Metric::Int(costs[6])),
118 (DLmr, Metric::Int(costs[7])),
119 (DLmw, Metric::Int(costs[8])),
120 ])
121 }
122
123 #[rstest]
124 fn test_regression_check_when_old_is_none() {
125 let regression = CallgrindRegressionConfig::default();
126 let new = cachesim_costs([0, 0, 0, 0, 0, 0, 0, 0, 0]);
127 let summary = MetricsSummary::new(EitherOrBoth::Left(new));
128
129 assert!(regression.check(&summary).is_empty());
130 }
131
132 #[rstest]
133 #[case::ir_all_zero(
134 vec![(Ir, 0f64)],
135 [0, 0, 0, 0, 0, 0, 0, 0, 0],
136 [0, 0, 0, 0, 0, 0, 0, 0, 0],
137 vec![]
138 )]
139 #[case::ir_when_regression(
140 vec![(Ir, 0f64)],
141 [2, 0, 0, 0, 0, 0, 0, 0, 0],
142 [1, 0, 0, 0, 0, 0, 0, 0, 0],
143 vec![(Ir, 2, 1, 100f64, 0f64)]
144 )]
145 #[case::ir_when_improved(
146 vec![(Ir, 0f64)],
147 [1, 0, 0, 0, 0, 0, 0, 0, 0],
148 [2, 0, 0, 0, 0, 0, 0, 0, 0],
149 vec![]
150 )]
151 #[case::ir_when_negative_limit(
152 vec![(Ir, -49f64)],
153 [1, 0, 0, 0, 0, 0, 0, 0, 0],
154 [2, 0, 0, 0, 0, 0, 0, 0, 0],
155 vec![(Ir, 1, 2, -50f64, -49f64)]
156 )]
157 #[case::derived_all_zero(
158 vec![(EstimatedCycles, 0f64)],
159 [0, 0, 0, 0, 0, 0, 0, 0, 0],
160 [0, 0, 0, 0, 0, 0, 0, 0, 0],
161 vec![]
162 )]
163 #[case::derived_when_regression(
164 vec![(EstimatedCycles, 0f64)],
165 [2, 0, 0, 0, 0, 0, 0, 0, 0],
166 [1, 0, 0, 0, 0, 0, 0, 0, 0],
167 vec![(EstimatedCycles, 2, 1, 100f64, 0f64)]
168 )]
169 #[case::derived_when_regression_multiple(
170 vec![(EstimatedCycles, 5f64), (Ir, 10f64)],
171 [2, 0, 0, 0, 0, 0, 0, 0, 0],
172 [1, 0, 0, 0, 0, 0, 0, 0, 0],
173 vec![(EstimatedCycles, 2, 1, 100f64, 5f64), (Ir, 2, 1, 100f64, 10f64)]
174 )]
175 #[case::derived_when_improved(
176 vec![(EstimatedCycles, 0f64)],
177 [1, 0, 0, 0, 0, 0, 0, 0, 0],
178 [2, 0, 0, 0, 0, 0, 0, 0, 0],
179 vec![]
180 )]
181 #[case::derived_when_regression_mixed(
182 vec![(EstimatedCycles, 0f64)],
183 [96, 24, 18, 6, 0, 2, 6, 0, 2],
184 [48, 12, 9, 3, 0, 1, 3, 0, 1],
185 vec![(EstimatedCycles, 410, 205, 100f64, 0f64)]
186 )]
187 fn test_regression_check_when_soft_and_old_is_some(
188 #[case] soft_limits: Vec<(EventKind, f64)>,
189 #[case] new: [u64; 9],
190 #[case] old: [u64; 9],
191 #[case] expected: Vec<(EventKind, u64, u64, f64, f64)>,
192 ) {
193 let regression = CallgrindRegressionConfig {
194 soft_limits,
195 ..Default::default()
196 };
197
198 let new = cachesim_costs(new);
199 let old = cachesim_costs(old);
200 let summary = MetricsSummary::new(EitherOrBoth::Both(new, old));
201 let expected = expected
202 .iter()
203 .map(|(e, n, o, d, l)| ToolRegression::Soft {
204 metric: MetricKind::Callgrind(*e),
205 new: (*n).into(),
206 old: (*o).into(),
207 diff_pct: *d,
208 limit: *l,
209 })
210 .collect::<Vec<ToolRegression>>();
211
212 assert_eq!(regression.check(&summary), expected);
213 }
214
215 #[rstest]
216 #[case::empty_then_default(Vec::<(EventKind, f64)>::new(), vec![(EventKind::Ir, 10f64)])]
217 #[case::single(vec![(Ir, 0f64)], vec![(Ir, 0f64)])]
218 #[case::two(vec![(Ir, 0f64), (Dr, 10f64)], vec![(Ir, 0f64), (Dr, 10f64)])]
219 #[case::duplicate(vec![(Ir, 0f64), (Ir, 10f64)], vec![(Ir, 10f64)])]
220 #[case::group(
221 vec![(CallgrindMetrics::WriteBackBehaviour, 10f64)],
222 vec![(ILdmr, 10f64), (DLdmr, 10f64), (DLdmw, 10f64)],
223 )]
224 #[case::group_overwrite_keeps_order(
225 vec![(CallgrindMetrics::WriteBackBehaviour, 10f64), (ILdmr.into(), 20f64)],
226 vec![(ILdmr, 20f64), (DLdmr, 10f64), (DLdmw, 10f64)],
227 )]
228 fn test_try_from_regression_config_for_soft_limits<T>(
229 #[case] soft_limits: Vec<(T, f64)>,
230 #[case] expected_soft_limits: Vec<(EventKind, f64)>,
231 ) where
232 T: Into<CallgrindMetrics>,
233 {
234 let expected = CallgrindRegressionConfig {
235 soft_limits: expected_soft_limits,
236 hard_limits: Vec::default(),
237 fail_fast: false,
238 };
239 let api_regression_config = api::CallgrindRegressionConfig {
240 soft_limits: soft_limits
241 .into_iter()
242 .map(|(m, l)| (m.into(), l))
243 .collect(),
244 hard_limits: Vec::default(),
245 fail_fast: Option::default(),
246 };
247
248 assert_eq!(
249 CallgrindRegressionConfig::try_from(api_regression_config).unwrap(),
250 expected
251 );
252 }
253
254 #[rstest]
255 #[case::empty_then_default(Vec::<(EventKind, f64)>::new(), Vec::<(EventKind, f64)>::new())]
256 #[case::single(vec![(Ir, 0)], vec![(Ir, 0)])]
257 #[case::single_convert(vec![(L1HitRate, 1)], vec![(L1HitRate, 1f64)])]
258 #[case::two(vec![(Ir, 0), (Dr, 2)], vec![(Ir, 0), (Dr, 2)])]
259 #[case::duplicate_overwrite( vec![(Ir, 0), (Ir, 20)], vec![(Ir, 20)])]
260 #[case::integer_group(
261 vec![(CallgrindMetrics::WriteBackBehaviour, 10)],
262 vec![(ILdmr, 10), (DLdmr, 10), (DLdmw, 10)],
263 )]
264 #[case::float_group(
265 vec![(CallgrindMetrics::CacheHitRates, 10f64)],
266 vec![(L1HitRate, 10f64), (LLHitRate, 10f64), (RamHitRate, 10f64)],
267 )]
268 #[case::float_group_convert(
269 vec![(CallgrindMetrics::CacheHitRates, 10)],
270 vec![(L1HitRate, 10f64), (LLHitRate, 10f64), (RamHitRate, 10f64)],
271 )]
272 #[case::mixed_group(
273 vec![(CallgrindMetrics::CacheSim, 10)],
274 IndexSet::from(CallgrindMetrics::CacheSim)
275 .into_iter()
276 .map(|m| {
277 if m.is_int() {
278 (m, Metric::Int(10))
279 } else {
280 (m, Metric::Float(10.0))
281 }
282 }).collect()
283 )]
284 #[case::group_overwrite_keeps_order(
285 vec![(CallgrindMetrics::WriteBackBehaviour, 10), (ILdmr.into(), 20)],
286 vec![(ILdmr, 20), (DLdmr, 10), (DLdmw, 10)],
287 )]
288 fn test_try_from_regression_config_for_hard_limits<T, U, V>(
289 #[case] hard_limits: Vec<(T, U)>,
290 #[case] expected_hard_limits: Vec<(EventKind, V)>,
291 ) where
292 T: Into<CallgrindMetrics>,
293 U: Into<Limit>,
294 V: Into<Metric>,
295 {
296 let expected = CallgrindRegressionConfig {
297 soft_limits: if hard_limits.is_empty() {
298 vec![(EventKind::Ir, 10f64)]
299 } else {
300 Vec::default()
301 },
302 hard_limits: expected_hard_limits
303 .into_iter()
304 .map(|(m, l)| (m, l.into()))
305 .collect::<Vec<(EventKind, Metric)>>(),
306 fail_fast: false,
307 };
308 let api_regression_config = api::CallgrindRegressionConfig {
309 soft_limits: Vec::default(),
310 hard_limits: hard_limits
311 .into_iter()
312 .map(|(m, l)| (m.into(), l.into()))
313 .collect(),
314 fail_fast: Option::default(),
315 };
316
317 assert_eq!(
318 CallgrindRegressionConfig::try_from(api_regression_config).unwrap(),
319 expected
320 );
321 }
322
323 #[test]
324 fn test_try_from_regression_config_for_hard_limits_then_error() {
325 let api_regression_config = api::CallgrindRegressionConfig {
326 soft_limits: Vec::default(),
327 hard_limits: vec![(EventKind::Ir.into(), Limit::Float(10f64))],
328 fail_fast: Option::default(),
329 };
330
331 CallgrindRegressionConfig::try_from(api_regression_config).unwrap_err();
332
333 let api_regression_config = api::CallgrindRegressionConfig {
334 soft_limits: Vec::default(),
335 hard_limits: vec![(CallgrindMetrics::All, Limit::Float(10f64))],
336 fail_fast: Option::default(),
337 };
338
339 CallgrindRegressionConfig::try_from(api_regression_config).unwrap_err();
340 }
341}