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 | 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//! | `MonitorWaitFallback` | Near-zero (~30 ns on Intel) | Near-zero | Intel Alder Lake+ |
16//!
17//! # Platform-specific optimizations
18//!
19//! On **aarch64**, `YieldSpin` and `BackoffSpin` use the `WFE` (Wait For
20//! Event) instruction instead of `core::hint::spin_loop()` (which maps to
21//! `YIELD`). `WFE` puts the core into a low-power state until an event —
22//! such as a cache line invalidation from the publisher's store — wakes it.
23//! The `SEVL` + `WFE` pattern is used: `SEVL` sets the local event register
24//! so the first `WFE` doesn't block unconditionally.
25//!
26//! On **x86/x86_64**, `core::hint::spin_loop()` emits `PAUSE`, which is the
27//! standard spin-wait hint (~140 cycles on Skylake+).
28//!
29//! On recent Intel (Alder Lake+), the `MonitorWaitFallback`
30//! strategy uses `TPAUSE` for near-zero-power wakeup,
31//! gated at runtime on the `WAITPKG` CPUID feature.
32
33/// Strategy for blocking `recv()`.
34///
35/// All variants are `no_std` compatible — no OS thread primitives required.
36///
37/// | Strategy | Latency | CPU usage | Best for |
38/// |---|---|---|---|
39/// | `BusySpin` | Lowest (~0 ns wakeup) | 100% core | Dedicated, pinned cores |
40/// | `YieldSpin` | Low (~30 ns on x86) | High | Shared cores, SMT |
41/// | `BackoffSpin` | Medium (exponential) | Decreasing | Background consumers |
42/// | `Adaptive` | Auto-scaling | Varies | General purpose |
43/// | `MonitorWaitFallback` | Near-zero (~30 ns on Intel) | Near-zero | Intel Alder Lake+ |
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum WaitStrategy {
46 /// Pure busy-spin with no PAUSE instruction. Minimum wakeup latency
47 /// but consumes 100% of one CPU core. Use on dedicated, pinned cores.
48 BusySpin,
49
50 /// Spin with `core::hint::spin_loop()` (PAUSE on x86, YIELD on ARM)
51 /// between iterations. Yields the CPU pipeline to the SMT sibling
52 /// and reduces power consumption vs `BusySpin`.
53 YieldSpin,
54
55 /// Exponential backoff spin. Starts with bare spins, then escalates
56 /// to PAUSE-based spins with increasing delays. Good for consumers
57 /// that may be idle for extended periods without burning a full core.
58 BackoffSpin,
59
60 /// Three-phase escalation: bare spin for `spin_iters` iterations,
61 /// then PAUSE-spin for `yield_iters`, then repeated PAUSE bursts.
62 Adaptive {
63 /// Number of bare-spin iterations before escalating to PAUSE.
64 spin_iters: u32,
65 /// Number of PAUSE iterations before entering deep backoff.
66 yield_iters: u32,
67 },
68
69 /// TPAUSE on Intel (Tremont+, Alder Lake+) or WFE on ARM.
70 ///
71 /// On x86_64 with WAITPKG support: `TPAUSE` puts the core into an
72 /// optimized C0.1 state for a bounded interval, giving near-zero power
73 /// consumption with ~30 ns wakeup latency. It monitors no address, which
74 /// is what makes it safe to offer — a variant taking an address to watch
75 /// cannot be constructed safely.
76 ///
77 /// Falls back to `YieldSpin` on x86 CPUs without WAITPKG support.
78 /// On aarch64: uses SEVL+WFE (identical to `YieldSpin`).
79 /// On x86_64 with WAITPKG: `TPAUSE` (timed wait in C0.1 state) for
80 /// low-power waiting. On aarch64: SEVL+WFE. On other x86: PAUSE.
81 MonitorWaitFallback,
82}
83
84impl WaitStrategy {}
85
86impl Default for WaitStrategy {
87 fn default() -> Self {
88 WaitStrategy::Adaptive {
89 spin_iters: 64,
90 yield_iters: 64,
91 }
92 }
93}
94
95/// Check at runtime whether the CPU supports WAITPKG (TPAUSE).
96///
97/// CPUID leaf 7, sub-leaf 0, ECX bit 5.
98#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
99#[inline]
100fn has_waitpkg() -> bool {
101 #[cfg(target_arch = "x86_64")]
102 {
103 let result = core::arch::x86_64::__cpuid_count(7, 0);
104 result.ecx & (1 << 5) != 0
105 }
106 #[cfg(target_arch = "x86")]
107 {
108 let result = core::arch::x86::__cpuid_count(7, 0);
109 result.ecx & (1 << 5) != 0
110 }
111}
112
113/// Cached WAITPKG support flag. Evaluated once via a racy init pattern
114/// (benign data race — worst case is redundant CPUID calls on first access).
115#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
116static WAITPKG_SUPPORT: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(0);
117
118/// 0 = unknown, 1 = not supported, 2 = supported.
119#[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
120#[inline]
121fn waitpkg_supported() -> bool {
122 let cached = WAITPKG_SUPPORT.load(core::sync::atomic::Ordering::Relaxed);
123 if cached != 0 {
124 return cached == 2;
125 }
126 let supported = has_waitpkg();
127 WAITPKG_SUPPORT.store(
128 if supported { 2 } else { 1 },
129 core::sync::atomic::Ordering::Relaxed,
130 );
131 supported
132}
133
134// SAFETY wrapper for the TPAUSE instruction.
135// These are encoded via raw bytes because stable Rust doesn't expose them
136// as intrinsics yet.
137//
138// TPAUSE: timed pause without address monitoring (66 0F AE /6)
139//
140// EDX:EAX = absolute TSC deadline. The instruction exits when either:
141// (b) TSC >= deadline, or
142// (b) an OS-configured timeout (IA32_UMWAIT_CONTROL MSR) fires.
143//
144// We set the deadline ~100µs in the future — long enough to actually
145// enter a low-power state, short enough to bound worst-case latency
146// if the wakeup event is missed (e.g., the store happened between
147// TPAUSE).
148#[cfg(target_arch = "x86_64")]
149mod umwait {
150 /// Read the TSC and return a deadline ~100µs in the future.
151 /// On a 3 GHz CPU, 100µs ≈ 300,000 cycles.
152 ///
153 /// Note: The 300,000 cycle offset assumes ~3 GHz TSC frequency. On slower
154 /// CPUs (1 GHz), this becomes ~300 µs; on faster CPUs (5 GHz), ~60 µs.
155 /// The deadline is a safety bound, not a precision target.
156 #[inline(always)]
157 fn deadline_100us() -> (u32, u32) {
158 // Read the TSC once.
159 let tsc = unsafe { core::arch::x86_64::_rdtsc() };
160 let deadline = tsc.wrapping_add(300_000); // ~100µs at 3 GHz
161 (deadline as u32, (deadline >> 32) as u32) // (eax, edx)
162 }
163
164 /// Timed pause without address monitoring. Enters C0.1 state
165 /// until the deadline (~100µs from now).
166 /// `ctrl` = 0 for C0.2, 1 for C0.1.
167 #[inline(always)]
168 pub(super) unsafe fn tpause(ctrl: u32) {
169 let (lo, hi) = deadline_100us();
170 // TPAUSE ecx: 66 0F AE /6 (with ecx for control)
171 core::arch::asm!(
172 ".byte 0x66, 0x0f, 0xae, 0xf1", // TPAUSE ecx
173 in("ecx") ctrl,
174 in("edx") hi,
175 in("eax") lo,
176 options(nostack, preserves_flags),
177 );
178 }
179}
180
181impl WaitStrategy {
182 /// Execute one wait iteration. Called by `recv_with` on each loop when
183 /// `try_recv` returns `Empty`.
184 ///
185 /// `iter` is the zero-based iteration count since the last successful
186 /// receive — it drives phase transitions in `Adaptive` and `BackoffSpin`.
187 #[inline]
188 pub(crate) fn wait(&self, iter: u32) {
189 match self {
190 WaitStrategy::BusySpin => {
191 // No hint — pure busy loop. Fastest wakeup, highest power.
192 }
193 WaitStrategy::YieldSpin => {
194 // On aarch64: SEVL + WFE puts the core into a low-power
195 // state until a cache-line event wakes it. SEVL sets the
196 // local event register so the first WFE returns immediately
197 // (avoids unconditional blocking).
198 // On x86: PAUSE yields the pipeline to the SMT sibling.
199 #[cfg(target_arch = "aarch64")]
200 unsafe {
201 core::arch::asm!("sevl", options(nomem, nostack));
202 core::arch::asm!("wfe", options(nomem, nostack));
203 }
204 #[cfg(not(target_arch = "aarch64"))]
205 core::hint::spin_loop();
206 }
207 WaitStrategy::BackoffSpin => {
208 // Exponential backoff: more iterations as we wait longer.
209 // On aarch64: WFE sleeps until a cache-line event, making
210 // each iteration near-zero power. On x86: PAUSE yields the
211 // pipeline with ~140 cycle delay per iteration.
212 let pauses = 1u32.wrapping_shl(iter.min(6)); // 1, 2, 4, 8, 16, 32, 64
213 for _ in 0..pauses {
214 #[cfg(target_arch = "aarch64")]
215 unsafe {
216 core::arch::asm!("wfe", options(nomem, nostack));
217 }
218 #[cfg(not(target_arch = "aarch64"))]
219 core::hint::spin_loop();
220 }
221 }
222 WaitStrategy::Adaptive {
223 spin_iters,
224 yield_iters,
225 } => {
226 if iter < *spin_iters {
227 // Phase 1: bare spin — fastest wakeup.
228 } else if iter < spin_iters + yield_iters {
229 // Phase 2: PAUSE-spin — yields pipeline.
230 core::hint::spin_loop();
231 } else {
232 // Phase 3: deep backoff — multiple PAUSE per iteration.
233 for _ in 0..8 {
234 core::hint::spin_loop();
235 }
236 }
237 }
238 WaitStrategy::MonitorWaitFallback => {
239 // On x86_64 with WAITPKG: TPAUSE enters C0.1 without
240 // address monitoring — still saves power vs PAUSE.
241 // On aarch64: SEVL + WFE.
242 // Elsewhere: PAUSE.
243 #[cfg(target_arch = "x86_64")]
244 {
245 if waitpkg_supported() {
246 unsafe {
247 umwait::tpause(1); // C0.1
248 }
249 } else {
250 core::hint::spin_loop();
251 }
252 }
253 #[cfg(target_arch = "aarch64")]
254 unsafe {
255 core::arch::asm!("sevl", options(nomem, nostack));
256 core::arch::asm!("wfe", options(nomem, nostack));
257 }
258 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
259 core::hint::spin_loop();
260 }
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn default_is_adaptive() {
271 let ws = WaitStrategy::default();
272 assert_eq!(
273 ws,
274 WaitStrategy::Adaptive {
275 spin_iters: 64,
276 yield_iters: 64,
277 }
278 );
279 }
280
281 #[test]
282 fn busy_spin_returns_immediately() {
283 let ws = WaitStrategy::BusySpin;
284 for i in 0..1000 {
285 ws.wait(i);
286 }
287 }
288
289 #[test]
290 fn yield_spin_returns() {
291 let ws = WaitStrategy::YieldSpin;
292 for i in 0..100 {
293 ws.wait(i);
294 }
295 }
296
297 #[test]
298 fn backoff_spin_returns() {
299 let ws = WaitStrategy::BackoffSpin;
300 for i in 0..20 {
301 ws.wait(i);
302 }
303 }
304
305 #[test]
306 fn adaptive_phases() {
307 let ws = WaitStrategy::Adaptive {
308 spin_iters: 4,
309 yield_iters: 4,
310 };
311 for i in 0..20 {
312 ws.wait(i);
313 }
314 }
315
316 #[test]
317 fn clone_and_copy() {
318 let ws = WaitStrategy::BusySpin;
319 let ws2 = ws;
320 #[allow(clippy::clone_on_copy)]
321 let ws3 = ws.clone();
322 assert_eq!(ws, ws2);
323 assert_eq!(ws, ws3);
324 }
325
326 #[test]
327 fn debug_format() {
328 use alloc::format;
329 let ws = WaitStrategy::BusySpin;
330 let s = format!("{ws:?}");
331 assert!(s.contains("BusySpin"));
332 }
333}