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