servo/components/timers/lib.rs
Martin Robinson 23acb623c8
script: Allow reflows that do not produce display lists (#37186)
This change has two parts which depend on each other:

1. An early exit in the layout process, which allows for skipping
   display list construction entirely when nothing would change.
2. A simplification and unification of the way that "fake" animation
   frames are triggered. Now this happens on an entire ScriptThread at
   once and is based on whether or not any Pipeline triggered a display
   list update.

   Animations are never canceled in the compositor when the Pipeline
   isn't updating, instead the fake animation frame is triggered far
   enough in the future that an unexpected compositor tick will cancel
   it. This could happen, for instance, if some other Pipeline in some
   other ScriptThread produced a new display list for a tick. This makes
   everything simpler about these ticks.

The goal is that in a future change the ScriptThread-based animation
ticks will be made more generic so that they can throttle the number of
"update the rendering" calls triggered by script.

This should make Servo do a lot less work when moving the cursor over a
page. Before it would constantly produce new display lists.

Fixes: #17029.
Testing: This should not cause any web observable changes. The fact that
all WPT tests keep passing is the test for this change.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
2025-06-12 19:25:04 +00:00

132 lines
4 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! A generic timer scheduler module that can be integrated into a crossbeam based event
//! loop or used to launch a background timer thread.
#![deny(unsafe_code)]
use std::cmp::{self, Ord};
use std::collections::BinaryHeap;
use std::time::{Duration, Instant};
use crossbeam_channel::{Receiver, after, never};
use malloc_size_of_derive::MallocSizeOf;
/// A callback to pass to the [`TimerScheduler`] to be called when the timer is
/// dispatched.
pub type BoxedTimerCallback = Box<dyn Fn() + Send + 'static>;
/// Requests a TimerEvent-Message be sent after the given duration.
#[derive(MallocSizeOf)]
pub struct TimerEventRequest {
#[ignore_malloc_size_of = "Size of a boxed function"]
pub callback: BoxedTimerCallback,
pub duration: Duration,
}
impl TimerEventRequest {
fn dispatch(self) {
(self.callback)()
}
}
#[derive(MallocSizeOf)]
struct ScheduledEvent {
id: TimerId,
request: TimerEventRequest,
for_time: Instant,
}
impl Ord for ScheduledEvent {
fn cmp(&self, other: &ScheduledEvent) -> cmp::Ordering {
self.for_time.cmp(&other.for_time).reverse()
}
}
impl PartialOrd for ScheduledEvent {
fn partial_cmp(&self, other: &ScheduledEvent) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for ScheduledEvent {}
impl PartialEq for ScheduledEvent {
fn eq(&self, other: &ScheduledEvent) -> bool {
std::ptr::eq(self, other)
}
}
#[derive(Clone, Copy, MallocSizeOf, PartialEq)]
pub struct TimerId(usize);
/// A queue of [`TimerEventRequest`]s that are stored in order of next-to-fire.
#[derive(Default, MallocSizeOf)]
pub struct TimerScheduler {
/// A priority queue of future events, sorted by due time.
queue: BinaryHeap<ScheduledEvent>,
/// The current timer id, used to generate new ones.
current_id: usize,
}
impl TimerScheduler {
/// Schedule a new timer for on this [`TimerScheduler`].
pub fn schedule_timer(&mut self, request: TimerEventRequest) -> TimerId {
let for_time = Instant::now() + request.duration;
let id = TimerId(self.current_id);
self.current_id += 1;
self.queue.push(ScheduledEvent {
id,
request,
for_time,
});
id
}
/// Cancel a timer with the given [`TimerId`]. If a timer with that id is not
/// currently waiting to fire, do nothing.
pub fn cancel_timer(&mut self, id: TimerId) {
self.queue.retain(|event| event.id != id);
}
/// Get a [`Receiver<Instant>`] that receives a message after waiting for the next timer
/// to fire. If there are no timers, the channel will *never* send a message.
pub fn wait_channel(&self) -> Receiver<Instant> {
self.queue
.peek()
.map(|event| {
let now = Instant::now();
if event.for_time < now {
after(Duration::ZERO)
} else {
after(event.for_time - now)
}
})
.unwrap_or_else(never)
}
/// Dispatch any timer events from this [`TimerScheduler`]'s `queue` when `now` is
/// past the due time of the event.
pub fn dispatch_completed_timers(&mut self) {
let now = Instant::now();
loop {
match self.queue.peek() {
// Dispatch the event if its due time is past.
Some(event) if event.for_time <= now => {},
// Otherwise, we're done dispatching events.
_ => break,
}
// Remove the event from the priority queue (Note this only executes when the
// first event has been dispatched
self.queue
.pop()
.expect("Expected request")
.request
.dispatch();
}
}
}