servo/components/style/invalidation/element/invalidation_map.rs
Emilio Cobos Álvarez cb06375fe2
style: Implement a more fine-grained invalidation method.
This commit also removes the old restyle_hints module and splits it into
multiple modules under components/style/invalidation/element/.

The basic approach is to walk down the tree using compound selectors as needed,
in order to do as little selector-matching as possible.

Bug: 1368240
MozReview-Commit-ID: 2YO8fKFygZI
2017-06-13 13:26:41 +02:00

402 lines
15 KiB
Rust

/* 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 for invalidations due to state or attribute changes.
use {Atom, LocalName, Namespace};
use context::QuirksMode;
use element_state::ElementState;
use selector_map::{MaybeCaseInsensitiveHashMap, SelectorMap, SelectorMapEntry};
use selector_parser::SelectorImpl;
use selectors::attr::NamespaceConstraint;
use selectors::parser::{AncestorHashes, Combinator, Component};
use selectors::parser::{Selector, SelectorAndHashes, SelectorIter, SelectorMethods};
use selectors::visitor::SelectorVisitor;
use smallvec::SmallVec;
#[cfg(feature = "gecko")]
/// Gets the element state relevant to the given `:dir` pseudo-class selector.
pub fn dir_selector_to_state(s: &[u16]) -> ElementState {
use element_state::{IN_LTR_STATE, IN_RTL_STATE};
// Jump through some hoops to deal with our Box<[u16]> thing.
const LTR: [u16; 4] = [b'l' as u16, b't' as u16, b'r' as u16, 0];
const RTL: [u16; 4] = [b'r' as u16, b't' as u16, b'l' as u16, 0];
if LTR == *s {
IN_LTR_STATE
} else if RTL == *s {
IN_RTL_STATE
} else {
// :dir(something-random) is a valid selector, but shouldn't
// match anything.
ElementState::empty()
}
}
/// Mapping between (partial) CompoundSelectors (and the combinator to their
/// right) and the states and attributes they depend on.
///
/// In general, for all selectors in all applicable stylesheets of the form:
///
/// |a _ b _ c _ d _ e|
///
/// Where:
/// * |b| and |d| are simple selectors that depend on state (like :hover) or
/// attributes (like [attr...], .foo, or #foo).
/// * |a|, |c|, and |e| are arbitrary simple selectors that do not depend on
/// state or attributes.
///
/// We generate a Dependency for both |a _ b:X _| and |a _ b:X _ c _ d:Y _|,
/// even though those selectors may not appear on their own in any stylesheet.
/// This allows us to quickly scan through the dependency sites of all style
/// rules and determine the maximum effect that a given state or attribute
/// change may have on the style of elements in the document.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct Dependency {
/// The dependency selector.
#[cfg_attr(feature = "servo", ignore_heap_size_of = "Arc")]
pub selector: Selector<SelectorImpl>,
/// The ancestor hashes associated with the above selector at the given
/// offset.
#[cfg_attr(feature = "servo", ignore_heap_size_of = "No heap data")]
pub hashes: AncestorHashes,
/// The offset into the selector that we should match on.
pub selector_offset: usize,
}
impl Dependency {
/// Returns the combinator to the right of the partial selector this
/// dependency represents.
///
/// TODO(emilio): Consider storing inline if it helps cache locality?
pub fn combinator(&self) -> Option<Combinator> {
if self.selector_offset == 0 {
return None;
}
Some(self.selector.combinator_at(self.selector_offset))
}
/// Whether this dependency affects the style of the element.
///
/// NOTE(emilio): pseudo-elements need to be here to account for eager
/// pseudos, since they just grab the style from the originating element.
///
/// TODO(emilio): We could look at the selector itself to see if it's an
/// eager pseudo, and return false here if not.
pub fn affects_self(&self) -> bool {
matches!(self.combinator(), None | Some(Combinator::PseudoElement))
}
/// Whether this dependency may affect style of any of our descendants.
pub fn affects_descendants(&self) -> bool {
matches!(self.combinator(), Some(Combinator::PseudoElement) |
Some(Combinator::Child) |
Some(Combinator::Descendant))
}
/// Whether this dependency may affect style of any of our later siblings.
pub fn affects_later_siblings(&self) -> bool {
matches!(self.combinator(), Some(Combinator::NextSibling) |
Some(Combinator::LaterSibling))
}
}
impl SelectorMapEntry for Dependency {
fn selector(&self) -> SelectorIter<SelectorImpl> {
self.selector.iter_from(self.selector_offset)
}
fn hashes(&self) -> &AncestorHashes {
&self.hashes
}
}
/// The same, but for state selectors, which can track more exactly what state
/// do they track.
#[derive(Clone)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct StateDependency {
/// The other dependency fields.
pub dep: Dependency,
/// The state this dependency is affected by.
pub state: ElementState,
}
impl SelectorMapEntry for StateDependency {
fn selector(&self) -> SelectorIter<SelectorImpl> {
self.dep.selector.iter_from(self.dep.selector_offset)
}
fn hashes(&self) -> &AncestorHashes {
&self.dep.hashes
}
}
/// A map where we store invalidations.
///
/// This is slightly different to a SelectorMap, in the sense of that the same
/// selector may appear multiple times.
///
/// In particular, we want to lookup as few things as possible to get the fewer
/// selectors the better, so this looks up by id, class, or looks at the list of
/// state/other attribute affecting selectors.
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
pub struct InvalidationMap {
/// A map from a given class name to all the selectors with that class
/// selector.
pub class_to_selector: MaybeCaseInsensitiveHashMap<Atom, SelectorMap<Dependency>>,
/// A map from a given id to all the selectors with that ID in the
/// stylesheets currently applying to the document.
pub id_to_selector: MaybeCaseInsensitiveHashMap<Atom, SelectorMap<Dependency>>,
/// A map of all the state dependencies.
pub state_affecting_selectors: SelectorMap<StateDependency>,
/// A map of other attribute affecting selectors.
pub other_attribute_affecting_selectors: SelectorMap<Dependency>,
/// Whether there are attribute rules of the form `[class~="foo"]` that may
/// match. In that case, we need to look at
/// `other_attribute_affecting_selectors` too even if only the `class` has
/// changed.
pub has_class_attribute_selectors: bool,
/// Whether there are attribute rules of the form `[id|="foo"]` that may
/// match. In that case, we need to look at
/// `other_attribute_affecting_selectors` too even if only the `id` has
/// changed.
pub has_id_attribute_selectors: bool,
}
impl InvalidationMap {
/// Creates an empty `InvalidationMap`.
pub fn new() -> Self {
Self {
class_to_selector: MaybeCaseInsensitiveHashMap::new(),
id_to_selector: MaybeCaseInsensitiveHashMap::new(),
state_affecting_selectors: SelectorMap::new(),
other_attribute_affecting_selectors: SelectorMap::new(),
has_class_attribute_selectors: false,
has_id_attribute_selectors: false,
}
}
/// Returns the number of dependencies stored in the invalidation map.
pub fn len(&self) -> usize {
self.state_affecting_selectors.len() +
self.other_attribute_affecting_selectors.len() +
self.id_to_selector.iter().fold(0, |accum, (_, ref v)| {
accum + v.len()
}) +
self.class_to_selector.iter().fold(0, |accum, (_, ref v)| {
accum + v.len()
})
}
/// Adds a selector to this `InvalidationMap`.
pub fn note_selector(
&mut self,
selector_and_hashes: &SelectorAndHashes<SelectorImpl>,
quirks_mode: QuirksMode)
{
self.collect_invalidations_for(selector_and_hashes, quirks_mode)
}
/// Clears this map, leaving it empty.
pub fn clear(&mut self) {
self.class_to_selector.clear();
self.id_to_selector.clear();
self.state_affecting_selectors = SelectorMap::new();
self.other_attribute_affecting_selectors = SelectorMap::new();
self.has_id_attribute_selectors = false;
self.has_class_attribute_selectors = false;
}
fn collect_invalidations_for(
&mut self,
selector_and_hashes: &SelectorAndHashes<SelectorImpl>,
quirks_mode: QuirksMode)
{
debug!("InvalidationMap::collect_invalidations_for({:?})",
selector_and_hashes.selector);
let mut iter = selector_and_hashes.selector.iter();
let mut combinator;
let mut index = 0;
loop {
let sequence_start = index;
let mut compound_visitor = CompoundSelectorDependencyCollector {
classes: SmallVec::new(),
ids: SmallVec::new(),
state: ElementState::empty(),
other_attributes: false,
has_id_attribute_selectors: false,
has_class_attribute_selectors: false,
};
// Visit all the simple selectors in this sequence.
//
// Note that this works because we can't have combinators nested
// inside simple selectors (i.e. in :not() or :-moz-any()).
//
// If we ever support that we'll need to visit nested complex
// selectors as well, in order to mark them as affecting descendants
// at least.
for ss in &mut iter {
ss.visit(&mut compound_visitor);
index += 1; // Account for the simple selector.
}
// Reuse the bloom hashes if this is the base selector. Otherwise,
// rebuild them.
let mut hashes = None;
let mut get_hashes = || -> AncestorHashes {
if hashes.is_none() {
hashes = Some(if sequence_start == 0 {
selector_and_hashes.hashes.clone()
} else {
let seq_iter = selector_and_hashes.selector.iter_from(sequence_start);
AncestorHashes::from_iter(seq_iter)
});
}
hashes.clone().unwrap()
};
self.has_id_attribute_selectors |= compound_visitor.has_id_attribute_selectors;
self.has_class_attribute_selectors |= compound_visitor.has_class_attribute_selectors;
for class in compound_visitor.classes {
self.class_to_selector
.entry(class, quirks_mode)
.or_insert_with(SelectorMap::new)
.insert(Dependency {
selector: selector_and_hashes.selector.clone(),
selector_offset: sequence_start,
hashes: get_hashes(),
}, quirks_mode);
}
for id in compound_visitor.ids {
self.id_to_selector
.entry(id, quirks_mode)
.or_insert_with(SelectorMap::new)
.insert(Dependency {
selector: selector_and_hashes.selector.clone(),
selector_offset: sequence_start,
hashes: get_hashes(),
}, quirks_mode);
}
if !compound_visitor.state.is_empty() {
self.state_affecting_selectors
.insert(StateDependency {
dep: Dependency {
selector: selector_and_hashes.selector.clone(),
selector_offset: sequence_start,
hashes: get_hashes(),
},
state: compound_visitor.state,
}, quirks_mode);
}
if compound_visitor.other_attributes {
self.other_attribute_affecting_selectors
.insert(Dependency {
selector: selector_and_hashes.selector.clone(),
selector_offset: sequence_start,
hashes: get_hashes(),
}, quirks_mode);
}
combinator = iter.next_sequence();
if combinator.is_none() {
break;
}
index += 1; // Account for the combinator.
}
}
}
/// A struct that collects invalidations for a given compound selector.
struct CompoundSelectorDependencyCollector {
/// The state this compound selector is affected by.
state: ElementState,
/// The classes this compound selector is affected by.
///
/// NB: This will be often a single class, but could be multiple in
/// presence of :not, :-moz-any, .foo.bar.baz, etc.
classes: SmallVec<[Atom; 5]>,
/// The IDs this compound selector is affected by.
///
/// NB: This will be almost always a single id, but could be multiple in
/// presence of :not, :-moz-any, #foo#bar, etc.
ids: SmallVec<[Atom; 5]>,
/// Whether it affects other attribute-dependent selectors that aren't ID or
/// class selectors (NB: We still set this to true in presence of [class] or
/// [id] attribute selectors).
other_attributes: bool,
/// Whether there were attribute selectors with the id attribute.
has_id_attribute_selectors: bool,
/// Whether there were attribute selectors with the class attribute.
has_class_attribute_selectors: bool,
}
impl SelectorVisitor for CompoundSelectorDependencyCollector {
type Impl = SelectorImpl;
fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool {
#[cfg(feature = "gecko")]
use selector_parser::NonTSPseudoClass;
match *s {
Component::ID(ref id) => {
self.ids.push(id.clone());
}
Component::Class(ref class) => {
self.classes.push(class.clone());
}
Component::NonTSPseudoClass(ref pc) => {
self.other_attributes |= pc.is_attr_based();
self.state |= match *pc {
#[cfg(feature = "gecko")]
NonTSPseudoClass::Dir(ref s) => {
dir_selector_to_state(s)
}
_ => pc.state_flag(),
};
}
_ => {}
}
true
}
fn visit_attribute_selector(
&mut self,
constraint: &NamespaceConstraint<&Namespace>,
_local_name: &LocalName,
local_name_lower: &LocalName,
) -> bool {
self.other_attributes = true;
let may_match_in_no_namespace = match *constraint {
NamespaceConstraint::Any => true,
NamespaceConstraint::Specific(ref ns) => ns.is_empty(),
};
if may_match_in_no_namespace {
self.has_id_attribute_selectors |= *local_name_lower == local_name!("id");
self.has_class_attribute_selectors |= *local_name_lower == local_name!("class");
}
true
}
}