servo/components/style/traversal.rs
Bobby Holley 80460cc549 Make Restyle tracking more granular.
The primary idea of this patch is to ditch the rigid enum of Previous/Current
styles, and replace it with a series of indicators for the various types of
work that needs to be performed (expanding snapshots, rematching, recascading,
and damage processing). This loses us a little bit of sanity checking (since
the up-to-date-ness of our style is no longer baked into the type system), but
gives us a lot more flexibility that we'll need going forward (especially when
we separate matching from cascading). We also eliminate get_styling_mode in
favor of a method on the traversal.

This patch does a few other things as ridealongs:
* Temporarily eliminates the handling for transfering ownership of styles to the
  frame. We'll need this again at some point, but for now it's causing too much
  complexity for a half-implemented feature.
* Ditches TRestyleDamage, which is no longer necessary post-crate-merge, and is
  a constant source of compilation failures from either needing to be imported
  or being unnecessarily imported (which varies between gecko and servo).
* Expands Snapshots for the traversal root, which was missing before.
* Fixes up the skip_root stuff to avoid visiting the skipped root.
* Unifies parallel traversal and avoids spawning for a single work item.
* Adds an explicit pre_traverse step do any pre-processing and determine whether
  we need to traverse at all.

MozReview-Commit-ID: IKhLAkAigXE
2016-12-09 14:46:40 -10: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,
skip_root: bool,
}
impl PreTraverseToken {
pub fn should_traverse(&self) -> bool {
self.traverse
}
pub fn should_skip_root(&self) -> bool {
self.skip_root
}
}
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 skip_root parameter is used in Gecko to style newly-appended children
/// without restyling the parent.
fn pre_traverse(root: N::ConcreteElement, stylist: &Stylist, skip_root: bool)
-> PreTraverseToken
{
// If we should skip the root, traverse unconditionally.
if skip_root {
return PreTraverseToken {
traverse: true,
skip_root: 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. This is fine, because we don't traverse roots
// with siblings.
debug_assert!(root.next_sibling_element().is_none());
if let Some(mut data) = root.mutate_data() {
if let Some(r) = data.as_restyle_mut() {
let _later_siblings = r.expand_snapshot(root, stylist);
}
}
PreTraverseToken {
traverse: Self::node_needs_traversal(root.as_node()),
skip_root: 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(); }
}