Skip to main content

photon_ring/
wait.rs

1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Wait strategies for blocking receive operations.
5//!
6//! [`WaitStrategy`] controls how a consumer thread waits when no message is
7//! available. Choose based on your latency vs CPU usage requirements:
8//!
9//! | Strategy | Latency | CPU usage | Best for |
10//! |---|---|---|---|
11//! | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | HFT, dedicated cores |
12//! | `YieldSpin` | Low (~1-5 us wakeup) | High | Shared cores, SMT |
13//! | `Park` | Medium (~10-50 us wakeup) | Near zero | Background consumers |
14//! | `Adaptive` | Auto-scaling | Varies | General purpose |
15
16/// Strategy for blocking `recv()` and `SubscriberGroup::recv()`.
17///
18/// Controls how the consumer thread waits when no message is available.
19/// Choose based on your latency vs CPU usage requirements:
20///
21/// | Strategy | Latency | CPU usage | Best for |
22/// |---|---|---|---|
23/// | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | HFT, dedicated cores |
24/// | `YieldSpin` | Low (~1-5 us wakeup) | High | Shared cores, SMT |
25/// | `Park` | Medium (~10-50 us wakeup) | Near zero | Background consumers |
26/// | `Adaptive` | Auto-scaling | Varies | General purpose |
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WaitStrategy {
29    /// Pure busy-spin with no PAUSE instruction. Minimum wakeup latency
30    /// but consumes 100% of one CPU core. Use on dedicated, pinned cores.
31    BusySpin,
32
33    /// Spin with `thread::yield_now()` between iterations. Yields the
34    /// OS time slice to other threads on the same core. Good for SMT.
35    #[cfg(feature = "std")]
36    YieldSpin,
37
38    /// `thread::park()` / `unpark()` based waiting. Near-zero CPU usage
39    /// when idle but higher wakeup latency (~10-50 us depending on OS).
40    #[cfg(feature = "std")]
41    Park,
42
43    /// Three-phase escalation: busy-spin for `spin_iters` iterations,
44    /// then yield for `yield_iters`, then park. Balances latency and CPU.
45    ///
46    /// On `no_std` (without the `std` feature), the yield and park phases
47    /// fall back to `core::hint::spin_loop()`.
48    Adaptive {
49        /// Number of bare-spin iterations before escalating.
50        spin_iters: u32,
51        /// Number of yield iterations before parking (or PAUSE-spinning on `no_std`).
52        yield_iters: u32,
53    },
54}
55
56impl Default for WaitStrategy {
57    fn default() -> Self {
58        WaitStrategy::Adaptive {
59            spin_iters: 64,
60            yield_iters: 64,
61        }
62    }
63}
64
65impl WaitStrategy {
66    /// Execute one wait iteration. Called by `recv_with` on each loop when
67    /// `try_recv` returns `Empty`.
68    ///
69    /// `iter` is the zero-based iteration count since the last successful
70    /// receive — it drives the phase transitions in `Adaptive`.
71    #[inline]
72    pub(crate) fn wait(&self, iter: u32) {
73        match self {
74            WaitStrategy::BusySpin => {
75                // No hint, no yield — pure busy loop.
76            }
77            #[cfg(feature = "std")]
78            WaitStrategy::YieldSpin => {
79                std::thread::yield_now();
80            }
81            #[cfg(feature = "std")]
82            WaitStrategy::Park => {
83                std::thread::park();
84            }
85            WaitStrategy::Adaptive {
86                spin_iters,
87                yield_iters,
88            } => {
89                if iter < *spin_iters {
90                    // Phase 1: bare spin
91                } else if iter < spin_iters + yield_iters {
92                    // Phase 2: yield (or spin_loop on no_std)
93                    #[cfg(feature = "std")]
94                    {
95                        std::thread::yield_now();
96                    }
97                    #[cfg(not(feature = "std"))]
98                    {
99                        core::hint::spin_loop();
100                    }
101                } else {
102                    // Phase 3: park (or spin_loop on no_std)
103                    #[cfg(feature = "std")]
104                    {
105                        std::thread::park();
106                    }
107                    #[cfg(not(feature = "std"))]
108                    {
109                        core::hint::spin_loop();
110                    }
111                }
112            }
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn default_is_adaptive() {
123        let ws = WaitStrategy::default();
124        assert_eq!(
125            ws,
126            WaitStrategy::Adaptive {
127                spin_iters: 64,
128                yield_iters: 64,
129            }
130        );
131    }
132
133    #[test]
134    fn busy_spin_returns_immediately() {
135        let ws = WaitStrategy::BusySpin;
136        // Should not block — just verify it completes.
137        for i in 0..1000 {
138            ws.wait(i);
139        }
140    }
141
142    #[cfg(feature = "std")]
143    #[test]
144    fn yield_spin_returns() {
145        let ws = WaitStrategy::YieldSpin;
146        for i in 0..100 {
147            ws.wait(i);
148        }
149    }
150
151    #[test]
152    fn adaptive_phases() {
153        let ws = WaitStrategy::Adaptive {
154            spin_iters: 4,
155            yield_iters: 4,
156        };
157        // Phase 1 (spin): iters 0..4
158        for i in 0..4 {
159            ws.wait(i);
160        }
161        // Phase 2 (yield): iters 4..8
162        for i in 4..8 {
163            ws.wait(i);
164        }
165        // Phase 3 (park/spin_loop): iter 8+
166        // On std this would park, but since no one unparks we only test
167        // non-park path here. The park path is tested via recv_with integration.
168        #[cfg(not(feature = "std"))]
169        {
170            ws.wait(8);
171            ws.wait(100);
172        }
173    }
174
175    #[test]
176    fn clone_and_copy() {
177        let ws = WaitStrategy::BusySpin;
178        let ws2 = ws;
179        #[allow(clippy::clone_on_copy)]
180        let ws3 = ws.clone();
181        assert_eq!(ws, ws2);
182        assert_eq!(ws, ws3);
183    }
184
185    #[test]
186    fn debug_format() {
187        use alloc::format;
188        let ws = WaitStrategy::BusySpin;
189        let s = format!("{ws:?}");
190        assert!(s.contains("BusySpin"));
191    }
192}