mirror of
https://github.com/servo/servo.git
synced 2025-08-06 06:00:15 +01:00
Bug 1317016 - Basic infrastructure for RestyleHint-driven traversal.
MozReview-Commit-ID: 7wH5XcILVmX
This commit is contained in:
parent
e1eff691f8
commit
992f7dddf4
35 changed files with 1465 additions and 901 deletions
|
@ -6,13 +6,14 @@
|
|||
|
||||
use atomic_refcell::{AtomicRefCell, AtomicRefMut};
|
||||
use context::{LocalStyleContext, SharedStyleContext, StyleContext};
|
||||
use data::ElementData;
|
||||
use dom::{OpaqueNode, StylingMode, TElement, TNode, UnsafeNode};
|
||||
use data::{ElementData, RestyleData};
|
||||
use dom::{OpaqueNode, TElement, TNode, UnsafeNode};
|
||||
use matching::{MatchMethods, StyleSharingResult};
|
||||
use restyle_hints::{RESTYLE_DESCENDANTS, RESTYLE_LATER_SIBLINGS, RESTYLE_SELF};
|
||||
use selectors::bloom::BloomFilter;
|
||||
use selectors::matching::StyleRelations;
|
||||
use std::cell::RefCell;
|
||||
use std::mem;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::atomic::{AtomicUsize, ATOMIC_USIZE_INIT, Ordering};
|
||||
use tid::tid;
|
||||
use util::opts;
|
||||
|
@ -109,7 +110,7 @@ fn insert_ancestors_into_bloom_filter<E>(bf: &mut Box<BloomFilter>,
|
|||
ancestors += 1;
|
||||
|
||||
el.insert_into_bloom_filter(&mut **bf);
|
||||
el = match el.as_node().layout_parent_element(root) {
|
||||
el = match el.layout_parent_element(root) {
|
||||
None => break,
|
||||
Some(p) => p,
|
||||
};
|
||||
|
@ -147,18 +148,6 @@ pub fn remove_from_bloom_filter<'a, N, C>(context: &C, root: OpaqueNode, node: N
|
|||
};
|
||||
}
|
||||
|
||||
pub fn prepare_for_styling<E: TElement>(element: E,
|
||||
data: &AtomicRefCell<ElementData>)
|
||||
-> AtomicRefMut<ElementData> {
|
||||
let mut d = data.borrow_mut();
|
||||
d.gather_previous_styles(|| element.get_styles_from_frame());
|
||||
if d.previous_styles().is_some() {
|
||||
d.ensure_restyle_data();
|
||||
}
|
||||
|
||||
d
|
||||
}
|
||||
|
||||
pub trait DomTraversalContext<N: TNode> {
|
||||
type SharedContext: Sync + 'static;
|
||||
|
||||
|
@ -179,21 +168,29 @@ pub trait DomTraversalContext<N: TNode> {
|
|||
fn needs_postorder_traversal(&self) -> bool { true }
|
||||
|
||||
/// Returns true if traversal should visit the given child.
|
||||
fn should_traverse_child(parent: N::ConcreteElement, child: N) -> bool;
|
||||
fn should_traverse_child(child: N, restyled_previous_element_sibling: bool) -> bool;
|
||||
|
||||
/// 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 we enqueue any children for traversal, we need to set the dirty
|
||||
// descendants bit. Avoid doing it more than once.
|
||||
let mut marked_dirty_descendants = false;
|
||||
use dom::StylingMode::Restyle;
|
||||
|
||||
if parent.is_display_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parallel traversal enqueues all the children before processing any
|
||||
// of them. This means that we need to conservatively enqueue all the
|
||||
// later siblings of an element needing restyle, since computing its
|
||||
// restyle hint may cause us to flag a restyle for later siblings.
|
||||
let mut restyled_element = false;
|
||||
|
||||
for kid in parent.as_node().children() {
|
||||
if Self::should_traverse_child(parent, kid) {
|
||||
if !marked_dirty_descendants {
|
||||
if Self::should_traverse_child(kid, restyled_element) {
|
||||
if kid.as_element().map_or(false, |el| el.styling_mode() == Restyle) {
|
||||
restyled_element = true;
|
||||
unsafe { parent.set_dirty_descendants(); }
|
||||
marked_dirty_descendants = true;
|
||||
}
|
||||
f(kid);
|
||||
}
|
||||
|
@ -208,13 +205,6 @@ pub trait DomTraversalContext<N: TNode> {
|
|||
/// children of |element|.
|
||||
unsafe fn ensure_element_data(element: &N::ConcreteElement) -> &AtomicRefCell<ElementData>;
|
||||
|
||||
/// Sets up the appropriate data structures to style or restyle a node,
|
||||
/// returing a mutable handle to the node data upon which further style
|
||||
/// calculations can be performed.
|
||||
unsafe fn prepare_for_styling(element: &N::ConcreteElement) -> AtomicRefMut<ElementData> {
|
||||
prepare_for_styling(*element, Self::ensure_element_data(element))
|
||||
}
|
||||
|
||||
/// Clears the ElementData attached to this element, if any.
|
||||
///
|
||||
/// This is only safe to call in top-down traversal before processing the
|
||||
|
@ -235,65 +225,40 @@ pub fn relations_are_shareable(relations: &StyleRelations) -> bool {
|
|||
AFFECTED_BY_PRESENTATIONAL_HINTS)
|
||||
}
|
||||
|
||||
pub fn ensure_element_styled<'a, E, C>(element: E,
|
||||
context: &'a C)
|
||||
/// 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>
|
||||
C: StyleContext<'a>,
|
||||
F: Fn(E),
|
||||
{
|
||||
let mut display_none = false;
|
||||
ensure_element_styled_internal(element, context, &mut display_none);
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn ensure_element_styled_internal<'a, E, C>(element: E,
|
||||
context: &'a C,
|
||||
parents_had_display_none: &mut bool)
|
||||
where E: TElement,
|
||||
C: StyleContext<'a>
|
||||
{
|
||||
use properties::longhands::display::computed_value as display;
|
||||
|
||||
// NB: The node data must be initialized here.
|
||||
|
||||
// We need to go to the root and ensure their style is up to date.
|
||||
//
|
||||
// This means potentially a bit of wasted work (usually not much). We could
|
||||
// add a flag at the node at which point we stopped the traversal to know
|
||||
// where should we stop, but let's not add that complication unless needed.
|
||||
let parent = element.parent_element();
|
||||
if let Some(parent) = parent {
|
||||
ensure_element_styled_internal(parent, context, parents_had_display_none);
|
||||
// Check the base case.
|
||||
if element.get_data().is_some() {
|
||||
debug_assert!(element.is_display_none());
|
||||
return element;
|
||||
}
|
||||
|
||||
// Common case: our style is already resolved and none of our ancestors had
|
||||
// display: none.
|
||||
//
|
||||
// We only need to mark whether we have display none, and forget about it,
|
||||
// our style is up to date.
|
||||
if let Some(data) = element.borrow_data() {
|
||||
if let Some(style) = data.get_current_styles().map(|x| &x.primary) {
|
||||
if !*parents_had_display_none {
|
||||
*parents_had_display_none = style.get_box().clone_display() == display::T::none;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Otherwise, our style might be out of date. Time to do selector matching
|
||||
// if appropriate and cascade the node.
|
||||
//
|
||||
// Note that we could add the bloom filter's complexity here, but that's
|
||||
// probably not necessary since we're likely to be matching only a few
|
||||
// nodes, at best.
|
||||
let data = prepare_for_styling(element, element.get_data().unwrap());
|
||||
// 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, data, parent,
|
||||
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.
|
||||
|
@ -307,101 +272,19 @@ pub fn recalc_style_at<'a, E, C, D>(context: &'a C,
|
|||
D: DomTraversalContext<E::ConcreteNode>
|
||||
{
|
||||
// Get the style bloom filter.
|
||||
//
|
||||
// FIXME(bholley): We need to do these even in the StylingMode::Stop case
|
||||
// to handshake with the unconditional pop during servo's bottom-up
|
||||
// traversal. We should avoid doing work here in the Stop case when we
|
||||
// redesign the bloom filter.
|
||||
let mut bf = take_thread_local_bloom_filter(element.parent_element(), root, context.shared_context());
|
||||
|
||||
let mode = element.styling_mode();
|
||||
debug_assert!(mode != StylingMode::Stop, "Parent should not have enqueued us");
|
||||
if mode != StylingMode::Traverse {
|
||||
let mut data = unsafe { D::prepare_for_styling(&element) };
|
||||
let should_compute = element.borrow_data().map_or(true, |d| d.get_current_styles().is_none());
|
||||
debug!("recalc_style_at: {:?} (should_compute={:?} mode={:?}, data={:?})",
|
||||
element, should_compute, element.styling_mode(), element.borrow_data());
|
||||
|
||||
// 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,
|
||||
context.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));
|
||||
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, 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,
|
||||
&element.borrow_data()
|
||||
.unwrap()
|
||||
.current_styles()
|
||||
.primary,
|
||||
relations);
|
||||
}
|
||||
}
|
||||
StyleSharingResult::StyleWasShared(index, damage) => {
|
||||
if opts::get().style_sharing_stats {
|
||||
STYLE_SHARING_CACHE_HITS.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
style_sharing_candidate_cache.touch(index);
|
||||
|
||||
// Drop the mutable borrow early, since Servo's set_restyle_damage also borrows.
|
||||
mem::drop(data);
|
||||
|
||||
element.set_restyle_damage(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if element.is_display_none() {
|
||||
// If this element is display:none, throw away all style data in the subtree.
|
||||
fn clear_descendant_data<E: TElement, D: DomTraversalContext<E::ConcreteNode>>(el: E) {
|
||||
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() {
|
||||
unsafe { D::clear_element_data(&kid) };
|
||||
clear_descendant_data::<_, D>(kid);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
clear_descendant_data::<_, D>(element);
|
||||
} else if mode == StylingMode::Restyle {
|
||||
// If we restyled this node, conservatively mark all our children as needing
|
||||
// processing. The eventual algorithm we're designing does this in a more granular
|
||||
// fashion.
|
||||
for kid in element.as_node().children() {
|
||||
if let Some(kid) = kid.as_element() {
|
||||
unsafe { let _ = D::prepare_for_styling(&kid); }
|
||||
}
|
||||
}
|
||||
if should_compute {
|
||||
compute_style::<_, _, D>(context, element, &*bf);
|
||||
}
|
||||
|
||||
let unsafe_layout_node = element.as_node().to_unsafe();
|
||||
|
@ -414,3 +297,210 @@ pub fn recalc_style_at<'a, E, C, D>(context: &'a C,
|
|||
// NB: flow construction updates the bloom filter on the way up.
|
||||
put_thread_local_bloom_filter(bf, &unsafe_layout_node, context.shared_context());
|
||||
}
|
||||
|
||||
fn compute_style<'a, E, C, D>(context: &'a C,
|
||||
element: E,
|
||||
bloom_filter: &BloomFilter)
|
||||
where E: TElement,
|
||||
C: StyleContext<'a>,
|
||||
D: DomTraversalContext<E::ConcreteNode>
|
||||
{
|
||||
let mut data = unsafe { D::ensure_element_data(&element).borrow_mut() };
|
||||
debug_assert!(!data.is_persistent());
|
||||
|
||||
// Grab a reference to the shared style system state.
|
||||
let stylist = &context.shared_context().stylist;
|
||||
|
||||
// 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,
|
||||
context.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(bloom_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.current_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);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what kind of restyling we need to perform for our children. If
|
||||
// we're performing initial styling on this element, we are also performing
|
||||
// initial styling on any children, so we're done here.
|
||||
if data.is_initial() {
|
||||
return;
|
||||
}
|
||||
debug_assert!(data.is_restyle());
|
||||
|
||||
// If we're restyling this element to display:none, throw away all style data
|
||||
// in the subtree, and return.
|
||||
if data.current_styles().is_display_none() {
|
||||
debug!("New element style is display:none - clearing data from descendants.");
|
||||
clear_descendant_data(element, &|e| unsafe { D::clear_element_data(&e) });
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the hint to propagate. We may modify this during the loop if
|
||||
// we encounter RESTYLE_LATER_SIBLINGS.
|
||||
let mut propagated_hint = data.as_restyle().unwrap().hint.propagate();
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
// Set up our lazy child restyle data.
|
||||
let mut child_data = unsafe { LazyRestyleData::<E, D>::new(&child) };
|
||||
|
||||
// Propagate the parent and sibling restyle hint.
|
||||
if !propagated_hint.is_empty() {
|
||||
child_data.ensure().map(|d| d.hint.insert(&propagated_hint));
|
||||
}
|
||||
|
||||
// Handle element snashots.
|
||||
if child_data.has_snapshot() {
|
||||
// Compute the restyle hint.
|
||||
let mut restyle_data = child_data.ensure().unwrap();
|
||||
let mut hint = stylist.compute_restyle_hint(&child,
|
||||
restyle_data.snapshot.as_ref().unwrap(),
|
||||
child.get_state());
|
||||
|
||||
// If the hint includes a directive for later siblings, strip
|
||||
// it out and modify the base hint for future siblings.
|
||||
if hint.contains(RESTYLE_LATER_SIBLINGS) {
|
||||
hint.remove(RESTYLE_LATER_SIBLINGS);
|
||||
propagated_hint.insert(&(RESTYLE_SELF | RESTYLE_DESCENDANTS).into());
|
||||
}
|
||||
|
||||
// Insert the hint.
|
||||
if !hint.is_empty() {
|
||||
restyle_data.hint.insert(&hint.into());
|
||||
}
|
||||
}
|
||||
|
||||
// If we restyled this node, conservatively mark all our children as
|
||||
// needing a re-cascade. Once we have the rule tree, we will be able
|
||||
// to distinguish between re-matching and re-cascading.
|
||||
if data.is_restyle() {
|
||||
child_data.ensure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(); }
|
||||
}
|
||||
|
||||
/// Various steps in the child preparation algorithm above may cause us to lazily
|
||||
/// instantiate the ElementData on the child. Encapsulate that logic into a
|
||||
/// convenient abstraction.
|
||||
struct LazyRestyleData<'b, E: TElement + 'b, D: DomTraversalContext<E::ConcreteNode>> {
|
||||
data: Option<AtomicRefMut<'b, ElementData>>,
|
||||
element: &'b E,
|
||||
phantom: PhantomData<D>,
|
||||
}
|
||||
|
||||
impl<'b, E: TElement, D: DomTraversalContext<E::ConcreteNode>> LazyRestyleData<'b, E, D> {
|
||||
/// This may lazily instantiate ElementData, and is therefore only safe to
|
||||
/// call on an element for which we have exclusive access.
|
||||
unsafe fn new(element: &'b E) -> Self {
|
||||
LazyRestyleData {
|
||||
data: None,
|
||||
element: element,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure(&mut self) -> Option<&mut RestyleData> {
|
||||
if self.data.is_none() {
|
||||
let mut d = unsafe { D::ensure_element_data(self.element).borrow_mut() };
|
||||
d.restyle();
|
||||
self.data = Some(d);
|
||||
}
|
||||
|
||||
self.data.as_mut().unwrap().as_restyle_mut()
|
||||
}
|
||||
|
||||
/// Checks for the existence of an element snapshot without lazily instantiating
|
||||
/// anything. This allows the traversal to cheaply pass through already-styled
|
||||
/// nodes when they don't need a restyle.
|
||||
fn has_snapshot(&self) -> bool {
|
||||
// If there's no element data, we're done.
|
||||
let raw_data = self.element.get_data();
|
||||
if raw_data.is_none() {
|
||||
debug_assert!(self.data.is_none());
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is element data, we still may not have committed to processing
|
||||
// the node. Carefully get a reference to the data.
|
||||
let maybe_tmp_borrow;
|
||||
let borrow_ref = match self.data {
|
||||
Some(ref d) => d,
|
||||
None => {
|
||||
maybe_tmp_borrow = raw_data.unwrap().borrow_mut();
|
||||
&maybe_tmp_borrow
|
||||
}
|
||||
};
|
||||
|
||||
// Check for a snapshot.
|
||||
borrow_ref.as_restyle().map_or(false, |d| d.snapshot.is_some())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue