compositor: Tick animations for an entire WebView at once (#36662)

Previously, when processing animations, the compositor would sent a tick
message to each pipeline. This is an issue because now the
`ScriptThread` always processes rendering updates for all `Document`s in
order to ensure properly ordering. This change makes it so that tick
messages are sent for an entire WebView. This means that each
`ScriptThread` will always receive a single tick for every time that
animations are processed, no matter how many frames are animating. This
is the first step toward a refresh driver.

In addition, we discard the idea of ticking animation only for
animations and or only for request animation frame callbacks. The
`ScriptThread` can no longer make this distinction due to the
specification and the compositor shouldn't either.

This should not really change observable behavior, but should make Servo
more efficient when more than a single frame in a `ScriptThread` is
animting at once.

Testing: This is covered by existing WPT tests as it mainly just improve
animation efficiency in a particular case.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-04-24 21:03:14 +02:00 committed by GitHub
parent 3793936f05
commit cbc363bedd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 134 additions and 172 deletions

View file

@ -21,12 +21,12 @@ use compositing_traits::rendering_context::RenderingContext;
use compositing_traits::{
CompositionPipeline, CompositorMsg, ImageUpdate, SendableFrameTree, WebViewTrait,
};
use constellation_traits::{AnimationTickType, EmbedderToConstellationMessage, PaintMetricEvent};
use constellation_traits::{EmbedderToConstellationMessage, PaintMetricEvent};
use crossbeam_channel::{Receiver, Sender};
use dpi::PhysicalSize;
use embedder_traits::{
AnimationState, CompositorHitTestResult, Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent,
ShutdownState, TouchEventType, UntrustedNodeAddress, ViewportDetails,
CompositorHitTestResult, Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent, ShutdownState,
TouchEventType, UntrustedNodeAddress, ViewportDetails,
};
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D};
use fnv::FnvHashMap;
@ -197,9 +197,6 @@ pub(crate) struct PipelineDetails {
/// The pipeline associated with this PipelineDetails object.
pub pipeline: Option<CompositionPipeline>,
/// The [`PipelineId`] of this pipeline.
pub id: PipelineId,
/// The id of the parent pipeline, if any.
pub parent_pipeline_id: Option<PipelineId>,
@ -243,32 +240,12 @@ impl PipelineDetails {
pub(crate) fn animating(&self) -> bool {
!self.throttled && (self.animation_callbacks_running || self.animations_running)
}
pub(crate) fn tick_animations(&self, compositor: &IOCompositor) {
if !self.animating() {
return;
}
let mut tick_type = AnimationTickType::empty();
if self.animations_running {
tick_type.insert(AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS);
}
if self.animation_callbacks_running {
tick_type.insert(AnimationTickType::REQUEST_ANIMATION_FRAME);
}
let msg = EmbedderToConstellationMessage::TickAnimation(self.id, tick_type);
if let Err(e) = compositor.global.borrow().constellation_sender.send(msg) {
warn!("Sending tick to constellation failed ({:?}).", e);
}
}
}
impl PipelineDetails {
pub(crate) fn new(id: PipelineId) -> PipelineDetails {
pub(crate) fn new() -> PipelineDetails {
PipelineDetails {
pipeline: None,
id,
parent_pipeline_id: None,
most_recent_display_list_epoch: None,
animations_running: false,
@ -543,22 +520,14 @@ impl IOCompositor {
pipeline_id,
animation_state,
) => {
let mut throttled = true;
if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) {
throttled = webview_renderer
.change_running_animations_state(pipeline_id, animation_state);
}
// These operations should eventually happen per-WebView, but they are global now as rendering
// is still global to all WebViews.
if !throttled && animation_state == AnimationState::AnimationsPresent {
self.set_needs_repaint(RepaintReason::ChangedAnimationState);
}
if !throttled && animation_state == AnimationState::AnimationCallbacksPresent {
// We need to fetch the WebView again in order to avoid a double borrow.
if let Some(webview_renderer) = self.webview_renderers.get(webview_id) {
webview_renderer.tick_animations_for_pipeline(pipeline_id, self);
if webview_renderer
.change_pipeline_running_animations_state(pipeline_id, animation_state) &&
webview_renderer.animating()
{
// These operations should eventually happen per-WebView, but they are
// global now as rendering is still global to all WebViews.
self.process_animations(true);
}
}
},
@ -605,8 +574,13 @@ impl IOCompositor {
CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => {
if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) {
webview_renderer.set_throttled(pipeline_id, throttled);
self.process_animations(true);
if webview_renderer.set_throttled(pipeline_id, throttled) &&
webview_renderer.animating()
{
// These operations should eventually happen per-WebView, but they are
// global now as rendering is still global to all WebViews.
self.process_animations(true);
}
}
},
@ -1283,8 +1257,23 @@ impl IOCompositor {
}
self.last_animation_tick = Instant::now();
for webview_renderer in self.webview_renderers.iter() {
webview_renderer.tick_all_animations(self);
let animating_webviews: Vec<_> = self
.webview_renderers
.iter()
.filter_map(|webview_renderer| {
if webview_renderer.animating() {
Some(webview_renderer.id)
} else {
None
}
})
.collect();
if !animating_webviews.is_empty() {
if let Err(error) = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::TickAnimation(animating_webviews),
) {
warn!("Sending tick to constellation failed ({error:?}).");
}
}
}

View file

@ -86,6 +86,9 @@ pub(crate) struct WebViewRenderer {
/// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled
/// by the embedding layer.
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
/// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with
/// active animations or animation frame callbacks.
animating: bool,
}
impl Drop for WebViewRenderer {
@ -119,6 +122,7 @@ impl WebViewRenderer {
min_viewport_zoom: Some(PinchZoomFactor::new(1.0)),
max_viewport_zoom: None,
hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
animating: false,
}
}
@ -138,6 +142,10 @@ impl WebViewRenderer {
self.pipelines.keys()
}
pub(crate) fn animating(&self) -> bool {
self.animating
}
/// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed.
pub(crate) fn ensure_pipeline_details(
&mut self,
@ -148,14 +156,10 @@ impl WebViewRenderer {
.borrow_mut()
.pipeline_to_webview_map
.insert(pipeline_id, self.id);
PipelineDetails::new(pipeline_id)
PipelineDetails::new()
})
}
pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) {
self.ensure_pipeline_details(pipeline_id).throttled = throttled;
}
pub(crate) fn remove_pipeline(&mut self, pipeline_id: PipelineId) {
self.global
.borrow_mut()
@ -245,51 +249,45 @@ impl WebViewRenderer {
})
}
/// Sets or unsets the animations-running flag for the given pipeline. Returns true if
/// the pipeline is throttled.
pub(crate) fn change_running_animations_state(
/// Sets or unsets the animations-running flag for the given pipeline. Returns
/// true if the [`WebViewRenderer`]'s overall animating state changed.
pub(crate) fn change_pipeline_running_animations_state(
&mut self,
pipeline_id: PipelineId,
animation_state: AnimationState,
) -> bool {
let throttled = {
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
match animation_state {
AnimationState::AnimationsPresent => {
pipeline_details.animations_running = true;
},
AnimationState::AnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = true;
},
AnimationState::NoAnimationsPresent => {
pipeline_details.animations_running = false;
},
AnimationState::NoAnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = false;
},
}
pipeline_details.throttled
};
let pipeline_details = self.ensure_pipeline_details(pipeline_id);
match animation_state {
AnimationState::AnimationsPresent => {
pipeline_details.animations_running = true;
},
AnimationState::AnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = true;
},
AnimationState::NoAnimationsPresent => {
pipeline_details.animations_running = false;
},
AnimationState::NoAnimationCallbacksPresent => {
pipeline_details.animation_callbacks_running = false;
},
}
self.update_animation_state()
}
/// Sets or unsets the throttled flag for the given pipeline. Returns
/// true if the [`WebViewRenderer`]'s overall animating state changed.
pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) -> bool {
self.ensure_pipeline_details(pipeline_id).throttled = throttled;
// Throttling a pipeline can cause it to be taken into the "not-animating" state.
self.update_animation_state()
}
pub(crate) fn update_animation_state(&mut self) -> bool {
let animating = self.pipelines.values().any(PipelineDetails::animating);
self.webview.set_animating(animating);
throttled
}
pub(crate) fn tick_all_animations(&self, compositor: &IOCompositor) {
for pipeline_details in self.pipelines.values() {
pipeline_details.tick_animations(compositor)
}
}
pub(crate) fn tick_animations_for_pipeline(
&self,
pipeline_id: PipelineId,
compositor: &IOCompositor,
) {
if let Some(pipeline_details) = self.pipelines.get(&pipeline_id) {
pipeline_details.tick_animations(compositor);
}
let old_animating = std::mem::replace(&mut self.animating, animating);
self.webview.set_animating(self.animating);
old_animating != self.animating
}
/// On a Window refresh tick (e.g. vsync)

