Skip to content

Commit b1c3e6b

Browse files
committed
Avoid creating JS wrapper on a removed node when the subtree is not observable
https://bugs.webkit.org/show_bug.cgi?id=221243 <rdar://problem/73719386> Reviewed by Geoffrey Garen. Prior to this patch, WebKit forced the creation the JS wrapper on the root DOM node of a removed subtree to avoid script observable deletion of nodes. This is necessary because DOM nodes are reference counted in the C++ side, and while a DOM node keeps its child nodes alive, it won't keep its parent node alive to avoid reference cycles (leaks). If we didn't force the creation of the JS wrapper and the root node of the removed subtree didn't have any external reference to it in C++ side, we would happily delete it and all its descendant nodes above any subtree with a JS wrapper or an external C++ reference. While this turned out to be an effective strategy for implementing DOM nodes' GC semantics correctly, it has a significant runtime and memory cost - the latter is because we don't collect the JS wrappers of DOM nodes once they're created until they're ready to be destructed. This patch introduces a new optimization to avoid creating these JS wrappers when the removed subtree won't be observable by scripts in the future. The current heuristic is to check if the removed node has any external reference to it (i.e. refCount() > 0 excluding any reference counting that happens within our algorithm). This is sufficient because a given node should not be observable at a later time unless it has an external reference to it. This is ~0.5% progression on Speedometer-2.0 on MacBookAir7,2. To do this, we take advantage of the fact notifyChildNodeRemoved already traverses each removed subtree, and check if any of them has a reference count greater than 1 (greater than 1 because notifyChildNodeRemoved itself increments node's reference count before calling itself on child nodes). Note that we exclude the root node of the removed subtree as these JS wrapper creation is only needed to keep the root node alive. If the root node is already kept alive by some external reference to it, there is no need to keep it alive again by creating a JS wrapper on it. No new tests since existing tests such as fast/dom/gc-3.html covers it. * dom/ContainerNode.cpp: (WebCore::ContainerNode::removeAllChildrenWithScriptAssertion): Call willCreatePossiblyOrphanedTreeByRemoval if the removed subtree contains an observable node. (WebCore::ContainerNode::removeNodeWithScriptAssertion): Ditto. (WebCore::ContainerNode::removeSelfOrChildNodesForInsertion): Renamed from collectChildrenAndRemoveFromOldParent. Avoid collecting the removed nodes in this function in addition to removeAllChildrenWithScriptAssertion. (WebCore::ContainerNode::insertBefore): (WebCore::ContainerNode::replaceChild): (WebCore::ContainerNode::appendChildWithoutPreInsertionValidityCheck): (WebCore::dispatchChildRemovalEvents): Don't call willCreatePossiblyOrphanedTreeByRemoval here. * dom/ContainerNode.h: * dom/ContainerNodeAlgorithms.cpp: (WebCore::observabilityOfRemovedNode): Added. Checks the observability of a node excluding the root node of the removed subtree since it needs a special case in removeAllChildrenWithScriptAssertion. As notifyNodeRemovedFromDocument and notifyNodeRemovedFromTree ref's each child node before recursing on itself, we check refCount() > 1 here. (WebCore::updateObservability): Added. A helper function to update RemovedSubtreeObservability. (WebCore::notifyNodeRemovedFromDocument): Now returns RemovedSubtreeObservability, (WebCore::notifyNodeRemovedFromTree): Ditto. (WebCore::notifyChildNodeRemoved): Ditto. * dom/ContainerNodeAlgorithms.h: Canonical link: https://commits.webkit.org/233719@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@272394 268f45cc-cd09-0410-ab3c-d52691b4dbfc
1 parent f813761 commit b1c3e6b

5 files changed

Lines changed: 121 additions & 34 deletions

File tree

