Skip to main content

padlock_core/
findings.rs

1// padlock-core/src/findings.rs
2
3use 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 wasted_bytes: usize,
72    pub score: f64,
73    pub findings: Vec<Finding>,
74}
75
76#[derive(Debug, serde::Serialize)]
77pub struct Report {
78    pub structs: Vec<StructReport>,
79    pub total_structs: usize,
80    pub total_wasted_bytes: usize,
81}
82
83impl Report {
84    /// Run all analysis passes over `layouts` and assemble the full report.
85    pub fn from_layouts(layouts: &[StructLayout]) -> Report {
86        let structs: Vec<StructReport> = layouts.iter().map(analyze_one).collect();
87        let total_wasted_bytes = structs.iter().map(|s| s.wasted_bytes).sum();
88        Report {
89            total_structs: structs.len(),
90            total_wasted_bytes,
91            structs,
92        }
93    }
94}
95
96fn analyze_one(layout: &StructLayout) -> StructReport {
97    let mut findings = Vec::new();
98
99    // ── padding waste ────────────────────────────────────────────────────────
100    let gaps = padding::find_padding(layout);
101    let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
102    // Unions: is_union suppresses padding at the find_padding level; no extra check needed.
103    if wasted > 0 {
104        let waste_pct = wasted as f64 / layout.total_size as f64 * 100.0;
105        let severity = if waste_pct >= 30.0 {
106            Severity::High
107        } else if waste_pct >= 10.0 {
108            Severity::Medium
109        } else {
110            Severity::Low
111        };
112        findings.push(Finding::PaddingWaste {
113            struct_name: layout.name.clone(),
114            total_size: layout.total_size,
115            wasted_bytes: wasted,
116            waste_pct,
117            gaps,
118            severity,
119        });
120    }
121
122    // ── reorder suggestion ───────────────────────────────────────────────────
123    // Packed structs have no padding to eliminate; union field order is irrelevant.
124    let (optimized_size, savings) = reorder::reorder_savings(layout);
125    if savings > 0 && !layout.is_packed && !layout.is_union {
126        let suggested_order = reorder::optimal_order(layout)
127            .iter()
128            .map(|f| f.name.clone())
129            .collect();
130        findings.push(Finding::ReorderSuggestion {
131            struct_name: layout.name.clone(),
132            original_size: layout.total_size,
133            optimized_size,
134            savings,
135            suggested_order,
136            severity: if savings >= 8 {
137                Severity::High
138            } else {
139                Severity::Medium
140            },
141        });
142    }
143
144    // ── false sharing ────────────────────────────────────────────────────────
145    // Unions place all fields at offset 0 by definition; that is not false sharing.
146    if !layout.is_union && false_sharing::has_false_sharing(layout) {
147        let conflicts = false_sharing::find_sharing_conflicts(layout);
148        findings.push(Finding::FalseSharing {
149            struct_name: layout.name.clone(),
150            conflicts,
151            severity: Severity::High,
152        });
153    }
154
155    // ── locality ─────────────────────────────────────────────────────────────
156    if locality::has_locality_issue(layout) {
157        let (hot, cold) = locality::partition_hot_cold(layout);
158        findings.push(Finding::LocalityIssue {
159            struct_name: layout.name.clone(),
160            hot_fields: hot,
161            cold_fields: cold,
162            severity: Severity::Medium,
163        });
164    }
165
166    let score = scorer::score(layout);
167
168    StructReport {
169        struct_name: layout.name.clone(),
170        source_file: layout.source_file.clone(),
171        source_line: layout.source_line,
172        total_size: layout.total_size,
173        wasted_bytes: wasted,
174        score,
175        findings,
176    }
177}
178
179// ── tests ─────────────────────────────────────────────────────────────────────
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::ir::test_fixtures::{connection_layout, packed_layout};
185
186    #[test]
187    fn report_from_misaligned_has_padding_finding() {
188        let report = Report::from_layouts(&[connection_layout()]);
189        assert_eq!(report.total_structs, 1);
190        let sr = &report.structs[0];
191        assert!(sr.wasted_bytes > 0);
192        assert!(sr
193            .findings
194            .iter()
195            .any(|f| matches!(f, Finding::PaddingWaste { .. })));
196    }
197
198    #[test]
199    fn report_from_packed_has_no_padding_finding() {
200        let report = Report::from_layouts(&[packed_layout()]);
201        let sr = &report.structs[0];
202        assert_eq!(sr.wasted_bytes, 0);
203        assert!(!sr
204            .findings
205            .iter()
206            .any(|f| matches!(f, Finding::PaddingWaste { .. })));
207    }
208
209    #[test]
210    fn report_from_misaligned_has_reorder_suggestion() {
211        let report = Report::from_layouts(&[connection_layout()]);
212        let sr = &report.structs[0];
213        assert!(sr
214            .findings
215            .iter()
216            .any(|f| matches!(f, Finding::ReorderSuggestion { .. })));
217    }
218
219    #[test]
220    fn severity_high_when_waste_over_30_pct() {
221        let report = Report::from_layouts(&[connection_layout()]);
222        let sr = &report.structs[0];
223        // Connection wastes 10/24 = 41% → High
224        let padding_finding = sr
225            .findings
226            .iter()
227            .find(|f| matches!(f, Finding::PaddingWaste { .. }))
228            .unwrap();
229        assert_eq!(padding_finding.severity(), &Severity::High);
230    }
231
232    #[test]
233    fn total_wasted_bytes_sums_across_structs() {
234        let report = Report::from_layouts(&[connection_layout(), packed_layout()]);
235        assert_eq!(report.total_structs, 2);
236        assert_eq!(report.total_wasted_bytes, 10); // only Connection wastes bytes
237    }
238}