<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[204326] trunk</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/204326">204326</a></dd>
<dt>Author</dt> <dd>cdumez@apple.com</dd>
<dt>Date</dt> <dd>2016-08-09 22:44:31 -0700 (Tue, 09 Aug 2016)</dd>
</dl>

<h3>Log Message</h3>
<pre>Optimization in Node.replaceChild() is not spec-compliant
https://bugs.webkit.org/show_bug.cgi?id=160730

Reviewed by Ryosuke Niwa.

LayoutTests/imported/w3c:

Rebaseline several W3C tests now that more checks are passing.

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

Source/WebCore:

We have an optimization in Node.replaceChild() to avoid doing any work
when the reference child and the new child are the same node. This
optimization is not spec-compliant:
- https://dom.spec.whatwg.org/#concept-node-replace

This is an issue because it is observable via Mutation observers /
listeners or DOM ranges.

To address the issue, this patch drops the optimization. This is
probably not common enough to be worth optimizing for. However,
if it turns out to regress performance of things we care about,
we can fall back to do the optimization only in cases where it
is not observable.

No new tests, rebaselined existing tests.

* dom/ContainerNode.cpp:
(WebCore::ContainerNode::replaceChild):
1. Drop the oldChild == newChild optimization which is not in the
   specification.
2. Add a null check for oldChild.parentNode() before trying to
   remove it from its parent, as per step 12 of the specification.
   oldChild.parentNode() is null when oldChild is newChild.
3. Make sure we enqueue separate mutation records for the adoption
   of newNode into parent's node document and for the removal of
   oldChild / insertion of newChild. This is mandated by the
   specification (steps 10 and 15). Without this change, the
   following test would still not pass after dropping the
   optimization:
   imported/w3c/web-platform-tests/dom/nodes/MutationObserver-childList.html</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsimportedw3cChangeLog">trunk/LayoutTests/imported/w3c/ChangeLog</a></li>