Source/WebCore/ChangeLog

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,59 @@
1+
2021-02-04 Ryosuke Niwa <rniwa@webkit.org>
2+
3+
Avoid creating JS wrapper on a removed node when the subtree is not observable
4+
https://bugs.webkit.org/show_bug.cgi?id=221243
5+
<rdar://problem/73719386>
6+
7+
Reviewed by Geoffrey Garen.
8+
9+
Prior to this patch, WebKit forced the creation the JS wrapper on the root DOM node of a removed subtree
10+
to avoid script observable deletion of nodes. This is necessary because DOM nodes are reference counted
11+
in the C++ side, and while a DOM node keeps its child nodes alive, it won't keep its parent node alive
12+
to avoid reference cycles (leaks). If we didn't force the creation of the JS wrapper and the root node
13+
of the removed subtree didn't have any external reference to it in C++ side, we would happily delete it
14+
and all its descendant nodes above any subtree with a JS wrapper or an external C++ reference.
15+
16+
While this turned out to be an effective strategy for implementing DOM nodes' GC semantics correctly,
17+
it has a significant runtime and memory cost - the latter is because we don't collect the JS wrappers
18+
of DOM nodes once they're created until they're ready to be destructed.
19+
20+
This patch introduces a new optimization to avoid creating these JS wrappers when the removed subtree
21+
won't be observable by scripts in the future. The current heuristic is to check if the removed node
22+
has any external reference to it (i.e. refCount() > 0 excluding any reference counting that happens within
23+
our algorithm). This is sufficient because a given node should not be observable at a later time unless
24+
it has an external reference to it. This is ~0.5% progression on Speedometer-2.0 on MacBookAir7,2.
25+
26+
To do this, we take advantage of the fact notifyChildNodeRemoved already traverses each removed subtree,
27+
and check if any of them has a reference count greater than 1 (greater than 1 because notifyChildNodeRemoved
28+
itself increments node's reference count before calling itself on child nodes). Note that we exclude the
29+
root node of the removed subtree as these JS wrapper creation is only needed to keep the root node alive.
30+
If the root node is already kept alive by some external reference to it, there is no need to keep it alive
31+
again by creating a JS wrapper on it.
32+
33+
No new tests since existing tests such as fast/dom/gc-3.html covers it.
34+
35+
* dom/ContainerNode.cpp:
36+
(WebCore::ContainerNode::removeAllChildrenWithScriptAssertion): Call willCreatePossiblyOrphanedTreeByRemoval
37+
if the removed subtree contains an observable node.
38+
(WebCore::ContainerNode::removeNodeWithScriptAssertion): Ditto.
39+
(WebCore::ContainerNode::removeSelfOrChildNodesForInsertion): Renamed from collectChildrenAndRemoveFromOldParent.
40+
Avoid collecting the removed nodes in this function in addition to removeAllChildrenWithScriptAssertion.
41+
(WebCore::ContainerNode::insertBefore):
42+
(WebCore::ContainerNode::replaceChild):
43+
(WebCore::ContainerNode::appendChildWithoutPreInsertionValidityCheck):
44+
(WebCore::dispatchChildRemovalEvents): Don't call willCreatePossiblyOrphanedTreeByRemoval here.
45+
* dom/ContainerNode.h:
46+
* dom/ContainerNodeAlgorithms.cpp:
47+
(WebCore::observabilityOfRemovedNode): Added. Checks the observability of a node excluding the root node of
48+
the removed subtree since it needs a special case in removeAllChildrenWithScriptAssertion.
49+
As notifyNodeRemovedFromDocument and notifyNodeRemovedFromTree ref's each child node before recursing on itself,
50+
we check refCount() > 1 here.
51+
(WebCore::updateObservability): Added. A helper function to update RemovedSubtreeObservability.
52+
(WebCore::notifyNodeRemovedFromDocument): Now returns RemovedSubtreeObservability,
53+
(WebCore::notifyNodeRemovedFromTree): Ditto.
54+
(WebCore::notifyChildNodeRemoved): Ditto.
55+
* dom/ContainerNodeAlgorithms.h:
56+
157
2021-02-04 Chris Dumez <cdumez@apple.com>
258

359
RELEASE_ASSERT(bigInt) in VM constructor when constructing a WorkletGlobalScope

Source/WebCore/dom/ContainerNode.cpp

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ ALWAYS_INLINE NodeVector ContainerNode::removeAllChildrenWithScriptAssertion(Chi
111111

112112
while (RefPtr<Node> child = m_firstChild) {
113113
removeBetween(nullptr, child->nextSibling(), *child);
114-
notifyChildNodeRemoved(*this, *child);
114+
auto subtreeObservability = notifyChildNodeRemoved(*this, *child);
115+
if (source == ChildChange::Source::API && subtreeObservability == RemovedSubtreeObservability::MaybeObservableByRefPtr)
116+
willCreatePossiblyOrphanedTreeByRemoval(child.get());
115117
}
116118

117119
if (deferChildrenChanged == DeferChildrenChanged::No)
@@ -149,6 +151,7 @@ ALWAYS_INLINE bool ContainerNode::removeNodeWithScriptAssertion(Node& childToRem
149151
return false;
150152

151153
ChildChange change;
154+
RemovedSubtreeObservability subtreeObservability;
152155
{
153156
WidgetHierarchyUpdatesSuspensionScope suspendWidgetHierarchyUpdates;
154157
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
@@ -164,7 +167,7 @@ ALWAYS_INLINE bool ContainerNode::removeNodeWithScriptAssertion(Node& childToRem
164167
RefPtr<Node> previousSibling = childToRemove.previousSibling();
165168
RefPtr<Node> nextSibling = childToRemove.nextSibling();
166169
removeBetween(previousSibling.get(), nextSibling.get(), childToRemove);
167-
notifyChildNodeRemoved(*this, childToRemove);
170+
subtreeObservability = notifyChildNodeRemoved(*this, childToRemove);
168171

169172
change.type = is<Element>(childToRemove) ?
170173
ChildChange::Type::ElementRemoved :
@@ -176,6 +179,9 @@ ALWAYS_INLINE bool ContainerNode::removeNodeWithScriptAssertion(Node& childToRem
176179
change.source = source;
177180
}
178181

182+
if (source == ChildChange::Source::API && subtreeObservability == RemovedSubtreeObservability::MaybeObservableByRefPtr)
183+
willCreatePossiblyOrphanedTreeByRemoval(&childToRemove);
184+
179185
// FIXME: Move childrenChanged into ScriptDisallowedScope block.
180186
childrenChanged(change);
181187

@@ -224,18 +230,26 @@ static ALWAYS_INLINE void executeNodeInsertionWithScriptAssertion(ContainerNode&
224230
dispatchChildInsertionEvents(child);
225231
}
226232

227-
static ExceptionOr<void> collectChildrenAndRemoveFromOldParent(Node& node, NodeVector& nodes)
233+
ExceptionOr<void> ContainerNode::removeSelfOrChildNodesForInsertion(Node& child, NodeVector& nodesForInsertion)
228234
{
229-
if (!is<DocumentFragment>(node)) {
230-
nodes.append(node);
231-
auto* oldParent = node.parentNode();
235+
if (!is<DocumentFragment>(child)) {
236+
nodesForInsertion.append(child);
237+
auto oldParent = makeRefPtr(child.parentNode());
232238
if (!oldParent)
233239
return { };
234-
return oldParent->removeChild(node);
240+
return oldParent->removeChild(child);
235241
}
236242

237-
nodes = collectChildNodes(node);
238-
downcast<DocumentFragment>(node).removeChildren();
243+
auto& fragment = downcast<DocumentFragment>(child);
244+
if (!fragment.hasChildNodes())
245+
return { };
246+
247+
auto removedChildNodes = fragment.removeAllChildrenWithScriptAssertion(ContainerNode::ChildChange::Source::API);
248+
nodesForInsertion.swap(removedChildNodes);
249+
250+
fragment.rebuildSVGExtensionsElementsIfNecessary();
251+
fragment.dispatchSubtreeModifiedEvent();
252+
239253
return { };
240254
}
241255

@@ -393,13 +407,13 @@ ExceptionOr<void> ContainerNode::insertBefore(Node& newChild, Node* refChild)
393407
Ref<Node> next(*refChild);
394408

395409
NodeVector targets;
396-
auto removeResult = collectChildrenAndRemoveFromOldParent(newChild, targets);
410+
auto removeResult = removeSelfOrChildNodesForInsertion(newChild, targets);
397411
if (removeResult.hasException())
398412
return removeResult.releaseException();
399413
if (targets.isEmpty())
400414
return { };
401415

402-
// We need this extra check because collectChildrenAndRemoveFromOldParent() can fire mutation events.
416+
// We need this extra check because removeSelfOrChildNodesForInsertion() can fire mutation events.
403417
for (auto& child : targets) {
404418
auto checkAcceptResult = checkAcceptChildGuaranteedNodeTypes(*this, child);
405419
if (checkAcceptResult.hasException())
@@ -508,12 +522,12 @@ ExceptionOr<void> ContainerNode::replaceChild(Node& newChild, Node& oldChild)
508522
NodeVector targets;
509523
{
510524
ChildListMutationScope mutation(*this);
511-
auto collectResult = collectChildrenAndRemoveFromOldParent(newChild, targets);
525+
auto collectResult = removeSelfOrChildNodesForInsertion(newChild, targets);
512526
if (collectResult.hasException())
513527
return collectResult.releaseException();
514528
}
515529

516-
// Do this one more time because collectChildrenAndRemoveFromOldParent() fires a MutationEvent.
530+
// Do this one more time because removeSelfOrChildNodesForInsertion() fires a MutationEvent.
517531
for (auto& child : targets) {
518532
validityResult = checkPreReplacementValidity(*this, child, oldChild, ShouldValidateChildParent::No);
519533
if (validityResult.hasException())
@@ -707,14 +721,14 @@ ExceptionOr<void> ContainerNode::appendChildWithoutPreInsertionValidityCheck(Nod
707721
Ref<ContainerNode> protectedThis(*this);
708722

709723
NodeVector targets;
710-
auto removeResult = collectChildrenAndRemoveFromOldParent(newChild, targets);
724+
auto removeResult = removeSelfOrChildNodesForInsertion(newChild, targets);
711725
if (removeResult.hasException())
712726
return removeResult.releaseException();
713727

714728
if (targets.isEmpty())
715729
return { };
716730

717-
// We need this extra check because collectChildrenAndRemoveFromOldParent() can fire mutation events.
731+
// We need this extra check because removeSelfOrChildNodesForInsertion() can fire mutation events.
718732
for (auto& child : targets) {
719733
auto nodeTypeResult = checkAcceptChildGuaranteedNodeTypes(*this, child);
720734
if (nodeTypeResult.hasException())
@@ -857,10 +871,6 @@ static void dispatchChildRemovalEvents(Ref<Node>& child)
857871
if (child->isInShadowTree())
858872
return;
859873

860-
// FIXME: This doesn't belong in dispatchChildRemovalEvents.
861-
// FIXME: Nodes removed from a shadow tree should also be kept alive.
862-
willCreatePossiblyOrphanedTreeByRemoval(child.ptr());
863-
864874
Ref<Document> document = child->document();
865875

866876
// dispatch pre-removal mutation events

Source/WebCore/dom/ContainerNode.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class ContainerNode : public Node {
150150
enum class DeferChildrenChanged { Yes, No };
151151
NodeVector removeAllChildrenWithScriptAssertion(ChildChange::Source, DeferChildrenChanged = DeferChildrenChanged::No);
152152
bool removeNodeWithScriptAssertion(Node&, ChildChange::Source);
153+
ExceptionOr<void> removeSelfOrChildNodesForInsertion(Node&, NodeVector&);
153154

154155
void removeBetween(Node* previousChild, Node* nextChild, Node& oldChild);
155156
ExceptionOr<void> appendChildWithoutPreInsertionValidityCheck(Node&);

Source/WebCore/dom/ContainerNodeAlgorithms.cpp

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,49 +107,66 @@ NodeVector notifyChildNodeInserted(ContainerNode& parentOfInsertedTree, Node& no
107107
return postInsertionNotificationTargets;
108108
}
109109

110-
static void notifyNodeRemovedFromDocument(ContainerNode& oldParentOfRemovedTree, TreeScopeChange treeScopeChange, Node& node)
110+
inline RemovedSubtreeObservability observabilityOfRemovedNode(Node& node)
111+
{
112+
bool isRootOfRemovedTree = !node.parentNode();
113+
return node.refCount() > 1 && !isRootOfRemovedTree ? RemovedSubtreeObservability::MaybeObservableByRefPtr : RemovedSubtreeObservability::NotObservable;
114+
}
115+
116+
inline void updateObservability(RemovedSubtreeObservability& currentObservability, RemovedSubtreeObservability newStatus)
117+
{
118+
if (newStatus == RemovedSubtreeObservability::MaybeObservableByRefPtr)
119+
currentObservability = newStatus;
120+
}
121+
122+
static RemovedSubtreeObservability notifyNodeRemovedFromDocument(ContainerNode& oldParentOfRemovedTree, TreeScopeChange treeScopeChange, Node& node)
111123
{
112124
ASSERT(oldParentOfRemovedTree.isConnected());
113125
ASSERT(node.isConnected());
114126
node.removedFromAncestor(Node::RemovalType { /* disconnectedFromDocument */ true, treeScopeChange == TreeScopeChange::Changed }, oldParentOfRemovedTree);
115127

128+
auto observability = observabilityOfRemovedNode(node);
116129
if (!is<ContainerNode>(node))
117-
return;
130+
return observability;
118131

119132
for (RefPtr<Node> child = downcast<ContainerNode>(node).firstChild(); child; child = child->nextSibling()) {
120133
RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!node.isConnected() && child->parentNode() == &node);
121-
notifyNodeRemovedFromDocument(oldParentOfRemovedTree, treeScopeChange, *child.get());
134+
updateObservability(observability, notifyNodeRemovedFromDocument(oldParentOfRemovedTree, treeScopeChange, *child.get()));
122135
}
123136

124137
if (!is<Element>(node))
125-
return;
138+
return observability;
126139

127140
if (RefPtr<ShadowRoot> root = downcast<Element>(node).shadowRoot()) {
128141
RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!node.isConnected() && root->host() == &node);
129-
notifyNodeRemovedFromDocument(oldParentOfRemovedTree, TreeScopeChange::DidNotChange, *root.get());
142+
updateObservability(observability, notifyNodeRemovedFromDocument(oldParentOfRemovedTree, TreeScopeChange::DidNotChange, *root.get()));
130143
}
144+
return observability;
131145
}
132146

133-
static void notifyNodeRemovedFromTree(ContainerNode& oldParentOfRemovedTree, TreeScopeChange treeScopeChange, Node& node)
147+
static RemovedSubtreeObservability notifyNodeRemovedFromTree(ContainerNode& oldParentOfRemovedTree, TreeScopeChange treeScopeChange, Node& node)
134148
{
135149
ASSERT(!oldParentOfRemovedTree.isConnected());
136150

137151
node.removedFromAncestor(Node::RemovalType { /* disconnectedFromDocument */ false, treeScopeChange == TreeScopeChange::Changed }, oldParentOfRemovedTree);
138152

153+
auto observability = observabilityOfRemovedNode(node);
139154
if (!is<ContainerNode>(node))
140-
return;
155+
return observability;
141156

142157
for (RefPtr<Node> child = downcast<ContainerNode>(node).firstChild(); child; child = child->nextSibling())
143-
notifyNodeRemovedFromTree(oldParentOfRemovedTree, treeScopeChange, *child);
158+
updateObservability(observability, notifyNodeRemovedFromTree(oldParentOfRemovedTree, treeScopeChange, *child));
144159

145160
if (!is<Element>(node))
146-
return;
161+
return observability;
147162

148163
if (RefPtr<ShadowRoot> root = downcast<Element>(node).shadowRoot())
149-
notifyNodeRemovedFromTree(oldParentOfRemovedTree, TreeScopeChange::DidNotChange, *root);
164+
updateObservability(observability, notifyNodeRemovedFromTree(oldParentOfRemovedTree, TreeScopeChange::DidNotChange, *root));
165+
166+
return observability;
150167
}
151168

152-
void notifyChildNodeRemoved(ContainerNode& oldParentOfRemovedTree, Node& child)
169+
RemovedSubtreeObservability notifyChildNodeRemoved(ContainerNode& oldParentOfRemovedTree, Node& child)
153170
{
154171
// Assert that the caller of this function has an instance of ScriptDisallowedScope.
155172
ASSERT(!isMainThread() || ScriptDisallowedScope::InMainThread::hasDisallowedScope());
@@ -158,9 +175,8 @@ void notifyChildNodeRemoved(ContainerNode& oldParentOfRemovedTree, Node& child)
158175
// Tree scope has changed if the container node from which "node" is removed is in a document or a shadow root.
159176
auto treeScopeChange = oldParentOfRemovedTree.isInTreeScope() ? TreeScopeChange::Changed : TreeScopeChange::DidNotChange;
160177
if (child.isConnected())
161-
notifyNodeRemovedFromDocument(oldParentOfRemovedTree, treeScopeChange, child);
162-
else
163-
notifyNodeRemovedFromTree(oldParentOfRemovedTree, treeScopeChange, child);
178+
return notifyNodeRemovedFromDocument(oldParentOfRemovedTree, treeScopeChange, child);
179+
return notifyNodeRemovedFromTree(oldParentOfRemovedTree, treeScopeChange, child);
164180
}
165181

166182
void addChildNodesToDeletionQueue(Node*& head, Node*& tail, ContainerNode& container)

Source/WebCore/dom/ContainerNodeAlgorithms.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ class ContainerChildRemovalScope {
6262
#endif // not ASSERT_ENABLED
6363

6464
NodeVector notifyChildNodeInserted(ContainerNode& parentOfInsertedTree, Node&);
65-
void notifyChildNodeRemoved(ContainerNode& oldParentOfRemovedTree, Node&);
65+
enum class RemovedSubtreeObservability {
66+
NotObservable,
67+
MaybeObservableByRefPtr,
68+
};
69+
RemovedSubtreeObservability notifyChildNodeRemoved(ContainerNode& oldParentOfRemovedTree, Node&);
6670
void removeDetachedChildrenInContainer(ContainerNode&);
6771

6872
enum SubframeDisconnectPolicy {

0 commit comments

Comments
 (0)