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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub uncertain_fields: Vec<String>,
118}
119
120#[derive(Debug, serde::Serialize)]
121pub struct Report {
122 pub structs: Vec<StructReport>,
123 pub total_structs: usize,
124 pub total_wasted_bytes: usize,
125 #[serde(skip_serializing_if = "Vec::is_empty")]
127 pub analyzed_paths: Vec<String>,
128 #[serde(skip)]
132 pub embedded_in: std::collections::HashMap<String, Vec<String>>,
133}
134
135impl Report {
136 pub fn from_layouts(layouts: &[StructLayout]) -> Report {
138 let structs: Vec<StructReport> = layouts.iter().map(analyze_one).collect();
139 let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
140
141 let struct_names: std::collections::HashSet<&str> =
144 structs.iter().map(|s| s.struct_name.as_str()).collect();
145 let mut embedded_in: std::collections::HashMap<String, Vec<String>> =
146 std::collections::HashMap::new();
147 for layout in layouts {
148 for field in &layout.fields {
149 let inner_name = match &field.ty {
150 crate::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
151 _ => continue,
152 };
153 if struct_names.contains(inner_name) {
154 embedded_in
155 .entry(inner_name.to_owned())
156 .or_default()
157 .push(layout.name.clone());
158 }
159 }
160 }
161
162 Report {
163 total_structs: structs.len(),
164 total_wasted_bytes,
165 structs,
166 analyzed_paths: Vec::new(),
167 embedded_in,
168 }
169 }
170}
171
172fn analyze_one(layout: &StructLayout) -> StructReport {
173 let mut findings = Vec::new();
174
175 let gaps = padding::find_padding(layout);
177 let num_holes = gaps.len();
178 let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
179 if wasted > 0 {
181 let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
182 let mut severity = if waste_pct >= 30.0 || wasted >= 32 {
186 Severity::High
187 } else if waste_pct >= 10.0 || wasted >= 8 {
188 Severity::Medium
189 } else {
190 Severity::Low
191 };
192 if layout.is_repr_rust {
196 severity = severity.downgrade();
197 }
198 findings.push(Finding::PaddingWaste {
199 struct_name: layout.name.clone(),
200 total_size: layout.total_size,
201 wasted_bytes: wasted,
202 waste_pct,
203 gaps,
204 severity,
205 });
206 }
207
208 let (optimized_size, savings) = reorder::reorder_savings(layout);
211 if savings > 0 && !layout.is_packed && !layout.is_union {
212 let suggested_order = reorder::optimal_order(layout)
213 .iter()
214 .map(|f| f.name.clone())
215 .collect();
216 let severity = if layout.is_repr_rust {
220 Severity::Medium
221 } else if savings >= 8 {
222 Severity::High
223 } else {
224 Severity::Medium
225 };
226 findings.push(Finding::ReorderSuggestion {
227 struct_name: layout.name.clone(),
228 original_size: layout.total_size,
229 optimized_size,
230 savings,
231 suggested_order,
232 severity,
233 });
234 }
235
236 if !layout.is_union && false_sharing::has_false_sharing(layout) {
239 let conflicts = false_sharing::find_sharing_conflicts(layout);
240 let is_inferred = !layout.fields.iter().any(|f| {
242 matches!(
243 f.access,
244 AccessPattern::Concurrent {
245 is_annotated: true,
246 ..
247 }
248 )
249 });
250 findings.push(Finding::FalseSharing {
251 struct_name: layout.name.clone(),
252 conflicts,
253 severity: Severity::High,
254 is_inferred,
255 });
256 }
257
258 if locality::has_locality_issue(layout) {
260 let (hot, cold) = locality::partition_hot_cold(layout);
261 let is_inferred = !layout.fields.iter().any(|f| {
265 matches!(
266 f.access,
267 AccessPattern::Concurrent {
268 is_annotated: true,
269 ..
270 }
271 )
272 });
273 findings.push(Finding::LocalityIssue {
274 struct_name: layout.name.clone(),
275 hot_fields: hot,
276 cold_fields: cold,
277 severity: Severity::Medium,
278 is_inferred,
279 });
280 }
281
282 if !layout.suppressed_findings.is_empty() {
286 findings.retain(|f| {
287 !layout
288 .suppressed_findings
289 .contains(&f.kind_name().to_string())
290 });
291 }
292
293 let score = scorer::score(layout);
294
295 StructReport {
296 struct_name: layout.name.clone(),
297 source_file: layout.source_file.clone(),
298 source_line: layout.source_line,
299 total_size: layout.total_size,
300 num_fields: layout.fields.len(),
301 num_holes,
302 wasted_bytes: wasted,
303 score,
304 findings,
305 is_repr_rust: layout.is_repr_rust,
306 uncertain_fields: layout.uncertain_fields.clone(),
307 }
308}
309
310#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::ir::test_fixtures::{connection_layout, packed_layout};
316
317 #[test]
318 fn report_from_misaligned_has_padding_finding() {
319 let report = Report::from_layouts(&[connection_layout()]);
320 assert_eq!(report.total_structs, 1);
321 let sr = &report.structs[0];
322 assert!(sr.wasted_bytes > 0);
323 assert!(
324 sr.findings
325 .iter()
326 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
327 );
328 }
329
330 #[test]
331 fn report_from_packed_has_no_padding_finding() {
332 let report = Report::from_layouts(&[packed_layout()]);
333 let sr = &report.structs[0];
334 assert_eq!(sr.wasted_bytes, 0);
335 assert!(
336 !sr.findings
337 .iter()
338 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
339 );
340 }
341
342 #[test]
343 fn report_from_misaligned_has_reorder_suggestion() {
344 let report = Report::from_layouts(&[connection_layout()]);
345 let sr = &report.structs[0];
346 assert!(
347 sr.findings
348 .iter()
349 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
350 );
351 }
352
353 #[test]
354 fn severity_high_when_waste_over_30_pct() {
355 let report = Report::from_layouts(&[connection_layout()]);
356 let sr = &report.structs[0];
357 let padding_finding = sr
359 .findings
360 .iter()
361 .find(|f| matches!(f, Finding::PaddingWaste { .. }))
362 .unwrap();
363 assert_eq!(padding_finding.severity(), &Severity::High);
364 }
365
366 #[test]
367 fn total_wasted_bytes_sums_across_structs() {
368 let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
369 assert_eq!(report.total_structs, 2);
370 assert_eq!(report.total_wasted_bytes, 10); }
372
373 #[test]
374 fn suppressed_finding_kind_not_in_report() {
375 let mut layout = connection_layout();
376 layout.suppressed_findings = vec!["ReorderSuggestion".to_string()];
377 let report = Report::from_layouts(&[layout]);
378 let sr = &report.structs[0];
379 assert!(
381 sr.findings
382 .iter()
383 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
384 );
385 assert!(
387 !sr.findings
388 .iter()
389 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
390 );
391 }
392
393 #[test]
394 fn suppressing_all_findings_yields_empty_findings() {
395 let mut layout = connection_layout();
396 layout.suppressed_findings = vec![
397 "PaddingWaste".to_string(),
398 "ReorderSuggestion".to_string(),
399 "FalseSharing".to_string(),
400 "LocalityIssue".to_string(),
401 ];
402 let report = Report::from_layouts(&[layout]);
403 assert!(report.structs[0].findings.is_empty());
404 }
405}