Title: [204368] trunk
Revision
204368
Author
cdu...@apple.com
Date
2016-08-10 19:21:42 -0700 (Wed, 10 Aug 2016)

Log Message

Optimization in Node.insertBefore() is not spec-compliant
https://bugs.webkit.org/show_bug.cgi?id=160746

Reviewed by Ryosuke Niwa.

LayoutTests/imported/w3c:

Rebaseline W3C test now that more checks are passing.

* web-platform-tests/dom/ranges/Range-mutations-expected.txt:

Source/WebCore:

We have an optimization in Node.insertBefore(newNode, refChild) to avoid
doing any work when newNode == refChild or newNode.nextSibling == refChild.

This optimization is not in the specification:
- https://dom.spec.whatwg.org/#concept-node-replace

The issue is that this optimization is observable with DOM mutation
observers / listeners or DOM ranges.

This patch addresses the issue by dropping the optimization. This case
does not seem common enough to be worth optimizing for. However, if it
turns out to regress the performance of things we care about, we could
fallback to doing the optimization only when it is not observable.

Test: fast/dom/Node/insertBefore-no-op-mutationobserver.html

* dom/ContainerNode.cpp:
(WebCore::checkAcceptChild):
Move refChild->parent() == parent check from insertBefore() to our
pre-insertion check function, right after checking if newNode contains
parent. This was done in order to match more closely the specification
and to make sure that exception are thrown in the correct order:
- https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity (steps 2 and 3)

(WebCore::ContainerNode::insertBefore):
1. Drop the (refChild->previousSibling() == &newChild || refChild == &newChild)
   optimization as it is not spc-compliant.
2. If refChild is newNode, then set refChild to newChild.nextSibling as per:
   - https://dom.spec.whatwg.org/#concept-node-pre-insert (step 3)

LayoutTests:

Add layout test to make sure mutation observers / listeners are always
notified when Node.insertBefore() is called.

* fast/dom/Node/insertBefore-no-op-mutationobserver-expected.txt: Added.
* fast/dom/Node/insertBefore-no-op-mutationobserver.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (204367 => 204368)


--- trunk/LayoutTests/ChangeLog	2016-08-11 02:08:17 UTC (rev 204367)
+++ trunk/LayoutTests/ChangeLog	2016-08-11 02:21:42 UTC (rev 204368)
@@ -1,3 +1,16 @@
+2016-08-10  Chris Dumez  <cdu...@apple.com>
+
+        Optimization in Node.insertBefore() is not spec-compliant
+        https://bugs.webkit.org/show_bug.cgi?id=160746
+
+        Reviewed by Ryosuke Niwa.
+
+        Add layout test to make sure mutation observers / listeners are always
+        notified when Node.insertBefore() is called.
+
+        * fast/dom/Node/insertBefore-no-op-mutationobserver-expected.txt: Added.
+        * fast/dom/Node/insertBefore-no-op-mutationobserver.html: Added.
+
 2016-08-10  Ryosuke Niwa  <rn...@webkit.org>
 
         Move document.defineElement to customElements.define

Added: trunk/LayoutTests/fast/dom/Node/insertBefore-no-op-mutationobserver-expected.txt (0 => 204368)


--- trunk/LayoutTests/fast/dom/Node/insertBefore-no-op-mutationobserver-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/fast/dom/Node/insertBefore-no-op-mutationobserver-expected.txt	2016-08-11 02:21:42 UTC (rev 204368)
@@ -0,0 +1,13 @@
+Tests that calls of insertBefore() lead to mutation events even if they are no-ops.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Mutation observer was notified
+PASS wasMutationEventListenerCalled is true
+PASS Mutation observer was notified
+PASS wasMutationEventListenerCalled is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/fast/dom/Node/insertBefore-no-op-mutationobserver.html (0 => 204368)


