Skip to main content

agent_sim/sim/
time.rs

1use crate::protocol::TimeStateData;
2use crate::sim::error::TimeError;
3use std::time::Instant;
4use tokio::time::Duration;
5
6const MIN_POLL_DELAY_US: f64 = 100.0;
7const MAX_POLL_DELAY_US: f64 = 1_000_000.0;
8
9#[derive(Debug, Clone)]
10pub struct TimeStatus {
11    pub state: TimeStateData,
12    pub elapsed_ticks: u64,
13    pub elapsed_time_us: u64,
14    pub speed: f64,
15}
16
17#[derive(Debug, Clone)]
18pub struct StepResult {
19    pub requested_us: u64,
20    pub advanced_ticks: u64,
21    pub advanced_us: u64,
22}
23
24#[derive(Debug)]
25pub struct TimeEngine {
26    state: TimeStateData,
27    speed: f64,
28    elapsed_ticks: u64,
29    remainder_us: f64,
30    last_wallclock: Option<Instant>,
31}
32
33impl Default for TimeEngine {
34    fn default() -> Self {
35        Self {
36            state: TimeStateData::Paused,
37            speed: 1.0,
38            elapsed_ticks: 0,
39            remainder_us: 0.0,
40            last_wallclock: None,
41        }
42    }
43}
44
45impl TimeEngine {
46    pub fn reset(&mut self) {
47        *self = Self::default();
48    }
49
50    pub fn status(&self, tick_duration_us: u32) -> TimeStatus {
51        let tick_us = tick_duration_us as u64;
52        TimeStatus {
53            state: self.state,
54            elapsed_ticks: self.elapsed_ticks,
55            elapsed_time_us: self.elapsed_ticks.saturating_mul(tick_us),
56            speed: self.speed,
57        }
58    }
59
60    pub fn start(&mut self) -> Result<(), TimeError> {
61        if self.state == TimeStateData::Running {
62            return Err(TimeError::AlreadyRunning);
63        }
64        self.state = TimeStateData::Running;
65        self.last_wallclock = Some(Instant::now());
66        Ok(())
67    }
68
69    pub fn pause(&mut self) -> Result<(), TimeError> {
70        if self.state == TimeStateData::Paused {
71            return Err(TimeError::AlreadyPaused);
72        }
73        self.state = TimeStateData::Paused;
74        self.last_wallclock = None;
75        self.remainder_us = 0.0;
76        Ok(())
77    }
78
79    pub fn set_speed(&mut self, speed: f64) -> Result<(), TimeError> {
80        if !speed.is_finite() || speed <= 0.0 {
81            return Err(TimeError::InvalidSpeed(speed));
82        }
83        self.speed = speed;
84        Ok(())
85    }
86
87    pub fn speed(&self) -> f64 {
88        self.speed
89    }
90
91    pub fn is_running(&self) -> bool {
92        self.state == TimeStateData::Running
93    }
94
95    pub fn step_ticks(
96        &mut self,
97        tick_duration_us: u32,
98        duration_us: u64,
99    ) -> Result<StepResult, TimeError> {
100        if self.state == TimeStateData::Running {
101            return Err(TimeError::StepWhileRunning);
102        }
103        let tick_us = tick_duration_us as u64;
104        let ticks = if tick_us == 0 {
105            0
106        } else {
107            duration_us / tick_us
108        };
109        let advanced_us = ticks.saturating_mul(tick_us);
110        Ok(StepResult {
111            requested_us: duration_us,
112            advanced_ticks: ticks,
113            advanced_us,
114        })
115    }
116
117    pub fn tick_realtime_due(&mut self, tick_duration_us: u32) -> u64 {
118        if self.state != TimeStateData::Running {
119            return 0;
120        }
121        let now = Instant::now();
122        let Some(last) = self.last_wallclock else {
123            self.last_wallclock = Some(now);
124            return 0;
125        };
126        let delta = now.duration_since(last).as_secs_f64() * 1_000_000.0 * self.speed;
127        self.last_wallclock = Some(now);
128        self.remainder_us += delta;
129
130        let tick_us = tick_duration_us as f64;
131        if tick_us <= 0.0 {
132            return 0;
133        }
134        let ticks = (self.remainder_us / tick_us).floor() as u64;
135        if ticks == 0 {
136            return 0;
137        }
138        self.remainder_us -= ticks as f64 * tick_us;
139        ticks
140    }
141
142    pub fn realtime_poll_delay(&self, tick_duration_us: u32) -> Duration {
143        if self.state != TimeStateData::Running {
144            return Duration::from_millis(5);
145        }
146        if tick_duration_us == 0 {
147            return Duration::from_millis(1);
148        }
149        let tick_us = tick_duration_us as f64;
150        let remaining_sim_us = (tick_us - self.remainder_us).max(0.0);
151        let wall_us = if self.speed > 0.0 {
152            (remaining_sim_us / self.speed).ceil()
153        } else {
154            tick_us.ceil()
155        };
156        let clamped_wall_us = wall_us.clamp(MIN_POLL_DELAY_US, MAX_POLL_DELAY_US);
157        Duration::from_micros(clamped_wall_us as u64)
158    }
159
160    pub fn advance_ticks(&mut self, ticks: u64) {
161        self.elapsed_ticks = self.elapsed_ticks.saturating_add(ticks);
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::TimeEngine;
168    use crate::protocol::TimeStateData;
169    use tokio::time::Duration;
170
171    #[test]
172    fn realtime_poll_delay_scales_with_tick_duration() {
173        let engine = TimeEngine {
174            state: TimeStateData::Running,
175            speed: 1.0,
176            remainder_us: 0.0,
177            ..TimeEngine::default()
178        };
179        let delay = engine.realtime_poll_delay(20_000);
180        assert!(
181            delay >= Duration::from_millis(20),
182            "expected >=20ms delay, got {delay:?}"
183        );
184    }
185
186    #[test]
187    fn realtime_poll_delay_is_capped_to_one_second() {
188        let engine = TimeEngine {
189            state: TimeStateData::Running,
190            speed: 0.001,
191            remainder_us: 0.0,
192            ..TimeEngine::default()
193        };
194        let delay = engine.realtime_poll_delay(1_000_000);
195        assert_eq!(
196            delay,
197            Duration::from_secs(1),
198            "poll delay should be capped for extreme slow speeds"
199        );
200    }
201}