use std::time::{Duration, Instant};
const CALIBRATION_ITERATIONS: usize = 100_000;
#[derive(Clone, Debug)]
pub struct Config {
pub budget_us: usize,
pub max_budget_us: usize,
pub quick_wake_us: usize,
}
impl Config {
#[allow(dead_code)]
pub const fn disabled() -> Self {
Self {
budget_us: 0,
max_budget_us: 0,
quick_wake_us: 0,
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
budget_us: 50,
max_budget_us: 300,
quick_wake_us: 150,
}
}
}
pub struct Spinner {
budget: usize,
min_budget: usize,
max_budget: usize,
near_miss_floor: usize,
iters_per_us: usize,
quick_wake: Duration,
}
impl Spinner {
pub fn new(cfg: &Config, probe: impl Fn() -> bool) -> Self {
assert!(
cfg.budget_us <= cfg.max_budget_us,
"spinner budget_us ({}) must not exceed max_budget_us ({})",
cfg.budget_us,
cfg.max_budget_us,
);
let iters_per_us = calibrate(probe);
let min_budget = cfg.budget_us.saturating_mul(iters_per_us);
Self {
budget: min_budget,
min_budget,
max_budget: cfg.max_budget_us.saturating_mul(iters_per_us),
near_miss_floor: min_budget,
iters_per_us,
quick_wake: Duration::from_micros(cfg.quick_wake_us as u64),
}
}
#[inline]
pub fn spin(&mut self, mut condition: impl FnMut() -> bool) -> bool {
if self.budget == 0 {
return false;
}
for _ in 0..self.budget {
if condition() {
self.on_hit();
return true;
}
core::hint::spin_loop();
}
self.on_miss();
false
}
#[inline]
fn on_hit(&mut self) {
self.budget = (self.budget + self.budget / 4).min(self.max_budget);
}
#[inline]
fn on_miss(&mut self) {
self.near_miss_floor -= self.near_miss_floor / 16;
let floor = self.near_miss_floor.max(self.min_budget);
self.budget = self.budget.saturating_sub(self.budget / 8).max(floor);
}
#[inline]
pub fn on_wake(&mut self, park_duration: Duration) {
if park_duration <= self.quick_wake {
self.budget = (self.budget + self.budget / 2).min(self.max_budget);
let park_iters = (park_duration.as_nanos() * self.iters_per_us as u128 / 1000) as usize;
let floor = (self.near_miss_floor * 3 + park_iters) / 4;
self.near_miss_floor = floor.min(self.max_budget);
}
}
}
fn calibrate(probe: impl Fn() -> bool) -> usize {
let start = Instant::now();
for _ in 0..CALIBRATION_ITERATIONS {
std::hint::black_box(probe());
core::hint::spin_loop();
}
let elapsed_us = start.elapsed().as_micros() as usize;
CALIBRATION_ITERATIONS / elapsed_us.max(1)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> Config {
Config {
budget_us: 10,
max_budget_us: 50,
quick_wake_us: 50,
}
}
#[test]
fn test_calibration_returns_nonzero() {
assert!(calibrate(|| false) > 0);
}
#[test]
fn test_spin_immediate_hit() {
let mut s = Spinner::new(&cfg(), || false);
assert!(s.spin(|| true));
}
#[test]
fn test_spin_miss() {
let mut s = Spinner::new(&cfg(), || false);
assert!(!s.spin(|| false));
}
#[test]
fn test_spin_delayed_hit() {
let mut s = Spinner::new(&cfg(), || false);
let mut count = 0usize;
let hit = s.spin(|| {
count += 1;
count >= 50
});
assert!(hit);
assert_eq!(count, 50);
}
#[test]
fn test_hit_grows_budget() {
let mut s = Spinner::new(&cfg(), || false);
let initial = s.budget;
s.spin(|| true);
assert!(s.budget > initial);
}
#[test]
fn test_miss_shrinks_budget() {
let mut s = Spinner::new(&cfg(), || false);
s.spin(|| true);
let after_hit = s.budget;
s.spin(|| false);
assert!(s.budget < after_hit);
}
#[test]
fn test_budget_does_not_go_below_min() {
let mut s = Spinner::new(&cfg(), || false);
for _ in 0..1000 {
s.spin(|| false);
}
assert!(s.budget >= s.min_budget);
}
#[test]
fn test_budget_does_not_exceed_max() {
let mut s = Spinner::new(&cfg(), || false);
for _ in 0..1000 {
s.spin(|| true);
}
assert!(s.budget <= s.max_budget);
}
#[test]
fn test_quick_wake_grows_budget_and_floor() {
let mut s = Spinner::new(&cfg(), || false);
assert_eq!(s.near_miss_floor, s.min_budget);
let before = s.near_miss_floor;
s.on_wake(Duration::from_micros(30));
assert!(s.near_miss_floor > before);
}
#[test]
fn test_slow_block_does_not_grow() {
let mut s = Spinner::new(&cfg(), || false);
let before = s.budget;
s.on_wake(Duration::from_nanos(100_000));
assert_eq!(s.budget, before);
}
#[test]
fn test_near_miss_floor_decays_on_miss() {
let mut s = Spinner::new(&cfg(), || false);
s.on_wake(Duration::from_nanos(10_000));
let floor_after_wake = s.near_miss_floor;
assert!(floor_after_wake > 0);
for _ in 0..100 {
s.spin(|| false);
}
assert!(s.near_miss_floor < floor_after_wake);
}
#[test]
fn test_near_miss_floor_clamped_to_max() {
let mut s = Spinner::new(&cfg(), || false);
s.on_wake(Duration::from_nanos(40_000));
assert!(s.near_miss_floor <= s.max_budget);
}
#[test]
#[should_panic(expected = "must not exceed max_budget_us")]
fn test_new_panics_when_budget_exceeds_max() {
let _ = Spinner::new(
&Config {
budget_us: 100,
max_budget_us: 50,
quick_wake_us: 50,
},
|| false,
);
}
#[test]
fn test_disabled_spinner() {
let mut s = Spinner::new(&Config::disabled(), || false);
assert!(!s.spin(|| true));
}
}