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
65#[derive(Debug, serde::Serialize)]
66pub struct StructReport {
67 pub struct_name: String,
68 pub source_file: Option<String>,
69 pub source_line: Option<u32>,
70 pub total_size: usize,
71 pub num_fields: usize,
73 pub num_holes: usize,
75 pub wasted_bytes: usize,
76 pub score: f64,
77 pub findings: Vec<Finding>,
78}
79
80#[derive(Debug, serde::Serialize)]
81pub struct Report {
82 pub structs: Vec<StructReport>,
83 pub total_structs: usize,
84 pub total_wasted_bytes: usize,
85 #[serde(skip_serializing_if = "Vec::is_empty")]
87 pub analyzed_paths: Vec<String>,
88}
89
90impl Report {
91 pub fn from_layouts(layouts: &[StructLayout]) -> Report {
93 let structs: Vec<StructReport> = layouts.iter().map(analyze_one).collect();
94 let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
95 Report {
96 total_structs: structs.len(),
97 total_wasted_bytes,
98 structs,
99 analyzed_paths: Vec::new(),
100 }
101 }
102}
103
104fn analyze_one(layout: &StructLayout) -> StructReport {
105 let mut findings = Vec::new();
106
107 let gaps = padding::find_padding(layout);
109 let num_holes = gaps.len();
110 let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
111 if wasted > 0 {
113 let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
114 let severity = if waste_pct >= 30.0 {
115 Severity::High
116 } else if waste_pct >= 10.0 {
117 Severity::Medium
118 } else {
119 Severity::Low
120 };
121 findings.push(Finding::PaddingWaste {
122 struct_name: layout.name.clone(),
123 total_size: layout.total_size,
124 wasted_bytes: wasted,
125 waste_pct,
126 gaps,
127 severity,
128 });
129 }
130
131 let (optimized_size, savings) = reorder::reorder_savings(layout);
134 if savings > 0 && !layout.is_packed && !layout.is_union {
135 let suggested_order = reorder::optimal_order(layout)
136 .iter()
137 .map(|f| f.name.clone())
138 .collect();
139 findings.push(Finding::ReorderSuggestion {
140 struct_name: layout.name.clone(),
141 original_size: layout.total_size,
142 optimized_size,
143 savings,
144 suggested_order,
145 severity: if savings >= 8 {
146 Severity::High
147 } else {
148 Severity::Medium
149 },
150 });
151 }
152
153 if !layout.is_union && false_sharing::has_false_sharing(layout) {
156 let conflicts = false_sharing::find_sharing_conflicts(layout);
157 findings.push(Finding::FalseSharing {
158 struct_name: layout.name.clone(),
159 conflicts,
160 severity: Severity::High,
161 });
162 }
163
164 if locality::has_locality_issue(layout) {
166 let (hot, cold) = locality::partition_hot_cold(layout);
167 findings.push(Finding::LocalityIssue {
168 struct_name: layout.name.clone(),
169 hot_fields: hot,
170 cold_fields: cold,
171 severity: Severity::Medium,
172 });
173 }
174
175 let score = scorer::score(layout);
176
177 StructReport {
178 struct_name: layout.name.clone(),
179 source_file: layout.source_file.clone(),
180 source_line: layout.source_line,
181 total_size: layout.total_size,
182 num_fields: layout.fields.len(),
183 num_holes,
184 wasted_bytes: wasted,
185 score,
186 findings,
187 }
188}
189
190#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::ir::test_fixtures::{connection_layout, packed_layout};
196
197 #[test]
198 fn report_from_misaligned_has_padding_finding() {
199 let report = Report::from_layouts(&[connection_layout()]);
200 assert_eq!(report.total_structs, 1);
201 let sr = &report.structs[0];
202 assert!(sr.wasted_bytes > 0);
203 assert!(sr
204 .findings
205 .iter()
206 .any(|f| matches!(f, Finding::PaddingWaste { .. })));
207 }
208
209 #[test]
210 fn report_from_packed_has_no_padding_finding() {
211 let report = Report::from_layouts(&[packed_layout()]);
212 let sr = &report.structs[0];
213 assert_eq!(sr.wasted_bytes, 0);
214 assert!(!sr
215 .findings
216 .iter()
217 .any(|f| matches!(f, Finding::PaddingWaste { .. })));
218 }
219
220 #[test]
221 fn report_from_misaligned_has_reorder_suggestion() {
222 let report = Report::from_layouts(&[connection_layout()]);
223 let sr = &report.structs[0];
224 assert!(sr
225 .findings
226 .iter()
227 .any(|f| matches!(f, Finding::ReorderSuggestion { .. })));
228 }
229
230 #[test]
231 fn severity_high_when_waste_over_30_pct() {
232 let report = Report::from_layouts(&[connection_layout()]);
233 let sr = &report.structs[0];
234 let padding_finding = sr
236 .findings
237 .iter()
238 .find(|f| matches!(f, Finding::PaddingWaste { .. }))
239 .unwrap();
240 assert_eq!(padding_finding.severity(), &Severity::High);
241 }
242
243 #[test]
244 fn total_wasted_bytes_sums_across_structs() {
245 let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
246 assert_eq!(report.total_structs, 2);
247 assert_eq!(report.total_wasted_bytes, 10); }
249}