mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
perf(tui): cap redraw scheduling to 60fps (#8499)
Clamp frame draw notifications in the `FrameRequester` scheduler so we don't redraw more frequently than a user can perceive. This applies to both `codex-tui` and `codex-tui2`, and keeps the draw/dispatch loops simple by centralizing the rate limiting in a small helper module. - Add `FrameRateLimiter` (pure, unit-tested) to clamp draw deadlines - Apply the limiter in the scheduler before emitting `TuiEvent::Draw` - Use immediate redraw requests for scroll paths (scheduler now coalesces + clamps) - Add scheduler tests covering immediate/delayed interactions
This commit is contained in:
1 parent
40de81e7af
commit
96a65ff0ed
8 files changed
+344
-10
No files matched your search
@@ -47,6 +47,7 @@ use crate::tui::event_stream::TuiEventStream;
|
|||||||
use crate::tui::job_control::SuspendContext;
|
use crate::tui::job_control::SuspendContext;
|
||||||
|
|
||||||
mod event_stream;
|
mod event_stream;
|
||||||
|
mod frame_rate_limiter;
|
||||||
mod frame_requester;
|
mod frame_requester;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod job_control;
|
mod job_control;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//! Limits how frequently frame draw notifications may be emitted.
|
||||||
|
//!
|
||||||
|
//! Widgets sometimes call `FrameRequester::schedule_frame()` more frequently than a user can
|
||||||
|
//! perceive. This limiter clamps draw notifications to a maximum of 60 FPS to avoid wasted work.
|
||||||
|
//!
|
||||||
|
//! This is intentionally a small, pure helper so it can be unit-tested in isolation and used by
|
||||||
|
//! the async frame scheduler without adding complexity to the app/event loop.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// A 60 FPS minimum frame interval (≈16.67ms).
|
||||||
|
pub(super) const MIN_FRAME_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
||||||
|
|
||||||
|
/// Remembers the most recent emitted draw, allowing deadlines to be clamped forward.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(super) struct FrameRateLimiter {
|
||||||
|
last_emitted_at: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameRateLimiter {
|
||||||
|
/// Returns `requested`, clamped forward if it would exceed the maximum frame rate.
|
||||||
|
pub(super) fn clamp_deadline(&self, requested: Instant) -> Instant {
|
||||||
|
let Some(last_emitted_at) = self.last_emitted_at else {
|
||||||
|
return requested;
|
||||||
|
};
|
||||||
|
let min_allowed = last_emitted_at
|
||||||
|
.checked_add(MIN_FRAME_INTERVAL)
|
||||||
|
.unwrap_or(last_emitted_at);
|
||||||
|
requested.max(min_allowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records that a draw notification was emitted at `emitted_at`.
|
||||||
|
pub(super) fn mark_emitted(&mut self, emitted_at: Instant) {
|
||||||
|
self.last_emitted_at = Some(emitted_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_does_not_clamp() {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let limiter = FrameRateLimiter::default();
|
||||||
|
assert_eq!(limiter.clamp_deadline(t0), t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamps_to_min_interval_since_last_emit() {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let mut limiter = FrameRateLimiter::default();
|
||||||
|
|
||||||
|
assert_eq!(limiter.clamp_deadline(t0), t0);
|
||||||
|
limiter.mark_emitted(t0);
|
||||||
|
|
||||||
|
let too_soon = t0 + Duration::from_millis(1);
|
||||||
|
assert_eq!(limiter.clamp_deadline(too_soon), t0 + MIN_FRAME_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ use std::time::Instant;
|
|||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use super::frame_rate_limiter::FrameRateLimiter;
|
||||||
|
|
||||||
/// A requester for scheduling future frame draws on the TUI event loop.
|
/// A requester for scheduling future frame draws on the TUI event loop.
|
||||||
///
|
///
|
||||||
/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
|
/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
|
||||||
@@ -68,15 +70,23 @@ impl FrameRequester {
|
|||||||
/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
|
/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
|
||||||
///
|
///
|
||||||
/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
|
/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
|
||||||
|
///
|
||||||
|
/// To avoid wasted redraw work, draw notifications are clamped to a maximum of 60 FPS (see
|
||||||
|
/// [`FrameRateLimiter`]).
|
||||||
struct FrameScheduler {
|
struct FrameScheduler {
|
||||||
receiver: mpsc::UnboundedReceiver<Instant>,
|
receiver: mpsc::UnboundedReceiver<Instant>,
|
||||||
draw_tx: broadcast::Sender<()>,
|
draw_tx: broadcast::Sender<()>,
|
||||||
|
rate_limiter: FrameRateLimiter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrameScheduler {
|
impl FrameScheduler {
|
||||||
/// Create a new FrameScheduler with the provided receiver and draw notification sender.
|
/// Create a new FrameScheduler with the provided receiver and draw notification sender.
|
||||||
fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self {
|
fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self {
|
||||||
Self { receiver, draw_tx }
|
Self {
|
||||||
|
receiver,
|
||||||
|
draw_tx,
|
||||||
|
rate_limiter: FrameRateLimiter::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
|
/// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
|
||||||
@@ -97,6 +107,7 @@ impl FrameScheduler {
|
|||||||
// All senders dropped; exit the scheduler.
|
// All senders dropped; exit the scheduler.
|
||||||
break
|
break
|
||||||
};
|
};
|
||||||
|
let draw_at = self.rate_limiter.clamp_deadline(draw_at);
|
||||||
next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at)));
|
next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at)));
|
||||||
|
|
||||||
// Do not send a draw immediately here. By continuing the loop,
|
// Do not send a draw immediately here. By continuing the loop,
|
||||||
@@ -107,6 +118,7 @@ impl FrameScheduler {
|
|||||||
_ = &mut deadline => {
|
_ = &mut deadline => {
|
||||||
if next_deadline.is_some() {
|
if next_deadline.is_some() {
|
||||||
next_deadline = None;
|
next_deadline = None;
|
||||||
|
self.rate_limiter.mark_emitted(target);
|
||||||
let _ = self.draw_tx.send(());
|
let _ = self.draw_tx.send(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +128,7 @@ impl FrameScheduler {
|
|||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::super::frame_rate_limiter::MIN_FRAME_INTERVAL;
|
||||||
use super::*;
|
use super::*;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use tokio_util::time::FutureExt;
|
use tokio_util::time::FutureExt;
|
||||||
@@ -218,6 +231,98 @@ mod tests {
|
|||||||
assert!(second.is_err(), "unexpected extra draw received");
|
assert!(second.is_err(), "unexpected extra draw received");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
|
async fn test_limits_draw_notifications_to_60fps() {
|
||||||
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
let requester = FrameRequester::new(draw_tx);
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let first = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for first draw");
|
||||||
|
assert!(first.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
|
||||||
|
assert!(
|
||||||
|
early.is_err(),
|
||||||
|
"draw fired too early; expected max 60fps (min interval {MIN_FRAME_INTERVAL:?})"
|
||||||
|
);
|
||||||
|
|
||||||
|
time::advance(MIN_FRAME_INTERVAL).await;
|
||||||
|
let second = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for second draw");
|
||||||
|
assert!(second.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
|
async fn test_rate_limit_clamps_early_delayed_requests() {
|
||||||
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
let requester = FrameRequester::new(draw_tx);
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let first = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for first draw");
|
||||||
|
assert!(first.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
|
||||||
|
requester.schedule_frame_in(Duration::from_millis(1));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(10)).await;
|
||||||
|
let too_early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
|
||||||
|
assert!(
|
||||||
|
too_early.is_err(),
|
||||||
|
"draw fired too early; expected max 60fps (min interval {MIN_FRAME_INTERVAL:?})"
|
||||||
|
);
|
||||||
|
|
||||||
|
time::advance(MIN_FRAME_INTERVAL).await;
|
||||||
|
let second = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for clamped draw");
|
||||||
|
assert!(second.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
|
async fn test_rate_limit_does_not_delay_future_draws() {
|
||||||
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
let requester = FrameRequester::new(draw_tx);
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let first = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for first draw");
|
||||||
|
assert!(first.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
|
||||||
|
requester.schedule_frame_in(Duration::from_millis(50));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(49)).await;
|
||||||
|
let early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
|
||||||
|
assert!(early.is_err(), "draw fired too early");
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let second = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for delayed draw");
|
||||||
|
assert!(second.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
async fn test_multiple_delayed_requests_coalesce_to_earliest() {
|
async fn test_multiple_delayed_requests_coalesce_to_earliest() {
|
||||||
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
|||||||
@@ -1114,9 +1114,8 @@ impl App {
|
|||||||
.scrolled_by(delta_lines, &line_meta, visible_lines);
|
.scrolled_by(delta_lines, &line_meta, visible_lines);
|
||||||
|
|
||||||
if schedule_frame {
|
if schedule_frame {
|
||||||
// Delay redraws slightly so scroll bursts coalesce into a single frame.
|
// Request a redraw; the frame scheduler coalesces bursts and clamps to 60fps.
|
||||||
tui.frame_requester()
|
tui.frame_requester().schedule_frame();
|
||||||
.schedule_frame_in(Duration::from_millis(16));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::io::Result;
|
use std::io::Result;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::history_cell::HistoryCell;
|
use crate::history_cell::HistoryCell;
|
||||||
use crate::history_cell::UserHistoryCell;
|
use crate::history_cell::UserHistoryCell;
|
||||||
@@ -280,8 +279,8 @@ impl PagerView {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tui.frame_requester()
|
// Request a redraw; the frame scheduler coalesces bursts and clamps to 60fps.
|
||||||
.schedule_frame_in(Duration::from_millis(16));
|
tui.frame_requester().schedule_frame();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,8 +297,8 @@ impl PagerView {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tui.frame_requester()
|
// Request a redraw; the frame scheduler coalesces bursts and clamps to 60fps.
|
||||||
.schedule_frame_in(Duration::from_millis(16));
|
tui.frame_requester().schedule_frame();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ use crate::tui::job_control::SUSPEND_KEY;
|
|||||||
use crate::tui::job_control::SuspendContext;
|
use crate::tui::job_control::SuspendContext;
|
||||||
|
|
||||||
mod alt_screen_nesting;
|
mod alt_screen_nesting;
|
||||||
|
mod frame_rate_limiter;
|
||||||
mod frame_requester;
|
mod frame_requester;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod job_control;
|
mod job_control;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//! Limits how frequently frame draw notifications may be emitted.
|
||||||
|
//!
|
||||||
|
//! Widgets sometimes call `FrameRequester::schedule_frame()` more frequently than a user can
|
||||||
|
//! perceive. This limiter clamps draw notifications to a maximum of 60 FPS to avoid wasted work.
|
||||||
|
//!
|
||||||
|
//! This is intentionally a small, pure helper so it can be unit-tested in isolation and used by
|
||||||
|
//! the async frame scheduler without adding complexity to the app/event loop.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// A 60 FPS minimum frame interval (≈16.67ms).
|
||||||
|
pub(super) const MIN_FRAME_INTERVAL: Duration = Duration::from_nanos(16_666_667);
|
||||||
|
|
||||||
|
/// Remembers the most recent emitted draw, allowing deadlines to be clamped forward.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(super) struct FrameRateLimiter {
|
||||||
|
last_emitted_at: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameRateLimiter {
|
||||||
|
/// Returns `requested`, clamped forward if it would exceed the maximum frame rate.
|
||||||
|
pub(super) fn clamp_deadline(&self, requested: Instant) -> Instant {
|
||||||
|
let Some(last_emitted_at) = self.last_emitted_at else {
|
||||||
|
return requested;
|
||||||
|
};
|
||||||
|
let min_allowed = last_emitted_at
|
||||||
|
.checked_add(MIN_FRAME_INTERVAL)
|
||||||
|
.unwrap_or(last_emitted_at);
|
||||||
|
requested.max(min_allowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records that a draw notification was emitted at `emitted_at`.
|
||||||
|
pub(super) fn mark_emitted(&mut self, emitted_at: Instant) {
|
||||||
|
self.last_emitted_at = Some(emitted_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_does_not_clamp() {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let limiter = FrameRateLimiter::default();
|
||||||
|
assert_eq!(limiter.clamp_deadline(t0), t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clamps_to_min_interval_since_last_emit() {
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let mut limiter = FrameRateLimiter::default();
|
||||||
|
|
||||||
|
assert_eq!(limiter.clamp_deadline(t0), t0);
|
||||||
|
limiter.mark_emitted(t0);
|
||||||
|
|
||||||
|
let too_soon = t0 + Duration::from_millis(1);
|
||||||
|
assert_eq!(limiter.clamp_deadline(too_soon), t0 + MIN_FRAME_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ use std::time::Instant;
|
|||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use super::frame_rate_limiter::FrameRateLimiter;
|
||||||
|
|
||||||
/// A requester for scheduling future frame draws on the TUI event loop.
|
/// A requester for scheduling future frame draws on the TUI event loop.
|
||||||
///
|
///
|
||||||
/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
|
/// This is the handler side of an actor/handler pair with `FrameScheduler`, which coalesces
|
||||||
@@ -68,15 +70,23 @@ impl FrameRequester {
|
|||||||
/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
|
/// A scheduler for coalescing frame draw requests and notifying the TUI event loop.
|
||||||
///
|
///
|
||||||
/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
|
/// This type is internal to `FrameRequester` and is spawned as a task to handle scheduling logic.
|
||||||
|
///
|
||||||
|
/// To avoid wasted redraw work, draw notifications are clamped to a maximum of 60 FPS (see
|
||||||
|
/// [`FrameRateLimiter`]).
|
||||||
struct FrameScheduler {
|
struct FrameScheduler {
|
||||||
receiver: mpsc::UnboundedReceiver<Instant>,
|
receiver: mpsc::UnboundedReceiver<Instant>,
|
||||||
draw_tx: broadcast::Sender<()>,
|
draw_tx: broadcast::Sender<()>,
|
||||||
|
rate_limiter: FrameRateLimiter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrameScheduler {
|
impl FrameScheduler {
|
||||||
/// Create a new FrameScheduler with the provided receiver and draw notification sender.
|
/// Create a new FrameScheduler with the provided receiver and draw notification sender.
|
||||||
fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self {
|
fn new(receiver: mpsc::UnboundedReceiver<Instant>, draw_tx: broadcast::Sender<()>) -> Self {
|
||||||
Self { receiver, draw_tx }
|
Self {
|
||||||
|
receiver,
|
||||||
|
draw_tx,
|
||||||
|
rate_limiter: FrameRateLimiter::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
|
/// Run the scheduling loop, coalescing frame requests and notifying the TUI event loop.
|
||||||
@@ -97,6 +107,7 @@ impl FrameScheduler {
|
|||||||
// All senders dropped; exit the scheduler.
|
// All senders dropped; exit the scheduler.
|
||||||
break
|
break
|
||||||
};
|
};
|
||||||
|
let draw_at = self.rate_limiter.clamp_deadline(draw_at);
|
||||||
next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at)));
|
next_deadline = Some(next_deadline.map_or(draw_at, |cur| cur.min(draw_at)));
|
||||||
|
|
||||||
// Do not send a draw immediately here. By continuing the loop,
|
// Do not send a draw immediately here. By continuing the loop,
|
||||||
@@ -107,6 +118,7 @@ impl FrameScheduler {
|
|||||||
_ = &mut deadline => {
|
_ = &mut deadline => {
|
||||||
if next_deadline.is_some() {
|
if next_deadline.is_some() {
|
||||||
next_deadline = None;
|
next_deadline = None;
|
||||||
|
self.rate_limiter.mark_emitted(target);
|
||||||
let _ = self.draw_tx.send(());
|
let _ = self.draw_tx.send(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,6 +128,7 @@ impl FrameScheduler {
|
|||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::super::frame_rate_limiter::MIN_FRAME_INTERVAL;
|
||||||
use super::*;
|
use super::*;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use tokio_util::time::FutureExt;
|
use tokio_util::time::FutureExt;
|
||||||
@@ -218,6 +231,98 @@ mod tests {
|
|||||||
assert!(second.is_err(), "unexpected extra draw received");
|
assert!(second.is_err(), "unexpected extra draw received");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
|
async fn test_limits_draw_notifications_to_60fps() {
|
||||||
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
let requester = FrameRequester::new(draw_tx);
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let first = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for first draw");
|
||||||
|
assert!(first.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
|
||||||
|
assert!(
|
||||||
|
early.is_err(),
|
||||||
|
"draw fired too early; expected max 60fps (min interval {MIN_FRAME_INTERVAL:?})"
|
||||||
|
);
|
||||||
|
|
||||||
|
time::advance(MIN_FRAME_INTERVAL).await;
|
||||||
|
let second = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for second draw");
|
||||||
|
assert!(second.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
|
async fn test_rate_limit_clamps_early_delayed_requests() {
|
||||||
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
let requester = FrameRequester::new(draw_tx);
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let first = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for first draw");
|
||||||
|
assert!(first.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
|
||||||
|
requester.schedule_frame_in(Duration::from_millis(1));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(10)).await;
|
||||||
|
let too_early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
|
||||||
|
assert!(
|
||||||
|
too_early.is_err(),
|
||||||
|
"draw fired too early; expected max 60fps (min interval {MIN_FRAME_INTERVAL:?})"
|
||||||
|
);
|
||||||
|
|
||||||
|
time::advance(MIN_FRAME_INTERVAL).await;
|
||||||
|
let second = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for clamped draw");
|
||||||
|
assert!(second.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
|
async fn test_rate_limit_does_not_delay_future_draws() {
|
||||||
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
let requester = FrameRequester::new(draw_tx);
|
||||||
|
|
||||||
|
requester.schedule_frame();
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let first = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for first draw");
|
||||||
|
assert!(first.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
|
||||||
|
requester.schedule_frame_in(Duration::from_millis(50));
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(49)).await;
|
||||||
|
let early = draw_rx.recv().timeout(Duration::from_millis(1)).await;
|
||||||
|
assert!(early.is_err(), "draw fired too early");
|
||||||
|
|
||||||
|
time::advance(Duration::from_millis(1)).await;
|
||||||
|
let second = draw_rx
|
||||||
|
.recv()
|
||||||
|
.timeout(Duration::from_millis(50))
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for delayed draw");
|
||||||
|
assert!(second.is_ok(), "broadcast closed unexpectedly");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||||
async fn test_multiple_delayed_requests_coalesce_to_earliest() {
|
async fn test_multiple_delayed_requests_coalesce_to_earliest() {
|
||||||
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
let (draw_tx, mut draw_rx) = broadcast::channel(16);
|
||||||
|
|||||||
Reference in new issue
Block a user