layout_audit/analysis/
false_sharing.rs

1use crate::types::{
2    AtomicMember, CacheLineSpanningWarning, FalseSharingAnalysis, FalseSharingWarning, StructLayout,
3};
4use std::collections::BTreeMap;
5
6const ATOMIC_PATTERNS: &[&str] = &[
7    // Rust std atomics (full paths)
8    "std::sync::atomic::Atomic",
9    "core::sync::atomic::Atomic",
10    // Rust std sync primitives (these use internal atomics)
11    "std::sync::Mutex",
12    "std::sync::RwLock",
13    "std::sync::Condvar",
14    "std::sync::Once",
15    "std::sync::OnceLock",
16    "std::sync::Barrier",
17    // C++ std::atomic (various implementations)
18    "std::atomic<",
19    "std::__1::atomic<",
20    "std::__cxx11::atomic<",
21    // C11 _Atomic (with space or parenthesized)
22    "_Atomic ",
23    "_Atomic(",
24    // parking_lot
25    "parking_lot::Mutex",
26    "parking_lot::RwLock",
27    "parking_lot::Once",
28    "parking_lot::Condvar",
29    "parking_lot::ReentrantMutex",
30    "parking_lot::FairMutex",
31    "parking_lot::RawMutex",
32    "parking_lot::RawRwLock",
33    // crossbeam
34    "crossbeam::atomic::AtomicCell",
35    "crossbeam_utils::atomic::AtomicCell",
36    "crossbeam_epoch::Atomic",
37    // atomic_refcell
38    "atomic_refcell::AtomicRefCell",
39    // tokio sync primitives
40    "tokio::sync::Mutex",
41    "tokio::sync::RwLock",
42    "tokio::sync::Semaphore",
43    "tokio::sync::Notify",
44    "tokio::sync::Barrier",
45    "tokio::sync::OnceCell",
46    // arc_swap
47    "arc_swap::ArcSwap",
48    "arc_swap::ArcSwapOption",
49    "arc_swap::ArcSwapAny",
50];
51
52fn is_atomic_type_by_name(type_name: &str) -> bool {
53    ATOMIC_PATTERNS.iter().any(|pattern| type_name.contains(pattern))
54}
55
56/// Analyzes a struct layout for potential false sharing issues.
57///
58/// # Panics
59/// Panics if `cache_line_size` is 0.
60pub fn analyze_false_sharing(layout: &StructLayout, cache_line_size: u32) -> FalseSharingAnalysis {
61    assert!(cache_line_size > 0, "cache_line_size must be > 0");
62    let cache_line_size_u64 = cache_line_size as u64;
63
64    let atomic_members: Vec<AtomicMember> = layout
65        .members
66        .iter()
67        // Use DWARF-detected is_atomic flag OR fall back to string pattern matching
68        .filter(|m| m.is_atomic || is_atomic_type_by_name(&m.type_name))
69        .filter_map(|m| {
70            let offset = m.offset?;
71            let size = m.size?;
72            if size == 0 {
73                return None;
74            }
75            let cache_line = offset / cache_line_size_u64;
76            // Use checked arithmetic to handle malformed DWARF with extreme offsets
77            let Some(end_offset) = offset.checked_add(size).and_then(|v| v.checked_sub(1)) else {
78                return None; // Skip member with overflowing offset+size
79            };
80            let end_cache_line = end_offset / cache_line_size_u64;
81            let spans_cache_lines = end_cache_line > cache_line;
82
83            Some(AtomicMember {
84                name: m.name.clone(),
85                type_name: m.type_name.clone(),
86                offset,
87                size,
88                cache_line,
89                end_cache_line,
90                spans_cache_lines,
91            })
92        })
93        .collect();
94
95    if atomic_members.is_empty() {
96        return FalseSharingAnalysis::default();
97    }
98
99    // Generate spanning warnings for atomics that cross cache line boundaries
100    let spanning_warnings: Vec<CacheLineSpanningWarning> = atomic_members
101        .iter()
102        .filter(|m| m.spans_cache_lines)
103        .map(|m| CacheLineSpanningWarning {
104            member: m.name.clone(),
105            type_name: m.type_name.clone(),
106            offset: m.offset,
107            size: m.size,
108            start_cache_line: m.cache_line,
109            end_cache_line: m.end_cache_line,
110            lines_spanned: m.end_cache_line - m.cache_line + 1,
111        })
112        .collect();
113
114    if atomic_members.len() < 2 {
115        return FalseSharingAnalysis { atomic_members, warnings: Vec::new(), spanning_warnings };
116    }
117
118    // Group atomics by all cache lines they touch (not just start)
119    // Use BTreeMap for deterministic iteration order (ascending by cache_line)
120    let mut by_cache_line: BTreeMap<u64, Vec<&AtomicMember>> = BTreeMap::new();
121    for member in &atomic_members {
122        for cache_line in member.cache_line..=member.end_cache_line {
123            by_cache_line.entry(cache_line).or_default().push(member);
124        }
125    }
126
127    let mut warnings = Vec::new();
128    let mut seen_pairs: std::collections::HashSet<(&str, &str)> = std::collections::HashSet::new();
129
130    for (cache_line, members) in &by_cache_line {
131        if members.len() < 2 {
132            continue;
133        }
134
135        for i in 0..members.len() {
136            for j in (i + 1)..members.len() {
137                let a = members[i];
138                let b = members[j];
139
140                // Ensure consistent ordering and deduplicate
141                let (first, second) = if a.offset <= b.offset { (a, b) } else { (b, a) };
142
143                let pair_key = (first.name.as_str(), second.name.as_str());
144                if seen_pairs.contains(&pair_key) {
145                    continue;
146                }
147                seen_pairs.insert(pair_key);
148
149                // gap_bytes = second.offset - (first.offset + first.size)
150                // Negative = overlap, Zero = adjacent, Positive = gap
151                let first_end = first.offset.saturating_add(first.size);
152                let gap_bytes = second.offset as i64 - first_end as i64;
153
154                warnings.push(FalseSharingWarning {
155                    member_a: first.name.clone(),
156                    member_b: second.name.clone(),
157                    cache_line: *cache_line,
158                    gap_bytes,
159                });
160            }
161        }
162    }
163
164    // Sort by (cache_line, member_a, member_b) without cloning strings
165    warnings.sort_by(|a, b| {
166        a.cache_line
167            .cmp(&b.cache_line)
168            .then_with(|| a.member_a.cmp(&b.member_a))
169            .then_with(|| a.member_b.cmp(&b.member_b))
170    });
171
172    FalseSharingAnalysis { atomic_members, warnings, spanning_warnings }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::types::MemberLayout;
179
180    fn make_layout_with_members(members: Vec<MemberLayout>) -> StructLayout {
181        let mut layout = StructLayout::new("TestStruct".to_string(), 128, Some(8));
182        layout.members = members;
183        layout
184    }
185
186    #[test]
187    fn test_two_atomics_same_cache_line() {
188        let layout = make_layout_with_members(vec![
189            MemberLayout::new(
190                "counter".to_string(),
191                "std::sync::atomic::AtomicU64".to_string(),
192                Some(0),
193                Some(8),
194            ),
195            MemberLayout::new(
196                "flag".to_string(),
197                "std::sync::atomic::AtomicBool".to_string(),
198                Some(8),
199                Some(1),
200            ),
201        ]);
202
203        let analysis = analyze_false_sharing(&layout, 64);
204
205        assert_eq!(analysis.atomic_members.len(), 2);
206        assert_eq!(analysis.warnings.len(), 1);
207        assert_eq!(analysis.warnings[0].cache_line, 0);
208        assert_eq!(analysis.warnings[0].member_a, "counter");
209        assert_eq!(analysis.warnings[0].member_b, "flag");
210        assert_eq!(analysis.warnings[0].gap_bytes, 0); // Adjacent
211    }
212
213    #[test]
214    fn test_two_atomics_different_cache_lines() {
215        let layout = make_layout_with_members(vec![
216            MemberLayout::new(
217                "counter1".to_string(),
218                "std::sync::atomic::AtomicU64".to_string(),
219                Some(0),
220                Some(8),
221            ),
222            MemberLayout::new(
223                "counter2".to_string(),
224                "std::sync::atomic::AtomicU64".to_string(),
225                Some(64),
226                Some(8),
227            ),
228        ]);
229
230        let analysis = analyze_false_sharing(&layout, 64);
231
232        assert_eq!(analysis.atomic_members.len(), 2);
233        assert!(analysis.warnings.is_empty());
234    }
235
236    #[test]
237    fn test_three_atomics_same_cache_line() {
238        let layout = make_layout_with_members(vec![
239            MemberLayout::new(
240                "a".to_string(),
241                "std::sync::atomic::AtomicU64".to_string(),
242                Some(0),
243                Some(8),
244            ),
245            MemberLayout::new(
246                "b".to_string(),
247                "std::sync::atomic::AtomicU64".to_string(),
248                Some(8),
249                Some(8),
250            ),
251            MemberLayout::new(
252                "c".to_string(),
253                "std::sync::atomic::AtomicU64".to_string(),
254                Some(16),
255                Some(8),
256            ),
257        ]);
258
259        let analysis = analyze_false_sharing(&layout, 64);
260
261        assert_eq!(analysis.atomic_members.len(), 3);
262        assert_eq!(analysis.warnings.len(), 3); // (a,b), (a,c), (b,c)
263    }
264
265    #[test]
266    fn test_non_atomic_ignored() {
267        let layout = make_layout_with_members(vec![
268            MemberLayout::new(
269                "counter".to_string(),
270                "std::sync::atomic::AtomicU64".to_string(),
271                Some(0),
272                Some(8),
273            ),
274            MemberLayout::new("data".to_string(), "u64".to_string(), Some(8), Some(8)),
275        ]);
276
277        let analysis = analyze_false_sharing(&layout, 64);
278
279        assert_eq!(analysis.atomic_members.len(), 1);
280        assert!(analysis.warnings.is_empty());
281    }
282
283    #[test]
284    fn test_cpp_atomic_detection() {
285        let layout = make_layout_with_members(vec![
286            MemberLayout::new("a".to_string(), "std::atomic<int>".to_string(), Some(0), Some(4)),
287            MemberLayout::new("b".to_string(), "std::atomic<int>".to_string(), Some(4), Some(4)),
288        ]);
289
290        let analysis = analyze_false_sharing(&layout, 64);
291
292        assert_eq!(analysis.atomic_members.len(), 2);
293        assert_eq!(analysis.warnings.len(), 1);
294    }
295
296    #[test]
297    fn test_c11_atomic_detection() {
298        let layout = make_layout_with_members(vec![
299            MemberLayout::new("a".to_string(), "_Atomic int".to_string(), Some(0), Some(4)),
300            MemberLayout::new("b".to_string(), "_Atomic int".to_string(), Some(4), Some(4)),
301        ]);
302
303        let analysis = analyze_false_sharing(&layout, 64);
304
305        assert_eq!(analysis.atomic_members.len(), 2);
306        assert_eq!(analysis.warnings.len(), 1);
307    }
308
309    #[test]
310    fn test_parking_lot_detection() {
311        let layout = make_layout_with_members(vec![
312            MemberLayout::new(
313                "lock1".to_string(),
314                "parking_lot::Mutex<T>".to_string(),
315                Some(0),
316                Some(8),
317            ),
318            MemberLayout::new(
319                "lock2".to_string(),
320                "parking_lot::RwLock<T>".to_string(),
321                Some(8),
322                Some(16),
323            ),
324        ]);
325
326        let analysis = analyze_false_sharing(&layout, 64);
327
328        assert_eq!(analysis.atomic_members.len(), 2);
329        assert_eq!(analysis.warnings.len(), 1);
330    }
331
332    // New tests for P2
333
334    #[test]
335    fn test_std_sync_mutex_detection() {
336        let layout = make_layout_with_members(vec![
337            MemberLayout::new(
338                "lock1".to_string(),
339                "std::sync::Mutex<i32>".to_string(),
340                Some(0),
341                Some(16),
342            ),
343            MemberLayout::new(
344                "lock2".to_string(),
345                "std::sync::RwLock<i32>".to_string(),
346                Some(16),
347                Some(24),
348            ),
349        ]);
350
351        let analysis = analyze_false_sharing(&layout, 64);
352
353        assert_eq!(analysis.atomic_members.len(), 2);
354        assert_eq!(analysis.warnings.len(), 1);
355    }
356
357    #[test]
358    fn test_single_atomic_no_warnings() {
359        let layout = make_layout_with_members(vec![MemberLayout::new(
360            "counter".to_string(),
361            "std::sync::atomic::AtomicU64".to_string(),
362            Some(0),
363            Some(8),
364        )]);
365
366        let analysis = analyze_false_sharing(&layout, 64);
367
368        assert_eq!(analysis.atomic_members.len(), 1);
369        assert!(analysis.warnings.is_empty());
370        assert!(analysis.spanning_warnings.is_empty());
371    }
372
373    #[test]
374    fn test_zero_size_atomic_ignored() {
375        let layout = make_layout_with_members(vec![
376            MemberLayout::new(
377                "counter".to_string(),
378                "std::sync::atomic::AtomicU64".to_string(),
379                Some(0),
380                Some(8),
381            ),
382            MemberLayout::new(
383                "zst".to_string(),
384                "std::sync::atomic::AtomicUnit".to_string(), // hypothetical ZST
385                Some(8),
386                Some(0),
387            ),
388        ]);
389
390        let analysis = analyze_false_sharing(&layout, 64);
391
392        assert_eq!(analysis.atomic_members.len(), 1);
393        assert!(analysis.warnings.is_empty());
394    }
395
396    #[test]
397    fn test_c11_atomic_parenthesized() {
398        let layout = make_layout_with_members(vec![
399            MemberLayout::new("a".to_string(), "_Atomic(int)".to_string(), Some(0), Some(4)),
400            MemberLayout::new("b".to_string(), "_Atomic(int)".to_string(), Some(4), Some(4)),
401        ]);
402
403        let analysis = analyze_false_sharing(&layout, 64);
404
405        assert_eq!(analysis.atomic_members.len(), 2);
406        assert_eq!(analysis.warnings.len(), 1);
407    }
408
409    #[test]
410    fn test_tokio_sync_detection() {
411        let layout = make_layout_with_members(vec![
412            MemberLayout::new(
413                "lock1".to_string(),
414                "tokio::sync::Mutex<i32>".to_string(),
415                Some(0),
416                Some(16),
417            ),
418            MemberLayout::new(
419                "lock2".to_string(),
420                "tokio::sync::RwLock<i32>".to_string(),
421                Some(16),
422                Some(24),
423            ),
424        ]);
425
426        let analysis = analyze_false_sharing(&layout, 64);
427
428        assert_eq!(analysis.atomic_members.len(), 2);
429        assert_eq!(analysis.warnings.len(), 1);
430    }
431
432    // Tests for P3: cache line spanning
433
434    #[test]
435    fn test_atomic_spanning_cache_lines() {
436        // An atomic at offset 60 with size 8 spans bytes 60-67, crossing the 64-byte boundary
437        let layout = make_layout_with_members(vec![MemberLayout::new(
438            "spanning".to_string(),
439            "std::sync::atomic::AtomicU64".to_string(),
440            Some(60),
441            Some(8),
442        )]);
443
444        let analysis = analyze_false_sharing(&layout, 64);
445
446        assert_eq!(analysis.atomic_members.len(), 1);
447        assert!(analysis.atomic_members[0].spans_cache_lines);
448        assert_eq!(analysis.atomic_members[0].cache_line, 0);
449        assert_eq!(analysis.atomic_members[0].end_cache_line, 1);
450
451        assert_eq!(analysis.spanning_warnings.len(), 1);
452        assert_eq!(analysis.spanning_warnings[0].member, "spanning");
453        assert_eq!(analysis.spanning_warnings[0].lines_spanned, 2);
454    }
455
456    #[test]
457    fn test_atomic_not_spanning() {
458        // An atomic at offset 0 with size 8 stays within cache line 0
459        let layout = make_layout_with_members(vec![MemberLayout::new(
460            "aligned".to_string(),
461            "std::sync::atomic::AtomicU64".to_string(),
462            Some(0),
463            Some(8),
464        )]);
465
466        let analysis = analyze_false_sharing(&layout, 64);
467
468        assert_eq!(analysis.atomic_members.len(), 1);
469        assert!(!analysis.atomic_members[0].spans_cache_lines);
470        assert!(analysis.spanning_warnings.is_empty());
471    }
472
473    #[test]
474    fn test_gap_bytes_calculation() {
475        let layout = make_layout_with_members(vec![
476            MemberLayout::new(
477                "a".to_string(),
478                "std::sync::atomic::AtomicU64".to_string(),
479                Some(0),
480                Some(8),
481            ),
482            MemberLayout::new(
483                "b".to_string(),
484                "std::sync::atomic::AtomicU64".to_string(),
485                Some(16), // 8-byte gap between a (ends at 8) and b (starts at 16)
486                Some(8),
487            ),
488        ]);
489
490        let analysis = analyze_false_sharing(&layout, 64);
491
492        assert_eq!(analysis.warnings.len(), 1);
493        assert_eq!(analysis.warnings[0].gap_bytes, 8); // Positive gap
494    }
495
496    #[test]
497    fn test_gap_bytes_adjacent() {
498        let layout = make_layout_with_members(vec![
499            MemberLayout::new(
500                "a".to_string(),
501                "std::sync::atomic::AtomicU64".to_string(),
502                Some(0),
503                Some(8),
504            ),
505            MemberLayout::new(
506                "b".to_string(),
507                "std::sync::atomic::AtomicU64".to_string(),
508                Some(8), // Adjacent: a ends at 8, b starts at 8
509                Some(8),
510            ),
511        ]);
512
513        let analysis = analyze_false_sharing(&layout, 64);
514
515        assert_eq!(analysis.warnings.len(), 1);
516        assert_eq!(analysis.warnings[0].gap_bytes, 0); // Zero = adjacent
517    }
518
519    #[test]
520    fn test_spanning_atomic_shares_with_both_lines() {
521        // Atomic at offset 60-67 spans cache lines 0 and 1
522        // Another atomic at offset 70 is on cache line 1
523        // They should produce a warning for cache line 1
524        let layout = make_layout_with_members(vec![
525            MemberLayout::new(
526                "spanning".to_string(),
527                "std::sync::atomic::AtomicU64".to_string(),
528                Some(60),
529                Some(8),
530            ),
531            MemberLayout::new(
532                "other".to_string(),
533                "std::sync::atomic::AtomicU64".to_string(),
534                Some(70),
535                Some(8),
536            ),
537        ]);
538
539        let analysis = analyze_false_sharing(&layout, 64);
540
541        assert_eq!(analysis.atomic_members.len(), 2);
542        // One warning for the pair on cache line 1
543        assert_eq!(analysis.warnings.len(), 1);
544        assert_eq!(analysis.warnings[0].cache_line, 1);
545    }
546
547    // Coverage tests for edge cases and newly fixed paths
548
549    #[test]
550    fn test_no_atomics_returns_default() {
551        // Layout with only non-atomic members should return empty analysis
552        let layout = make_layout_with_members(vec![
553            MemberLayout::new("x".to_string(), "u64".to_string(), Some(0), Some(8)),
554            MemberLayout::new("y".to_string(), "u64".to_string(), Some(8), Some(8)),
555        ]);
556
557        let analysis = analyze_false_sharing(&layout, 64);
558
559        assert!(analysis.atomic_members.is_empty());
560        assert!(analysis.warnings.is_empty());
561        assert!(analysis.spanning_warnings.is_empty());
562    }
563
564    #[test]
565    fn test_overflow_offset_skipped() {
566        // Atomic with offset near u64::MAX that would overflow when adding size
567        // Should be skipped gracefully, not panic
568        let layout = make_layout_with_members(vec![
569            MemberLayout::new(
570                "normal".to_string(),
571                "std::sync::atomic::AtomicU64".to_string(),
572                Some(0),
573                Some(8),
574            ),
575            MemberLayout::new(
576                "overflow".to_string(),
577                "std::sync::atomic::AtomicU64".to_string(),
578                Some(u64::MAX - 3), // offset + size (8) would overflow
579                Some(8),
580            ),
581        ]);
582
583        let analysis = analyze_false_sharing(&layout, 64);
584
585        // Only the normal atomic should be included; overflow one is skipped
586        assert_eq!(analysis.atomic_members.len(), 1);
587        assert_eq!(analysis.atomic_members[0].name, "normal");
588        assert!(analysis.warnings.is_empty());
589    }
590
591    #[test]
592    fn test_duplicate_pair_dedupe_deterministic() {
593        // Two atomics that both span cache lines 0 and 1
594        // The pair should appear only ONCE, and always with the lowest cache_line (0)
595        // This tests both the dedupe logic AND the BTreeMap determinism fix
596        let layout = make_layout_with_members(vec![
597            MemberLayout::new(
598                "spanning_a".to_string(),
599                "std::sync::atomic::AtomicU64".to_string(),
600                Some(60), // spans cache lines 0 and 1
601                Some(8),
602            ),
603            MemberLayout::new(
604                "spanning_b".to_string(),
605                "std::sync::atomic::AtomicU64".to_string(),
606                Some(62), // also spans cache lines 0 and 1
607                Some(8),
608            ),
609        ]);
610
611        let analysis = analyze_false_sharing(&layout, 64);
612
613        assert_eq!(analysis.atomic_members.len(), 2);
614        // Only ONE warning for this pair (dedupe worked)
615        assert_eq!(analysis.warnings.len(), 1);
616        // Should always be cache_line 0 (BTreeMap iteration is deterministic, ascending)
617        assert_eq!(analysis.warnings[0].cache_line, 0);
618        assert_eq!(analysis.warnings[0].member_a, "spanning_a");
619        assert_eq!(analysis.warnings[0].member_b, "spanning_b");
620    }
621
622    #[test]
623    fn test_empty_layout_returns_default() {
624        // Layout with no members at all
625        let layout = make_layout_with_members(vec![]);
626
627        let analysis = analyze_false_sharing(&layout, 64);
628
629        assert!(analysis.atomic_members.is_empty());
630        assert!(analysis.warnings.is_empty());
631        assert!(analysis.spanning_warnings.is_empty());
632    }
633}