use core::time::Duration;
use alloc::boxed::Box;
use super::law::{ControlLaw, Law, LawKind};
use super::strategy::{Concurrency, Preset, Strategy};
use super::{Bounds, Gate, Objective, Sample, Signal, SignalKind};
pub struct ConcurrencyBuilder {
fixed: Option<usize>,
preset: Option<Preset>,
signal: Option<Signal>,
objective: Option<Objective>,
law: Option<Law>,
gate: Gate,
bounds: Bounds,
}
impl Default for ConcurrencyBuilder {
fn default() -> Self {
Self::new()
}
}
impl core::fmt::Debug for ConcurrencyBuilder {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("ConcurrencyBuilder")
.field("fixed", &self.fixed)
.field("preset", &self.preset)
.field("signal", &self.signal)
.field("objective", &self.objective)
.field("law", &self.law)
.field("gate", &self.gate)
.field("bounds", &self.bounds)
.finish()
}
}
impl ConcurrencyBuilder {
#[must_use]
pub fn new() -> Self {
Self {
fixed: None,
preset: None,
signal: None,
objective: None,
law: None,
gate: Gate::default(),
bounds: Bounds::default(),
}
}
#[must_use]
pub fn fixed(mut self, n: usize) -> Self {
self.fixed = Some(n);
self
}
#[must_use]
pub fn hillclimb(mut self) -> Self {
self.preset = Some(Preset::HillClimb);
self
}
#[must_use]
pub fn gradient(mut self) -> Self {
self.preset = Some(Preset::Gradient);
self
}
#[must_use]
pub fn latency_target(mut self, target: Duration) -> Self {
self.preset = Some(Preset::LatencyTarget(target));
self
}
#[must_use]
pub fn headroom(mut self, ceiling: f64) -> Self {
self.preset = Some(Preset::Headroom(ceiling));
self
}
#[must_use]
pub fn preset(mut self, preset: Preset) -> Self {
match preset {
Preset::Fixed(n) => self.fixed = Some(n),
other => self.preset = Some(other),
}
self
}
#[must_use]
pub fn signal(mut self, kind: SignalKind) -> Self {
self.signal = Some(Signal::Builtin(kind));
self
}
#[must_use]
pub fn signal_fn(mut self, extract: impl Fn(&Sample) -> f64 + Send + 'static) -> Self {
self.signal = Some(Signal::Custom(Box::new(extract)));
self
}
#[must_use]
pub fn objective(mut self, objective: Objective) -> Self {
self.objective = Some(objective);
self
}
#[must_use]
pub fn maximize(self) -> Self {
self.objective(Objective::Maximize)
}
#[must_use]
pub fn target(self, value: f64) -> Self {
self.objective(Objective::Target(value))
}
#[must_use]
pub fn ceiling(self, value: f64) -> Self {
self.objective(Objective::Ceiling(value))
}
#[must_use]
pub fn knee(self) -> Self {
self.objective(Objective::Knee)
}
#[must_use]
pub fn law_kind(mut self, kind: LawKind) -> Self {
self.law = Some(Law::Builtin(kind));
self
}
#[must_use]
pub fn law(mut self, law: impl ControlLaw + 'static) -> Self {
self.law = Some(Law::Custom(Box::new(law)));
self
}
#[must_use]
pub fn coefficient_of_variation_threshold(
mut self,
coefficient_of_variation_threshold: f64,
) -> Self {
self.gate.coefficient_of_variation_threshold = coefficient_of_variation_threshold;
self
}
#[must_use]
pub fn window(mut self, window: Duration) -> Self {
self.gate.window = window;
self
}
#[must_use]
pub fn reprobe(mut self, reprobe: u32) -> Self {
self.gate.reprobe = reprobe;
self
}
#[must_use]
pub fn gate(mut self, gate: Gate) -> Self {
self.gate = gate;
self
}
#[must_use]
pub fn bounds(mut self, min: usize, max: usize) -> Self {
self.bounds.min = min;
self.bounds.max = max;
self
}
#[must_use]
pub fn start(mut self, start: usize) -> Self {
self.bounds.start = start;
self
}
#[must_use]
pub fn bounds_full(mut self, bounds: Bounds) -> Self {
self.bounds = bounds;
self
}
pub fn build(self) -> Result<Concurrency, &'static str> {
if let Some(n) = self.fixed {
return Ok(Concurrency::Fixed(n));
}
let preset_levers = self.preset.and_then(Preset::levers);
let objective = self
.objective
.or(preset_levers.map(|(_, objective)| objective))
.ok_or("adaptive build needs a preset or an explicit objective")?;
let signal = match self.signal {
Some(signal) => signal,
None => {
let kind = preset_levers
.map(|(kind, _)| kind)
.ok_or("adaptive build needs a preset or an explicit signal")?;
Signal::Builtin(kind)
}
};
let (law_kind, law_box): (Option<LawKind>, Box<dyn ControlLaw>) = match self.law {
Some(Law::Builtin(kind)) => (Some(kind), kind.build()),
Some(Law::Custom(law)) => (None, law),
None => {
let kind = objective.default_law();
(Some(kind), kind.build())
}
};
let strategy = Strategy::new(signal, objective, law_box, law_kind, self.gate, self.bounds)?;
Ok(Concurrency::Adaptive(strategy))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::super::{SignalClass, StrategyDescriptor};
use super::*;
fn descriptor(concurrency: Concurrency) -> StrategyDescriptor {
match concurrency {
Concurrency::Adaptive(strategy) => strategy.descriptor(),
Concurrency::Fixed(_) => panic!("expected adaptive"),
}
}
#[test]
fn fixed_short_circuits() {
let concurrency = ConcurrencyBuilder::new().fixed(25).build().unwrap();
assert!(matches!(concurrency, Concurrency::Fixed(25)));
}
#[test]
fn preset_then_lever_override() {
let concurrency = ConcurrencyBuilder::new()
.hillclimb()
.coefficient_of_variation_threshold(0.05)
.bounds(1, 512)
.build()
.unwrap();
let descriptor = descriptor(concurrency);
assert_eq!(
descriptor.signal,
SignalClass::Builtin(SignalKind::Throughput)
);
assert_eq!(descriptor.objective, Objective::Maximize);
assert_eq!(descriptor.law, Some(LawKind::HillClimb));
assert_eq!(descriptor.gate.coefficient_of_variation_threshold, 0.05);
assert_eq!(descriptor.bounds.max, 512);
}
#[test]
fn law_override_changes_only_the_law() {
let concurrency = ConcurrencyBuilder::new()
.hillclimb()
.law_kind(LawKind::Aimd)
.build()
.unwrap();
assert_eq!(descriptor(concurrency).law, Some(LawKind::Aimd));
}
#[test]
fn incoherent_pair_is_rejected() {
let result = ConcurrencyBuilder::new()
.signal(SignalKind::LatencyP99)
.maximize()
.build();
assert!(result.is_err());
}
#[test]
fn custom_law_descriptor_is_foreign() {
struct Noop;
impl ControlLaw for Noop {
fn step(&mut self, ctx: super::super::LawStep) -> usize {
ctx.current
}
}
let concurrency = ConcurrencyBuilder::new()
.hillclimb()
.law(Noop)
.build()
.unwrap();
assert_eq!(descriptor(concurrency).law, None);
}
}