layout_audit/analysis/
padding.rs

1use crate::types::{LayoutMetrics, PaddingHole, StructLayout};
2
3/// Analyzes a struct layout for padding holes and cache line metrics.
4///
5/// # Panics
6/// Panics if `cache_line_size` is 0.
7pub fn analyze_layout(layout: &mut StructLayout, cache_line_size: u32) {
8    assert!(cache_line_size > 0, "cache_line_size must be > 0");
9    #[derive(Clone)]
10    struct Span {
11        start: u64,
12        end: u64,
13        member_name: String,
14    }
15
16    let mut spans = Vec::new();
17    let mut partial = false;
18
19    for member in &layout.members {
20        let Some(member_offset) = member.offset else {
21            partial = true;
22            continue;
23        };
24        let Some(member_size) = member.size else {
25            partial = true;
26            continue;
27        };
28        if member_size == 0 {
29            continue;
30        }
31
32        spans.push(Span {
33            start: member_offset,
34            end: member_offset.saturating_add(member_size),
35            member_name: member.name.clone(),
36        });
37    }
38
39    spans.sort_by_key(|s| (s.start, s.end));
40
41    let mut padding_holes = Vec::new();
42    let mut useful_size: u64 = 0;
43
44    // Can't infer padding without at least one reliable span.
45    if spans.is_empty() {
46        let cache_line_size_u64 = cache_line_size as u64;
47        let cache_lines_spanned =
48            if layout.size > 0 { layout.size.div_ceil(cache_line_size_u64) as u32 } else { 0 };
49
50        layout.metrics = LayoutMetrics {
51            total_size: layout.size,
52            useful_size: 0,
53            padding_bytes: 0,
54            padding_percentage: 0.0,
55            cache_lines_spanned,
56            cache_line_density: 0.0,
57            padding_holes,
58            partial,
59            false_sharing: None,
60        };
61        return;
62    }
63
64    // Merge overlapping spans (bitfields share storage, unions overlap, etc.). We use the merged
65    // covered bytes for "useful_size" to avoid double-counting overlapping members.
66    let mut current_start = spans[0].start;
67    let mut current_end = spans[0].end;
68    let mut current_end_member: Option<String> = Some(spans[0].member_name.clone());
69
70    for span in spans.into_iter().skip(1) {
71        if span.start > current_end {
72            useful_size = useful_size.saturating_add(current_end.saturating_sub(current_start));
73
74            if !partial {
75                padding_holes.push(PaddingHole {
76                    offset: current_end,
77                    size: span.start - current_end,
78                    after_member: current_end_member.clone(),
79                });
80            }
81
82            current_start = span.start;
83            current_end = span.end;
84            current_end_member = Some(span.member_name);
85            continue;
86        }
87
88        if span.end >= current_end {
89            current_end = span.end;
90            current_end_member = Some(span.member_name);
91        }
92    }
93
94    useful_size = useful_size.saturating_add(current_end.saturating_sub(current_start));
95
96    if !partial && current_end < layout.size {
97        padding_holes.push(PaddingHole {
98            offset: current_end,
99            size: layout.size - current_end,
100            after_member: current_end_member,
101        });
102    }
103
104    let padding_bytes: u64 = padding_holes.iter().map(|h| h.size).sum();
105    let padding_percentage =
106        if layout.size > 0 { (padding_bytes as f64 / layout.size as f64) * 100.0 } else { 0.0 };
107
108    let cache_line_size_u64 = cache_line_size as u64;
109    let cache_lines_spanned =
110        if layout.size > 0 { layout.size.div_ceil(cache_line_size_u64) as u32 } else { 0 };
111
112    let total_cache_bytes = cache_lines_spanned as u64 * cache_line_size_u64;
113    let cache_line_density = if total_cache_bytes > 0 {
114        (useful_size as f64 / total_cache_bytes as f64) * 100.0
115    } else {
116        0.0
117    };
118
119    layout.metrics = LayoutMetrics {
120        total_size: layout.size,
121        useful_size,
122        padding_bytes,
123        padding_percentage,
124        cache_lines_spanned,
125        cache_line_density,
126        padding_holes,
127        partial,
128        false_sharing: None,
129    };
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::types::MemberLayout;
136
137    fn make_layout(size: u64, members: Vec<MemberLayout>) -> StructLayout {
138        let mut layout = StructLayout::new("TestStruct".to_string(), size, Some(8));
139        layout.members = members;
140        layout
141    }
142
143    #[test]
144    fn test_empty_members_no_spans() {
145        // Layout with no members at all - exercises the "spans.is_empty()" branch
146        let mut layout = make_layout(64, vec![]);
147
148        analyze_layout(&mut layout, 64);
149
150        assert_eq!(layout.metrics.total_size, 64);
151        assert_eq!(layout.metrics.useful_size, 0);
152        assert_eq!(layout.metrics.padding_bytes, 0);
153        assert_eq!(layout.metrics.padding_percentage, 0.0);
154        assert_eq!(layout.metrics.cache_lines_spanned, 1);
155        assert!(!layout.metrics.partial);
156        assert!(layout.metrics.padding_holes.is_empty());
157    }
158
159    #[test]
160    fn test_zero_size_layout_no_spans() {
161        // Layout with size=0 and no members
162        let mut layout = make_layout(0, vec![]);
163
164        analyze_layout(&mut layout, 64);
165
166        assert_eq!(layout.metrics.total_size, 0);
167        assert_eq!(layout.metrics.cache_lines_spanned, 0);
168        assert_eq!(layout.metrics.cache_line_density, 0.0);
169    }
170
171    #[test]
172    fn test_partial_layout_missing_offset() {
173        // Member with missing offset should set partial=true
174        let mut layout = make_layout(
175            16,
176            vec![
177                MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(8)),
178                MemberLayout::new("b".to_string(), "u64".to_string(), None, Some(8)), // missing offset
179            ],
180        );
181
182        analyze_layout(&mut layout, 64);
183
184        assert!(layout.metrics.partial);
185        // With partial=true, no padding holes should be reported
186        assert!(layout.metrics.padding_holes.is_empty());
187    }
188
189    #[test]
190    fn test_partial_layout_missing_size() {
191        // Member with missing size should set partial=true
192        let mut layout = make_layout(
193            16,
194            vec![
195                MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(8)),
196                MemberLayout::new("b".to_string(), "u64".to_string(), Some(8), None), // missing size
197            ],
198        );
199
200        analyze_layout(&mut layout, 64);
201
202        assert!(layout.metrics.partial);
203        assert!(layout.metrics.padding_holes.is_empty());
204    }
205
206    #[test]
207    fn test_zero_size_member_skipped() {
208        // Zero-size members should be skipped (not contribute to spans)
209        let mut layout = make_layout(
210            16,
211            vec![
212                MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(8)),
213                MemberLayout::new("zst".to_string(), "()".to_string(), Some(8), Some(0)), // ZST
214                MemberLayout::new("b".to_string(), "u64".to_string(), Some(8), Some(8)),
215            ],
216        );
217
218        analyze_layout(&mut layout, 64);
219
220        assert!(!layout.metrics.partial);
221        assert_eq!(layout.metrics.useful_size, 16);
222    }
223
224    #[test]
225    fn test_overlapping_spans_merged() {
226        // Overlapping members (e.g., union-like) should be merged, not double-counted
227        let mut layout = make_layout(
228            16,
229            vec![
230                MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(16)),
231                MemberLayout::new("b".to_string(), "u64".to_string(), Some(4), Some(8)), // overlaps with a
232            ],
233        );
234
235        analyze_layout(&mut layout, 64);
236
237        // Should count as 16 bytes useful, not 24
238        assert_eq!(layout.metrics.useful_size, 16);
239        assert!(layout.metrics.padding_holes.is_empty());
240    }
241
242    #[test]
243    fn test_padding_hole_detected() {
244        // Gap between members should create a padding hole
245        let mut layout = make_layout(
246            24,
247            vec![
248                MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(8)),
249                // 8 bytes gap
250                MemberLayout::new("b".to_string(), "u64".to_string(), Some(16), Some(8)),
251            ],
252        );
253
254        analyze_layout(&mut layout, 64);
255
256        assert_eq!(layout.metrics.padding_bytes, 8);
257        assert_eq!(layout.metrics.padding_holes.len(), 1);
258        assert_eq!(layout.metrics.padding_holes[0].offset, 8);
259        assert_eq!(layout.metrics.padding_holes[0].size, 8);
260    }
261
262    #[test]
263    fn test_tail_padding_detected() {
264        // Gap at end of struct is tail padding
265        let mut layout = make_layout(
266            16,
267            vec![MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(8))],
268        );
269
270        analyze_layout(&mut layout, 64);
271
272        assert_eq!(layout.metrics.padding_bytes, 8);
273        assert_eq!(layout.metrics.padding_holes.len(), 1);
274        assert_eq!(layout.metrics.padding_holes[0].offset, 8);
275    }
276
277    #[test]
278    fn test_partial_layout_no_tail_padding_reported() {
279        // With partial=true, tail padding should NOT be reported
280        let mut layout = make_layout(
281            32,
282            vec![
283                MemberLayout::new("a".to_string(), "u64".to_string(), Some(0), Some(8)),
284                MemberLayout::new("b".to_string(), "u64".to_string(), None, Some(8)), // missing offset
285            ],
286        );
287
288        analyze_layout(&mut layout, 64);
289
290        assert!(layout.metrics.partial);
291        // No padding holes when partial (can't reliably detect them)
292        assert!(layout.metrics.padding_holes.is_empty());
293    }
294}