Skip to main content

entrenar/train/tui/
refresh.rs

1//! Refresh Policy - Adaptive refresh rate control (ENT-060)
2//!
3//! Rate-limiting for terminal updates to balance responsiveness and performance.
4
5use std::time::{Duration, Instant};
6
7/// Adaptive refresh rate policy.
8#[derive(Debug, Clone)]
9pub struct RefreshPolicy {
10    /// Minimum interval between refreshes
11    pub min_interval: Duration,
12    /// Maximum interval (force refresh)
13    pub max_interval: Duration,
14    /// Refresh every N steps
15    pub step_interval: usize,
16    /// Last refresh time
17    last_refresh: Instant,
18    /// Last refresh step
19    last_step: usize,
20}
21
22impl Default for RefreshPolicy {
23    fn default() -> Self {
24        Self {
25            min_interval: Duration::from_millis(50),
26            max_interval: Duration::from_millis(1000),
27            step_interval: 10,
28            last_refresh: Instant::now(),
29            last_step: 0,
30        }
31    }
32}
33
34impl RefreshPolicy {
35    /// Create a new refresh policy.
36    pub fn new(min_ms: u64, max_ms: u64, step_interval: usize) -> Self {
37        Self {
38            min_interval: Duration::from_millis(min_ms),
39            max_interval: Duration::from_millis(max_ms),
40            step_interval,
41            last_refresh: Instant::now(),
42            last_step: 0,
43        }
44    }
45
46    /// Check if a refresh should occur.
47    pub fn should_refresh(&mut self, global_step: usize) -> bool {
48        let elapsed = self.last_refresh.elapsed();
49
50        // Force refresh after max interval
51        if elapsed >= self.max_interval {
52            self.last_refresh = Instant::now();
53            self.last_step = global_step;
54            return true;
55        }
56
57        // Rate-limit to min interval
58        if elapsed < self.min_interval {
59            return false;
60        }
61
62        // Step-based refresh
63        if global_step.saturating_sub(self.last_step) >= self.step_interval {
64            self.last_refresh = Instant::now();
65            self.last_step = global_step;
66            return true;
67        }
68
69        false
70    }
71
72    /// Force a refresh (resets timer).
73    pub fn force_refresh(&mut self, global_step: usize) {
74        self.last_refresh = Instant::now();
75        self.last_step = global_step;
76    }
77
78    /// Simulate time passage for deterministic testing.
79    #[cfg(test)]
80    fn advance_time(&mut self, duration: Duration) {
81        self.last_refresh -= duration;
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_refresh_policy_default() {
91        let policy = RefreshPolicy::default();
92        assert_eq!(policy.min_interval, Duration::from_millis(50));
93        assert_eq!(policy.max_interval, Duration::from_millis(1000));
94        assert_eq!(policy.step_interval, 10);
95    }
96
97    #[test]
98    fn test_refresh_policy_new() {
99        let policy = RefreshPolicy::new(100, 500, 5);
100        assert_eq!(policy.min_interval, Duration::from_millis(100));
101        assert_eq!(policy.max_interval, Duration::from_millis(500));
102        assert_eq!(policy.step_interval, 5);
103    }
104
105    #[test]
106    fn test_refresh_policy_rate_limiting() {
107        // Use a large min_interval (10s) so wall-clock jitter cannot cause
108        // the "immediate call blocked" assertion to flake under CI load.
109        let mut policy = RefreshPolicy::new(10_000, 60_000, 1);
110        policy.force_refresh(0);
111
112        // Immediate call should be blocked (min_interval not elapsed)
113        let blocked = !policy.should_refresh(1);
114        assert!(blocked, "Immediate refresh should be blocked");
115
116        // Simulate 15s passing (deterministic, no thread::sleep) — past min_interval
117        policy.advance_time(Duration::from_millis(15_000));
118        let allowed = policy.should_refresh(2);
119        assert!(allowed, "Refresh should be allowed after min_interval");
120    }
121
122    #[test]
123    fn test_refresh_policy_step_interval() {
124        let mut policy = RefreshPolicy::new(0, 10000, 10);
125        policy.force_refresh(0);
126
127        // Simulate time past min_interval (deterministic)
128        policy.advance_time(Duration::from_millis(20));
129
130        // Step 5 should not trigger (need 10 steps)
131        assert!(!policy.should_refresh(5));
132        // Step 10 should trigger
133        assert!(policy.should_refresh(10));
134    }
135
136    #[test]
137    fn test_refresh_policy_force_refresh() {
138        let mut policy = RefreshPolicy::default();
139        policy.force_refresh(100);
140        assert_eq!(policy.last_step, 100);
141    }
142
143    #[test]
144    fn test_refresh_policy_max_interval_triggers() {
145        let mut policy = RefreshPolicy::new(10, 50, 1000);
146        policy.force_refresh(0);
147
148        // Simulate 500ms passing (deterministic)
149        policy.advance_time(Duration::from_millis(500));
150
151        // Should trigger due to max_interval
152        assert!(policy.should_refresh(1));
153    }
154
155    #[test]
156    fn test_refresh_policy_clone() {
157        let policy = RefreshPolicy::new(100, 500, 5);
158        let cloned = policy.clone();
159        assert_eq!(policy.min_interval, cloned.min_interval);
160        assert_eq!(policy.max_interval, cloned.max_interval);
161        assert_eq!(policy.step_interval, cloned.step_interval);
162    }
163
164    #[test]
165    fn test_refresh_policy_debug() {
166        let policy = RefreshPolicy::default();
167        let debug_str = format!("{policy:?}");
168        assert!(debug_str.contains("RefreshPolicy"));
169    }
170
171    #[test]
172    fn test_refresh_policy_no_refresh_below_step_interval() {
173        let mut policy = RefreshPolicy::new(0, 10000, 100);
174        policy.force_refresh(0);
175
176        // Simulate time past min_interval (deterministic)
177        policy.advance_time(Duration::from_millis(20));
178
179        // Steps below interval should not trigger
180        assert!(!policy.should_refresh(50));
181        assert!(!policy.should_refresh(99));
182        // But at 100 it should trigger
183        assert!(policy.should_refresh(100));
184    }
185}