Do a second pass on the sharing cache to reuse style by rule node identity.

MozReview-Commit-ID: H67j3Sbt3gr
This commit is contained in:
Bobby Holley 2017-09-08 16:03:24 -07:00
parent 7a7070e075
commit 1c9b39a8e8
6 changed files with 122 additions and 8 deletions

View file

@ -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)?;

View file

@ -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 {

View file

@ -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`

View file

@ -46,8 +46,7 @@ pub fn parents_allow_sharing<E>(
// 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;
}

View file

@ -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<E: TElement> StyleSharingCache<E> {
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<E> {
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
})
}
}

View file

@ -53,6 +53,8 @@ struct MatchingResults {
pub struct ResolvedStyle {
/// The style itself.
pub style: Arc<ComputedValues>,
/// 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<ComputedValues>) -> Self {
ResolvedStyle { style }
pub fn new(style: Arc<ComputedValues>, 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,
}
}