use core::time::Duration;
use alloc::boxed::Box;
use super::law::{ControlLaw, LawKind};
use super::{
Bounds, Gate, LawStep, Objective, Sample, Signal, SignalClass, SignalKind, duration_millis,
};
pub trait ConcurrencyStrategy: Send {
fn next(&mut self, sample: Sample) -> usize;
}
pub struct Strategy {
signal: Signal,
objective: Objective,
law: Box<dyn ControlLaw>,
gate: Gate,
bounds: Bounds,
recorded_law_kind: Option<LawKind>,
prev_signal: Option<f64>,
}
impl core::fmt::Debug for Strategy {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("Strategy")
.field("signal", &self.signal)
.field("objective", &self.objective)
.field("law", &self.law_kind())
.field("gate", &self.gate)
.field("bounds", &self.bounds)
.finish()
}
}
impl Strategy {
pub fn new(
signal: Signal,
objective: Objective,
law: Box<dyn ControlLaw>,
law_kind: Option<LawKind>,
gate: Gate,
bounds: Bounds,
) -> Result<Self, &'static str> {
objective.coherent_with(signal.class())?;
bounds.validate()?;
Ok(Self {
signal,
objective,
law,
gate,
bounds,
recorded_law_kind: law_kind,
prev_signal: None,
})
}
#[must_use]
pub fn window(&self) -> Duration {
self.gate.window
}
#[must_use]
pub fn start(&self) -> usize {
self.bounds.start
}
#[must_use]
fn law_kind(&self) -> Option<LawKind> {
self.recorded_law_kind
}
#[must_use]
pub fn descriptor(&self) -> StrategyDescriptor {
StrategyDescriptor {
signal: self.signal.class(),
objective: self.objective,
law: self.recorded_law_kind,
gate: self.gate,
bounds: self.bounds,
}
}
}
impl ConcurrencyStrategy for Strategy {
fn next(&mut self, sample: Sample) -> usize {
let signal = self.signal.read(&sample);
let prev = self.prev_signal.unwrap_or(signal);
let step = LawStep {
current: sample.concurrency,
signal,
prev_signal: prev,
objective: self.objective,
cov: sample.cov,
coefficient_of_variation_threshold: self.gate.coefficient_of_variation_threshold,
min: self.bounds.min,
max: self.bounds.max,
};
let next = self.bounds.clamp(self.law.step(step));
self.prev_signal = Some(signal);
next
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StrategyDescriptor {
pub signal: SignalClass,
pub objective: Objective,
pub law: Option<LawKind>,
pub gate: Gate,
pub bounds: Bounds,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Preset {
Fixed(usize),
HillClimb,
Gradient,
LatencyTarget(Duration),
Headroom(f64),
}
impl Preset {
#[must_use]
pub fn levers(self) -> Option<(SignalKind, Objective)> {
match self {
Self::Fixed(_) => None,
Self::HillClimb => Some((SignalKind::Throughput, Objective::Maximize)),
Self::Gradient => Some((SignalKind::LatencyGradient, Objective::Knee)),
Self::LatencyTarget(target) => Some((
SignalKind::LatencyP99,
Objective::Target(duration_millis(target)),
)),
Self::Headroom(ceiling) => Some((SignalKind::Utilization, Objective::Ceiling(ceiling))),
}
}
pub fn parse(token: &str) -> Result<Self, &'static str> {
match token.trim().to_ascii_lowercase().as_str() {
"hillclimb" | "hill_climb" => Ok(Self::HillClimb),
"gradient" => Ok(Self::Gradient),
"latency_target" | "latency" => Ok(Self::LatencyTarget(Duration::from_millis(5))),
"headroom" => Ok(Self::Headroom(0.85)),
"fixed" => Ok(Self::Fixed(25)),
_ => Err("preset must be fixed|hillclimb|gradient|latency_target|headroom"),
}
}
}
pub enum Concurrency {
Fixed(usize),
Adaptive(Strategy),
}
impl core::fmt::Debug for Concurrency {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Fixed(n) => write!(formatter, "Concurrency::Fixed({n})"),
Self::Adaptive(strategy) => {
write!(formatter, "Concurrency::Adaptive({strategy:?})")
}
}
}
}
impl Concurrency {
#[must_use]
pub fn fixed(n: usize) -> Self {
Self::Fixed(n)
}
pub fn from_preset(preset: Preset) -> Result<Self, &'static str> {
Self::builder().preset(preset).build()
}
#[must_use]
pub fn builder() -> super::ConcurrencyBuilder {
super::ConcurrencyBuilder::new()
}
pub fn adaptive() -> Result<Self, &'static str> {
Self::from_preset(Preset::Gradient)
}
#[must_use]
pub fn initial(&self) -> usize {
match self {
Self::Fixed(n) => *n,
Self::Adaptive(strategy) => strategy.start(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn preset_levers_bundle_signal_and_objective() {
assert_eq!(
Preset::HillClimb.levers(),
Some((SignalKind::Throughput, Objective::Maximize))
);
assert_eq!(
Preset::Gradient.levers(),
Some((SignalKind::LatencyGradient, Objective::Knee))
);
assert!(Preset::Fixed(4).levers().is_none());
}
#[test]
fn from_preset_fixed_is_fixed() {
let concurrency = Concurrency::from_preset(Preset::Fixed(25)).unwrap();
assert!(matches!(concurrency, Concurrency::Fixed(25)));
assert_eq!(concurrency.initial(), 25);
}
#[test]
fn adaptive_default_is_gradient() {
let concurrency = Concurrency::adaptive().unwrap();
let Concurrency::Adaptive(strategy) = concurrency else {
panic!("expected adaptive");
};
let descriptor = strategy.descriptor();
assert_eq!(
descriptor.signal,
SignalClass::Builtin(SignalKind::LatencyGradient)
);
assert_eq!(descriptor.objective, Objective::Knee);
assert_eq!(descriptor.law, Some(LawKind::Multiplicative));
}
#[test]
fn strategy_next_climbs_a_monotonic_workload() {
let mut concurrency = Concurrency::from_preset(Preset::HillClimb).unwrap();
let Concurrency::Adaptive(ref mut strategy) = concurrency else {
panic!("adaptive");
};
let start = strategy.start();
let mut current = start;
for _ in 0..50 {
let mut sample = Sample::seed(current);
sample.cov = 0.0;
sample.throughput = current as f64; current = strategy.next(sample);
}
assert!(
current > start,
"monotonic gain → climbed from {start} to {current}"
);
}
}