photon_ring/wait.rs
1// Copyright 2026 Photon Ring Contributors
2// SPDX-License-Identifier: 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 | Dedicated, pinned 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 | Dedicated, pinned 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}