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