spin_sleep/
lib.rs

1//! Accurate sleeping. Only use native sleep as far as it can be trusted, then spin.
2//!
3//! The problem with `thread::sleep` is it isn't always very accurate, and this accuracy varies
4//! on platform and state. Spinning is as accurate as we can get, but consumes the CPU
5//! rather ungracefully.
6//!
7//! This library adds a middle ground, using a configurable native accuracy setting allowing
8//! `thread::sleep` to wait the bulk of a sleep time, and spin the final section to guarantee
9//! accuracy.
10//!
11//! # Example: Replace `thread::sleep`
12//!
13//! The simplest usage with default native accuracy is a drop in replacement for `thread::sleep`.
14//! ```no_run
15//! # use std::time::Duration;
16//! spin_sleep::sleep(Duration::new(1, 12_550_000));
17//! ```
18//!
19//! # Example: Configure
20//! More advanced usage, including setting a custom native accuracy, can be achieved by
21//! constructing a `SpinSleeper`.
22//! ```no_run
23//! # use std::time::Duration;
24//! // Create a new sleeper that trusts native thread::sleep with 100μs accuracy
25//! let spin_sleeper = spin_sleep::SpinSleeper::new(100_000)
26//!     .with_spin_strategy(spin_sleep::SpinStrategy::YieldThread);
27//!
28//! // Sleep for 1.01255 seconds, this will:
29//! //  - thread:sleep for 1.01245 seconds, i.e., 100μs less than the requested duration
30//! //  - spin until total 1.01255 seconds have elapsed
31//! spin_sleeper.sleep(Duration::new(1, 12_550_000));
32//! ```
33//!
34//! Sleep can also be requested in `f64` seconds or `u64` nanoseconds
35//! (useful when used with `time` crate)
36//!
37//! ```no_run
38//! # use std::time::Duration;
39//! # let spin_sleeper = spin_sleep::SpinSleeper::new(100_000);
40//! spin_sleeper.sleep_s(1.01255);
41//! spin_sleeper.sleep_ns(1_012_550_000);
42//! ```
43//!
44//! OS-specific default settings should be good enough for most cases.
45//! ```
46//! # use spin_sleep::SpinSleeper;
47//! let sleeper = SpinSleeper::default();
48//! # let _ = sleeper;
49//! ```
50mod loop_helper;
51#[cfg(windows)]
52mod windows;
53
54pub use crate::loop_helper::*;
55
56use std::{
57    thread,
58    time::{Duration, Instant},
59};
60
61/// Marker alias to show the meaning of a `f64` in certain methods.
62pub type Seconds = f64;
63/// Marker alias to show the meaning of a `f64` in certain methods.
64pub type RatePerSecond = f64;
65/// Marker alias to show the meaning of a `u64` in certain methods.
66pub type Nanoseconds = u64;
67/// Marker alias to show the meaning of a `u32` in certain methods.
68pub type SubsecondNanoseconds = u32;
69
70/// Accuracy container for spin sleeping. See [crate docs](index.html).
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub struct SpinSleeper {
73    native_accuracy_ns: u32,
74    spin_strategy: SpinStrategy,
75}
76
77#[cfg(not(windows))]
78const DEFAULT_NATIVE_SLEEP_ACCURACY: SubsecondNanoseconds = 125_000;
79
80/// Asks the OS to put the current thread to sleep for at least the specified amount of time.
81/// **Does not spin.**
82///
83/// Equivalent to [`std::thread::sleep`], with the following exceptions:
84/// * **Windows** (>= Windows 10, version 1803): Uses a high resolution waitable timer, similar to std in rust >= 1.75.
85/// * **Windows** (< Windows 10, version 1803): Automatically selects the best native sleep accuracy
86///   generally achieving ~1ms native sleep accuracy, instead of default ~16ms.
87#[inline]
88pub fn native_sleep(duration: Duration) {
89    #[cfg(windows)]
90    windows::native_sleep(duration);
91
92    #[cfg(not(windows))]
93    thread::sleep(duration);
94}
95
96impl Default for SpinSleeper {
97    /// Constructs new SpinSleeper with defaults suiting the current OS
98    #[inline]
99    fn default() -> Self {
100        #[cfg(windows)]
101        let accuracy = windows::sleep_accuracy();
102        #[cfg(not(windows))]
103        let accuracy = DEFAULT_NATIVE_SLEEP_ACCURACY;
104
105        SpinSleeper::new(accuracy)
106    }
107}
108
109impl SpinSleeper {
110    /// Constructs new SpinSleeper with the input native sleep accuracy.
111    /// The lower the `native_accuracy_ns` the more we effectively trust the accuracy of the
112    /// [`native_sleep`] function.
113    #[inline]
114    pub fn new(native_accuracy_ns: SubsecondNanoseconds) -> SpinSleeper {
115        SpinSleeper {
116            native_accuracy_ns,
117            spin_strategy: <_>::default(),
118        }
119    }
120
121    /// Returns configured native_accuracy_ns.
122    pub fn native_accuracy_ns(self) -> SubsecondNanoseconds {
123        self.native_accuracy_ns
124    }
125
126    /// Returns configured spin strategy.
127    pub fn spin_strategy(self) -> SpinStrategy {
128        self.spin_strategy
129    }
130
131    /// Returns a spin sleeper with the given [`SpinStrategy`].
132    ///
133    /// # Example
134    /// ```
135    /// use spin_sleep::{SpinSleeper, SpinStrategy};
136    ///
137    /// let sleeper = SpinSleeper::default().with_spin_strategy(SpinStrategy::SpinLoopHint);
138    /// ```
139    pub fn with_spin_strategy(mut self, strategy: SpinStrategy) -> Self {
140        self.spin_strategy = strategy;
141        self
142    }
143
144    /// The internal `spin_sleep` method that puts the [current thread to sleep](fn.native_sleep.html)
145    /// for the duration less the configured native accuracy, then spins until the specified deadline.
146    #[inline]
147    fn spin_sleep(self, duration: Duration, deadline: Instant) {
148        let accuracy = Duration::new(0, self.native_accuracy_ns);
149        if duration > accuracy {
150            native_sleep(duration - accuracy);
151        }
152        // spin the rest of the duration
153        while Instant::now() < deadline {
154            match self.spin_strategy {
155                SpinStrategy::YieldThread => thread::yield_now(),
156                SpinStrategy::SpinLoopHint => std::hint::spin_loop(),
157            }
158        }
159    }
160
161    /// Puts the [current thread to sleep](fn.native_sleep.html) for the `duration` less the
162    /// configured native accuracy. Then spins until the specified duration has elapsed.
163    pub fn sleep(self, duration: Duration) {
164        let deadline = Instant::now() + duration;
165        self.spin_sleep(duration, deadline);
166    }
167
168    /// Puts the [current thread to sleep](fn.native_sleep.html) until the `deadline` less
169    /// the configured native accuracy. Then spins until the specified deadline is reached.
170    pub fn sleep_until(self, deadline: Instant) {
171        let duration = deadline.saturating_duration_since(Instant::now());
172        self.spin_sleep(duration, deadline);
173    }
174
175    /// Puts the [current thread to sleep](fn.native_sleep.html) for the give seconds-duration
176    /// less the configured native accuracy. Then spins until the specified duration has elapsed.
177    pub fn sleep_s(self, seconds: Seconds) {
178        if seconds > 0.0 {
179            self.sleep(Duration::from_secs_f64(seconds));
180        }
181    }
182
183    /// Puts the [current thread to sleep](fn.native_sleep.html) for the give nanoseconds-duration
184    /// less the configured native accuracy. Then spins until the specified duration has elapsed.
185    pub fn sleep_ns(self, nanoseconds: Nanoseconds) {
186        self.sleep(Duration::from_nanos(nanoseconds))
187    }
188}
189
190/// Puts the [current thread to sleep](fn.native_sleep.html) for the `duration` less the
191/// default native accuracy. Then spins until the specified duration has elapsed.
192///
193/// Convenience function for `SpinSleeper::default().sleep(duration)`. Can directly take the
194/// place of `thread::sleep`.
195pub fn sleep(duration: Duration) {
196    SpinSleeper::default().sleep(duration);
197}
198
199/// Puts the [current thread to sleep](fn.native_sleep.html) until the `deadline` less
200/// the configured native accuracy. Then spins until the specified deadline is reached.
201///
202/// Convenience function for `SpinSleeper::default().sleep_until(instant)`. Can directly take
203/// the place of `thread::sleep_until`.
204pub fn sleep_until(instant: Instant) {
205    SpinSleeper::default().sleep_until(instant);
206}
207
208/// What to do while spinning.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
210#[non_exhaustive]
211pub enum SpinStrategy {
212    /// Call [`std::thread::yield_now`] while spinning.
213    YieldThread,
214    /// Call [`std::hint::spin_loop`] while spinning.
215    SpinLoopHint,
216}
217
218/// Per-OS default strategy.
219/// * Windows  `SpinLoopHint`
220/// * !Windows `YieldThread`
221impl Default for SpinStrategy {
222    #[inline]
223    fn default() -> Self {
224        #[cfg(windows)]
225        return Self::SpinLoopHint;
226
227        #[cfg(not(windows))]
228        Self::YieldThread
229    }
230}
231
232// Not run unless specifically enabled with `cargo test --features "nondeterministic_tests"`
233// Travis does not do well with these tests, as they require a certain CPU priority.
234#[cfg(feature = "nondeterministic_tests")]
235#[cfg(test)]
236mod spin_sleep_test {
237    use super::*;
238
239    // The worst case error is unbounded even when spinning, but this accuracy is reasonable
240    // for most platforms.
241    const ACCEPTABLE_DELTA_NS: SubsecondNanoseconds = 50_000;
242
243    // Since on spin performance is not guaranteed it suffices that the assertions are valid
244    // 'most of the time'. This macro should avoid most 1-off failures.
245    macro_rules! passes_eventually {
246        ($test:expr) => {{
247            let mut error = None;
248            for _ in 0..50 {
249                match ::std::panic::catch_unwind(|| $test) {
250                    Ok(_) => break,
251                    Err(err) => {
252                        // test is failing, maybe due to spin unreliability
253                        error = error.or(Some(err));
254                        thread::sleep(Duration::new(0, 1000));
255                    }
256                }
257            }
258            assert!(
259                error.is_none(),
260                "Test failed 50/50 times: {:?}",
261                error.unwrap()
262            );
263        }};
264    }
265
266    #[test]
267    fn sleep_small() {
268        passes_eventually!({
269            let ns_duration = 12_345_678;
270
271            let ps = SpinSleeper::new(20_000_000);
272            ps.sleep(Duration::new(0, 1000)); // warm up
273
274            let before = Instant::now();
275            ps.sleep(Duration::new(0, ns_duration));
276            let elapsed = before.elapsed();
277
278            println!("Actual: {:?}", elapsed);
279            assert!(elapsed <= Duration::new(0, ns_duration + ACCEPTABLE_DELTA_NS));
280            assert!(elapsed >= Duration::new(0, ns_duration - ACCEPTABLE_DELTA_NS));
281        });
282    }
283
284    #[test]
285    fn sleep_big() {
286        passes_eventually!({
287            let ns_duration = 212_345_678;
288
289            let ps = SpinSleeper::new(20_000_000);
290            ps.sleep(Duration::new(0, 1000)); // warm up
291
292            let before = Instant::now();
293            ps.sleep(Duration::new(1, ns_duration));
294            let elapsed = before.elapsed();
295
296            println!("Actual: {:?}", elapsed);
297            assert!(elapsed <= Duration::new(1, ns_duration + ACCEPTABLE_DELTA_NS));
298            assert!(elapsed >= Duration::new(1, ns_duration - ACCEPTABLE_DELTA_NS));
299        });
300    }
301
302    #[test]
303    fn sleep_s() {
304        passes_eventually!({
305            let ns_duration = 12_345_678_f64;
306
307            let ps = SpinSleeper::new(20_000_000);
308            ps.sleep_s(0.000001); // warm up
309
310            let before = Instant::now();
311            ps.sleep_s(ns_duration / 1_000_000_000_f64);
312            let elapsed = before.elapsed();
313
314            println!("Actual: {:?}", elapsed);
315            assert!(elapsed <= Duration::new(0, ns_duration.round() as u32 + ACCEPTABLE_DELTA_NS));
316            assert!(elapsed >= Duration::new(0, ns_duration.round() as u32 - ACCEPTABLE_DELTA_NS));
317        });
318    }
319
320    #[test]
321    fn sleep_ns() {
322        passes_eventually!({
323            let ns_duration: u32 = 12_345_678;
324
325            let ps = SpinSleeper::new(20_000_000);
326            ps.sleep_ns(1000); // warm up
327
328            let before = Instant::now();
329            ps.sleep_ns(ns_duration as u64);
330            let elapsed = before.elapsed();
331
332            println!("Actual: {:?}", elapsed);
333            assert!(elapsed <= Duration::new(0, ns_duration + ACCEPTABLE_DELTA_NS));
334            assert!(elapsed >= Duration::new(0, ns_duration - ACCEPTABLE_DELTA_NS));
335        });
336    }
337}