Skip to main content

ghostlink_core/
ring.rs

1//! Zero-Copy SPSC Ring Buffer with AF_XDP Integration
2//!
3//! This implementation uses pinned allocations and proper memory ordering
4//! for single-producer/single-consumer DMA-style hand-off.
5//!
6//! Optimizations:
7//! - 4x larger capacity (4096) for better batching
8//! - Prefetching in hot paths for cache-friendly access
9//! - Optimal cache-line padding (128 bytes) to avoid false sharing
10
11use std::cell::UnsafeCell;
12use std::mem::MaybeUninit;
13use std::sync::atomic::{AtomicUsize, Ordering};
14
15/// Ring buffer configuration with backpressure thresholds
16#[derive(Clone, Copy, Debug)]
17pub struct RingConfig {
18    /// Maximum number of elements the ring can hold
19    pub capacity: usize,
20    /// Backpressure threshold at 70% capacity
21    pub backpressure_threshold: usize,
22}
23
24impl Default for RingConfig {
25    fn default() -> Self {
26        Self {
27            capacity: 4095,               // 4x larger for better batching
28            backpressure_threshold: 2867, // ~70% of 4095
29        }
30    }
31}
32
33/// Fixed capacity used for DMA alignment (power of 2 for fast modulo)
34const RING_CAPACITY: usize = 4096;
35const CACHE_LINE: usize = 128; // 128-byte cache lines on modern x86/ARM
36
37/// Prefetch hint for read access (equivalent to `prefetcht0` on x86)
38#[inline(always)]
39fn prefetch_read<T>(ptr: *const T) {
40    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
41    unsafe {
42        std::arch::x86_64::_mm_prefetch(ptr as *const i8, std::arch::x86_64::_MM_HINT_T0);
43    }
44    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
45    {
46        // On other platforms, hint to compiler/CPU
47        std::hint::black_box(ptr);
48    }
49}
50
51/// Prefetch hint for write access (equivalent to `prefetchw` on x86)
52#[inline(always)]
53fn prefetch_write<T>(ptr: *mut T) {
54    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
55    unsafe {
56        std::arch::x86_64::_mm_prefetch(ptr as *const i8, std::arch::x86_64::_MM_HINT_ET1);
57    }
58    #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
59    {
60        std::hint::black_box(ptr);
61    }
62}
63
64/// Zero-copy SPSC ring buffer with proper memory ordering
65///
66/// # Safety
67/// This type is only safe to use when:
68/// - Only one producer thread exists
69/// - Only one consumer thread exists
70/// - Memory is properly aligned for the element type T
71#[derive(Debug)]
72pub struct SpscRingBuffer<T> {
73    /// Buffer of uninitialized elements (aligned to cache line)
74    buffer: UnsafeCell<[MaybeUninit<T>; RING_CAPACITY]>,
75
76    /// Padding to prevent false sharing between buffer and head
77    _pad1: [u8; CACHE_LINE],
78    /// Current head position (consumer reads here)
79    head: AtomicUsize,
80    /// Cached tail for consumer to avoid reading shared tail
81    cached_tail: AtomicUsize,
82
83    /// Padding to prevent false sharing between head and tail
84    _pad2: [u8; CACHE_LINE],
85    /// Current tail position (producer writes here)
86    tail: AtomicUsize,
87    /// Cached head for producer to avoid reading shared head
88    cached_head: AtomicUsize,
89
90    /// Padding to prevent false sharing between tail and other counters
91    _pad3: [u8; CACHE_LINE],
92    /// Overflow counter for backpressure monitoring
93    overflow_count: AtomicUsize,
94    /// Configuration for this ring buffer
95    config: RingConfig,
96}
97
98impl<T> SpscRingBuffer<T> {
99    const CAPACITY: usize = RING_CAPACITY;
100
101    /// Create a new SPSC ring buffer with the given configuration
102    pub fn new(config: RingConfig) -> Self {
103        assert!(
104            config.capacity < Self::CAPACITY,
105            "capacity must be strictly less than RING_CAPACITY"
106        );
107
108        // Safety: MaybeUninit does not require initialization
109        let buffer = UnsafeCell::new(unsafe {
110            MaybeUninit::<[MaybeUninit<T>; RING_CAPACITY]>::uninit().assume_init()
111        });
112
113        Self {
114            buffer,
115            _pad1: [0; CACHE_LINE],
116            head: AtomicUsize::new(0),
117            cached_tail: AtomicUsize::new(0),
118            _pad2: [0; CACHE_LINE],
119            tail: AtomicUsize::new(0),
120            cached_head: AtomicUsize::new(0),
121            _pad3: [0; CACHE_LINE],
122            overflow_count: AtomicUsize::new(0),
123            config,
124        }
125    }
126
127    /// Push an element into the ring buffer
128    ///
129    /// Returns `Ok(())` on success, or `Err(value)` if the ring is full.
130    /// When full, the producer should wait/backpressure until space is available.
131    pub fn push(&self, value: T) -> Result<(), T> {
132        // Producer loads its own tail with Relaxed.
133        let tail = self.tail.load(Ordering::Relaxed);
134
135        // First check against cached head to avoid atomic contention.
136        let mut head = self.cached_head.load(Ordering::Relaxed);
137        let mut current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
138
139        if current_len >= self.config.capacity {
140            // Only if cached head suggests we are full, refresh from shared head.
141            head = self.head.load(Ordering::Acquire);
142            self.cached_head.store(head, Ordering::Relaxed);
143            current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
144
145            if current_len >= self.config.capacity {
146                self.overflow_count.fetch_add(1, Ordering::Relaxed);
147                return Err(value);
148            }
149        }
150
151        let next_tail = Self::increment(tail);
152
153        // Write the value at tail position
154        unsafe {
155            let buf = &mut *self.buffer.get();
156            buf[tail].write(value);
157
158            // Release store to make write visible to consumer
159            self.tail.store(next_tail, Ordering::Release);
160        }
161
162        Ok(())
163    }
164
165    /// Pop an element from the ring buffer
166    ///
167    /// Returns `Some(value)` on success, or `None` if the ring is empty.
168    pub fn pop(&self) -> Option<T> {
169        // Consumer loads its own head with Relaxed.
170        let head = self.head.load(Ordering::Relaxed);
171
172        // First check against cached tail to avoid atomic contention.
173        let mut tail = self.cached_tail.load(Ordering::Relaxed);
174
175        if head == tail {
176            // Only if cached tail suggests we are empty, refresh from shared tail.
177            tail = self.tail.load(Ordering::Acquire);
178            self.cached_tail.store(tail, Ordering::Relaxed);
179
180            if head == tail {
181                return None;
182            }
183        }
184
185        // Read the value at head position
186        let value = unsafe {
187            let buf = &mut *self.buffer.get();
188            let val = buf[head].assume_init_read();
189
190            // Release store to make read visible to producer
191            self.head.store(Self::increment(head), Ordering::Release);
192
193            val
194        };
195
196        Some(value)
197    }
198
199    /// Get the current number of elements in the ring
200    pub fn len(&self) -> usize {
201        let head = self.head.load(Ordering::Acquire);
202        let tail = self.tail.load(Ordering::Acquire);
203
204        tail.wrapping_sub(head) & (Self::CAPACITY - 1)
205    }
206
207    /// Check if the ring is empty
208    pub fn is_empty(&self) -> bool {
209        let head = self.head.load(Ordering::Relaxed);
210        let mut tail = self.cached_tail.load(Ordering::Relaxed);
211
212        if head == tail {
213            tail = self.tail.load(Ordering::Acquire);
214            self.cached_tail.store(tail, Ordering::Relaxed);
215        }
216
217        head == tail
218    }
219
220    /// Get the configured capacity
221    pub fn capacity(&self) -> usize {
222        self.config.capacity
223    }
224
225    /// Get overflow count for backpressure monitoring
226    pub fn overflow_count(&self) -> usize {
227        self.overflow_count.load(Ordering::Relaxed)
228    }
229
230    /// Get empty count for backpressure monitoring
231    pub fn empty_count(&self) -> usize {
232        self.config.capacity.saturating_sub(self.len())
233    }
234
235    /// Check if backpressure should be applied (ring >= threshold)
236    pub fn should_backpressure(&self) -> bool {
237        let tail = self.tail.load(Ordering::Relaxed);
238        let mut head = self.cached_head.load(Ordering::Relaxed);
239        let mut len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
240
241        if len >= self.config.backpressure_threshold {
242            head = self.head.load(Ordering::Acquire);
243            self.cached_head.store(head, Ordering::Relaxed);
244            len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
245        }
246
247        len >= self.config.backpressure_threshold
248    }
249
250    /// Number of elements that can currently be pushed without blocking.
251    pub fn write_available(&self) -> usize {
252        let tail = self.tail.load(Ordering::Relaxed);
253        let mut head = self.cached_head.load(Ordering::Relaxed);
254        let mut len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
255        if len >= self.config.capacity {
256            head = self.head.load(Ordering::Acquire);
257            self.cached_head.store(head, Ordering::Relaxed);
258            len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
259        }
260        self.config.capacity.saturating_sub(len)
261    }
262
263    /// Number of elements currently available to pop (best-effort, not a snapshot).
264    pub fn read_available(&self) -> usize {
265        let head = self.head.load(Ordering::Relaxed);
266        let mut tail = self.cached_tail.load(Ordering::Relaxed);
267        if head == tail {
268            tail = self.tail.load(Ordering::Acquire);
269            self.cached_tail.store(tail, Ordering::Relaxed);
270        }
271        tail.wrapping_sub(head) & (Self::CAPACITY - 1)
272    }
273
274    /// Wait for space to become available
275    ///
276    /// Uses exponential backoff: spins with PAUSE up to 256 iterations,
277    /// then transitions to OS yield to avoid starving the consumer.
278    pub fn wait_for_space(&self) {
279        let mut backoff = 1u32;
280        loop {
281            if !self.should_backpressure() {
282                return;
283            }
284            // Exponential backoff: 1, 2, 4, 8, … 256
285            let limit = backoff.min(256);
286            for _ in 0..limit {
287                core::hint::spin_loop();
288            }
289            backoff = backoff.saturating_mul(2);
290            if backoff > 256 {
291                std::thread::yield_now();
292                backoff = 1;
293            }
294        }
295    }
296
297    /// Wait for an element to become available
298    ///
299    /// Uses exponential backoff: spins with PAUSE up to 256 iterations,
300    /// then transitions to OS yield to avoid starving the producer.
301    pub fn wait_for_data(&self) {
302        let mut backoff = 1u32;
303        loop {
304            if !self.is_empty() {
305                return;
306            }
307            let limit = backoff.min(256);
308            for _ in 0..limit {
309                core::hint::spin_loop();
310            }
311            backoff = backoff.saturating_mul(2);
312            if backoff > 256 {
313                std::thread::yield_now();
314                backoff = 1;
315            }
316        }
317    }
318
319    /// Push a batch of elements, returning the number successfully pushed.
320    ///
321    /// Stops early if the ring becomes full. This amortizes atomic store
322    /// overhead across multiple elements: only two atomic stores (tail + cached_head)
323    /// for up to `slice.len()` pushes.
324    ///
325    /// Uses prefetching to reduce cache misses on write path.
326    pub fn push_batch(&self, slice: &[T]) -> usize
327    where
328        T: Copy,
329    {
330        let tail = self.tail.load(Ordering::Relaxed);
331        let mut head = self.cached_head.load(Ordering::Relaxed);
332        let mut current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
333
334        if current_len >= self.config.capacity {
335            head = self.head.load(Ordering::Acquire);
336            self.cached_head.store(head, Ordering::Relaxed);
337            current_len = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
338            if current_len >= self.config.capacity {
339                // Ring is full: every requested element is rejected.
340                self.overflow_count
341                    .fetch_add(slice.len(), Ordering::Relaxed);
342                return 0;
343            }
344        }
345
346        let available = self.config.capacity - current_len;
347        let count = available.min(slice.len());
348        if count == 0 {
349            return 0;
350        }
351        if count < slice.len() {
352            // Partial batch: track the elements that didn't fit as overflow,
353            // matching push()'s per-element overflow accounting.
354            self.overflow_count
355                .fetch_add(slice.len() - count, Ordering::Relaxed);
356        }
357
358        let buf = unsafe { &mut *self.buffer.get() };
359        let mask = Self::CAPACITY - 1;
360
361        // Prefetch the first write location
362        prefetch_write(&mut buf[tail & mask] as *mut _);
363
364        for i in 0..count {
365            let idx = (tail + i) & mask;
366            // Prefetch next cache line (64 bytes = ~16 elements for small T)
367            if i % 16 == 14 && i + 2 < count {
368                prefetch_write(&mut buf[(tail + i + 2) & mask] as *mut _);
369            }
370            buf[idx].write(slice[i]);
371        }
372        let new_tail = tail.wrapping_add(count) & mask;
373        self.tail.store(new_tail, Ordering::Release);
374        count
375    }
376
377    /// Pop up to `count` elements into the provided mutable slice, returning
378    /// the number of elements actually popped.
379    ///
380    /// Uses `MaybeUninit` to avoid zero-initialisation overhead.
381    /// Uses prefetching to reduce cache misses on read path.
382    pub fn pop_batch(&self, out: &mut [std::mem::MaybeUninit<T>]) -> usize {
383        let head = self.head.load(Ordering::Relaxed);
384        let mut tail = self.cached_tail.load(Ordering::Relaxed);
385
386        if head == tail {
387            tail = self.tail.load(Ordering::Acquire);
388            self.cached_tail.store(tail, Ordering::Relaxed);
389            if head == tail {
390                return 0;
391            }
392        }
393
394        let available = tail.wrapping_sub(head) & (Self::CAPACITY - 1);
395        let count = available.min(out.len());
396        if count == 0 {
397            return 0;
398        }
399
400        let buf = unsafe { &mut *self.buffer.get() };
401        let mask = Self::CAPACITY - 1;
402
403        // Prefetch the first read location
404        prefetch_read(&buf[head & mask] as *const _);
405
406        for (i, slot) in out.iter_mut().enumerate().take(count) {
407            let src = (head + i) & mask;
408            // Prefetch next cache line
409            if i % 16 == 14 && i + 2 < count {
410                prefetch_read(&buf[(head + i + 2) & mask] as *const _);
411            }
412            *slot = std::mem::MaybeUninit::new(unsafe { buf[src].assume_init_read() });
413        }
414        let new_head = head.wrapping_add(count) & mask;
415        self.head.store(new_head, Ordering::Release);
416        count
417    }
418
419    fn increment(index: usize) -> usize {
420        (index + 1) & (Self::CAPACITY - 1)
421    }
422}
423
424// Safety: SpscRingBuffer is safe to send across threads when used as SPSC.
425// The producer and consumer operate on disjoint memory regions with proper
426// atomic ordering, making this safe for our SPSC use case.
427unsafe impl<T: Send> Send for SpscRingBuffer<T> {}
428unsafe impl<T: Send> Sync for SpscRingBuffer<T> {}
429
430impl<T> Drop for SpscRingBuffer<T> {
431    fn drop(&mut self) {
432        // Drain remaining elements on drop
433        while self.pop().is_some() {}
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use std::sync::Arc;
441    use std::thread;
442
443    #[test]
444    fn supports_fifo_push_and_pop() {
445        let ring = SpscRingBuffer::<i32>::new(RingConfig::default());
446        assert!(ring.push(1).is_ok());
447        assert!(ring.push(2).is_ok());
448        assert_eq!(ring.len(), 2);
449        assert_eq!(ring.pop(), Some(1));
450        assert_eq!(ring.pop(), Some(2));
451        assert!(ring.is_empty());
452    }
453
454    #[test]
455    fn returns_full_when_capacity_is_reached() {
456        let config = RingConfig {
457            capacity: 2,
458            backpressure_threshold: 1,
459        };
460        let ring = SpscRingBuffer::<i32>::new(config);
461        assert!(ring.push(1).is_ok());
462        assert!(ring.push(2).is_ok());
463        assert_eq!(ring.push(3), Err(3));
464    }
465
466    #[test]
467    fn supports_single_producer_single_consumer_threads() {
468        let ring = Arc::new(SpscRingBuffer::<i32>::new(RingConfig::default()));
469        let producer_ring = Arc::clone(&ring);
470        let consumer_ring = Arc::clone(&ring);
471
472        let producer = thread::spawn(move || {
473            for value in 0..1_000 {
474                loop {
475                    if producer_ring.push(value).is_ok() {
476                        break;
477                    }
478                    std::thread::yield_now();
479                }
480            }
481        });
482
483        let consumer = thread::spawn(move || {
484            let mut values = Vec::new();
485            while values.len() < 1_000 {
486                if let Some(value) = consumer_ring.pop() {
487                    values.push(value);
488                } else {
489                    std::thread::yield_now();
490                }
491            }
492            values
493        });
494
495        producer.join().unwrap();
496        let values = consumer.join().unwrap();
497
498        assert_eq!(values.len(), 1_000);
499        assert_eq!(values.first(), Some(&0));
500        assert_eq!(values.last(), Some(&999));
501    }
502
503    #[test]
504    fn handles_wrap_around_correctly() {
505        let ring = SpscRingBuffer::<i32>::new(RingConfig::default());
506
507        // Fill most of the ring
508        for i in 0..1020 {
509            ring.push(i).unwrap();
510        }
511
512        assert_eq!(ring.len(), 1020);
513
514        // Pop all elements in FIFO order
515        for i in 0..1020 {
516            assert_eq!(ring.pop(), Some(i));
517        }
518
519        assert!(ring.is_empty());
520    }
521
522    #[test]
523    fn backpressure_threshold_works() {
524        let config = RingConfig {
525            capacity: 100,
526            backpressure_threshold: 70,
527        };
528        let ring = SpscRingBuffer::<i32>::new(config);
529
530        // Fill to threshold - should not backpressure
531        for i in 0..69 {
532            ring.push(i).unwrap();
533        }
534        assert!(!ring.should_backpressure());
535
536        // Fill one more - should backpressure
537        let result = ring.push(70);
538        assert!(result.is_ok()); // Push succeeds until capacity (100) is reached
539        assert!(ring.should_backpressure());
540    }
541
542    #[test]
543    fn wait_for_space_releases() {
544        let ring = Arc::new(SpscRingBuffer::<i32>::new(RingConfig::default()));
545        let consumer_ring = Arc::clone(&ring);
546
547        // Fill the ring
548        for i in 0..1020 {
549            ring.push(i).unwrap();
550        }
551
552        // Consumer pops one element
553        consumer_ring.pop();
554
555        // Producer should be able to push again
556        let result = ring.push(1021);
557        assert!(result.is_ok());
558    }
559
560    #[test]
561    fn wait_for_data_releases() {
562        let ring = Arc::new(SpscRingBuffer::<i32>::new(RingConfig::default()));
563        let producer_ring = Arc::clone(&ring);
564
565        // Ring is empty
566
567        // Producer pushes one element
568        producer_ring.push(42).unwrap();
569
570        // Consumer should be able to pop now
571        assert_eq!(ring.pop(), Some(42));
572    }
573
574    #[test]
575    fn push_batch_writes_multiple_elements() {
576        let ring = SpscRingBuffer::<u64>::new(RingConfig::default());
577        let data: Vec<u64> = (0..100).collect();
578        let n = ring.push_batch(&data);
579        assert_eq!(n, 100);
580        assert_eq!(ring.len(), 100);
581    }
582
583    #[test]
584    fn push_batch_respects_capacity() {
585        let config = RingConfig {
586            capacity: 10,
587            backpressure_threshold: 8,
588        };
589        let ring = SpscRingBuffer::<u64>::new(config);
590        let data: Vec<u64> = (0..100).collect();
591        let n = ring.push_batch(&data);
592        assert_eq!(n, 10);
593        assert_eq!(ring.len(), 10);
594        // The 90 elements that didn't fit must be observable via overflow_count,
595        // otherwise backpressure through the batch path is invisible to monitoring.
596        assert_eq!(ring.overflow_count(), 90);
597    }
598
599    #[test]
600    fn pop_batch_reads_multiple_elements() {
601        let ring = SpscRingBuffer::<u64>::new(RingConfig::default());
602        for i in 0..50u64 {
603            ring.push(i).unwrap();
604        }
605        let mut out = vec![std::mem::MaybeUninit::uninit(); 20];
606        let n = ring.pop_batch(&mut out);
607        assert_eq!(n, 20);
608        let drained: Vec<u64> = out[..n]
609            .iter()
610            .map(|x| unsafe { x.assume_init_read() })
611            .collect();
612        assert_eq!(drained, (0..20).collect::<Vec<u64>>());
613        assert_eq!(ring.len(), 30);
614    }
615
616    #[test]
617    fn pop_batch_returns_available_count() {
618        let ring = SpscRingBuffer::<u64>::new(RingConfig::default());
619        for i in 0..5u64 {
620            ring.push(i).unwrap();
621        }
622        let mut out = vec![std::mem::MaybeUninit::uninit(); 100];
623        let n = ring.pop_batch(&mut out);
624        assert_eq!(n, 5);
625    }
626
627    #[test]
628    fn push_pop_batch_round_trip_spsc() {
629        let ring = Arc::new(SpscRingBuffer::<u64>::new(RingConfig::default()));
630        let prod = Arc::clone(&ring);
631        let cons = Arc::clone(&ring);
632        let iters = 5_000u64;
633        let batch_size = 64;
634
635        let h_prod = std::thread::spawn(move || {
636            let mut sent = 0u64;
637            while sent < iters {
638                let chunk_size = batch_size.min((iters - sent) as usize);
639                let chunk: Vec<u64> = (sent..sent + chunk_size as u64).collect();
640                let n = prod.push_batch(&chunk);
641                sent += n as u64;
642                if n == 0 {
643                    prod.wait_for_space();
644                }
645            }
646        });
647
648        let h_cons = std::thread::spawn(move || {
649            let mut received = 0u64;
650            let mut buf = vec![std::mem::MaybeUninit::uninit(); batch_size];
651            while received < iters {
652                let n = cons.pop_batch(&mut buf);
653                if n > 0 {
654                    received += n as u64;
655                } else {
656                    cons.wait_for_data();
657                }
658            }
659        });
660
661        h_prod.join().unwrap();
662        h_cons.join().unwrap();
663        assert!(ring.is_empty());
664    }
665
666    #[test]
667    fn write_read_available_tracking() {
668        let config = RingConfig {
669            capacity: 100,
670            backpressure_threshold: 70,
671        };
672        let ring = SpscRingBuffer::<u64>::new(config);
673        assert_eq!(ring.write_available(), 100);
674        assert_eq!(ring.read_available(), 0);
675
676        for i in 0..30u64 {
677            ring.push(i).unwrap();
678        }
679        assert_eq!(ring.write_available(), 70);
680        assert_eq!(ring.read_available(), 30);
681
682        // pop() advances head; write_available() is conservatively stale
683        // (cached_head not refreshed since no push failed), so it may report
684        // slightly less than the true capacity.
685        ring.pop();
686        assert_eq!(ring.read_available(), 29);
687        let wa = ring.write_available();
688        assert!(
689            wa == 70 || wa == 71,
690            "write_available should be 70 (conservative) or 71 (true): got {wa}"
691        );
692    }
693}