1use crate::analysis::{false_sharing, locality, padding, reorder, scorer};
4use crate::ir::{AccessPattern, PaddingGap, SharingConflict, StructLayout};
5use rayon::prelude::*;
6
7#[derive(Debug, Clone, PartialEq, serde::Serialize)]
8pub enum Severity {
9 Low,
10 Medium,
11 High,
12}
13
14impl Severity {
15 pub fn downgrade(self) -> Self {
17 match self {
18 Severity::High => Severity::Medium,
19 Severity::Medium => Severity::Low,
20 Severity::Low => Severity::Low,
21 }
22 }
23}
24
25#[derive(Debug, Clone, serde::Serialize)]
26#[serde(tag = "kind")]
27pub enum Finding {
28 PaddingWaste {
29 struct_name: String,
30 total_size: usize,
31 wasted_bytes: usize,
32 waste_pct: f64,
33 gaps: Vec<PaddingGap>,
34 severity: Severity,
35 },
36 FalseSharing {
37 struct_name: String,
38 conflicts: Vec<SharingConflict>,
39 severity: Severity,
40 is_inferred: bool,
45 },
46 ReorderSuggestion {
47 struct_name: String,
48 original_size: usize,
49 optimized_size: usize,
50 savings: usize,
51 suggested_order: Vec<String>,
52 severity: Severity,
53 },
54 LocalityIssue {
55 struct_name: String,
56 hot_fields: Vec<String>,
57 cold_fields: Vec<String>,
58 severity: Severity,
59 is_inferred: bool,
61 },
62}
63
64impl Finding {
65 pub fn severity(&self) -> &Severity {
66 match self {
67 Finding::PaddingWaste { severity, .. } => severity,
68 Finding::FalseSharing { severity, .. } => severity,
69 Finding::ReorderSuggestion { severity, .. } => severity,
70 Finding::LocalityIssue { severity, .. } => severity,
71 }
72 }
73
74 pub fn struct_name(&self) -> &str {
75 match self {
76 Finding::PaddingWaste { struct_name, .. } => struct_name,
77 Finding::FalseSharing { struct_name, .. } => struct_name,
78 Finding::ReorderSuggestion { struct_name, .. } => struct_name,
79 Finding::LocalityIssue { struct_name, .. } => struct_name,
80 }
81 }
82
83 pub fn kind_name(&self) -> &'static str {
88 match self {
89 Finding::PaddingWaste { .. } => "PaddingWaste",
90 Finding::FalseSharing { .. } => "FalseSharing",
91 Finding::ReorderSuggestion { .. } => "ReorderSuggestion",
92 Finding::LocalityIssue { .. } => "LocalityIssue",
93 }
94 }
95}
96
97#[derive(Debug, serde::Serialize)]
98pub struct StructReport {
99 pub struct_name: String,
100 pub source_file: Option<String>,
101 pub source_line: Option<u32>,
102 pub total_size: usize,
103 pub num_fields: usize,
105 pub num_holes: usize,
107 pub wasted_bytes: usize,
108 pub score: f64,
109 pub findings: Vec<Finding>,
110 pub is_repr_rust: bool,
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 pub uncertain_fields: Vec<String>,
119}
120
121#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
124pub struct SkippedStruct {
125 pub name: String,
126 pub reason: String,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub source_file: Option<String>,
129}
130
131#[derive(Debug, serde::Serialize)]
132pub struct Report {
133 pub structs: Vec<StructReport>,
134 pub total_structs: usize,
135 pub total_wasted_bytes: usize,
136 #[serde(skip_serializing_if = "Vec::is_empty")]
138 pub analyzed_paths: Vec<String>,
139 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub skipped: Vec<SkippedStruct>,
143 #[serde(skip)]
147 pub embedded_in: std::collections::HashMap<String, Vec<String>>,
148}
149
150impl Report {
151 pub fn from_layouts(layouts: &[StructLayout]) -> Report {
153 let structs: Vec<StructReport> = layouts.par_iter().map(analyze_one).collect();
154 let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
155
156 let struct_names: std::collections::HashSet<&str> =
159 structs.iter().map(|s| s.struct_name.as_str()).collect();
160 let mut embedded_in: std::collections::HashMap<String, Vec<String>> =
161 std::collections::HashMap::new();
162 for layout in layouts {
163 for field in &layout.fields {
164 let inner_name = match &field.ty {
165 crate::ir::TypeInfo::Opaque { name, .. } => name.as_str(),
166 _ => continue,
167 };
168 if struct_names.contains(inner_name) {
169 embedded_in
170 .entry(inner_name.to_owned())
171 .or_default()
172 .push(layout.name.clone());
173 }
174 }
175 }
176
177 Report {
178 total_structs: structs.len(),
179 total_wasted_bytes,
180 structs,
181 analyzed_paths: Vec::new(),
182 skipped: Vec::new(),
183 embedded_in,
184 }
185 }
186}
187
188fn analyze_one(layout: &StructLayout) -> StructReport {
189 let mut findings = Vec::new();
190
191 let gaps = padding::find_padding(layout);
193 let num_holes = gaps.len();
194 let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
195 if wasted > 0 {
197 let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
198 let mut severity = if waste_pct >= 30.0 || wasted >= 32 {
202 Severity::High
203 } else if waste_pct >= 10.0 || wasted >= 8 {
204 Severity::Medium
205 } else {
206 Severity::Low
207 };
208 if layout.is_repr_rust {
212 severity = severity.downgrade();
213 }
214 findings.push(Finding::PaddingWaste {
215 struct_name: layout.name.clone(),
216 total_size: layout.total_size,
217 wasted_bytes: wasted,
218 waste_pct,
219 gaps,
220 severity,
221 });
222 }
223
224 let (optimized_size, savings) = reorder::reorder_savings(layout);
227 if savings > 0 && !layout.is_packed && !layout.is_union {
228 let suggested_order = reorder::optimal_order(layout)
229 .iter()
230 .map(|f| f.name.clone())
231 .collect();
232 let severity = if layout.is_repr_rust {
236 Severity::Medium
237 } else if savings >= 8 {
238 Severity::High
239 } else {
240 Severity::Medium
241 };
242 findings.push(Finding::ReorderSuggestion {
243 struct_name: layout.name.clone(),
244 original_size: layout.total_size,
245 optimized_size,
246 savings,
247 suggested_order,
248 severity,
249 });
250 }
251
252 if !layout.is_union && false_sharing::has_false_sharing(layout) {
255 let conflicts = false_sharing::find_sharing_conflicts(layout);
256 let is_inferred = !layout.fields.iter().any(|f| {
258 matches!(
259 f.access,
260 AccessPattern::Concurrent {
261 is_annotated: true,
262 ..
263 }
264 )
265 });
266 findings.push(Finding::FalseSharing {
267 struct_name: layout.name.clone(),
268 conflicts,
269 severity: Severity::High,
270 is_inferred,
271 });
272 }
273
274 if locality::has_locality_issue(layout) {
276 let (hot, cold) = locality::partition_hot_cold(layout);
277 let is_inferred = !layout.fields.iter().any(|f| {
281 matches!(
282 f.access,
283 AccessPattern::Concurrent {
284 is_annotated: true,
285 ..
286 }
287 )
288 });
289 findings.push(Finding::LocalityIssue {
290 struct_name: layout.name.clone(),
291 hot_fields: hot,
292 cold_fields: cold,
293 severity: Severity::Medium,
294 is_inferred,
295 });
296 }
297
298 if !layout.suppressed_findings.is_empty() {
302 findings.retain(|f| {
303 !layout
304 .suppressed_findings
305 .contains(&f.kind_name().to_string())
306 });
307 }
308
309 let score = scorer::score(layout);
310
311 StructReport {
312 struct_name: layout.name.clone(),
313 source_file: layout.source_file.clone(),
314 source_line: layout.source_line,
315 total_size: layout.total_size,
316 num_fields: layout.fields.len(),
317 num_holes,
318 wasted_bytes: wasted,
319 score,
320 findings,
321 is_repr_rust: layout.is_repr_rust,
322 uncertain_fields: layout.uncertain_fields.clone(),
323 }
324}
325
326#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::ir::test_fixtures::{connection_layout, packed_layout};
332
333 #[test]
334 fn report_from_misaligned_has_padding_finding() {
335 let report = Report::from_layouts(&[connection_layout()]);
336 assert_eq!(report.total_structs, 1);
337 let sr = &report.structs[0];
338 assert!(sr.wasted_bytes > 0);
339 assert!(
340 sr.findings
341 .iter()
342 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
343 );
344 }
345
346 #[test]
347 fn report_from_packed_has_no_padding_finding() {
348 let report = Report::from_layouts(&[packed_layout()]);
349 let sr = &report.structs[0];
350 assert_eq!(sr.wasted_bytes, 0);
351 assert!(
352 !sr.findings
353 .iter()
354 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
355 );
356 }
357
358 #[test]
359 fn report_from_misaligned_has_reorder_suggestion() {
360 let report = Report::from_layouts(&[connection_layout()]);
361 let sr = &report.structs[0];
362 assert!(
363 sr.findings
364 .iter()
365 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
366 );
367 }
368
369 #[test]
370 fn severity_high_when_waste_over_30_pct() {
371 let report = Report::from_layouts(&[connection_layout()]);
372 let sr = &report.structs[0];
373 let padding_finding = sr
375 .findings
376 .iter()
377 .find(|f| matches!(f, Finding::PaddingWaste { .. }))
378 .unwrap();
379 assert_eq!(padding_finding.severity(), &Severity::High);
380 }
381
382 #[test]
383 fn total_wasted_bytes_sums_across_structs() {
384 let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
385 assert_eq!(report.total_structs, 2);
386 assert_eq!(report.total_wasted_bytes, 10); }
388
389 #[test]
390 fn suppressed_finding_kind_not_in_report() {
391 let mut layout = connection_layout();
392 layout.suppressed_findings = vec!["ReorderSuggestion".to_string()];
393 let report = Report::from_layouts(&[layout]);
394 let sr = &report.structs[0];
395 assert!(
397 sr.findings
398 .iter()
399 .any(|f| matches!(f, Finding::PaddingWaste { .. }))
400 );
401 assert!(
403 !sr.findings
404 .iter()
405 .any(|f| matches!(f, Finding::ReorderSuggestion { .. }))
406 );
407 }
408
409 #[test]
410 fn suppressing_all_findings_yields_empty_findings() {
411 let mut layout = connection_layout();
412 layout.suppressed_findings = vec![
413 "PaddingWaste".to_string(),
414 "ReorderSuggestion".to_string(),
415 "FalseSharing".to_string(),
416 "LocalityIssue".to_string(),
417 ];
418 let report = Report::from_layouts(&[layout]);
419 assert!(report.structs[0].findings.is_empty());
420 }
421}