Optimize several things about DOM Parts [1/2]
See [1] for details. This CL makes these changes:
- Stop maintaining “live” parts lists across DOM mutations. This is
controlled by a new feature flag, DOMPartsAPIActivePartTracking,
that when disabled, short-circuits all of the part list maintenance
code during DOM mutations. This means that if DOM nodes that are
connected to parts are moved around in the DOM, including being
removed from the document, the cached parts list does not change.
- Update ToV8HelperSequence to work directly with Deques, and get
rid of getParts() code to convert to a HeapVector.
- Optimize ToV8HelperSequence to use iterators instead of array
indexing, which should also speed up all sequence bindings. (TBD)
- Change from FrozenArray<Part> to sequence<Part>. This eliminated
the overhead of Object::SetIntegrityLevel(), which was ~29% of the
total getParts() time.
- Add a fast path for PartRoot.replaceChildren when all children
are being removed.
- Inline IsValid().
- Call InsertBefore() instead of before() to avoid some extra
validity checks. This required making ConvertNodeUnionsIntoNode
public.
- Add PartRoot.getPartNode(n), which allows direct access to a
single Part's Node, to avoid wrapping up the entire parts list.
[1] https://docs.google.com/document/d/1wSbBV6tS-KLlHWxaK_FmEJFOwkY-pb8_YDoFJgDrbMg/edit
Bug: 1453291
Change-Id: I3df15b2a552fe9c2793a7f7f2095d632bcea8326
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4878048
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Auto-Submit: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1204772}
diff --git a/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html b/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html
index 7a79af5..dd5f1ba 100644
--- a/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html
+++ b/dom/parts/basic-dom-part-declarative-brace-syntax.tentative.html
@@ -48,31 +48,16 @@
</div>
</div>
</template>
-<div id=declarative_shadow_dom>
- <template shadowrootmode="open">
- <div>
- <div id=target3>Declarative syntax
- <h1 id="name" parseparts>
- {{#}}
- First
- {{#}} <span {{}}>Middle</span> {{/}}
- Last
- {{/}}
- <a foo {{}} id=nodepart1>content</a>
- <a {{}} id=nodepart2>content</a>
- <a {{}}id=nodepart3>content</a>
- <a id=nodepart4 {{}}>content</a>
- <a id=nodepart5 foo {{}}>content</a>
- <a id=nodepart6 foo {{}} >content</a>
- </h1>
- </div>
- </div>
- </template>
-</div>
+
+<!-- TODO: This test should look at declarative shadow DOM behavior. -->
<script> {
+function addPartsCleanup(t,partRoot) {
+ t.add_cleanup(() => partRoot.getParts().forEach(part => part.disconnect()));
+}
+
const template = document.getElementById('declarative');
-['Main Document','Template','Clone','PartClone','DeclarativeShadowDOM'].forEach(testCase => {
+['Main Document','Template','Clone','PartClone'].forEach(testCase => {
function assertIsComment(node,commentText) {
assert_true(node instanceof Comment);
assert_equals(node.textContent,commentText);
@@ -106,22 +91,20 @@
wrapper = document.body.appendChild(document.createElement('div'));
wrapper.appendChild(template.content.getPartRoot().clone().rootContainer);
target = wrapper.querySelector('#target3');
+ // Even a PartRoot clone should not add parts to the document, when that
+ // clone is appendChilded to the document.
+ expectDOMParts = false;
cleanup = [wrapper];
break;
- case 'DeclarativeShadowDOM': {
- const host = document.getElementById('declarative_shadow_dom');
- doc = host.shadowRoot;
- target = doc.querySelector('#target3'); // Shadow isolated
- cleanup = [host];
- break;
- }
default:
assert_unreached('Invalid test case');
}
assert_true(!!(doc && target && target.parentElement));
- t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup
const root = doc.getPartRoot();
+ t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup
+ addPartsCleanup(t,root); // Clean up all Parts when this test ends.
+
assert_true(root instanceof DocumentPartRoot);
if (expectDOMParts) {
let expectedRootParts = [{type:'ChildNodePart',metadata:[]}];
@@ -158,6 +141,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,1);
assert_equals(target.innerHTML,'Before <!---->Parts<!----> After');
@@ -174,6 +158,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,0);
assert_equals(target.innerHTML,'Before {{#}}Parts{{/}} After');
target.setAttribute('parseparts','');
@@ -188,6 +173,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,1);
assert_equals(target.innerHTML,'<!----><!---->');
}, 'Just parts, no text before');
@@ -199,6 +185,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,0);
assert_equals(target.innerHTML,'<input parseparts=\"\">{{#}}{{/}}');
}, 'Self closing elements can\'t use parseparts');
@@ -210,6 +197,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,0);
assert_equals(target.innerHTML,'{{#}}{{/}}');
}, 'Second head element can\'t use parseparts');
@@ -222,6 +210,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,1);
assert_equals(target.innerHTML,'<svg><!----><circle/><!----></svg>');
}, 'Foreign content should support Parts');
@@ -236,6 +225,7 @@
const target = document.currentScript.previousElementSibling;
t.add_cleanup(() => target.remove());
const root = document.getPartRoot();
+ addPartsCleanup(t,root);
assert_equals(root.getParts().length,0);
assert_equals(target.childElementCount,2);
Array.from(target.children).forEach(el => {
diff --git a/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html b/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html
index 47abe82..f646035 100644
--- a/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html
+++ b/dom/parts/basic-dom-part-declarative-pi-syntax.tentative.html
@@ -50,32 +50,12 @@
</div>
</div>
</template>
-<div id=declarative_shadow_dom>
- <template shadowrootmode="open">
- <div>
- <div id=target3>Declarative syntax
- <h1 id="name">
- <?child-node-part fullname?>
- First
- <!--?child-node-part middle?--> <?node-part middle-node?>Middle <?/child-node-part middle?>
- Last
- <!-- ?/child-node-part foobar? -->
- </h1>
- Email: <?node-part email-link?><a id="link"></a>
- Here are some invalid parts that should not get parsed:
- <!--child-node-part test comment without leading ?-->
- <child-node-part test PI without leading ?>
- <!--?child-node-partfoobar?-->
- <?child-node-partfoobar?>
- </div>
- </div>
- </template>
-</div>
+<!-- TODO: This test should look at declarative shadow DOM behavior. -->
<script> {
const template = document.getElementById('declarative');
-['Main Document','Template','Clone','PartClone','DeclarativeShadowDOM'].forEach(testCase => {
+['Main Document','Template','Clone','PartClone'].forEach(testCase => {
function assertIsComment(node,commentText) {
assert_true(node instanceof Comment);
assert_equals(node.textContent,commentText);
@@ -107,24 +87,22 @@
case 'PartClone':
doc = document;
wrapper = document.body.appendChild(document.createElement('div'));
+ assert_true(template.content.getPartRoot().getParts().length != 0);
wrapper.appendChild(template.content.getPartRoot().clone().rootContainer);
target = wrapper.querySelector('#target3');
+ // Even a PartRoot clone should not add parts to the document, when that
+ // clone is appendChilded to the document.
+ expectDOMParts = false;
cleanup = [wrapper];
break;
- case 'DeclarativeShadowDOM': {
- const host = document.getElementById('declarative_shadow_dom');
- doc = host.shadowRoot;
- target = doc.querySelector('#target3'); // Shadow isolated
- cleanup = [host];
- break;
- }
default:
assert_unreached('Invalid test case');
}
assert_true(!!(doc && target && target.parentElement));
- t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup
-
const root = doc.getPartRoot();
+ t.add_cleanup(() => cleanup.forEach(el => el.remove())); // Cleanup
+ t.add_cleanup(() => root.getParts().forEach(part => part.disconnect()));
+
assert_true(root instanceof DocumentPartRoot);
if (expectDOMParts) {
const expectedRootParts = [{type:'ChildNodePart',metadata:['fullname']},{type:'NodePart',metadata:['email-link']}];
diff --git a/dom/parts/basic-dom-part-objects.tentative.html b/dom/parts/basic-dom-part-objects.tentative.html
index 302edb7..af77b28 100644
--- a/dom/parts/basic-dom-part-objects.tentative.html
+++ b/dom/parts/basic-dom-part-objects.tentative.html
@@ -70,19 +70,19 @@
const nodeBefore = target.previousSibling || target.parentNode;
const nodePartBefore = addCleanup(t,new NodePart(root,nodeBefore));
- runningPartsExpectation.unshift({type:'NodePart',metadata:[]});
- assertEqualParts(root.getParts(),runningPartsExpectation,[nodePartBefore,nodePart,childNodePart],'getParts() for the root should now have this nodePart, in tree order');
+ runningPartsExpectation.push({type:'NodePart',metadata:[]});
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,childNodePart,nodePartBefore],'getParts() for the root should now have this nodePart, in construction order');
const nodePart2 = addCleanup(t,new NodePart(childNodePart,target.children[2],{metadata:['blah']}));
assert_equals(nodePart2.root,childNodePart);
- assertEqualParts(root.getParts(),runningPartsExpectation,[nodePartBefore,nodePart,childNodePart],'getParts() for the root DocumentPartRoot shouldn\'t change');
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,childNodePart,nodePartBefore],'getParts() for the root DocumentPartRoot shouldn\'t change');
assertEqualParts(childNodePart.getParts(),[{type:'NodePart',metadata:['blah']}],[nodePart2],'getParts() for the childNodePart should have it');
nodePart2.disconnect();
assert_equals(nodePart2.root,null,'root should be null after disconnect');
assert_equals(nodePart2.node,null,'node should be null after disconnect');
assert_equals(childNodePart.getParts().length,0,'calling disconnect() should remove the part from root.getParts()');
- assertEqualParts(root.getParts(),runningPartsExpectation,[nodePartBefore,nodePart,childNodePart],'getParts() for the root DocumentPartRoot still shouldn\'t change');
+ assertEqualParts(root.getParts(),runningPartsExpectation,[nodePart,childNodePart,nodePartBefore],'getParts() for the root DocumentPartRoot still shouldn\'t change');
nodePart2.disconnect(); // Calling twice should be ok.
childNodePart.disconnect();
@@ -182,41 +182,41 @@
const nodePartB = addCleanup(t,new NodePart(root,b));
const nodePartA = addCleanup(t,new NodePart(root,a));
const nodePartC = addCleanup(t,new NodePart(root,c));
- assert_array_equals(root.getParts(),[nodePartA,nodePartB,nodePartC]);
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Parts can be out of order, if added out of order');
b.remove();
- assert_array_equals(root.getParts(),[nodePartA,nodePartC]);
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Removals are not tracked');
target.parentElement.insertBefore(b,target);
- assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC]);
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Insertions are not tracked');
target.insertBefore(b,c);
- assert_array_equals(root.getParts(),[nodePartA,nodePartB,nodePartC]);
+ assert_array_equals(root.getParts(),[nodePartB,nodePartA,nodePartC],'Nothing is tracked');
nodePartA.disconnect();
nodePartB.disconnect();
nodePartC.disconnect();
- assert_array_equals(root.getParts(),[]);
+ assert_array_equals(root.getParts(),[],'disconnections are tracked');
const childPartAC = addCleanup(t,new ChildNodePart(root,a,c));
assert_array_equals(root.getParts(),[childPartAC]);
a.remove();
assert_array_equals(root.getParts(),[],'Removing endpoints invalidates the part');
target.insertBefore(a,b); // Restore
- assert_array_equals(root.getParts(),[childPartAC]);
+ assert_array_equals(root.getParts(),[],'Insertions are not tracked');
target.insertBefore(c,a);
assert_array_equals(root.getParts(),[],'Endpoints out of order');
target.appendChild(c); // Restore
- assert_array_equals(root.getParts(),[childPartAC]);
+ assert_array_equals(root.getParts(),[],'Insertions are not tracked');
document.body.appendChild(c);
- assert_array_equals(root.getParts(),[],'Children need to have same parent');
+ assert_array_equals(root.getParts(),[],'Parts are\'t invalidated when endpoints have different parents');
target.appendChild(c); // Restore
- assert_array_equals(root.getParts(),[childPartAC]);
+ assert_array_equals(root.getParts(),[],'Insertions are not tracked');
const oldParent = target.parentElement;
target.remove();
- assert_array_equals(root.getParts(),[],'Parent needs to be connected');
+ assert_array_equals(root.getParts(),[],'Parts are\'t invalidated when disconnected');
oldParent.appendChild(target); // Restore
- assert_array_equals(root.getParts(),[childPartAC]);
- }, `DOM mutation support (${description})`);
+ assert_array_equals(root.getParts(),[]);
+ }, `DOM mutations are not tracked (${description})`);
test((t) => {
const root = doc.getPartRoot();
diff --git a/dom/parts/resources/domparts-utils.js b/dom/parts/resources/domparts-utils.js
index 1242bb0..5deeec8 100644
--- a/dom/parts/resources/domparts-utils.js
+++ b/dom/parts/resources/domparts-utils.js
@@ -6,6 +6,7 @@
assert_array_equals(parts[i].metadata,partDescriptions[i].metadata,`${description}: index ${i} wrong metadata`);
if (expectedParts) {
assert_equals(parts[i],expectedParts[i],`${description}: index ${i} object equality`);
+ assert_equals(parts[i].root.getPartNode(i),parts[i].node || parts[i].previousSibling,'getPartNode() should return the same node as getParts().node/previousSibling');
}
}
}