mirror of
https://github.com/servo/servo.git
synced 2025-07-30 02:30:21 +01:00
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.
This commit is contained in:
parent
f3c8f7e0d0
commit
dabebdfbf5
11 changed files with 126 additions and 25 deletions
|
@ -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<ConstellationMsg>,
|
|||
script_chan: &IpcSender<ConstellationControlMsg>,
|
||||
running_animations: &mut HashMap<OpaqueNode, Vec<Animation>>,
|
||||
expired_animations: &mut HashMap<OpaqueNode, Vec<Animation>>,
|
||||
newly_transitioning_nodes: &mut Vec<UntrustedNodeAddress>,
|
||||
new_animations_receiver: &Receiver<Animation>,
|
||||
pipeline_id: PipelineId,
|
||||
timer: &Timer) {
|
||||
|
@ -71,7 +74,7 @@ pub fn update_animation_state(constellation_chan: &IpcSender<ConstellationMsg>,
|
|||
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<ConstellationMsg>,
|
|||
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<ConstellationMsg>,
|
|||
|
||||
// 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)
|
||||
|
|
|
@ -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<UntrustedNodeAddress>,
|
||||
|
||||
/// A list of nodes that have just started a CSS transition.
|
||||
pub newly_transitioning_nodes: Vec<UntrustedNodeAddress>,
|
||||
}
|
||||
|
||||
pub struct LayoutRPCImpl(pub Arc<Mutex<LayoutThreadData>>);
|
||||
|
@ -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<UntrustedNodeAddress> {
|
||||
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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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
|
||||
|
@ -574,6 +576,17 @@ impl ScriptThreadFactory for ScriptThread {
|
|||
}
|
||||
|
||||
impl ScriptThread {
|
||||
pub fn note_newly_transitioning_nodes(nodes: Vec<UntrustedNodeAddress>) {
|
||||
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);
|
||||
|
|
|
@ -42,6 +42,8 @@ pub trait LayoutRPC {
|
|||
fn pending_images(&self) -> Vec<PendingImage>;
|
||||
/// Requests the list of nodes from the given point.
|
||||
fn nodes_from_point_response(&self) -> Vec<UntrustedNodeAddress>;
|
||||
/// Requests the list of nodes that have just started CSS transitions in the last reflow.
|
||||
fn newly_transitioning_nodes(&self) -> Vec<UntrustedNodeAddress>;
|
||||
|
||||
fn text_index(&self) -> TextIndexResponse;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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<Animation>,
|
||||
opaque_node: OpaqueNode,
|
||||
unsafe_node: UnsafeNode,
|
||||
old_style: &ComputedValues,
|
||||
new_style: &mut Arc<ComputedValues>,
|
||||
timer: &Timer,
|
||||
|
@ -436,7 +444,7 @@ pub fn start_transitions_if_applicable(new_animations_sender: &Sender<Animation>
|
|||
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<ComputedVa
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -745,7 +745,6 @@ trait PrivateMatchMethods: TElement {
|
|||
animation::start_transitions_if_applicable(
|
||||
new_animations_sender,
|
||||
this_opaque,
|
||||
self.as_node().to_unsafe(),
|
||||
&**values,
|
||||
new_values,
|
||||
&shared_context.timer,
|
||||
|
@ -843,7 +842,7 @@ trait PrivateMatchMethods: TElement {
|
|||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19897,6 +19897,12 @@
|
|||
{}
|
||||
]
|
||||
],
|
||||
"mozilla/transitionend_safety.html": [
|
||||
[
|
||||
"/_mozilla/mozilla/transitionend_safety.html",
|
||||
{}
|
||||
]
|
||||
],
|
||||
"mozilla/union.html": [
|
||||
[
|
||||
"/_mozilla/mozilla/union.html",
|
||||
|
@ -31544,6 +31550,10 @@
|
|||
"38d8991444d05c40f1d0168bfce8472e378b603c",
|
||||
"testharness"
|
||||
],
|
||||
"mozilla/transitionend_safety.html": [
|
||||
"778e43b049aa421bad7f86eb03d0955576a84ce0",
|
||||
"testharness"
|
||||
],
|
||||
"mozilla/union.html": [
|
||||
"47ee847e660eb907a7bd916cf37cf3ceba68ea7d",
|
||||
"testharness"
|
||||
|
|
27
tests/wpt/mozilla/tests/mozilla/transitionend_safety.html
Normal file
27
tests/wpt/mozilla/tests/mozilla/transitionend_safety.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Asynchronous transitionend event is not a GC hazard</title>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<body>
|
||||
<script>
|
||||
async_test(function(t) {
|
||||
var elem = document.createElement('div');
|
||||
document.body.appendChild(elem);
|
||||
elem.textContent = 'hi there';
|
||||
elem.style.transition = 'color 10ms';
|
||||
elem.style.color = 'black';
|
||||
elem.ontransitionend = t.step_func_done();
|
||||
|
||||
t.step_timeout(function() {
|
||||
elem.style.color = 'red';
|
||||
|
||||
t.step_timeout(function() {
|
||||
document.body.removeChild(elem);
|
||||
elem = null;
|
||||
window.gc();
|
||||
}, 0);
|
||||
}, 0);
|
||||
}, 'Nodes cannot be GCed while a CSS transition is in effect.');
|
||||
</script>
|
||||
</body>
|
Loading…
Add table
Add a link
Reference in a new issue