From dabebdfbf5e502e3eef7cb0159b20f0fa5e74f65 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Fri, 7 Apr 2017 06:57:08 +0900 Subject: [PATCH] Root nodes for the duration of their CSS transitions. This ensures that we can pass a node address as part of the asynchronous transition end notification, making it safe to fire the corresponding DOM event on the node from the script thread. Without explicitly rooting this node when the transition starts, we risk the node being GCed before the transition is complete. --- components/layout/animation.rs | 13 +++-- components/layout/query.rs | 9 ++++ components/layout_thread/lib.rs | 4 ++ components/script/dom/window.rs | 6 ++- components/script/script_thread.rs | 47 +++++++++++++++---- components/script_layout_interface/rpc.rs | 2 + components/script_traits/lib.rs | 4 +- components/style/animation.rs | 26 ++++++---- components/style/matching.rs | 3 +- tests/wpt/mozilla/meta/MANIFEST.json | 10 ++++ .../tests/mozilla/transitionend_safety.html | 27 +++++++++++ 11 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 tests/wpt/mozilla/tests/mozilla/transitionend_safety.html diff --git a/components/layout/animation.rs b/components/layout/animation.rs index be6d5baf77c..ebde01323e5 100644 --- a/components/layout/animation.rs +++ b/components/layout/animation.rs @@ -9,7 +9,9 @@ use flow::{self, Flow}; use gfx::display_list::OpaqueNode; use ipc_channel::ipc::IpcSender; use msg::constellation_msg::PipelineId; +use opaque_node::OpaqueNodeMethods; use script_traits::{AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg}; +use script_traits::UntrustedNodeAddress; use std::collections::HashMap; use std::sync::mpsc::Receiver; use style::animation::{Animation, update_style_for_animation}; @@ -24,6 +26,7 @@ pub fn update_animation_state(constellation_chan: &IpcSender, script_chan: &IpcSender, running_animations: &mut HashMap>, expired_animations: &mut HashMap>, + newly_transitioning_nodes: &mut Vec, new_animations_receiver: &Receiver, pipeline_id: PipelineId, timer: &Timer) { @@ -71,7 +74,7 @@ pub fn update_animation_state(constellation_chan: &IpcSender, let mut animations_still_running = vec![]; for mut running_animation in running_animations.drain(..) { let still_running = !running_animation.is_expired() && match running_animation { - Animation::Transition(_, _, started_at, ref frame, _expired) => { + Animation::Transition(_, started_at, ref frame, _expired) => { now < started_at + frame.duration } Animation::Keyframes(_, _, ref mut state) => { @@ -86,8 +89,8 @@ pub fn update_animation_state(constellation_chan: &IpcSender, continue } - if let Animation::Transition(_, unsafe_node, _, ref frame, _) = running_animation { - script_chan.send(ConstellationControlMsg::TransitionEnd(unsafe_node, + if let Animation::Transition(node, _, ref frame, _) = running_animation { + script_chan.send(ConstellationControlMsg::TransitionEnd(node.to_untrusted_node_address(), frame.property_animation .property_name().into(), frame.duration)) @@ -112,6 +115,10 @@ pub fn update_animation_state(constellation_chan: &IpcSender, // Add new running animations. for new_running_animation in new_running_animations { + if new_running_animation.is_transition() { + newly_transitioning_nodes.push(new_running_animation.node().to_untrusted_node_address()); + } + running_animations.entry(*new_running_animation.node()) .or_insert_with(Vec::new) .push(new_running_animation) diff --git a/components/layout/query.rs b/components/layout/query.rs index c12a2b7f63d..a8d5913e3db 100644 --- a/components/layout/query.rs +++ b/components/layout/query.rs @@ -94,6 +94,9 @@ pub struct LayoutThreadData { /// A queued response for the list of nodes at a given point. pub nodes_from_point_response: Vec, + + /// A list of nodes that have just started a CSS transition. + pub newly_transitioning_nodes: Vec, } pub struct LayoutRPCImpl(pub Arc>); @@ -204,6 +207,12 @@ impl LayoutRPC for LayoutRPCImpl { let mut rw_data = rw_data.lock().unwrap(); mem::replace(&mut rw_data.pending_images, vec![]) } + + fn newly_transitioning_nodes(&self) -> Vec { + let &LayoutRPCImpl(ref rw_data) = self; + let mut rw_data = rw_data.lock().unwrap(); + mem::replace(&mut rw_data.newly_transitioning_nodes, vec![]) + } } struct UnioningFragmentBorderBoxIterator { diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index 9e8cc9bc165..2bef4bc1c21 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -478,6 +478,7 @@ impl LayoutThread { text_index_response: TextIndexResponse(None), pending_images: vec![], nodes_from_point_response: vec![], + newly_transitioning_nodes: vec![], })), error_reporter: CSSErrorReporter { pipelineid: id, @@ -1436,11 +1437,14 @@ impl LayoutThread { document: Option<&ServoLayoutDocument>, rw_data: &mut LayoutThreadData, context: &mut LayoutContext) { + assert!(rw_data.newly_transitioning_nodes.is_empty()); + // Kick off animations if any were triggered, expire completed ones. animation::update_animation_state(&self.constellation_chan, &self.script_chan, &mut *self.running_animations.write(), &mut *self.expired_animations.write(), + &mut rw_data.newly_transitioning_nodes, &self.new_animations_receiver, self.id, &self.timer); diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 993749b00c2..a783c15531b 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -76,7 +76,7 @@ use script_layout_interface::rpc::{MarginStyleResponse, NodeScrollRootIdResponse use script_layout_interface::rpc::{ResolvedStyleResponse, TextIndexResponse}; use script_runtime::{CommonScriptMsg, ScriptChan, ScriptPort, ScriptThreadEventCategory}; use script_thread::{MainThreadScriptChan, MainThreadScriptMsg, Runnable, RunnableWrapper}; -use script_thread::{SendableMainThreadScriptChan, ImageCacheMsg}; +use script_thread::{SendableMainThreadScriptChan, ImageCacheMsg, ScriptThread}; use script_traits::{ConstellationControlMsg, LoadData, MozBrowserEvent, UntrustedNodeAddress}; use script_traits::{DocumentState, TimerEvent, TimerEventId}; use script_traits::{ScriptMsg as ConstellationMsg, TimerSchedulerMsg, WindowSizeData, WindowSizeType}; @@ -1150,6 +1150,7 @@ impl Window { /// off-main-thread layout. /// /// Returns true if layout actually happened, false otherwise. + #[allow(unsafe_code)] pub fn force_reflow(&self, goal: ReflowGoal, query_type: ReflowQueryType, @@ -1261,6 +1262,9 @@ impl Window { } } + let newly_transitioning_nodes = self.layout_rpc.newly_transitioning_nodes(); + ScriptThread::note_newly_transitioning_nodes(newly_transitioning_nodes); + true } diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 8a861e92570..805b19a24b2 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -47,7 +47,7 @@ use dom::globalscope::GlobalScope; use dom::htmlanchorelement::HTMLAnchorElement; use dom::htmliframeelement::{HTMLIFrameElement, NavigationType}; use dom::mutationobserver::MutationObserver; -use dom::node::{Node, NodeDamage, window_from_node}; +use dom::node::{Node, NodeDamage, window_from_node, from_untrusted_node_address}; use dom::serviceworker::TrustedServiceWorkerAddress; use dom::serviceworkerregistration::ServiceWorkerRegistration; use dom::servoparser::{ParserContext, ServoParser}; @@ -69,7 +69,6 @@ use js::jsapi::{JSAutoCompartment, JSContext, JS_SetWrapObjectCallbacks}; use js::jsapi::{JSTracer, SetWindowProxyClass}; use js::jsval::UndefinedValue; use js::rust::Runtime; -use layout_wrapper::ServoLayoutNode; use mem::heap_size_of_self_and_children; use microtask::{MicrotaskQueue, Microtask}; use msg::constellation_msg::{FrameId, FrameType, PipelineId, PipelineNamespace}; @@ -109,7 +108,6 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{Receiver, Select, Sender, channel}; use std::thread; use style::context::ReflowGoal; -use style::dom::{TNode, UnsafeNode}; use style::thread_state; use task_source::dom_manipulation::{DOMManipulationTask, DOMManipulationTaskSource}; use task_source::file_reading::FileReadingTaskSource; @@ -490,6 +488,10 @@ pub struct ScriptThread { /// A list of pipelines containing documents that finished loading all their blocking /// resources during a turn of the event loop. docs_with_no_blocking_loads: DOMRefCell>>, + + /// A list of nodes with in-progress CSS transitions, which roots them for the duration + /// of the transition. + transitioning_nodes: DOMRefCell>>, } /// In the event of thread panic, all data on the stack runs its destructor. However, there @@ -574,6 +576,17 @@ impl ScriptThreadFactory for ScriptThread { } impl ScriptThread { + pub fn note_newly_transitioning_nodes(nodes: Vec) { + SCRIPT_THREAD_ROOT.with(|root| { + let script_thread = unsafe { &*root.get().unwrap() }; + let js_runtime = script_thread.js_runtime.rt(); + let new_nodes = nodes + .into_iter() + .map(|n| JS::from_ref(&*from_untrusted_node_address(js_runtime, n))); + script_thread.transitioning_nodes.borrow_mut().extend(new_nodes); + }) + } + pub fn add_mutation_observer(observer: &MutationObserver) { SCRIPT_THREAD_ROOT.with(|root| { let script_thread = unsafe { &*root.get().unwrap() }; @@ -742,6 +755,8 @@ impl ScriptThread { webvr_thread: state.webvr_thread, docs_with_no_blocking_loads: Default::default(), + + transitioning_nodes: Default::default(), } } @@ -1602,11 +1617,27 @@ impl ScriptThread { } /// Handles firing of transition events. - #[allow(unsafe_code)] - fn handle_transition_event(&self, unsafe_node: UnsafeNode, name: String, duration: f64) { - let node = unsafe { ServoLayoutNode::from_unsafe(&unsafe_node) }; - let node = unsafe { node.get_jsmanaged().get_for_script() }; - let window = window_from_node(node); + fn handle_transition_event(&self, unsafe_node: UntrustedNodeAddress, name: String, duration: f64) { + let js_runtime = self.js_runtime.rt(); + let node = from_untrusted_node_address(js_runtime, unsafe_node); + + let idx = self.transitioning_nodes + .borrow() + .iter() + .position(|n| &**n as *const _ == &*node as *const _); + match idx { + Some(idx) => { + self.transitioning_nodes.borrow_mut().remove(idx); + } + None => { + // If no index is found, we can't know whether this node is safe to use. + // It's better not to fire a DOM event than crash. + warn!("Ignoring transition end notification for unknown node."); + return; + } + } + + let window = window_from_node(&*node); // Not quite the right thing - see #13865. node.dirty(NodeDamage::NodeStyleDamaged); diff --git a/components/script_layout_interface/rpc.rs b/components/script_layout_interface/rpc.rs index 82dd9b9ff08..1c694a7ff8c 100644 --- a/components/script_layout_interface/rpc.rs +++ b/components/script_layout_interface/rpc.rs @@ -42,6 +42,8 @@ pub trait LayoutRPC { fn pending_images(&self) -> Vec; /// Requests the list of nodes from the given point. fn nodes_from_point_response(&self) -> Vec; + /// Requests the list of nodes that have just started CSS transitions in the last reflow. + fn newly_transitioning_nodes(&self) -> Vec; fn text_index(&self) -> TextIndexResponse; } diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 29baf8821e6..4f3cfca76a5 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -69,7 +69,7 @@ use std::collections::HashMap; use std::fmt; use std::sync::Arc; use std::sync::mpsc::{Receiver, Sender}; -use style_traits::{CSSPixel, UnsafeNode}; +use style_traits::CSSPixel; use webdriver_msg::{LoadStatus, WebDriverScriptCommand}; use webrender_traits::ClipId; use webvr_traits::{WebVREvent, WebVRMsg}; @@ -274,7 +274,7 @@ pub enum ConstellationControlMsg { /// Notifies script thread that all animations are done TickAllAnimations(PipelineId), /// Notifies the script thread of a transition end - TransitionEnd(UnsafeNode, String, f64), + TransitionEnd(UntrustedNodeAddress, String, f64), /// Notifies the script thread that a new Web font has been loaded, and thus the page should be /// reflowed. WebFontLoaded(PipelineId), diff --git a/components/style/animation.rs b/components/style/animation.rs index 632588988c2..5f53508ba4e 100644 --- a/components/style/animation.rs +++ b/components/style/animation.rs @@ -8,7 +8,7 @@ use Atom; use bezier::Bezier; use context::SharedStyleContext; -use dom::{OpaqueNode, UnsafeNode}; +use dom::OpaqueNode; use euclid::point::Point2D; use font_metrics::FontMetricsProvider; use keyframes::{KeyframesStep, KeyframesStepValue}; @@ -188,7 +188,7 @@ pub enum Animation { /// the f64 field is the start time as returned by `time::precise_time_s()`. /// /// The `bool` field is werther this animation should no longer run. - Transition(OpaqueNode, UnsafeNode, f64, AnimationFrame, bool), + Transition(OpaqueNode, f64, AnimationFrame, bool), /// A keyframes animation is identified by a name, and can have a /// node-dependent state (i.e. iteration count, etc.). Keyframes(OpaqueNode, Atom, KeyframesAnimationState), @@ -200,7 +200,7 @@ impl Animation { pub fn mark_as_expired(&mut self) { debug_assert!(!self.is_expired()); match *self { - Animation::Transition(_, _, _, _, ref mut expired) => *expired = true, + Animation::Transition(_, _, _, ref mut expired) => *expired = true, Animation::Keyframes(_, _, ref mut state) => state.expired = true, } } @@ -209,7 +209,7 @@ impl Animation { #[inline] pub fn is_expired(&self) -> bool { match *self { - Animation::Transition(_, _, _, _, expired) => expired, + Animation::Transition(_, _, _, expired) => expired, Animation::Keyframes(_, _, ref state) => state.expired, } } @@ -218,7 +218,7 @@ impl Animation { #[inline] pub fn node(&self) -> &OpaqueNode { match *self { - Animation::Transition(ref node, _, _, _, _) => node, + Animation::Transition(ref node, _, _, _) => node, Animation::Keyframes(ref node, _, _) => node, } } @@ -231,6 +231,15 @@ impl Animation { Animation::Keyframes(_, _, ref state) => state.is_paused(), } } + + /// Whether this animation is a transition. + #[inline] + pub fn is_transition(&self) -> bool { + match *self { + Animation::Transition(..) => true, + Animation::Keyframes(..) => false, + } + } } @@ -402,7 +411,6 @@ impl PropertyAnimation { #[cfg(feature = "servo")] pub fn start_transitions_if_applicable(new_animations_sender: &Sender, opaque_node: OpaqueNode, - unsafe_node: UnsafeNode, old_style: &ComputedValues, new_style: &mut Arc, timer: &Timer, @@ -436,7 +444,7 @@ pub fn start_transitions_if_applicable(new_animations_sender: &Sender let start_time = now + (box_style.transition_delay_mod(i).seconds() as f64); new_animations_sender - .send(Animation::Transition(opaque_node, unsafe_node, start_time, AnimationFrame { + .send(Animation::Transition(opaque_node, start_time, AnimationFrame { duration: box_style.transition_duration_mod(i).seconds() as f64, property_animation: property_animation, }, /* is_expired = */ false)).unwrap(); @@ -589,7 +597,7 @@ pub fn update_style_for_animation(context: &SharedStyleContext, debug_assert!(!animation.is_expired()); match *animation { - Animation::Transition(_, _, start_time, ref frame, _) => { + Animation::Transition(_, start_time, ref frame, _) => { debug!("update_style_for_animation: transition found"); let now = context.timer.seconds(); let mut new_style = (*style).clone(); @@ -767,7 +775,7 @@ pub fn complete_expired_transitions(node: OpaqueNode, style: &mut Arc + +Asynchronous transitionend event is not a GC hazard + + + + +