perf(compositor): account for late frame dispatch
Include observed timer dispatch delay in the render budget, capped at one refresh and decayed by elapsed time. Preserve the scheduled presentation target and output-local state. Existing scheduling assertions remain intact; a negative control verifies the new deadline regression.
This commit is contained in:
1 parent
f5fff6d930
commit
417f78aa94
4 files changed
+112
-15
No files matched your search
@@ -246,7 +246,7 @@ mod tests {
|
|||||||
scheduler.request(Duration::ZERO);
|
scheduler.request(Duration::ZERO);
|
||||||
let scheduled = scheduler.plan(Duration::ZERO).unwrap();
|
let scheduled = scheduler.plan(Duration::ZERO).unwrap();
|
||||||
assert_eq!(fallback.deadline(start, interval, true, true), None);
|
assert_eq!(fallback.deadline(start, interval, true, true), None);
|
||||||
let plan = scheduler.begin().unwrap();
|
let plan = scheduler.begin(scheduled.render_at).unwrap();
|
||||||
assert_eq!(plan, scheduled);
|
assert_eq!(plan, scheduled);
|
||||||
scheduler.rendered(
|
scheduler.rendered(
|
||||||
plan,
|
plan,
|
||||||
@@ -297,7 +297,7 @@ mod tests {
|
|||||||
fallback.deadline(deadline, interval, needs_final_frame, true),
|
fallback.deadline(deadline, interval, needs_final_frame, true),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
scheduler.begin().unwrap();
|
scheduler.begin(deadline.duration_since(start)).unwrap();
|
||||||
assert!(!scheduler.has_pending_request());
|
assert!(!scheduler.has_pending_request());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(plan) = output.scheduler.begin() else {
|
let Some(plan) = output.scheduler.begin(monotonic_now()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
const MIN_RENDER: Duration = Duration::from_millis(2);
|
const MIN_RENDER: Duration = Duration::from_millis(2);
|
||||||
const TIMER_MARGIN: Duration = Duration::from_millis(1);
|
const TIMER_MARGIN: Duration = Duration::from_millis(1);
|
||||||
|
const WAKEUP_DECAY: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub(crate) struct FramePlan {
|
pub(crate) struct FramePlan {
|
||||||
@@ -25,6 +26,8 @@ pub(crate) struct FrameScheduler {
|
|||||||
render_variance: Duration,
|
render_variance: Duration,
|
||||||
last_sample: Option<Duration>,
|
last_sample: Option<Duration>,
|
||||||
extra_margin: Duration,
|
extra_margin: Duration,
|
||||||
|
wakeup_delay: Duration,
|
||||||
|
last_wakeup: Option<Duration>,
|
||||||
pub missed_deadlines: u64,
|
pub missed_deadlines: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +45,8 @@ impl FrameScheduler {
|
|||||||
render_variance: Duration::ZERO,
|
render_variance: Duration::ZERO,
|
||||||
last_sample: None,
|
last_sample: None,
|
||||||
extra_margin: Duration::ZERO,
|
extra_margin: Duration::ZERO,
|
||||||
|
wakeup_delay: Duration::ZERO,
|
||||||
|
last_wakeup: None,
|
||||||
missed_deadlines: 0,
|
missed_deadlines: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,11 +73,27 @@ impl FrameScheduler {
|
|||||||
// A cold GPU is less predictable. Start immediately on idle wakeup.
|
// A cold GPU is less predictable. Start immediately on idle wakeup.
|
||||||
self.interval
|
self.interval
|
||||||
} else {
|
} else {
|
||||||
(self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin)
|
(self.render_estimate
|
||||||
|
+ self.render_variance * 2
|
||||||
|
+ TIMER_MARGIN
|
||||||
|
+ self.extra_margin
|
||||||
|
+ self.wakeup_margin(now))
|
||||||
.min(self.interval)
|
.min(self.interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wakeup_margin(&self, now: Duration) -> Duration {
|
||||||
|
if self.wakeup_delay.is_zero() {
|
||||||
|
return Duration::ZERO;
|
||||||
|
}
|
||||||
|
let elapsed = self
|
||||||
|
.last_wakeup
|
||||||
|
.map_or(Duration::ZERO, |last| now.saturating_sub(last));
|
||||||
|
// Forget transient contention by elapsed time, independent of refresh rate.
|
||||||
|
self.wakeup_delay
|
||||||
|
.mul_f64((-elapsed.as_secs_f64() / WAKEUP_DECAY.as_secs_f64()).exp())
|
||||||
|
}
|
||||||
|
|
||||||
fn phase_at_or_after(&self, time: Duration) -> Duration {
|
fn phase_at_or_after(&self, time: Duration) -> Duration {
|
||||||
let Some(last) = self.last_presented else {
|
let Some(last) = self.last_presented else {
|
||||||
return time;
|
return time;
|
||||||
@@ -134,8 +155,15 @@ impl FrameScheduler {
|
|||||||
Some(plan)
|
Some(plan)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn begin(&mut self) -> Option<FramePlan> {
|
pub fn begin(&mut self, now: Duration) -> Option<FramePlan> {
|
||||||
let plan = self.scheduled.take()?;
|
let plan = self.scheduled.take()?;
|
||||||
|
if plan.phase_known {
|
||||||
|
// Rendering cost starts after dispatch. Account separately for time
|
||||||
|
// spent waiting for a CPU or for earlier event-loop callbacks.
|
||||||
|
let late = now.saturating_sub(plan.render_at).min(self.interval);
|
||||||
|
self.wakeup_delay = self.wakeup_margin(now).max(late);
|
||||||
|
self.last_wakeup = Some(now);
|
||||||
|
}
|
||||||
self.requested_at = None;
|
self.requested_at = None;
|
||||||
self.last_target = Some(plan.present_at);
|
self.last_target = Some(plan.present_at);
|
||||||
Some(plan)
|
Some(plan)
|
||||||
@@ -259,7 +287,7 @@ mod tests {
|
|||||||
assert_eq!((plan.render_at, plan.present_at), (ms(107), ms(110)));
|
assert_eq!((plan.render_at, plan.present_at), (ms(107), ms(110)));
|
||||||
scheduler.request(ms(102));
|
scheduler.request(ms(102));
|
||||||
assert_eq!(scheduler.plan(ms(102)), Some(plan));
|
assert_eq!(scheduler.plan(ms(102)), Some(plan));
|
||||||
assert_eq!(scheduler.begin(), Some(plan));
|
assert_eq!(scheduler.begin(plan.render_at), Some(plan));
|
||||||
scheduler.rendered(plan, ms(108), ms(1), true);
|
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||||
scheduler.request(ms(109));
|
scheduler.request(ms(109));
|
||||||
assert_eq!(scheduler.plan(ms(109)), None);
|
assert_eq!(scheduler.plan(ms(109)), None);
|
||||||
@@ -274,7 +302,7 @@ mod tests {
|
|||||||
scheduler.request(ms(10_100));
|
scheduler.request(ms(10_100));
|
||||||
let plan = scheduler.plan(ms(10_100)).unwrap();
|
let plan = scheduler.plan(ms(10_100)).unwrap();
|
||||||
assert_eq!(plan.render_at, ms(10_100));
|
assert_eq!(plan.render_at, ms(10_100));
|
||||||
scheduler.begin();
|
scheduler.begin(plan.render_at);
|
||||||
scheduler.rendered(plan, ms(10_101), ms(1), true);
|
scheduler.rendered(plan, ms(10_101), ms(1), true);
|
||||||
scheduler.presented(ms(10_110));
|
scheduler.presented(ms(10_110));
|
||||||
assert_eq!(scheduler.missed_deadlines, 0);
|
assert_eq!(scheduler.missed_deadlines, 0);
|
||||||
@@ -286,7 +314,7 @@ mod tests {
|
|||||||
scheduler.presented(ms(100));
|
scheduler.presented(ms(100));
|
||||||
scheduler.request(ms(101));
|
scheduler.request(ms(101));
|
||||||
let plan = scheduler.plan(ms(101)).unwrap();
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
scheduler.begin();
|
scheduler.begin(plan.render_at);
|
||||||
scheduler.rendered(plan, ms(108), ms(1), true);
|
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||||
assert_eq!(scheduler.presented(ms(130)), Some(plan));
|
assert_eq!(scheduler.presented(ms(130)), Some(plan));
|
||||||
assert_eq!(scheduler.missed_deadlines, 2);
|
assert_eq!(scheduler.missed_deadlines, 2);
|
||||||
@@ -294,12 +322,81 @@ mod tests {
|
|||||||
assert_eq!(scheduler.presented(ms(129)), None);
|
assert_eq!(scheduler.presented(ms(129)), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn late_timer_dispatch_moves_the_next_render_earlier_without_skipping_a_refresh() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let first = scheduler.plan(ms(101)).unwrap();
|
||||||
|
assert_eq!(first.render_at, ms(107));
|
||||||
|
let dispatch = first.render_at + ms(3);
|
||||||
|
assert_eq!(scheduler.begin(dispatch), Some(first));
|
||||||
|
scheduler.rendered(first, dispatch + ms(2), ms(2), true);
|
||||||
|
scheduler.presented(ms(120));
|
||||||
|
assert_eq!(scheduler.missed_deadlines, 1);
|
||||||
|
|
||||||
|
scheduler.request(ms(120));
|
||||||
|
let next = scheduler.plan(ms(120)).unwrap();
|
||||||
|
assert_eq!(next.present_at, ms(130), "retain the next physical refresh");
|
||||||
|
assert!(
|
||||||
|
next.render_at + ms(3) + ms(2) <= next.present_at - TIMER_MARGIN,
|
||||||
|
"the next frame must budget both the observed wakeup delay and rendering"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
scheduler.plan(ms(121)),
|
||||||
|
Some(next),
|
||||||
|
"new input must not postpone the plan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wakeup_history_decays_by_time_is_bounded_and_resets_with_the_output() {
|
||||||
|
let mut fast = FrameScheduler::new(ms(5));
|
||||||
|
let mut slow = FrameScheduler::new(ms(20));
|
||||||
|
let mut margins = Vec::new();
|
||||||
|
for scheduler in [&mut fast, &mut slow] {
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
|
let now = plan.render_at + ms(3);
|
||||||
|
scheduler.begin(now);
|
||||||
|
assert_eq!(scheduler.wakeup_margin(now), ms(3));
|
||||||
|
margins.push(scheduler.wakeup_margin(now + ms(1000)));
|
||||||
|
assert!(scheduler.wakeup_margin(now + ms(5000)) < Duration::from_micros(21));
|
||||||
|
scheduler.reset(ms(10));
|
||||||
|
assert_eq!(scheduler.wakeup_margin(now), Duration::ZERO);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
margins[0], margins[1],
|
||||||
|
"decay must not depend on display refresh rate"
|
||||||
|
);
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
|
let now = plan.render_at + ms(200);
|
||||||
|
scheduler.begin(now);
|
||||||
|
assert_eq!(scheduler.wakeup_margin(now), ms(10));
|
||||||
|
assert!(scheduler.budget(now) <= ms(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_phase_does_not_train_wakeup_history_or_move_a_pending_plan() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.request(ms(100));
|
||||||
|
let plan = scheduler.plan(ms(100)).unwrap();
|
||||||
|
assert!(!plan.phase_known);
|
||||||
|
assert_eq!(scheduler.plan(ms(120)), Some(plan));
|
||||||
|
scheduler.begin(ms(130));
|
||||||
|
assert_eq!(scheduler.wakeup_margin(ms(130)), Duration::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn first_retirement_aligns_the_next_frame_to_the_observed_refresh() {
|
fn first_retirement_aligns_the_next_frame_to_the_observed_refresh() {
|
||||||
let mut scheduler = FrameScheduler::new(ms(10));
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
scheduler.request(ms(100));
|
scheduler.request(ms(100));
|
||||||
let plan = scheduler.plan(ms(100)).unwrap();
|
let plan = scheduler.plan(ms(100)).unwrap();
|
||||||
scheduler.begin();
|
scheduler.begin(plan.render_at);
|
||||||
scheduler.rendered(plan, ms(102), ms(2), true);
|
scheduler.rendered(plan, ms(102), ms(2), true);
|
||||||
scheduler.presented(ms(103));
|
scheduler.presented(ms(103));
|
||||||
|
|
||||||
@@ -313,7 +410,7 @@ mod tests {
|
|||||||
scheduler.presented(ms(100));
|
scheduler.presented(ms(100));
|
||||||
scheduler.request(ms(101));
|
scheduler.request(ms(101));
|
||||||
let plan = scheduler.plan(ms(101)).unwrap();
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
scheduler.begin();
|
scheduler.begin(plan.render_at);
|
||||||
scheduler.rendered(plan, ms(108), ms(1), false);
|
scheduler.rendered(plan, ms(108), ms(1), false);
|
||||||
scheduler.callback_sent(ms(110));
|
scheduler.callback_sent(ms(110));
|
||||||
scheduler.request(ms(108));
|
scheduler.request(ms(108));
|
||||||
@@ -330,7 +427,7 @@ mod tests {
|
|||||||
scheduler.presented(ms(100));
|
scheduler.presented(ms(100));
|
||||||
scheduler.request(ms(101));
|
scheduler.request(ms(101));
|
||||||
let unchanged = scheduler.plan(ms(101)).unwrap();
|
let unchanged = scheduler.plan(ms(101)).unwrap();
|
||||||
scheduler.begin();
|
scheduler.begin(unchanged.render_at);
|
||||||
scheduler.rendered(unchanged, ms(108), ms(1), false);
|
scheduler.rendered(unchanged, ms(108), ms(1), false);
|
||||||
let callback_at = scheduler
|
let callback_at = scheduler
|
||||||
.callback_deadline(ms(108), unchanged.present_at)
|
.callback_deadline(ms(108), unchanged.present_at)
|
||||||
@@ -339,7 +436,7 @@ mod tests {
|
|||||||
|
|
||||||
scheduler.request(ms(109));
|
scheduler.request(ms(109));
|
||||||
let changed = scheduler.plan(ms(109)).unwrap();
|
let changed = scheduler.plan(ms(109)).unwrap();
|
||||||
scheduler.begin();
|
scheduler.begin(changed.render_at);
|
||||||
scheduler.rendered(changed, ms(118), ms(1), true);
|
scheduler.rendered(changed, ms(118), ms(1), true);
|
||||||
|
|
||||||
// The old timer is dispatched late, after a new frame was queued.
|
// The old timer is dispatched late, after a new frame was queued.
|
||||||
@@ -393,7 +490,7 @@ mod tests {
|
|||||||
scheduler.presented(ms(100));
|
scheduler.presented(ms(100));
|
||||||
scheduler.request(ms(101));
|
scheduler.request(ms(101));
|
||||||
let plan = scheduler.plan(ms(101)).unwrap();
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
scheduler.begin();
|
scheduler.begin(plan.render_at);
|
||||||
scheduler.rendered(plan, ms(108), ms(1), true);
|
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||||
scheduler.request(ms(109));
|
scheduler.request(ms(109));
|
||||||
assert_eq!(scheduler.presented(ms(90)), Some(plan));
|
assert_eq!(scheduler.presented(ms(90)), Some(plan));
|
||||||
@@ -416,7 +513,7 @@ mod tests {
|
|||||||
unknown.request(ms(100));
|
unknown.request(ms(100));
|
||||||
let plan = unknown.plan(ms(100)).unwrap();
|
let plan = unknown.plan(ms(100)).unwrap();
|
||||||
assert_eq!(unknown.forecast_time(ms(101), plan), None);
|
assert_eq!(unknown.forecast_time(ms(101), plan), None);
|
||||||
unknown.begin();
|
unknown.begin(plan.render_at);
|
||||||
unknown.rendered(plan, ms(102), ms(2), true);
|
unknown.rendered(plan, ms(102), ms(2), true);
|
||||||
unknown.presented(ms(130));
|
unknown.presented(ms(130));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ mod tests {
|
|||||||
for tick in 0..(hz * 3 / 10) {
|
for tick in 0..(hz * 3 / 10) {
|
||||||
let now = interval * tick as u32;
|
let now = interval * tick as u32;
|
||||||
if scheduler.plan(now).is_some() {
|
if scheduler.plan(now).is_some() {
|
||||||
let plan = scheduler.begin().unwrap();
|
let plan = scheduler.begin(now).unwrap();
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
scheduler.rendered(plan, now, Duration::ZERO, false);
|
scheduler.rendered(plan, now, Duration::ZERO, false);
|
||||||
if state.sample_frame(&output, Duration::ZERO).animating {
|
if state.sample_frame(&output, Duration::ZERO).animating {
|
||||||
|
|||||||
Reference in new issue
Block a user