servo: Merge #18827 - style: Support more selectors in the style sheet invalidator (from heycam:invalidate-less); r=emilio
authorCameron McCormack <cam@mcc.id.au>
Wed, 11 Oct 2017 06:55:03 -0500
changeset 679091 dd04bc03bb5ef42e178e9ecaeb202f78281f0f26
parent 679090 bab0d9666afb77ce8a924cb995aab9062f79a712
child 679092 370a4a2a78980dea15b6a3f0801f902a62602741
push id84141
push userbmo:schien@mozilla.com
push dateThu, 12 Oct 2017 11:13:04 +0000
reviewersemilio
milestone58.0a1
servo: Merge #18827 - style: Support more selectors in the style sheet invalidator (from heycam:invalidate-less); r=emilio In addition to being able to invalidate just based on local name, this also adds support for invalidating based on selectors with no ancestor combinator in them (resulting in a RESTYLE_SELF for those elements that match). Reviewed in https://bugzilla.mozilla.org/show_bug.cgi?id=1407522 by Emilio. Source-Repo: https://github.com/servo/servo Source-Revision: 2f3bc0de499a9d301459dbb27978051a9a7d5f4a
servo/components/style/invalidation/stylesheets.rs
--- a/servo/components/style/invalidation/stylesheets.rs
+++ b/servo/components/style/invalidation/stylesheets.rs
@@ -3,84 +3,108 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 //! A collection of invalidations due to changes in which stylesheets affect a
 //! document.
 
 #![deny(unsafe_code)]
 
 use Atom;
+use LocalName as SelectorLocalName;
 use dom::{TElement, TNode};
 use fnv::FnvHashSet;
-use invalidation::element::restyle_hints::RestyleHint;
+use invalidation::element::restyle_hints::{RESTYLE_SELF, RestyleHint};
 use media_queries::Device;
 use selector_parser::SelectorImpl;
 use selectors::attr::CaseSensitivity;
-use selectors::parser::{Component, Selector};
+use selectors::parser::{Component, LocalName, Selector};
 use shared_lock::SharedRwLockReadGuard;
 use stylesheets::{CssRule, StylesheetInDocument};
 
