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}