--- trunk/LayoutTests/fast/dom/Node/insertBefore-no-op-mutationobserver.html	                        (rev 0)
+++ trunk/LayoutTests/fast/dom/Node/insertBefore-no-op-mutationobserver.html	2016-08-11 02:21:42 UTC (rev 204368)
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src=""
+<div id="testDiv"><p></p><p></p></div>
+<script>
+description("Tests that calls of insertBefore() lead to mutation events even if they are no-ops.");
+jsTestIsAsync = true;
+
+var testDiv = document.getElementById("testDiv");
+var p1 = testDiv.firstChild;
+var p2 = testDiv.lastChild;
+
+var operations = [
+    function() { testDiv.insertBefore(p1, p1); },
+    function() { testDiv.insertBefore(p1, p2); }
+];
+var currentOperation = 0;
+
+function testMutationObserver()
+{
+    var config = { childList: true };
+    var observer = new MutationObserver(function(mutations) {
+        testPassed("Mutation observer was notified");
+        observer.disconnect();
+        testMutationEventListener();
+    });
+
+    observer.observe(testDiv, config);
+    operations[currentOperation]();
+}
+
+function subtreeModifiedHandler()
+{
+    wasMutationEventListenerCalled = true;
+}
+
+function testMutationEventListener()
+{
+    wasMutationEventListenerCalled = false;
+    testDiv.addEventListener("DOMSubtreeModified", subtreeModifiedHandler);
+    operations[currentOperation]();
+    shouldBeTrue("wasMutationEventListenerCalled");
+    testDiv.removeEventListener("DOMSubtreeModified", subtreeModifiedHandler);
+
+    currentOperation++;
+    if (currentOperation < operations.length)
+        testMutationObserver();
+    else
+        finishJSTest();
+}
+
+testMutationObserver();
+</script>
+<script src=""
+</body>
+</html>

Modified: trunk/LayoutTests/imported/w3c/ChangeLog (204367 => 204368)


--- trunk/LayoutTests/imported/w3c/ChangeLog	2016-08-11 02:08:17 UTC (rev 204367)
+++ trunk/LayoutTests/imported/w3c/ChangeLog	2016-08-11 02:21:42 UTC (rev 204368)
@@ -1,3 +1,14 @@
+2016-08-10  Chris Dumez  <cdu...@apple.com>
+
+        Optimization in Node.insertBefore() is not spec-compliant
+        https://bugs.webkit.org/show_bug.cgi?id=160746
+
+        Reviewed by Ryosuke Niwa.
+
+        Rebaseline W3C test now that more checks are passing.
+
+        * web-platform-tests/dom/ranges/Range-mutations-expected.txt:
+
 2016-08-09  Chris Dumez  <cdu...@apple.com>
 
         Optimization in Node.replaceChild() is not spec-compliant

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt (204367 => 204368)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt	2016-08-11 02:08:17 UTC (rev 204367)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt	2016-08-11 02:21:42 UTC (rev 204368)
@@ -5507,18 +5507,12 @@
 FAIL detachedXmlComment.nodeValue += "foo", with selected range collapsed at (detachedXmlComment, detachedXmlComment.length) assert_equals: Sanity check: selection must have exactly one range after adding the range expected 1 but got 0
 PASS detachedXmlComment.nodeValue += detachedXmlComment.nodeValue, with unselected range collapsed at (detachedXmlComment, detachedXmlComment.length) 
 FAIL detachedXmlComment.nodeValue += detachedXmlComment.nodeValue, with selected range collapsed at (detachedXmlComment, detachedXmlComment.length) assert_equals: Sanity check: selection must have exactly one range after adding the range expected 1 but got 0
-FAIL testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (paras[0], 0) assert_equals: Wrong start container expected Element node <div id="test"><p id="a">Äb̈c̈d̈ëf̈g̈ḧ
-</p><p id="b" s... but got Element node <p id="a">Äb̈c̈d̈ëf̈g̈ḧ
-</p>
+PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (paras[0], 0) 
 FAIL testDiv.insertBefore(paras[0], paras[1]), with selected range collapsed at (paras[0], 0) assert_equals: Sanity check: selection's range must initially be the same as the range we added expected object "" but got object ""
-FAIL testDiv.insertBefore(paras[0], paras[1]), with unselected range on paras[0] from 0 to 1 assert_equals: Wrong start container expected Element node <div id="test"><p id="a">Äb̈c̈d̈ëf̈g̈ḧ
-</p><p id="b" s... but got Element node <p id="a">Äb̈c̈d̈ëf̈g̈ḧ
-</p>
+PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range on paras[0] from 0 to 1 
 FAIL testDiv.insertBefore(paras[0], paras[1]), with selected range on paras[0] from 0 to 1 assert_equals: Sanity check: selection's range must initially be the same as the range we added expected object "Äb̈c̈d̈ëf̈g̈ḧ
 " but got object "Äb̈c̈d̈ëf̈g̈ḧ"
