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(), } } }