use std::future::pending;
use std::time::Duration;
use tokio::time::{Instant, sleep_until};
use crate::cadence::next_anchor_phase_deadline;
use super::frame_rate::FrameRate;
use super::pending_work::PendingWork;
pub(super) struct FrameScheduler {
period: Duration,
schedule: Option<FrameSchedule>,
pub(super) pending: PendingWork,
}
#[derive(Clone, Copy)]
struct FrameSchedule {
anchor: Instant,
next_deadline: Instant,
}
impl FrameScheduler {
pub(super) fn new(frame_rate: FrameRate) -> Self {
Self {
period: frame_rate.frame_duration(),
schedule: None,
pending: PendingWork::new(),
}
}
#[cfg(test)]
pub(super) const fn frame_period(&self) -> Duration {
self.period
}
pub(super) const fn record_redraw(&mut self, requested: bool) {
self.pending.record_redraw(requested);
}
pub(super) const fn mark_subscriptions_dirty(&mut self) {
self.pending.mark_subscriptions_dirty();
}
pub(super) const fn has_pending_work(&self) -> bool {
self.pending.has_pending_work()
}
pub(super) const fn take_redraw(&mut self) -> bool {
self.pending.take_redraw()
}
pub(super) const fn take_subscriptions_dirty(&mut self) -> bool {
self.pending.take_subscriptions_dirty()
}
pub(super) async fn next_work_frame(&mut self) {
if !self.has_pending_work() {
pending::<()>().await;
}
let FrameSchedule {
anchor,
next_deadline,
} = *self.schedule.get_or_insert_with(|| {
let anchor = Instant::now();
FrameSchedule {
anchor,
next_deadline: anchor,
}
});
sleep_until(next_deadline).await;
self.schedule = Some(FrameSchedule {
anchor,
next_deadline: next_anchor_phase_deadline(anchor, self.period, Instant::now()),
});
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use std::num::NonZeroU32;
use std::pin::pin;
use tokio::time::{Duration, Instant, advance, timeout};
use crate::noop_waker::noop_context;
use super::*;
fn frame_rate(value: u32) -> FrameRate {
FrameRate::new(NonZeroU32::new(value).expect("frame rate must be non-zero"))
.expect("frame rate must be valid")
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn next_work_frame_parks_while_idle() {
let mut scheduler = FrameScheduler::new(frame_rate(60));
scheduler.take_redraw();
let result = timeout(Duration::from_secs(1), scheduler.next_work_frame()).await;
assert!(
result.is_err(),
"an idle scheduler must park instead of waking on the interval"
);
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn next_work_frame_is_ready_immediately_after_idle_work_arrives() {
let mut scheduler = FrameScheduler::new(frame_rate(60));
scheduler.next_work_frame().await;
scheduler.take_redraw();
let result = timeout(Duration::from_secs(1), scheduler.next_work_frame()).await;
assert!(result.is_err(), "scheduler should be parked while idle");
scheduler.mark_subscriptions_dirty();
let before = Instant::now();
timeout(Duration::from_secs(1), scheduler.next_work_frame())
.await
.expect("elapsed interval should make the re-armed frame ready");
assert_eq!(
Instant::now(),
before,
"re-arming after idle should not wait an extra frame period"
);
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn stalled_frames_do_not_replay_a_catch_up_burst() {
let mut scheduler = FrameScheduler::new(frame_rate(500));
scheduler.record_redraw(true);
scheduler.next_work_frame().await;
let anchor = Instant::now();
scheduler.record_redraw(true);
advance(Duration::from_millis(5)).await;
scheduler.next_work_frame().await;
assert_eq!(
Instant::now(),
anchor + Duration::from_millis(5),
"one frame fires immediately after a stall"
);
scheduler.record_redraw(true);
scheduler.next_work_frame().await;
assert_eq!(
Instant::now(),
anchor + Duration::from_millis(6),
"missed frame deadlines must not replay as a catch-up burst"
);
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn dropping_a_pending_frame_future_preserves_the_deadline() {
let mut scheduler = FrameScheduler::new(frame_rate(250));
scheduler.record_redraw(true);
scheduler.next_work_frame().await;
let anchor = Instant::now();
scheduler.record_redraw(true);
advance(Duration::from_millis(1)).await;
{
let fut = pin!(scheduler.next_work_frame());
assert!(
fut.poll(&mut noop_context()).is_pending(),
"mid-period frame future starts pending"
);
}
scheduler.next_work_frame().await;
assert_eq!(
Instant::now(),
anchor + Duration::from_millis(4),
"the deadline survives dropping a pending frame future"
);
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn post_stall_cadence_resumes_on_the_anchor_phase() {
let mut scheduler = FrameScheduler::new(frame_rate(250));
scheduler.record_redraw(true);
scheduler.next_work_frame().await;
let anchor = Instant::now();
scheduler.record_redraw(true);
advance(Duration::from_millis(18)).await;
scheduler.next_work_frame().await;
scheduler.record_redraw(true);
scheduler.next_work_frame().await;
assert_eq!(
Instant::now(),
anchor + Duration::from_millis(20),
"cadence resumes on the anchor's phase, not reset to now + period"
);
}
}