servo: Merge #16358 - Fix up the style sharing cache (from bholley:style_sharing_fixes); r=emilio

Reviewed in:
https://bugzilla.mozilla.org/show_bug.cgi?id=1354895
https://bugzilla.mozilla.org/show_bug.cgi?id=1332525

Source-Repo: https://github.com/servo/servo
Source-Revision: 7ba3f1e4f3064ec3054b2346062758375b8ab117

--HG--
extra : subtree_source : https%3A//hg.mozilla.org/projects/converted-servo-linear
extra : subtree_revision : ea1494fe9e1c0720730f3999a0c11bd0b3434617
This commit is contained in:
Bobby Holley 2017-04-11 22:52:23 -05:00
parent 8cedccb89e
commit 206fa732a5
11 changed files with 199 additions and 150 deletions

1
servo/Cargo.lock generated
View File

@ -2733,6 +2733,7 @@ dependencies = [
"app_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bindgen 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -25,6 +25,7 @@ testing = []
app_units = "0.4"
atomic_refcell = "0.1"
bitflags = "0.7"
bit-vec = "0.4.3"
byteorder = "1.0"
cfg-if = "0.1.0"
cssparser = "0.12.1"

View File

@ -31,6 +31,11 @@ impl<K: PartialEq> LRUCache<K> {
}
}
/// Returns the number of elements in the cache.
pub fn num_entries(&self) -> usize {
self.entries.len()
}
#[inline]
/// Touch a given position, and put it in the last item on the list.
pub fn touch(&mut self, pos: usize) {

View File

@ -7,6 +7,7 @@
use animation::{Animation, PropertyAnimation};
use app_units::Au;
use bit_vec::BitVec;
use bloom::StyleBloom;
use data::ElementData;
use dom::{OpaqueNode, TNode, TElement, SendElement};
@ -111,6 +112,9 @@ pub struct CurrentElementInfo {
element: OpaqueNode,
/// Whether the element is being styled for the first time.
is_initial_style: bool,
/// Lazy cache of the result of matching the current element against the
/// revalidation selectors.
pub revalidation_match_results: Option<BitVec>,
/// A Vec of possibly expired animations. Used only by Servo.
#[allow(dead_code)]
pub possibly_expired_animations: Vec<PropertyAnimation>,
@ -317,6 +321,7 @@ impl<E: TElement> ThreadLocalStyleContext<E> {
self.current_element_info = Some(CurrentElementInfo {
element: element.as_node().opaque(),
is_initial_style: !data.has_styles(),
revalidation_match_results: None,
possibly_expired_animations: Vec::new(),
});
}

View File

@ -39,6 +39,7 @@
extern crate app_units;
extern crate atomic_refcell;
extern crate bit_vec;
#[macro_use]
extern crate bitflags;
#[allow(unused_extern_crates)] extern crate byteorder;

View File

@ -10,9 +10,10 @@
use Atom;
use animation::{self, Animation, PropertyAnimation};
use atomic_refcell::AtomicRefMut;
use bit_vec::BitVec;
use cache::{LRUCache, LRUCacheMutIterator};
use cascade_info::CascadeInfo;
use context::{SequentialTask, SharedStyleContext, StyleContext};
use context::{CurrentElementInfo, SequentialTask, SharedStyleContext, StyleContext};
use data::{ComputedStyle, ElementData, ElementStyles, RestyleData};
use dom::{AnimationRules, SendElement, TElement, TNode};
use font_metrics::FontMetricsProvider;
@ -53,6 +54,8 @@ struct StyleSharingCandidate<E: TElement> {
element: SendElement<E>,
/// The cached class names.
class_attributes: Option<Vec<Atom>>,
/// The cached result of matching this entry against the revalidation selectors.
revalidation_match_results: Option<BitVec>,
}
impl<E: TElement> PartialEq<StyleSharingCandidate<E>> for StyleSharingCandidate<E> {
@ -95,18 +98,18 @@ pub enum CacheMiss {
Class,
/// The presentation hints didn't match.
PresHints,
/// The element and the candidate didn't match the same set of
/// sibling-affecting rules.
SiblingRules,
/// The element and the candidate didn't match the same set of style
/// affecting attribute selectors.
AttrRules,
/// The element and the candidate didn't match the same set of revalidation
/// selectors.
Revalidation,
}
fn element_matches_candidate<E: TElement>(element: &E,
candidate: &mut StyleSharingCandidate<E>,
candidate_element: &E,
shared_context: &SharedStyleContext)
shared: &SharedStyleContext,
bloom: &BloomFilter,
info: &mut CurrentElementInfo,
tasks: &mut Vec<SequentialTask<E>>)
-> Result<ComputedStyle, CacheMiss> {
macro_rules! miss {
($miss: ident) => {
@ -154,16 +157,9 @@ fn element_matches_candidate<E: TElement>(element: &E,
miss!(PresHints)
}
if !match_same_sibling_affecting_rules(element,
candidate_element,
shared_context) {
miss!(SiblingRules)
}
if !match_same_style_affecting_attributes_rules(element,
candidate_element,
shared_context) {
miss!(AttrRules)
if !revalidate(element, candidate, candidate_element,
shared, bloom, info, tasks) {
miss!(Revalidation)
}
let data = candidate_element.borrow_data().unwrap();
@ -203,19 +199,60 @@ fn have_same_class<E: TElement>(element: &E,
element_class_attributes == *candidate.class_attributes.as_ref().unwrap()
}
// TODO: These re-match the candidate every time, which is suboptimal.
#[inline]
fn match_same_style_affecting_attributes_rules<E: TElement>(element: &E,
candidate: &E,
ctx: &SharedStyleContext) -> bool {
ctx.stylist.match_same_style_affecting_attributes_rules(element, candidate)
}
fn revalidate<E: TElement>(element: &E,
candidate: &mut StyleSharingCandidate<E>,
candidate_element: &E,
shared: &SharedStyleContext,
bloom: &BloomFilter,
info: &mut CurrentElementInfo,
tasks: &mut Vec<SequentialTask<E>>)
-> 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;
#[inline]
fn match_same_sibling_affecting_rules<E: TElement>(element: &E,
candidate: &E,
ctx: &SharedStyleContext) -> bool {
ctx.stylist.match_same_sibling_affecting_rules(element, candidate)
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:
// <style> #e:empty + span { ... } </style>
// <span id="e">
// <span></span>
// </span>
// <span></span>
//
// 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(tasks, 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;
@ -228,6 +265,11 @@ impl<E: TElement> StyleSharingCandidateCache<E> {
}
}
/// Returns the number of entries in the cache.
pub fn num_entries(&self) -> usize {
self.cache.num_entries()
}
fn iter_mut(&mut self) -> LRUCacheMutIterator<StyleSharingCandidate<E>> {
self.cache.iter_mut()
}
@ -238,7 +280,8 @@ impl<E: TElement> StyleSharingCandidateCache<E> {
pub fn insert_if_possible(&mut self,
element: &E,
style: &Arc<ComputedValues>,
relations: StyleRelations) {
relations: StyleRelations,
revalidation_match_results: Option<BitVec>) {
let parent = match element.parent_element() {
Some(element) => element,
None => {
@ -255,7 +298,7 @@ impl<E: TElement> StyleSharingCandidateCache<E> {
}
let box_style = style.get_box();
if box_style.transition_property_count() > 0 {
if box_style.specifies_transitions() {
debug!("Failing to insert to the cache: transitions");
return;
}
@ -271,6 +314,7 @@ impl<E: TElement> StyleSharingCandidateCache<E> {
self.cache.insert(StyleSharingCandidate {
element: unsafe { SendElement::new(*element) },
class_attributes: None,
revalidation_match_results: revalidation_match_results,
});
}
@ -651,11 +695,15 @@ trait PrivateMatchMethods: TElement {
}
fn share_style_with_candidate_if_possible(&self,
shared_context: &SharedStyleContext,
candidate: &mut StyleSharingCandidate<Self>)
candidate: &mut StyleSharingCandidate<Self>,
shared: &SharedStyleContext,
bloom: &BloomFilter,
info: &mut CurrentElementInfo,
tasks: &mut Vec<SequentialTask<Self>>)
-> Result<ComputedStyle, CacheMiss> {
let candidate_element = *candidate.element;
element_matches_candidate(self, candidate, &candidate_element, shared_context)
element_matches_candidate(self, candidate, &candidate_element,
shared, bloom, info, tasks)
}
}
@ -718,11 +766,23 @@ pub trait MatchMethods : TElement {
// If the style is shareable, add it to the LRU cache.
if sharing == StyleSharingBehavior::Allow && relations_are_shareable(&primary_relations) {
// If we previously tried to match this element against the cache,
// the revalidation match results will already be cached. Otherwise
// we'll have None, and compute them later on-demand.
//
// If we do have the results, grab them here to satisfy the borrow
// checker.
let revalidation_match_results = context.thread_local
.current_element_info
.as_mut().unwrap()
.revalidation_match_results
.take();
context.thread_local
.style_sharing_candidate_cache
.insert_if_possible(self,
data.styles().primary.values(),
primary_relations);
primary_relations,
revalidation_match_results);
}
}
@ -970,32 +1030,42 @@ pub trait MatchMethods : TElement {
/// live nodes in it, and we have no way to guarantee that at the type
/// system level yet.
unsafe fn share_style_if_possible(&self,
style_sharing_candidate_cache:
&mut StyleSharingCandidateCache<Self>,
shared_context: &SharedStyleContext,
context: &mut StyleContext<Self>,
data: &mut AtomicRefMut<ElementData>)
-> StyleSharingResult {
if is_share_style_cache_disabled() {
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.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 current_element_info =
&mut context.thread_local.current_element_info.as_mut().unwrap();
let bloom = context.thread_local.bloom_filter.filter();
let tasks = &mut context.thread_local.tasks;
let mut should_clear_cache = false;
for (i, candidate) in style_sharing_candidate_cache.iter_mut().enumerate() {
for (i, candidate) in cache.iter_mut().enumerate() {
let sharing_result =
self.share_style_with_candidate_if_possible(shared_context,
candidate);
self.share_style_with_candidate_if_possible(candidate,
&context.shared,
bloom,
current_element_info,
tasks);
match sharing_result {
Ok(shared_style) => {
// Yay, cache hit. Share the style.
@ -1005,7 +1075,8 @@ pub trait MatchMethods : TElement {
let old_values = data.get_styles_mut()
.and_then(|s| s.primary.values.take());
if let Some(old) = old_values {
self.accumulate_damage(shared_context, data.restyle_mut(), &old,
self.accumulate_damage(&context.shared,
data.restyle_mut(), &old,
shared_style.values(), None);
}
@ -1033,15 +1104,17 @@ pub trait MatchMethods : TElement {
// Too expensive failure, give up, we don't want another
// one of these.
CacheMiss::PresHints |
CacheMiss::SiblingRules |
CacheMiss::AttrRules => break,
CacheMiss::Revalidation => break,
_ => {}
}
}
}
}
debug!("{:?} Cannot share style: {} cache entries", self, cache.num_entries());
if should_clear_cache {
style_sharing_candidate_cache.clear();
cache.clear();
}
StyleSharingResult::CannotShare

View File

@ -1890,6 +1890,19 @@ fn static_assert() {
self.gecko.mTransitions[0].mProperty = nsCSSPropertyID_eCSSPropertyExtra_no_properties;
}
}
/// Returns whether there are any transitions specified.
pub fn specifies_transitions(&self) -> bool {
use gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_all_properties;
if self.gecko.mTransitionPropertyCount == 1 &&
self.gecko.mTransitions[0].mProperty == eCSSPropertyExtra_all_properties &&
self.gecko.mTransitions[0].mDuration.max(0.0) + self.gecko.mTransitions[0].mDelay <= 0.0f32 {
return false;
}
self.gecko.mTransitionPropertyCount > 0
}
pub fn transition_property_at(&self, index: usize)
-> longhands::transition_property::computed_value::SingleComputedValue {
self.gecko.mTransitions[index].mProperty.into()

View File

@ -1509,6 +1509,12 @@ pub mod style_structs {
pub fn specifies_animations(&self) -> bool {
self.animation_name_iter().any(|name| name.0 != atom!(""))
}
/// Returns whether there are any transitions specified.
#[cfg(feature = "servo")]
pub fn specifies_transitions(&self) -> bool {
self.transition_property_count() > 0
}
% endif
}

View File

@ -400,8 +400,15 @@ fn is_attr_selector(sel: &SimpleSelector<SelectorImpl>) -> bool {
}
}
fn is_sibling_affecting_selector(sel: &SimpleSelector<SelectorImpl>) -> bool {
/// Whether a selector containing this simple selector needs to be explicitly
/// matched against both the style sharing cache entry and the candidate.
///
///
/// We use this for selectors that can have different matching behavior between
/// siblings that are otherwise identical as far as the cache is concerned.
fn needs_cache_revalidation(sel: &SimpleSelector<SelectorImpl>) -> bool {
match *sel {
SimpleSelector::Empty |
SimpleSelector::FirstChild |
SimpleSelector::LastChild |
SimpleSelector::OnlyChild |
@ -412,6 +419,7 @@ fn is_sibling_affecting_selector(sel: &SimpleSelector<SelectorImpl>) -> bool {
SimpleSelector::FirstOfType |
SimpleSelector::LastOfType |
SimpleSelector::OnlyOfType => true,
SimpleSelector::NonTSPseudoClass(ref p) => p.state_flag().is_empty(),
_ => false,
}
}
@ -481,8 +489,7 @@ struct Dependency {
/// selector.
pub struct SelectorDependencyVisitor<'a> {
dependency_set: &'a mut DependencySet,
affects_siblings: bool,
affected_by_attribute: bool,
needs_cache_revalidation: bool,
}
impl<'a> SelectorDependencyVisitor<'a> {
@ -490,20 +497,14 @@ impl<'a> SelectorDependencyVisitor<'a> {
pub fn new(dependency_set: &'a mut DependencySet) -> Self {
SelectorDependencyVisitor {
dependency_set: dependency_set,
affects_siblings: false,
affected_by_attribute: false,
needs_cache_revalidation: false,
}
}
/// Returns whether this visitor has known of a sibling-dependent selector.
pub fn affects_siblings(&self) -> bool {
self.affects_siblings
}
/// Returns whether this visitor has known of a attribute-dependent
/// selector.
pub fn affected_by_attribute(&self) -> bool {
self.affected_by_attribute
/// Returns whether this visitor has encountered a simple selector that needs
/// cache revalidation.
pub fn needs_cache_revalidation(&self) -> bool {
self.needs_cache_revalidation
}
}
@ -518,8 +519,8 @@ impl<'a> SelectorVisitor for SelectorDependencyVisitor<'a> {
let mut sensitivities = Sensitivities::new();
for s in &selector.compound_selector {
sensitivities.states.insert(selector_to_state(s));
if !self.affects_siblings {
self.affects_siblings = is_sibling_affecting_selector(s);
if !self.needs_cache_revalidation {
self.needs_cache_revalidation = needs_cache_revalidation(s);
}
if !sensitivities.attrs {
sensitivities.attrs = is_attr_selector(s);
@ -528,8 +529,8 @@ impl<'a> SelectorVisitor for SelectorDependencyVisitor<'a> {
let hint = combinator_to_restyle_hint(combinator);
self.affected_by_attribute |= sensitivities.attrs;
self.affects_siblings |= hint.intersects(RESTYLE_LATER_SIBLINGS);
self.needs_cache_revalidation |= sensitivities.attrs;
self.needs_cache_revalidation |= hint.intersects(RESTYLE_LATER_SIBLINGS);
if !sensitivities.is_empty() {
self.dependency_set.add_dependency(Dependency {

View File

@ -7,6 +7,7 @@
#![deny(missing_docs)]
use {Atom, LocalName};
use bit_vec::BitVec;
use data::ComputedStyle;
use dom::{AnimationRules, PresentationalHintsSynthetizer, TElement};
use error_reporting::StdoutErrorReporter;
@ -111,14 +112,11 @@ pub struct Stylist {
/// Selector dependencies used to compute restyle hints.
state_deps: DependencySet,
/// Selectors in the page affecting siblings
/// Selectors that require explicit cache revalidation (i.e. which depend
/// on state that is not otherwise visible to the cache, like attributes or
/// tree-structural state like child index and pseudos).
#[cfg_attr(feature = "servo", ignore_heap_size_of = "Arc")]
sibling_affecting_selectors: Vec<Selector<SelectorImpl>>,
/// Selectors in the page matching elements with non-common style-affecting
/// attributes.
#[cfg_attr(feature = "servo", ignore_heap_size_of = "Arc")]
style_affecting_attributes_selectors: Vec<Selector<SelectorImpl>>,
selectors_for_cache_revalidation: Vec<Selector<SelectorImpl>>,
}
/// This struct holds data which user of Stylist may want to extract
@ -170,8 +168,7 @@ impl Stylist {
rule_tree: RuleTree::new(),
state_deps: DependencySet::new(),
sibling_affecting_selectors: vec![],
style_affecting_attributes_selectors: vec![]
selectors_for_cache_revalidation: vec![],
};
SelectorImpl::each_eagerly_cascaded_pseudo_element(|pseudo| {
@ -225,8 +222,7 @@ impl Stylist {
self.state_deps.clear();
self.animations.clear();
self.sibling_affecting_selectors.clear();
self.style_affecting_attributes_selectors.clear();
self.selectors_for_cache_revalidation.clear();
extra_data.clear_font_faces();
@ -246,10 +242,8 @@ impl Stylist {
}
debug!("Stylist stats:");
debug!(" - Got {} sibling-affecting selectors",
self.sibling_affecting_selectors.len());
debug!(" - Got {} non-common-style-attribute-affecting selectors",
self.style_affecting_attributes_selectors.len());
debug!(" - Got {} selectors for cache revalidation",
self.selectors_for_cache_revalidation.len());
debug!(" - Got {} deps for style-hint calculation",
self.state_deps.len());
@ -304,12 +298,8 @@ impl Stylist {
SelectorDependencyVisitor::new(&mut self.state_deps);
selector.visit(&mut visitor);
if visitor.affects_siblings() {
self.sibling_affecting_selectors.push(selector.clone());
}
if visitor.affected_by_attribute() {
self.style_affecting_attributes_selectors.push(selector.clone());
if visitor.needs_cache_revalidation() {
self.selectors_for_cache_revalidation.push(selector.clone());
}
}
}
@ -770,83 +760,38 @@ impl Stylist {
&self.animations
}
/// Whether two elements match the same not-common style-affecting attribute
/// rules.
///
/// This is used to test elements and candidates in the style-sharing
/// candidate cache.
pub fn match_same_style_affecting_attributes_rules<E>(&self,
element: &E,
candidate: &E) -> bool
where E: TElement,
{
use selectors::matching::StyleRelations;
use selectors::matching::matches_complex_selector;
// TODO(emilio): we can probably do better, the candidate should already
// know what rules it matches. Also, we should only match until we find
// a descendant combinator, the rest should be ok, since the parent is
// the same.
//
// TODO(emilio): Use the bloom filter, since they contain the element's
// ancestor chain and it's correct for the candidate too.
for ref selector in self.style_affecting_attributes_selectors.iter() {
let element_matches =
matches_complex_selector(&selector.complex_selector, element,
None, &mut StyleRelations::empty(),
&mut |_, _| {});
let candidate_matches =
matches_complex_selector(&selector.complex_selector, candidate,
None, &mut StyleRelations::empty(),
&mut |_, _| {});
if element_matches != candidate_matches {
return false;
}
}
true
}
/// Returns the rule root node.
#[inline]
pub fn rule_tree_root(&self) -> StrongRuleNode {
self.rule_tree.root()
}
/// Returns whether two elements match the same sibling-affecting rules.
///
/// This is also for the style sharing candidate cache.
pub fn match_same_sibling_affecting_rules<E>(&self,
element: &E,
candidate: &E) -> bool
/// Computes the match results of a given element against the set of
/// revalidation selectors.
pub fn match_revalidation_selectors<E, F>(&self,
element: &E,
bloom: &BloomFilter,
flags_setter: &mut F)
-> BitVec
where E: TElement,
F: FnMut(&E, ElementSelectorFlags)
{
use selectors::matching::StyleRelations;
use selectors::matching::matches_complex_selector;
// TODO(emilio): we can probably do better, the candidate should already
// know what rules it matches.
//
// TODO(emilio): Use the bloom filter, since they contain the element's
// ancestor chain and it's correct for the candidate too.
for ref selector in self.sibling_affecting_selectors.iter() {
let element_matches =
matches_complex_selector(&selector.complex_selector, element,
None, &mut StyleRelations::empty(),
&mut |_, _| {});
let candidate_matches =
matches_complex_selector(&selector.complex_selector, candidate,
None, &mut StyleRelations::empty(),
&mut |_, _| {});
let len = self.selectors_for_cache_revalidation.len();
let mut results = BitVec::from_elem(len, false);
if element_matches != candidate_matches {
debug!("match_same_sibling_affecting_rules: Failure due to {:?}",
selector.complex_selector);
return false;
}
for (i, ref selector) in self.selectors_for_cache_revalidation
.iter().enumerate() {
results.set(i, matches_complex_selector(&selector.complex_selector,
element,
Some(bloom),
&mut StyleRelations::empty(),
flags_setter));
}
true
results
}
/// Given an element, and a snapshot that represents a previous state of the

View File

@ -680,15 +680,13 @@ fn compute_style<E, D>(_traversal: &D,
use matching::StyleSharingResult::*;
context.thread_local.statistics.elements_styled += 1;
let shared_context = context.shared;
let kind = data.restyle_kind();
// First, try the style sharing cache. If we get a match we can skip the rest
// of the work.
if let MatchAndCascade = kind {
let sharing_result = unsafe {
let cache = &mut context.thread_local.style_sharing_candidate_cache;
element.share_style_if_possible(cache, shared_context, &mut data)
element.share_style_if_possible(context, &mut data)
};
if let StyleWasShared(index) = sharing_result {
context.thread_local.statistics.styles_shared += 1;