-/// An invalidation scope represents a kind of subtree that may need to be
-/// restyled.
+/// A style sheet invalidation represents a kind of element or subtree that may
+/// need to be restyled. Whether it represents a whole subtree or just a single
+/// element is determined by whether the invalidation is stored in the
+/// StylesheetInvalidationSet's invalid_scopes or invalid_elements table.
 #[derive(Debug, Eq, Hash, PartialEq)]
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
-enum InvalidationScope {
-    /// All the descendants of an element with a given id.
+enum Invalidation {
+    /// An element with a given id.
     ID(Atom),
-    /// All the descendants of an element with a given class name.
+    /// An element with a given class name.
     Class(Atom),
+    /// An element with a given local name.
+    LocalName { name: SelectorLocalName, lower_name: SelectorLocalName },
 }
 
-impl InvalidationScope {
+impl Invalidation {
     fn is_id(&self) -> bool {
-        matches!(*self, InvalidationScope::ID(..))
+        matches!(*self, Invalidation::ID(..))
+    }
+
+    fn is_id_or_class(&self) -> bool {
+        matches!(*self, Invalidation::ID(..) | Invalidation::Class(..))
     }
 
     fn matches<E>(&self, element: E) -> bool
         where E: TElement,
     {
         match *self {
-            InvalidationScope::Class(ref class) => {
+            Invalidation::Class(ref class) => {
+                // FIXME This should look at the quirks mode of the document to
+                // determine case sensitivity.
                 element.has_class(class, CaseSensitivity::CaseSensitive)
             }
-            InvalidationScope::ID(ref id) => {
+            Invalidation::ID(ref id) => {
                 match element.get_id() {
+                    // FIXME This should look at the quirks mode of the document
+                    // to determine case sensitivity.
                     Some(element_id) => element_id == *id,
                     None => false,
                 }
             }
+            Invalidation::LocalName { ref name, ref lower_name } => {
+                // This could look at the quirks mode of the document, instead
+                // of testing against both names, but it's probably not worth
+                // it.
+                let local_name = element.get_local_name();
+                *local_name == **name || *local_name == **lower_name
+            }
         }
     }
 }
 
 /// A set of invalidations due to stylesheet additions.
 ///
 /// TODO(emilio): We might be able to do the same analysis for media query
 /// changes too (or even selector changes?).
 #[cfg_attr(feature = "servo", derive(HeapSizeOf))]
 pub struct StylesheetInvalidationSet {
-    /// The style scopes we know we have to restyle so far.
-    invalid_scopes: FnvHashSet<InvalidationScope>,
-    /// Whether the whole document should be invalid.
+    /// The subtrees we know we have to restyle so far.
+    invalid_scopes: FnvHashSet<Invalidation>,
+    /// The elements we know we have to restyle so far.
+    invalid_elements: FnvHashSet<Invalidation>,
+    /// Whether the whole document should be restyled.
     fully_invalid: bool,
 }
 
 impl StylesheetInvalidationSet {
     /// Create an empty `StylesheetInvalidationSet`.
     pub fn new() -> Self {
         Self {
             invalid_scopes: FnvHashSet::default(),
+            invalid_elements: FnvHashSet::default(),
             fully_invalid: false,
         }
     }
 
     /// Mark the DOM tree styles' as fully invalid.
     pub fn invalidate_fully(&mut self) {
         debug!("StylesheetInvalidationSet::invalidate_fully");
         self.invalid_scopes.clear();
+        self.invalid_elements.clear();
         self.fully_invalid = true;
     }
 
     /// Analyze the given stylesheet, and collect invalidations from their
     /// rules, in order to avoid doing a full restyle when we style the document
     /// next time.
     pub fn collect_invalidations_for<S>(
         &mut self,
@@ -102,21 +126,23 @@ impl StylesheetInvalidationSet {
             debug!(" > Stylesheet was not effective");
             return; // Nothing to do here.
         }
 
         for rule in stylesheet.effective_rules(device, guard) {
             self.collect_invalidations_for_rule(rule, guard);
             if self.fully_invalid {
                 self.invalid_scopes.clear();
+                self.invalid_elements.clear();
                 break;
             }
         }
 
-        debug!(" > resulting invalidations: {:?}", self.invalid_scopes);
+        debug!(" > resulting subtree invalidations: {:?}", self.invalid_scopes);
+        debug!(" > resulting self invalidations: {:?}", self.invalid_elements);
         debug!(" > fully_invalid: {}", self.fully_invalid);
     }
 
     /// Clears the invalidation set, invalidating elements as needed if
     /// `document_element` is provided.
     ///
     /// Returns true if any invalidations ocurred.
     pub fn flush<E>(&mut self, document_element: Option<E>) -> bool
@@ -128,16 +154,17 @@ impl StylesheetInvalidationSet {
         };
         self.clear();
         have_invalidations
     }
 
     /// Clears the invalidation set without processing.
     pub fn clear(&mut self) {
         self.invalid_scopes.clear();
+        self.invalid_elements.clear();
         self.fully_invalid = false;
     }
 
     fn process_invalidations<E>(&self, element: E) -> bool
         where E: TElement,
     {
         {
             let mut data = match element.mutate_data() {
@@ -148,27 +175,27 @@ impl StylesheetInvalidationSet {
             if self.fully_invalid {
                 debug!("process_invalidations: fully_invalid({:?})",
                        element);
                 data.hint.insert(RestyleHint::restyle_subtree());
                 return true;
             }
         }
 
-        if self.invalid_scopes.is_empty() {
+        if self.invalid_scopes.is_empty() && self.invalid_elements.is_empty() {
             debug!("process_invalidations: empty invalidation set");
             return false;
         }
 
         self.process_invalidations_in_subtree(element)
     }
 
-    /// Process style invalidations in a given subtree, that is, look for all
-    /// the relevant scopes in the subtree, and mark as dirty only the relevant
-    /// ones.
+    /// Process style invalidations in a given subtree. This traverses the
+    /// subtree looking for elements that match the invalidations in
+    /// invalid_scopes and invalid_elements.
     ///
     /// Returns whether it invalidated at least one element's style.
     #[allow(unsafe_code)]
     fn process_invalidations_in_subtree<E>(&self, element: E) -> bool
         where E: TElement,
     {
         let mut data = match element.mutate_data() {
             Some(data) => data,
@@ -180,25 +207,38 @@ impl StylesheetInvalidationSet {
         }
 
         if data.hint.contains_subtree() {
             debug!("process_invalidations_in_subtree: {:?} was already invalid",
                    element);
             return false;
         }
 
-        for scope in &self.invalid_scopes {
-            if scope.matches(element) {
-                debug!("process_invalidations_in_subtree: {:?} matched {:?}",
-                       element, scope);
+        for invalidation in &self.invalid_scopes {
+            if invalidation.matches(element) {
+                debug!("process_invalidations_in_subtree: {:?} matched subtree {:?}",
+                       element, invalidation);
                 data.hint.insert(RestyleHint::restyle_subtree());
                 return true;
             }
         }
 
+        let mut self_invalid = false;
+
+        if !data.hint.contains(RESTYLE_SELF) {
+            for invalidation in &self.invalid_elements {
+                if invalidation.matches(element) {
+                    debug!("process_invalidations_in_subtree: {:?} matched self {:?}",
+                           element, invalidation);
+                    data.hint.insert(RESTYLE_SELF);
+                    self_invalid = true;
+                    break;
+                }
+            }
+        }
 
         let mut any_children_invalid = false;
 
         for child in element.as_node().traversal_children() {
             let child = match child.as_element() {
                 Some(e) => e,
                 None => continue,
             };
@@ -207,81 +247,101 @@ impl StylesheetInvalidationSet {
         }
 
         if any_children_invalid {
             debug!("Children of {:?} changed, setting dirty descendants",
                    element);
             unsafe { element.set_dirty_descendants() }
         }
 
-        return any_children_invalid
+        return self_invalid || any_children_invalid
     }
 
     fn scan_component(
         component: &Component<SelectorImpl>,
-        scope: &mut Option<InvalidationScope>)
+        invalidation: &mut Option<Invalidation>)
     {
         match *component {
+            Component::LocalName(LocalName { ref name, ref lower_name }) => {
+                if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) {
+                    *invalidation = Some(Invalidation::LocalName {
+                        name: name.clone(),
+                        lower_name: lower_name.clone(),
+                    });
+                }
+            }
             Component::Class(ref class) => {
-                if scope.as_ref().map_or(true, |s| !s.is_id()) {
-                    *scope = Some(InvalidationScope::Class(class.clone()));
+                if invalidation.as_ref().map_or(true, |s| !s.is_id()) {
+                    *invalidation = Some(Invalidation::Class(class.clone()));
                 }
             }
             Component::ID(ref id) => {
-                if scope.is_none() {
-                    *scope = Some(InvalidationScope::ID(id.clone()));
+                if invalidation.is_none() {
+                    *invalidation = Some(Invalidation::ID(id.clone()));
                 }
             }
             _ => {
                 // Ignore everything else, at least for now.
             }
         }
     }
 
-    /// Collect a style scopes for a given selector.
+    /// Collect invalidations for a given selector.
     ///
-    /// We look at the outermost class or id selector to the left of an ancestor
-    /// combinator, in order to restyle only a given subtree.
+    /// We look at the outermost local name, class, or ID selector to the left
+    /// of an ancestor combinator, in order to restyle only a given subtree.
+    ///
+    /// If the selector has no ancestor combinator, then we do the same for
+    /// the only sequence it has, but record it as an element invalidation
+    /// instead of a subtree invalidation.
     ///
-    /// We prefer id scopes to class scopes, and outermost scopes to innermost
-    /// scopes (to reduce the amount of traversal we need to do).
-    fn collect_scopes(&mut self, selector: &Selector<SelectorImpl>) {
-        debug!("StylesheetInvalidationSet::collect_scopes({:?})", selector);
+    /// We prefer IDs to classs, and classes to local names, on the basis
+    /// that the former should be more specific than the latter. We also
+    /// prefer to generate subtree invalidations for the outermost part
+    /// of the selector, to reduce the amount of traversal we need to do
+    /// when flushing invalidations.
+    fn collect_invalidations(&mut self, selector: &Selector<SelectorImpl>) {
+        debug!("StylesheetInvalidationSet::collect_invalidations({:?})", selector);
 
-        let mut scope: Option<InvalidationScope> = None;
+        let mut element_invalidation: Option<Invalidation> = None;
+        let mut subtree_invalidation: Option<Invalidation> = None;
 
-        let mut scan = true;
+        let mut scan_for_element_invalidation = true;
+        let mut scan_for_subtree_invalidation = false;
+
         let mut iter = selector.iter();
 
         loop {
             for component in &mut iter {
-                if scan {
-                    Self::scan_component(component, &mut scope);
+                if scan_for_element_invalidation {
+                    Self::scan_component(component, &mut element_invalidation);
+                } else if scan_for_subtree_invalidation {
+                    Self::scan_component(component, &mut subtree_invalidation);
                 }
             }
             match iter.next_sequence() {
                 None => break,
                 Some(combinator) => {
-                    scan = combinator.is_ancestor();
+                    scan_for_subtree_invalidation = combinator.is_ancestor();
                 }
             }
+            scan_for_element_invalidation = false;
         }
 
-        match scope {
-            Some(s) => {
-                debug!(" > Found scope: {:?}", s);
-                self.invalid_scopes.insert(s);
-            }
-            None => {
-                debug!(" > Scope not found");
-
-                // If we didn't find a scope, any element could match this, so
-                // let's just bail out.
-                self.fully_invalid = true;
-            }
+        if let Some(s) = subtree_invalidation {
+            debug!(" > Found subtree invalidation: {:?}", s);
+            self.invalid_scopes.insert(s);
+        } else if let Some(s) = element_invalidation {
+            debug!(" > Found element invalidation: {:?}", s);
+            self.invalid_elements.insert(s);
+        } else {
+            // The selector was of a form that we can't handle. Any element
+            // could match it, so let's just bail out.
+            debug!(" > Can't handle selector, marking fully invalid");
+            self.fully_invalid = true;
         }
     }
 
     /// Collects invalidations for a given CSS rule.
     fn collect_invalidations_for_rule(
         &mut self,
         rule: &CssRule,
         guard: &SharedRwLockReadGuard)
@@ -289,17 +349,17 @@ impl StylesheetInvalidationSet {
         use stylesheets::CssRule::*;
         debug!("StylesheetInvalidationSet::collect_invalidations_for_rule");
         debug_assert!(!self.fully_invalid, "Not worth to be here!");
 
         match *rule {
             Style(ref lock) => {
                 let style_rule = lock.read_with(guard);
                 for selector in &style_rule.selectors.0 {
-                    self.collect_scopes(selector);
+                    self.collect_invalidations(selector);
                     if self.fully_invalid {
                         return;
                     }
                 }
             }
             Document(..) |
             Namespace(..) |
             Import(..) |