supercode_frontend_tui/terminal/
frame_requester.rs1use std::time::Duration;
22use std::time::Instant;
23
24use tokio::sync::broadcast;
25use tokio::sync::mpsc;
26
27use super::frame_rate_limiter::FrameRateLimiter;
28
29#[derive(Clone, Debug)]
37pub struct FrameRequester {
38 frame_schedule_tx: mpsc::UnboundedSender<Instant>,
39}
40
41impl FrameRequester {
42 pub fn new(draw_tx: broadcast::Sender<()>) -> Self {
46 let (tx, rx) = mpsc::unbounded_channel();
47 let scheduler = FrameScheduler::new(rx, draw_tx);
48 tokio::spawn(scheduler.run());
49 Self {
50 frame_schedule_tx: tx,
51 }
52 }
53
54 pub fn schedule_frame(&self) {
56 let _ = self.frame_schedule_tx.send(Instant::now());
57 }
58
59 pub fn schedule_frame_in(&self, dur: Duration) {
61 let _ = self.frame_schedule_tx.send(Instant::now() + dur);
62 }
63}
64
65#[cfg(test)]
66impl FrameRequester {
67 #[allow(dead_code)]
69 pub(crate) fn test_dummy() -> Self {
70 let (tx, _rx) = mpsc::unbounded_channel();
71 FrameRequester {
72 frame_schedule_tx: tx,
73 }
74 }
75}
76
77struct FrameScheduler {
84 receiver: mpsc::UnboundedReceiver<Instant>,
85 draw_tx: broadcast::Sender<()>,
86 rate_limiter: FrameRateLimiter,
87}
88
89impl FrameScheduler {
90 fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self {
92 Self {
93 receiver,
94 draw_tx,
95 rate_limiter: FrameRateLimiter::default(),
96 }
97 }
98
99 async fn run(mut self) {
104 const ONE_YEAR: Duration = Duration::from_secs(60 * 60 * 24 * 365);
105 let mut next_deadline: Option<Instant> = None;
106 loop {
107 let target = next_deadline.unwrap_or_else(|| Instant::now() + ONE_YEAR);
108 let deadline = tokio::time::sleep_until(target.into());
109 tokio::pin!(deadline);
110
111 tokio::select! {
112 draw_at = self.receiver.recv() => {
113 let Some(draw_at) = draw_at else {
114 break
116 };
117 let draw_at = self.rate_limiter.clamp_deadline(draw_at);
118 next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at)));
119
120 continue;
124 }
125 _ = &mut deadline => {
126 if next_deadline.is_some() {
127 next_deadline = None;
128 self.rate_limiter.mark_emitted(target);
129 let _ = self.draw_tx.send(());
130 }
131 }
132 }
133 }
134 }
135}
136#[cfg(test)]
137mod tests {
138 use super::super::frame_rate_limiter::MIN_FRAME_INTERVAL;
139 use super::*;
140 use tokio::time;
141 use tokio_util::time::FutureExt;
142
143 #[tokio::test(flavor = "current_thread", start_paused = true)]
144 async fn test_schedule_frame_immediate_triggers_once() {
145 let (draw_tx, mut draw_rx) = broadcast::channel(16);
146 let requester = FrameRequester::new(draw_tx);
147
148 requester.schedule_frame();
149
150 time::advance(Duration::from_millis(1)).await;
152
153 let first = draw_rx
155 .recv()
156 .timeout(Duration::from_millis(50))
157 .await
158 .expect("timed out waiting for first draw");
159 assert!(first.is_ok(), "broadcast closed unexpectedly");
160
161 let second = draw_rx.recv().timeout(Duration::from_millis(20)).await;
163 assert!(second.is_err(), "unexpected extra draw received");
164 }
165
166 #[tokio::test(flavor = "current_thread", start_paused = true)]
167 async fn test_schedule_frame_in_triggers_at_delay() {
168 let (draw_tx, mut draw_rx) = broadcast::channel(16);
169 let requester = FrameRequester::new(draw_tx);
170
171 requester.schedule_frame_in(Duration::from_millis(50));
172
173 time::advance(Duration::from_millis(30)).await;
175 let early = draw_rx.recv().timeout(Duration::from_millis(10)).await;
176 assert!(early.is_err(), "draw fired too early");
177
178 time::advance(Duration::from_millis(25)).await;
180 let first = draw_rx
181 .recv()
182 .timeout(Duration::from_millis(50))
183 .await
184 .expect("timed out waiting for scheduled draw");
185 assert!(first.is_ok(), "broadcast closed unexpectedly");
186
187 let second = draw_rx.recv().timeout(Duration::from_millis(20)).await;
189 assert!(second.is_err(), "unexpected extra draw received");
190 }
191
192 #[tokio::test(flavor = "current_thread", start_paused = true)]
193 async fn test_coalesces_multiple_requests_into_single_draw() {
194 let (draw_tx, mut draw_rx) = broadcast::channel(16);
195 let requester = FrameRequester::new(draw_tx);
196
197 requester.schedule_frame();
199 requester.schedule_frame();
200 requester.schedule_frame();
201
202 time::advance(Duration::from_millis(1)).await;
204
205 let first = draw_rx
207 .recv()
208 .timeout(Duration::from_millis(50))
209 .await
210 .expect("timed out waiting for coalesced draw");
211 assert!(first.is_ok(), "broadcast closed unexpectedly");
212
213 let second = draw_rx.recv().timeout(Duration::from_millis(20)).await;
215 assert!(second.is_err(), "unexpected extra draw received");
216 }
217
218 #[tokio::test(flavor = "current_thread", start_paused = true)]
219 async fn test_coalesces_mixed_immediate_and_delayed_requests() {
220 let (draw_tx, mut draw_rx) = broadcast::channel(16);
221 let requester = FrameRequester::new(draw_tx);
222
223 requester.schedule_frame_in(Duration::from_millis(100));
225 requester.schedule_frame();
226
227 time::advance(Duration::from_millis(1)).await;
228
229 let first = draw_rx
230 .recv()
231 .timeout(Duration::from_millis(50))
232 .await
233 .expect("timed out waiting for coalesced immediate draw");
234 assert!(first.is_ok(), "broadcast closed unexpectedly");
235
236 let second = draw_rx.recv().timeout(Duration::from_millis(120)).await;
238 assert!(second.is_err(), "unexpected extra draw received");
239 }
240
241 #[tokio::test(flavor = "current_thread", start_paused = true)]
242 async fn test_limits_draw_notifications_to_120fps() {
243 let (draw_tx, mut draw_rx) = broadcast::channel(16);
244 let requester = FrameRequester::new(draw_tx);
245
246 requester.schedule_frame();
247 time::advance(Duration::from_millis(1)).await;
248 let first = draw_rx
249 .recv()
250 .timeout(Duration::from_millis(50))
251 .await
252 .expect("timed out waiting for first draw");
253 assert!(first.is_ok(), "broadcast closed unexpectedly");
254
255 requester.schedule_frame();
256 time::advance(Duration::from_millis(1)).await;
257 let early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
258 assert!(
259 early.is_err(),
260 "draw fired too early; expected max 120fps (min interval {MIN_FRAME_INTERVAL:?})"
261 );
262
263 time::advance(MIN_FRAME_INTERVAL).await;
264 let second = draw_rx
265 .recv()
266 .timeout(Duration::from_millis(50))
267 .await
268 .expect("timed out waiting for second draw");
269 assert!(second.is_ok(), "broadcast closed unexpectedly");
270 }
271
272 #[tokio::test(flavor = "current_thread", start_paused = true)]
273 async fn test_rate_limit_clamps_early_delayed_requests() {
274 let (draw_tx, mut draw_rx) = broadcast::channel(16);
275 let requester = FrameRequester::new(draw_tx);
276
277 requester.schedule_frame();
278 time::advance(Duration::from_millis(1)).await;
279 let first = draw_rx
280 .recv()
281 .timeout(Duration::from_millis(50))
282 .await
283 .expect("timed out waiting for first draw");
284 assert!(first.is_ok(), "broadcast closed unexpectedly");
285
286 requester.schedule_frame_in(Duration::from_millis(1));
287
288 time::advance(MIN_FRAME_INTERVAL / 2).await;
289 let too_early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
290 assert!(
291 too_early.is_err(),
292 "draw fired too early; expected max 120fps (min interval {MIN_FRAME_INTERVAL:?})"
293 );
294
295 time::advance(MIN_FRAME_INTERVAL).await;
296 let second = draw_rx
297 .recv()
298 .timeout(Duration::from_millis(50))
299 .await
300 .expect("timed out waiting for clamped draw");
301 assert!(second.is_ok(), "broadcast closed unexpectedly");
302 }
303
304 #[tokio::test(flavor = "current_thread", start_paused = true)]
305 async fn test_rate_limit_does_not_delay_future_draws() {
306 let (draw_tx, mut draw_rx) = broadcast::channel(16);
307 let requester = FrameRequester::new(draw_tx);
308
309 requester.schedule_frame();
310 time::advance(Duration::from_millis(1)).await;
311 let first = draw_rx
312 .recv()
313 .timeout(Duration::from_millis(50))
314 .await
315 .expect("timed out waiting for first draw");
316 assert!(first.is_ok(), "broadcast closed unexpectedly");
317
318 requester.schedule_frame_in(Duration::from_millis(50));
319
320 time::advance(Duration::from_millis(49)).await;
321 let early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
322 assert!(early.is_err(), "draw fired too early");
323
324 time::advance(Duration::from_millis(1)).await;
325 let second = draw_rx
326 .recv()
327 .timeout(Duration::from_millis(50))
328 .await
329 .expect("timed out waiting for delayed draw");
330 assert!(second.is_ok(), "broadcast closed unexpectedly");
331 }
332
333 #[tokio::test(flavor = "current_thread", start_paused = true)]
334 async fn test_multiple_delayed_requests_coalesce_to_earliest() {
335 let (draw_tx, mut draw_rx) = broadcast::channel(16);
336 let requester = FrameRequester::new(draw_tx);
337
338 requester.schedule_frame_in(Duration::from_millis(100));
340 requester.schedule_frame_in(Duration::from_millis(20));
341 requester.schedule_frame_in(Duration::from_millis(120));
342
343 time::advance(Duration::from_millis(10)).await;
345 let early = draw_rx.recv().timeout(Duration::from_millis(10)).await;
346 assert!(early.is_err(), "draw fired too early");
347
348 time::advance(Duration::from_millis(20)).await;
350 let first = draw_rx
351 .recv()
352 .timeout(Duration::from_millis(50))
353 .await
354 .expect("timed out waiting for earliest coalesced draw");
355 assert!(first.is_ok(), "broadcast closed unexpectedly");
356
357 let second = draw_rx.recv().timeout(Duration::from_millis(120)).await;
359 assert!(second.is_err(), "unexpected extra draw received");
360 }
361}