View file

@ -112,13 +112,12 @@ use compositing_traits::{
CompositorMsg, CompositorProxy, SendableFrameTree, WebrenderExternalImageRegistry,
};
use constellation_traits::{
AnimationTickType, AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse,
BroadcastMsg, DocumentState, EmbedderToConstellationMessage, IFrameLoadInfo,
IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry,
MessagePortMsg, NavigationHistoryBehavior, PaintMetricEvent, PortMessageTask, SWManagerMsg,
SWManagerSenders, ScriptToConstellationChan, ScriptToConstellationMessage, ScrollState,
ServiceWorkerManagerFactory, ServiceWorkerMsg, StructuredSerializedData, TraversalDirection,
WindowSizeType,
AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse, BroadcastMsg, DocumentState,
EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState,
IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior,
PaintMetricEvent, PortMessageTask, SWManagerMsg, SWManagerSenders, ScriptToConstellationChan,
ScriptToConstellationMessage, ScrollState, ServiceWorkerManagerFactory, ServiceWorkerMsg,
StructuredSerializedData, TraversalDirection, WindowSizeType,
};
use crossbeam_channel::{Receiver, Select, Sender, unbounded};
use devtools_traits::{
@ -1398,8 +1397,8 @@ where
EmbedderToConstellationMessage::ThemeChange(theme) => {
self.handle_theme_change(theme);
},
EmbedderToConstellationMessage::TickAnimation(pipeline_id, tick_type) => {
self.handle_tick_animation(pipeline_id, tick_type)
EmbedderToConstellationMessage::TickAnimation(webview_ids) => {
self.handle_tick_animation(webview_ids)
},
EmbedderToConstellationMessage::WebDriverCommand(command) => {
self.handle_webdriver_msg(command);
@ -3528,15 +3527,24 @@ where
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)]
fn handle_tick_animation(&mut self, pipeline_id: PipelineId, tick_type: AnimationTickType) {
let pipeline = match self.pipelines.get(&pipeline_id) {
Some(pipeline) => pipeline,
None => return warn!("{}: Got script tick after closure", pipeline_id),
};
fn handle_tick_animation(&mut self, webview_ids: Vec<WebViewId>) {
let mut animating_event_loops = HashSet::new();
let message = ScriptThreadMessage::TickAllAnimations(pipeline_id, tick_type);
if let Err(e) = pipeline.event_loop.send(message) {
self.handle_send_error(pipeline_id, e);
for webview_id in webview_ids.iter() {
for browsing_context in self.fully_active_browsing_contexts_iter(*webview_id) {
let Some(pipeline) = self.pipelines.get(&browsing_context.pipeline_id) else {
continue;
};
animating_event_loops.insert(pipeline.event_loop.clone());
}
}
for event_loop in animating_event_loops {
// No error handling here. It's unclear what to do when this fails as the error isn't associated
// with a particular pipeline. In addition, the danger of not progressing animations is pretty
// low, so it's probably safe to ignore this error and handle the crashed ScriptThread on
// some other message.
let _ = event_loop.send(ScriptThreadMessage::TickAllAnimations(webview_ids.clone()));
}
}

View file

@ -6,17 +6,36 @@
//! view of a script thread. When an `EventLoop` is dropped, an `ExitScriptThread`
//! message is sent to the script thread, asking it to shut down.
use std::hash::Hash;
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use ipc_channel::Error;
use ipc_channel::ipc::IpcSender;
use script_traits::ScriptThreadMessage;
static CURRENT_EVENT_LOOP_ID: AtomicUsize = AtomicUsize::new(0);
/// <https://html.spec.whatwg.org/multipage/#event-loop>
pub struct EventLoop {
script_chan: IpcSender<ScriptThreadMessage>,
dont_send_or_sync: PhantomData<Rc<()>>,
id: usize,
}
impl PartialEq for EventLoop {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for EventLoop {}
impl Hash for EventLoop {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Drop for EventLoop {
@ -28,9 +47,11 @@ impl Drop for EventLoop {
impl EventLoop {
/// Create a new event loop from the channel to its script thread.
pub fn new(script_chan: IpcSender<ScriptThreadMessage>) -> Rc<EventLoop> {
let id = CURRENT_EVENT_LOOP_ID.fetch_add(1, Ordering::Relaxed);
Rc::new(EventLoop {
script_chan,
dont_send_or_sync: PhantomData,
id,
})
}

View file

@ -21,9 +21,7 @@ use base::id::WebViewId;
use canvas_traits::canvas::CanvasId;
use canvas_traits::webgl::{self, WebGLContextId, WebGLMsg};
use chrono::Local;
use constellation_traits::{
AnimationTickType, NavigationHistoryBehavior, ScriptToConstellationMessage,
};
use constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage};
use content_security_policy::{self as csp, CspList, PolicyDisposition};
use cookie::Cookie;
use cssparser::match_ignore_ascii_case;
@ -516,10 +514,6 @@ pub(crate) struct Document {
pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>,
/// The index of the last mouse move event in the pending compositor events queue.
mouse_move_event_index: DomRefCell<Option<usize>>,
/// Pending animation ticks, to be handled at the next rendering opportunity.
#[no_trace]
#[ignore_malloc_size_of = "AnimationTickType contains data from an outside crate"]
pending_animation_ticks: DomRefCell<AnimationTickType>,
/// <https://drafts.csswg.org/resize-observer/#dom-document-resizeobservers-slot>
///
/// Note: we are storing, but never removing, resize observers.
@ -2397,10 +2391,6 @@ impl Document {
pub(crate) fn run_the_animation_frame_callbacks(&self, can_gc: CanGc) {
let _realm = enter_realm(self);
self.pending_animation_ticks
.borrow_mut()
.remove(AnimationTickType::REQUEST_ANIMATION_FRAME);
self.running_animation_callbacks.set(true);
let was_faking_animation_frames = self.is_faking_animation_frames();
let timing = self.global().performance().Now();
@ -3916,7 +3906,6 @@ impl Document {
image_animation_manager: DomRefCell::new(ImageAnimationManager::new()),
dirty_root: Default::default(),
declarative_refresh: Default::default(),
pending_animation_ticks: Default::default(),
pending_input_events: Default::default(),
mouse_move_event_index: Default::default(),
resize_observers: Default::default(),
@ -4689,18 +4678,6 @@ impl Document {
.collect()
}
/// Note a pending animation tick, to be processed at the next `update_the_rendering` task.
pub(crate) fn note_pending_animation_tick(&self, tick_type: AnimationTickType) {
self.pending_animation_ticks.borrow_mut().extend(tick_type);
}
/// Whether this document has received an animation tick for rafs.
pub(crate) fn has_received_raf_tick(&self) -> bool {
self.pending_animation_ticks
.borrow()
.contains(AnimationTickType::REQUEST_ANIMATION_FRAME)
}
pub(crate) fn advance_animation_timeline_for_testing(&self, delta: f64) {
self.animation_timeline.borrow_mut().advance_specific(delta);
let current_timeline_value = self.current_animation_timeline_value();
@ -6437,10 +6414,7 @@ impl FakeRequestAnimationFrameCallback {
pub(crate) fn invoke(self, can_gc: CanGc) {
// TODO: Once there is a more generic mechanism to trigger `update_the_rendering` when
// not driven by the compositor, it should be used here.
self.document
.root()
.note_pending_animation_tick(AnimationTickType::REQUEST_ANIMATION_FRAME);
with_script_thread(|script_thread| script_thread.update_the_rendering(false, can_gc))
with_script_thread(|script_thread| script_thread.update_the_rendering(true, can_gc))
}
}

View file

@ -73,7 +73,7 @@ impl MixedMessage {
ScriptThreadMessage::RemoveHistoryStates(id, ..) => Some(*id),
ScriptThreadMessage::FocusIFrame(id, ..) => Some(*id),
ScriptThreadMessage::WebDriverScriptCommand(id, ..) => Some(*id),
ScriptThreadMessage::TickAllAnimations(id, ..) => Some(*id),
ScriptThreadMessage::TickAllAnimations(..) => None,
ScriptThreadMessage::WebFontLoaded(id, ..) => Some(*id),
ScriptThreadMessage::DispatchIFrameLoadEvent {
target: _,

View file

@ -1147,14 +1147,6 @@ impl ScriptThread {
return;
}
// Run rafs for all pipeline, if a raf tick was received for any.
// This ensures relative ordering of rafs between parent doc and iframes.
let should_run_rafs = self
.documents
.borrow()
.iter()
.any(|(_, doc)| doc.is_fully_active() && doc.has_received_raf_tick());
let any_animations_running = self.documents.borrow().iter().any(|(_, document)| {
document.is_fully_active() && document.animations().running_animation_count() != 0
});
@ -1242,7 +1234,7 @@ impl ScriptThread {
// > 14. For each doc of docs, run the animation frame callbacks for doc, passing
// > in the relative high resolution time given frameTimestamp and doc's
// > relevant global object as the timestamp.
if should_run_rafs {
if requested_by_compositor {
document.run_the_animation_frame_callbacks(can_gc);
}
@ -1421,18 +1413,9 @@ impl ScriptThread {
self.handle_viewport(id, rect);
}),
MixedMessage::FromConstellation(ScriptThreadMessage::TickAllAnimations(
pipeline_id,
tick_type,
_webviews,
)) => {
if let Some(document) = self.documents.borrow().find_document(pipeline_id) {
document.note_pending_animation_tick(tick_type);
compositor_requested_update_the_rendering = true;
} else {
warn!(
"Trying to note pending animation tick for closed pipeline {}.",
pipeline_id
)
}
compositor_requested_update_the_rendering = true;
},
MixedMessage::FromConstellation(ScriptThreadMessage::SendInputEvent(id, event)) => {
self.handle_input_event(id, event)

View file

@ -18,7 +18,6 @@ use std::time::Duration;
use base::Epoch;
use base::cross_process_instant::CrossProcessInstant;
use base::id::{MessagePortId, PipelineId, WebViewId};
use bitflags::bitflags;
use embedder_traits::{
CompositorHitTestResult, Cursor, InputEvent, MediaSessionActionType, Theme, ViewportDetails,
WebDriverCommandMsg,
@ -57,8 +56,9 @@ pub enum EmbedderToConstellationMessage {
ChangeViewportDetails(WebViewId, ViewportDetails, WindowSizeType),
/// Inform the constellation of a theme change.
ThemeChange(Theme),
/// Requests that the constellation instruct layout to begin a new tick of the animation.
TickAnimation(PipelineId, AnimationTickType),
/// Requests that the constellation instruct script/layout to try to layout again and tick
/// animations.
TickAnimation(Vec<WebViewId>),
/// Dispatch a webdriver command
WebDriverCommand(WebDriverCommandMsg),
/// Reload a top-level browsing context.
@ -130,17 +130,6 @@ pub enum WindowSizeType {
Resize,
}
bitflags! {
#[derive(Debug, Default, Deserialize, Serialize)]
/// Specifies if rAF should be triggered and/or CSS Animations and Transitions.
pub struct AnimationTickType: u8 {
/// Trigger a call to requestAnimationFrame.
const REQUEST_ANIMATION_FRAME = 0b001;
/// Trigger restyles for CSS Animations and Transitions.
const CSS_ANIMATIONS_AND_TRANSITIONS = 0b010;
}
}
/// The scroll state of a stacking context.
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub struct ScrollState {

View file

@ -20,7 +20,7 @@ use bluetooth_traits::BluetoothRequest;
use canvas_traits::webgl::WebGLPipeline;
use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{
AnimationTickType, LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, ScrollState,
LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, ScrollState,
StructuredSerializedData, WindowSizeType,
};
use crossbeam_channel::{RecvTimeoutError, Sender};
@ -195,7 +195,7 @@ pub enum ScriptThreadMessage {
/// Passes a webdriver command to the script thread for execution
WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
/// Notifies script thread that all animations are done
TickAllAnimations(PipelineId, AnimationTickType),
TickAllAnimations(Vec<WebViewId>),
/// Notifies the script thread that a new Web font has been loaded, and thus the page should be
/// reflowed.
WebFontLoaded(PipelineId, bool /* success */),