Use a restyle for animation ticks

This change corrects synchronization issues with animations, by
reworking the animation processing model to do a quick restyle and
incremental layout when ticking animations.

While this change adds overhead to animation ticks, the idea is that
this will be the fallback when synchronous behavior is required to
fulfill specification requirements. In the optimistic case, many
animations could be updated and applied off-the-main-thread and then
resynchronized when style information is queried by script.

Fixes #13865.
This commit is contained in:
Martin Robinson 2020-04-30 15:38:56 +02:00
parent 5e44327325
commit b585ce5b1f
39 changed files with 1285 additions and 1662 deletions

View file

@ -60,8 +60,8 @@ use mime::{self, Mime};
use msg::constellation_msg::PipelineId;
use net_traits::image::base::{Image, ImageMetadata};
use net_traits::image_cache::{
CanRequestImages, CorsStatus, ImageCache, ImageCacheResult, ImageOrMetadataAvailable,
ImageResponse, PendingImageId, PendingImageResponse, UsePlaceholder,
CorsStatus, ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponse,
PendingImageId, PendingImageResponse, UsePlaceholder,
};
use net_traits::request::{CorsSettings, Destination, Initiator, RequestBuilder};
use net_traits::{FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError};
@ -326,7 +326,6 @@ impl HTMLImageElement {
cors_setting_for_element(self.upcast()),
sender,
UsePlaceholder::Yes,
CanRequestImages::Yes,
);
match cache_result {
@ -1107,7 +1106,6 @@ impl HTMLImageElement {
cors_setting_for_element(self.upcast()),
sender,
UsePlaceholder::No,
CanRequestImages::Yes,
);
match cache_result {

View file

@ -27,8 +27,8 @@ use html5ever::{LocalName, Prefix};
use ipc_channel::ipc;
use ipc_channel::router::ROUTER;
use net_traits::image_cache::{
CanRequestImages, ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponse,
PendingImageId, UsePlaceholder,
ImageCache, ImageCacheResult, ImageOrMetadataAvailable, ImageResponse, PendingImageId,
UsePlaceholder,
};
use net_traits::request::{CredentialsMode, Destination, RequestBuilder};
use net_traits::{
@ -163,7 +163,6 @@ impl HTMLVideoElement {
None,
sender,
UsePlaceholder::No,
CanRequestImages::Yes,
);
match cache_result {

View file

@ -1054,8 +1054,8 @@ impl TestBindingMethods for TestBinding {
}
}
fn AdvanceClock(&self, ms: i32, tick: bool) {
self.global().as_window().advance_animation_clock(ms, tick);
fn AdvanceClock(&self, ms: i32) {
self.global().as_window().advance_animation_clock(ms);
}
fn Panic(&self) {

View file

@ -517,7 +517,7 @@ interface TestBinding {
[Pref="dom.testbinding.prefcontrolled.enabled"]
const unsigned short prefControlledConstDisabled = 0;
[Pref="layout.animations.test.enabled"]
void advanceClock(long millis, optional boolean forceLayoutTick = true);
void advanceClock(long millis);
[Pref="dom.testbinding.prefcontrolled2.enabled"]
readonly attribute boolean prefControlledAttributeEnabled;

View file

@ -173,6 +173,8 @@ pub enum ReflowReason {
IFrameLoadEvent,
MissingExplicitReflow,
ElementStateChanged,
TickAnimations,
AdvanceClock(i32),
}
#[dom_struct]
@ -1544,16 +1546,16 @@ impl Window {
)
}
/// Advances the layout animation clock by `delta` milliseconds, and then
/// forces a reflow if `tick` is true.
pub fn advance_animation_clock(&self, delta: i32, tick: bool) {
self.layout_chan
.send(Msg::AdvanceClockMs(
delta,
tick,
self.origin().immutable().clone(),
))
.unwrap();
/// Prepares to tick animations and then does a reflow which also advances the
/// layout animation clock.
pub fn advance_animation_clock(&self, delta: i32) {
let pipeline_id = self.upcast::<GlobalScope>().pipeline_id();
ScriptThread::restyle_animating_nodes_for_advancing_clock(&pipeline_id);
self.force_reflow(
ReflowGoal::TickAnimations,
ReflowReason::AdvanceClock(delta),
None,
);
}
/// Reflows the page unconditionally if possible and not suppressed. This
@ -1626,11 +1628,15 @@ impl Window {
// If this reflow is for display, ensure webgl canvases are composited with
// up-to-date contents.
match reflow_goal {
ReflowGoal::Full => document.flush_dirty_canvases(),
ReflowGoal::TickAnimations | ReflowGoal::LayoutQuery(..) => {},
if for_display {
document.flush_dirty_canvases();
}
let advance_clock_delta = match reason {
ReflowReason::AdvanceClock(delta) => Some(delta),
_ => None,
};
// Send new document and relevant styles to layout.
let needs_display = reflow_goal.needs_display();
let reflow = ScriptReflow {
@ -1645,6 +1651,7 @@ impl Window {
script_join_chan: join_chan,
dom_count: document.dom_count(),
pending_restyles: document.drain_pending_restyles(),
advance_clock_delta,
};
self.layout_chan
@ -2487,6 +2494,8 @@ fn debug_reflow_events(id: PipelineId, reflow_goal: &ReflowGoal, reason: &Reflow
ReflowReason::IFrameLoadEvent => "\tIFrameLoadEvent",
ReflowReason::MissingExplicitReflow => "\tMissingExplicitReflow",
ReflowReason::ElementStateChanged => "\tElementStateChanged",
ReflowReason::TickAnimations => "\tTickAnimations",
ReflowReason::AdvanceClock(..) => "\tAdvanceClock",
});
println!("{}", debug_msg);

View file

@ -136,12 +136,12 @@ use script_traits::CompositorEvent::{
WheelEvent,
};
use script_traits::{
CompositorEvent, ConstellationControlMsg, DiscardBrowsingContext, DocumentActivity,
EventResult, HistoryEntryReplacement, InitialScriptState, JsEvalResult, LayoutMsg, LoadData,
LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo, Painter,
ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory, ScriptToConstellationChan,
StructuredSerializedData, TimerSchedulerMsg, TouchEventType, TouchId,
TransitionOrAnimationEventType, UntrustedNodeAddress, UpdatePipelineIdReason,
AnimationTickType, CompositorEvent, ConstellationControlMsg, DiscardBrowsingContext,
DocumentActivity, EventResult, HistoryEntryReplacement, InitialScriptState, JsEvalResult,
LayoutMsg, LoadData, LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType,
NewLayoutInfo, Painter, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory,
ScriptToConstellationChan, StructuredSerializedData, TimerSchedulerMsg, TouchEventType,
TouchId, TransitionOrAnimationEventType, UntrustedNodeAddress, UpdatePipelineIdReason,
WebrenderIpcSender, WheelDelta, WindowSizeData, WindowSizeType,
};
use servo_atoms::Atom;
@ -1497,7 +1497,7 @@ impl ScriptThread {
self.handle_set_scroll_state(id, &scroll_state);
})
},
FromConstellation(ConstellationControlMsg::TickAllAnimations(pipeline_id)) => {
FromConstellation(ConstellationControlMsg::TickAllAnimations(pipeline_id, _)) => {
// step 7.8
if !animation_ticks.contains(&pipeline_id) {
animation_ticks.insert(pipeline_id);
@ -1703,7 +1703,7 @@ impl ScriptThread {
RemoveHistoryStates(id, ..) => Some(id),
FocusIFrame(id, ..) => Some(id),
WebDriverScriptCommand(id, ..) => Some(id),
TickAllAnimations(id) => Some(id),
TickAllAnimations(id, ..) => Some(id),
TransitionOrAnimationEvent { .. } => None,
WebFontLoaded(id) => Some(id),
DispatchIFrameLoadEvent {
@ -1902,8 +1902,8 @@ impl ScriptThread {
ConstellationControlMsg::WebDriverScriptCommand(pipeline_id, msg) => {
self.handle_webdriver_msg(pipeline_id, msg)
},
ConstellationControlMsg::TickAllAnimations(pipeline_id) => {
self.handle_tick_all_animations(pipeline_id)
ConstellationControlMsg::TickAllAnimations(pipeline_id, tick_type) => {
self.handle_tick_all_animations(pipeline_id, tick_type)
},
ConstellationControlMsg::TransitionOrAnimationEvent {
pipeline_id,
@ -2914,13 +2914,45 @@ impl ScriptThread {
debug!("Exited script thread.");
}
fn restyle_animating_nodes(&self, id: &PipelineId) -> bool {
match self.animating_nodes.borrow().get(id) {
Some(nodes) => {
for node in nodes.iter() {
node.dirty(NodeDamage::NodeStyleDamaged);
}
true
},
None => false,
}
}
pub fn restyle_animating_nodes_for_advancing_clock(id: &PipelineId) {
SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = unsafe { &*root.get().unwrap() };
script_thread.restyle_animating_nodes(id);
});
}
/// Handles when layout thread finishes all animation in one tick
fn handle_tick_all_animations(&self, id: PipelineId) {
fn handle_tick_all_animations(&self, id: PipelineId, tick_type: AnimationTickType) {
let document = match self.documents.borrow().find_document(id) {
Some(document) => document,
None => return warn!("Message sent to closed pipeline {}.", id),
};
document.run_the_animation_frame_callbacks();
if tick_type.contains(AnimationTickType::REQUEST_ANIMATION_FRAME) {
document.run_the_animation_frame_callbacks();
}
if tick_type.contains(AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS) {
if !self.restyle_animating_nodes(&id) {
return;
}
document.window().force_reflow(
ReflowGoal::TickAnimations,
ReflowReason::TickAnimations,
None,
);
}
}
/// Handles firing of transition-related events.
@ -2969,11 +3001,6 @@ impl ScriptThread {
}
}
// Not quite the right thing - see #13865.
if event_type.finalizes_transition_or_animation() {
node.dirty(NodeDamage::NodeStyleDamaged);
}
let event_atom = match event_type {
TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"),
TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"),