use std::future::pending;
#[cfg(test)]
use std::time::Duration;
use tokio::time::{Interval, MissedTickBehavior, interval};
use super::frame_rate::FrameRate;
use super::pending_work::PendingWork;
pub(super) struct FrameScheduler {
interval: Interval,
pub(super) pending: PendingWork,
}
impl FrameScheduler {
pub(super) fn new(frame_rate: FrameRate) -> Self {
let frame_duration = frame_rate.frame_duration();
let mut interval = interval(frame_duration);
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
Self {
interval,
pending: PendingWork::new(),
}
}
#[cfg(test)]
pub(super) fn frame_period(&self) -> Duration {
self.interval.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;
}
self.interval.tick().await;
}
}
#[cfg(test)]
mod tests {
use tokio::time::{Duration, Instant, timeout};
use super::*;
fn frame_rate(value: u32) -> FrameRate {
FrameRate::try_new(value).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"
);
}
}