fix(devtools): support defer blocks in IdentityTracker (#61139)

This reverts the fix of #61080 which wasn't adequate.

PR Close #61139
This commit is contained in:
Matthieu Riegler
2025-05-06 16:34:39 +02:00
committed by Andrew Kushnir
parent 70f959b3bd
commit 6cd5d8ebdc
9 changed files with 51 additions and 18 deletions
@@ -267,6 +267,7 @@ const insertElementProfile = (
let lastFrame: ElementProfile = {
children: [],
directives: [],
type: 'element',
};
if (frames[lastIdx]) {
lastFrame = frames[lastIdx];
@@ -290,6 +291,8 @@ const prepareInitialFrame = (source: string, duration: number) => {
position = directiveForestHooks.getDirectivePosition(node.component.instance);
} else if (node.directives[0]) {
position = directiveForestHooks.getDirectivePosition(node.directives[0].instance);
} else if (node.defer) {
position = directiveForestHooks.getDirectivePosition(node.defer);
}
if (position === undefined) {
@@ -313,9 +316,10 @@ const prepareInitialFrame = (source: string, duration: number) => {
name: getDirectiveName(node.component.instance),
});
}
const result = {
const result: ElementProfile = {
children: [],
directives,
type: node.defer ? 'defer' : 'element',
};
children[position[position.length - 1]] = result;
node.children.forEach((n) => traverse(n, result.children));
@@ -96,6 +96,9 @@ export class IdentityTracker {
this.isComponent.set(dir.instance, false);
this._indexNode(dir.instance, node.position, newNodes);
});
if (node.defer) {
this._indexNode(node.defer, node.position, newNodes);
}
node.children.forEach((child) => this._index(child, parent, newNodes, allNodes));
}
@@ -124,21 +127,12 @@ const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstan
parentPosition: number[] = [],
): IndexedNode => {
const position = parentPosition.concat([idx]);
// Not every node represents a DOM element (ex @defer blocks), we shouldn't account for them
const children: IndexedNode[] = [];
node.children.forEach((n, i) => {
if (n.nativeElement) {
children.push(indexTree(n, i, position));
}
});
return {
position,
element: node.element,
component: node.component,
directives: node.directives.map((d) => ({position, ...d})),
children,
children: node.children.map((n, i) => indexTree(n, i, position)),
nativeElement: node.nativeElement,
hydration: node.hydration,
defer: node.defer,
@@ -147,5 +141,4 @@ const indexTree = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstan
export const indexForest = <T extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType>>(
forest: T[],
// Not every node represents a DOM element (ex @defer blocks), we shouldn't account for them
): IndexedNode[] => forest.filter((n) => n.nativeElement).map((n, i) => indexTree(n, i));
): IndexedNode[] => forest.map((n, i) => indexTree(n, i));
@@ -38,6 +38,7 @@ export class FlamegraphFormatter extends RecordFormatter<FlamegraphNode> {
original: {
children: [],
directives: [],
type: 'element',
},
};
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ProfilerFrame} from 'protocol';
import {ElementProfile, ProfilerFrame} from 'protocol';
import {mergeFrames} from './frame-merger';
@@ -16,7 +16,7 @@ describe('mergeFrames', () => {
});
it('should work with a single frame', () => {
const frame = {
const frame: ProfilerFrame = {
directives: [
{
children: [],
@@ -29,6 +29,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
duration: 5,
@@ -43,7 +44,7 @@ describe('mergeFrames', () => {
});
it('should merge frames when nesting matches', () => {
const frame = {
const frame: ProfilerFrame = {
directives: [
{
children: [],
@@ -57,6 +58,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
duration: 5,
@@ -78,6 +80,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
duration: 10,
@@ -86,7 +89,7 @@ describe('mergeFrames', () => {
});
it('should merge frames when nesting does not match', () => {
const frame = {
const frame: ProfilerFrame = {
directives: [
{
children: [],
@@ -100,6 +103,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
duration: 5,
@@ -122,6 +126,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
directives: [
@@ -134,6 +139,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
duration: 5,
@@ -157,6 +163,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
directives: [
@@ -169,6 +176,7 @@ describe('mergeFrames', () => {
name: 'Foo',
},
],
type: 'element',
},
],
duration: 10,
@@ -32,6 +32,7 @@ const mergeDirectives = (mergeIn: ElementProfile[], second: ElementProfile[]) =>
mergeIn[i] = {
children: [],
directives: [],
type: 'element',
};
}
second[i].directives.forEach((d, idx) => {
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {ElementProfile} from 'protocol';
import {FlamegraphNode} from './flamegraph-formatter';
export const SIMPLE_RECORD = [
@@ -33,6 +34,7 @@ export const SIMPLE_RECORD = [
changeDetected: true,
},
],
type: 'element' as ElementProfile['type'],
},
],
directives: [
@@ -46,6 +48,7 @@ export const SIMPLE_RECORD = [
changeDetection: 0,
},
],
type: 'element' as ElementProfile['type'],
},
];
export const SIMPLE_FORMATTED_FLAMEGRAPH_RECORD = [
@@ -84,7 +87,7 @@ export const SIMPLE_FORMATTED_TREE_MAP_RECORD = [
original: SIMPLE_RECORD[0],
}),
];
export const NESTED_RECORD = [
export const NESTED_RECORD: ElementProfile[] = [
{
children: [
{
@@ -121,6 +124,7 @@ export const NESTED_RECORD = [
name: 'TodoComponent',
},
],
type: 'element',
},
{
children: [],
@@ -142,6 +146,7 @@ export const NESTED_RECORD = [
name: 'TodoComponent',
},
],
type: 'element',
},
{
children: [],
@@ -163,6 +168,7 @@ export const NESTED_RECORD = [
name: 'TodoComponent',
},
],
type: 'element',
},
],
directives: [
@@ -175,6 +181,7 @@ export const NESTED_RECORD = [
changeDetection: 0,
},
],
type: 'element',
},
],
directives: [
@@ -187,6 +194,7 @@ export const NESTED_RECORD = [
name: 'TodosComponent',
},
],
type: 'element',
},
],
directives: [
@@ -199,6 +207,7 @@ export const NESTED_RECORD = [
changeDetection: 0,
},
],
type: 'element',
},
],
directives: [
@@ -211,6 +220,7 @@ export const NESTED_RECORD = [
name: 'AppComponent',
},
],
type: 'element',
},
],
directives: [
@@ -223,6 +233,7 @@ export const NESTED_RECORD = [
changeDetection: 0,
},
],
type: 'element',
},
{
children: [],
@@ -236,6 +247,7 @@ export const NESTED_RECORD = [
name: 'HeavyComponent',
},
],
type: 'element',
},
],
directives: [
@@ -248,6 +260,7 @@ export const NESTED_RECORD = [
name: 'DemoAppComponent',
},
],
type: 'element',
},
],
directives: [
@@ -260,6 +273,7 @@ export const NESTED_RECORD = [
changeDetection: 0,
},
],
type: 'element',
},
],
directives: [
@@ -272,6 +286,7 @@ export const NESTED_RECORD = [
name: 'AppComponent',
},
],
type: 'element',
},
{
children: [],
@@ -285,6 +300,7 @@ export const NESTED_RECORD = [
name: 'ZippyComponent',
},
],
type: 'element',
},
];
export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
@@ -425,6 +441,7 @@ export const NESTED_FORMATTED_FLAMEGRAPH_RECORD: FlamegraphNode[] = [
name: 'ZippyComponent',
},
],
type: 'element',
},
},
];
@@ -106,6 +106,7 @@ describe('getLabel cases', () => {
name: 'AppComponent',
},
],
type: 'element',
};
expect(formatter.getLabel(element)).toBe('AppComponent');
});
@@ -123,6 +124,7 @@ describe('getLabel cases', () => {
changeDetection: 0,
},
],
type: 'element',
};
expect(formatter.getLabel(element)).toBe('[RouterOutlet]');
});
@@ -148,6 +150,7 @@ describe('getLabel cases', () => {
name: 'TodoComponent',
},
],
type: 'element',
};
expect(formatter.getLabel(element)).toBe('TodoComponent[TooltipDirective]');
});
@@ -181,6 +184,7 @@ describe('getLabel cases', () => {
name: 'TodoComponent',
},
],
type: 'element',
};
expect(formatter.getLabel(element)).toBe('TodoComponent[TooltipDirective, RandomDirective]');
});
@@ -29,6 +29,10 @@ export abstract class RecordFormatter<T> {
abstract addFrame(nodes: T | T[], elements: ElementProfile[]): number | void;
getLabel(element: ElementProfile): string {
if (element.type === 'defer') {
return '@defer';
}
const name = element.directives
.filter((d) => d.isComponent)
.map((c) => c.name)
@@ -250,6 +250,7 @@ export interface DirectiveProfile {
export interface ElementProfile {
directives: DirectiveProfile[];
children: ElementProfile[];
type: 'defer' | 'element';
}
export interface ProfilerFrame {