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 | 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}