Skip to content

Commit a8a20e1

Browse files
committed
improve component tree perf
1 parent a9e301e commit a8a20e1

File tree

7 files changed

+167
-46
lines changed

7 files changed

+167
-46
lines changed

app/components/component-tree-item.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
{{on "mouseenter" @item.showPreview}}
1212
{{on "mouseleave" @item.hidePreview}}
1313
>
14+
{{resource @item.id create=@item.load update=@item.update teardown=@item.unload}}
1415
<div class="component-tree-item-background flex items-center pr-2 rounded">
1516
{{#if @item.hasChildren}}
1617
<Ui::DisclosureTriangle

app/controllers/component-tree.js

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Controller, { inject as controller } from '@ember/controller';
22
import { action } from '@ember/object';
3-
import { debounce } from '@ember/runloop';
3+
import { debounce, next } from '@ember/runloop';
44
import { inject as service } from '@ember/service';
55
import { htmlSafe } from '@ember/template';
66
import { tracked } from '@glimmer/tracking';
@@ -42,6 +42,9 @@ export default class ComponentTreeController extends Controller {
4242
item = new RenderItem(this, parent, renderNode);
4343
} else {
4444
item.renderNode = renderNode;
45+
if (item.isRendered) {
46+
item.load();
47+
}
4548
}
4649

4750
store[renderNode.id] = item;
@@ -59,6 +62,15 @@ export default class ComponentTreeController extends Controller {
5962
this.renderItems = renderItems;
6063
}
6164

65+
setRenderTreeItem(item) {
66+
if (item && this._store[item.id]) {
67+
this._store[item.id].renderNode = Object.assign(
68+
this._store[item.id].renderNode,
69+
item
70+
);
71+
}
72+
}
73+
6274
findItem(id) {
6375
return this._store[id];
6476
}
@@ -275,6 +287,7 @@ function arrowKeyPressed(keyCode) {
275287
class RenderItem {
276288
@tracked isExpanded;
277289
@tracked renderNode;
290+
@tracked renderCounter = 0;
278291

279292
constructor(controller, parentItem, renderNode) {
280293
this.controller = controller;
@@ -284,6 +297,42 @@ class RenderItem {
284297
this.isExpanded = this.isExpandable;
285298
}
286299

300+
get isRendered() {
301+
return this.renderCounter > 0;
302+
}
303+
304+
@action
305+
load() {
306+
next(() => {
307+
this.renderCounter += 1;
308+
if (this.renderNode.args) {
309+
return;
310+
}
311+
console.log('load', this.id);
312+
this.send('view:getTreeItem', { id: this.id });
313+
});
314+
}
315+
316+
@action
317+
async update(prevId) {
318+
next(() => {
319+
this.controller._store[prevId].renderCounter -= 1;
320+
this.renderCounter += 1;
321+
if (this.renderNode.args) {
322+
return;
323+
}
324+
console.log('load', this.id);
325+
this.send('view:getTreeItem', { id: this.id });
326+
});
327+
}
328+
329+
@action
330+
async unload() {
331+
next(() => {
332+
this.renderCounter -= 1;
333+
});
334+
}
335+
287336
get id() {
288337
return this.renderNode.id;
289338
}

app/helpers/resource.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Helper from '@ember/component/helper';
2+
import { registerDestructor, unregisterDestructor } from '@ember/destroyable';
3+
4+
export default class ResourceHelper extends Helper {
5+
compute(positional, named) {
6+
const firstTime = !this.updateCallback;
7+
this.updateCallback = named.update;
8+
if (named.teardown) {
9+
if (this.teardownCallback) {
10+
unregisterDestructor(this, this.teardownCallback);
11+
}
12+
this.teardownCallback = named.teardown;
13+
registerDestructor(this, this.teardownCallback);
14+
}
15+
if (this.updateCallback && !firstTime) {
16+
this.updateCallback(this.prevState, positional);
17+
}
18+
if (firstTime && named.create) {
19+
named.create();
20+
}
21+
//access all positional params
22+
positional.forEach(() => null);
23+
this.prevState = [...positional];
24+
}
25+
}

app/routes/component-tree.js

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default class ComponentTreeRoute extends TabRoute {
2828
super.activate(...arguments);
2929

3030
this.port.on('view:renderTree', this, this.setRenderTree);
31+
this.port.on('view:renderTreeItem', this, this.setRenderTreeItem);
3132
this.port.on('view:cancelSelection', this, this.cancelSelection);
3233
this.port.on('view:startInspecting', this, this.startInspecting);
3334
this.port.on('view:stopInspecting', this, this.stopInspecting);
@@ -38,6 +39,7 @@ export default class ComponentTreeRoute extends TabRoute {
3839
super.deactivate(...arguments);
3940

4041
this.port.off('view:renderTree', this, this.setRenderTree);
42+
this.port.off('view:renderTreeItem', this, this.setRenderTreeItem);
4143
this.port.off('view:cancelSelection', this, this.cancelSelection);
4244
this.port.off('view:startInspecting', this, this.startInspecting);
4345
this.port.off('view:stopInspecting', this, this.stopInspecting);
@@ -48,6 +50,10 @@ export default class ComponentTreeRoute extends TabRoute {
4850
this.controller.renderTree = tree;
4951
}
5052

53+
setRenderTreeItem({ treeItem }) {
54+
this.controller.setRenderTreeItem(treeItem);
55+
}
56+
5157
cancelSelection({ id, pin }) {
5258
this.controller.cancelSelection(id, pin);
5359
}

ember_debug/libs/render-tree.js

+58-45
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,7 @@ export default class RenderTree {
314314
this._reset();
315315

316316
this.tree = captureRenderTree(this.owner);
317-
let serialized = this._serializeRenderNodes(this.tree);
318-
319-
this._releaseStaleObjects();
320-
321-
return serialized;
317+
return this._createSimpleNodes(this.tree);
322318
}
323319

324320
/**
@@ -540,7 +536,7 @@ export default class RenderTree {
540536
};
541537
const idx = parentNode.children.indexOf(node);
542538
parentNode.children.splice(idx, 0, htmlNode);
543-
return this._serializeRenderNode(htmlNode, parentNode);
539+
return this._createSimpleNode(htmlNode, parentNode);
544540
}
545541

546542
_serializeRenderNodes(nodes, parentNode = null) {
@@ -552,14 +548,66 @@ export default class RenderTree {
552548
return mapped;
553549
}
554550

555-
_serializeRenderNode(node, parentNode = null) {
551+
_createSimpleNode(node, parentNode) {
556552
if (!node.id.startsWith(this.renderNodeIdPrefix)) {
557553
node.id = `${this.renderNodeIdPrefix}-${node.id}`;
558554
}
555+
556+
this.nodes[node.id] = node;
557+
this.parentNodes[node.id] = parentNode;
558+
559+
if (node.type === 'modifier') {
560+
if (parentNode.instance !== node.bounds.firstNode) {
561+
return this._insertHtmlElementNode(node, parentNode);
562+
}
563+
}
564+
565+
if (node.type === 'html-element') {
566+
// show set attributes in inspector
567+
Array.from(node.instance.attributes).forEach((attr) => {
568+
node.args.named[attr.nodeName] = attr.nodeValue;
569+
});
570+
// move modifiers and components into the element children
571+
parentNode.children.forEach((child) => {
572+
if (
573+
child.bounds.parentElement === node.instance ||
574+
(child.type === 'modifier' &&
575+
child.bounds.firstNode === node.instance)
576+
) {
577+
node.children.push(child);
578+
}
579+
});
580+
node.children.forEach((child) => {
581+
const idx = parentNode.children.indexOf(child);
582+
if (idx >= 0) {
583+
parentNode.children.splice(idx, 1);
584+
}
585+
});
586+
}
587+
588+
return {
589+
id: node.id,
590+
type: node.type,
591+
name: node.name,
592+
children: this._createSimpleNodes(node.children, node),
593+
};
594+
}
595+
596+
_createSimpleNodes(nodes, parentNode = null) {
597+
const mapped = [];
598+
// nodes can be mutated during serialize, which is why we use indexing instead of .map
599+
for (let i = 0; i < nodes.length; i++) {
600+
mapped.push(this._createSimpleNode(nodes[i], parentNode));
601+
}
602+
return mapped;
603+
}
604+
605+
_serializeRenderNode(nodeId) {
606+
const node = this.nodes[nodeId];
607+
if (!node) return null;
559608
let serialized = this.serialized[node.id];
560609

561610
if (serialized === undefined) {
562-
this.nodes[node.id] = node;
563611
if (node.type === 'keyword') {
564612
node.type = 'component';
565613
this.inElementSupport?.nodeMap.set(node, node.id);
@@ -586,33 +634,6 @@ export default class RenderTree {
586634
});
587635
}
588636

589-
if (parentNode) {
590-
this.parentNodes[node.id] = parentNode;
591-
}
592-
593-
if (node.type === 'html-element') {
594-
// show set attributes in inspector
595-
Array.from(node.instance.attributes).forEach((attr) => {
596-
node.args.named[attr.nodeName] = attr.nodeValue;
597-
});
598-
// move modifiers and components into the element children
599-
parentNode.children.forEach((child) => {
600-
if (
601-
child.bounds.parentElement === node.instance ||
602-
(child.type === 'modifier' &&
603-
child.bounds.firstNode === node.instance)
604-
) {
605-
node.children.push(child);
606-
}
607-
});
608-
node.children.forEach((child) => {
609-
const idx = parentNode.children.indexOf(child);
610-
if (idx >= 0) {
611-
parentNode.children.splice(idx, 1);
612-
}
613-
});
614-
}
615-
616637
if (node.type === 'component' && !node.instance) {
617638
node.instance = this._createSimpleInstance(
618639
'TemplateOnlyComponent',
@@ -624,18 +645,15 @@ export default class RenderTree {
624645
node.instance =
625646
node.instance || this._createSimpleInstance(node.name, node.args);
626647
node.instance.toString = () => node.name;
627-
if (parentNode.instance !== node.bounds.firstNode) {
628-
return this._insertHtmlElementNode(node, parentNode);
629-
}
630648
}
631649

632650
this.serialized[node.id] = serialized = {
633651
...node,
634652
args: this._serializeArgs(node.args),
635653
instance: this._serializeItem(node.instance),
636654
bounds: this._serializeBounds(node.bounds),
637-
children: this._serializeRenderNodes(node.children, node),
638655
};
656+
delete serialized.children;
639657
}
640658

641659
return serialized;
@@ -691,12 +709,7 @@ export default class RenderTree {
691709
}
692710

693711
_serializeObject(object) {
694-
let id = this.previouslyRetainedObjects.get(object);
695-
696-
if (id === undefined) {
697-
id = this.retainObject(object);
698-
}
699-
712+
let id = this.retainObject(object);
700713
this.retainedObjects.set(object, id);
701714

702715
return { id, type: typeof object, inspect: inspect(object) };

ember_debug/view-debug.js

+10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export default class extends DebugPort {
2020
this.sendTree(immediate);
2121
},
2222

23+
getTreeItem({ id }) {
24+
this.sendTreeItem(id);
25+
},
26+
2327
showInspection({ id, pin }) {
2428
this.viewInspection.show(id, pin);
2529
},
@@ -151,6 +155,12 @@ export default class extends DebugPort {
151155
}, 250);
152156
}
153157

158+
sendTreeItem(id) {
159+
this.sendMessage('renderTreeItem', {
160+
treeItem: this.renderTree._serializeRenderNode(id),
161+
});
162+
}
163+
154164
send() {
155165
if (this.isDestroying || this.isDestroyed) {
156166
return;

tests/ember_debug/view-debug-test.js

+17
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ async function getRenderTree() {
8787
EmberDebug.port.trigger('view:getTree', {});
8888
});
8989

90+
const all = [];
91+
const stack = [...message.tree];
92+
while (stack.length) {
93+
const item = stack.pop();
94+
all.push(item);
95+
stack.push(...item.children);
96+
}
97+
98+
const fetchAll = all.map(async (item) => {
99+
const message = await captureMessage('view:renderTreeItem', async () => {
100+
EmberDebug.port.trigger('view:getTreeItem', { id: item.id });
101+
});
102+
Object.assign(item, message.treeItem);
103+
});
104+
105+
await Promise.all(fetchAll);
106+
90107
if (message) {
91108
return message.tree;
92109
}

0 commit comments

Comments
 (0)