#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TimeUnit {
Nanos,
Micros,
Millis,
Seconds,
}
impl TimeUnit {
pub fn for_rate(ops_per_sec: f64) -> Self {
if ops_per_sec > 1.0 {
TimeUnit::Nanos
} else if ops_per_sec > 0.001 {
TimeUnit::Micros
} else if ops_per_sec > 0.000001 {
TimeUnit::Millis
} else {
TimeUnit::Seconds
}
}
pub fn nanos_to_ticks(self, nanos: u64) -> u32 {
let ticks = match self {
TimeUnit::Nanos => nanos,
TimeUnit::Micros => nanos / 1_000,
TimeUnit::Millis => nanos / 1_000_000,
TimeUnit::Seconds => nanos / 1_000_000_000,
};
ticks.min(u32::MAX as u64) as u32
}
pub fn ticks_to_nanos(self, ticks: u32) -> u64 {
match self {
TimeUnit::Nanos => ticks as u64,
TimeUnit::Micros => ticks as u64 * 1_000,
TimeUnit::Millis => ticks as u64 * 1_000_000,
TimeUnit::Seconds => ticks as u64 * 1_000_000_000,
}
}
pub fn ticks_per_op(self, ops_per_sec: f64) -> u32 {
let ticks = match self {
TimeUnit::Nanos => 1_000_000_000.0 / ops_per_sec,
TimeUnit::Micros => 1_000_000.0 / ops_per_sec,
TimeUnit::Millis => 1_000.0 / ops_per_sec,
TimeUnit::Seconds => 1.0 / ops_per_sec,
};
(ticks as u64).min(u32::MAX as u64) as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Verb {
Start,
Configure,
Restart,
Stop,
}
#[derive(Debug, Clone)]
pub struct RateSpec {
pub ops_per_sec: f64,
pub burst_ratio: f64,
pub verb: Verb,
pub unit: TimeUnit,
}
impl RateSpec {
pub fn new(ops_per_sec: f64) -> Self {
Self::with_burst(ops_per_sec, 1.1)
}
pub fn with_burst(ops_per_sec: f64, burst_ratio: f64) -> Self {
Self {
ops_per_sec,
burst_ratio,
verb: Verb::Start,
unit: TimeUnit::for_rate(ops_per_sec),
}
}
pub fn parse(spec: &str) -> Result<Self, String> {
let parts: Vec<&str> = spec.split(',').map(|s| s.trim()).collect();
if parts.is_empty() {
return Err("empty rate spec".into());
}
let ops_per_sec: f64 = parts[0].parse().map_err(|e| format!("invalid rate: {e}"))?;
if ops_per_sec <= 0.0 {
return Err("rate must be positive".into());
}
let burst_ratio = if parts.len() > 1 {
parts[1].parse::<f64>().unwrap_or(1.1)
} else {
1.1
};
let verb = if parts.len() > 2 {
match parts[2].to_lowercase().as_str() {
"start" => Verb::Start,
"configure" => Verb::Configure,
"restart" => Verb::Restart,
"stop" => Verb::Stop,
other => return Err(format!("unknown verb: {other}")),
}
} else {
Verb::Start
};
Ok(Self {
ops_per_sec,
burst_ratio,
verb,
unit: TimeUnit::for_rate(ops_per_sec),
})
}
pub fn ticks_per_op(&self) -> u32 {
self.unit.ticks_per_op(self.ops_per_sec)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn time_unit_scaling() {
assert_eq!(TimeUnit::for_rate(50.0), TimeUnit::Nanos);
assert_eq!(TimeUnit::for_rate(0.5), TimeUnit::Micros);
assert_eq!(TimeUnit::for_rate(0.0005), TimeUnit::Millis);
assert_eq!(TimeUnit::for_rate(0.0000001), TimeUnit::Seconds);
}
#[test]
fn ticks_per_op_fits() {
let unit = TimeUnit::Nanos;
assert_eq!(unit.ticks_per_op(50.0), 20_000_000);
let unit = TimeUnit::Micros;
assert_eq!(unit.ticks_per_op(0.5), 2_000_000);
}
#[test]
fn nanos_ticks_roundtrip() {
let unit = TimeUnit::Micros;
let nanos = 5_000_000u64; let ticks = unit.nanos_to_ticks(nanos);
assert_eq!(ticks, 5000);
assert_eq!(unit.ticks_to_nanos(ticks), nanos);
}
#[test]
fn parse_simple() {
let spec = RateSpec::parse("1000").unwrap();
assert_eq!(spec.ops_per_sec, 1000.0);
assert_eq!(spec.burst_ratio, 1.1);
assert_eq!(spec.verb, Verb::Start);
}
#[test]
fn parse_with_burst() {
let spec = RateSpec::parse("500, 1.5").unwrap();
assert_eq!(spec.ops_per_sec, 500.0);
assert_eq!(spec.burst_ratio, 1.5);
}
#[test]
fn parse_with_verb() {
let spec = RateSpec::parse("1000, 1.1, restart").unwrap();
assert_eq!(spec.verb, Verb::Restart);
}
#[test]
fn parse_errors() {
assert!(RateSpec::parse("").is_err());
assert!(RateSpec::parse("-1").is_err());
assert!(RateSpec::parse("abc").is_err());
}
}