use std::time::Instant;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Policy {
pub headroom: f64,
pub max: u64,
pub min: u64,
pub hysteresis: f64,
pub ramp: f64,
}
impl Policy {
pub fn new(max: u64) -> Self {
Self {
headroom: 0.9,
max,
min: max / 10,
hysteresis: 0.05,
ramp: 0.25,
}
}
}
#[derive(Clone, Debug)]
pub struct Control {
policy: Policy,
target: u64,
applied: Option<Instant>,
}
impl Control {
pub fn new(policy: Policy) -> Self {
Self {
target: policy.max.max(policy.min),
policy,
applied: None,
}
}
pub fn target(&self) -> u64 {
self.target
}
pub fn update(&mut self, estimate: Option<u64>, now: Instant) -> Option<u64> {
let estimate = estimate?;
let min = self.policy.min.min(self.policy.max);
let headroom = if self.policy.headroom.is_finite() {
self.policy.headroom.clamp(0.0, 1.0)
} else {
0.0
};
let desired = ((estimate as f64 * headroom) as u64).clamp(min, self.policy.max);
let next = if desired <= self.target {
desired
} else {
match self.applied {
Some(applied) => {
let elapsed = now.saturating_duration_since(applied).as_secs_f64();
let ramp = self.policy.ramp.max(0.0);
let grown = self.target as f64 * (1.0 + ramp * elapsed);
(grown as u64).min(desired).clamp(min, self.policy.max)
}
None => desired,
}
};
let hysteresis = self.policy.hysteresis.max(0.0);
if (next.abs_diff(self.target) as f64) < self.target as f64 * hysteresis {
return None;
}
self.target = next;
self.applied = Some(now);
Some(next)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn control() -> Control {
Control::new(Policy::new(4_000_000))
}
#[test]
fn starts_optimistic() {
assert_eq!(control().target(), 4_000_000);
}
#[test]
fn drop_applies_immediately_with_headroom() {
let mut control = control();
assert_eq!(control.update(Some(2_000_000), Instant::now()), Some(1_800_000));
assert_eq!(control.target(), 1_800_000);
}
#[test]
fn missing_estimate_holds_the_target() {
let mut control = control();
let now = Instant::now();
control.update(Some(2_000_000), now).unwrap();
assert_eq!(control.update(None, now + Duration::from_secs(10)), None);
assert_eq!(control.target(), 1_800_000);
}
#[test]
fn estimate_never_raises_above_max() {
let mut control = control();
assert_eq!(control.update(Some(100_000_000), Instant::now()), None);
assert_eq!(control.target(), 4_000_000);
}
#[test]
fn target_never_falls_below_min() {
let mut control = control();
assert_eq!(control.update(Some(1), Instant::now()), Some(400_000));
assert_eq!(control.target(), 400_000);
}
#[test]
fn raise_is_ramp_limited() {
let mut control = control();
let start = Instant::now();
control.update(Some(1_000_000), start).unwrap();
let raised = control.update(Some(4_000_000), start + Duration::from_secs(1)).unwrap();
assert_eq!(raised, 1_125_000);
}
#[test]
fn raise_eventually_reaches_the_estimate() {
let mut control = control();
let start = Instant::now();
control.update(Some(1_000_000), start).unwrap();
for tick in 1..=200 {
control.update(Some(4_000_000), start + Duration::from_millis(100 * tick));
}
assert_eq!(control.target(), 3_600_000);
}
#[test]
fn suppressed_raises_do_not_starve_the_ramp() {
let mut control = control();
let start = Instant::now();
control.update(Some(1_000_000), start).unwrap();
let mut raised = None;
for tick in 1..=10 {
if let Some(next) = control.update(Some(4_000_000), start + Duration::from_millis(100 * tick)) {
raised = Some((tick, next));
break;
}
}
let (tick, next) = raised.expect("a raise must eventually clear hysteresis");
assert_eq!(tick, 2);
assert_eq!(next, 945_000);
}
#[test]
fn small_moves_are_suppressed() {
let mut control = control();
let now = Instant::now();
control.update(Some(2_000_000), now).unwrap();
assert_eq!(control.update(Some(1_960_000), now + Duration::from_secs(1)), None);
assert_eq!(control.target(), 1_800_000);
assert_eq!(
control.update(Some(1_600_000), now + Duration::from_secs(2)),
Some(1_440_000)
);
}
#[test]
fn inverted_bounds_do_not_panic() {
let mut policy = Policy::new(1_000_000);
policy.min = 5_000_000;
let mut control = Control::new(policy);
control.update(Some(2_000_000), Instant::now());
assert!(control.target() <= 5_000_000);
}
#[test]
fn non_finite_headroom_does_not_poison_the_target() {
let mut policy = Policy::new(4_000_000);
policy.headroom = f64::NAN;
let mut control = Control::new(policy);
control.update(Some(2_000_000), Instant::now());
assert_eq!(control.target(), 400_000); }
}