servo: Merge #16295 - Root nodes for the duration of their CSS transitions (from jdm:transition-safety); r=nox
authorJosh Matthews <josh@joshmatthews.net>
Mon, 15 May 2017 15:00:19 -0500
changeset 358533 ba0f4d2877263ffea0f71f5311ae8c780606b0fc
parent 358532 e12dcb4be8551c14073c891a4c9f790e0d790215
child 358534 7a2841383525164d5651218a06f3b5e74bd570af
push id90352
push usercbook@mozilla.com
push dateTue, 16 May 2017 13:09:14 +0000
treeherdermozilla-inbound@8f89d291e303 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnox
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
servo: Merge #16295 - Root nodes for the duration of their CSS transitions (from jdm:transition-safety); r=nox 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. --- - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #14972 - [X] There are tests for these changes Source-Repo: https://github.com/servo/servo Source-Revision: fa251ec96b445b9ba8439d76e05870a88c2caa0f
servo/components/layout/animation.rs
servo/components/layout/context.rs
servo/components/layout/query.rs
servo/components/layout_thread/lib.rs
servo/components/script/dom/document.rs
servo/components/script/dom/node.rs
servo/components/script/dom/window.rs
servo/components/script/script_thread.rs
servo/components/script_layout_interface/message.rs
servo/components/script_layout_interface/rpc.rs
servo/components/script_traits/lib.rs
servo/components/style/animation.rs
servo/components/style/matching.rs
--- a/servo/components/layout/animation.rs
+++ b/servo/components/layout/animation.rs
@@ -4,31 +4,34 @@
 
 //! CSS transitions and animations.
 
 use context::LayoutContext;
 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};
 use style::font_metrics::ServoMetricsProvider;
 use style::selector_parser::RestyleDamage;
 use style::timer::Timer;
 
 /// Processes any new animations that were discovered after style recalculation.
 /// Also expire any old animations that have completed, inserting them into
 /// `expired_animations`.
 pub fn update_animation_state(constellation_chan: &IpcSender<ConstellationMsg>,
                               script_chan: &IpcSender<ConstellationControlMsg>,
                               running_animations: &mut HashMap<OpaqueNode, Vec<Animation>>,
                               expired_animations: &mut HashMap<OpaqueNode, Vec<Animation>>,
+                              mut newly_transitioning_nodes: Option<&mut Vec<UntrustedNodeAddress>>,
                               new_animations_receiver: &Receiver<Animation>,
                               pipeline_id: PipelineId,
                               timer: &Timer) {
     let mut new_running_animations = vec![];
     while let Ok(animation) = new_animations_receiver.try_recv() {
         let mut should_push = true;
         if let Animation::Keyframes(ref node, ref name, ref state) = animation {
             // If the animation was already present in the list for the
@@ -66,33 +69,33 @@ pub fn update_animation_state(constellat
     // TODO: Do not expunge Keyframes animations, since we need that state if
     // the animation gets re-triggered. Probably worth splitting in two
     // different maps, or at least using a linked list?
     let mut keys_to_remove = vec![];
     for (key, running_animations) in running_animations.iter_mut() {
         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) => {
                     // This animation is still running, or we need to keep
                     // iterating.
                     now < state.started_at + state.duration || state.tick()
                 }
             };
 
             if still_running {
                 animations_still_running.push(running_animation);
                 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))
                            .unwrap();
             }
 
             expired_animations.entry(*key)
                               .or_insert_with(Vec::new)
@@ -107,16 +110,27 @@ pub fn update_animation_state(constellat
     }
 
     for key in keys_to_remove {
         running_animations.remove(&key).unwrap();
     }
 
     // Add new running animations.
     for new_running_animation in new_running_animations {
+        if new_running_animation.is_transition() {
+            match newly_transitioning_nodes {
+                Some(ref mut nodes) => {
+                    nodes.push(new_running_animation.node().to_untrusted_node_address());
+                }
+                None => {
+                    warn!("New transition encountered from compositor-initiated layout.");
+                }
+            }
+        }
+
         running_animations.entry(*new_running_animation.node())
                           .or_insert_with(Vec::new)
                           .push(new_running_animation)
     }
 
     let animation_state = if running_animations.is_empty() {
         AnimationState::NoAnimationsPresent
     } else {
--- a/servo/components/layout/context.rs
+++ b/servo/components/layout/context.rs
@@ -10,16 +10,17 @@ use gfx::font_cache_thread::FontCacheThr
 use gfx::font_context::FontContext;
 use heapsize::HeapSizeOf;
 use msg::constellation_msg::PipelineId;
 use net_traits::image_cache::{CanRequestImages, ImageCache, ImageState};
 use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder};
 use opaque_node::OpaqueNodeMethods;
 use parking_lot::RwLock;
 use script_layout_interface::{PendingImage, PendingImageState};
+use script_traits::UntrustedNodeAddress;
 use servo_url::ServoUrl;
 use std::borrow::{Borrow, BorrowMut};
 use std::cell::{RefCell, RefMut};
 use std::collections::HashMap;
 use std::hash::BuildHasherDefault;
 use std::sync::{Arc, Mutex};
 use std::thread;
 use style::context::{SharedStyleContext, ThreadLocalStyleContext};
@@ -91,17 +92,21 @@ pub struct LayoutContext<'a> {
 
     /// A cache of WebRender image info.
     pub webrender_image_cache: Arc<RwLock<HashMap<(ServoUrl, UsePlaceholder),
                                                   WebRenderImageInfo,
                                                   BuildHasherDefault<FnvHasher>>>>,
 
     /// A list of in-progress image loads to be shared with the script thread.
     /// A None value means that this layout was not initiated by the script thread.
-    pub pending_images: Option<Mutex<Vec<PendingImage>>>
+    pub pending_images: Option<Mutex<Vec<PendingImage>>>,
+
+    /// A list of nodes that have just initiated a CSS transition.
+    /// A None value means that this layout was not initiated by the script thread.
+    pub newly_transitioning_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>,
 }
 
 impl<'a> Drop for LayoutContext<'a> {
     fn drop(&mut self) {
         if !thread::panicking() {
             if let Some(ref pending_images) = self.pending_images {
                 assert!(pending_images.lock().unwrap().is_empty());
             }
--- a/servo/components/layout/query.rs
+++ b/servo/components/layout/query.rs
@@ -12,28 +12,26 @@ use euclid::rect::Rect;
 use euclid::size::Size2D;
 use flow::{self, Flow};
 use fragment::{Fragment, FragmentBorderBoxIterator, SpecificFragmentInfo};
 use gfx::display_list::{DisplayItemMetadata, DisplayList, OpaqueNode, ScrollOffsetMap};
 use inline::LAST_FRAGMENT_OF_ELEMENT;
 use ipc_channel::ipc::IpcSender;
 use msg::constellation_msg::PipelineId;
 use opaque_node::OpaqueNodeMethods;
-use script_layout_interface::PendingImage;
 use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse};
 use script_layout_interface::rpc::{HitTestResponse, LayoutRPC};
 use script_layout_interface::rpc::{MarginStyleResponse, NodeGeometryResponse};
 use script_layout_interface::rpc::{NodeOverflowResponse, OffsetParentResponse};
 use script_layout_interface::rpc::{NodeScrollRootIdResponse, ResolvedStyleResponse, TextIndexResponse};
 use script_layout_interface::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode};
 use script_traits::LayoutMsg as ConstellationMsg;
 use script_traits::UntrustedNodeAddress;
 use sequential;
 use std::cmp::{min, max};
-use std::mem;
 use std::ops::Deref;
 use std::sync::{Arc, Mutex};
 use style::computed_values;
 use style::context::{StyleContext, ThreadLocalStyleContext};
 use style::dom::TElement;
 use style::logical_geometry::{WritingMode, BlockFlowDirection, InlineBaseDirection};
 use style::properties::{style_structs, PropertyId, PropertyDeclarationId, LonghandId};
 use style::properties::longhands::{display, position};
@@ -84,19 +82,16 @@ pub struct LayoutThreadData {
     pub margin_style_response: MarginStyleResponse,
 
     /// Scroll offsets of stacking contexts. This will only be populated if WebRender is in use.
     pub stacking_context_scroll_offsets: ScrollOffsetMap,
 
     /// Index in a text fragment. We need this do determine the insertion point.
     pub text_index_response: TextIndexResponse,
 
-    /// A list of images requests that need to be initiated.
-    pub pending_images: Vec<PendingImage>,
-
     /// A queued response for the list of nodes at a given point.
     pub nodes_from_point_response: Vec<UntrustedNodeAddress>,
 }
 
 pub struct LayoutRPCImpl(pub Arc<Mutex<LayoutThreadData>>);
 
 // https://drafts.csswg.org/cssom-view/#overflow-directions
 fn overflow_direction(writing_mode: &WritingMode) -> OverflowDirection {
@@ -193,22 +188,16 @@ impl LayoutRPC for LayoutRPCImpl {
         rw_data.margin_style_response.clone()
     }
 
     fn text_index(&self) -> TextIndexResponse {
         let &LayoutRPCImpl(ref rw_data) = self;
         let rw_data = rw_data.lock().unwrap();
         rw_data.text_index_response.clone()
     }
-
-    fn pending_images(&self) -> Vec<PendingImage> {
-        let &LayoutRPCImpl(ref rw_data) = self;
-        let mut rw_data = rw_data.lock().unwrap();
-        mem::replace(&mut rw_data.pending_images, vec![])
-    }
 }
 
 struct UnioningFragmentBorderBoxIterator {
     node_address: OpaqueNode,
     rect: Option<Rect<Au>>,
 }
 
 impl UnioningFragmentBorderBoxIterator {
--- a/servo/components/layout_thread/lib.rs
+++ b/servo/components/layout_thread/lib.rs
@@ -76,17 +76,18 @@ use layout::wrapper::drop_style_and_layo
 use layout_traits::LayoutThreadFactory;
 use msg::constellation_msg::{FrameId, PipelineId};
 use net_traits::image_cache::{ImageCache, UsePlaceholder};
 use parking_lot::RwLock;
 use profile_traits::mem::{self, Report, ReportKind, ReportsChan};
 use profile_traits::time::{self, TimerMetadata, profile};
 use profile_traits::time::{TimerMetadataFrameType, TimerMetadataReflowType};
 use script::layout_wrapper::{ServoLayoutElement, ServoLayoutDocument, ServoLayoutNode};
-use script_layout_interface::message::{Msg, NewLayoutThreadInfo, Reflow, ReflowQueryType, ScriptReflow};
+use script_layout_interface::message::{Msg, NewLayoutThreadInfo, Reflow, ReflowQueryType};
+use script_layout_interface::message::{ScriptReflow, ReflowComplete};
 use script_layout_interface::reporter::CSSErrorReporter;
 use script_layout_interface::rpc::{LayoutRPC, MarginStyleResponse, NodeOverflowResponse, OffsetParentResponse};
 use script_layout_interface::rpc::TextIndexResponse;
 use script_layout_interface::wrapper_traits::LayoutNode;
 use script_traits::{ConstellationControlMsg, LayoutControlMsg, LayoutMsg as ConstellationMsg};
 use script_traits::{StackingContextScrollState, UntrustedNodeAddress};
 use selectors::Element;
 use servo_config::opts;
@@ -287,16 +288,47 @@ impl LayoutThreadFactory for LayoutThrea
             }
             if let Some(content_process_shutdown_chan) = content_process_shutdown_chan {
                 let _ = content_process_shutdown_chan.send(());
             }
         }).expect("Thread spawning failed");
     }
 }
 