-FAIL testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (paras[0], 1) assert_equals: Wrong start container expected Element node <div id="test"><p id="a">Äb̈c̈d̈ëf̈g̈ḧ
-</p><p id="b" s... but got Element node <p id="a">Äb̈c̈d̈ëf̈g̈ḧ
-</p>
+PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (paras[0], 1) 
 FAIL testDiv.insertBefore(paras[0], paras[1]), with selected range collapsed at (paras[0], 1) assert_equals: Sanity check: selection's range must initially be the same as the range we added expected object "" but got object ""
 PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range on testDiv from 0 to 2 
 FAIL testDiv.insertBefore(paras[0], paras[1]), with selected range on testDiv from 0 to 2 assert_equals: Sanity check: selection's range must initially be the same as the range we added expected object "Äb̈c̈d̈ëf̈g̈ḧ
@@ -5526,9 +5520,9 @@
 " but got object "Äb̈c̈d̈ëf̈g̈ḧ
 Ijklmnop
 "
-FAIL testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (testDiv, 1) assert_equals: Wrong start offset expected 0 but got 1
+PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (testDiv, 1) 
 FAIL testDiv.insertBefore(paras[0], paras[1]), with selected range collapsed at (testDiv, 1) assert_equals: Sanity check: selection's range must initially be the same as the range we added expected object "" but got object ""
-FAIL testDiv.insertBefore(paras[0], paras[1]), with unselected range on testDiv from 1 to 2 assert_equals: Wrong start offset expected 0 but got 1
+PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range on testDiv from 1 to 2 
 FAIL testDiv.insertBefore(paras[0], paras[1]), with selected range on testDiv from 1 to 2 assert_equals: Sanity check: selection's range must initially be the same as the range we added expected object "Ijklmnop
 " but got object ""
 PASS testDiv.insertBefore(paras[0], paras[1]), with unselected range collapsed at (testDiv, 2) 

Modified: trunk/Source/WebCore/ChangeLog (204367 => 204368)


--- trunk/Source/WebCore/ChangeLog	2016-08-11 02:08:17 UTC (rev 204367)
+++ trunk/Source/WebCore/ChangeLog	2016-08-11 02:21:42 UTC (rev 204368)
@@ -1,3 +1,40 @@
+2016-08-10  Chris Dumez  <cdu...@apple.com>
+
+        Optimization in Node.insertBefore() is not spec-compliant
+        https://bugs.webkit.org/show_bug.cgi?id=160746
+
+        Reviewed by Ryosuke Niwa.
+
+        We have an optimization in Node.insertBefore(newNode, refChild) to avoid
+        doing any work when newNode == refChild or newNode.nextSibling == refChild.
+
+        This optimization is not in the specification:
+        - https://dom.spec.whatwg.org/#concept-node-replace
+
+        The issue is that this optimization is observable with DOM mutation
+        observers / listeners or DOM ranges.
+
+        This patch addresses the issue by dropping the optimization. This case
+        does not seem common enough to be worth optimizing for. However, if it
+        turns out to regress the performance of things we care about, we could
+        fallback to doing the optimization only when it is not observable.
+
+        Test: fast/dom/Node/insertBefore-no-op-mutationobserver.html
+
+        * dom/ContainerNode.cpp:
+        (WebCore::checkAcceptChild):
+        Move refChild->parent() == parent check from insertBefore() to our
+        pre-insertion check function, right after checking if newNode contains
+        parent. This was done in order to match more closely the specification
+        and to make sure that exception are thrown in the correct order:
+        - https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity (steps 2 and 3)
+
+        (WebCore::ContainerNode::insertBefore):
+        1. Drop the (refChild->previousSibling() == &newChild || refChild == &newChild)
+           optimization as it is not spc-compliant.
+        2. If refChild is newNode, then set refChild to newChild.nextSibling as per:
+           - https://dom.spec.whatwg.org/#concept-node-pre-insert (step 3)
+
 2016-08-10  Ryosuke Niwa  <rn...@webkit.org>
 
         Move document.defineElement to customElements.define

