diff --git a/components/style/context.rs b/components/style/context.rs index a628ef1377f..f0fadc4bd36 100644 --- a/components/style/context.rs +++ b/components/style/context.rs @@ -17,13 +17,13 @@ use euclid::Size2D; use fnv::FnvHashMap; use font_metrics::FontMetricsProvider; #[cfg(feature = "gecko")] use gecko_bindings::structs; -use matching::StyleSharingCandidateCache; #[cfg(feature = "servo")] use parking_lot::RwLock; #[cfg(feature = "gecko")] use properties::ComputedValues; use selector_parser::SnapshotMap; use selectors::matching::ElementSelectorFlags; #[cfg(feature = "servo")] use servo_config::opts; use shared_lock::StylesheetGuards; +use sharing::StyleSharingCandidateCache; #[cfg(feature = "servo")] use std::collections::HashMap; #[cfg(feature = "gecko")] use std::env; use std::fmt; diff --git a/components/style/lib.rs b/components/style/lib.rs index 98745ef2b3e..c294885aed6 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -121,6 +121,7 @@ pub mod rule_tree; pub mod scoped_tls; pub mod selector_parser; pub mod shared_lock; +pub mod sharing; pub mod stylist; #[cfg(feature = "servo")] #[allow(unsafe_code)] pub mod servo; pub mod sequential; diff --git a/components/style/matching.rs b/components/style/matching.rs index 59f2a19912d..12fb21931ea 100644 --- a/components/style/matching.rs +++ b/components/style/matching.rs @@ -7,14 +7,11 @@ #![allow(unsafe_code)] #![deny(missing_docs)] -use Atom; use atomic_refcell::AtomicRefMut; -use bit_vec::BitVec; -use cache::{LRUCache, LRUCacheMutIterator}; use cascade_info::CascadeInfo; -use context::{CurrentElementInfo, SelectorFlagsMap, SharedStyleContext, StyleContext}; -use data::{ComputedStyle, ElementData, ElementStyles, RestyleData}; -use dom::{AnimationRules, SendElement, TElement, TNode}; +use context::{SelectorFlagsMap, SharedStyleContext, StyleContext}; +use data::{ComputedStyle, ElementData, RestyleData}; +use dom::{AnimationRules, TElement, TNode}; use font_metrics::FontMetricsProvider; use log::LogLevel::Trace; use properties::{CascadeFlags, ComputedValues, SKIP_ROOT_AND_ITEM_BASED_DISPLAY_FIXUP, cascade}; @@ -23,11 +20,10 @@ use restyle_hints::{RESTYLE_CSS_ANIMATIONS, RESTYLE_CSS_TRANSITIONS, RestyleRepl use restyle_hints::{RESTYLE_STYLE_ATTRIBUTE, RESTYLE_SMIL}; use rule_tree::{CascadeLevel, RuleTree, StrongRuleNode}; use selector_parser::{PseudoElement, RestyleDamage, SelectorImpl}; -use selectors::bloom::BloomFilter; use selectors::matching::{ElementSelectorFlags, MatchingContext, MatchingMode, StyleRelations}; use selectors::matching::AFFECTED_BY_PSEUDO_ELEMENTS; use shared_lock::StylesheetGuards; -use sink::ForgetfulSink; +use sharing::{StyleSharingBehavior, StyleSharingResult}; use stylearc::Arc; use stylist::ApplicableDeclarationList; @@ -42,16 +38,6 @@ enum InheritMode { FromPrimaryStyle, } -/// Determines the amount of relations where we're going to share style. -#[inline] -fn relations_are_shareable(relations: &StyleRelations) -> bool { - use selectors::matching::*; - !relations.intersects(AFFECTED_BY_ID_SELECTOR | - AFFECTED_BY_PSEUDO_ELEMENTS | - AFFECTED_BY_STYLE_ATTRIBUTE | - AFFECTED_BY_PRESENTATIONAL_HINTS) -} - /// Represents the result of comparing an element's old and new style. pub struct StyleDifference { /// The resulting damage. @@ -80,336 +66,6 @@ pub enum StyleChange { Changed, } -/// Information regarding a style sharing candidate. -/// -/// Note that this information is stored in TLS and cleared after the traversal, -/// and once here, the style information of the element is immutable, so it's -/// safe to access. -/// -/// TODO: We can stick a lot more info here. -#[derive(Debug)] -struct StyleSharingCandidate { - /// The element. We use SendElement here so that the cache may live in - /// ScopedTLS. - element: SendElement, - /// The cached class names. - class_attributes: Option>, - /// The cached result of matching this entry against the revalidation selectors. - revalidation_match_results: Option, -} - -impl PartialEq> for StyleSharingCandidate { - fn eq(&self, other: &Self) -> bool { - self.element == other.element - } -} - -/// An LRU cache of the last few nodes seen, so that we can aggressively try to -/// reuse their styles. -/// -/// Note that this cache is flushed every time we steal work from the queue, so -/// storing nodes here temporarily is safe. -pub struct StyleSharingCandidateCache { - cache: LRUCache>, -} - -/// A cache miss result. -#[derive(Clone, Debug)] -pub enum CacheMiss { - /// The parents don't match. - Parent, - /// One element was NAC, while the other wasn't. - NativeAnonymousContent, - /// The local name of the element and the candidate don't match. - LocalName, - /// The namespace of the element and the candidate don't match. - Namespace, - /// One of the element or the candidate was a link, but the other one - /// wasn't. - Link, - /// The element and the candidate match different kind of rules. This can - /// only happen in Gecko. - UserAndAuthorRules, - /// The element and the candidate are in a different state. - State, - /// The element had an id attribute, which qualifies for a unique style. - IdAttr, - /// The element had a style attribute, which qualifies for a unique style. - StyleAttr, - /// The element and the candidate class names didn't match. - Class, - /// The presentation hints didn't match. - PresHints, - /// The element and the candidate didn't match the same set of revalidation - /// selectors. - Revalidation, -} - -fn same_computed_values(first: Option, second: Option) -> bool { - let (a, b) = match (first, second) { - (Some(f), Some(s)) => (f, s), - _ => return false, - }; - - let eq = Arc::ptr_eq(a.borrow_data().unwrap().styles().primary.values(), - b.borrow_data().unwrap().styles().primary.values()); - eq -} - -fn element_matches_candidate(element: &E, - candidate: &mut StyleSharingCandidate, - candidate_element: &E, - shared: &SharedStyleContext, - bloom: &BloomFilter, - info: &mut CurrentElementInfo, - selector_flags_map: &mut SelectorFlagsMap) - -> Result { - macro_rules! miss { - ($miss: ident) => { - return Err(CacheMiss::$miss); - } - } - - // Check that we have the same parent, or at least the same pointer identity - // for parent computed style. The latter check allows us to share style - // between cousins if the parents shared style. - let parent = element.parent_element(); - let candidate_parent = candidate_element.parent_element(); - if parent != candidate_parent && !same_computed_values(parent, candidate_parent) { - miss!(Parent) - } - - if element.is_native_anonymous() { - debug_assert!(!candidate_element.is_native_anonymous(), - "Why inserting NAC into the cache?"); - miss!(NativeAnonymousContent) - } - - if *element.get_local_name() != *candidate_element.get_local_name() { - miss!(LocalName) - } - - if *element.get_namespace() != *candidate_element.get_namespace() { - miss!(Namespace) - } - - if element.is_link() != candidate_element.is_link() { - miss!(Link) - } - - if element.matches_user_and_author_rules() != candidate_element.matches_user_and_author_rules() { - miss!(UserAndAuthorRules) - } - - if element.get_state() != candidate_element.get_state() { - miss!(State) - } - - if element.get_id() != candidate_element.get_id() { - miss!(IdAttr) - } - - if element.style_attribute().is_some() { - miss!(StyleAttr) - } - - if !have_same_class(element, candidate, candidate_element) { - miss!(Class) - } - - if has_presentational_hints(element) { - miss!(PresHints) - } - - if !revalidate(element, candidate, candidate_element, - shared, bloom, info, selector_flags_map) { - miss!(Revalidation) - } - - let data = candidate_element.borrow_data().unwrap(); - debug_assert!(element.has_current_styles(&data)); - let current_styles = data.styles(); - - debug!("Sharing style between {:?} and {:?}", element, candidate_element); - - Ok(current_styles.primary.clone()) -} - -fn has_presentational_hints(element: &E) -> bool { - let mut hints = ForgetfulSink::new(); - element.synthesize_presentational_hints_for_legacy_attributes(&mut hints); - !hints.is_empty() -} - -fn have_same_class(element: &E, - candidate: &mut StyleSharingCandidate, - candidate_element: &E) -> bool { - // XXX Efficiency here, I'm only validating ideas. - let mut element_class_attributes = vec![]; - element.each_class(|c| element_class_attributes.push(c.clone())); - - if candidate.class_attributes.is_none() { - let mut attrs = vec![]; - candidate_element.each_class(|c| attrs.push(c.clone())); - candidate.class_attributes = Some(attrs) - } - - element_class_attributes == *candidate.class_attributes.as_ref().unwrap() -} - -#[inline] -fn revalidate(element: &E, - candidate: &mut StyleSharingCandidate, - candidate_element: &E, - shared: &SharedStyleContext, - bloom: &BloomFilter, - info: &mut CurrentElementInfo, - selector_flags_map: &mut SelectorFlagsMap) - -> bool { - // NB: We could avoid matching ancestor selectors entirely (rather than - // just depending on the bloom filter), at the expense of some complexity. - // Gecko bug 1354965 tracks this. - // - // We could also be even more careful about only matching the minimal number - // of revalidation selectors until we find a mismatch. Gecko bug 1355668 - // tracks this. - // - // These potential optimizations may not be worth the complexity. - let stylist = &shared.stylist; - - if info.revalidation_match_results.is_none() { - // It's important to set the selector flags. Otherwise, if we succeed in - // sharing the style, we may not set the slow selector flags for the - // right elements (which may not necessarily be |element|), causing missed - // restyles after future DOM mutations. - // - // Gecko's test_bug534804.html exercises this. A minimal testcase is: - // - // - // - // - // - // - // The style sharing cache will get a hit for the second span. When the - // child span is subsequently removed from the DOM, missing selector - // flags would cause us to miss the restyle on the second span. - let mut set_selector_flags = |el: &E, flags: ElementSelectorFlags| { - element.apply_selector_flags(selector_flags_map, el, flags); - }; - info.revalidation_match_results = - Some(stylist.match_revalidation_selectors(element, bloom, - &mut set_selector_flags)); - } - - if candidate.revalidation_match_results.is_none() { - candidate.revalidation_match_results = - Some(stylist.match_revalidation_selectors(candidate_element, bloom, - &mut |_, _| {})); - } - - let for_element = info.revalidation_match_results.as_ref().unwrap(); - let for_candidate = candidate.revalidation_match_results.as_ref().unwrap(); - debug_assert!(for_element.len() == for_candidate.len()); - for_element == for_candidate -} - -static STYLE_SHARING_CANDIDATE_CACHE_SIZE: usize = 8; - -impl StyleSharingCandidateCache { - /// Create a new style sharing candidate cache. - pub fn new() -> Self { - StyleSharingCandidateCache { - cache: LRUCache::new(STYLE_SHARING_CANDIDATE_CACHE_SIZE), - } - } - - /// Returns the number of entries in the cache. - pub fn num_entries(&self) -> usize { - self.cache.num_entries() - } - - fn iter_mut(&mut self) -> LRUCacheMutIterator> { - self.cache.iter_mut() - } - - /// Tries to insert an element in the style sharing cache. - /// - /// Fails if we know it should never be in the cache. - pub fn insert_if_possible(&mut self, - element: &E, - style: &Arc, - relations: StyleRelations, - revalidation_match_results: Option) { - let parent = match element.parent_element() { - Some(element) => element, - None => { - debug!("Failing to insert to the cache: no parent element"); - return; - } - }; - - if element.is_native_anonymous() { - debug!("Failing to insert into the cache: NAC"); - return; - } - - // These are things we don't check in the candidate match because they - // are either uncommon or expensive. - if !relations_are_shareable(&relations) { - debug!("Failing to insert to the cache: {:?}", relations); - return; - } - - // Make sure we noted any presentational hints in the StyleRelations. - if cfg!(debug_assertions) { - let mut hints = ForgetfulSink::new(); - element.synthesize_presentational_hints_for_legacy_attributes(&mut hints); - debug_assert!(hints.is_empty(), "Style relations should not be shareable!"); - } - - let box_style = style.get_box(); - if box_style.specifies_transitions() { - debug!("Failing to insert to the cache: transitions"); - return; - } - - if box_style.specifies_animations() { - debug!("Failing to insert to the cache: animations"); - return; - } - - debug!("Inserting into cache: {:?} with parent {:?}", - element, parent); - - self.cache.insert(StyleSharingCandidate { - element: unsafe { SendElement::new(*element) }, - class_attributes: None, - revalidation_match_results: revalidation_match_results, - }); - } - - /// Touch a given index in the style sharing candidate cache. - pub fn touch(&mut self, index: usize) { - self.cache.touch(index); - } - - /// Clear the style sharing candidate cache. - pub fn clear(&mut self) { - self.cache.evict_all() - } -} - -/// The results of attempting to share a style. -pub enum StyleSharingResult { - /// We didn't find anybody to share the style with. - CannotShare, - /// The node's style can be shared. The integer specifies the index in the - /// LRU cache that was hit and the damage that was done. The - /// `ChildCascadeRequirement` indicates whether style changes due to using - /// the shared style mean we need to recascade to children. - StyleWasShared(usize, ChildCascadeRequirement), -} - /// Whether or not newly computed values for an element need to be cascade /// to children. pub enum ChildCascadeRequirement { @@ -838,43 +494,6 @@ trait PrivateMatchMethods: TElement { } } - /// Computes and applies restyle damage. - fn accumulate_damage(&self, - shared_context: &SharedStyleContext, - restyle: Option<&mut RestyleData>, - old_values: Option<&ComputedValues>, - new_values: &Arc, - pseudo: Option<&PseudoElement>) - -> ChildCascadeRequirement { - let restyle = match restyle { - Some(r) => r, - None => return ChildCascadeRequirement::MustCascade, - }; - - let old_values = match old_values { - Some(v) => v, - None => return ChildCascadeRequirement::MustCascade, - }; - - // ::before and ::after are element-backed in Gecko, so they do the - // damage calculation for themselves, when there's an actual pseudo. - let is_existing_before_or_after = - cfg!(feature = "gecko") && - pseudo.map_or(false, |p| p.is_before_or_after()) && - self.existing_style_for_restyle_damage(old_values, pseudo) - .is_some(); - - if is_existing_before_or_after { - return ChildCascadeRequirement::CanSkipCascade; - } - - self.accumulate_damage_for(shared_context, - restyle, - old_values, - new_values, - pseudo) - } - /// Computes and applies non-redundant damage. #[cfg(feature = "gecko")] fn accumulate_damage_for(&self, @@ -966,18 +585,6 @@ trait PrivateMatchMethods: TElement { } } } - - fn share_style_with_candidate_if_possible(&self, - candidate: &mut StyleSharingCandidate, - shared: &SharedStyleContext, - bloom: &BloomFilter, - info: &mut CurrentElementInfo, - selector_flags_map: &mut SelectorFlagsMap) - -> Result { - let candidate_element = *candidate.element; - element_matches_candidate(self, candidate, &candidate_element, - shared, bloom, info, selector_flags_map) - } } fn compute_rule_node(rule_tree: &RuleTree, @@ -992,15 +599,6 @@ fn compute_rule_node(rule_tree: &RuleTree, impl PrivateMatchMethods for E {} -/// Controls whether the style sharing cache is used. -#[derive(Clone, Copy, PartialEq)] -pub enum StyleSharingBehavior { - /// Style sharing allowed. - Allow, - /// Style sharing disallowed. - Disallow, -} - /// The public API that elements expose for selector matching. pub trait MatchMethods : TElement { /// Performs selector matching and property cascading on an element and its @@ -1008,7 +606,8 @@ pub trait MatchMethods : TElement { fn match_and_cascade(&self, context: &mut StyleContext, data: &mut ElementData, - sharing: StyleSharingBehavior) -> ChildCascadeRequirement + sharing: StyleSharingBehavior) + -> ChildCascadeRequirement { // Perform selector matching for the primary style. let mut relations = StyleRelations::empty(); @@ -1323,6 +922,43 @@ pub trait MatchMethods : TElement { } } + /// Computes and applies restyle damage. + fn accumulate_damage(&self, + shared_context: &SharedStyleContext, + restyle: Option<&mut RestyleData>, + old_values: Option<&ComputedValues>, + new_values: &Arc, + pseudo: Option<&PseudoElement>) + -> ChildCascadeRequirement { + let restyle = match restyle { + Some(r) => r, + None => return ChildCascadeRequirement::MustCascade, + }; + + let old_values = match old_values { + Some(v) => v, + None => return ChildCascadeRequirement::MustCascade, + }; + + // ::before and ::after are element-backed in Gecko, so they do the + // damage calculation for themselves, when there's an actual pseudo. + let is_existing_before_or_after = + cfg!(feature = "gecko") && + pseudo.map_or(false, |p| p.is_before_or_after()) && + self.existing_style_for_restyle_damage(old_values, pseudo) + .is_some(); + + if is_existing_before_or_after { + return ChildCascadeRequirement::CanSkipCascade; + } + + self.accumulate_damage_for(shared_context, + restyle, + old_values, + new_values, + pseudo) + } + /// Updates the rule nodes without re-running selector matching, using just /// the rule tree. Returns RulesChanged which indicates whether the rule nodes changed /// and whether the important rules changed. @@ -1407,99 +1043,22 @@ pub trait MatchMethods : TElement { /// system level yet. unsafe fn share_style_if_possible(&self, context: &mut StyleContext, - data: &mut AtomicRefMut) + data: &mut ElementData) -> StyleSharingResult { - if context.shared.options.disable_style_sharing_cache { - debug!("{:?} Cannot share style: style sharing cache disabled", self); - return StyleSharingResult::CannotShare - } - - if self.parent_element().is_none() { - debug!("{:?} Cannot share style: element has style attribute", self); - return StyleSharingResult::CannotShare - } - - if self.is_native_anonymous() { - debug!("{:?} Cannot share style: NAC", self); - return StyleSharingResult::CannotShare; - } - - if self.style_attribute().is_some() { - debug!("{:?} Cannot share style: element has style attribute", self); - return StyleSharingResult::CannotShare - } - - if self.has_attr(&ns!(), &local_name!("id")) { - debug!("{:?} Cannot share style: element has id", self); - return StyleSharingResult::CannotShare - } - - let cache = &mut context.thread_local.style_sharing_candidate_cache; + let shared_context = &context.shared; let current_element_info = - &mut context.thread_local.current_element_info.as_mut().unwrap(); - let bloom = context.thread_local.bloom_filter.filter(); + context.thread_local.current_element_info.as_mut().unwrap(); let selector_flags_map = &mut context.thread_local.selector_flags; - let mut should_clear_cache = false; - for (i, candidate) in cache.iter_mut().enumerate() { - let sharing_result = - self.share_style_with_candidate_if_possible(candidate, - &context.shared, - bloom, - current_element_info, - selector_flags_map); - match sharing_result { - Ok(shared_style) => { - // Yay, cache hit. Share the style. + let bloom_filter = context.thread_local.bloom_filter.filter(); - // Accumulate restyle damage. - debug_assert_eq!(data.has_styles(), data.has_restyle()); - let old_values = data.get_styles_mut() - .and_then(|s| s.primary.values.take()); - let child_cascade_requirement = - self.accumulate_damage(&context.shared, - data.get_restyle_mut(), - old_values.as_ref().map(|v| v.as_ref()), - shared_style.values(), - None); - - // We never put elements with pseudo style into the style - // sharing cache, so we can just mint an ElementStyles - // directly here. - // - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1329361 - let styles = ElementStyles::new(shared_style); - data.set_styles(styles); - - return StyleSharingResult::StyleWasShared(i, child_cascade_requirement) - } - Err(miss) => { - debug!("Cache miss: {:?}", miss); - - // Cache miss, let's see what kind of failure to decide - // whether we keep trying or not. - match miss { - // Cache miss because of parent, clear the candidate cache. - CacheMiss::Parent => { - should_clear_cache = true; - break; - }, - // Too expensive failure, give up, we don't want another - // one of these. - CacheMiss::PresHints | - CacheMiss::Revalidation => break, - _ => {} - } - } - } - } - - debug!("{:?} Cannot share style: {} cache entries", self, cache.num_entries()); - - if should_clear_cache { - cache.clear(); - } - - StyleSharingResult::CannotShare + context.thread_local + .style_sharing_candidate_cache + .share_style_if_possible(shared_context, + current_element_info, + selector_flags_map, + bloom_filter, + *self, + data) } /// Given the old and new style of this element, and whether it's a diff --git a/components/style/sharing/checks.rs b/components/style/sharing/checks.rs new file mode 100644 index 00000000000..5cc5869e509 --- /dev/null +++ b/components/style/sharing/checks.rs @@ -0,0 +1,141 @@ +/* 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/. */ + +//! Different checks done during the style sharing process in order to determine +//! quickly whether it's worth to share style, and whether two different +//! elements can indeed share the same style. + +use context::{CurrentElementInfo, SelectorFlagsMap, SharedStyleContext}; +use dom::TElement; +use matching::MatchMethods; +use selectors::bloom::BloomFilter; +use selectors::matching::{ElementSelectorFlags, StyleRelations}; +use sharing::StyleSharingCandidate; +use sink::ForgetfulSink; +use stylearc::Arc; + +/// Determines, based on the results of selector matching, whether it's worth to +/// try to share style with this element, that is, to try to insert the element +/// in the chache. +#[inline] +pub fn relations_are_shareable(relations: &StyleRelations) -> bool { + use selectors::matching::*; + !relations.intersects(AFFECTED_BY_ID_SELECTOR | + AFFECTED_BY_PSEUDO_ELEMENTS | + AFFECTED_BY_STYLE_ATTRIBUTE | + AFFECTED_BY_PRESENTATIONAL_HINTS) +} + +/// Whether, given two elements, they have pointer-equal computed values. +/// +/// Both elements need to be styled already. +/// +/// This is used to know whether we can share style across cousins (if the two +/// parents have the same style). +pub fn same_computed_values(first: Option, second: Option) -> bool + where E: TElement, +{ + let (a, b) = match (first, second) { + (Some(f), Some(s)) => (f, s), + _ => return false, + }; + + let eq = Arc::ptr_eq(a.borrow_data().unwrap().styles().primary.values(), + b.borrow_data().unwrap().styles().primary.values()); + eq +} + +/// Whether a given element has presentational hints. +/// +/// We consider not worth to share style with an element that has presentational +/// hints, both because implementing the code that compares that the hints are +/// equal is somewhat annoying, and also because it'd be expensive enough. +pub fn has_presentational_hints(element: E) -> bool + where E: TElement, +{ + let mut hints = ForgetfulSink::new(); + element.synthesize_presentational_hints_for_legacy_attributes(&mut hints); + !hints.is_empty() +} + +/// Whether a given element has the same class attribute than a given candidate. +/// +/// We don't try to share style across elements with different class attributes. +pub fn have_same_class(element: E, + candidate: &mut StyleSharingCandidate) + -> bool + where E: TElement, +{ + // XXX Efficiency here, I'm only validating ideas. + let mut element_class_attributes = vec![]; + element.each_class(|c| element_class_attributes.push(c.clone())); + + if candidate.class_attributes.is_none() { + let mut attrs = vec![]; + candidate.element.each_class(|c| attrs.push(c.clone())); + candidate.class_attributes = Some(attrs) + } + + element_class_attributes == *candidate.class_attributes.as_ref().unwrap() +} + +/// Whether a given element and a candidate match the same set of "revalidation" +/// selectors. +/// +/// Revalidation selectors are those that depend on the DOM structure, like +/// :first-child, etc, or on attributes that we don't check off-hand (pretty +/// much every attribute selector except `id` and `class`. +#[inline] +pub fn revalidate(element: E, + candidate: &mut StyleSharingCandidate, + shared_context: &SharedStyleContext, + bloom: &BloomFilter, + info: &mut CurrentElementInfo, + selector_flags_map: &mut SelectorFlagsMap) + -> bool + where E: TElement, +{ + let stylist = &shared_context.stylist; + + if info.revalidation_match_results.is_none() { + // It's important to set the selector flags. Otherwise, if we succeed in + // sharing the style, we may not set the slow selector flags for the + // right elements (which may not necessarily be |element|), causing + // missed restyles after future DOM mutations. + // + // Gecko's test_bug534804.html exercises this. A minimal testcase is: + // + // + // + // + // + // + // The style sharing cache will get a hit for the second span. When the + // child span is subsequently removed from the DOM, missing selector + // flags would cause us to miss the restyle on the second span. + let mut set_selector_flags = |el: &E, flags: ElementSelectorFlags| { + element.apply_selector_flags(selector_flags_map, el, flags); + }; + info.revalidation_match_results = + Some(stylist.match_revalidation_selectors(&element, bloom, + &mut set_selector_flags)); + } + + if candidate.revalidation_match_results.is_none() { + let results = + stylist.match_revalidation_selectors(&*candidate.element, bloom, + &mut |_, _| {}); + candidate.revalidation_match_results = Some(results); + } + + let for_element = info.revalidation_match_results.as_ref().unwrap(); + let for_candidate = candidate.revalidation_match_results.as_ref().unwrap(); + + // This assert "ensures", to some extent, that the two candidates have + // matched the same rulehash buckets, and as such, that the bits we're + // comparing represent the same set of selectors. + debug_assert_eq!(for_element.len(), for_candidate.len()); + + for_element == for_candidate +} diff --git a/components/style/sharing/mod.rs b/components/style/sharing/mod.rs new file mode 100644 index 00000000000..744571ca863 --- /dev/null +++ b/components/style/sharing/mod.rs @@ -0,0 +1,384 @@ +/* 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/. */ + +//! Code related to the style sharing cache, an optimization that allows similar +//! nodes to share style without having to run selector matching twice. + +use Atom; +use bit_vec::BitVec; +use cache::{LRUCache, LRUCacheMutIterator}; +use context::{CurrentElementInfo, SelectorFlagsMap, SharedStyleContext}; +use data::{ComputedStyle, ElementData, ElementStyles}; +use dom::{TElement, SendElement}; +use matching::{ChildCascadeRequirement, MatchMethods}; +use properties::ComputedValues; +use selectors::bloom::BloomFilter; +use selectors::matching::StyleRelations; +use sink::ForgetfulSink; + +mod checks; + +/// The amount of nodes that the style sharing candidate cache should hold at +/// most. +pub const STYLE_SHARING_CANDIDATE_CACHE_SIZE: usize = 8; + +/// Controls whether the style sharing cache is used. +#[derive(Clone, Copy, PartialEq)] +pub enum StyleSharingBehavior { + /// Style sharing allowed. + Allow, + /// Style sharing disallowed. + Disallow, +} + +/// Information regarding a style sharing candidate, that is, an entry in the +/// style sharing cache. +/// +/// Note that this information is stored in TLS and cleared after the traversal, +/// and once here, the style information of the element is immutable, so it's +/// safe to access. +/// +/// TODO: We can stick a lot more info here. +#[derive(Debug)] +pub struct StyleSharingCandidate { + /// The element. We use SendElement here so that the cache may live in + /// ScopedTLS. + element: SendElement, + /// The cached class names. + class_attributes: Option>, + /// The cached result of matching this entry against the revalidation selectors. + revalidation_match_results: Option, +} + +impl PartialEq> for StyleSharingCandidate { + fn eq(&self, other: &Self) -> bool { + self.element == other.element + } +} + +/// A cache miss result. +#[derive(Clone, Debug)] +pub enum CacheMiss { + /// The parents don't match. + Parent, + /// One element was NAC, while the other wasn't. + NativeAnonymousContent, + /// The local name of the element and the candidate don't match. + LocalName, + /// The namespace of the element and the candidate don't match. + Namespace, + /// One of the element or the candidate was a link, but the other one + /// wasn't. + Link, + /// The element and the candidate match different kind of rules. This can + /// only happen in Gecko. + UserAndAuthorRules, + /// The element and the candidate are in a different state. + State, + /// The element had an id attribute, which qualifies for a unique style. + IdAttr, + /// The element had a style attribute, which qualifies for a unique style. + StyleAttr, + /// The element and the candidate class names didn't match. + Class, + /// The presentation hints didn't match. + PresHints, + /// The element and the candidate didn't match the same set of revalidation + /// selectors. + Revalidation, +} + +/// The results of attempting to share a style. +pub enum StyleSharingResult { + /// We didn't find anybody to share the style with. + CannotShare, + /// The node's style can be shared. The integer specifies the index in the + /// LRU cache that was hit and the damage that was done. The + /// `ChildCascadeRequirement` indicates whether style changes due to using + /// the shared style mean we need to recascade to children. + StyleWasShared(usize, ChildCascadeRequirement), +} + +/// An LRU cache of the last few nodes seen, so that we can aggressively try to +/// reuse their styles. +/// +/// Note that this cache is flushed every time we steal work from the queue, so +/// storing nodes here temporarily is safe. +pub struct StyleSharingCandidateCache { + cache: LRUCache>, +} + +impl StyleSharingCandidateCache { + /// Create a new style sharing candidate cache. + pub fn new() -> Self { + StyleSharingCandidateCache { + cache: LRUCache::new(STYLE_SHARING_CANDIDATE_CACHE_SIZE), + } + } + + /// Returns the number of entries in the cache. + pub fn num_entries(&self) -> usize { + self.cache.num_entries() + } + + fn iter_mut(&mut self) -> LRUCacheMutIterator> { + self.cache.iter_mut() + } + + /// Tries to insert an element in the style sharing cache. + /// + /// Fails if we know it should never be in the cache. + pub fn insert_if_possible(&mut self, + element: &E, + style: &ComputedValues, + relations: StyleRelations, + revalidation_match_results: Option) { + let parent = match element.parent_element() { + Some(element) => element, + None => { + debug!("Failing to insert to the cache: no parent element"); + return; + } + }; + + if element.is_native_anonymous() { + debug!("Failing to insert into the cache: NAC"); + return; + } + + // These are things we don't check in the candidate match because they + // are either uncommon or expensive. + if !checks::relations_are_shareable(&relations) { + debug!("Failing to insert to the cache: {:?}", relations); + return; + } + + // Make sure we noted any presentational hints in the StyleRelations. + if cfg!(debug_assertions) { + let mut hints = ForgetfulSink::new(); + element.synthesize_presentational_hints_for_legacy_attributes(&mut hints); + debug_assert!(hints.is_empty(), + "Style relations should not be shareable!"); + } + + let box_style = style.get_box(); + if box_style.specifies_transitions() { + debug!("Failing to insert to the cache: transitions"); + return; + } + + if box_style.specifies_animations() { + debug!("Failing to insert to the cache: animations"); + return; + } + + debug!("Inserting into cache: {:?} with parent {:?}", element, parent); + + self.cache.insert(StyleSharingCandidate { + element: unsafe { SendElement::new(*element) }, + class_attributes: None, + revalidation_match_results: revalidation_match_results, + }); + } + + /// Touch a given index in the style sharing candidate cache. + pub fn touch(&mut self, index: usize) { + self.cache.touch(index); + } + + /// Clear the style sharing candidate cache. + pub fn clear(&mut self) { + self.cache.evict_all() + } + + /// Attempts to share a style with another node. + /// + /// This method is unsafe because it depends on the + /// `style_sharing_candidate_cache` having only live nodes in it, and we + /// have no way to guarantee that at the type system level yet. + pub unsafe fn share_style_if_possible( + &mut self, + shared_context: &SharedStyleContext, + current_element_info: &mut CurrentElementInfo, + selector_flags_map: &mut SelectorFlagsMap, + bloom_filter: &BloomFilter, + element: E, + data: &mut ElementData + ) -> StyleSharingResult { + if shared_context.options.disable_style_sharing_cache { + debug!("{:?} Cannot share style: style sharing cache disabled", + element); + return StyleSharingResult::CannotShare + } + + if element.parent_element().is_none() { + debug!("{:?} Cannot share style: element has no parent", element); + return StyleSharingResult::CannotShare + } + + if element.is_native_anonymous() { + debug!("{:?} Cannot share style: NAC", element); + return StyleSharingResult::CannotShare; + } + + if element.style_attribute().is_some() { + debug!("{:?} Cannot share style: element has style attribute", + element); + return StyleSharingResult::CannotShare + } + + if element.get_id().is_some() { + debug!("{:?} Cannot share style: element has id", element); + return StyleSharingResult::CannotShare + } + + let mut should_clear_cache = false; + for (i, candidate) in self.iter_mut().enumerate() { + let sharing_result = + Self::test_candidate( + element, + candidate, + &shared_context, + bloom_filter, + current_element_info, + selector_flags_map + ); + + match sharing_result { + Ok(shared_style) => { + // Yay, cache hit. Share the style. + + // Accumulate restyle damage. + debug_assert_eq!(data.has_styles(), data.has_restyle()); + let old_values = data.get_styles_mut() + .and_then(|s| s.primary.values.take()); + let child_cascade_requirement = + element.accumulate_damage( + &shared_context, + data.get_restyle_mut(), + old_values.as_ref().map(|v| &**v), + shared_style.values(), + None + ); + + // We never put elements with pseudo style into the style + // sharing cache, so we can just mint an ElementStyles + // directly here. + // + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1329361 + let styles = ElementStyles::new(shared_style); + data.set_styles(styles); + + return StyleSharingResult::StyleWasShared(i, child_cascade_requirement) + } + Err(miss) => { + debug!("Cache miss: {:?}", miss); + + // Cache miss, let's see what kind of failure to decide + // whether we keep trying or not. + match miss { + // Cache miss because of parent, clear the candidate cache. + CacheMiss::Parent => { + should_clear_cache = true; + break; + }, + // Too expensive failure, give up, we don't want another + // one of these. + CacheMiss::PresHints | + CacheMiss::Revalidation => break, + _ => {} + } + } + } + } + + debug!("{:?} Cannot share style: {} cache entries", element, + self.cache.num_entries()); + + if should_clear_cache { + self.clear(); + } + + StyleSharingResult::CannotShare + } + + fn test_candidate(element: E, + candidate: &mut StyleSharingCandidate, + shared: &SharedStyleContext, + bloom: &BloomFilter, + info: &mut CurrentElementInfo, + selector_flags_map: &mut SelectorFlagsMap) + -> Result { + macro_rules! miss { + ($miss: ident) => { + return Err(CacheMiss::$miss); + } + } + + // Check that we have the same parent, or at least the same pointer + // identity for parent computed style. The latter check allows us to + // share style between cousins if the parents shared style. + let parent = element.parent_element(); + let candidate_parent = candidate.element.parent_element(); + if parent != candidate_parent && + !checks::same_computed_values(parent, candidate_parent) { + miss!(Parent) + } + + if element.is_native_anonymous() { + debug_assert!(!candidate.element.is_native_anonymous(), + "Why inserting NAC into the cache?"); + miss!(NativeAnonymousContent) + } + + if *element.get_local_name() != *candidate.element.get_local_name() { + miss!(LocalName) + } + + if *element.get_namespace() != *candidate.element.get_namespace() { + miss!(Namespace) + } + + if element.is_link() != candidate.element.is_link() { + miss!(Link) + } + + if element.matches_user_and_author_rules() != + candidate.element.matches_user_and_author_rules() { + miss!(UserAndAuthorRules) + } + + if element.get_state() != candidate.element.get_state() { + miss!(State) + } + + if element.get_id() != candidate.element.get_id() { + miss!(IdAttr) + } + + if element.style_attribute().is_some() { + miss!(StyleAttr) + } + + if !checks::have_same_class(element, candidate) { + miss!(Class) + } + + if checks::has_presentational_hints(element) { + miss!(PresHints) + } + + if !checks::revalidate(element, candidate, shared, bloom, info, + selector_flags_map) { + miss!(Revalidation) + } + + let data = candidate.element.borrow_data().unwrap(); + debug_assert!(element.has_current_styles(&data)); + + debug!("Sharing style between {:?} and {:?}", + element, candidate.element); + Ok(data.styles().primary.clone()) + } +} diff --git a/components/style/traversal.rs b/components/style/traversal.rs index 6ce7d9d4b18..f1f1a26342d 100644 --- a/components/style/traversal.rs +++ b/components/style/traversal.rs @@ -8,9 +8,10 @@ use atomic_refcell::{AtomicRefCell, AtomicRefMut}; use context::{SharedStyleContext, StyleContext, ThreadLocalStyleContext}; use data::{ElementData, ElementStyles, StoredRestyleHint}; use dom::{DirtyDescendants, NodeInfo, OpaqueNode, TElement, TNode}; -use matching::{ChildCascadeRequirement, MatchMethods, StyleSharingBehavior}; +use matching::{ChildCascadeRequirement, MatchMethods}; use restyle_hints::{HintComputationContext, RestyleHint}; use selector_parser::RestyleDamage; +use sharing::StyleSharingBehavior; #[cfg(feature = "servo")] use servo_config::opts; use std::borrow::BorrowMut; @@ -723,7 +724,7 @@ fn compute_style(_traversal: &D, D: DomTraversal, { use data::RestyleKind::*; - use matching::StyleSharingResult::*; + use sharing::StyleSharingResult::*; context.thread_local.statistics.elements_styled += 1; let kind = data.restyle_kind();