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