Modified: trunk/Source/WebCore/dom/ContainerNode.cpp (204367 => 204368)


--- trunk/Source/WebCore/dom/ContainerNode.cpp	2016-08-11 02:08:17 UTC (rev 204367)
+++ trunk/Source/WebCore/dom/ContainerNode.cpp	2016-08-11 02:21:42 UTC (rev 204368)
@@ -183,6 +183,8 @@
         ASSERT(isChildTypeAllowed(newParent, newChild));
         if (containsConsideringHostElements(newChild, newParent))
             return HIERARCHY_REQUEST_ERR;
+        if (operation == Document::AcceptChildOperation::InsertOrAdd && refChild && refChild->parentNode() != &newParent)
+            return NOT_FOUND_ERR;
         return 0;
     }
 
@@ -194,6 +196,9 @@
     if (containsConsideringHostElements(newChild, newParent))
         return HIERARCHY_REQUEST_ERR;
 
+    if (operation == Document::AcceptChildOperation::InsertOrAdd && refChild && refChild->parentNode() != &newParent)
+        return NOT_FOUND_ERR;
+
     if (is<Document>(newParent)) {
         if (!downcast<Document>(newParent).canAcceptChild(newChild, refChild, operation))
             return HIERARCHY_REQUEST_ERR;
@@ -235,27 +240,20 @@
     // If it is, it can be deleted as a side effect of sending mutation events.
     ASSERT(refCount() || parentOrShadowHostNode());
 
-    Ref<ContainerNode> protectedThis(*this);
-
     ec = 0;
 
-    // insertBefore(node, 0) is equivalent to appendChild(node)
-    if (!refChild)
-        return appendChild(newChild, ec);
-
     // Make sure adding the new child is OK.
     if (!ensurePreInsertionValidity(newChild, refChild, ec))
         return false;
 
-    // NOT_FOUND_ERR: Raised if refChild is not a child of this node
-    if (refChild->parentNode() != this) {
-        ec = NOT_FOUND_ERR;
-        return false;
-    }
+    if (refChild == &newChild)
+        refChild = newChild.nextSibling();
 
-    if (refChild->previousSibling() == &newChild || refChild == &newChild) // nothing to do
-        return true;
+    // insertBefore(node, null) is equivalent to appendChild(node)
+    if (!refChild)
+        return appendChildWithoutPreInsertionValidityCheck(newChild, ec);
 
+    Ref<ContainerNode> protectedThis(*this);
     Ref<Node> next(*refChild);
 
     NodeVector targets;
@@ -640,8 +638,6 @@
 
 bool ContainerNode::appendChild(Node& newChild, ExceptionCode& ec)
 {
-    Ref<ContainerNode> protectedThis(*this);
-
     // Check that this node is not "floating".
     // If it is, it can be deleted as a side effect of sending mutation events.
     ASSERT(refCount() || parentOrShadowHostNode());
@@ -652,6 +648,13 @@
     if (!ensurePreInsertionValidity(newChild, nullptr, ec))
         return false;
 
+    return appendChildWithoutPreInsertionValidityCheck(newChild, ec);
+}
+
+bool ContainerNode::appendChildWithoutPreInsertionValidityCheck(Node& newChild, ExceptionCode& ec)
+{
+    Ref<ContainerNode> protectedThis(*this);
+
     NodeVector targets;
     collectChildrenAndRemoveFromOldParent(newChild, targets, ec);
     if (ec)

Modified: trunk/Source/WebCore/dom/ContainerNode.h (204367 => 204368)


--- trunk/Source/WebCore/dom/ContainerNode.h	2016-08-11 02:08:17 UTC (rev 204367)
+++ trunk/Source/WebCore/dom/ContainerNode.h	2016-08-11 02:21:42 UTC (rev 204368)
@@ -117,6 +117,7 @@
 
 private:
     void removeBetween(Node* previousChild, Node* nextChild, Node& oldChild);
+    bool appendChildWithoutPreInsertionValidityCheck(Node&, ExceptionCode&);
     void insertBeforeCommon(Node& nextChild, Node& oldChild);
     void appendChildCommon(Node&);
 
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to