<li><a href="#trunkLayoutTestsimportedw3cwebplatformtestsdomnodesMutationObserverchildListexpectedtxt">trunk/LayoutTests/imported/w3c/web-platform-tests/dom/nodes/MutationObserver-childList-expected.txt</a></li>
<li><a href="#trunkLayoutTestsimportedw3cwebplatformtestsdomrangesRangemutationsexpectedtxt">trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt</a></li>
<li><a href="#trunkSourceWebCoreChangeLog">trunk/Source/WebCore/ChangeLog</a></li>
<li><a href="#trunkSourceWebCoredomContainerNodecpp">trunk/Source/WebCore/dom/ContainerNode.cpp</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkLayoutTestsimportedw3cChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/imported/w3c/ChangeLog (204325 => 204326)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/imported/w3c/ChangeLog        2016-08-10 04:08:48 UTC (rev 204325)
+++ trunk/LayoutTests/imported/w3c/ChangeLog        2016-08-10 05:44:31 UTC (rev 204326)
</span><span class="lines">@@ -1,5 +1,17 @@
</span><span class="cx"> 2016-08-09  Chris Dumez  &lt;cdumez@apple.com&gt;
</span><span class="cx"> 
</span><ins>+        Optimization in Node.replaceChild() is not spec-compliant
+        https://bugs.webkit.org/show_bug.cgi?id=160730
+
+        Reviewed by Ryosuke Niwa.
+
+        Rebaseline several W3C tests now that more checks are passing.
+
+        * web-platform-tests/dom/nodes/MutationObserver-childList-expected.txt:
+        * web-platform-tests/dom/ranges/Range-mutations-expected.txt:
+
+2016-08-09  Chris Dumez  &lt;cdumez@apple.com&gt;
+
</ins><span class="cx">         Optimization in Node::appendChild() is not spec-compliant
</span><span class="cx">         https://bugs.webkit.org/show_bug.cgi?id=160728
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkLayoutTestsimportedw3cwebplatformtestsdomnodesMutationObserverchildListexpectedtxt"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/dom/nodes/MutationObserver-childList-expected.txt (204325 => 204326)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/imported/w3c/web-platform-tests/dom/nodes/MutationObserver-childList-expected.txt        2016-08-10 04:08:48 UTC (rev 204325)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/dom/nodes/MutationObserver-childList-expected.txt        2016-08-10 05:44:31 UTC (rev 204326)
</span><span class="lines">@@ -1,8 +1,6 @@
</span><span class="cx"> MutationObservers: childList mutations
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-Harness Error (TIMEOUT), message = null
-
</del><span class="cx"> PASS childList Node.nodeValue: no mutation 
</span><span class="cx"> PASS childList Node.textContent: replace content mutation 
</span><span class="cx"> PASS childList Node.textContent: no previous content mutation 
</span><span class="lines">@@ -25,7 +23,7 @@
</span><span class="cx"> PASS childList Node.replaceChild: replacement mutation 
</span><span class="cx"> PASS childList Node.replaceChild: removal mutation 
</span><span class="cx"> PASS childList Node.replaceChild: internal replacement mutation 
</span><del>-TIMEOUT childList Node.replaceChild: self internal replacement mutation Test timed out
</del><ins>+PASS childList Node.replaceChild: self internal replacement mutation 
</ins><span class="cx"> PASS childList Node.removeChild: removal mutation 
</span><span class="cx"> PASS Range (r70) is created 
</span><span class="cx"> PASS childList Range.deleteContents: child removal mutation 
</span></span></pre></div>
<a id="trunkLayoutTestsimportedw3cwebplatformtestsdomrangesRangemutationsexpectedtxt"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt (204325 => 204326)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt        2016-08-10 04:08:48 UTC (rev 204325)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/dom/ranges/Range-mutations-expected.txt        2016-08-10 05:44:31 UTC (rev 204326)
</span><span class="lines">@@ -5619,18 +5619,12 @@
</span><span class="cx"> PASS paras[0].insertBefore(document.doctype, paras[0].firstChild), with unselected range on paras[0] from 0 to 1 
</span><span class="cx"> FAIL paras[0].insertBefore(document.doctype, paras[0].firstChild), 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 &quot;Äb̈c̈d̈ëf̈g̈ḧ
</span><span class="cx"> &quot; but got object &quot;Äb̈c̈d̈ëf̈g̈ḧ&quot;
</span><del>-FAIL testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (paras[0], 0) assert_equals: Wrong start container expected Element node &lt;div id=&quot;test&quot;&gt;&lt;p id=&quot;a&quot;&gt;Äb̈c̈d̈ëf̈g̈ḧ
-&lt;/p&gt;&lt;p id=&quot;b&quot; s... but got Element node &lt;p id=&quot;a&quot;&gt;Äb̈c̈d̈ëf̈g̈ḧ
-&lt;/p&gt;
</del><ins>+PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (paras[0], 0) 
</ins><span class="cx"> FAIL testDiv.replaceChild(paras[0], paras[0]), 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 &quot;&quot; but got object &quot;&quot;
</span><del>-FAIL testDiv.replaceChild(paras[0], paras[0]), with unselected range on paras[0] from 0 to 1 assert_equals: Wrong start container expected Element node &lt;div id=&quot;test&quot;&gt;&lt;p id=&quot;a&quot;&gt;Äb̈c̈d̈ëf̈g̈ḧ
-&lt;/p&gt;&lt;p id=&quot;b&quot; s... but got Element node &lt;p id=&quot;a&quot;&gt;Äb̈c̈d̈ëf̈g̈ḧ
-&lt;/p&gt;
</del><ins>+PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range on paras[0] from 0 to 1 
</ins><span class="cx"> FAIL testDiv.replaceChild(paras[0], paras[0]), 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 &quot;Äb̈c̈d̈ëf̈g̈ḧ
</span><span class="cx"> &quot; but got object &quot;Äb̈c̈d̈ëf̈g̈ḧ&quot;
</span><del>-FAIL testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (paras[0], 1) assert_equals: Wrong start container expected Element node &lt;div id=&quot;test&quot;&gt;&lt;p id=&quot;a&quot;&gt;Äb̈c̈d̈ëf̈g̈ḧ
-&lt;/p&gt;&lt;p id=&quot;b&quot; s... but got Element node &lt;p id=&quot;a&quot;&gt;Äb̈c̈d̈ëf̈g̈ḧ
-&lt;/p&gt;
</del><ins>+PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (paras[0], 1) 
</ins><span class="cx"> FAIL testDiv.replaceChild(paras[0], paras[0]), 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 &quot;&quot; but got object &quot;&quot;
</span><span class="cx"> PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range on testDiv from 0 to 2 
</span><span class="cx"> FAIL testDiv.replaceChild(paras[0], paras[0]), 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 &quot;Äb̈c̈d̈ëf̈g̈ḧ
</span><span class="lines">@@ -5638,9 +5632,9 @@
</span><span class="cx"> &quot; but got object &quot;Äb̈c̈d̈ëf̈g̈ḧ
</span><span class="cx"> Ijklmnop
</span><span class="cx"> &quot;
</span><del>-FAIL testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (testDiv, 1) assert_equals: Wrong start offset expected 0 but got 1
</del><ins>+PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (testDiv, 1) 
</ins><span class="cx"> FAIL testDiv.replaceChild(paras[0], paras[0]), 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 &quot;&quot; but got object &quot;&quot;
</span><del>-FAIL testDiv.replaceChild(paras[0], paras[0]), with unselected range on testDiv from 1 to 2 assert_equals: Wrong start offset expected 0 but got 1
</del><ins>+PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range on testDiv from 1 to 2 
</ins><span class="cx"> FAIL testDiv.replaceChild(paras[0], paras[0]), 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 &quot;Ijklmnop
</span><span class="cx"> &quot; but got object &quot;&quot;
</span><span class="cx"> PASS testDiv.replaceChild(paras[0], paras[0]), with unselected range collapsed at (testDiv, 2) 
</span></span></pre></div>
<a id="trunkSourceWebCoreChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/ChangeLog (204325 => 204326)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/ChangeLog        2016-08-10 04:08:48 UTC (rev 204325)
+++ trunk/Source/WebCore/ChangeLog        2016-08-10 05:44:31 UTC (rev 204326)
</span><span class="lines">@@ -1,5 +1,43 @@
</span><span class="cx"> 2016-08-09  Chris Dumez  &lt;cdumez@apple.com&gt;
</span><span class="cx"> 
</span><ins>+        Optimization in Node.replaceChild() is not spec-compliant
+        https://bugs.webkit.org/show_bug.cgi?id=160730
+
+        Reviewed by Ryosuke Niwa.
+
+        We have an optimization in Node.replaceChild() to avoid doing any work
+        when the reference child and the new child are the same node. This
+        optimization is not spec-compliant:
+        - https://dom.spec.whatwg.org/#concept-node-replace
+
+        This is an issue because it is observable via Mutation observers /
+        listeners or DOM ranges.
+
+        To address the issue, this patch drops the optimization. This is
+        probably not common enough to be worth optimizing for. However,
+        if it turns out to regress performance of things we care about,
+        we can fall back to do the optimization only in cases where it
+        is not observable.
+
+        No new tests, rebaselined existing tests.
+
+        * dom/ContainerNode.cpp:
+        (WebCore::ContainerNode::replaceChild):
+        1. Drop the oldChild == newChild optimization which is not in the
+           specification.
+        2. Add a null check for oldChild.parentNode() before trying to
+           remove it from its parent, as per step 12 of the specification.
+           oldChild.parentNode() is null when oldChild is newChild.
+        3. Make sure we enqueue separate mutation records for the adoption
+           of newNode into parent's node document and for the removal of
+           oldChild / insertion of newChild. This is mandated by the
+           specification (steps 10 and 15). Without this change, the
+           following test would still not pass after dropping the
+           optimization:
+           imported/w3c/web-platform-tests/dom/nodes/MutationObserver-childList.html
+
+2016-08-09  Chris Dumez  &lt;cdumez@apple.com&gt;
+
</ins><span class="cx">         Optimization in Node::appendChild() is not spec-compliant
</span><span class="cx">         https://bugs.webkit.org/show_bug.cgi?id=160728
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkSourceWebCoredomContainerNodecpp"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebCore/dom/ContainerNode.cpp (204325 => 204326)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebCore/dom/ContainerNode.cpp        2016-08-10 04:08:48 UTC (rev 204325)
+++ trunk/Source/WebCore/dom/ContainerNode.cpp        2016-08-10 05:44:31 UTC (rev 204326)
</span><span class="lines">@@ -402,19 +402,17 @@
</span><span class="cx">         return false;
</span><span class="cx">     }
</span><span class="cx"> 
</span><del>-    if (&amp;oldChild == &amp;newChild) // nothing to do
-        return true;
-
-    ChildListMutationScope mutation(*this);
-
</del><span class="cx">     RefPtr&lt;Node&gt; refChild = oldChild.nextSibling();
</span><span class="cx">     if (refChild.get() == &amp;newChild)
</span><span class="cx">         refChild = refChild-&gt;nextSibling();
</span><span class="cx"> 
</span><span class="cx">     NodeVector targets;
</span><del>-    collectChildrenAndRemoveFromOldParent(newChild, targets, ec);
-    if (ec)
-        return false;
</del><ins>+    {
+        ChildListMutationScope mutation(*this);
+        collectChildrenAndRemoveFromOldParent(newChild, targets, ec);
+        if (ec)
+            return false;
+    }
</ins><span class="cx"> 
</span><span class="cx">     // Does this one more time because collectChildrenAndRemoveFromOldParent() fires a MutationEvent.
</span><span class="cx">     if (!checkPreReplacementValidity(*this, newChild, oldChild, ec))
</span><span class="lines">@@ -422,14 +420,20 @@
</span><span class="cx"> 
</span><span class="cx">     // Remove the node we're replacing.
</span><span class="cx">     Ref&lt;Node&gt; protectOldChild(oldChild);
</span><del>-    removeChild(oldChild, ec);
-    if (ec)
-        return false;
</del><span class="cx"> 
</span><del>-    // Does this one more time because removeChild() fires a MutationEvent.
-    if (!checkPreReplacementValidity(*this, newChild, oldChild, ec))
-        return false;
</del><ins>+    ChildListMutationScope mutation(*this);
</ins><span class="cx"> 
</span><ins>+    // If oldChild == newChild then oldChild no longer has a parent at this point.
+    if (oldChild.parentNode()) {
+        removeChild(oldChild, ec);
+        if (ec)
+            return false;
+
+        // Does this one more time because removeChild() fires a MutationEvent.
+        if (!checkPreReplacementValidity(*this, newChild, oldChild, ec))
+            return false;
+    }
+
</ins><span class="cx">     InspectorInstrumentation::willInsertDOMNode(document(), *this);
</span><span class="cx"> 
</span><span class="cx">     // Add the new child(ren).
</span></span></pre>
</div>
</div>

</body>
</html>