use super::internal::{AtomicU8, Ordering};
#[cfg(not(all(loom, test)))]
pub(super) const MAX: u8 = 128;
#[cfg(all(loom, test))]
pub(super) const MAX: u8 = 1;
#[cfg(not(all(loom, test)))]
const START: u8 = 64;
#[cfg(all(loom, test))]
const START: u8 = 1;
const SPINLESS: bool = cfg!(all(target_family = "wasm", not(target_feature = "atomics")));
#[inline]
pub(super) fn pause() {
#[cfg(all(loom, test))]
loom::hint::spin_loop();
#[cfg(not(all(loom, test)))]
core::hint::spin_loop();
}
#[cfg(not(all(loom, test)))]
const GAP_MAX: u32 = 16;
#[cfg(all(loom, test))]
const GAP_MAX: u32 = 1;
pub(super) struct Gaps {
gap: u32,
left: u32,
}
impl Gaps {
pub(super) fn new(rounds: u32) -> Self {
Self {
gap: 1,
left: rounds,
}
}
#[inline]
pub(super) fn wait(&mut self) -> bool {
if self.left == 0 {
return false;
}
let gap = self.gap.min(self.left);
for _ in 0..gap {
pause();
}
self.left -= gap;
self.gap = (self.gap * 2).min(GAP_MAX);
true
}
}
const NEVER: u8 = u8::MAX;
pub(super) struct Budget(AtomicU8);
impl Budget {
loom_const_fn! {
pub(super) fn new() -> Self {
Self(AtomicU8::new(START))
}
}
loom_const_fn! {
pub(super) fn never() -> Self {
Self(AtomicU8::new(NEVER))
}
}
#[inline]
pub(super) fn rounds(&self) -> u32 {
if SPINLESS {
return 0;
}
match self.0.load(Ordering::Relaxed) {
NEVER => 0,
rounds => u32::from(rounds),
}
}
pub(super) fn won(&self, rounds: u32) {
self.store(rounds, (rounds.saturating_mul(2)).clamp(1, u32::from(MAX)));
}
pub(super) fn ran_out(&self, rounds: u32) {
self.store(rounds, rounds / 2);
}
fn store(&self, from: u32, next: u32) {
if next != from
&& let Ok(next) = u8::try_from(next)
&& self.0.load(Ordering::Relaxed) != NEVER
{
self.0.store(next, Ordering::Relaxed);
}
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
#[test]
fn a_won_spin_doubles_up_to_the_cap_and_a_run_out_halves_to_zero() {
let budget = Budget::new();
assert_eq!(budget.rounds(), u32::from(START));
budget.won(budget.rounds());
assert_eq!(budget.rounds(), 2 * u32::from(START));
for _ in 0..8 {
budget.won(budget.rounds());
}
assert_eq!(budget.rounds(), u32::from(MAX), "doubling stops at the cap");
for _ in 0..8 {
budget.ran_out(budget.rounds());
}
assert_eq!(budget.rounds(), 0, "halving reaches zero");
budget.ran_out(budget.rounds());
assert_eq!(budget.rounds(), 0);
budget.won(0);
assert_eq!(budget.rounds(), 1, "a won retry brings a spent budget back");
}
#[test]
fn a_budget_that_never_spins_stays_at_zero() {
let budget = Budget::never();
assert_eq!(budget.rounds(), 0);
budget.won(0);
budget.won(4);
budget.ran_out(4);
assert_eq!(
budget.rounds(),
0,
"no report raises a budget that never spins"
);
}
#[test]
fn a_spin_that_keeps_its_length_writes_nothing_and_the_last_report_wins() {
let budget = Budget::new();
for _ in 0..8 {
budget.won(budget.rounds());
}
budget.won(u32::from(MAX));
assert_eq!(budget.rounds(), u32::from(MAX));
budget.won(4);
assert_eq!(budget.rounds(), 8);
budget.ran_out(8);
assert_eq!(budget.rounds(), 4);
}
}