#![allow(missing_docs)]
use std::time::{Duration, Instant};
use crate::types::UnitId;
pub const DEFAULT_UNIT_TIMEOUT_MS: u64 = 1_000;
pub const DEFAULT_CYCLE_TIMEOUT_MS: u64 = 10_000;
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum TimingError {
#[error("Unit timeout: {unit} took {elapsed_ms}ms, exceeds δ_max={max_ms}ms")]
UnitTimeout {
unit: UnitId,
elapsed_ms: u64,
max_ms: u64,
},
#[error("Cycle timeout: {elapsed_ms}ms exceeds Δt_max={max_ms}ms")]
CycleTimeout { elapsed_ms: u64, max_ms: u64 },
#[error("Timing protocol violation: end_unit({unit}) called before start_unit({unit})")]
TimingProtocolViolation { unit: UnitId },
}
#[derive(Debug, Clone, Copy)]
pub struct TimingConfig {
pub unit_timeout_ms: u64,
pub cycle_timeout_ms: u64,
}
impl Default for TimingConfig {
fn default() -> Self {
Self {
unit_timeout_ms: DEFAULT_UNIT_TIMEOUT_MS,
cycle_timeout_ms: DEFAULT_CYCLE_TIMEOUT_MS,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnitTiming {
pub unit_id: UnitId,
pub start: Instant,
pub end: Option<Instant>,
}
impl UnitTiming {
pub fn elapsed(&self) -> Duration {
match self.end {
Some(end) => end.duration_since(self.start),
None => self.start.elapsed(),
}
}
pub fn elapsed_ms(&self) -> u64 {
self.elapsed().as_millis() as u64
}
}
#[derive(Debug)]
pub struct CycleTimer {
config: TimingConfig,
cycle_start: Instant,
unit_timings: [Option<UnitTiming>; 6],
}
impl CycleTimer {
pub fn new(config: TimingConfig) -> Self {
Self {
config,
cycle_start: Instant::now(),
unit_timings: [None; 6],
}
}
pub fn with_defaults() -> Self {
Self::new(TimingConfig::default())
}
#[allow(clippy::indexing_slicing)]
pub fn start_unit(&mut self, unit_id: UnitId) {
self.unit_timings[unit_id.index()] = Some(UnitTiming {
unit_id,
start: Instant::now(),
end: None,
});
}
#[allow(clippy::indexing_slicing)]
pub fn end_unit(&mut self, unit_id: UnitId) -> Result<Duration, TimingError> {
let timing = self.unit_timings[unit_id.index()]
.as_mut()
.ok_or(TimingError::TimingProtocolViolation { unit: unit_id })?;
timing.end = Some(Instant::now());
let elapsed = timing.elapsed();
let elapsed_ms = elapsed.as_millis() as u64;
if elapsed_ms > self.config.unit_timeout_ms {
return Err(TimingError::UnitTimeout {
unit: unit_id,
elapsed_ms,
max_ms: self.config.unit_timeout_ms,
});
}
Ok(elapsed)
}
pub fn check_cycle_timeout(&self) -> Result<(), TimingError> {
let elapsed_ms = self.cycle_elapsed_ms();
if elapsed_ms > self.config.cycle_timeout_ms {
return Err(TimingError::CycleTimeout {
elapsed_ms,
max_ms: self.config.cycle_timeout_ms,
});
}
Ok(())
}
pub fn cycle_elapsed_ms(&self) -> u64 {
self.cycle_start.elapsed().as_millis() as u64
}
pub fn cycle_elapsed(&self) -> Duration {
self.cycle_start.elapsed()
}
#[allow(clippy::indexing_slicing)]
pub fn unit_timing(&self, unit_id: UnitId) -> Option<&UnitTiming> {
self.unit_timings[unit_id.index()].as_ref()
}
pub fn config(&self) -> &TimingConfig {
&self.config
}
pub fn total_unit_time_ms(&self) -> u64 {
self.unit_timings
.iter()
.filter_map(|t| t.as_ref())
.filter(|t| t.end.is_some())
.map(|t| t.elapsed_ms())
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn default_config_has_spec_values() {
let c = TimingConfig::default();
assert_eq!(c.unit_timeout_ms, 1_000);
assert_eq!(c.cycle_timeout_ms, 10_000);
}
#[test]
fn cycle_timer_starts_immediately() {
let timer = CycleTimer::with_defaults();
assert!(timer.cycle_elapsed().as_nanos() > 0);
}
#[test]
fn unit_timing_roundtrip() {
let mut timer = CycleTimer::with_defaults();
timer.start_unit(UnitId::FU);
thread::sleep(Duration::from_millis(5));
let result = timer.end_unit(UnitId::FU);
assert!(result.is_ok());
assert!(result.unwrap().as_millis() >= 4);
}
#[test]
fn unit_timeout_enforced() {
let config = TimingConfig { unit_timeout_ms: 1, cycle_timeout_ms: 10_000 };
let mut timer = CycleTimer::new(config);
timer.start_unit(UnitId::FU);
thread::sleep(Duration::from_millis(10));
assert!(matches!(timer.end_unit(UnitId::FU), Err(TimingError::UnitTimeout { .. })));
}
#[test]
fn cycle_timeout_enforced() {
let config = TimingConfig { unit_timeout_ms: 10_000, cycle_timeout_ms: 1 };
let timer = CycleTimer::new(config);
thread::sleep(Duration::from_millis(10));
assert!(matches!(timer.check_cycle_timeout(), Err(TimingError::CycleTimeout { .. })));
}
}