+struct ScriptReflowResult {
+    script_reflow: ScriptReflow,
+    result: RefCell<Option<ReflowComplete>>,
+}
+
+impl Deref for ScriptReflowResult {
+    type Target = ScriptReflow;
+    fn deref(&self) -> &ScriptReflow {
+        &self.script_reflow
+    }
+}
+
+impl ScriptReflowResult {
+    fn new(script_reflow: ScriptReflow) -> ScriptReflowResult {
+        ScriptReflowResult {
+            script_reflow: script_reflow,
+            result: RefCell::new(Some(Default::default())),
+        }
+    }
+}
+
+impl Drop for ScriptReflowResult {
+    fn drop(&mut self) {
+        self.script_reflow.script_join_chan.send(
+            self.result
+                .borrow_mut()
+                .take()
+                .unwrap()).unwrap();
+    }
+}
+
 /// The `LayoutThread` `rw_data` lock must remain locked until the first reflow,
 /// as RPC calls don't make sense until then. Use this in combination with
 /// `LayoutThread::lock_rw_data` and `LayoutThread::return_rw_data`.
 pub enum RWGuard<'a> {
     /// If the lock was previously held, from when the thread started.
     Held(MutexGuard<'a, LayoutThreadData>),
     /// If the lock was just used, and has been returned since there has been
     /// a reflow already.
@@ -471,17 +503,16 @@ impl LayoutThread {
                     scroll_root_id_response: None,
                     scroll_area_response: Rect::zero(),
                     overflow_response: NodeOverflowResponse(None),
                     resolved_style_response: String::new(),
                     offset_parent_response: OffsetParentResponse::empty(),
                     margin_style_response: MarginStyleResponse::empty(),
                     stacking_context_scroll_offsets: HashMap::new(),
                     text_index_response: TextIndexResponse(None),
-                    pending_images: vec![],
                     nodes_from_point_response: vec![],
                 })),
             error_reporter: CSSErrorReporter {
                 pipelineid: id,
                 script_chan: Arc::new(Mutex::new(script_chan)),
             },
             webrender_image_cache:
                 Arc::new(RwLock::new(HashMap::with_hasher(Default::default()))),
