1use dev_report::{CheckResult, Evidence, Severity};
41
42#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct AllocationStats {
48 pub total_bytes: u64,
50 pub total_blocks: u64,
52 pub peak_bytes: u64,
54 pub peak_blocks: u64,
56}
57
58impl AllocationStats {
59 pub fn snapshot() -> Self {
64 let s = dhat::HeapStats::get();
65 Self {
66 total_bytes: s.total_bytes,
67 total_blocks: s.total_blocks,
68 peak_bytes: s.max_bytes as u64,
69 peak_blocks: s.max_blocks as u64,
70 }
71 }
72
73 pub fn compare_against_baseline(
79 &self,
80 name: &str,
81 baseline: Option<AllocationStats>,
82 pct_threshold: f64,
83 ) -> CheckResult {
84 let check_name = format!("alloc::{}", name);
85 let mut evidence = vec![
86 Evidence::numeric("total_bytes", self.total_bytes as f64),
87 Evidence::numeric("total_blocks", self.total_blocks as f64),
88 Evidence::numeric("peak_bytes", self.peak_bytes as f64),
89 Evidence::numeric("peak_blocks", self.peak_blocks as f64),
90 ];
91
92 let Some(base) = baseline else {
93 let mut c = CheckResult::skip(check_name).with_detail("no allocation baseline");
94 c.tags = vec!["alloc".to_string()];
95 c.evidence = evidence;
96 return c;
97 };
98
99 evidence.push(Evidence::numeric(
100 "baseline_total_bytes",
101 base.total_bytes as f64,
102 ));
103 let allowed = base.total_bytes as f64 * (1.0 + pct_threshold / 100.0);
104 let regressed = (self.total_bytes as f64) > allowed;
105 let detail = format!(
106 "current_total_bytes={} baseline_total_bytes={} threshold_pct={}",
107 self.total_bytes, base.total_bytes, pct_threshold
108 );
109 if regressed {
110 let mut c = CheckResult::fail(check_name, Severity::Warning).with_detail(detail);
111 c.tags = vec!["alloc".to_string(), "regression".to_string()];
112 c.evidence = evidence;
113 c
114 } else {
115 let mut c = CheckResult::pass(check_name).with_detail(detail);
116 c.tags = vec!["alloc".to_string()];
117 c.evidence = evidence;
118 c
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use dev_report::Verdict;
127
128 fn synthetic(total_bytes: u64) -> AllocationStats {
129 AllocationStats {
130 total_bytes,
131 total_blocks: 10,
132 peak_bytes: total_bytes,
133 peak_blocks: 5,
134 }
135 }
136
137 #[test]
138 fn no_baseline_skips() {
139 let s = synthetic(1_000);
140 let c = s.compare_against_baseline("x", None, 10.0);
141 assert_eq!(c.verdict, Verdict::Skip);
142 assert!(c.has_tag("alloc"));
143 }
144
145 #[test]
146 fn within_threshold_passes() {
147 let curr = synthetic(105);
148 let base = synthetic(100);
149 let c = curr.compare_against_baseline("x", Some(base), 10.0);
150 assert_eq!(c.verdict, Verdict::Pass);
151 }
152
153 #[test]
154 fn over_threshold_fails() {
155 let curr = synthetic(120);
156 let base = synthetic(100);
157 let c = curr.compare_against_baseline("x", Some(base), 10.0);
158 assert_eq!(c.verdict, Verdict::Fail);
159 assert!(c.has_tag("regression"));
160 }
161
162 #[test]
163 fn evidence_includes_all_metrics() {
164 let curr = synthetic(100);
165 let c = curr.compare_against_baseline("x", None, 10.0);
166 let labels: Vec<&str> = c.evidence.iter().map(|e| e.label.as_str()).collect();
167 assert!(labels.contains(&"total_bytes"));
168 assert!(labels.contains(&"total_blocks"));
169 assert!(labels.contains(&"peak_bytes"));
170 assert!(labels.contains(&"peak_blocks"));
171 }
172}