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}