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. All strategies are `no_std` compatible.
8//!
9//! | Strategy | Latency | CPU usage | Best for |
10//! |---|---|---|---|
11//! | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | HFT, dedicated cores |
12//! | `YieldSpin` | Low (~30 ns on x86) | High | Shared cores, SMT |
13//! | `BackoffSpin` | Medium (exponential) | Decreasing | Background consumers |
14//! | `Adaptive` | Auto-scaling | Varies | General purpose |
15
16/// Strategy for blocking `recv()` and `SubscriberGroup::recv()`.
17///
18/// All variants are `no_std` compatible — no OS thread primitives required.
19///
20/// | Strategy | Latency | CPU usage | Best for |
21/// |---|---|---|---|
22/// | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | HFT, dedicated cores |
23/// | `YieldSpin` | Low (~30 ns on x86) | High | Shared cores, SMT |
24/// | `BackoffSpin` | Medium (exponential) | Decreasing | Background consumers |
25/// | `Adaptive` | Auto-scaling | Varies | General purpose |
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum WaitStrategy {
28    /// Pure busy-spin with no PAUSE instruction. Minimum wakeup latency
29    /// but consumes 100% of one CPU core. Use on dedicated, pinned cores.
30    BusySpin,
31
32    /// Spin with `core::hint::spin_loop()` (PAUSE on x86, YIELD on ARM)
33    /// between iterations. Yields the CPU pipeline to the SMT sibling
34    /// and reduces power consumption vs `BusySpin`.
35    YieldSpin,
36
37    /// Exponential backoff spin. Starts with bare spins, then escalates
38    /// to PAUSE-based spins with increasing delays. Good for consumers
39    /// that may be idle for extended periods without burning a full core.
40    BackoffSpin,
41
42    /// Three-phase escalation: bare spin for `spin_iters` iterations,
43    /// then PAUSE-spin for `yield_iters`, then repeated PAUSE bursts.
44    Adaptive {
45        /// Number of bare-spin iterations before escalating to PAUSE.
46        spin_iters: u32,
47        /// Number of PAUSE iterations before entering deep backoff.
48        yield_iters: u32,
49    },
50}
51
52impl Default for WaitStrategy {
53    fn default() -> Self {
54        WaitStrategy::Adaptive {
55            spin_iters: 64,
56            yield_iters: 64,
57        }
58    }
59}
60
61impl WaitStrategy {
62    /// Execute one wait iteration. Called by `recv_with` on each loop when
63    /// `try_recv` returns `Empty`.
64    ///
65    /// `iter` is the zero-based iteration count since the last successful
66    /// receive — it drives phase transitions in `Adaptive` and `BackoffSpin`.
67    #[inline]
68    pub(crate) fn wait(&self, iter: u32) {
69        match self {
70            WaitStrategy::BusySpin => {
71                // No hint — pure busy loop. Fastest wakeup, highest power.
72            }
73            WaitStrategy::YieldSpin => {
74                // PAUSE on x86, YIELD on ARM, WFE-hint on RISC-V.
75                core::hint::spin_loop();
76            }
77            WaitStrategy::BackoffSpin => {
78                // Exponential backoff: more PAUSE iterations as we wait longer.
79                let pauses = 1u32.wrapping_shl(iter.min(6)); // 1, 2, 4, 8, 16, 32, 64
80                for _ in 0..pauses {
81                    core::hint::spin_loop();
82                }
83            }
84            WaitStrategy::Adaptive {
85                spin_iters,
86                yield_iters,
87            } => {
88                if iter < *spin_iters {
89                    // Phase 1: bare spin — fastest wakeup.
90                } else if iter < spin_iters + yield_iters {
91                    // Phase 2: PAUSE-spin — yields pipeline.
92                    core::hint::spin_loop();
93                } else {
94                    // Phase 3: deep backoff — multiple PAUSE per iteration.
95                    for _ in 0..8 {
96                        core::hint::spin_loop();
97                    }
98                }
99            }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn default_is_adaptive() {
110        let ws = WaitStrategy::default();
111        assert_eq!(
112            ws,
113            WaitStrategy::Adaptive {
114                spin_iters: 64,
115                yield_iters: 64,
116            }
117        );
118    }
119
120    #[test]
121    fn busy_spin_returns_immediately() {
122        let ws = WaitStrategy::BusySpin;
123        for i in 0..1000 {
124            ws.wait(i);
125        }
126    }
127
128    #[test]
129    fn yield_spin_returns() {
130        let ws = WaitStrategy::YieldSpin;
131        for i in 0..100 {
132            ws.wait(i);
133        }
134    }
135
136    #[test]
137    fn backoff_spin_returns() {
138        let ws = WaitStrategy::BackoffSpin;
139        for i in 0..20 {
140            ws.wait(i);
141        }
142    }
143
144    #[test]
145    fn adaptive_phases() {
146        let ws = WaitStrategy::Adaptive {
147            spin_iters: 4,
148            yield_iters: 4,
149        };
150        for i in 0..20 {
151            ws.wait(i);
152        }
153    }
154
155    #[test]
156    fn clone_and_copy() {
157        let ws = WaitStrategy::BusySpin;
158        let ws2 = ws;
159        #[allow(clippy::clone_on_copy)]
160        let ws3 = ws.clone();
161        assert_eq!(ws, ws2);
162        assert_eq!(ws, ws3);
163    }
164
165    #[test]
166    fn debug_format() {
167        use alloc::format;
168        let ws = WaitStrategy::BusySpin;
169        let s = format!("{ws:?}");
170        assert!(s.contains("BusySpin"));
171    }
172}