1use crate::analysis::{false_sharing, locality, padding, reorder, scorer};
4use crate::ir::{AccessPattern, PaddingGap, SharingConflict, StructLayout};
5
6#[derive(Debug, Clone, PartialEq, serde::Serialize)]
7pub enum Severity {
8 Low,
9 Medium,
10 High,
11}
12
13impl Severity {
14 pub fn downgrade(self) -> Self {
16 match self {
17 Severity::High => Severity::Medium,
18 Severity::Medium => Severity::Low,
19 Severity::Low => Severity::Low,
20 }
21 }
22}
23
24#[derive(Debug, Clone, serde::Serialize)]
25#[serde(tag = "kind")]
26pub enum Finding {
27 PaddingWaste {
28 struct_name: String,
29 total_size: usize,
30 wasted_bytes: usize,
31 waste_pct: f64,
32 gaps: Vec<PaddingGap>,
33 severity: Severity,
34 },
35 FalseSharing {
36 struct_name: String,
37 conflicts: Vec<SharingConflict>,
38 severity: Severity,
39 is_inferred: bool,
44 },
45 ReorderSuggestion {
46 struct_name: String,
47 original_size: usize,
48 optimized_size: usize,
49 savings: usize,
50 suggested_order: Vec<String>,
51 severity: Severity,
52 },
53 LocalityIssue {
54 struct_name: String,
55 hot_fields: Vec<String>,
56 cold_fields: Vec<String>,
57 severity: Severity,
58 is_inferred: bool,
60 },
61}
62
63impl Finding {
64 pub fn severity(&self) -> &Severity {
65 match self {
66 Finding::PaddingWaste { severity, .. } => severity,
67 Finding::FalseSharing { severity, .. } => severity,
68 Finding::ReorderSuggestion { severity, .. } => severity,
69 Finding::LocalityIssue { severity, .. } => severity,
70 }
71 }
72
73 pub fn struct_name(&self) -> &str {
74 match self {
75 Finding::PaddingWaste { struct_name, .. } => struct_name,
76 Finding::FalseSharing { struct_name, .. } => struct_name,
77 Finding::ReorderSuggestion { struct_name, .. } => struct_name,
78 Finding::LocalityIssue { struct_name, .. } => struct_name,
79 }
80 }
81
82 pub fn kind_name(&self) -> &'static str {
87 match self {
88 Finding::PaddingWaste { .. } => "PaddingWaste",
89 Finding::FalseSharing { .. } => "FalseSharing",
90 Finding::ReorderSuggestion { .. } => "ReorderSuggestion",
91 Finding::LocalityIssue { .. } => "LocalityIssue",
92 }
93 }
94}
95
96#[derive(Debug, serde::Serialize)]
97pub struct StructReport {
98 pub struct_name: String,
99 pub source_file: Option<String>,
100 pub source_line: Option<u32>,
101 pub total_size: usize,
102 pub num_fields: usize,
104 pub num_holes: usize,
106 pub wasted_bytes: usize,
107 pub score: f64,
108 pub findings: Vec<Finding>,
109 pub is_repr_rust: bool,
112}
113
114#[derive(Debug, serde::Serialize)]
115pub struct Report {
116 pub structs: Vec<StructReport>,
117 pub total_structs: usize,
118 pub total_wasted_bytes: usize,
119 #[serde(skip_serializing_if = "Vec::is_empty")]
121 pub analyzed_paths: Vec<String>,
122}
123
124impl Report {
125 pub fn from_layouts(layouts: &[StructLayout]) -> Report {
127 let structs: Vec<StructReport> = layouts.iter().map(analyze_one).collect();
128 let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
129 Report {
130 total_structs: structs.len(),
131 total_wasted_bytes,
132 structs,
133 analyzed_paths: Vec::new(),
134 }
135 }
136}
137
138fn analyze_one(layout: &StructLayout) -> StructReport {
139 let mut findings = Vec::new();
140
141 let gaps = padding::find_padding(layout);
143 let num_holes = gaps.len();
144 let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
145 if wasted > 0 {
147 let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
148 let mut severity = if waste_pct >= 30.0 {
149 Severity::High
150 } else if waste_pct >= 10.0 {
151 Severity::Medium
152 } else {
153 Severity::Low
154 };
155 if layout.is_repr_rust {
159 severity = severity.downgrade();
160 }
161 findings.push(Finding::PaddingWaste {
162 struct_name: layout.name.clone(),
163 total_size: layout.total_size,
164 wasted_bytes: wasted,
165 waste_pct,
166 gaps,
167 severity,
168 });
169 }
170
171 let (optimized_size, savings) = reorder::reorder_savings(layout);
174 if savings > 0 && !layout.is_packed && !layout.is_union {
175 let suggested_order = reorder::optimal_order(layout)
176 .iter()
177 .map(|f| f.name.clone())
178 .collect();
179 let severity = if layout.is_repr_rust {
183 Severity::Medium
184 } else if savings >= 8 {
185 Severity::High
186 } else {
187 Severity::Medium
188 };
189 findings.push(Finding::ReorderSuggestion {
190 struct_name: layout.name.clone(),
191 original_size: layout.total_size,
192 optimized_size,
193 savings,
194 suggested_order,
195 severity,
196 });
197 }
198
199 if !layout.is_union && false_sharing::has_false_sharing(layout) {
202 let conflicts = false_sharing::find_sharing_conflicts(layout);
203 let is_inferred = !layout.fields.iter().any(|f| {
205 matches!(
206 f.access,
207 AccessPattern::Concurrent {
208 is_annotated: true,
209 ..
210 }
211 )
212 });
213 findings.push(Finding::FalseSharing {
214 struct_name: layout.name.clone(),
215 conflicts,
216 severity: Severity::High,
217 is_inferred,
218 });
219 }
220
221 if locality::has_locality_issue(layout) {
223 let (hot, cold) = locality::partition_hot_cold(layout);
224 let is_inferred = !layout.fields.iter().any(|f| {
228 matches!(
229 f.access,
230 AccessPattern::Concurrent {
231 is_annotated: true,
232 ..
233 }
234 )
235 });
236 findings.push(Finding::LocalityIssue {
237 struct_name: layout.name.clone(),
238 hot_fields: hot,
239 cold_fields: cold,
240 severity: Severity::Medium,
241 is_inferred,
242 });
243 }
244
245 if !layout.suppressed_findings.is_empty() {
249 findings.retain(|f| {
250 !layout
251 .suppressed_findings
252 .contains(&f.kind_name().to_string())
253 });
254 }
255
256 let score = scorer::score(layout);
257
258 StructReport {
259 struct_name: layout.name.clone(),
260 source_file: layout.source_file.clone(),
261 source_line: layout.source_line,
262 total_size: layout.total_size,
263 num_fields: layout.fields.len(),
264 num_holes,
265 wasted_bytes: wasted,
266 score,
267 findings,
268 is_repr_rust: layout.is_repr_rust,
269 }
270}
271
272#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::ir::test_fixtures::{connection_layout, packed_layout};
278
279 #[test]
280 fn report_from_misaligned_has_padding_finding() {
281 let report = Report::from_layouts(&[connection_layout()]);
282 assert_eq!(report.total_structs, 1);
283 let sr = &report.structs[0];
284 assert!(sr.wasted_bytes > 0);
285 assert!(
286 sr.findings
287 .iter()
288 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
289 );
290 }
291
292 #[test]
293 fn report_from_packed_has_no_padding_finding() {
294 let report = Report::from_layouts(&[packed_layout()]);
295 let sr = &report.structs[0];
296 assert_eq!(sr.wasted_bytes, 0);
297 assert!(
298 !sr.findings
299 .iter()
300 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
301 );
302 }
303
304 #[test]
305 fn report_from_misaligned_has_reorder_suggestion() {
306 let report = Report::from_layouts(&[connection_layout()]);
307 let sr = &report.structs[0];
308 assert!(
309 sr.findings
310 .iter()
311 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
312 );
313 }
314
315 #[test]
316 fn severity_high_when_waste_over_30_pct() {
317 let report = Report::from_layouts(&[connection_layout()]);
318 let sr = &report.structs[0];
319 let padding_finding = sr
321 .findings
322 .iter()
323 .find(|f| matches!(f, Finding::PaddingWaste { .. }))
324 .unwrap();
325 assert_eq!(padding_finding.severity(), &Severity::High);
326 }
327
328 #[test]
329 fn total_wasted_bytes_sums_across_structs() {
330 let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
331 assert_eq!(report.total_structs, 2);
332 assert_eq!(report.total_wasted_bytes, 10); }
334
335 #[test]
336 fn suppressed_finding_kind_not_in_report() {
337 let mut layout = connection_layout();
338 layout.suppressed_findings = vec!["ReorderSuggestion".to_string()];
339 let report = Report::from_layouts(&[layout]);
340 let sr = &report.structs[0];
341 assert!(
343 sr.findings
344 .iter()
345 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
346 );
347 assert!(
349 !sr.findings
350 .iter()
351 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
352 );
353 }
354
355 #[test]
356 fn suppressing_all_findings_yields_empty_findings() {
357 let mut layout = connection_layout();
358 layout.suppressed_findings = vec![
359 "PaddingWaste".to_string(),
360 "ReorderSuggestion".to_string(),
361 "FalseSharing".to_string(),
362 "LocalityIssue".to_string(),
363 ];
364 let report = Report::from_layouts(&[layout]);
365 assert!(report.structs[0].findings.is_empty());
366 }
367}