@@ -508,17 +539,17 @@ impl LayoutThread {
         while self.handle_request(&mut rw_data) {
             // Loop indefinitely.
         }
     }
 
     // Create a layout context for use in building display lists, hit testing, &c.
     fn build_layout_context<'a>(&'a self,
                                 guards: StylesheetGuards<'a>,
-                                request_images: bool,
+                                script_initiated_layout: bool,
                                 snapshot_map: &'a SnapshotMap)
                                 -> LayoutContext<'a> {
         let thread_local_style_context_creation_data =
             ThreadLocalStyleContextCreationInfo::new(self.new_animations_sender.clone());
 
         LayoutContext {
             id: self.id,
             style_context: SharedStyleContext {
@@ -532,17 +563,18 @@ impl LayoutThread {
                 timer: self.timer.clone(),
                 quirks_mode: self.quirks_mode.unwrap(),
                 traversal_flags: TraversalFlags::empty(),
                 snapshot_map: snapshot_map,
             },
             image_cache: self.image_cache.clone(),
             font_cache_thread: Mutex::new(self.font_cache_thread.clone()),
             webrender_image_cache: self.webrender_image_cache.clone(),
-            pending_images: if request_images { Some(Mutex::new(vec![])) } else { None },
+            pending_images: if script_initiated_layout { Some(Mutex::new(vec![])) } else { None },
+            newly_transitioning_nodes: if script_initiated_layout { Some(Mutex::new(vec![])) } else { None },
         }
     }
 
     /// Receives and dispatches messages from the script and constellation threads
     fn handle_request<'a, 'b>(&mut self, possibly_locked_rw_data: &mut RwData<'a, 'b>) -> bool {
         enum Request {
             FromPipeline(LayoutControlMsg),
             FromScript(Msg),
@@ -609,20 +641,21 @@ impl LayoutThread {
                 self.handle_add_stylesheet(style_info, possibly_locked_rw_data)
             }
             Msg::SetQuirksMode(mode) => self.handle_set_quirks_mode(mode),
             Msg::GetRPC(response_chan) => {
                 response_chan.send(box LayoutRPCImpl(self.rw_data.clone()) as
                                    Box<LayoutRPC + Send>).unwrap();
             },
             Msg::Reflow(data) => {
+                let mut data = ScriptReflowResult::new(data);
                 profile(time::ProfilerCategory::LayoutPerform,
                         self.profiler_metadata(),
                         self.time_profiler_chan.clone(),
-                        || self.handle_reflow(&data, possibly_locked_rw_data));
+                        || self.handle_reflow(&mut data, possibly_locked_rw_data));
             },
             Msg::TickAnimations => self.tick_all_animations(possibly_locked_rw_data),
             Msg::SetStackingContextScrollStates(new_scroll_states) => {
                 self.set_stacking_context_scroll_states(new_scroll_states,
                                                         possibly_locked_rw_data);
             }
             Msg::ReapStyleAndLayoutData(dead_data) => {
                 unsafe {
@@ -948,17 +981,17 @@ impl LayoutThread {
                 builder.finalize(),
                 true);
             self.webrender_api.generate_frame(None);
         });
     }
 
     /// The high-level routine that performs layout threads.
     fn handle_reflow<'a, 'b>(&mut self,
-                             data: &ScriptReflow,
+                             data: &mut ScriptReflowResult,
                              possibly_locked_rw_data: &mut RwData<'a, 'b>) {
         let document = unsafe { ServoLayoutNode::new(&data.document) };
         let document = document.as_document().unwrap();
         self.quirks_mode = Some(document.quirks_mode());
 
         // FIXME(pcwalton): Combine `ReflowGoal` and `ReflowQueryType`. Then remove this assert.
         debug_assert!((data.reflow_info.goal == ReflowGoal::ForDisplay &&
                        data.query_type == ReflowQueryType::NoQuery) ||
@@ -1233,28 +1266,36 @@ impl LayoutThread {
                                                          Some(&data.query_type),
                                                          Some(&document),
                                                          &mut rw_data,
                                                          &mut layout_context);
         }
 
         self.respond_to_query_if_necessary(&data.query_type,
                                            &mut *rw_data,
-                                           &mut layout_context);
+                                           &mut layout_context,
+                                           data.result.borrow_mut().as_mut().unwrap());
     }
 
     fn respond_to_query_if_necessary(&self,
                                      query_type: &ReflowQueryType,
                                      rw_data: &mut LayoutThreadData,
-                                     context: &mut LayoutContext) {
+                                     context: &mut LayoutContext,
+                                     reflow_result: &mut ReflowComplete) {
         let pending_images = match context.pending_images {
             Some(ref pending) => std_mem::replace(&mut *pending.lock().unwrap(), vec![]),
             None => vec![],
         };
-        rw_data.pending_images = pending_images;
+        reflow_result.pending_images = pending_images;
+
+        let newly_transitioning_nodes = match context.newly_transitioning_nodes {
+            Some(ref nodes) => std_mem::replace(&mut *nodes.lock().unwrap(), vec![]),
+            None => vec![],
+        };
+        reflow_result.newly_transitioning_nodes = newly_transitioning_nodes;
 
         let mut root_flow = match self.root_flow.borrow().clone() {
             Some(root_flow) => root_flow,
             None => return,
         };
         let root_flow = FlowRef::deref_mut(&mut root_flow);
         match *query_type {
             ReflowQueryType::ContentBoxQuery(node) => {
@@ -1421,34 +1462,45 @@ impl LayoutThread {
             }
             self.perform_post_style_recalc_layout_passes(&mut root_flow,
                                                          &reflow_info,
                                                          None,
                                                          None,
                                                          &mut *rw_data,
                                                          &mut layout_context);
             assert!(layout_context.pending_images.is_none());
+            assert!(layout_context.newly_transitioning_nodes.is_none());
         }
     }
 
     fn perform_post_style_recalc_layout_passes(&self,
                                                root_flow: &mut FlowRef,
                                                data: &Reflow,
                                                query_type: Option<&ReflowQueryType>,
                                                document: Option<&ServoLayoutDocument>,
                                                rw_data: &mut LayoutThreadData,
                                                context: &mut LayoutContext) {
-        // 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(),
-                                          &self.new_animations_receiver,
-                                          self.id,
-                                          &self.timer);
+        {
+            let mut newly_transitioning_nodes = context
+                .newly_transitioning_nodes
+                .as_ref()
+                .map(|nodes| nodes.lock().unwrap());
+            let newly_transitioning_nodes = newly_transitioning_nodes
+                .as_mut()
+                .map(|nodes| &mut **nodes);
+            // 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(),
+                                              newly_transitioning_nodes,
+                                              &self.new_animations_receiver,
+                                              self.id,
+                                              &self.timer);
+        }
 
         profile(time::ProfilerCategory::LayoutRestyleDamagePropagation,
                 self.profiler_metadata(),
                 self.time_profiler_chan.clone(),
                 || {
             // Call `compute_layout_damage` even in non-incremental mode, because it sets flags
             // that are needed in both incremental and non-incremental traversals.
             let damage = FlowRef::deref_mut(root_flow).compute_layout_damage();
--- a/servo/components/script/dom/document.rs
+++ b/servo/components/script/dom/document.rs
@@ -821,32 +821,35 @@ impl Document {
 
     pub fn dirty_all_nodes(&self) {
         let root = self.upcast::<Node>();
         for node in root.traverse_preorder() {
             node.dirty(NodeDamage::OtherNodeDamage)
         }
     }
 
+    #[allow(unsafe_code)]
     pub fn handle_mouse_event(&self,
                               js_runtime: *mut JSRuntime,
                               button: MouseButton,
                               client_point: Point2D<f32>,
                               mouse_event_type: MouseEventType) {
         let mouse_event_type_string = match mouse_event_type {
             MouseEventType::Click => "click".to_owned(),
             MouseEventType::MouseUp => "mouseup".to_owned(),
             MouseEventType::MouseDown => "mousedown".to_owned(),
         };
         debug!("{}: at {:?}", mouse_event_type_string, client_point);
 
         let node = match self.window.hit_test_query(client_point, false) {
             Some(node_address) => {
                 debug!("node address is {:?}", node_address);
-                node::from_untrusted_node_address(js_runtime, node_address)
+                unsafe {
+                    node::from_untrusted_node_address(js_runtime, node_address)
+                }
             },
             None => return,
         };
 
         let el = match node.downcast::<Element>() {
             Some(el) => Root::from_ref(el),
             None => {
                 let parent = node.GetParentNode();
@@ -983,23 +986,26 @@ impl Document {
                 return;
             }
         }
 
         // Update last_click_info with the time and position of the click.
         *self.last_click_info.borrow_mut() = Some((now, click_pos));
     }
 
+    #[allow(unsafe_code)]
     pub fn handle_touchpad_pressure_event(&self,
                                           js_runtime: *mut JSRuntime,
                                           client_point: Point2D<f32>,
                                           pressure: f32,
                                           phase_now: TouchpadPressurePhase) {
         let node = match self.window.hit_test_query(client_point, false) {
-            Some(node_address) => node::from_untrusted_node_address(js_runtime, node_address),
+            Some(node_address) => unsafe {
+                node::from_untrusted_node_address(js_runtime, node_address)
+            },
             None => return
         };
 
         let el = match node.downcast::<Element>() {
             Some(el) => Root::from_ref(el),
             None => {
                 let parent = node.GetParentNode();
                 match parent.and_then(Root::downcast::<Element>) {
@@ -1084,32 +1090,33 @@ impl Document {
                                           false,
                                           false,
                                           0i16,
                                           None);
         let event = mouse_event.upcast::<Event>();
         event.fire(target);
     }
 
+    #[allow(unsafe_code)]
     pub fn handle_mouse_move_event(&self,
                                    js_runtime: *mut JSRuntime,
                                    client_point: Option<Point2D<f32>>,
                                    prev_mouse_over_target: &MutNullableJS<Element>) {
         let client_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(client_point) => client_point,
         };
 
         let maybe_new_target = self.window.hit_test_query(client_point, true).and_then(|address| {
-            let node = node::from_untrusted_node_address(js_runtime, address);
+            let node = unsafe { node::from_untrusted_node_address(js_runtime, address) };
             node.inclusive_ancestors()
                 .filter_map(Root::downcast::<Element>)
                 .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.
@@ -1181,33 +1188,36 @@ impl Document {
         // Store the current mouse over target for next frame.
         prev_mouse_over_target.set(maybe_new_target.r());
 
         self.window.reflow(ReflowGoal::ForDisplay,
                            ReflowQueryType::NoQuery,
                            ReflowReason::MouseEvent);
     }
 
+    #[allow(unsafe_code)]
     pub fn handle_touch_event(&self,
                               js_runtime: *mut JSRuntime,
                               event_type: TouchEventType,
                               touch_id: TouchId,
                               point: Point2D<f32>)
                               -> TouchEventResult {
         let TouchId(identifier) = touch_id;
 
         let event_name = match event_type {
             TouchEventType::Down => "touchstart",
             TouchEventType::Move => "touchmove",
             TouchEventType::Up => "touchend",
             TouchEventType::Cancel => "touchcancel",
         };
 
         let node = match self.window.hit_test_query(point, false) {
-            Some(node_address) => node::from_untrusted_node_address(js_runtime, node_address),
+            Some(node_address) => unsafe {
+                node::from_untrusted_node_address(js_runtime, node_address)
+            },
             None => return TouchEventResult::Processed(false),
         };
         let el = match node.downcast::<Element>() {
             Some(el) => Root::from_ref(el),
             None => {
                 let parent = node.GetParentNode();
                 match parent.and_then(Root::downcast::<Element>) {
                     Some(parent) => parent,
@@ -3475,17 +3485,19 @@ impl DocumentMethods for Document {
         if x < 0.0 || y < 0.0 || x > viewport.width || y > viewport.height {
             return None;
         }
 
         match self.window.hit_test_query(*point, false) {
             Some(untrusted_node_address) => {
                 let js_runtime = unsafe { JS_GetRuntime(window.get_cx()) };
 
-                let node = node::from_untrusted_node_address(js_runtime, untrusted_node_address);
+                let node = unsafe {
+                    node::from_untrusted_node_address(js_runtime, untrusted_node_address)
+                };
                 let parent_node = node.GetParentNode().unwrap();
                 let element_ref = node.downcast::<Element>().unwrap_or_else(|| {
                     parent_node.downcast::<Element>().unwrap()
                 });
 
                 Some(Root::from_ref(element_ref))
             },
             None => self.GetDocumentElement()
@@ -3510,17 +3522,19 @@ impl DocumentMethods for Document {
             return vec!();
         }
 
         let js_runtime = unsafe { JS_GetRuntime(window.get_cx()) };
 
         // Step 1 and Step 3
         let mut elements: Vec<Root<Element>> = self.nodes_from_point(point).iter()
             .flat_map(|&untrusted_node_address| {
-                let node = node::from_untrusted_node_address(js_runtime, untrusted_node_address);
+                let node = unsafe {
+                    node::from_untrusted_node_address(js_runtime, untrusted_node_address)
+                };
                 Root::downcast::<Element>(node)
         }).collect();
 
         // Step 4
         if let Some(root_element) = self.GetDocumentElement() {
             if elements.last() != Some(&root_element) {
                 elements.push(root_element);
             }
--- a/servo/components/script/dom/node.rs
+++ b/servo/components/script/dom/node.rs
@@ -922,30 +922,28 @@ fn first_node_not_in<I>(mut nodes: I, no
             }
         })
     })
 }
 
 /// If the given untrusted node address represents a valid DOM node in the given runtime,
 /// returns it.
 #[allow(unsafe_code)]
-pub fn from_untrusted_node_address(_runtime: *mut JSRuntime, candidate: UntrustedNodeAddress)
+pub unsafe fn from_untrusted_node_address(_runtime: *mut JSRuntime, candidate: UntrustedNodeAddress)
     -> Root<Node> {
-    unsafe {
-        // https://github.com/servo/servo/issues/6383
-        let candidate: uintptr_t = mem::transmute(candidate.0);
+    // https://github.com/servo/servo/issues/6383
+    let candidate: uintptr_t = mem::transmute(candidate.0);
 //        let object: *mut JSObject = jsfriendapi::bindgen::JS_GetAddressableObject(runtime,
 //                                                                                  candidate);
-        let object: *mut JSObject = mem::transmute(candidate);
-        if object.is_null() {
-            panic!("Attempted to create a `JS<Node>` from an invalid pointer!")
-        }
-        let boxed_node = conversions::private_from_object(object) as *const Node;
-        Root::from_ref(&*boxed_node)
+    let object: *mut JSObject = mem::transmute(candidate);
+    if object.is_null() {
+        panic!("Attempted to create a `JS<Node>` from an invalid pointer!")
     }
+    let boxed_node = conversions::private_from_object(object) as *const Node;
+    Root::from_ref(&*boxed_node)
 }
 
 #[allow(unsafe_code)]
 pub trait LayoutNodeHelpers {
     unsafe fn type_id_for_layout(&self) -> NodeTypeId;
 
     unsafe fn parent_node_ref(&self) -> Option<LayoutJS<Node>>;
     unsafe fn first_child_ref(&self) -> Option<LayoutJS<Node>>;
--- a/servo/components/script/dom/window.rs
+++ b/servo/components/script/dom/window.rs
@@ -71,17 +71,17 @@ use profile_traits::time::ProfilerChan a
 use script_layout_interface::{TrustedNodeAddress, PendingImageState};
 use script_layout_interface::message::{Msg, Reflow, ReflowQueryType, ScriptReflow};
 use script_layout_interface::reporter::CSSErrorReporter;
 use script_layout_interface::rpc::{ContentBoxResponse, ContentBoxesResponse, LayoutRPC};
 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};
 use script_traits::webdriver_msg::{WebDriverJSError, WebDriverJSResult};
 use servo_atoms::Atom;
 use servo_config::opts;
 use servo_config::prefs::PREFS;
 use servo_geometry::{f32_rect_to_au_rect, max_rect};
@@ -1145,16 +1145,17 @@ impl Window {
     /// below). If there is no window size yet, the page is presumed invisible
     /// and no reflow is performed. If reflow is suppressed, no reflow will be
     /// performed for ForDisplay goals.
     ///
     /// TODO(pcwalton): Only wait for style recalc, since we have
     /// 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,
                         reason: ReflowReason) -> bool {
         // Check if we need to unsuppress reflow. Note that this needs to be
         // *before* any early bailouts, or reflow might never be unsuppresed!
         match reason {
             ReflowReason::FirstLoad |
@@ -1208,45 +1209,44 @@ impl Window {
             query_type: query_type,
             dom_count: self.Document().dom_count(),
         };
 
         self.layout_chan.send(Msg::Reflow(reflow)).unwrap();
 
         debug!("script: layout forked");
 
-        match join_port.try_recv() {
+        let complete = match join_port.try_recv() {
             Err(Empty) => {
                 info!("script: waiting on layout");
-                join_port.recv().unwrap();
+                join_port.recv().unwrap()
             }
-            Ok(_) => {}
+            Ok(reflow_complete) => reflow_complete,
             Err(Disconnected) => {
                 panic!("Layout thread failed while script was waiting for a result.");
             }
-        }
+        };
 
         debug!("script: layout joined");
 
         // Pending reflows require display, so only reset the pending reflow count if this reflow
         // was to be displayed.
         if goal == ReflowGoal::ForDisplay {
             self.pending_reflow_count.set(0);
         }
 
         if let Some(marker) = marker {
             self.emit_timeline_marker(marker.end());
         }
 
-        let pending_images = self.layout_rpc.pending_images();
-        for image in pending_images {
+        for image in complete.pending_images {
             let id = image.id;
             let js_runtime = self.js_runtime.borrow();
             let js_runtime = js_runtime.as_ref().unwrap();
-            let node = from_untrusted_node_address(js_runtime.rt(), image.node);
+            let node = unsafe { from_untrusted_node_address(js_runtime.rt(), image.node) };
 
             if let PendingImageState::Unrequested(ref url) = image.state {
                 fetch_image_for_layout(url.clone(), &*node, id, self.image_cache.clone());
             }
 
             let mut images = self.pending_layout_images.borrow_mut();
             let nodes = images.entry(id).or_insert(vec![]);
             if nodes.iter().find(|n| &***n as *const _ == &*node as *const _).is_none() {
@@ -1256,16 +1256,20 @@ impl Window {
                 ROUTER.add_route(responder_listener.to_opaque(), box move |message| {
                     let _ = image_cache_chan.send((pipeline, message.to().unwrap()));
                 });
                 self.image_cache.add_listener(id, ImageResponder::new(responder, id));
                 nodes.push(JS::from_ref(&*node));
             }
         }
 
+        unsafe {
+            ScriptThread::note_newly_transitioning_nodes(complete.newly_transitioning_nodes);
+        }
+
         true
     }
 
     /// Reflows the page if it's possible to do so and the page is dirty. This
     /// method will wait for the layout thread to complete (but see the `TODO`
     /// below). If there is no window size yet, the page is presumed invisible
     /// and no reflow is performed.
     ///
@@ -1450,28 +1454,29 @@ impl Window {
                         ReflowQueryType::ResolvedStyleQuery(element, pseudo, property),
                         ReflowReason::Query) {
             return DOMString::new();
         }
         let ResolvedStyleResponse(resolved) = self.layout_rpc.resolved_style();
         DOMString::from(resolved)
     }
 
+    #[allow(unsafe_code)]
     pub fn offset_parent_query(&self, node: TrustedNodeAddress) -> (Option<Root<Element>>, Rect<Au>) {
         if !self.reflow(ReflowGoal::ForScriptQuery,
                         ReflowQueryType::OffsetParentQuery(node),
                         ReflowReason::Query) {
             return (None, Rect::zero());
         }
 
         let response = self.layout_rpc.offset_parent();
         let js_runtime = self.js_runtime.borrow();
         let js_runtime = js_runtime.as_ref().unwrap();
         let element = response.node_address.and_then(|parent_node_address| {
-            let node = from_untrusted_node_address(js_runtime.rt(), parent_node_address);
+            let node = unsafe { from_untrusted_node_address(js_runtime.rt(), parent_node_address) };
             Root::downcast(node)
         });
         (element, response.rect)
     }
 
     pub fn margin_style_query(&self, node: TrustedNodeAddress) -> MarginStyleResponse {
         if !self.reflow(ReflowGoal::ForScriptQuery,
                         ReflowQueryType::MarginStyleQuery(node),
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -42,17 +42,17 @@ use dom::bindings::trace::JSTraceable;
 use dom::bindings::utils::WRAP_CALLBACKS;
 use dom::document::{Document, DocumentSource, FocusType, HasBrowsingContext, IsHTMLDocument, TouchEventResult};
 use dom::element::Element;
 use dom::event::{Event, EventBubbles, EventCancelable};
 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};
 use dom::transitionevent::TransitionEvent;
 use dom::uievent::UIEvent;
 use dom::window::{ReflowReason, Window};
 use dom::windowproxy::WindowProxy;
 use dom::worker::TrustedWorkerAddress;
@@ -64,17 +64,16 @@ use hyper::mime::{Mime, SubLevel, TopLev
 use hyper_serde::Serde;
 use ipc_channel::ipc::{self, IpcSender};
 use ipc_channel::router::ROUTER;
 use js::glue::GetWindowProxyClass;
 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};
 use net_traits::{CoreResourceMsg, FetchMetadata, FetchResponseListener};
 use net_traits::{IpcSend, Metadata, ReferrerPolicy, ResourceThreads};
 use net_traits::image_cache::{ImageCache, PendingImageResponse};
 use net_traits::request::{CredentialsMode, Destination, RequestInit};
 use net_traits::storage_thread::StorageType;
@@ -104,17 +103,16 @@ use std::option::Option;
 use std::ptr;
 use std::rc::Rc;
 use std::result::Result;
 use std::sync::{Arc, Mutex};
 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;
 use task_source::history_traversal::HistoryTraversalTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::user_interaction::{UserInteractionTask, UserInteractionTaskSource};
 use time::Tm;
 use url::Position;
@@ -485,16 +483,20 @@ pub struct ScriptThread {
     mutation_observers: DOMRefCell<Vec<JS<MutationObserver>>>,
 
     /// A handle to the webvr thread, if available
     webvr_thread: Option<IpcSender<WebVRMsg>>,
 
     /// 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<HashSet<JS<Document>>>,
+
+    /// A list of nodes with in-progress CSS transitions, which roots them for the duration
+    /// of the transition.
+    transitioning_nodes: DOMRefCell<Vec<JS<Node>>>,
 }
 
 /// In the event of thread panic, all data on the stack runs its destructor. However, there
 /// are no reachable, owning pointers to the DOM memory, so it never gets freed by default
 /// when the script thread fails. The ScriptMemoryFailsafe uses the destructor bomb pattern
 /// to forcibly tear down the JS compartments for pages associated with the failing ScriptThread.
 struct ScriptMemoryFailsafe<'a> {
     owner: Option<&'a ScriptThread>,
@@ -569,16 +571,27 @@ impl ScriptThreadFactory for ScriptThrea
             failsafe.neuter();
         }).expect("Thread spawning failed");
 
         (sender, receiver)
     }
 }
 
 impl ScriptThread {
+    pub unsafe fn note_newly_transitioning_nodes(nodes: Vec<UntrustedNodeAddress>) {
+        SCRIPT_THREAD_ROOT.with(|root| {
+            let script_thread = &*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() };
             script_thread.mutation_observers
                 .borrow_mut()
                 .push(JS::from_ref(observer));
         })
     }
@@ -737,16 +750,18 @@ impl ScriptThread {
 
             mutation_observers: Default::default(),
 
             layout_to_constellation_chan: state.layout_to_constellation_chan,
 
             webvr_thread: state.webvr_thread,
 
             docs_with_no_blocking_loads: Default::default(),
+
+            transitioning_nodes: Default::default(),
         }
     }
 
     pub fn get_cx(&self) -> *mut JSContext {
         self.js_runtime.cx()
     }
 
     /// Starts the script thread. After calling this method, the script thread will loop receiving
@@ -1597,21 +1612,39 @@ impl ScriptThread {
         let document = match { self.documents.borrow().find_document(id) } {
             Some(document) => document,
             None => return warn!("Message sent to closed pipeline {}.", id),
         };
         document.run_the_animation_frame_callbacks();
     }
 
     /// 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 = unsafe {
+            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);
 
         if let Some(el) = node.downcast::<Element>() {
             if &*window.GetComputedStyle(el, None).Display() == "none" {
                 return;
             }
--- a/servo/components/script_layout_interface/message.rs
+++ b/servo/components/script_layout_interface/message.rs
@@ -1,23 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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 {OpaqueStyleAndLayoutData, TrustedNodeAddress};
+use {OpaqueStyleAndLayoutData, TrustedNodeAddress, PendingImage};
 use app_units::Au;
 use euclid::point::Point2D;
 use euclid::rect::Rect;
 use gfx_traits::Epoch;
 use ipc_channel::ipc::{IpcReceiver, IpcSender};
 use msg::constellation_msg::PipelineId;
 use net_traits::image_cache::ImageCache;
 use profile_traits::mem::ReportsChan;
 use rpc::LayoutRPC;
-use script_traits::{ConstellationControlMsg, LayoutControlMsg};
+use script_traits::{ConstellationControlMsg, LayoutControlMsg, UntrustedNodeAddress};
 use script_traits::{LayoutMsg as ConstellationMsg, StackingContextScrollState, WindowSizeData};
 use servo_url::ServoUrl;
 use std::sync::Arc;
 use std::sync::mpsc::{Receiver, Sender};
 use style::context::{QuirksMode, ReflowGoal};
 use style::properties::PropertyId;
 use style::selector_parser::PseudoElement;
 use style::stylesheets::Stylesheet;
@@ -104,42 +104,45 @@ pub enum ReflowQueryType {
 /// Information needed for a reflow.
 pub struct Reflow {
     /// The goal of reflow: either to render to the screen or to flush layout info for script.
     pub goal: ReflowGoal,
     ///  A clipping rectangle for the page, an enlarged rectangle containing the viewport.
     pub page_clip_rect: Rect<Au>,
 }
 
+/// Information derived from a layout pass that needs to be returned to the script thread.
+#[derive(Default)]
+pub struct ReflowComplete {
+    /// The list of images that were encountered that are in progress.
+    pub pending_images: Vec<PendingImage>,
+    /// The list of nodes that initiated a CSS transition.
+    pub newly_transitioning_nodes: Vec<UntrustedNodeAddress>,
+}
+
 /// Information needed for a script-initiated reflow.
 pub struct ScriptReflow {
     /// General reflow data.
     pub reflow_info: Reflow,
     /// The document node.
     pub document: TrustedNodeAddress,
     /// The document's list of stylesheets.
     pub document_stylesheets: Vec<::style::stylearc::Arc<Stylesheet>>,
     /// Whether the document's stylesheets have changed since the last script reflow.
     pub stylesheets_changed: bool,
     /// The current window size.
     pub window_size: WindowSizeData,
     /// The channel that we send a notification to.
-    pub script_join_chan: Sender<()>,
+    pub script_join_chan: Sender<ReflowComplete>,
     /// The type of query if any to perform during this reflow.
     pub query_type: ReflowQueryType,
     /// The number of objects in the dom #10110
     pub dom_count: u32,
 }
 
-impl Drop for ScriptReflow {
-    fn drop(&mut self) {
-        self.script_join_chan.send(()).unwrap();
-    }
-}
-
 pub struct NewLayoutThreadInfo {
     pub id: PipelineId,
     pub url: ServoUrl,
     pub is_parent: bool,
     pub layout_pair: (Sender<Msg>, Receiver<Msg>),
     pub pipeline_port: IpcReceiver<LayoutControlMsg>,
     pub constellation_chan: IpcSender<ConstellationMsg>,
     pub script_chan: IpcSender<ConstellationControlMsg>,
--- a/servo/components/script_layout_interface/rpc.rs
+++ b/servo/components/script_layout_interface/rpc.rs
@@ -1,13 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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 PendingImage;
 use app_units::Au;
 use euclid::point::Point2D;
 use euclid::rect::Rect;
 use script_traits::UntrustedNodeAddress;
 use style::properties::longhands::{margin_top, margin_right, margin_bottom, margin_left, overflow_x};
 use webrender_traits::ClipId;
 
 /// Synchronous messages that script can send to layout.
@@ -33,18 +32,16 @@ pub trait LayoutRPC {
     fn node_scroll_root_id(&self) -> NodeScrollRootIdResponse;
     /// Requests the node containing the point of interest
     fn hit_test(&self) -> HitTestResponse;
     /// Query layout for the resolved value of a given CSS property
     fn resolved_style(&self) -> ResolvedStyleResponse;
     fn offset_parent(&self) -> OffsetParentResponse;
     /// Query layout for the resolve values of the margin properties for an element.
     fn margin_style(&self) -> MarginStyleResponse;
-    /// Requests the list of not-yet-loaded images that were encountered in the last reflow.
-    fn pending_images(&self) -> Vec<PendingImage>;
     /// Requests the list of nodes from the given point.
     fn nodes_from_point_response(&self) -> Vec<UntrustedNodeAddress>;
 
     fn text_index(&self) -> TextIndexResponse;
 }
 
 pub struct ContentBoxResponse(pub Option<Rect<Au>>);
 
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -64,17 +64,17 @@ use profile_traits::mem;
 use profile_traits::time as profile_time;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use servo_url::ImmutableOrigin;
 use servo_url::ServoUrl;
 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};
 
 pub use script_msg::{LayoutMsg, ScriptMsg, EventResult, LogEntry};
 pub use script_msg::{ServiceWorkerMsg, ScopeThings, SWManagerMsg, SWManagerSenders, DOMMessage};
 
 /// The address of a node. Layout sends these back. They must be validated via
@@ -269,17 +269,17 @@ pub enum ConstellationControlMsg {
     /// Set an iframe to be focused. Used when an element in an iframe gains focus.
     /// PipelineId is for the parent, FrameId is for the actual frame.
     FocusIFrame(PipelineId, FrameId),
     /// Passes a webdriver command to the script thread for execution
     WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
     /// 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),
     /// Cause a `load` event to be dispatched at the appropriate frame element.
     DispatchFrameLoadEvent {
         /// The frame that has been marked as loaded.
         target: FrameId,
         /// The pipeline that contains a frame loading the target pipeline.
--- a/servo/components/style/animation.rs
+++ b/servo/components/style/animation.rs
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! CSS transitions and animations.
 #![deny(missing_docs)]
 
 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};
 use properties::{self, CascadeFlags, ComputedValues, Importance};
 use properties::animated_properties::{AnimatedProperty, TransitionProperty};
 use properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection;
 use properties::longhands::animation_iteration_count::single_value::computed_value::T as AnimationIterationCount;
 use properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState;
@@ -183,59 +183,68 @@ impl KeyframesAnimationState {
 /// State relating to an animation.
 #[derive(Clone, Debug)]
 pub enum Animation {
     /// A transition is just a single frame triggered at a time, with a reflow.
     ///
     /// 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),
 }
 
 impl Animation {
     /// Mark this animation as expired.
     #[inline]
     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,
         }
     }
 
     /// Whether this animation is expired.
     #[inline]
     pub fn is_expired(&self) -> bool {
         match *self {
-            Animation::Transition(_, _, _, _, expired) => expired,
+            Animation::Transition(_, _, _, expired) => expired,
             Animation::Keyframes(_, _, ref state) => state.expired,
         }
     }
 
     /// The opaque node that owns the animation.
     #[inline]
     pub fn node(&self) -> &OpaqueNode {
         match *self {
-            Animation::Transition(ref node, _, _, _, _) => node,
+            Animation::Transition(ref node, _, _, _) => node,
             Animation::Keyframes(ref node, _, _) => node,
         }
     }
 
     /// Whether this animation is paused. A transition can never be paused.
     #[inline]
     pub fn is_paused(&self) -> bool {
         match *self {
             Animation::Transition(..) => false,
             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,
+        }
+    }
 }
 
 
 /// A single animation frame of a single property.
 #[derive(Debug, Clone)]
 pub struct AnimationFrame {
     /// A description of the property animation that is occurring.
     pub property_animation: PropertyAnimation,
@@ -397,17 +406,16 @@ impl PropertyAnimation {
 /// the given style difference. This is called from the layout worker threads.
 /// Returns true if any animations were kicked off and false otherwise.
 //
 // TODO(emilio): Take rid of this mutex splitting SharedLayoutContex into a
 // cloneable part and a non-cloneable part..
 #[cfg(feature = "servo")]
 pub fn start_transitions_if_applicable(new_animations_sender: &Sender<Animation>,
                                        opaque_node: OpaqueNode,
-                                       unsafe_node: UnsafeNode,
                                        old_style: &ComputedValues,
                                        new_style: &mut Arc<ComputedValues>,
                                        timer: &Timer,
                                        possibly_expired_animations: &[PropertyAnimation])
                                        -> bool {
     let mut had_animations = false;
     for i in 0..new_style.get_box().transition_property_count() {
         // Create any property animations, if applicable.
@@ -431,17 +439,17 @@ pub fn start_transitions_if_applicable(n
             property_animation.update(Arc::get_mut(new_style).unwrap(), 0.0);
 
             // Kick off the animation.
             let box_style = new_style.get_box();
             let now = timer.seconds();
             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();
 
             had_animations = true;
         }
     }
 
@@ -584,17 +592,17 @@ pub fn update_style_for_animation_frame(
 pub fn update_style_for_animation(context: &SharedStyleContext,
                                   animation: &Animation,
                                   style: &mut Arc<ComputedValues>,
                                   font_metrics_provider: &FontMetricsProvider) {
     debug!("update_style_for_animation: entering");
     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();
             let updated_style = update_style_for_animation_frame(&mut new_style,
                                                                  now, start_time,
                                                                  frame);
             if updated_style {
                 *style = new_style
@@ -762,17 +770,17 @@ pub fn complete_expired_transitions(node
     let had_animations_to_expire;
     {
         let all_expired_animations = context.expired_animations.read();
         let animations_to_expire = all_expired_animations.get(&node);
         had_animations_to_expire = animations_to_expire.is_some();
         if let Some(ref animations) = animations_to_expire {
             for animation in *animations {
                 // TODO: support animation-fill-mode
-                if let Animation::Transition(_, _, _, ref frame, _) = *animation {
+                if let Animation::Transition(_, _, ref frame, _) = *animation {
                     frame.property_animation.update(Arc::make_mut(style), 1.0);
                 }
             }
         }
     }
 
     if had_animations_to_expire {
         context.expired_animations.write().remove(&node);
--- a/servo/components/style/matching.rs
+++ b/servo/components/style/matching.rs
@@ -740,17 +740,16 @@ trait PrivateMatchMethods: TElement {
                                           this_opaque, &new_values);
 
         // Trigger transitions if necessary. This will reset `new_values` back
         // to its old value if it did trigger a transition.
         if let Some(ref values) = *old_values {
             animation::start_transitions_if_applicable(
                 new_animations_sender,
                 this_opaque,
-                self.as_node().to_unsafe(),
                 &**values,
                 new_values,
                 &shared_context.timer,
                 &possibly_expired_animations);
         }
     }
 
     /// Computes and applies non-redundant damage.
@@ -838,17 +837,17 @@ trait PrivateMatchMethods: TElement {
                 //
                 // See #12171 and the associated PR for an example where this
                 // happened while debugging other release panic.
                 if !running_animation.is_expired() {
                     animation::update_style_for_animation(context,
                                                           running_animation,
                                                           style,
                                                           font_metrics);
-                    if let Animation::Transition(_, _, _, ref frame, _) = *running_animation {
+                    if let Animation::Transition(_, _, ref frame, _) = *running_animation {
                         possibly_expired_animations.push(frame.property_animation.clone())
                     }
                 }
             }
         }
     }
 
     fn share_style_with_candidate_if_possible(&self,