Title: [288502] branches/safari-613-branch
Revision
288502
Author
[email protected]
Date
2022-01-24 17:55:06 -0800 (Mon, 24 Jan 2022)

Log Message

Cherry-pick r288012. rdar://problem/87397176

    [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
    https://bugs.webkit.org/show_bug.cgi?id=234842
    <rdar://problem/87397176>

    Reviewed by Dean Jackson.

    LayoutTests/imported/w3c:

    * web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt: Added.
    * web-platform-tests/css/selectors/invalidation/has-complexity.html: Added.

    Source/WebCore:

    Use invalidation selectors to check if a given mutation needs :has() invalidation.

    Test: imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html

    * css/SelectorChecker.cpp:
    (WebCore::SelectorChecker::checkOne const):
    * css/SelectorChecker.h:
    * style/ChildChangeInvalidation.cpp:
    (WebCore::Style::ChildChangeInvalidation::invalidateForChangedElement):

    Invalidate only if the invalidation ruleset has an invalidation selector that matches
    the added/removed element. Even in that case we only need to invalidate if that selector
    has not already matched within this parent.

    As we don't have persistent state that would remember what already matched accross multiple
    mutations, approximate this by checking if the closest sibling matched.

    (WebCore::Style::ChildChangeInvalidation::invalidateForHasBeforeMutation):
    (WebCore::Style::ChildChangeInvalidation::invalidateForHasAfterMutation):
    * style/ChildChangeInvalidation.h:

    LayoutTests:

    * TestExpectations:

    git-svn-id: https://svn.webkit.org/repository/webkit/trunk@288012 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Modified Paths

Added Paths

Diff

Modified: branches/safari-613-branch/LayoutTests/ChangeLog (288501 => 288502)


--- branches/safari-613-branch/LayoutTests/ChangeLog	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/LayoutTests/ChangeLog	2022-01-25 01:55:06 UTC (rev 288502)
@@ -1,5 +1,60 @@
 2022-01-24  Alan Coon  <[email protected]>
 
+        Cherry-pick r288012. rdar://problem/87397176
+
+    [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
+    https://bugs.webkit.org/show_bug.cgi?id=234842
+    <rdar://problem/87397176>
+    
+    Reviewed by Dean Jackson.
+    
+    LayoutTests/imported/w3c:
+    
+    * web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt: Added.
+    * web-platform-tests/css/selectors/invalidation/has-complexity.html: Added.
+    
+    Source/WebCore:
+    
+    Use invalidation selectors to check if a given mutation needs :has() invalidation.
+    
+    Test: imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html
+    
+    * css/SelectorChecker.cpp:
+    (WebCore::SelectorChecker::checkOne const):
+    * css/SelectorChecker.h:
+    * style/ChildChangeInvalidation.cpp:
+    (WebCore::Style::ChildChangeInvalidation::invalidateForChangedElement):
+    
+    Invalidate only if the invalidation ruleset has an invalidation selector that matches
+    the added/removed element. Even in that case we only need to invalidate if that selector
+    has not already matched within this parent.
+    
+    As we don't have persistent state that would remember what already matched accross multiple
+    mutations, approximate this by checking if the closest sibling matched.
+    
+    (WebCore::Style::ChildChangeInvalidation::invalidateForHasBeforeMutation):
+    (WebCore::Style::ChildChangeInvalidation::invalidateForHasAfterMutation):
+    * style/ChildChangeInvalidation.h:
+    
+    LayoutTests:
+    
+    * TestExpectations:
+    
+    
+    git-svn-id: https://svn.webkit.org/repository/webkit/trunk@288012 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+
+    2022-01-14  Antti Koivisto  <[email protected]>
+
+            [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
+            https://bugs.webkit.org/show_bug.cgi?id=234842
+            <rdar://problem/87397176>
+
+            Reviewed by Dean Jackson.
+
+            * TestExpectations:
+
+2022-01-24  Alan Coon  <[email protected]>
+
         Cherry-pick r287934. rdar://problem/86578732
 
     Web Inspector: Unhandled exception when moving cursor mid-token after receiving CSS property name completions

Modified: branches/safari-613-branch/LayoutTests/TestExpectations (288501 => 288502)


--- branches/safari-613-branch/LayoutTests/TestExpectations	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/LayoutTests/TestExpectations	2022-01-25 01:55:06 UTC (rev 288502)
@@ -1478,6 +1478,9 @@
 webkit.org/b/223497 imported/w3c/web-platform-tests/css/selectors/nesting.html [ ImageOnlyFailure ]
 imported/w3c/web-platform-tests/css/selectors/xml-class-selector.xml [ ImageOnlyFailure ]
 
+# This test is bit heavy for debug
+[ Debug ] imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html [ Skip ]
+
 # ref for this is under a directory that is not yet imported
 webkit.org/b/209735 imported/w3c/web-platform-tests/css/selectors/root-siblings.htm [ Skip ]
 

Modified: branches/safari-613-branch/LayoutTests/imported/w3c/ChangeLog (288501 => 288502)


--- branches/safari-613-branch/LayoutTests/imported/w3c/ChangeLog	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/LayoutTests/imported/w3c/ChangeLog	2022-01-25 01:55:06 UTC (rev 288502)
@@ -1,5 +1,61 @@
 2022-01-24  Alan Coon  <[email protected]>
 
+        Cherry-pick r288012. rdar://problem/87397176
+
+    [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
+    https://bugs.webkit.org/show_bug.cgi?id=234842
+    <rdar://problem/87397176>
+    
+    Reviewed by Dean Jackson.
+    
+    LayoutTests/imported/w3c:
+    
+    * web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt: Added.
+    * web-platform-tests/css/selectors/invalidation/has-complexity.html: Added.
+    
+    Source/WebCore:
+    
+    Use invalidation selectors to check if a given mutation needs :has() invalidation.
+    
+    Test: imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html
+    
+    * css/SelectorChecker.cpp:
+    (WebCore::SelectorChecker::checkOne const):
+    * css/SelectorChecker.h:
+    * style/ChildChangeInvalidation.cpp:
+    (WebCore::Style::ChildChangeInvalidation::invalidateForChangedElement):
+    
+    Invalidate only if the invalidation ruleset has an invalidation selector that matches
+    the added/removed element. Even in that case we only need to invalidate if that selector
+    has not already matched within this parent.
+    
+    As we don't have persistent state that would remember what already matched accross multiple
+    mutations, approximate this by checking if the closest sibling matched.
+    
+    (WebCore::Style::ChildChangeInvalidation::invalidateForHasBeforeMutation):
+    (WebCore::Style::ChildChangeInvalidation::invalidateForHasAfterMutation):
+    * style/ChildChangeInvalidation.h:
+    
+    LayoutTests:
+    
+    * TestExpectations:
+    
+    
+    git-svn-id: https://svn.webkit.org/repository/webkit/trunk@288012 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+
+    2022-01-14  Antti Koivisto  <[email protected]>
+
+            [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
+            https://bugs.webkit.org/show_bug.cgi?id=234842
+            <rdar://problem/87397176>
+
+            Reviewed by Dean Jackson.
+
+            * web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt: Added.
+            * web-platform-tests/css/selectors/invalidation/has-complexity.html: Added.
+
+2022-01-24  Alan Coon  <[email protected]>
+
         Cherry-pick r287878. rdar://problem/85359803
 
     ::backdrop pseudo element should react to associated element event listeners

Added: branches/safari-613-branch/LayoutTests/imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt (0 => 288502)


--- branches/safari-613-branch/LayoutTests/imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt	                        (rev 0)
+++ branches/safari-613-branch/LayoutTests/imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt	2022-01-25 01:55:06 UTC (rev 288502)
@@ -0,0 +1,9 @@
+
+PASS Before appending 25000 elements
+PASS After appending 25000 elements. This should not time out.
+PASS After appending another 25000 elements. This should not time out.
+PASS After appending div with 25000 elements. This should not time out.
+PASS After removing div with 25000 elements. This should not time out.
+PASS After removing 25000 elements one-by-one. This should not time out.
+PASS After removing the remaining elements. This should not time out.
+

Added: branches/safari-613-branch/LayoutTests/imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html (0 => 288502)


--- branches/safari-613-branch/LayoutTests/imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html	                        (rev 0)
+++ branches/safari-613-branch/LayoutTests/imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html	2022-01-25 01:55:06 UTC (rev 288502)
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() invalidation should not be O(n^2)</title>
+<link rel="author" title="Antti Koivisto" href=""
+<script src=""
+<script src=""
+<link rel="help" href=""
+<style>
+div, main { color: grey }
+main:has(span) .subject { color: red }
+main:has(span + span) .subject { color: green }
+main:has(final) .subject { color: blue }
+main:has(nonexistent + span) .subject { color: black }
+main:has(span) span { color: black }
+main:has(nonexistent) span { color: black }
+main:has(div div span) .subject { color: purple }
+</style>
+<main>
+    <div id=container>
+        <span></span>
+    </div>
+    <div id=subject class=subject></div>
+</main>
+<script>
+const grey = 'rgb(128, 128, 128)';
+const red = 'rgb(255, 0, 0)';
+const green = 'rgb(0, 128, 0)';
+const blue = 'rgb(0, 0, 255)';
+const purple = 'rgb(128, 0, 128)';
+
+function testColor(test_name, color) {
+    test(function() {
+        assert_equals(getComputedStyle(subject).color, color);
+    }, test_name);
+}
+
+const count = 25000;
+
+testColor(`Before appending ${count} elements`, red);
+
+for (let i = 0; i < count; ++i) {
+    const span = document.createElement("span");
+    container.appendChild(span);
+}
+
+testColor(`After appending ${count} elements. This should not time out.`, green);
+
+for (let i = 0; i < count - 1; ++i) {
+    const span = document.createElement("span");
+    container.appendChild(span);
+}
+
+const final = document.createElement("final");
+container.appendChild(final);
+
+testColor(`After appending another ${count} elements. This should not time out.`, blue);
+
+const div = document.createElement("div");
+for (let i = 0; i < count; ++i) {
+    const span = document.createElement("span");
+    div.appendChild(span);
+}
+container.appendChild(div);
+
+testColor(`After appending div with ${count} elements. This should not time out.`, purple);
+
+div.remove();
+
+testColor(`After removing div with ${count} elements. This should not time out.`, blue);
+
+for (let i = 0; i < count; ++i)
+    container.lastChild.remove();
+
+testColor(`After removing ${count} elements one-by-one. This should not time out.`, green);
+
+container.replaceChildren();
+
+testColor(`After removing the remaining elements. This should not time out.`, grey);
+
+</script>

Modified: branches/safari-613-branch/Source/WebCore/ChangeLog (288501 => 288502)


--- branches/safari-613-branch/Source/WebCore/ChangeLog	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/Source/WebCore/ChangeLog	2022-01-25 01:55:06 UTC (rev 288502)
@@ -1,5 +1,79 @@
 2022-01-24  Alan Coon  <[email protected]>
 
+        Cherry-pick r288012. rdar://problem/87397176
+
+    [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
+    https://bugs.webkit.org/show_bug.cgi?id=234842
+    <rdar://problem/87397176>
+    
+    Reviewed by Dean Jackson.
+    
+    LayoutTests/imported/w3c:
+    
+    * web-platform-tests/css/selectors/invalidation/has-complexity-expected.txt: Added.
+    * web-platform-tests/css/selectors/invalidation/has-complexity.html: Added.
+    
+    Source/WebCore:
+    
+    Use invalidation selectors to check if a given mutation needs :has() invalidation.
+    
+    Test: imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html
+    
+    * css/SelectorChecker.cpp:
+    (WebCore::SelectorChecker::checkOne const):
+    * css/SelectorChecker.h:
+    * style/ChildChangeInvalidation.cpp:
+    (WebCore::Style::ChildChangeInvalidation::invalidateForChangedElement):
+    
+    Invalidate only if the invalidation ruleset has an invalidation selector that matches
+    the added/removed element. Even in that case we only need to invalidate if that selector
+    has not already matched within this parent.
+    
+    As we don't have persistent state that would remember what already matched accross multiple
+    mutations, approximate this by checking if the closest sibling matched.
+    
+    (WebCore::Style::ChildChangeInvalidation::invalidateForHasBeforeMutation):
+    (WebCore::Style::ChildChangeInvalidation::invalidateForHasAfterMutation):
+    * style/ChildChangeInvalidation.h:
+    
+    LayoutTests:
+    
+    * TestExpectations:
+    
+    
+    git-svn-id: https://svn.webkit.org/repository/webkit/trunk@288012 268f45cc-cd09-0410-ab3c-d52691b4dbfc
+
+    2022-01-14  Antti Koivisto  <[email protected]>
+
+            [:has() pseudo-class] Avoid O(n^2) in style invalidation with repeated DOM mutations
+            https://bugs.webkit.org/show_bug.cgi?id=234842
+            <rdar://problem/87397176>
+
+            Reviewed by Dean Jackson.
+
+            Use invalidation selectors to check if a given mutation needs :has() invalidation.
+
+            Test: imported/w3c/web-platform-tests/css/selectors/invalidation/has-complexity.html
+
+            * css/SelectorChecker.cpp:
+            (WebCore::SelectorChecker::checkOne const):
+            * css/SelectorChecker.h:
+            * style/ChildChangeInvalidation.cpp:
+            (WebCore::Style::ChildChangeInvalidation::invalidateForChangedElement):
+
+            Invalidate only if the invalidation ruleset has an invalidation selector that matches
+            the added/removed element. Even in that case we only need to invalidate if that selector
+            has not already matched within this parent.
+
+            As we don't have persistent state that would remember what already matched accross multiple
+            mutations, approximate this by checking if the closest sibling matched.
+
+            (WebCore::Style::ChildChangeInvalidation::invalidateForHasBeforeMutation):
+            (WebCore::Style::ChildChangeInvalidation::invalidateForHasAfterMutation):
+            * style/ChildChangeInvalidation.h:
+
+2022-01-24  Alan Coon  <[email protected]>
+
         Cherry-pick r287973. rdar://problem/87533906
 
     [:has() pseudo-class] Collect invalidation selectors for child invalidation

Modified: branches/safari-613-branch/Source/WebCore/css/SelectorChecker.cpp (288501 => 288502)


--- branches/safari-613-branch/Source/WebCore/css/SelectorChecker.cpp	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/Source/WebCore/css/SelectorChecker.cpp	2022-01-25 01:55:06 UTC (rev 288502)
@@ -1084,7 +1084,7 @@
         case CSSSelector::PseudoClassRelativeScope: {
             const Node* contextualReferenceNode = !checkingContext.scope ? element.document().documentElement() : checkingContext.scope;
 
-            bool matches = &element == contextualReferenceNode;
+            bool matches = &element == contextualReferenceNode || checkingContext.matchesAllScopes;
 
             if (!matches && checkingContext.scope) {
                 if (element.isDescendantOf(*checkingContext.scope))

Modified: branches/safari-613-branch/Source/WebCore/css/SelectorChecker.h (288501 => 288502)


--- branches/safari-613-branch/Source/WebCore/css/SelectorChecker.h	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/Source/WebCore/css/SelectorChecker.h	2022-01-25 01:55:06 UTC (rev 288502)
@@ -95,6 +95,7 @@
         std::optional<StyleScrollbarState> scrollbarState;
         AtomString nameForHightlightPseudoElement;
         const ContainerNode* scope { nullptr };
+        bool matchesAllScopes { false };
         Style::ScopeOrdinal styleScopeOrdinal { Style::ScopeOrdinal::Element };
         Style::SelectorMatchingState* selectorMatchingState { nullptr };
 

Modified: branches/safari-613-branch/Source/WebCore/style/ChildChangeInvalidation.cpp (288501 => 288502)


--- branches/safari-613-branch/Source/WebCore/style/ChildChangeInvalidation.cpp	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/Source/WebCore/style/ChildChangeInvalidation.cpp	2022-01-25 01:55:06 UTC (rev 288502)
@@ -37,28 +37,66 @@
 
 namespace WebCore::Style {
 
-void ChildChangeInvalidation::invalidateForChangedElement(Element& changedElement)
+void ChildChangeInvalidation::invalidateForChangedElement(Element& changedElement, MatchingHasSelectors& matchingHasSelectors)
 {
     auto& ruleSets = parentElement().styleResolver().ruleSets();
 
     Invalidator::MatchElementRuleSets matchElementRuleSets;
 
-    bool isDescendant = changedElement.parentElement() != &parentElement();
+    bool isChild = changedElement.parentElement() == &parentElement();
 
-    auto canAffectAncestors = [&](MatchElement matchElement) {
-        if (!isDescendant)
+    auto canAffectElementsWithStyle = [&](MatchElement matchElement) {
+        switch (matchElement) {
+        case MatchElement::HasSibling:
+        case MatchElement::HasChild:
+            return isChild;
+        case MatchElement::HasDescendant:
+        case MatchElement::HasSiblingDescendant:
+        case MatchElement::HasNonSubject:
             return true;
-        return matchElement == MatchElement::HasDescendant
-            || matchElement == MatchElement::HasSiblingDescendant
-            || matchElement == MatchElement::HasNonSubject;
+        default:
+            ASSERT_NOT_REACHED();
+            return false;
+        }
     };
 
+    bool isFirst = isChild && m_childChange.previousSiblingElement == changedElement.previousElementSibling();
+
+    auto hasMatchingInvalidationSelector = [&](auto& invalidationRuleSet) {
+        SelectorChecker selectorChecker(changedElement.document());
+        SelectorChecker::CheckingContext checkingContext(SelectorChecker::Mode::CollectingRulesIgnoringVirtualPseudoElements);
+        checkingContext.matchesAllScopes = true;
+
+        for (auto* selector : invalidationRuleSet.invalidationSelectors) {
+            if (isFirst) {
+                // If this :has() matches ignoring this mutation, nothing actually changes and we don't need to invalidate.
+                // FIXME: We could cache this state across invalidations instead of just testing a single sibling.
+                auto* sibling = m_childChange.previousSiblingElement ? m_childChange.previousSiblingElement : m_childChange.nextSiblingElement;
+                if (sibling && selectorChecker.match(*selector, *sibling, checkingContext)) {
+                    matchingHasSelectors.add(selector);
+                    continue;
+                }
+            }
+
+            if (matchingHasSelectors.contains(selector))
+                continue;
+
+            if (selectorChecker.match(*selector, changedElement, checkingContext)) {
+                matchingHasSelectors.add(selector);
+                return true;
+            }
+        }
+        return false;
+    };
+
     auto addHasInvalidation = [&](const Vector<InvalidationRuleSet>* invalidationRuleSets)  {
         if (!invalidationRuleSets)
             return;
         for (auto& invalidationRuleSet : *invalidationRuleSets) {
-            if (!canAffectAncestors(invalidationRuleSet.matchElement))
+            if (!canAffectElementsWithStyle(invalidationRuleSet.matchElement))
                 continue;
+            if (!hasMatchingInvalidationSelector(invalidationRuleSet))
+                continue;
             Invalidator::addToMatchElementRuleSets(matchElementRuleSets, invalidationRuleSet);
         }
     };
@@ -76,8 +114,10 @@
     if (m_childChange.isInsertion() && m_childChange.type != ContainerNode::ChildChange::Type::AllChildrenReplaced)
         return;
 
+    MatchingHasSelectors matchingHasSelectors;
+
     traverseRemovedElements([&](auto& changedElement) {
-        invalidateForChangedElement(changedElement);
+        invalidateForChangedElement(changedElement, matchingHasSelectors);
     });
 }
 
@@ -88,8 +128,10 @@
     if (!m_childChange.isInsertion())
         return;
 
+    MatchingHasSelectors matchingHasSelectors;
+
     traverseAddedElements([&](auto& changedElement) {
-        invalidateForChangedElement(changedElement);
+        invalidateForChangedElement(changedElement, matchingHasSelectors);
     });
 }
 
@@ -106,8 +148,9 @@
     auto& features = parentElement().styleResolver().ruleSets().features();
     bool needsDescendantTraversal = Style::needsDescendantTraversal(features);
 
-    auto* toRemove = m_childChange.previousSiblingElement ? m_childChange.previousSiblingElement->nextElementSibling() : parentElement().firstElementChild();
-    for (; toRemove != m_childChange.nextSiblingElement; toRemove = toRemove->nextElementSibling()) {
+    auto* firstToRemove = m_childChange.previousSiblingElement ? m_childChange.previousSiblingElement->nextElementSibling() : parentElement().firstElementChild();
+
+    for (auto* toRemove = firstToRemove; toRemove != m_childChange.nextSiblingElement; toRemove = toRemove->nextElementSibling()) {
         function(*toRemove);
 
         if (!needsDescendantTraversal)

Modified: branches/safari-613-branch/Source/WebCore/style/ChildChangeInvalidation.h (288501 => 288502)


--- branches/safari-613-branch/Source/WebCore/style/ChildChangeInvalidation.h	2022-01-25 01:55:01 UTC (rev 288501)
+++ branches/safari-613-branch/Source/WebCore/style/ChildChangeInvalidation.h	2022-01-25 01:55:06 UTC (rev 288502)
@@ -28,6 +28,7 @@
 #include "Element.h"
 #include "StyleInvalidator.h"
 #include "StyleScope.h"
+#include <wtf/HashSet.h>
 
 namespace WebCore {
 namespace Style {
@@ -44,7 +45,8 @@
     void invalidateForHasAfterMutation();
     void invalidateAfterChange();
     void checkForSiblingStyleChanges();
-    void invalidateForChangedElement(Element&);
+    using MatchingHasSelectors = HashSet<const CSSSelector*>;
+    void invalidateForChangedElement(Element&, MatchingHasSelectors&);
 
     template<typename Function> void traverseRemovedElements(Function&&);
     template<typename Function> void traverseAddedElements(Function&&);
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to