1#![allow(missing_docs)]
3
4use std::time::{Duration, Instant};
16
17use crate::types::UnitId;
18
19pub const DEFAULT_UNIT_TIMEOUT_MS: u64 = 1_000;
21
22pub const DEFAULT_CYCLE_TIMEOUT_MS: u64 = 10_000;
24
25#[derive(Debug, Clone, thiserror::Error)]
27#[non_exhaustive]
28pub enum TimingError {
29 #[error("Unit timeout: {unit} took {elapsed_ms}ms, exceeds δ_max={max_ms}ms")]
31 UnitTimeout {
32 unit: UnitId,
33 elapsed_ms: u64,
34 max_ms: u64,
35 },
36
37 #[error("Cycle timeout: {elapsed_ms}ms exceeds Δt_max={max_ms}ms")]
39 CycleTimeout { elapsed_ms: u64, max_ms: u64 },
40
41 #[error("Timing protocol violation: end_unit({unit}) called before start_unit({unit})")]
43 TimingProtocolViolation { unit: UnitId },
44}
45
46#[derive(Debug, Clone, Copy)]
48pub struct TimingConfig {
49 pub unit_timeout_ms: u64,
51
52 pub cycle_timeout_ms: u64,
54}
55
56impl Default for TimingConfig {
57 fn default() -> Self {
58 Self {
59 unit_timeout_ms: DEFAULT_UNIT_TIMEOUT_MS,
60 cycle_timeout_ms: DEFAULT_CYCLE_TIMEOUT_MS,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy)]
67pub struct UnitTiming {
68 pub unit_id: UnitId,
69 pub start: Instant,
70 pub end: Option<Instant>,
71}
72
73impl UnitTiming {
74 pub fn elapsed(&self) -> Duration {
75 match self.end {
76 Some(end) => end.duration_since(self.start),
77 None => self.start.elapsed(),
78 }
79 }
80
81 pub fn elapsed_ms(&self) -> u64 {
82 self.elapsed().as_millis() as u64
83 }
84}
85
86#[derive(Debug)]
90pub struct CycleTimer {
91 config: TimingConfig,
92 cycle_start: Instant,
93 unit_timings: [Option<UnitTiming>; 6],
94}
95
96impl CycleTimer {
97 pub fn new(config: TimingConfig) -> Self {
98 Self {
99 config,
100 cycle_start: Instant::now(),
101 unit_timings: [None; 6],
102 }
103 }
104
105 pub fn with_defaults() -> Self {
106 Self::new(TimingConfig::default())
107 }
108
109 #[allow(clippy::indexing_slicing)]
110 pub fn start_unit(&mut self, unit_id: UnitId) {
111 self.unit_timings[unit_id.index()] = Some(UnitTiming {
112 unit_id,
113 start: Instant::now(),
114 end: None,
115 });
116 }
117
118 #[allow(clippy::indexing_slicing)]
119 pub fn end_unit(&mut self, unit_id: UnitId) -> Result<Duration, TimingError> {
120 let timing = self.unit_timings[unit_id.index()]
121 .as_mut()
122 .ok_or(TimingError::TimingProtocolViolation { unit: unit_id })?;
123
124 timing.end = Some(Instant::now());
125 let elapsed = timing.elapsed();
126 let elapsed_ms = elapsed.as_millis() as u64;
127
128 if elapsed_ms > self.config.unit_timeout_ms {
129 return Err(TimingError::UnitTimeout {
130 unit: unit_id,
131 elapsed_ms,
132 max_ms: self.config.unit_timeout_ms,
133 });
134 }
135
136 Ok(elapsed)
137 }
138
139 pub fn check_cycle_timeout(&self) -> Result<(), TimingError> {
140 let elapsed_ms = self.cycle_elapsed_ms();
141 if elapsed_ms > self.config.cycle_timeout_ms {
142 return Err(TimingError::CycleTimeout {
143 elapsed_ms,
144 max_ms: self.config.cycle_timeout_ms,
145 });
146 }
147 Ok(())
148 }
149
150 pub fn cycle_elapsed_ms(&self) -> u64 {
151 self.cycle_start.elapsed().as_millis() as u64
152 }
153
154 pub fn cycle_elapsed(&self) -> Duration {
155 self.cycle_start.elapsed()
156 }
157
158 #[allow(clippy::indexing_slicing)]
159 pub fn unit_timing(&self, unit_id: UnitId) -> Option<&UnitTiming> {
160 self.unit_timings[unit_id.index()].as_ref()
161 }
162
163 pub fn config(&self) -> &TimingConfig {
164 &self.config
165 }
166
167 pub fn total_unit_time_ms(&self) -> u64 {
168 self.unit_timings
169 .iter()
170 .filter_map(|t| t.as_ref())
171 .filter(|t| t.end.is_some())
172 .map(|t| t.elapsed_ms())
173 .sum()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::thread;
181
182 #[test]
183 fn default_config_has_spec_values() {
184 let c = TimingConfig::default();
185 assert_eq!(c.unit_timeout_ms, 1_000);
186 assert_eq!(c.cycle_timeout_ms, 10_000);
187 }
188
189 #[test]
190 fn cycle_timer_starts_immediately() {
191 let timer = CycleTimer::with_defaults();
192 assert!(timer.cycle_elapsed().as_nanos() > 0);
193 }
194
195 #[test]
196 fn unit_timing_roundtrip() {
197 let mut timer = CycleTimer::with_defaults();
198 timer.start_unit(UnitId::FU);
199 thread::sleep(Duration::from_millis(5));
200 let result = timer.end_unit(UnitId::FU);
201 assert!(result.is_ok());
202 assert!(result.unwrap().as_millis() >= 4);
203 }
204
205 #[test]
206 fn unit_timeout_enforced() {
207 let config = TimingConfig { unit_timeout_ms: 1, cycle_timeout_ms: 10_000 };
208 let mut timer = CycleTimer::new(config);
209 timer.start_unit(UnitId::FU);
210 thread::sleep(Duration::from_millis(10));
211 assert!(matches!(timer.end_unit(UnitId::FU), Err(TimingError::UnitTimeout { .. })));
212 }
213
214 #[test]
215 fn cycle_timeout_enforced() {
216 let config = TimingConfig { unit_timeout_ms: 10_000, cycle_timeout_ms: 1 };
217 let timer = CycleTimer::new(config);
218 thread::sleep(Duration::from_millis(10));
219 assert!(matches!(timer.check_cycle_timeout(), Err(TimingError::CycleTimeout { .. })));
220 }
221}