diff --git a/components/layout/construct.rs b/components/layout/construct.rs index afdf903c500..1125261dce4 100644 --- a/components/layout/construct.rs +++ b/components/layout/construct.rs @@ -84,11 +84,7 @@ pub enum ConstructionResult { } impl ConstructionResult { - pub fn swap_out(&mut self) -> ConstructionResult { - if opts::get().nonincremental_layout { - return mem::replace(self, ConstructionResult::None) - } - + pub fn get(&mut self) -> ConstructionResult { // FIXME(pcwalton): Stop doing this with inline fragments. Cloning fragments is very // inefficient! (*self).clone() @@ -485,7 +481,7 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> inline_fragment_accumulator: &mut InlineFragmentsAccumulator, abs_descendants: &mut AbsoluteDescendants, legalizer: &mut Legalizer) { - match kid.swap_out_construction_result() { + match kid.get_construction_result() { ConstructionResult::None => {} ConstructionResult::Flow(kid_flow, kid_abs_descendants) => { // If kid_flow is TableCaptionFlow, kid_flow should be added under @@ -784,7 +780,7 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> if kid.get_pseudo_element_type() != PseudoElementType::Normal { self.process(&kid); } - match kid.swap_out_construction_result() { + match kid.get_construction_result() { ConstructionResult::None => {} ConstructionResult::Flow(flow, kid_abs_descendants) => { if !flow::base(&*flow).flags.contains(IS_ABSOLUTELY_POSITIONED) { @@ -1035,7 +1031,7 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> side: caption_side::T) { // Only flows that are table captions are matched here. for kid in node.children() { - match kid.swap_out_construction_result() { + match kid.get_construction_result() { ConstructionResult::Flow(kid_flow, _) => { if kid_flow.is_table_caption() && kid_flow.as_block() @@ -1304,7 +1300,7 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> // CSS 2.1 § 17.2.1. Treat all non-column child fragments of `table-column-group` // as `display: none`. if let ConstructionResult::ConstructionItem(ConstructionItem::TableColumnFragment(fragment)) = - kid.swap_out_construction_result() { + kid.get_construction_result() { col_fragments.push(fragment) } } @@ -1641,9 +1637,8 @@ trait NodeUtils { /// Sets the construction result of a flow. fn set_flow_construction_result(self, result: ConstructionResult); - /// Replaces the flow construction result in a node with `ConstructionResult::None` and returns - /// the old value. - fn swap_out_construction_result(self) -> ConstructionResult; + /// Returns the construction result for this node. + fn get_construction_result(self) -> ConstructionResult; } impl NodeUtils for ConcreteThreadSafeLayoutNode @@ -1686,9 +1681,9 @@ impl NodeUtils for ConcreteThreadSafeLayoutNode } #[inline(always)] - fn swap_out_construction_result(self) -> ConstructionResult { + fn get_construction_result(self) -> ConstructionResult { let mut layout_data = self.mutate_layout_data().unwrap(); - self.construction_result_mut(&mut *layout_data).swap_out() + self.construction_result_mut(&mut *layout_data).get() } } diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index 1609ea2ae17..7e8c4e1e3bf 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -774,7 +774,7 @@ impl LayoutThread { Some(x) => x, None => return None, }; - let result = data.flow_construction_result.swap_out(); + let result = data.flow_construction_result.get(); let mut flow = match result { ConstructionResult::Flow(mut flow, abs_descendants) => { diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 991d93f7fcd..e71961882e4 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -111,7 +111,7 @@ use script_runtime::{CommonScriptMsg, ScriptThreadEventCategory}; use script_thread::{MainThreadScriptMsg, Runnable}; use script_traits::{AnimationState, CompositorEvent, DocumentActivity}; use script_traits::{MouseButton, MouseEventType, MozBrowserEvent}; -use script_traits::{ScriptMsg as ConstellationMsg, TouchpadPressurePhase}; +use script_traits::{MsDuration, ScriptMsg as ConstellationMsg, TouchpadPressurePhase}; use script_traits::{TouchEventType, TouchId}; use script_traits::UntrustedNodeAddress; use servo_atoms::Atom; @@ -136,9 +136,19 @@ use style::str::{HTML_SPACE_CHARACTERS, split_html_space_chars, str_join}; use style::stylesheets::Stylesheet; use task_source::TaskSource; use time; +use timers::OneshotTimerCallback; use url::Host; use url::percent_encoding::percent_decode; +/// The number of times we are allowed to see spurious `requestAnimationFrame()` calls before +/// falling back to fake ones. +/// +/// A spurious `requestAnimationFrame()` call is defined as one that does not change the DOM. +const SPURIOUS_ANIMATION_FRAME_THRESHOLD: u8 = 5; + +/// The amount of time between fake `requestAnimationFrame()`s. +const FAKE_REQUEST_ANIMATION_FRAME_DELAY: u64 = 16; + pub enum TouchEventResult { Processed(bool), Forwarded, @@ -290,6 +300,11 @@ pub struct Document { last_click_info: DOMRefCell)>>, /// https://html.spec.whatwg.org/multipage/#ignore-destructive-writes-counter ignore_destructive_writes_counter: Cell, + /// The number of spurious `requestAnimationFrame()` requests we've received. + /// + /// A rAF request is considered spurious if nothing was actually reflowed. + spurious_animation_frames: Cell, + /// Track the total number of elements in this DOM's tree. /// This is sent to the layout thread every time a reflow is done; /// layout uses this to determine if the gains from parallel layout will be worth the overhead. @@ -1498,11 +1513,20 @@ impl Document { // // TODO: Should tick animation only when document is visible if !self.running_animation_callbacks.get() { - let global_scope = self.window.upcast::(); - let event = ConstellationMsg::ChangeRunningAnimationsState( - global_scope.pipeline_id(), - AnimationState::AnimationCallbacksPresent); - global_scope.constellation_chan().send(event).unwrap(); + if !self.is_faking_animation_frames() { + let global_scope = self.window.upcast::(); + let event = ConstellationMsg::ChangeRunningAnimationsState( + global_scope.pipeline_id(), + AnimationState::AnimationCallbacksPresent); + global_scope.constellation_chan().send(event).unwrap(); + } else { + let callback = FakeRequestAnimationFrameCallback { + document: Trusted::new(self), + }; + self.global() + .schedule_callback(OneshotTimerCallback::FakeRequestAnimationFrame(callback), + MsDuration::new(FAKE_REQUEST_ANIMATION_FRAME_DELAY)); + } } ident @@ -1524,6 +1548,7 @@ impl Document { &mut *self.animation_frame_list.borrow_mut()); self.running_animation_callbacks.set(true); + let was_faking_animation_frames = self.is_faking_animation_frames(); let timing = self.window.Performance().Now(); for (_, callback) in animation_frame_list.drain(..) { @@ -1532,24 +1557,40 @@ impl Document { } } + self.running_animation_callbacks.set(false); + + let spurious = !self.window.reflow(ReflowGoal::ForDisplay, + ReflowQueryType::NoQuery, + ReflowReason::RequestAnimationFrame); + // Only send the animation change state message after running any callbacks. // This means that if the animation callback adds a new callback for // the next frame (which is the common case), we won't send a NoAnimationCallbacksPresent // message quickly followed by an AnimationCallbacksPresent message. - if self.animation_frame_list.borrow().is_empty() { + // + // If this frame was spurious and we've seen too many spurious frames in a row, tell the + // constellation to stop giving us video refresh callbacks, to save energy. (A spurious + // animation frame is one in which the callback did not mutate the DOM—that is, an + // animation frame that wasn't actually used for animation.) + if self.animation_frame_list.borrow().is_empty() || + (!was_faking_animation_frames && self.is_faking_animation_frames()) { mem::swap(&mut *self.animation_frame_list.borrow_mut(), &mut *animation_frame_list); let global_scope = self.window.upcast::(); - let event = ConstellationMsg::ChangeRunningAnimationsState(global_scope.pipeline_id(), - AnimationState::NoAnimationCallbacksPresent); + let event = ConstellationMsg::ChangeRunningAnimationsState( + global_scope.pipeline_id(), + AnimationState::NoAnimationCallbacksPresent); global_scope.constellation_chan().send(event).unwrap(); } - self.running_animation_callbacks.set(false); - - self.window.reflow(ReflowGoal::ForDisplay, - ReflowQueryType::NoQuery, - ReflowReason::RequestAnimationFrame); + // Update the counter of spurious animation frames. + if spurious { + if self.spurious_animation_frames.get() < SPURIOUS_ANIMATION_FRAME_THRESHOLD { + self.spurious_animation_frames.set(self.spurious_animation_frames.get() + 1) + } + } else { + self.spurious_animation_frames.set(0) + } } pub fn fetch_async(&self, load: LoadType, @@ -2048,6 +2089,7 @@ impl Document { target_element: MutNullableJS::new(None), last_click_info: DOMRefCell::new(None), ignore_destructive_writes_counter: Default::default(), + spurious_animation_frames: Cell::new(0), dom_count: Cell::new(1), fullscreen_element: MutNullableJS::new(None), } @@ -2254,6 +2296,12 @@ impl Document { self.ignore_destructive_writes_counter.get() - 1); } + /// Whether we've seen so many spurious animation frames (i.e. animation frames that didn't + /// mutate the DOM) that we've decided to fall back to fake ones. + fn is_faking_animation_frames(&self) -> bool { + self.spurious_animation_frames.get() >= SPURIOUS_ANIMATION_FRAME_THRESHOLD + } + // https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen #[allow(unrooted_must_root)] pub fn enter_fullscreen(&self, pending: &Element) -> Rc { @@ -3668,6 +3716,26 @@ pub enum FocusEventType { Blur, // Element lost focus. Doesn't bubble. } +/// A fake `requestAnimationFrame()` callback—"fake" because it is not triggered by the video +/// refresh but rather a simple timer. +/// +/// If the page is observed to be using `requestAnimationFrame()` for non-animation purposes (i.e. +/// without mutating the DOM), then we fall back to simple timeouts to save energy over video +/// refresh. +#[derive(JSTraceable, HeapSizeOf)] +pub struct FakeRequestAnimationFrameCallback { + /// The document. + #[ignore_heap_size_of = "non-owning"] + document: Trusted, +} + +impl FakeRequestAnimationFrameCallback { + pub fn invoke(self) { + let document = self.document.root(); + document.run_the_animation_frame_callbacks(); + } +} + #[derive(HeapSizeOf, JSTraceable)] pub enum AnimationFrameCallback { DevtoolsFramerateTick { actor_name: String }, diff --git a/components/script/timers.rs b/components/script/timers.rs index 5de0dc2416b..c90a3fa5b1d 100644 --- a/components/script/timers.rs +++ b/components/script/timers.rs @@ -7,6 +7,7 @@ use dom::bindings::cell::DOMRefCell; use dom::bindings::codegen::Bindings::FunctionBinding::Function; use dom::bindings::reflector::DomObject; use dom::bindings::str::DOMString; +use dom::document::FakeRequestAnimationFrameCallback; use dom::eventsource::EventSourceTimeoutCallback; use dom::globalscope::GlobalScope; use dom::testbinding::TestBindingCallback; @@ -69,6 +70,7 @@ pub enum OneshotTimerCallback { EventSourceTimeout(EventSourceTimeoutCallback), JsTimer(JsTimerTask), TestBindingCallback(TestBindingCallback), + FakeRequestAnimationFrame(FakeRequestAnimationFrameCallback), } impl OneshotTimerCallback { @@ -78,6 +80,7 @@ impl OneshotTimerCallback { OneshotTimerCallback::EventSourceTimeout(callback) => callback.invoke(), OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers), OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(), + OneshotTimerCallback::FakeRequestAnimationFrame(callback) => callback.invoke(), } } }