Skip to main content

padlock_core/analysis/
scorer.rs

1// padlock-core/src/analysis/scorer.rs
2
3use crate::analysis::{false_sharing, locality, padding, reorder};
4use crate::ir::StructLayout;
5
6#[derive(Debug, Clone)]
7pub struct ScoreBreakdown {
8    /// Final score in [0.0, 100.0]. Higher = better (less to fix).
9    pub total: f64,
10    /// Points deducted for padding waste (max 40).
11    pub padding_deduction: f64,
12    /// Points deducted for confirmed false sharing (30) or potential (10).
13    pub false_sharing_deduction: f64,
14    /// Points deducted for hot/cold locality issues (max 15).
15    pub locality_deduction: f64,
16}
17
18/// Score a layout on a 0–100 scale (100 = perfect, 0 = very bad).
19pub fn score(layout: &StructLayout) -> f64 {
20    score_with_breakdown(layout).total
21}
22
23pub fn score_with_breakdown(layout: &StructLayout) -> ScoreBreakdown {
24    let mut deductions = 0.0f64;
25
26    // Padding: up to 40 points
27    let gaps = padding::find_padding(layout);
28    let wasted: usize = gaps.iter().map(|g| g.bytes).sum();
29    let padding_deduction = if layout.total_size > 0 {
30        (wasted as f64 / layout.total_size as f64 * 40.0).min(40.0)
31    } else {
32        0.0
33    };
34    deductions += padding_deduction;
35
36    // False sharing: 30 confirmed, 10 potential.
37    // Unions place all fields at offset 0 by definition — that is not a sharing hazard.
38    let false_sharing_deduction = if layout.is_union {
39        0.0
40    } else if false_sharing::has_false_sharing(layout) {
41        30.0
42    } else if !false_sharing::find_sharing_conflicts(layout).is_empty() {
43        10.0
44    } else {
45        0.0
46    };
47    deductions += false_sharing_deduction;
48
49    // Locality: up to 15 points
50    let locality_deduction = if locality::has_locality_issue(layout) {
51        15.0
52    } else {
53        0.0
54    };
55    deductions += locality_deduction;
56
57    // Reorder potential: up to 15 points
58    let (_, savings) = reorder::reorder_savings(layout);
59    let reorder_deduction = if layout.total_size > 0 && savings > 0 {
60        (savings as f64 / layout.total_size as f64 * 15.0).min(15.0)
61    } else {
62        0.0
63    };
64    deductions += reorder_deduction;
65
66    let total = (100.0 - deductions).clamp(0.0, 100.0);
67
68    ScoreBreakdown {
69        total,
70        padding_deduction,
71        false_sharing_deduction,
72        locality_deduction,
73    }
74}
75
76// ── tests ─────────────────────────────────────────────────────────────────────
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::ir::test_fixtures::{connection_layout, packed_layout};
82
83    #[test]
84    fn score_in_range() {
85        let s = score(&connection_layout());
86        assert!((0.0..=100.0).contains(&s), "score out of range: {s}");
87    }
88
89    #[test]
90    fn packed_scores_higher_than_misaligned() {
91        assert!(score(&packed_layout()) > score(&connection_layout()));
92    }
93
94    #[test]
95    fn perfect_layout_scores_100() {
96        // Single 8-byte field — no padding, no sharing, no locality issues
97        use crate::arch::X86_64_SYSV;
98        use crate::ir::{AccessPattern, Field, StructLayout, TypeInfo};
99        let layout = StructLayout {
100            name: "Single".into(),
101            total_size: 8,
102            align: 8,
103            fields: vec![Field {
104                name: "x".into(),
105                ty: TypeInfo::Primitive {
106                    name: "u64".into(),
107                    size: 8,
108                    align: 8,
109                },
110                offset: 0,
111                size: 8,
112                align: 8,
113                source_file: None,
114                source_line: None,
115                access: AccessPattern::Unknown,
116            }],
117            source_file: None,
118            source_line: None,
119            arch: &X86_64_SYSV,
120            is_packed: false,
121            is_union: false,
122            is_repr_rust: false,
123        };
124        assert!((score(&layout) - 100.0).abs() < 1e-9);
125    }
126
127    #[test]
128    fn connection_loses_padding_and_reorder_points() {
129        let bd = score_with_breakdown(&connection_layout());
130        assert!(bd.padding_deduction > 0.0);
131        assert!(bd.total < 100.0);
132    }
133}