<!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>[201833] trunk/Source/WebInspectorUI</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/201833">201833</a></dd>
<dt>Author</dt> <dd>bburg@apple.com</dd>
<dt>Date</dt> <dd>2016-06-08 14:17:54 -0700 (Wed, 08 Jun 2016)</dd>
</dl>
<h3>Log Message</h3>
<pre>Web Inspector: reduce redundant attribute modification updates in DOMTreeUpdater and DOMTreeElement
https://bugs.webkit.org/show_bug.cgi?id=158504
<rdar://problem/25561452>
Reviewed by Timothy Hatcher.
When the frontend gets lots of DOM.attributeModified events, it forwards these on to
DOMTreeUpdater, which pushes a record for every single modification. It then updates
the DOM elements with the attibute changes on an animation frame. However, since it
doesn't do any deduplication of the modification records, a lot of time is wasted
on updating DOMTreeElements with intermediate (non-final) attribute values.
This patch rewrites DOMTreeUpdater to precisely track which nodes and attributes
of each node need to be updated on the next animation frame. This is done using
Sets and Maps that only hold onto the most recent attribute values rather than
pushing a record object for every single mutation.
This improves the performance of the Elements tab on an SVG particle simulator
dramatically so that the Inspector will not immediately hang. It still only achieves
a few updates per second in this case, so there is still optimization to be done on
the frontend and throttling to be done on the backend.
* UserInterface/Views/DOMTreeElement.js:
(WebInspector.DOMTreeElement):
(WebInspector.DOMTreeElement.prototype.attributeDidChange):
(WebInspector.DOMTreeElement.prototype._buildAttributeDOM):
(WebInspector.DOMTreeElement.prototype._markNodeChanged):
(WebInspector.DOMTreeElement.prototype._nodeChangedAnimationEnd):
(WebInspector.DOMTreeElement.prototype._fireDidChange):
(WebInspector.DOMTreeElement.prototype.nodeStateChanged): Deleted.
Simplify the list of modified attributes a little bit. This still uses a worklist
approach, so it's possible that duplicate updates for the same attribute could accumulate
if DOMTreeUpdater pushes updates faster than DOMTreeElement can render them.
* UserInterface/Views/DOMTreeUpdater.js:
(WebInspector.DOMTreeUpdater):
(WebInspector.DOMTreeUpdater.prototype._attributesUpdated):
(WebInspector.DOMTreeUpdater.prototype._characterDataModified):
(WebInspector.DOMTreeUpdater.prototype._nodeAttributeModified):
(WebInspector.DOMTreeUpdater.prototype._nodeInserted):
(WebInspector.DOMTreeUpdater.prototype._nodeRemoved):
(WebInspector.DOMTreeUpdater.prototype._childNodeCountUpdated):
(WebInspector.DOMTreeUpdater.prototype._updateModifiedNodes):
(WebInspector.DOMTreeUpdater.prototype._reset):
Rewrite this class to separately track insertions, deletions, and modifications. Use
Sets and Maps so redundant entries are not kept around. Split the main work loop
and use fewer enum-like properties to control how each DOM element change is handled.
Attempt to update all inserted children before modifying their attributes. This
wasn't done previously, but enough duplicate attribute modifications occurred that
usually some of them would be processed after being added to the tree. There is only
one chance to do this now.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkSourceWebInspectorUIChangeLog">trunk/Source/WebInspectorUI/ChangeLog</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceViewsDOMTreeElementjs">trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceViewsDOMTreeUpdaterjs">trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeUpdater.js</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkSourceWebInspectorUIChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/ChangeLog (201832 => 201833)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/ChangeLog        2016-06-08 21:15:09 UTC (rev 201832)
+++ trunk/Source/WebInspectorUI/ChangeLog        2016-06-08 21:17:54 UTC (rev 201833)
</span><span class="lines">@@ -1,3 +1,58 @@
</span><ins>+2016-06-08 Brian Burg <bburg@apple.com>
+
+ Web Inspector: reduce redundant attribute modification updates in DOMTreeUpdater and DOMTreeElement
+ https://bugs.webkit.org/show_bug.cgi?id=158504
+ <rdar://problem/25561452>
+
+ Reviewed by Timothy Hatcher.
+
+ When the frontend gets lots of DOM.attributeModified events, it forwards these on to
+ DOMTreeUpdater, which pushes a record for every single modification. It then updates
+ the DOM elements with the attibute changes on an animation frame. However, since it
+ doesn't do any deduplication of the modification records, a lot of time is wasted
+ on updating DOMTreeElements with intermediate (non-final) attribute values.
+
+ This patch rewrites DOMTreeUpdater to precisely track which nodes and attributes
+ of each node need to be updated on the next animation frame. This is done using
+ Sets and Maps that only hold onto the most recent attribute values rather than
+ pushing a record object for every single mutation.
+
+ This improves the performance of the Elements tab on an SVG particle simulator
+ dramatically so that the Inspector will not immediately hang. It still only achieves
+ a few updates per second in this case, so there is still optimization to be done on
+ the frontend and throttling to be done on the backend.
+
+ * UserInterface/Views/DOMTreeElement.js:
+ (WebInspector.DOMTreeElement):
+ (WebInspector.DOMTreeElement.prototype.attributeDidChange):
+ (WebInspector.DOMTreeElement.prototype._buildAttributeDOM):
+ (WebInspector.DOMTreeElement.prototype._markNodeChanged):
+ (WebInspector.DOMTreeElement.prototype._nodeChangedAnimationEnd):
+ (WebInspector.DOMTreeElement.prototype._fireDidChange):
+ (WebInspector.DOMTreeElement.prototype.nodeStateChanged): Deleted.
+ Simplify the list of modified attributes a little bit. This still uses a worklist
+ approach, so it's possible that duplicate updates for the same attribute could accumulate
+ if DOMTreeUpdater pushes updates faster than DOMTreeElement can render them.
+
+ * UserInterface/Views/DOMTreeUpdater.js:
+ (WebInspector.DOMTreeUpdater):
+ (WebInspector.DOMTreeUpdater.prototype._attributesUpdated):
+ (WebInspector.DOMTreeUpdater.prototype._characterDataModified):
+ (WebInspector.DOMTreeUpdater.prototype._nodeAttributeModified):
+ (WebInspector.DOMTreeUpdater.prototype._nodeInserted):
+ (WebInspector.DOMTreeUpdater.prototype._nodeRemoved):
+ (WebInspector.DOMTreeUpdater.prototype._childNodeCountUpdated):
+ (WebInspector.DOMTreeUpdater.prototype._updateModifiedNodes):
+ (WebInspector.DOMTreeUpdater.prototype._reset):
+ Rewrite this class to separately track insertions, deletions, and modifications. Use
+ Sets and Maps so redundant entries are not kept around. Split the main work loop
+ and use fewer enum-like properties to control how each DOM element change is handled.
+
+ Attempt to update all inserted children before modifying their attributes. This
+ wasn't done previously, but enough duplicate attribute modifications occurred that
+ usually some of them would be processed after being added to the tree. There is only
+ one chance to do this now.
+
</ins><span class="cx"> 2016-06-08 Nikita Vasilyev <nvasilyev@apple.com>
</span><span class="cx">
</span><span class="cx"> REGRESSION (r158219): Web Inspector: Border under the default Timeline content view is too thick
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceViewsDOMTreeElementjs"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js (201832 => 201833)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js        2016-06-08 21:15:09 UTC (rev 201832)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js        2016-06-08 21:17:54 UTC (rev 201833)
</span><span class="lines">@@ -1,5 +1,5 @@
</span><span class="cx"> /*
</span><del>- * Copyright (C) 2007, 2008, 2013, 2015 Apple Inc. All rights reserved.
</del><ins>+ * Copyright (C) 2007, 2008, 2013, 2015, 2016 Apple Inc. All rights reserved.
</ins><span class="cx"> * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
</span><span class="cx"> * Copyright (C) 2009 Joseph Pecoraro
</span><span class="cx"> *
</span><span class="lines">@@ -42,7 +42,7 @@
</span><span class="cx"> this._searchQuery = null;
</span><span class="cx"> this._expandedChildrenLimit = WebInspector.DOMTreeElement.InitialChildrenLimit;
</span><span class="cx">
</span><del>- this._nodeStateChanges = [];
</del><ins>+ this._recentlyModifiedAttributes = [];
</ins><span class="cx"> this._boundNodeChangedAnimationEnd = this._nodeChangedAnimationEnd.bind(this);
</span><span class="cx">
</span><span class="cx"> node.addEventListener(WebInspector.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this);
</span><span class="lines">@@ -197,12 +197,9 @@
</span><span class="cx"> return count;
</span><span class="cx"> }
</span><span class="cx">
</span><del>- nodeStateChanged(change)
</del><ins>+ attributeDidChange(name)
</ins><span class="cx"> {
</span><del>- if (!change)
- return;
-
- this._nodeStateChanges.push(change);
</del><ins>+ this._recentlyModifiedAttributes.push({name});
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> showChildNode(node)
</span><span class="lines">@@ -1148,9 +1145,9 @@
</span><span class="cx"> if (hasText)
</span><span class="cx"> attrSpanElement.append("\"");
</span><span class="cx">
</span><del>- for (let change of this._nodeStateChanges) {
- if (change.type === WebInspector.DOMTreeElement.ChangeType.Attribute && change.attribute === name)
- change.element = hasText ? attrValueElement : attrNameElement;
</del><ins>+ for (let attribute of this._recentlyModifiedAttributes) {
+ if (attribute.name === name)
+ attribute.element = hasText ? attrValueElement : attrNameElement;
</ins><span class="cx"> }
</span><span class="cx"> }
</span><span class="cx">
</span><span class="lines">@@ -1498,8 +1495,8 @@
</span><span class="cx">
</span><span class="cx"> _markNodeChanged()
</span><span class="cx"> {
</span><del>- for (let change of this._nodeStateChanges) {
- let element = change.element;
</del><ins>+ for (let attribute of this._recentlyModifiedAttributes) {
+ let element = attribute.element;
</ins><span class="cx"> if (!element)
</span><span class="cx"> continue;
</span><span class="cx">
</span><span class="lines">@@ -1515,9 +1512,9 @@
</span><span class="cx"> element.classList.remove("node-state-changed");
</span><span class="cx"> element.removeEventListener("animationend", this._boundNodeChangedAnimationEnd);
</span><span class="cx">
</span><del>- for (let i = this._nodeStateChanges.length - 1; i >= 0; --i) {
- if (this._nodeStateChanges[i].element === element)
- this._nodeStateChanges.splice(i, 1);
</del><ins>+ for (let i = this._recentlyModifiedAttributes.length - 1; i >= 0; --i) {
+ if (this._recentlyModifiedAttributes[i].element === element)
+ this._recentlyModifiedAttributes.splice(i, 1);
</ins><span class="cx"> }
</span><span class="cx"> }
</span><span class="cx">
</span><span class="lines">@@ -1533,8 +1530,7 @@
</span><span class="cx"> {
</span><span class="cx"> super._fireDidChange();
</span><span class="cx">
</span><del>- if (this._nodeStateChanges)
- this._markNodeChanged();
</del><ins>+ this._markNodeChanged();
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx"> handleEvent(event)
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceViewsDOMTreeUpdaterjs"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeUpdater.js (201832 => 201833)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeUpdater.js        2016-06-08 21:15:09 UTC (rev 201832)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/DOMTreeUpdater.js        2016-06-08 21:17:54 UTC (rev 201833)
</span><span class="lines">@@ -1,5 +1,5 @@
</span><span class="cx"> /*
</span><del>- * Copyright (C) 2007, 2008, 2013 Apple Inc. All rights reserved.
</del><ins>+ * Copyright (C) 2007, 2008, 2013, 2016 Apple Inc. All rights reserved.
</ins><span class="cx"> * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
</span><span class="cx"> * Copyright (C) 2009 Joseph Pecoraro
</span><span class="cx"> *
</span><span class="lines">@@ -39,7 +39,15 @@
</span><span class="cx"> WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
</span><span class="cx">
</span><span class="cx"> this._treeOutline = treeOutline;
</span><del>- this._recentlyModifiedNodes = [];
</del><ins>+
+ this._recentlyInsertedNodes = new Map;
+ this._recentlyDeletedNodes = new Map;
+ this._recentlyModifiedNodes = new Set;
+ // Map from attribute names to nodes that had the attributes.
+ this._recentlyModifiedAttributes = new Map;
+
+ // Dummy "attribute" that is used to track textContent changes.
+ this._textContentAttributeSymbol = Symbol("text-content-attribute");
</ins><span class="cx"> };
</span><span class="cx">
</span><span class="cx"> WebInspector.DOMTreeUpdater.prototype = {
</span><span class="lines">@@ -55,28 +63,37 @@
</span><span class="cx">
</span><span class="cx"> _attributesUpdated: function(event)
</span><span class="cx"> {
</span><del>- this._recentlyModifiedNodes.push({node: event.data.node, updated: true, attribute: event.data.name});
- if (this._treeOutline._visible)
- this.onNextFrame._updateModifiedNodes();
</del><ins>+ let {node, name} = event.data;
+ this._nodeAttributeModified(node, name);
</ins><span class="cx"> },
</span><span class="cx">
</span><span class="cx"> _characterDataModified: function(event)
</span><span class="cx"> {
</span><del>- this._recentlyModifiedNodes.push({node: event.data.node, updated: true});
</del><ins>+ let {node} = event.data;
+ this._nodeAttributeModified(node, this._textContentAttributeSymbol);
+ },
+
+ _nodeAttributeModified: function(node, attribute)
+ {
+ if (!this._recentlyModifiedAttributes.has(attribute))
+ this._recentlyModifiedAttributes.set(attribute, new Set);
+ this._recentlyModifiedAttributes.get(attribute).add(node);
+ this._recentlyModifiedNodes.add(node);
+
</ins><span class="cx"> if (this._treeOutline._visible)
</span><span class="cx"> this.onNextFrame._updateModifiedNodes();
</span><del>- },
</del><ins>+ },
</ins><span class="cx">
</span><span class="cx"> _nodeInserted: function(event)
</span><span class="cx"> {
</span><del>- this._recentlyModifiedNodes.push({node: event.data.node, parent: event.data.parent, inserted: true});
</del><ins>+ this._recentlyInsertedNodes.set(event.data.node, {parent: event.data.parent});
</ins><span class="cx"> if (this._treeOutline._visible)
</span><span class="cx"> this.onNextFrame._updateModifiedNodes();
</span><span class="cx"> },
</span><span class="cx">
</span><span class="cx"> _nodeRemoved: function(event)
</span><span class="cx"> {
</span><del>- this._recentlyModifiedNodes.push({node: event.data.node, parent: event.data.parent, removed: true});
</del><ins>+ this._recentlyDeletedNodes.set(event.data.node, {parent: event.data.parent});
</ins><span class="cx"> if (this._treeOutline._visible)
</span><span class="cx"> this.onNextFrame._updateModifiedNodes();
</span><span class="cx"> },
</span><span class="lines">@@ -90,46 +107,53 @@
</span><span class="cx">
</span><span class="cx"> _updateModifiedNodes: function()
</span><span class="cx"> {
</span><del>- let updatedParentTreeElements = [];
- for (let recentlyModifiedNode of this._recentlyModifiedNodes) {
- let parent = recentlyModifiedNode.parent;
- let node = recentlyModifiedNode.node;
- let changeInfo = null;
- if (recentlyModifiedNode.attribute)
- changeInfo = {type: WebInspector.DOMTreeElement.ChangeType.Attribute, attribute: recentlyModifiedNode.attribute};
</del><ins>+ // Update for insertions and deletions before attribute modifications. This ensures
+ // tree elements get created for newly attached children before we try to update them.
+ let parentElementsToUpdate = new Set;
+ let markNodeParentForUpdate = (value, key, map) => {
+ let parentNode = value.parent;
+ let parentTreeElement = this._treeOutline.findTreeElement(parentNode);
+ if (parentTreeElement)
+ parentElementsToUpdate.add(parentTreeElement);
+ };
+ this._recentlyInsertedNodes.forEach(markNodeParentForUpdate);
+ this._recentlyDeletedNodes.forEach(markNodeParentForUpdate);
</ins><span class="cx">
</span><del>- if (recentlyModifiedNode.updated) {
- let nodeTreeElement = this._treeOutline.findTreeElement(node);
- if (!nodeTreeElement)
</del><ins>+ for (let parentTreeElement of parentElementsToUpdate) {
+ parentTreeElement.updateTitle();
+ parentTreeElement.updateChildren();
+ }
+
+ for (let node of this._recentlyModifiedNodes.values()) {
+ let nodeTreeElement = this._treeOutline.findTreeElement(node);
+ if (!nodeTreeElement)
+ return;
+
+ for (let [attribute, nodes] of this._recentlyModifiedAttributes.entries()) {
+ // Don't report textContent changes as attribute modifications.
+ if (attribute === this._textContentAttributeSymbol)
</ins><span class="cx"> continue;
</span><span class="cx">
</span><del>- if (changeInfo)
- nodeTreeElement.nodeStateChanged(changeInfo);
-
- nodeTreeElement.updateTitle();
</del><ins>+ if (nodes.has(node))
+ nodeTreeElement.attributeDidChange(attribute);
</ins><span class="cx"> }
</span><span class="cx">
</span><del>- if (!parent)
- continue;
-
- let parentNodeItem = this._treeOutline.findTreeElement(parent);
- if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
- parentNodeItem.updateTitle();
- parentNodeItem.updateChildren();
- parentNodeItem.alreadyUpdatedChildren = true;
- updatedParentTreeElements.push(parentNodeItem);
- }
</del><ins>+ nodeTreeElement.updateTitle();
</ins><span class="cx"> }
</span><span class="cx">
</span><del>- for (let i = 0; i < updatedParentTreeElements.length; ++i)
- updatedParentTreeElements[i].alreadyUpdatedChildren = null;
-
- this._recentlyModifiedNodes = [];
</del><ins>+ this._recentlyInsertedNodes.clear();
+ this._recentlyDeletedNodes.clear();
+ this._recentlyModifiedNodes.clear();
+ this._recentlyModifiedAttributes.clear();
</ins><span class="cx"> },
</span><span class="cx">
</span><span class="cx"> _reset: function()
</span><span class="cx"> {
</span><span class="cx"> WebInspector.domTreeManager.hideDOMNodeHighlight();
</span><del>- this._recentlyModifiedNodes = [];
</del><ins>+
+ this._recentlyInsertedNodes.clear();
+ this._recentlyDeletedNodes.clear();
+ this._recentlyModifiedNodes.clear();
+ this._recentlyModifiedAttributes.clear();
</ins><span class="cx"> }
</span><span class="cx"> };
</span></span></pre>
</div>
</div>
</body>
</html>