1use crate::analysis::{false_sharing, locality, padding, reorder, scorer};
4use crate::ir::{PaddingGap, SharingConflict, StructLayout};
5
6#[derive(Debug, Clone, PartialEq, serde::Serialize)]
7pub enum Severity {
8 Low,
9 Medium,
10 High,
11}
12
13#[derive(Debug, Clone, serde::Serialize)]
14#[serde(tag = "kind")]
15pub enum Finding {
16 PaddingWaste {
17 struct_name: String,
18 total_size: usize,
19 wasted_bytes: usize,
20 waste_pct: f64,
21 gaps: Vec<PaddingGap>,
22 severity: Severity,
23 },
24 FalseSharing {
25 struct_name: String,
26 conflicts: Vec<SharingConflict>,
27 severity: Severity,
28 },
29 ReorderSuggestion {
30 struct_name: String,
31 original_size: usize,
32 optimized_size: usize,
33 savings: usize,
34 suggested_order: Vec<String>,
35 severity: Severity,
36 },
37 LocalityIssue {
38 struct_name: String,
39 hot_fields: Vec<String>,
40 cold_fields: Vec<String>,
41 severity: Severity,
42 },
43}
44
45impl Finding {
46 pub fn severity(&self) -> &Severity {
47 match self {
48 Finding::PaddingWaste { severity, .. } => severity,
49 Finding::FalseSharing { severity, .. } => severity,
50 Finding::ReorderSuggestion { severity, .. } => severity,
51 Finding::LocalityIssue { severity, .. } => severity,
52 }
53 }
54
55 pub fn struct_name(&self) -> &str {
56 match self {
57 Finding::PaddingWaste { struct_name, .. } => struct_name,
58 Finding::FalseSharing { struct_name, .. } => struct_name,
59 Finding::ReorderSuggestion { struct_name, .. } => struct_name,
60 Finding::LocalityIssue { struct_name, .. } => struct_name,
61 }
62 }
63
64 pub fn kind_name(&self) -> &'static str {
69 match self {
70 Finding::PaddingWaste { .. } => "PaddingWaste",
71 Finding::FalseSharing { .. } => "FalseSharing",
72 Finding::ReorderSuggestion { .. } => "ReorderSuggestion",
73 Finding::LocalityIssue { .. } => "LocalityIssue",
74 }
75 }
76}
77
78#[derive(Debug, serde::Serialize)]
79pub struct StructReport {
80 pub struct_name: String,
81 pub source_file: Option<String>,
82 pub source_line: Option<u32>,
83 pub total_size: usize,
84 pub num_fields: usize,
86 pub num_holes: usize,
88 pub wasted_bytes: usize,
89 pub score: f64,
90 pub findings: Vec<Finding>,
91 pub is_repr_rust: bool,
94}
95
96#[derive(Debug, serde::Serialize)]
97pub struct Report {
98 pub structs: Vec<StructReport>,
99 pub total_structs: usize,
100 pub total_wasted_bytes: usize,
101 #[serde(skip_serializing_if = "Vec::is_empty")]
103 pub analyzed_paths: Vec<String>,
104}
105
106impl Report {
107 pub fn from_layouts(layouts: &[StructLayout]) -> Report {
109 let structs: Vec<StructReport> = layouts.iter().map(analyze_one).collect();
110 let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
111 Report {
112 total_structs: structs.len(),
113 total_wasted_bytes,
114 structs,
115 analyzed_paths: Vec::new(),
116 }
117 }
118}
119
120fn analyze_one(layout: &StructLayout) -> StructReport {
121 let mut findings = Vec::new();
122
123 let gaps = padding::find_padding(layout);
125 let num_holes = gaps.len();
126 let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
127 if wasted > 0 {
129 let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
130 let severity = if waste_pct >= 30.0 {
131 Severity::High
132 } else if waste_pct >= 10.0 {
133 Severity::Medium
134 } else {
135 Severity::Low
136 };
137 findings.push(Finding::PaddingWaste {
138 struct_name: layout.name.clone(),
139 total_size: layout.total_size,
140 wasted_bytes: wasted,
141 waste_pct,
142 gaps,
143 severity,
144 });
145 }
146
147 let (optimized_size, savings) = reorder::reorder_savings(layout);
150 if savings > 0 && !layout.is_packed && !layout.is_union {
151 let suggested_order = reorder::optimal_order(layout)
152 .iter()
153 .map(|f| f.name.clone())
154 .collect();
155 findings.push(Finding::ReorderSuggestion {
156 struct_name: layout.name.clone(),
157 original_size: layout.total_size,
158 optimized_size,
159 savings,
160 suggested_order,
161 severity: if savings >= 8 {
162 Severity::High
163 } else {
164 Severity::Medium
165 },
166 });
167 }
168
169 if !layout.is_union && false_sharing::has_false_sharing(layout) {
172 let conflicts = false_sharing::find_sharing_conflicts(layout);
173 findings.push(Finding::FalseSharing {
174 struct_name: layout.name.clone(),
175 conflicts,
176 severity: Severity::High,
177 });
178 }
179
180 if locality::has_locality_issue(layout) {
182 let (hot, cold) = locality::partition_hot_cold(layout);
183 findings.push(Finding::LocalityIssue {
184 struct_name: layout.name.clone(),
185 hot_fields: hot,
186 cold_fields: cold,
187 severity: Severity::Medium,
188 });
189 }
190
191 if !layout.suppressed_findings.is_empty() {
195 findings.retain(|f| {
196 !layout
197 .suppressed_findings
198 .contains(&f.kind_name().to_string())
199 });
200 }
201
202 let score = scorer::score(layout);
203
204 StructReport {
205 struct_name: layout.name.clone(),
206 source_file: layout.source_file.clone(),
207 source_line: layout.source_line,
208 total_size: layout.total_size,
209 num_fields: layout.fields.len(),
210 num_holes,
211 wasted_bytes: wasted,
212 score,
213 findings,
214 is_repr_rust: layout.is_repr_rust,
215 }
216}
217
218#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::ir::test_fixtures::{connection_layout, packed_layout};
224
225 #[test]
226 fn report_from_misaligned_has_padding_finding() {
227 let report = Report::from_layouts(&[connection_layout()]);
228 assert_eq!(report.total_structs, 1);
229 let sr = &report.structs[0];
230 assert!(sr.wasted_bytes > 0);
231 assert!(
232 sr.findings
233 .iter()
234 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
235 );
236 }
237
238 #[test]
239 fn report_from_packed_has_no_padding_finding() {
240 let report = Report::from_layouts(&[packed_layout()]);
241 let sr = &report.structs[0];
242 assert_eq!(sr.wasted_bytes, 0);
243 assert!(
244 !sr.findings
245 .iter()
246 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
247 );
248 }
249
250 #[test]
251 fn report_from_misaligned_has_reorder_suggestion() {
252 let report = Report::from_layouts(&[connection_layout()]);
253 let sr = &report.structs[0];
254 assert!(
255 sr.findings
256 .iter()
257 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
258 );
259 }
260
261 #[test]
262 fn severity_high_when_waste_over_30_pct() {
263 let report = Report::from_layouts(&[connection_layout()]);
264 let sr = &report.structs[0];
265 let padding_finding = sr
267 .findings
268 .iter()
269 .find(|f| matches!(f, Finding::PaddingWaste { .. }))
270 .unwrap();
271 assert_eq!(padding_finding.severity(), &Severity::High);
272 }
273
274 #[test]
275 fn total_wasted_bytes_sums_across_structs() {
276 let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
277 assert_eq!(report.total_structs, 2);
278 assert_eq!(report.total_wasted_bytes, 10); }
280
281 #[test]
282 fn suppressed_finding_kind_not_in_report() {
283 let mut layout = connection_layout();
284 layout.suppressed_findings = vec!["ReorderSuggestion".to_string()];
285 let report = Report::from_layouts(&[layout]);
286 let sr = &report.structs[0];
287 assert!(
289 sr.findings
290 .iter()
291 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
292 );
293 assert!(
295 !sr.findings
296 .iter()
297 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
298 );
299 }
300
301 #[test]
302 fn suppressing_all_findings_yields_empty_findings() {
303 let mut layout = connection_layout();
304 layout.suppressed_findings = vec![
305 "PaddingWaste".to_string(),
306 "ReorderSuggestion".to_string(),
307 "FalseSharing".to_string(),
308 "LocalityIssue".to_string(),
309 ];
310 let report = Report::from_layouts(&[layout]);
311 assert!(report.structs[0].findings.is_empty());
312 }
313}