Rewrite the style sharing candidate cache.

The style candidate cache had regressed a few times (see #12534), and my
intuition is that being able to disable all style sharing with a single rule in
the page is really unfortunate.

This commit redesigns the style sharing cache in order to be a optimistic cache,
but then reject candidates if they match different sibling-affecting selectors
in the page, for example.

So far the numbers have improved, but not so much as I'd wanted (~10%/20% of
non-incremental restyling time in general). The current implementation is really
dumb though (we recompute and re-match a lot of stuff), so we should be able to
optimise it quite a bit.

I have different ideas for improving it (that may or may not work), apart of the
low-hanging fruit like don't re-matching candidates all the time but I have to
measure the real impact.

Also, I need to verify it against try.
This commit is contained in:
Emilio Cobos Álvarez 2016-07-29 17:24:12 -07:00
parent ec53136863
commit 3af774bd75
No known key found for this signature in database
GPG key ID: 056B727BB9C1027C
23 changed files with 467 additions and 236 deletions

View file

@ -12,17 +12,19 @@ use cache::{LRUCache, SimpleHashCache};
use cascade_info::CascadeInfo;
use context::{StyleContext, SharedStyleContext};
use data::PrivateStyleData;
use dom::{TElement, TNode, TRestyleDamage};
use dom::{TElement, TNode, TRestyleDamage, UnsafeNode};
use properties::longhands::display::computed_value as display;
use properties::{ComputedValues, PropertyDeclaration, cascade};
use selector_impl::{ElementExt, TheSelectorImpl, PseudoElement};
use selector_matching::{DeclarationBlock, Stylist};
use selectors::bloom::BloomFilter;
use selectors::matching::{StyleRelations, AFFECTED_BY_PSEUDO_ELEMENTS};
use selectors::{Element, MatchAttr};
use sink::ForgetfulSink;
use smallvec::SmallVec;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::fmt;
use std::hash::{BuildHasherDefault, Hash, Hasher};
use std::slice::Iter;
use std::sync::Arc;
@ -178,155 +180,119 @@ impl ApplicableDeclarationsCache {
}
}
/// An LRU cache of the last few nodes seen, so that we can aggressively try to reuse their styles.
/// 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.
///
/// NB: We store UnsafeNode's, but this is not unsafe. It's a shame being
/// generic over elements is unfeasible (you can make compile style without much
/// difficulty, but good luck with layout and all the types with assoc.
/// lifetimes).
pub struct StyleSharingCandidateCache {
cache: LRUCache<StyleSharingCandidate, ()>,
cache: LRUCache<UnsafeNode, ()>,
}
#[derive(Clone)]
pub struct StyleSharingCandidate {
pub style: Arc<ComputedValues>,
pub parent_style: Arc<ComputedValues>,
pub local_name: Atom,
pub classes: Vec<Atom>,
pub namespace: Namespace,
pub common_style_affecting_attributes: CommonStyleAffectingAttributes,
pub link: bool,
#[derive(Clone, Debug)]
pub enum CacheMiss {
Parent,
LocalName,
Namespace,
Link,
State,
IdAttr,
StyleAttr,
Class,
CommonStyleAffectingAttributes,
PresHints,
SiblingRules,
NonCommonAttrRules,
}
impl PartialEq for StyleSharingCandidate {
fn eq(&self, other: &Self) -> bool {
arc_ptr_eq(&self.style, &other.style) &&
arc_ptr_eq(&self.parent_style, &other.parent_style) &&
self.local_name == other.local_name &&
self.classes == other.classes &&
self.link == other.link &&
self.namespace == other.namespace &&
self.common_style_affecting_attributes == other.common_style_affecting_attributes
fn element_matches_candidate<E: TElement>(element: &E,
candidate: &E,
shared_context: &SharedStyleContext)
-> Result<Arc<ComputedValues>, CacheMiss> {
macro_rules! miss {
($miss: ident) => {
return Err(CacheMiss::$miss);
}
}
if element.parent_element() != candidate.parent_element() {
miss!(Parent)
}
if *element.get_local_name() != *candidate.get_local_name() {
miss!(LocalName)
}
if *element.get_namespace() != *candidate.get_namespace() {
miss!(Namespace)
}
if element.is_link() != candidate.is_link() {
miss!(Link)
}
if element.get_state() != candidate.get_state() {
miss!(State)
}
if element.get_id().is_some() {
miss!(IdAttr)
}
if element.style_attribute().is_some() {
miss!(StyleAttr)
}
if !have_same_class(element, candidate) {
miss!(Class)
}
if !have_same_common_style_affecting_attributes(element, candidate) {
miss!(CommonStyleAffectingAttributes)
}
if !have_same_presentational_hints(element, candidate) {
miss!(PresHints)
}
if !match_same_sibling_affecting_rules(element, candidate, shared_context) {
miss!(SiblingRules)
}
if !match_same_not_common_style_affecting_attributes_rules(element, candidate, shared_context) {
miss!(NonCommonAttrRules)
}
let candidate_node = candidate.as_node();
let candidate_style = candidate_node.borrow_data().unwrap().style.as_ref().unwrap().clone();
Ok(candidate_style)
}
impl StyleSharingCandidate {
/// Attempts to create a style sharing candidate from this node. Returns
/// the style sharing candidate or `None` if this node is ineligible for
/// style sharing.
#[allow(unsafe_code)]
fn new<N: TNode>(element: &N::ConcreteElement) -> Option<Self> {
let parent_element = match element.parent_element() {
None => return None,
Some(parent_element) => parent_element,
};
fn have_same_common_style_affecting_attributes<E: TElement>(element: &E,
candidate: &E) -> bool {
// XXX probably could do something smarter. Also, the cache should
// precompute this for the parent. Just experimenting now though.
create_common_style_affecting_attributes_from_element(element) ==
create_common_style_affecting_attributes_from_element(candidate)
}
let style = unsafe {
match element.as_node().borrow_data_unchecked() {
None => return None,
Some(data_ref) => {
match (*data_ref).style {
None => return None,
Some(ref data) => (*data).clone(),
}
}
}
};
let parent_style = unsafe {
match parent_element.as_node().borrow_data_unchecked() {
None => return None,
Some(parent_data_ref) => {
match (*parent_data_ref).style {
None => return None,
Some(ref data) => (*data).clone(),
}
}
}
};
if element.style_attribute().is_some() {
return None
}
let mut classes = Vec::new();
element.each_class(|c| classes.push(c.clone()));
Some(StyleSharingCandidate {
style: style,
parent_style: parent_style,
local_name: element.get_local_name().clone(),
classes: classes,
link: element.is_link(),
namespace: (*element.get_namespace()).clone(),
common_style_affecting_attributes:
create_common_style_affecting_attributes_from_element::<N::ConcreteElement>(&element)
})
fn have_same_presentational_hints<E: TElement>(element: &E, candidate: &E) -> bool {
let mut first = vec![];
element.synthesize_presentational_hints_for_legacy_attributes(&mut first);
if cfg!(debug_assertions) {
let mut second = vec![];
candidate.synthesize_presentational_hints_for_legacy_attributes(&mut second);
debug_assert!(second.is_empty(),
"Should never have inserted an element with preshints in the cache!");
}
pub fn can_share_style_with<E: TElement>(&self, element: &E) -> bool {
if element.get_local_name() != self.local_name.borrow() {
return false
}
let mut num_classes = 0;
let mut classes_match = true;
element.each_class(|c| {
num_classes += 1;
// Note that we could do this check more cheaply if we decided to
// only consider class lists as equal if the orders match, since
// we could then index by num_classes instead of using .contains().
if classes_match && !self.classes.contains(c) {
classes_match = false;
}
});
if !classes_match || num_classes != self.classes.len() {
return false;
}
if element.get_namespace() != self.namespace.borrow() {
return false
}
let mut matching_rules = ForgetfulSink::new();
element.synthesize_presentational_hints_for_legacy_attributes(&mut matching_rules);
if !matching_rules.is_empty() {
return false;
}
// FIXME(pcwalton): It's probably faster to iterate over all the element's attributes and
// use the {common, rare}-style-affecting-attributes tables as lookup tables.
for attribute_info in &common_style_affecting_attributes() {
match attribute_info.mode {
CommonStyleAffectingAttributeMode::IsPresent(flag) => {
if self.common_style_affecting_attributes.contains(flag) !=
element.has_attr(&ns!(), &attribute_info.atom) {
return false
}
}
CommonStyleAffectingAttributeMode::IsEqual(ref target_value, flag) => {
let contains = self.common_style_affecting_attributes.contains(flag);
if element.has_attr(&ns!(), &attribute_info.atom) {
if !contains || !element.attr_equals(&ns!(), &attribute_info.atom, target_value) {
return false
}
} else if contains {
return false
}
}
}
}
for attribute_name in &rare_style_affecting_attributes() {
if element.has_attr(&ns!(), attribute_name) {
return false
}
}
if element.is_link() != self.link {
return false
}
// TODO(pcwalton): We don't support visited links yet, but when we do there will need to
// be some logic here.
true
}
first.is_empty()
}
bitflags! {
@ -384,7 +350,32 @@ pub fn rare_style_affecting_attributes() -> [Atom; 3] {
[ atom!("bgcolor"), atom!("border"), atom!("colspan") ]
}
static STYLE_SHARING_CANDIDATE_CACHE_SIZE: usize = 40;
fn have_same_class<E: TElement>(element: &E, candidate: &E) -> bool {
// XXX Efficiency here, I'm only validating ideas.
let mut first = vec![];
let mut second = vec![];
element.each_class(|c| first.push(c.clone()));
candidate.each_class(|c| second.push(c.clone()));
first == second
}
fn match_same_not_common_style_affecting_attributes_rules<E: TElement>(element: &E,
candidate: &E,
ctx: &SharedStyleContext) -> bool {
// XXX Same here, could store in the cache an index with the matched rules,
// for example.
ctx.stylist.match_same_not_common_style_affecting_attributes_rules(element, candidate)
}
fn match_same_sibling_affecting_rules<E: TElement>(element: &E,
candidate: &E,
ctx: &SharedStyleContext) -> bool {
ctx.stylist.match_same_sibling_affecting_rules(element, candidate)
}
static STYLE_SHARING_CANDIDATE_CACHE_SIZE: usize = 8;
impl StyleSharingCandidateCache {
pub fn new() -> Self {
@ -393,20 +384,45 @@ impl StyleSharingCandidateCache {
}
}
pub fn iter(&self) -> Iter<(StyleSharingCandidate, ())> {
pub fn iter(&self) -> Iter<(UnsafeNode, ())> {
self.cache.iter()
}
pub fn insert_if_possible<N: TNode>(&mut self, element: &N::ConcreteElement) {
match StyleSharingCandidate::new::<N>(element) {
None => {}
Some(candidate) => self.cache.insert(candidate, ())
pub fn insert_if_possible<E: TElement>(&mut self,
element: &E,
relations: StyleRelations) {
use selectors::matching::*; // For flags
use traversal::relations_are_shareable;
let parent = match element.parent_element() {
Some(element) => element,
None => {
debug!("Failing to insert to the cache: no parent element");
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;
}
// XXX check transitions/animations and reject!
debug!("Inserting into cache: {:?} with parent {:?}",
element.as_node().to_unsafe(), parent.as_node().to_unsafe());
self.cache.insert(element.as_node().to_unsafe(), ())
}
pub fn touch(&mut self, index: usize) {
self.cache.touch(index);
}
pub fn clear(&mut self) {
self.cache.evict_all()
}
}
/// The results of attempting to share a style.
@ -562,31 +578,19 @@ impl<N: TNode> PrivateMatchMethods for N {}
trait PrivateElementMatchMethods: TElement {
fn share_style_with_candidate_if_possible(&self,
parent_node: Option<Self::ConcreteNode>,
candidate: &StyleSharingCandidate)
parent_node: Self::ConcreteNode,
shared_context: &SharedStyleContext,
candidate: &Self)
-> Option<Arc<ComputedValues>> {
let parent_node = match parent_node {
Some(ref parent_node) if parent_node.as_element().is_some() => parent_node,
Some(_) | None => return None,
};
debug_assert!(parent_node.is_element());
let parent_data: Option<&PrivateStyleData> = unsafe {
parent_node.borrow_data_unchecked().map(|d| &*d)
};
if let Some(parent_data_ref) = parent_data {
// Check parent style.
let parent_style = (*parent_data_ref).style.as_ref().unwrap();
if !arc_ptr_eq(parent_style, &candidate.parent_style) {
return None
match element_matches_candidate(self, candidate, shared_context) {
Ok(cv) => Some(cv),
Err(error) => {
debug!("Cache miss: {:?}", error);
None
}
// Check tag names, classes, etc.
if !candidate.can_share_style_with(self) {
return None
}
return Some(candidate.style.clone())
}
None
}
}
@ -597,15 +601,19 @@ pub trait ElementMatchMethods : TElement {
stylist: &Stylist,
parent_bf: Option<&BloomFilter>,
applicable_declarations: &mut ApplicableDeclarations)
-> bool {
-> StyleRelations {
use traversal::relations_are_shareable;
let style_attribute = self.style_attribute().as_ref();
applicable_declarations.normal_shareable =
let mut relations =
stylist.push_applicable_declarations(self,
parent_bf,
style_attribute,
None,
&mut applicable_declarations.normal);
applicable_declarations.normal_shareable = relations_are_shareable(&relations);
TheSelectorImpl::each_eagerly_cascaded_pseudo_element(|pseudo| {
stylist.push_applicable_declarations(self,
parent_bf,
@ -614,8 +622,14 @@ pub trait ElementMatchMethods : TElement {
applicable_declarations.per_pseudo.entry(pseudo).or_insert(vec![]));
});
applicable_declarations.normal_shareable &&
applicable_declarations.per_pseudo.values().all(|v| v.is_empty())
let has_pseudos =
applicable_declarations.per_pseudo.values().any(|v| !v.is_empty());
if has_pseudos {
relations |= AFFECTED_BY_PSEUDO_ELEMENTS;
}
relations
}
/// Attempts to share a style with another node. This method is unsafe because it depends on
@ -624,6 +638,7 @@ pub trait ElementMatchMethods : TElement {
unsafe fn share_style_if_possible(&self,
style_sharing_candidate_cache:
&mut StyleSharingCandidateCache,
shared_context: &SharedStyleContext,
parent: Option<Self::ConcreteNode>)
-> StyleSharingResult<<Self::ConcreteNode as TNode>::ConcreteRestyleDamage> {
if opts::get().disable_share_style_cache {
@ -633,12 +648,24 @@ pub trait ElementMatchMethods : TElement {
if self.style_attribute().is_some() {
return StyleSharingResult::CannotShare
}
if self.has_attr(&ns!(), &atom!("id")) {
return StyleSharingResult::CannotShare
}
for (i, &(ref candidate, ())) in style_sharing_candidate_cache.iter().enumerate() {
if let Some(shared_style) = self.share_style_with_candidate_if_possible(parent.clone(), candidate) {
let parent = match parent {
Some(parent) if parent.is_element() => parent,
_ => return StyleSharingResult::CannotShare,
};
let iter = style_sharing_candidate_cache.iter().map(|&(unsafe_node, ())| {
Self::ConcreteNode::from_unsafe(&unsafe_node).as_element().unwrap()
});
for (i, candidate) in iter.enumerate() {
if let Some(shared_style) = self.share_style_with_candidate_if_possible(parent,
shared_context,
&candidate) {
// Yay, cache hit. Share the style.
let node = self.as_node();