servo/components/style/traversal.rs
Bobby Holley 3a56954069 Bug 1322945 - Change skip_root to unstyled_children_only and use StyleNewChildren in more places. r=heycam
I noticed that our current behavior in ContentRangeInserted is incorrect. Unlike
ContentInserted (where this code lived originally), ContentRangeInserted takes a
start and end element. I'm not sure if we ever take that path for new content that
needs style, but it seemed sketchy. And generally, it seems nice to just always
style new content the same way (though we still need to style NAC by the subtree
root, since it hasn't been attached to the parent yet).

For situations where there is indeed only one unstyled child, the traversal
overhead should be neglible, since we special-case the single-element in
parallel.rs to avoid calling into rayon.

Being more explicit about what we want here also makes us more robust against
the other handful of callpaths that can take us into
nsCSSFrameConstructor::{ContentRangeInserted,ContentAppended}. Currently we
can call StyleNewSubtree on an already-styled element via RecreateFramesForContent,
which triggers an assertion in the servo traversal.

MozReview-Commit-ID: DqCGh90deHH
2016-12-12 18:50:33 -08:00

525 lines
20 KiB
Rust

/* 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/. */
//! Traversing the DOM tree; the bloom filter.
use atomic_refcell::{AtomicRefCell, AtomicRefMut};
use bloom::StyleBloom;
use context::{LocalStyleContext, SharedStyleContext, StyleContext};
use data::{ElementData, StoredRestyleHint};
use dom::{OpaqueNode, TElement, TNode};
use matching::{MatchMethods, StyleSharingResult};
use restyle_hints::{RESTYLE_DESCENDANTS, RESTYLE_SELF};
use selector_parser::RestyleDamage;
use selectors::Element;
use selectors::matching::StyleRelations;
use std::borrow::Borrow;
use std::cell::RefCell;
use std::mem;
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
use stylist::Stylist;
use util::opts;
/// Every time we do another layout, the old bloom filters are invalid. This is
/// detected by ticking a generation number every layout.
pub type Generation = u32;
/// Style sharing candidate cache stats. These are only used when
/// `-Z style-sharing-stats` is given.
pub static STYLE_SHARING_CACHE_HITS: AtomicUsize = ATOMIC_USIZE_INIT;
pub static STYLE_SHARING_CACHE_MISSES: AtomicUsize = ATOMIC_USIZE_INIT;
thread_local!(
static STYLE_BLOOM: RefCell<Option<StyleBloom>> = RefCell::new(None));
/// Returns the thread local bloom filter.
///
/// If one does not exist, a new one will be made for you. If it is out of date,
/// it will be cleared and reused.
pub fn take_thread_local_bloom_filter(context: &SharedStyleContext)
-> StyleBloom
{
trace!("{} taking bf", ::tid::tid());
STYLE_BLOOM.with(|style_bloom| {
style_bloom.borrow_mut().take()
.unwrap_or_else(|| StyleBloom::new(context.generation))
})
}
pub fn put_thread_local_bloom_filter(bf: StyleBloom) {
trace!("[{}] putting bloom filter back", ::tid::tid());
STYLE_BLOOM.with(move |style_bloom| {
debug_assert!(style_bloom.borrow().is_none(),
"Putting into a never-taken thread-local bloom filter");
*style_bloom.borrow_mut() = Some(bf);
})
}
/// Remove `element` from the bloom filter if it's the last element we inserted.
///
/// Restores the bloom filter if this is not the root of the reflow.
///
/// This is mostly useful for sequential traversal, where the element will
/// always be the last one.
pub fn remove_from_bloom_filter<'a, E, C>(context: &C, root: OpaqueNode, element: E)
where E: TElement,
C: StyleContext<'a>
{
trace!("[{}] remove_from_bloom_filter", ::tid::tid());
// We may have arrived to `reconstruct_flows` without entering in style
// recalc at all due to our optimizations, nor that it's up to date, so we
// can't ensure there's a bloom filter at all.
let bf = STYLE_BLOOM.with(|style_bloom| {
style_bloom.borrow_mut().take()
});
if let Some(mut bf) = bf {
if context.shared_context().generation == bf.generation() {
bf.maybe_pop(element);
// If we're the root of the reflow, just get rid of the bloom
// filter.
//
// FIXME: We might want to just leave it in TLS? You don't do 4k
// allocations every day. Also, this just clears one thread's bloom
// filter, which is... not great?
if element.as_node().opaque() != root {
put_thread_local_bloom_filter(bf);
}
}
}
}
// NB: Keep this as small as possible, please!
#[derive(Clone, Debug)]
pub struct PerLevelTraversalData {
pub current_dom_depth: Option<usize>,
}
/// This structure exists to enforce that callers invoke pre_traverse, and also
/// to pass information from the pre-traversal into the primary traversal.
pub struct PreTraverseToken {
traverse: bool,
unstyled_children_only: bool,
}
impl PreTraverseToken {
pub fn should_traverse(&self) -> bool {
self.traverse
}
pub fn traverse_unstyled_children_only(&self) -> bool {
self.unstyled_children_only
}
}
pub trait DomTraversalContext<N: TNode> {
type SharedContext: Sync + 'static + Borrow<SharedStyleContext>;
fn new<'a>(&'a Self::SharedContext, OpaqueNode) -> Self;
/// Process `node` on the way down, before its children have been processed.
fn process_preorder(&self, node: N, data: &mut PerLevelTraversalData);
/// Process `node` on the way up, after its children have been processed.
///
/// This is only executed if `needs_postorder_traversal` returns true.
fn process_postorder(&self, node: N);
/// Boolean that specifies whether a bottom up traversal should be
/// performed.
///
/// If it's false, then process_postorder has no effect at all.
fn needs_postorder_traversal() -> bool { true }
/// Must be invoked before traversing the root element to determine whether
/// a traversal is needed. Returns a token that allows the caller to prove
/// that the call happened.
///
/// The unstyled_children_only parameter is used in Gecko to style newly-
/// appended children without restyling the parent.
fn pre_traverse(root: N::ConcreteElement, stylist: &Stylist,
unstyled_children_only: bool)
-> PreTraverseToken
{
if unstyled_children_only {
return PreTraverseToken {
traverse: true,
unstyled_children_only: true,
};
}
// Expand the snapshot, if any. This is normally handled by the parent, so
// we need a special case for the root.
//
// Expanding snapshots here may create a LATER_SIBLINGS restyle hint, which
// we will drop on the floor. To prevent missed restyles, we assert against
// restyling a root with later siblings.
if let Some(mut data) = root.mutate_data() {
if let Some(r) = data.as_restyle_mut() {
debug_assert!(root.next_sibling_element().is_none());
let _later_siblings = r.expand_snapshot(root, stylist);
}
}
PreTraverseToken {
traverse: Self::node_needs_traversal(root.as_node()),
unstyled_children_only: false,
}
}
/// Returns true if traversal should visit a text node. The style system never
/// processes text nodes, but Servo overrides this to visit them for flow
/// construction when necessary.
fn text_node_needs_traversal(node: N) -> bool { debug_assert!(node.is_text_node()); false }
/// Returns true if traversal is needed for the given node and subtree.
fn node_needs_traversal(node: N) -> bool {
// Non-incremental layout visits every node.
if cfg!(feature = "servo") && opts::get().nonincremental_layout {
return true;
}
match node.as_element() {
None => Self::text_node_needs_traversal(node),
Some(el) => {
// If the dirty descendants bit is set, we need to traverse no
// matter what. Skip examining the ElementData.
if el.has_dirty_descendants() {
return true;
}
// Check the element data. If it doesn't exist, we need to visit
// the element.
let data = match el.borrow_data() {
Some(d) => d,
None => return true,
};
// Check what kind of element data we have. If it's Initial or Persistent,
// we're done.
let restyle = match *data {
ElementData::Initial(ref i) => return i.is_none(),
ElementData::Persistent(_) => return false,
ElementData::Restyle(ref r) => r,
};
// Check whether we have any selector matching or re-cascading to
// do in this subtree.
debug_assert!(restyle.snapshot.is_none(), "Snapshots should already be expanded");
if !restyle.hint.is_empty() || restyle.recascade {
return true;
}
// Servo uses the post-order traversal for flow construction, so
// we need to traverse any element with damage so that we can perform
// fixup / reconstruction on our way back up the tree.
if cfg!(feature = "servo") && restyle.damage != RestyleDamage::empty() {
return true;
}
false
},
}
}
/// Helper for the traversal implementations to select the children that
/// should be enqueued for processing.
fn traverse_children<F: FnMut(N)>(parent: N::ConcreteElement, mut f: F)
{
if parent.is_display_none() {
return;
}
for kid in parent.as_node().children() {
if Self::node_needs_traversal(kid) {
let el = kid.as_element();
if el.as_ref().and_then(|el| el.borrow_data())
.map_or(false, |d| d.is_restyle())
{
unsafe { parent.set_dirty_descendants(); }
}
f(kid);
}
}
}
/// Ensures the existence of the ElementData, and returns it. This can't live
/// on TNode because of the trait-based separation between Servo's script
/// and layout crates.
///
/// This is only safe to call in top-down traversal before processing the
/// children of |element|.
unsafe fn ensure_element_data(element: &N::ConcreteElement) -> &AtomicRefCell<ElementData>;
/// Clears the ElementData attached to this element, if any.
///
/// This is only safe to call in top-down traversal before processing the
/// children of |element|.
unsafe fn clear_element_data(element: &N::ConcreteElement);
fn local_context(&self) -> &LocalStyleContext;
}
/// Determines the amount of relations where we're going to share style.
#[inline]
pub fn relations_are_shareable(relations: &StyleRelations) -> bool {
use selectors::matching::*;
!relations.intersects(AFFECTED_BY_ID_SELECTOR |
AFFECTED_BY_PSEUDO_ELEMENTS | AFFECTED_BY_STATE |
AFFECTED_BY_NON_COMMON_STYLE_AFFECTING_ATTRIBUTE_SELECTOR |
AFFECTED_BY_STYLE_ATTRIBUTE |
AFFECTED_BY_PRESENTATIONAL_HINTS)
}
/// Handles lazy resolution of style in display:none subtrees. See the comment
/// at the callsite in query.rs.
pub fn style_element_in_display_none_subtree<'a, E, C, F>(element: E,
init_data: &F,
context: &'a C) -> E
where E: TElement,
C: StyleContext<'a>,
F: Fn(E),
{
// Check the base case.
if element.get_data().is_some() {
debug_assert!(element.is_display_none());
return element;
}
// Ensure the parent is styled.
let parent = element.parent_element().unwrap();
let display_none_root = style_element_in_display_none_subtree(parent, init_data, context);
// Initialize our data.
init_data(element);
// Resolve our style.
let mut data = element.mutate_data().unwrap();
let match_results = element.match_element(context, None);
unsafe {
let shareable = match_results.primary_is_shareable();
element.cascade_node(context, &mut data, Some(parent),
match_results.primary,
match_results.per_pseudo,
shareable);
}
display_none_root
}
/// Calculates the style for a single node.
#[inline]
#[allow(unsafe_code)]
pub fn recalc_style_at<'a, E, C, D>(context: &'a C,
traversal_data: &mut PerLevelTraversalData,
element: E,
mut data: &mut AtomicRefMut<ElementData>)
where E: TElement,
C: StyleContext<'a>,
D: DomTraversalContext<E::ConcreteNode>
{
debug_assert!(data.as_restyle().map_or(true, |r| r.snapshot.is_none()),
"Snapshots should be expanded by the caller");
let compute_self = !data.has_current_styles();
let mut inherited_style_changed = false;
debug!("recalc_style_at: {:?} (compute_self={:?}, dirty_descendants={:?}, data={:?})",
element, compute_self, element.has_dirty_descendants(), data);
// Compute style for this element if necessary.
if compute_self {
inherited_style_changed = compute_style::<_, _, D>(context, &mut data, traversal_data, element);
}
// Now that matching and cascading is done, clear the bits corresponding to
// those operations and compute the propagated restyle hint.
let empty_hint = StoredRestyleHint::empty();
let propagated_hint = match data.as_restyle_mut() {
None => empty_hint,
Some(r) => {
r.recascade = false;
mem::replace(&mut r.hint, empty_hint).propagate()
},
};
debug_assert!(data.has_current_styles());
trace!("propagated_hint={:?}, inherited_style_changed={:?}", propagated_hint, inherited_style_changed);
// Preprocess children, propagating restyle hints and handling sibling relationships.
if !data.styles().is_display_none() &&
(element.has_dirty_descendants() || !propagated_hint.is_empty() || inherited_style_changed) {
preprocess_children::<_, _, D>(context, element, propagated_hint, inherited_style_changed);
}
}
// Computes style, returning true if the inherited styles changed for this
// element.
//
// FIXME(bholley): This should differentiate between matching and cascading,
// since we have separate bits for each now.
fn compute_style<'a, E, C, D>(context: &'a C,
mut data: &mut AtomicRefMut<ElementData>,
traversal_data: &mut PerLevelTraversalData,
element: E) -> bool
where E: TElement,
C: StyleContext<'a>,
D: DomTraversalContext<E::ConcreteNode>,
{
let shared_context = context.shared_context();
let mut bf = take_thread_local_bloom_filter(shared_context);
// Ensure the bloom filter is up to date.
let dom_depth = bf.insert_parents_recovering(element,
traversal_data.current_dom_depth,
shared_context.generation);
// Update the dom depth with the up-to-date dom depth.
//
// Note that this is always the same than the pre-existing depth, but it can
// change from unknown to known at this step.
traversal_data.current_dom_depth = Some(dom_depth);
bf.assert_complete(element);
// Check to see whether we can share a style with someone.
let style_sharing_candidate_cache =
&mut context.local_context().style_sharing_candidate_cache.borrow_mut();
let sharing_result = if element.parent_element().is_none() {
StyleSharingResult::CannotShare
} else {
unsafe { element.share_style_if_possible(style_sharing_candidate_cache,
shared_context, &mut data) }
};
// Otherwise, match and cascade selectors.
match sharing_result {
StyleSharingResult::CannotShare => {
let match_results;
let shareable_element = {
if opts::get().style_sharing_stats {
STYLE_SHARING_CACHE_MISSES.fetch_add(1, Ordering::Relaxed);
}
// Perform the CSS selector matching.
match_results = element.match_element(context, Some(bf.filter()));
if match_results.primary_is_shareable() {
Some(element)
} else {
None
}
};
let relations = match_results.relations;
// Perform the CSS cascade.
unsafe {
let shareable = match_results.primary_is_shareable();
element.cascade_node(context, &mut data,
element.parent_element(),
match_results.primary,
match_results.per_pseudo,
shareable);
}
// Add ourselves to the LRU cache.
if let Some(element) = shareable_element {
style_sharing_candidate_cache.insert_if_possible(&element,
&data.styles().primary.values,
relations);
}
}
StyleSharingResult::StyleWasShared(index) => {
if opts::get().style_sharing_stats {
STYLE_SHARING_CACHE_HITS.fetch_add(1, Ordering::Relaxed);
}
style_sharing_candidate_cache.touch(index);
}
}
// If we're restyling this element to display:none, throw away all style data
// in the subtree, notify the caller to early-return.
let display_none = data.styles().is_display_none();
if display_none {
debug!("New element style is display:none - clearing data from descendants.");
clear_descendant_data(element, &|e| unsafe { D::clear_element_data(&e) });
}
// TODO(emilio): It's pointless to insert the element in the parallel
// traversal, but it may be worth todo it for sequential restyling. What we
// do now is trying to recover it which in that case is really cheap, so
// we'd save a few instructions, but probably not worth given the added
// complexity.
put_thread_local_bloom_filter(bf);
// FIXME(bholley): Compute this accurately from the call to CalcStyleDifference.
let inherited_styles_changed = true;
inherited_styles_changed
}
fn preprocess_children<'a, E, C, D>(context: &'a C,
element: E,
mut propagated_hint: StoredRestyleHint,
parent_inherited_style_changed: bool)
where E: TElement,
C: StyleContext<'a>,
D: DomTraversalContext<E::ConcreteNode>
{
// Loop over all the children.
for child in element.as_node().children() {
// FIXME(bholley): Add TElement::element_children instead of this.
let child = match child.as_element() {
Some(el) => el,
None => continue,
};
let mut child_data = unsafe { D::ensure_element_data(&child).borrow_mut() };
if child_data.is_unstyled_initial() {
continue;
}
let mut restyle_data = match child_data.restyle() {
Some(d) => d,
None => continue,
};
// Propagate the parent and sibling restyle hint.
if !propagated_hint.is_empty() {
restyle_data.hint.insert(&propagated_hint);
}
// Handle element snapshots.
let stylist = &context.shared_context().stylist;
let later_siblings = restyle_data.expand_snapshot(child, stylist);
if later_siblings {
propagated_hint.insert(&(RESTYLE_SELF | RESTYLE_DESCENDANTS).into());
}
// If properties that we inherited from the parent changed, we need to recascade.
//
// FIXME(bholley): Need to handle explicitly-inherited reset properties somewhere.
if parent_inherited_style_changed {
restyle_data.recascade = true;
}
}
}
pub fn clear_descendant_data<E: TElement, F: Fn(E)>(el: E, clear_data: &F) {
for kid in el.as_node().children() {
if let Some(kid) = kid.as_element() {
// We maintain an invariant that, if an element has data, all its ancestors
// have data as well. By consequence, any element without data has no
// descendants with data.
if kid.get_data().is_some() {
clear_data(kid);
clear_descendant_data(kid, clear_data);
}
}
}
unsafe { el.unset_dirty_descendants(); }
}