Skip to main content

photon_ring/
ring.rs

1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::pod::Pod;
5use crate::slot::Slot;
6use alloc::boxed::Box;
7use alloc::sync::{Arc, Weak};
8use alloc::vec::Vec;
9use core::sync::atomic::AtomicU64;
10use spin::Mutex;
11
12/// Cache-line padding to prevent false sharing between hot atomics.
13///
14/// Wraps a value with `#[repr(align(64))]` to ensure it occupies its
15/// own cache line. Used internally for cursor trackers and the ring
16/// cursor.
17///
18/// Exposed publicly so that [`DependencyBarrier::new`](crate::DependencyBarrier::new)
19/// and [`Subscriber::tracker`](crate::Subscriber::tracker) can refer to
20/// `Arc<Padded<AtomicU64>>` in their signatures.
21#[repr(align(64))]
22pub struct Padded<T>(pub T);
23
24// ---------------------------------------------------------------------------
25// RingIndex — encapsulates slot indexing for both pow2 and arbitrary capacity
26// ---------------------------------------------------------------------------
27
28/// Precomputed indexing constants for mapping sequence numbers to ring slots.
29///
30/// For power-of-two capacities, uses bitwise AND (single-cycle, ~0.3 ns).
31/// For arbitrary capacities, uses Lemire's fastmod algorithm (~1.5 ns):
32/// two 64-bit multiplies with no division instruction.
33///
34/// Reference: Daniel Lemire, "Faster Remainder by Direct Computation" (2019),
35/// <https://arxiv.org/abs/1902.01961>
36#[derive(Clone, Copy)]
37pub(crate) struct RingIndex {
38    /// Ring capacity.
39    pub(crate) capacity: u64,
40    /// For power-of-two: `capacity - 1`. For arbitrary: unused but harmless.
41    pub(crate) mask: u64,
42    /// Precomputed reciprocal for fast modulo: `floor(2^64 / capacity)`.
43    /// Used to approximate `n / capacity` via `mulhi(n, reciprocal)`.
44    pub(crate) reciprocal: u64,
45    /// True if capacity is a power of two (use AND instead of fastmod).
46    pub(crate) is_pow2: bool,
47}
48
49impl RingIndex {
50    /// Create a new `RingIndex` for the given capacity.
51    ///
52    /// # Panics
53    ///
54    /// Panics if `capacity < 2`.
55    pub(crate) fn new(capacity: usize) -> Self {
56        assert!(capacity >= 2, "capacity must be at least 2");
57        let cap = capacity as u64;
58        let is_pow2 = capacity.is_power_of_two();
59        let mask = if is_pow2 { cap - 1 } else { 0 };
60        // Reciprocal: floor(2^64 / d). Used with mulhi to approximate n/d.
61        let reciprocal = ((1u128 << 64) / cap as u128) as u64;
62        RingIndex {
63            capacity: cap,
64            mask,
65            reciprocal,
66            is_pow2,
67        }
68    }
69
70    /// Map a sequence number to a slot index.
71    ///
72    /// Power-of-two: bitwise AND (~0.3 ns). Arbitrary: reciprocal multiply
73    /// (~1.5 ns). The branch is perfectly predicted (always the same direction
74    /// after warmup).
75    #[inline(always)]
76    pub(crate) fn slot(&self, seq: u64) -> usize {
77        if self.is_pow2 {
78            (seq & self.mask) as usize
79        } else {
80            let q = ((seq as u128 * self.reciprocal as u128) >> 64) as u64;
81            let mut r = seq - q.wrapping_mul(self.capacity);
82            if r >= self.capacity {
83                r -= self.capacity;
84            }
85            r as usize
86        }
87    }
88}
89
90/// Backpressure state attached to a [`SharedRing`] when created via
91/// [`channel_bounded`](crate::channel::channel_bounded).
92pub(crate) struct BackpressureState {
93    /// How many slots of headroom to leave between the publisher and the
94    /// slowest subscriber.
95    pub(crate) watermark: u64,
96    /// Per-subscriber cursor trackers (weak references). The publisher scans
97    /// these to find the minimum (slowest) cursor when it is close to lapping.
98    /// Weak references prevent a panicked subscriber (that fails to drop) from
99    /// blocking the publisher forever.
100    pub(crate) trackers: Mutex<Vec<Weak<Padded<AtomicU64>>>>,
101}
102
103/// Shared ring buffer: a pre-allocated array of seqlock-stamped slots
104/// plus the producer cursor.
105///
106/// The cursor stores the sequence number of the last published message
107/// (`u64::MAX` means nothing published yet).
108pub(crate) struct SharedRing<T> {
109    slots: Box<[Slot<T>]>,
110    pub(crate) index: RingIndex,
111    pub(crate) cursor: Padded<AtomicU64>,
112    /// Present only for bounded (backpressure-capable) channels.
113    pub(crate) backpressure: Option<BackpressureState>,
114    /// Shared sequence counter for multi-producer channels.
115    /// `None` for SPMC channels, `Some` for MPMC channels.
116    pub(crate) next_seq: Option<Padded<AtomicU64>>,
117}
118
119impl<T: Pod> SharedRing<T> {
120    pub(crate) fn new(capacity: usize) -> Self {
121        let index = RingIndex::new(capacity);
122
123        let slots: Vec<Slot<T>> = (0..capacity).map(|_| Slot::new()).collect();
124
125        SharedRing {
126            slots: slots.into_boxed_slice(),
127            index,
128            cursor: Padded(AtomicU64::new(u64::MAX)),
129            backpressure: None,
130            next_seq: None,
131        }
132    }
133
134    pub(crate) fn new_bounded(capacity: usize, watermark: usize) -> Self {
135        assert!(capacity >= 2, "capacity must be at least 2");
136        assert!(watermark < capacity, "watermark must be less than capacity");
137
138        let index = RingIndex::new(capacity);
139        let slots: Vec<Slot<T>> = (0..capacity).map(|_| Slot::new()).collect();
140
141        SharedRing {
142            slots: slots.into_boxed_slice(),
143            index,
144            cursor: Padded(AtomicU64::new(u64::MAX)),
145            backpressure: Some(BackpressureState {
146                watermark: watermark as u64,
147                trackers: Mutex::new(Vec::new()),
148            }),
149            next_seq: None,
150        }
151    }
152
153    pub(crate) fn new_mpmc(capacity: usize) -> Self {
154        let index = RingIndex::new(capacity);
155
156        let slots: Vec<Slot<T>> = (0..capacity).map(|_| Slot::new()).collect();
157
158        SharedRing {
159            slots: slots.into_boxed_slice(),
160            index,
161            cursor: Padded(AtomicU64::new(u64::MAX)),
162            backpressure: None,
163            next_seq: Some(Padded(AtomicU64::new(0))),
164        }
165    }
166
167    /// Raw pointer to the start of the slot array.
168    #[inline]
169    pub(crate) fn slots_ptr(&self) -> *const Slot<T> {
170        self.slots.as_ptr()
171    }
172
173    /// Raw pointer to the cursor atomic.
174    #[inline]
175    pub(crate) fn cursor_ptr(&self) -> *const AtomicU64 {
176        &self.cursor.0 as *const AtomicU64
177    }
178
179    /// Total byte length of the slot array.
180    #[cfg(all(target_os = "linux", feature = "hugepages"))]
181    #[inline]
182    pub(crate) fn slots_byte_len(&self) -> usize {
183        self.slots.len() * core::mem::size_of::<Slot<T>>()
184    }
185
186    #[inline]
187    pub(crate) fn capacity(&self) -> u64 {
188        self.index.capacity
189    }
190
191    /// Register a new subscriber tracker and return it.
192    /// Only meaningful when backpressure is enabled; returns `None` otherwise.
193    pub(crate) fn register_tracker(&self, initial: u64) -> Option<Arc<Padded<AtomicU64>>> {
194        let bp = self.backpressure.as_ref()?;
195        let tracker = Arc::new(Padded(AtomicU64::new(initial)));
196        bp.trackers.lock().push(Arc::downgrade(&tracker));
197        Some(tracker)
198    }
199
200    /// Pick a subscriber's start position and register its tracker atomically
201    /// with respect to the publisher's tracker scan.
202    ///
203    /// Reading the head cursor and registering separately is racy: the publisher
204    /// only rescans trackers when its cached slowest cursor says it is close to
205    /// lapping (see `Publisher::has_room`). A subscriber that reads the head,
206    /// stalls, and registers late can therefore be invisible to a publisher
207    /// whose cached value came from a faster consumer — and be lapped before the
208    /// next rescan, losing messages it was promised.
209    ///
210    /// Taking the tracker lock across both steps closes that window. Any rescan
211    /// ordered before this call observed the tracker set when the head was no
212    /// further along than the value read here, which bounds the publisher's
213    /// cached budget below this subscriber's first slot; any rescan ordered
214    /// after it sees this tracker.
215    ///
216    /// Returns the start sequence and the tracker (`None` on a lossy channel,
217    /// where nothing gates the publisher and no window exists).
218    pub(crate) fn register_tracker_at_head(&self) -> (u64, Option<Arc<Padded<AtomicU64>>>) {
219        let Some(bp) = self.backpressure.as_ref() else {
220            let head = self.cursor.0.load(core::sync::atomic::Ordering::Acquire);
221            return (if head == u64::MAX { 0 } else { head + 1 }, None);
222        };
223        let mut trackers = bp.trackers.lock();
224        let head = self.cursor.0.load(core::sync::atomic::Ordering::Acquire);
225        let start = if head == u64::MAX { 0 } else { head + 1 };
226        let tracker = Arc::new(Padded(AtomicU64::new(start)));
227        trackers.push(Arc::downgrade(&tracker));
228        (start, Some(tracker))
229    }
230
231    /// Scan all subscriber trackers and return the minimum cursor.
232    /// Returns `None` if there are no live subscribers. Dead (dropped)
233    /// trackers are pruned during the scan.
234    #[inline]
235    pub(crate) fn slowest_cursor(&self) -> Option<u64> {
236        let bp = self.backpressure.as_ref()?;
237        let mut trackers = bp.trackers.lock();
238        let mut min = u64::MAX;
239        let mut has_live = false;
240        trackers.retain(|weak| {
241            if let Some(arc) = weak.upgrade() {
242                let val = arc.0.load(core::sync::atomic::Ordering::Relaxed);
243                if val < min {
244                    min = val;
245                }
246                has_live = true;
247                true // retain live tracker
248            } else {
249                false // prune dead tracker
250            }
251        });
252        if has_live {
253            Some(min)
254        } else {
255            None
256        }
257    }
258}