@@ -18,6 +18,8 @@ use std::time::Instant;
1818use tokio:: sync:: broadcast;
1919use tokio:: sync:: mpsc;
2020
21+ use super :: frame_rate_limiter:: FrameRateLimiter ;
22+
2123/// A requester for scheduling future frame draws on the TUI event loop.
2224///
2325/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
@@ -68,15 +70,23 @@ impl FrameRequester {
6870/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
6971///
7072/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
73+ ///
74+ /// To avoid wasted redraw work, draw notifications are clamped to a maximum of 60 FPS (see
75+ /// [`FrameRateLimiter`]).
7176struct FrameScheduler {
7277 receiver : mpsc:: UnboundedReceiver < Instant > ,
7378 draw_tx : broadcast:: Sender < ( ) > ,
79+ rate_limiter : FrameRateLimiter ,
7480}
7581
7682impl FrameScheduler {
7783 /// Create a new FrameScheduler with the provided receiver and draw notification sender.
7884 fn new ( receiver : mpsc:: UnboundedReceiver < Instant > , draw_tx : broadcast:: Sender < ( ) > ) -> Self {
79- Self { receiver, draw_tx }
85+ Self {
86+ receiver,
87+ draw_tx,
88+ rate_limiter : FrameRateLimiter :: default ( ) ,
89+ }
8090 }
8191
8292 /// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
@@ -97,6 +107,7 @@ impl FrameScheduler {
97107 // All senders dropped; exit the scheduler.
98108 break
99109 } ;
110+ let draw_at = self . rate_limiter. clamp_deadline( draw_at) ;
100111 next_deadline = Some ( next_deadline. map_or( draw_at, |cur| cur. min( draw_at) ) ) ;
101112
102113 // Do not send a draw immediately here. By continuing the loop,
@@ -107,6 +118,7 @@ impl FrameScheduler {
107118 _ = & mut deadline => {
108119 if next_deadline. is_some( ) {
109120 next_deadline = None ;
121+ self . rate_limiter. mark_emitted( target) ;
110122 let _ = self . draw_tx. send( ( ) ) ;
111123 }
112124 }
@@ -116,6 +128,7 @@ impl FrameScheduler {
116128}
117129#[ cfg( test) ]
118130mod tests {
131+ use super :: super :: frame_rate_limiter:: MIN_FRAME_INTERVAL ;
119132 use super :: * ;
120133 use tokio:: time;
121134 use tokio_util:: time:: FutureExt ;
@@ -218,6 +231,98 @@ mod tests {
218231 assert ! ( second. is_err( ) , "unexpected extra draw received" ) ;
219232 }
220233
234+ #[ tokio:: test( flavor = "current_thread" , start_paused = true ) ]
235+ async fn test_limits_draw_notifications_to_60fps ( ) {
236+ let ( draw_tx, mut draw_rx) = broadcast:: channel ( 16 ) ;
237+ let requester = FrameRequester :: new ( draw_tx) ;
238+
239+ requester. schedule_frame ( ) ;
240+ time:: advance ( Duration :: from_millis ( 1 ) ) . await ;
241+ let first = draw_rx
242+ . recv ( )
243+ . timeout ( Duration :: from_millis ( 50 ) )
244+ . await
245+ . expect ( "timed out waiting for first draw" ) ;
246+ assert ! ( first. is_ok( ) , "broadcast closed unexpectedly" ) ;
247+
248+ requester. schedule_frame ( ) ;
249+ time:: advance ( Duration :: from_millis ( 1 ) ) . await ;
250+ let early = draw_rx. recv ( ) . timeout ( Duration :: from_millis ( 1 ) ) . await ;
251+ assert ! (
252+ early. is_err( ) ,
253+ "draw fired too early; expected max 60fps (min interval {MIN_FRAME_INTERVAL:?})"
254+ ) ;
255+
256+ time:: advance ( MIN_FRAME_INTERVAL ) . await ;
257+ let second = draw_rx
258+ . recv ( )
259+ . timeout ( Duration :: from_millis ( 50 ) )
260+ . await
261+ . expect ( "timed out waiting for second draw" ) ;
262+ assert ! ( second. is_ok( ) , "broadcast closed unexpectedly" ) ;
263+ }
264+
265+ #[ tokio:: test( flavor = "current_thread" , start_paused = true ) ]
266+ async fn test_rate_limit_clamps_early_delayed_requests ( ) {
267+ let ( draw_tx, mut draw_rx) = broadcast:: channel ( 16 ) ;
268+ let requester = FrameRequester :: new ( draw_tx) ;
269+
270+ requester. schedule_frame ( ) ;
271+ time:: advance ( Duration :: from_millis ( 1 ) ) . await ;
272+ let first = draw_rx
273+ . recv ( )
274+ . timeout ( Duration :: from_millis ( 50 ) )
275+ . await
276+ . expect ( "timed out waiting for first draw" ) ;
277+ assert ! ( first. is_ok( ) , "broadcast closed unexpectedly" ) ;
278+
279+ requester. schedule_frame_in ( Duration :: from_millis ( 1 ) ) ;
280+
281+ time:: advance ( Duration :: from_millis ( 10 ) ) . await ;
282+ let too_early = draw_rx. recv ( ) . timeout ( Duration :: from_millis ( 1 ) ) . await ;
283+ assert ! (
284+ too_early. is_err( ) ,
285+ "draw fired too early; expected max 60fps (min interval {MIN_FRAME_INTERVAL:?})"
286+ ) ;
287+
288+ time:: advance ( MIN_FRAME_INTERVAL ) . await ;
289+ let second = draw_rx
290+ . recv ( )
291+ . timeout ( Duration :: from_millis ( 50 ) )
292+ . await
293+ . expect ( "timed out waiting for clamped draw" ) ;
294+ assert ! ( second. is_ok( ) , "broadcast closed unexpectedly" ) ;
295+ }
296+
297+ #[ tokio:: test( flavor = "current_thread" , start_paused = true ) ]
298+ async fn test_rate_limit_does_not_delay_future_draws ( ) {
299+ let ( draw_tx, mut draw_rx) = broadcast:: channel ( 16 ) ;
300+ let requester = FrameRequester :: new ( draw_tx) ;
301+
302+ requester. schedule_frame ( ) ;
303+ time:: advance ( Duration :: from_millis ( 1 ) ) . await ;
304+ let first = draw_rx
305+ . recv ( )
306+ . timeout ( Duration :: from_millis ( 50 ) )
307+ . await
308+ . expect ( "timed out waiting for first draw" ) ;
309+ assert ! ( first. is_ok( ) , "broadcast closed unexpectedly" ) ;
310+
311+ requester. schedule_frame_in ( Duration :: from_millis ( 50 ) ) ;
312+
313+ time:: advance ( Duration :: from_millis ( 49 ) ) . await ;
314+ let early = draw_rx. recv ( ) . timeout ( Duration :: from_millis ( 1 ) ) . await ;
315+ assert ! ( early. is_err( ) , "draw fired too early" ) ;
316+
317+ time:: advance ( Duration :: from_millis ( 1 ) ) . await ;
318+ let second = draw_rx
319+ . recv ( )
320+ . timeout ( Duration :: from_millis ( 50 ) )
321+ . await
322+ . expect ( "timed out waiting for delayed draw" ) ;
323+ assert ! ( second. is_ok( ) , "broadcast closed unexpectedly" ) ;
324+ }
325+
221326 #[ tokio:: test( flavor = "current_thread" , start_paused = true ) ]
222327 async fn test_multiple_delayed_requests_coalesce_to_earliest ( ) {
223328 let ( draw_tx, mut draw_rx) = broadcast:: channel ( 16 ) ;
0 commit comments