servo: Merge #15816 - Improve performance of layout queries and requestAnimationFrame (from servo:raf-timer); r=jdm,emilio
authorPatrick Walton <pcwalton@mimiga.net>
Sun, 05 Mar 2017 05:14:45 -0800
changeset 374970 bee2a7a54c04e45c5d96135fda975cd7908cf9b0
parent 374969 84c1dd84d182a55e5a8367c563a78635486edc07
child 374971 37683d1886aabb779c66084ebb2a1e197bf268eb
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm, emilio
bugs15816, 14442
milestone54.0a1
servo: Merge #15816 - Improve performance of layout queries and requestAnimationFrame (from servo:raf-timer); r=jdm,emilio Part of #14442. Source-Repo: https://github.com/servo/servo Source-Revision: 72fd27bbccf9214cee11c78834ca1fbc96b3bf22
servo/components/layout/construct.rs
servo/components/layout_thread/lib.rs
servo/components/script/dom/document.rs
servo/components/script/timers.rs
--- a/servo/components/layout/construct.rs
+++ b/servo/components/layout/construct.rs
@@ -79,21 +79,17 @@ pub enum ConstructionResult {
     Flow(FlowRef, AbsoluteDescendants),
 
     /// This node contributed some object or objects that will be needed to construct a proper flow
     /// later up the tree, but these objects have not yet found their home.
     ConstructionItem(ConstructionItem),
 }
 
 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()
     }
 
     pub fn debug_id(&self) -> usize {
         match *self {
             ConstructionResult::None => 0,
@@ -480,17 +476,17 @@ impl<'a, ConcreteThreadSafeLayoutNode: T
     fn build_block_flow_using_construction_result_of_child(
             &mut self,
             flow: &mut FlowRef,
             node: &ConcreteThreadSafeLayoutNode,
             kid: ConcreteThreadSafeLayoutNode,
             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
                 // TableWrapperFlow.
                 if flow.is_table() && kid_flow.is_table_caption() {
                     let construction_result =
                         ConstructionResult::Flow(kid_flow, AbsoluteDescendants::new());
                     self.set_flow_construction_result(&kid, construction_result)
@@ -779,17 +775,17 @@ impl<'a, ConcreteThreadSafeLayoutNode: T
 
         // Concatenate all the fragments of our kids, creating {ib} splits as necessary.
         let mut is_empty = true;
         for kid in node.children() {
             is_empty = false;
             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) {
                         opt_inline_block_splits.push_back(InlineBlockSplit::new(
                             &mut fragment_accumulator, node, self.style_context(), flow));
                         abs_descendants.push_descendants(kid_abs_descendants);
                     } else {
                         // Push the absolutely-positioned kid as an inline containing block.
@@ -1030,17 +1026,17 @@ impl<'a, ConcreteThreadSafeLayoutNode: T
     /// Places any table captions found under the given table wrapper, if the value of their
     /// `caption-side` property is equal to the given `side`.
     fn place_table_caption_under_table_wrapper_on_side(&mut self,
                                                        table_wrapper_flow: &mut FlowRef,
                                                        node: &ConcreteThreadSafeLayoutNode,
                                                        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()
                                 .fragment
                                 .style()
                                 .get_inheritedtable()
                                 .caption_side == side {
                         table_wrapper_flow.add_new_child(kid_flow);
@@ -1299,17 +1295,17 @@ impl<'a, ConcreteThreadSafeLayoutNode: T
             Fragment::new(node,
                           SpecificFragmentInfo::TableColumn(TableColumnFragmentInfo::new(node)),
                           self.layout_context);
         let mut col_fragments = vec!();
         for kid in node.children() {
             // 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)
             }
         }
         if col_fragments.is_empty() {
             debug!("add SpecificFragmentInfo::TableColumn for empty colgroup");
             let specific = SpecificFragmentInfo::TableColumn(TableColumnFragmentInfo::new(node));
             col_fragments.push(Fragment::new(node, specific, self.layout_context));
         }
@@ -1636,19 +1632,18 @@ trait NodeUtils {
     /// Returns true if this node doesn't render its kids and false otherwise.
     fn is_replaced_content(&self) -> bool;
 
     fn construction_result_mut(self, layout_data: &mut PersistentLayoutData) -> &mut ConstructionResult;
 
     /// 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<ConcreteThreadSafeLayoutNode> NodeUtils for ConcreteThreadSafeLayoutNode
                                              where ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode {
     fn is_replaced_content(&self) -> bool {
         match self.type_id() {
             Some(LayoutNodeType::Text) |
             Some(LayoutNodeType::Element(LayoutElementType::HTMLImageElement)) |
@@ -1681,19 +1676,19 @@ impl<ConcreteThreadSafeLayoutNode> NodeU
 
         let mut layout_data = self.mutate_layout_data().unwrap();
         let dst = self.construction_result_mut(&mut *layout_data);
 
         *dst = result;
     }
 
     #[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()
     }
 }
 
 /// Methods for interacting with HTMLObjectElement nodes
 trait ObjectElement {
     /// Returns true if this node has object data that is correct uri.
     fn has_object_data(&self) -> bool;
 
--- a/servo/components/layout_thread/lib.rs
+++ b/servo/components/layout_thread/lib.rs
@@ -769,17 +769,17 @@ impl LayoutThread {
         possibly_locked_rw_data.block(rw_data);
     }
 
     fn try_get_layout_root<N: LayoutNode>(&self, node: N) -> Option<FlowRef> {
         let mut data = match node.mutate_layout_data() {
             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) => {
                 // Note: Assuming that the root has display 'static' (as per
                 // CSS Section 9.3.1). Otherwise, if it were absolutely
                 // positioned, it would return a reference to itself in
                 // `abs_descendants` and would lead to a circular reference.
                 // Set Root as CB for any remaining absolute descendants.
--- a/servo/components/script/dom/document.rs
+++ b/servo/components/script/dom/document.rs
@@ -106,17 +106,17 @@ use net_traits::CoreResourceMsg::{GetCoo
 use net_traits::request::RequestInit;
 use net_traits::response::HttpsState;
 use num_traits::ToPrimitive;
 use script_layout_interface::message::{Msg, ReflowQueryType};
 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;
 use servo_config::prefs::PREFS;
 use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
 use std::ascii::AsciiExt;
 use std::borrow::ToOwned;
 use std::cell::{Cell, Ref, RefMut};
@@ -131,19 +131,29 @@ use std::time::{Duration, Instant};
 use style::attr::AttrValue;
 use style::context::{QuirksMode, ReflowGoal};
 use style::restyle_hints::{RestyleHint, RESTYLE_STYLE_ATTRIBUTE};
 use style::selector_parser::{RestyleDamage, Snapshot};
 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,
 }
 
 #[derive(Clone, Copy, Debug, HeapSizeOf, JSTraceable, PartialEq)]
 pub enum IsHTMLDocument {
     HTMLDocument,
@@ -285,16 +295,21 @@ pub struct Document {
     referrer: Option<String>,
     /// https://html.spec.whatwg.org/multipage/#target-element
     target_element: MutNullableJS<Element>,
     /// https://w3c.github.io/uievents/#event-type-dblclick
     #[ignore_heap_size_of = "Defined in std"]
     last_click_info: DOMRefCell<Option<(Instant, Point2D<f32>)>>,
     /// https://html.spec.whatwg.org/multipage/#ignore-destructive-writes-counter
     ignore_destructive_writes_counter: Cell<u32>,
+    /// The number of spurious `requestAnimationFrame()` requests we've received.
+    ///
+    /// A rAF request is considered spurious if nothing was actually reflowed.
+    spurious_animation_frames: Cell<u8>,
+
     /// 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.
     ///
     /// See also: https://github.com/servo/servo/issues/10110
     dom_count: Cell<u32>,
     /// Entry node for fullscreen.
     fullscreen_element: MutNullableJS<Element>,
@@ -1493,21 +1508,30 @@ impl Document {
         // No need to send a `ChangeRunningAnimationsState` if we're running animation callbacks:
         // we're guaranteed to already be in the "animation callbacks present" state.
         //
         // This reduces CPU usage by avoiding needless thread wakeups in the common case of
         // repeated rAF.
         //
         // TODO: Should tick animation only when document is visible
         if !self.running_animation_callbacks.get() {
-            let global_scope = self.window.upcast::<GlobalScope>();
-            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::<GlobalScope>();
+                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
     }
 
     /// https://html.spec.whatwg.org/multipage/#dom-window-cancelanimationframe
     pub fn cancel_animation_frame(&self, ident: u32) {
         let mut list = self.animation_frame_list.borrow_mut();
@@ -1519,42 +1543,59 @@ impl Document {
     /// https://html.spec.whatwg.org/multipage/#run-the-animation-frame-callbacks
     pub fn run_the_animation_frame_callbacks(&self) {
         rooted_vec!(let mut animation_frame_list);
         mem::swap(
             &mut *animation_frame_list,
             &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(..) {
             if let Some(callback) = callback {
                 callback.call(self, *timing);
             }
         }
 
+        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::<GlobalScope>();
-            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,
                        request: RequestInit,
                        fetch_target: IpcSender<FetchResponseMsg>) {
         let mut loader = self.loader.borrow_mut();
         loader.fetch_async(load, request, fetch_target);
     }
@@ -2043,16 +2084,17 @@ impl Document {
             https_state: Cell::new(HttpsState::None),
             touchpad_pressure_phase: Cell::new(TouchpadPressurePhase::BeforeClick),
             origin: origin,
             referrer: referrer,
             referrer_policy: Cell::new(referrer_policy),
             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),
         }
     }
 
     // https://dom.spec.whatwg.org/#dom-document-document
     pub fn Constructor(window: &Window) -> Fallible<Root<Document>> {
         let doc = window.Document();
@@ -2249,16 +2291,22 @@ impl Document {
             self.ignore_destructive_writes_counter.get() + 1);
     }
 
     pub fn decr_ignore_destructive_writes_counter(&self) {
         self.ignore_destructive_writes_counter.set(
             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<Promise> {
         // Step 1
         let promise = Promise::new(self.global().r());
         let mut error = false;
 
         // Step 4
@@ -3663,16 +3711,36 @@ pub enum FocusType {
 }
 
 /// Focus events
 pub enum FocusEventType {
     Focus,      // Element gained focus. Doesn't bubble.
     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<Document>,
+}
+
+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 },
     FrameRequestCallback {
         #[ignore_heap_size_of = "Rc is hard"]
         callback: Rc<FrameRequestCallback>
     },
 }
--- a/servo/components/script/timers.rs
+++ b/servo/components/script/timers.rs
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use dom::bindings::callback::ExceptionHandling::Report;
 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;
 use dom::xmlhttprequest::XHRTimeoutCallback;
 use euclid::length::Length;
 use heapsize::HeapSizeOf;
 use ipc_channel::ipc::IpcSender;
 use js::jsapi::{HandleValue, Heap};
@@ -64,25 +65,27 @@ struct OneshotTimer {
 // A replacement trait would have a method such as
 //     `invoke<T: DomObject>(self: Box<Self>, this: &T, js_timers: &JsTimers);`.
 #[derive(JSTraceable, HeapSizeOf)]
 pub enum OneshotTimerCallback {
     XhrTimeout(XHRTimeoutCallback),
     EventSourceTimeout(EventSourceTimeoutCallback),
     JsTimer(JsTimerTask),
     TestBindingCallback(TestBindingCallback),
+    FakeRequestAnimationFrame(FakeRequestAnimationFrameCallback),
 }
 
 impl OneshotTimerCallback {
     fn invoke<T: DomObject>(self, this: &T, js_timers: &JsTimers) {
         match self {
             OneshotTimerCallback::XhrTimeout(callback) => callback.invoke(),
             OneshotTimerCallback::EventSourceTimeout(callback) => callback.invoke(),
             OneshotTimerCallback::JsTimer(task) => task.invoke(this, js_timers),
             OneshotTimerCallback::TestBindingCallback(callback) => callback.invoke(),
+            OneshotTimerCallback::FakeRequestAnimationFrame(callback) => callback.invoke(),
         }
     }
 }
 
 impl Ord for OneshotTimer {
     fn cmp(&self, other: &OneshotTimer) -> Ordering {
         match self.scheduled_for.cmp(&other.scheduled_for).reverse() {
             Ordering::Equal => self.handle.cmp(&other.handle).reverse(),