From 3662491d8f8c135eba360d0d6bae771c42e09a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Wed, 24 Feb 2016 17:37:11 +0100 Subject: [PATCH] Refactor mouseover code to be more performant This increases mouseover/out performance drastically on my machine. --- components/script/dom/document.rs | 148 ++++++++++++++--------------- components/script/dom/node.rs | 6 +- components/script/script_thread.rs | 77 ++++++++------- 3 files changed, 121 insertions(+), 110 deletions(-) diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index d3bbfded3b4..ec0e78fafc2 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -798,81 +798,33 @@ impl Document { pub fn handle_mouse_move_event(&self, js_runtime: *mut JSRuntime, client_point: Option>, - prev_mouse_over_targets: &mut RootedVec>) { - // Build a list of elements that are currently under the mouse. - let mouse_over_address = client_point.as_ref().map(|client_point| { - let page_point = Point2D::new(client_point.x + self.window.PageXOffset() as f32, - client_point.y + self.window.PageYOffset() as f32); - self.hit_test(&page_point, true) - }).unwrap_or(None); + prev_mouse_over_target: &MutNullableHeap>) { + let page_point = match client_point { + None => { + // If there's no point, there's no target under the mouse + // FIXME: dispatch mouseout here. We have no point. + prev_mouse_over_target.set(None); + return; + } + Some(ref client_point) => { + Point2D::new(client_point.x + self.window.PageXOffset() as f32, + client_point.y + self.window.PageYOffset() as f32) + } + }; - let mut mouse_over_targets = RootedVec::>::new(); + let client_point = client_point.unwrap(); - if let Some(address) = mouse_over_address { + let maybe_new_target = self.hit_test(&page_point, true).and_then(|address| { let node = node::from_untrusted_node_address(js_runtime, address); - for node in node.inclusive_ancestors() - .filter_map(Root::downcast::) { - mouse_over_targets.push(JS::from_rooted(&node)); - } - } - - // Werther the topmost element we are hovering now is diffrent than the previous - let is_different_topmost_target = mouse_over_targets.is_empty() || - prev_mouse_over_targets.is_empty() || - prev_mouse_over_targets[0].upcast::().to_trusted_node_address() != - mouse_over_targets[0].upcast::().to_trusted_node_address(); - - // Remove hover from any elements in the previous list that are no longer - // under the mouse. - for (index, target) in prev_mouse_over_targets.iter().enumerate() { - // Hover state is reset as appropiate later - target.set_hover_state(false); - - // https://www.w3.org/TR/uievents/#event-type-mouseout - // - // mouseout must be dispatched when the mouse moves off an element or when pointer - // mouse moves from an element onto the boundaries of one of its descendent elements. - let has_to_dispatch_mouse_out = index == 0 && is_different_topmost_target; - - if has_to_dispatch_mouse_out { - let target = target.upcast(); - if let Some(client_point) = client_point { - self.fire_mouse_event(client_point, &target, "mouseout".to_owned()); - } - } - } - - // Set hover state for any elements in the current mouse over list. - // Check if any of them changed state to determine whether to - // force a reflow below. - for (index, target) in mouse_over_targets.r().iter().enumerate() { - target.set_hover_state(true); - - // https://www.w3.org/TR/uievents/#event-type-mouseover - // - // Mouseover must be fired when a pointing device is moved onto the boundaries of an - // element (we only fire it in the first because it bubbles), or when the pointer has - // moved from our children to ours. - // - // The below condition adresses both situations. - let has_to_dispatch_mouse_over = index == 0 && is_different_topmost_target; - - if has_to_dispatch_mouse_over { - let target = target.upcast(); - if let Some(client_point) = client_point { - self.fire_mouse_event(client_point, target, "mouseover".to_owned()); - } - } - } - - // Send mousemove event to topmost target - if let Some(address) = mouse_over_address { - let top_most_node = node::from_untrusted_node_address(js_runtime, - address); - let client_point = client_point.unwrap(); // Must succeed because hit test succeeded. + node.inclusive_ancestors() + .filter_map(Root::downcast::) + .next() + }); + // Send mousemove event to topmost target, and forward it if it's an iframe + if let Some(ref new_target) = maybe_new_target { // If the target is an iframe, forward the event to the child document. - if let Some(iframe) = top_most_node.downcast::() { + if let Some(iframe) = new_target.downcast::() { if let Some(pipeline_id) = iframe.pipeline_id() { let rect = iframe.upcast::().GetBoundingClientRect(); let child_origin = Point2D::new(rect.X() as f32, rect.Y() as f32); @@ -884,13 +836,59 @@ impl Document { return; } - let target = top_most_node.upcast(); - self.fire_mouse_event(client_point, target, "mousemove".to_owned()); + self.fire_mouse_event(client_point, new_target.upcast(), "mousemove".to_owned()); } - // Store the current mouse over targets for next frame - prev_mouse_over_targets.clear(); - prev_mouse_over_targets.append(&mut *mouse_over_targets); + // Nothing more to do here, mousemove is sent, + // and the element under the mouse hasn't changed. + if maybe_new_target == prev_mouse_over_target.get() { + return; + } + + let old_target_is_ancestor_of_new_target = match (prev_mouse_over_target.get(), maybe_new_target.as_ref()) { + (Some(old_target), Some(new_target)) + => old_target.upcast::().is_ancestor_of(new_target.upcast::()), + _ => false, + }; + + // Here we know the target has changed, so we must update the state, + // dispatch mouseout to the previous one, mouseover to the new one, + if let Some(old_target) = prev_mouse_over_target.get() { + // If the old target is an ancestor of the new target, this can be skipped + // completely, since the node's hover state will be reseted below. + if !old_target_is_ancestor_of_new_target { + for element in old_target.upcast::() + .inclusive_ancestors() + .filter_map(Root::downcast::) { + element.set_hover_state(false); + } + } + + // Remove hover state to old target and its parents + self.fire_mouse_event(client_point, old_target.upcast(), "mouseout".to_owned()); + + // TODO: Fire mouseleave here only if the old target is + // not an ancestor of the new target. + } + + if let Some(ref new_target) = maybe_new_target { + for element in new_target.upcast::() + .inclusive_ancestors() + .filter_map(Root::downcast::) { + if element.get_hover_state() { + break; + } + + element.set_hover_state(true); + } + + self.fire_mouse_event(client_point, &new_target.upcast(), "mouseover".to_owned()); + + // TODO: Fire mouseenter here. + } + + // Store the current mouse over target for next frame. + prev_mouse_over_target.set(maybe_new_target.as_ref().map(|target| target.r())); self.window.reflow(ReflowGoal::ForDisplay, ReflowQueryType::NoQuery, diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index ec72b6eed2f..717be0331d5 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -518,7 +518,11 @@ impl Node { } pub fn is_inclusive_ancestor_of(&self, parent: &Node) -> bool { - self == parent || parent.ancestors().any(|ancestor| ancestor.r() == self) + self == parent || self.is_ancestor_of(parent) + } + + pub fn is_ancestor_of(&self, parent: &Node) -> bool { + parent.ancestors().any(|ancestor| ancestor.r() == self) } pub fn following_siblings(&self) -> NodeSiblingIterator { diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index ddf32f85406..a735603431a 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -27,10 +27,10 @@ use dom::bindings::codegen::Bindings::DocumentBinding::{DocumentMethods, Documen use dom::bindings::conversions::{FromJSValConvertible, StringificationBehavior}; use dom::bindings::global::GlobalRef; use dom::bindings::inheritance::Castable; -use dom::bindings::js::{JS, RootCollection, trace_roots}; +use dom::bindings::js::{JS, MutNullableHeap, Root, RootCollection, trace_roots}; use dom::bindings::js::{RootCollectionPtr, RootedReference}; use dom::bindings::refcounted::{LiveDOMReferences, Trusted, TrustedReference, trace_refcounted_objects}; -use dom::bindings::trace::{JSTraceable, RootedVec, trace_traceables}; +use dom::bindings::trace::{JSTraceable, trace_traceables}; use dom::bindings::utils::{DOM_CALLBACKS, WRAP_CALLBACKS}; use dom::browsingcontext::BrowsingContext; use dom::document::{Document, DocumentProgressHandler, DocumentSource, FocusType, IsHTMLDocument}; @@ -91,7 +91,6 @@ use std::cell::{Cell, RefCell}; use std::collections::HashSet; use std::io::{Write, stdout}; use std::marker::PhantomData; -use std::mem as std_mem; use std::option::Option; use std::ptr; use std::rc::Rc; @@ -543,7 +542,8 @@ pub struct ScriptThread { /// The JavaScript runtime. js_runtime: Rc, - mouse_over_targets: DOMRefCell>>, + /// The topmost element over the mouse. + topmost_mouse_over_target: MutNullableHeap>, /// List of pipelines that have been owned and closed by this script thread. closed_pipelines: DOMRefCell>, @@ -789,7 +789,7 @@ impl ScriptThread { devtools_sender: ipc_devtools_sender, js_runtime: Rc::new(runtime), - mouse_over_targets: DOMRefCell::new(vec!()), + topmost_mouse_over_target: MutNullableHeap::new(Default::default()), closed_pipelines: DOMRefCell::new(HashSet::new()), scheduler_chan: state.scheduler_chan, @@ -1979,46 +1979,55 @@ impl ScriptThread { let page = get_page(&self.root_page(), pipeline_id); let document = page.document(); - let mut prev_mouse_over_targets: RootedVec> = RootedVec::new(); - for target in &*self.mouse_over_targets.borrow_mut() { - prev_mouse_over_targets.push(target.clone()); + // Get the previous target temporarily + let prev_mouse_over_target = self.topmost_mouse_over_target.get(); + + document.handle_mouse_move_event(self.js_runtime.rt(), point, + &self.topmost_mouse_over_target); + + // Short-circuit if nothing changed + if self.topmost_mouse_over_target.get() == prev_mouse_over_target { + return; } - // We temporarily steal the list of targets over which the mouse is to pass it to - // handle_mouse_move_event() in a safe RootedVec container. - let mut mouse_over_targets = RootedVec::new(); - std_mem::swap(&mut *self.mouse_over_targets.borrow_mut(), &mut *mouse_over_targets); - document.handle_mouse_move_event(self.js_runtime.rt(), point, &mut mouse_over_targets); - - // Notify Constellation about anchors that are no longer mouse over targets. - for target in &*prev_mouse_over_targets { - if !mouse_over_targets.contains(target) && target.is::() { - let event = ConstellationMsg::NodeStatus(None); - let ConstellationChan(ref chan) = self.constellation_chan; - chan.send(event).unwrap(); - break; - } - } + let mut state_already_changed = false; // Notify Constellation about the topmost anchor mouse over target. - for target in &*mouse_over_targets { - if target.is::() { - let status = target.get_attribute(&ns!(), &atom!("href")) - .and_then(|href| { - let value = href.value(); - let url = document.url(); - url.join(&value).map(|url| url.serialize()).ok() - }); + if let Some(target) = self.topmost_mouse_over_target.get() { + if let Some(anchor) = target.upcast::() + .inclusive_ancestors() + .filter_map(Root::downcast::) + .next() { + let status = anchor.upcast::() + .get_attribute(&ns!(), &atom!("href")) + .and_then(|href| { + let value = href.value(); + let url = document.url(); + url.join(&value).map(|url| url.serialize()).ok() + }); + let event = ConstellationMsg::NodeStatus(status); let ConstellationChan(ref chan) = self.constellation_chan; chan.send(event).unwrap(); - break; + + state_already_changed = true; } } - std_mem::swap(&mut *self.mouse_over_targets.borrow_mut(), &mut *mouse_over_targets); + // We might have to reset the anchor state + if !state_already_changed { + if let Some(target) = prev_mouse_over_target { + if let Some(_) = target.upcast::() + .inclusive_ancestors() + .filter_map(Root::downcast::) + .next() { + let event = ConstellationMsg::NodeStatus(None); + let ConstellationChan(ref chan) = self.constellation_chan; + chan.send(event).unwrap(); + } + } + } } - TouchEvent(event_type, identifier, point) => { let handled = self.handle_touch_event(pipeline_id, event_type, identifier, point); match event_type {