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