Skip to main content

supercode_frontend_tui/terminal/
frame_requester.rs

1// Derived from OpenAI Codex: codex-rs/tui/src/tui/frame_requester.rs
2// Pinned source: 8604689ec5e3437eb79802d8d72249b7722fbf5b
3// Copyright 2025 OpenAI
4// Licensed under the Apache License, Version 2.0.
5// Modified by the Supercode contributors; see docs/legal/codex-frontend-extraction.toml.
6
7//! Frame draw scheduling utilities for the TUI.
8//!
9//! This module exposes [`FrameRequester`], a lightweight handle that widgets and
10//! background tasks can clone to request future redraws of the TUI.
11//!
12//! Internally it spawns a [`FrameScheduler`] task that coalesces many requests
13//! into a single notification on a broadcast channel used by the main TUI event
14//! loop. This keeps animations and status updates smooth without redrawing more
15//! often than necessary.
16//!
17//! This follows the actor-style design from
18//! [“Actors with Tokio”](https://ryhl.io/blog/actors-with-tokio/), with a
19//! dedicated scheduler task and lightweight request handles.
20
21use 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/// A requester for scheduling future frame draws on the TUI event loop.
30///
31/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
32/// multiple frame requests into a single draw operation.
33///
34/// Clones of this type can be freely shared across tasks to make it possible to trigger frame draws
35/// from anywhere in the TUI code.
36#[derive(Clone, Debug)]
37pub struct FrameRequester {
38    frame_schedule_tx: mpsc::UnboundedSender<Instant>,
39}
40
41impl FrameRequester {
42    /// Create a new FrameRequester and spawn its associated FrameScheduler task.
43    ///
44    /// The provided `draw_tx` is used to notify the TUI event loop of scheduled draws.
45    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    /// Schedule a frame draw as soon as possible.
55    pub fn schedule_frame(&self) {
56        let _ = self.frame_schedule_tx.send(Instant::now());
57    }
58
59    /// Schedule a frame draw to occur after the specified duration.
60    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    /// Create a no-op frame requester for tests.
68    #[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
77/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
78///
79/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
80///
81/// To avoid wasted redraw work, draw notifications are clamped to a maximum of 120 FPS (see
82/// [`FrameRateLimiter`]).
83struct FrameScheduler {
84    receiver: mpsc::UnboundedReceiver<Instant>,
85    draw_tx: broadcast::Sender<()>,
86    rate_limiter: FrameRateLimiter,
87}
88
89impl FrameScheduler {
90    /// Create a new FrameScheduler with the provided receiver and draw notification sender.
91    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    /// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
100    ///
101    /// This method runs indefinitely until all senders are dropped. A single draw notification
102    /// is sent for multiple requests scheduled before the next draw deadline.
103    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                        // All senders dropped; exit the scheduler.
115                        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                    // Do not send a draw immediately here. By continuing the loop,
121                    // we recompute the sleep target so the draw fires once via the
122                    // sleep branch, coalescing multiple requests into a single draw.
123                    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        // Advance time minimally to let the scheduler process and hit the deadline == now.
151        time::advance(Duration::from_millis(1)).await;
152
153        // First draw should arrive.
154        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        // No second draw should arrive.
162        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        // Advance less than the delay: no draw yet.
174        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        // Advance past the deadline: one draw should fire.
179        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        // No second draw should arrive.
188        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        // Schedule multiple immediate requests close together.
198        requester.schedule_frame();
199        requester.schedule_frame();
200        requester.schedule_frame();
201
202        // Allow the scheduler to process and hit the coalesced deadline.
203        time::advance(Duration::from_millis(1)).await;
204
205        // Expect only a single draw notification despite three requests.
206        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        // No additional draw should be sent for the same coalesced batch.
214        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        // Schedule a delayed draw and then an immediate one; should coalesce and fire at the earliest (immediate).
224        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        // The later delayed request should have been coalesced into the earlier one; no second draw.
237        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        // Schedule multiple delayed draws; they should coalesce to the earliest (10ms).
339        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        // Advance to just before the earliest deadline: no draw yet.
344        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        // Advance past the earliest deadline: one draw should fire.
349        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        // No additional draw should fire for the later delayed requests.
358        let second = draw_rx.recv().timeout(Duration::from_millis(120)).await;
359        assert!(second.is_err(), "unexpected extra draw received");
360    }
361}