diff --git a/components/style/context.rs b/components/style/context.rs index de8c95bb430..b94f7049129 100644 --- a/components/style/context.rs +++ b/components/style/context.rs @@ -313,6 +313,9 @@ pub struct TraversalStatistics { pub elements_matched: u32, /// The number of cache hits from the StyleSharingCache. pub styles_shared: u32, + /// The number of styles reused via rule node comparison from the + /// StyleSharingCache. + pub styles_reused: u32, /// The number of selectors in the stylist. pub selectors: u32, /// The number of revalidation selectors. @@ -347,6 +350,7 @@ impl<'a> ops::Add for &'a TraversalStatistics { elements_styled: self.elements_styled + other.elements_styled, elements_matched: self.elements_matched + other.elements_matched, styles_shared: self.styles_shared + other.styles_shared, + styles_reused: self.styles_reused + other.styles_reused, selectors: 0, revalidation_selectors: 0, dependency_selectors: 0, @@ -374,6 +378,7 @@ impl fmt::Display for TraversalStatistics { writeln!(f, "[PERF],elements_styled,{}", self.elements_styled)?; writeln!(f, "[PERF],elements_matched,{}", self.elements_matched)?; writeln!(f, "[PERF],styles_shared,{}", self.styles_shared)?; + writeln!(f, "[PERF],styles_reused,{}", self.styles_reused)?; writeln!(f, "[PERF],selectors,{}", self.selectors)?; writeln!(f, "[PERF],revalidation_selectors,{}", self.revalidation_selectors)?; writeln!(f, "[PERF],dependency_selectors,{}", self.dependency_selectors)?; diff --git a/components/style/data.rs b/components/style/data.rs index 430737c499e..575d18e55b0 100644 --- a/components/style/data.rs +++ b/components/style/data.rs @@ -22,8 +22,9 @@ use std::ops::{Deref, DerefMut}; use style_resolver::{PrimaryStyle, ResolvedElementStyles, ResolvedStyle}; bitflags! { + /// Various flags stored on ElementData. #[derive(Default)] - flags ElementDataFlags: u8 { + pub flags ElementDataFlags: u8 { /// Whether the styles changed for this restyle. const WAS_RESTYLED = 1 << 0, /// Whether the last traversal of this element did not do @@ -37,6 +38,13 @@ bitflags! { const TRAVERSED_WITHOUT_STYLING = 1 << 1, /// Whether we reframed/reconstructed any ancestor or self. const ANCESTOR_WAS_RECONSTRUCTED = 1 << 2, + /// Whether the primary style of this element data was reused from another + /// element via a rule node comparison. This allows us to differentiate + /// between elements that shared styles because they met all the criteria + /// of the style sharing cache, compared to elements that reused style + /// structs via rule node identity. The former gives us stronger transitive + /// guarantees that allows us to apply the style sharing cache to cousins. + const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 3, } } @@ -202,7 +210,7 @@ pub struct ElementData { pub hint: RestyleHint, /// Flags. - flags: ElementDataFlags, + pub flags: ElementDataFlags, } /// The kind of restyle that a single element should do. @@ -276,11 +284,17 @@ impl ElementData { /// Returns this element's primary style as a resolved style to use for sharing. pub fn share_primary_style(&self) -> PrimaryStyle { - PrimaryStyle(ResolvedStyle::new(self.styles.primary().clone())) + let primary_is_reused = self.flags.contains(PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + PrimaryStyle(ResolvedStyle::new(self.styles.primary().clone(), primary_is_reused)) } /// Sets a new set of styles, returning the old ones. pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles { + if new_styles.primary.0.reused_via_rule_node { + self.flags.insert(PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } else { + self.flags.remove(PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } mem::replace(&mut self.styles, new_styles.into()) } @@ -442,6 +456,28 @@ impl ElementData { #[cfg(feature = "servo")] pub fn skip_applying_damage(&self) -> bool { false } + /// Returns whether it is safe to perform cousin sharing based on the ComputedValues + /// identity of the primary style in this ElementData. There are a few subtle things + /// to check. + /// + /// First, if a parent element was already styled and we traversed past it without + /// restyling it, that may be because our clever invalidation logic was able to prove + /// that the styles of that element would remain unchanged despite changes to the id + /// or class attributes. However, style sharing relies on the strong guarantee that all + /// the classes and ids up the respective parent chains are identical. As such, if we + /// skipped styling for one (or both) of the parents on this traversal, we can't share + /// styles across cousins. Note that this is a somewhat conservative check. We could + /// tighten it by having the invalidation logic explicitly flag elements for which it + /// ellided styling. + /// + /// Second, we want to only consider elements whose ComputedValues match due to a hit + /// in the style sharing cache, rather than due to the rule-node-based reuse that + /// happens later in the styling pipeline. The former gives us the stronger guarantees + /// we need for style sharing, the latter does not. + pub fn safe_for_cousin_sharing(&self) -> bool { + !self.flags.intersects(TRAVERSED_WITHOUT_STYLING | PRIMARY_STYLE_REUSED_VIA_RULE_NODE) + } + /// Measures memory usage. #[cfg(feature = "gecko")] pub fn size_of_excluding_cvs(&self, ops: &mut MallocSizeOfOps) -> usize { diff --git a/components/style/properties/properties.mako.rs b/components/style/properties/properties.mako.rs index 91429401b6c..44da289ca0f 100644 --- a/components/style/properties/properties.mako.rs +++ b/components/style/properties/properties.mako.rs @@ -1997,6 +1997,13 @@ pub struct ComputedValues { inner: ComputedValuesInner, } +impl ComputedValues { + /// Returns the visited rules, if applicable. + pub fn visited_rules(&self) -> Option<<&StrongRuleNode> { + self.visited_style.as_ref().and_then(|s| s.rules.as_ref()) + } +} + #[cfg(feature = "servo")] impl ComputedValues { /// Create a new refcounted `ComputedValues` diff --git a/components/style/sharing/checks.rs b/components/style/sharing/checks.rs index 6ff0bf68f51..d5607590cd3 100644 --- a/components/style/sharing/checks.rs +++ b/components/style/sharing/checks.rs @@ -46,8 +46,7 @@ pub fn parents_allow_sharing( // invalidation logic explicitly flag elements for which it ellided styling. let parent_data = parent.borrow_data().unwrap(); let candidate_parent_data = candidate_parent.borrow_data().unwrap(); - if parent_data.traversed_without_styling() || - candidate_parent_data.traversed_without_styling() { + if !parent_data.safe_for_cousin_sharing() || !candidate_parent_data.safe_for_cousin_sharing() { return false; } diff --git a/components/style/sharing/mod.rs b/components/style/sharing/mod.rs index 435944bb1c7..8046d2339e6 100644 --- a/components/style/sharing/mod.rs +++ b/components/style/sharing/mod.rs @@ -74,6 +74,7 @@ use dom::{TElement, SendElement}; use matching::MatchMethods; use owning_ref::OwningHandle; use properties::ComputedValues; +use rule_tree::StrongRuleNode; use selectors::matching::{ElementSelectorFlags, VisitedHandlingMode}; use servo_arc::{Arc, NonZeroPtrMut}; use smallbitvec::SmallBitVec; @@ -115,6 +116,8 @@ impl OpaqueComputedValues { let p = NonZeroPtrMut::new(cv as *const ComputedValues as *const () as *mut ()); OpaqueComputedValues(p) } + + fn eq(&self, cv: &ComputedValues) -> bool { Self::from(cv) == *self } } /// Some data we want to avoid recomputing all the time while trying to share @@ -730,4 +733,44 @@ impl StyleSharingCache { true } + + /// Attempts to find an element in the cache with the given primary rule node and parent. + pub fn lookup_by_rules( + &mut self, + inherited: &ComputedValues, + rules: &StrongRuleNode, + visited_rules: Option<&StrongRuleNode>, + target: E, + ) -> Option { + self.cache_mut().lookup(|candidate| { + if !candidate.parent_style_identity().eq(inherited) { + return false; + } + let data = candidate.element.borrow_data().unwrap(); + let style = data.styles.primary(); + if style.rules.as_ref() != Some(&rules) { + return false; + } + if style.visited_rules() != visited_rules { + return false; + } + + // Rule nodes and styles are computed independent of the element's + // actual visitedness, but at the end of the cascade (in + // `adjust_for_visited`) we do store the visitedness as a flag in + // style. (This is a subtle change from initial visited work that + // landed when computed values were fused, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1381635.) + // So at the moment, we need to additionally compare visitedness, + // since that is not accounted for by rule nodes alone. + // FIXME(jryans): This seems like it breaks the constant time + // requirements of visited, assuming we get a cache hit on only one + // of unvisited vs. visited. + if target.is_visited_link() != candidate.element.is_visited_link() { + return false; + } + + true + }) + } } diff --git a/components/style/style_resolver.rs b/components/style/style_resolver.rs index 184f095c09e..f236d9dfb1f 100644 --- a/components/style/style_resolver.rs +++ b/components/style/style_resolver.rs @@ -53,6 +53,8 @@ struct MatchingResults { pub struct ResolvedStyle { /// The style itself. pub style: Arc, + /// Whether the style was reused from another element via the rule node. + pub reused_via_rule_node: bool, } /// The primary style of an element or an element-backed pseudo-element. @@ -68,8 +70,8 @@ pub struct ResolvedElementStyles { impl ResolvedStyle { /// Creates a new ResolvedStyle. - pub fn new(style: Arc) -> Self { - ResolvedStyle { style } + pub fn new(style: Arc, reused_via_rule_node: bool) -> Self { + ResolvedStyle { style, reused_via_rule_node } } } @@ -264,6 +266,27 @@ where layout_parent_style: Option<&ComputedValues>, pseudo: Option<&PseudoElement>, ) -> ResolvedStyle { + // Before doing the cascade, check the sharing cache and see if we can reuse + // the style via rule node identity. + let may_reuse = pseudo.is_none() && !self.element.is_native_anonymous() && + parent_style.is_some() && inputs.rules.is_some(); + if may_reuse { + let cached = self.context.thread_local.sharing_cache.lookup_by_rules( + parent_style.unwrap(), + inputs.rules.as_ref().unwrap(), + inputs.visited_rules.as_ref(), + self.element, + ); + if let Some(el) = cached { + self.context.thread_local.statistics.styles_reused += 1; + let mut style = el.borrow_data().unwrap().share_primary_style().0; + style.reused_via_rule_node = true; + return style; + } + } + + // No style to reuse. Cascade the style, starting with visited style + // if necessary. let mut style_if_visited = None; if parent_style.map_or(false, |s| s.get_visited_style().is_some()) || inputs.visited_rules.is_some() { @@ -285,7 +308,8 @@ where layout_parent_style, CascadeVisitedMode::Unvisited, pseudo, - ) + ), + reused_via_rule_node: false, } }