mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(devtools): observer logic so that we can have a single instance
We currently have a single instance of the directive forest observer. It's shared between the identity tracker and the capturer.
This commit is contained in:
parent
2eb4771ea6
commit
ccee9302a0
6 changed files with 437 additions and 385 deletions
|
|
@ -9,15 +9,9 @@ import {
|
|||
ProfilerFrame,
|
||||
ComponentExplorerViewQuery,
|
||||
} from 'protocol';
|
||||
import { listenAndNotifyOnUpdates, onChangeDetection$ } from './change-detection-tracker';
|
||||
import {
|
||||
buildDirectiveForest,
|
||||
ComponentTreeNode,
|
||||
getLatestComponentState,
|
||||
queryDirectiveForest,
|
||||
updateState,
|
||||
} from './component-tree';
|
||||
import { start as startProfiling, stop as stopProfiling } from './observer';
|
||||
import { onChangeDetection$ } from './change-detection-tracker';
|
||||
import { ComponentTreeNode, getLatestComponentState, queryDirectiveForest, updateState } from './component-tree';
|
||||
import { start as startProfiling, stop as stopProfiling } from './observer/capture';
|
||||
import { serializeDirectiveState } from './state-serializer/state-serializer';
|
||||
import { ComponentInspector, ComponentInspectorOptions } from './component-inspector/component-inspector';
|
||||
import { setConsoleReference } from './set-console-reference';
|
||||
|
|
@ -29,8 +23,8 @@ import {
|
|||
appIsAngularInProdMode,
|
||||
appIsAngularIvy,
|
||||
} from './angular-check';
|
||||
import { getDirectiveId, getDirectiveForest, indexDirectiveForest, observeDOM } from './component-tree-identifiers';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { getDirectiveForestObserver } from './observer';
|
||||
|
||||
export const subscribeToClientEvents = (messageBus: MessageBus<Events>): void => {
|
||||
messageBus.on('shutdown', shutdownCallback(messageBus));
|
||||
|
|
@ -71,18 +65,18 @@ const getLatestComponentExplorerViewCallback = (messageBus: MessageBus<Events>)
|
|||
) => {
|
||||
// We want to force re-indexing of the component tree.
|
||||
// Pressing the refresh button means the user saw stuck UI.
|
||||
indexDirectiveForest();
|
||||
getDirectiveForestObserver().indexForest();
|
||||
if (!query) {
|
||||
messageBus.emit('latestComponentExplorerView', [
|
||||
{
|
||||
forest: prepareForestForSerialization(getDirectiveForest()),
|
||||
forest: prepareForestForSerialization(getDirectiveForestObserver().getDirectiveForest()),
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
messageBus.emit('latestComponentExplorerView', [
|
||||
{
|
||||
forest: prepareForestForSerialization(getDirectiveForest()),
|
||||
forest: prepareForestForSerialization(getDirectiveForestObserver().getDirectiveForest()),
|
||||
properties: getLatestComponentState(query),
|
||||
},
|
||||
]);
|
||||
|
|
@ -100,7 +94,7 @@ const stopProfilingCallback = (messageBus: MessageBus<Events>) => () => {
|
|||
};
|
||||
|
||||
const selectedComponentCallback = (position: ElementPosition) => {
|
||||
const node = queryDirectiveForest(position, getDirectiveForest());
|
||||
const node = queryDirectiveForest(position, getDirectiveForestObserver().getDirectiveForest());
|
||||
setConsoleReference({ node, position });
|
||||
};
|
||||
|
||||
|
|
@ -109,7 +103,7 @@ const getNestedPropertiesCallback = (messageBus: MessageBus<Events>) => (
|
|||
propPath: string[]
|
||||
) => {
|
||||
const emitEmpty = () => messageBus.emit('nestedProperties', [position, { props: {} }, propPath]);
|
||||
const node = queryDirectiveForest(position.element, getDirectiveForest());
|
||||
const node = queryDirectiveForest(position.element, getDirectiveForestObserver().getDirectiveForest());
|
||||
if (!node) {
|
||||
return emitEmpty();
|
||||
}
|
||||
|
|
@ -134,10 +128,7 @@ const checkForAngular = (messageBus: MessageBus<Events>, attempt = 0): void => {
|
|||
const ngVersion = getAngularVersion();
|
||||
const appIsIvy = appIsAngularIvy();
|
||||
if (!!ngVersion) {
|
||||
if (appIsIvy) {
|
||||
listenAndNotifyOnUpdates(buildDirectiveForest());
|
||||
observeDOM();
|
||||
}
|
||||
getDirectiveForestObserver();
|
||||
messageBus.emit('ngAvailability', [
|
||||
{ version: ngVersion.toString(), prodMode: appIsAngularInProdMode(), ivy: appIsIvy },
|
||||
]);
|
||||
|
|
@ -193,10 +184,13 @@ export const prepareForestForSerialization = (roots: ComponentTreeNode[]): Seria
|
|||
? {
|
||||
name: node.component.name,
|
||||
isElement: node.component.isElement,
|
||||
id: getDirectiveId(node.component.instance),
|
||||
id: getDirectiveForestObserver().getDirectiveId(node.component.instance),
|
||||
}
|
||||
: null,
|
||||
directives: node.directives.map((d) => ({ name: d.name, id: getDirectiveId(d.instance) })),
|
||||
directives: node.directives.map((d) => ({
|
||||
name: d.name,
|
||||
id: getDirectiveForestObserver().getDirectiveId(d.instance),
|
||||
})),
|
||||
children: prepareForestForSerialization(node.children),
|
||||
} as SerializableComponentTreeNode;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { unHighlight, highlight, findComponentAndHost } from '../highlighter';
|
|||
import { Type } from '@angular/core';
|
||||
import { buildDirectiveForest, ComponentTreeNode, findNodeInForest } from '../component-tree';
|
||||
import { ElementPosition } from 'protocol';
|
||||
import { getDirectiveId } from '../component-tree-identifiers';
|
||||
import { getDirectiveForestObserver } from '../observer';
|
||||
|
||||
export interface ComponentInspectorOptions {
|
||||
onComponentEnter: (id: number) => void;
|
||||
|
|
@ -46,7 +46,7 @@ export class ComponentInspector {
|
|||
e.preventDefault();
|
||||
|
||||
if (this._selectedComponent.component && this._selectedComponent.host) {
|
||||
this._onComponentSelect(getDirectiveId(this._selectedComponent.component));
|
||||
this._onComponentSelect(getDirectiveForestObserver().getDirectiveId(this._selectedComponent.component));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ export class ComponentInspector {
|
|||
unHighlight();
|
||||
if (this._selectedComponent.component && this._selectedComponent.host) {
|
||||
highlight(this._selectedComponent.host);
|
||||
this._onComponentEnter(getDirectiveId(this._selectedComponent.component));
|
||||
this._onComponentEnter(getDirectiveForestObserver().getDirectiveId(this._selectedComponent.component));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
import { DirectiveForestObserver } from './observer/observer';
|
||||
import { getDirectiveName } from './highlighter';
|
||||
|
||||
let observer: DirectiveForestObserver;
|
||||
|
||||
const markName = (s: string) => `🅰️ ${s}`;
|
||||
|
||||
const supportsPerformance = globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
|
||||
|
||||
const recordMark = (s: string) => {
|
||||
if (supportsPerformance) {
|
||||
performance.mark(markName(s));
|
||||
}
|
||||
};
|
||||
|
||||
const endMark = (nodeName: string) => {
|
||||
if (supportsPerformance) {
|
||||
const name = markName(nodeName);
|
||||
const start = `${name}_start`;
|
||||
const end = `${name}_end`;
|
||||
if (performance.getEntriesByName(start).length > 0) {
|
||||
performance.mark(end);
|
||||
performance.measure(name, start, end);
|
||||
}
|
||||
performance.clearMarks(start);
|
||||
performance.clearMarks(end);
|
||||
performance.clearMeasures(name);
|
||||
}
|
||||
};
|
||||
|
||||
export const observeDOM = () => {
|
||||
if (observer) {
|
||||
console.error('Cannot initialize the DOM observer more than once');
|
||||
return;
|
||||
}
|
||||
observer = new DirectiveForestObserver({
|
||||
onChangeDetectionStart(component: any): void {
|
||||
recordMark(`${getDirectiveName(component)}_start`);
|
||||
},
|
||||
onChangeDetectionEnd(component: any): void {
|
||||
endMark(getDirectiveName(component));
|
||||
},
|
||||
});
|
||||
observer.initialize();
|
||||
};
|
||||
|
||||
export const getDirectiveId = (dir: any) => {
|
||||
if (!observer) {
|
||||
console.warn('Observer not yet instantiated');
|
||||
return -1;
|
||||
}
|
||||
return observer.getDirectiveId(dir);
|
||||
};
|
||||
|
||||
export const getDirectiveForest = () => {
|
||||
if (!observer) {
|
||||
console.warn('Observer not yet instantiated');
|
||||
return [];
|
||||
}
|
||||
return observer.getDirectiveForest();
|
||||
};
|
||||
|
||||
export const indexDirectiveForest = () => {
|
||||
observer.indexForest();
|
||||
};
|
||||
|
||||
export const getDirectivePosition = (dir: any) => {
|
||||
if (!observer) {
|
||||
console.warn('Observer not yet instantiated');
|
||||
return null;
|
||||
}
|
||||
return observer.getDirectivePosition(dir);
|
||||
};
|
||||
268
projects/ng-devtools-backend/src/lib/observer/capture.ts
Normal file
268
projects/ng-devtools-backend/src/lib/observer/capture.ts
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
import { DirectiveForestObserver } from './observer';
|
||||
import { ElementPosition, ProfilerFrame, ElementProfile, DirectiveProfile, LifecycleProfile } from 'protocol';
|
||||
import { runOutsideAngular, isCustomElement } from '../utils';
|
||||
import { getDirectiveName } from '../highlighter';
|
||||
import { ComponentTreeNode } from '../component-tree';
|
||||
import { getDirectiveForestObserver } from '.';
|
||||
|
||||
let inProgress = false;
|
||||
let inChangeDetection = false;
|
||||
let eventMap: Map<any, DirectiveProfile>;
|
||||
let frameDuration = 0;
|
||||
let observerCallbacks: any = null;
|
||||
|
||||
export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
|
||||
if (inProgress) {
|
||||
throw new Error('Recording already in progress');
|
||||
}
|
||||
eventMap = new Map<any, DirectiveProfile>();
|
||||
inProgress = true;
|
||||
observerCallbacks = getObserverCallbacks(onFrame);
|
||||
getDirectiveForestObserver().subscribe(observerCallbacks);
|
||||
};
|
||||
|
||||
export const stop = (): ProfilerFrame => {
|
||||
const observer = getDirectiveForestObserver();
|
||||
const result = flushBuffer(observer);
|
||||
// We want to garbage collect the records;
|
||||
getDirectiveForestObserver().unsubscribe(observerCallbacks);
|
||||
inProgress = false;
|
||||
return result;
|
||||
};
|
||||
|
||||
const getObserverCallbacks = (onFrame: (frame: ProfilerFrame) => void) => {
|
||||
let changeDetectionStart = 0;
|
||||
let lifecycleHookStart = 0;
|
||||
return {
|
||||
// We flush here because it's possible the current node to overwrite
|
||||
// an existing removed node.
|
||||
onCreate(directive: any, node: Node, _: number, isComponent: boolean, position: ElementPosition): void {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
});
|
||||
},
|
||||
onChangeDetectionStart(component: any, node: Node): void {
|
||||
changeDetectionStart = performance.now();
|
||||
if (!inChangeDetection) {
|
||||
inChangeDetection = true;
|
||||
const source = getChangeDetectionSource();
|
||||
runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
inChangeDetection = false;
|
||||
onFrame(flushBuffer(getDirectiveForestObserver(), source));
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!eventMap.has(component)) {
|
||||
eventMap.set(component, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(component),
|
||||
isComponent: true,
|
||||
changeDetection: 0,
|
||||
lifecycle: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onChangeDetectionEnd(component: any, node: Node): void {
|
||||
const profile = eventMap.get(component);
|
||||
if (profile) {
|
||||
let current = profile.changeDetection;
|
||||
if (current === undefined) {
|
||||
current = 0;
|
||||
}
|
||||
const duration = performance.now() - changeDetectionStart;
|
||||
profile.changeDetection = current + duration;
|
||||
frameDuration += duration;
|
||||
} else {
|
||||
console.warn('Could not find profile for', component);
|
||||
}
|
||||
},
|
||||
onDestroy(directive: any, node: Node, _: number, isComponent: boolean, __: ElementPosition): void {
|
||||
// Make sure we reflect such directives in the report.
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isComponent && isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onLifecycleHookStart(
|
||||
directive: any,
|
||||
_: keyof LifecycleProfile,
|
||||
node: Node,
|
||||
__: number,
|
||||
isComponent: boolean
|
||||
): void {
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
});
|
||||
}
|
||||
lifecycleHookStart = performance.now();
|
||||
},
|
||||
onLifecycleHookEnd(directive: any, hook: keyof LifecycleProfile, _: Node, __: number, ___: boolean): void {
|
||||
const dir = eventMap.get(directive);
|
||||
if (!dir) {
|
||||
console.warn('Could not find directive in onLifecycleHook callback', directive, hook);
|
||||
return;
|
||||
}
|
||||
const duration = performance.now() - lifecycleHookStart;
|
||||
dir.lifecycle[hook] = (dir.lifecycle[hook] || 0) + duration;
|
||||
frameDuration += duration;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const insertOrMerge = (lastFrame: ElementProfile, profile: DirectiveProfile) => {
|
||||
let exists = false;
|
||||
lastFrame.directives.forEach((d) => {
|
||||
if (d.name === profile.name) {
|
||||
exists = true;
|
||||
let current = d.changeDetection;
|
||||
if (current === undefined) {
|
||||
current = 0;
|
||||
}
|
||||
d.changeDetection = current + (profile.changeDetection ?? 0);
|
||||
for (const key of Object.keys(profile.lifecycle)) {
|
||||
if (!d.lifecycle[key]) {
|
||||
d.lifecycle[key] = 0;
|
||||
}
|
||||
d.lifecycle[key] += profile.lifecycle[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!exists) {
|
||||
lastFrame.directives.push(profile);
|
||||
}
|
||||
};
|
||||
|
||||
const insertElementProfile = (frames: ElementProfile[], position: ElementPosition, profile?: DirectiveProfile) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const original = frames;
|
||||
for (let i = 0; i < position.length - 1; i++) {
|
||||
const pos = position[i];
|
||||
if (!frames[pos]) {
|
||||
// TODO(mgechev): consider how to ensure we don't hit this case
|
||||
console.warn('Unable to find parent node for', profile, original);
|
||||
return;
|
||||
}
|
||||
frames = frames[pos].children;
|
||||
}
|
||||
const lastIdx = position[position.length - 1];
|
||||
let lastFrame: ElementProfile = {
|
||||
children: [],
|
||||
directives: [],
|
||||
};
|
||||
if (frames[lastIdx]) {
|
||||
lastFrame = frames[lastIdx];
|
||||
} else {
|
||||
frames[lastIdx] = lastFrame;
|
||||
}
|
||||
insertOrMerge(lastFrame, profile);
|
||||
};
|
||||
|
||||
const prepareInitialFrame = (source: string, duration: number) => {
|
||||
const frame: ProfilerFrame = {
|
||||
source,
|
||||
duration,
|
||||
directives: [],
|
||||
};
|
||||
const observer = getDirectiveForestObserver();
|
||||
const directiveForest = observer.getDirectiveForest();
|
||||
const traverse = (node: ComponentTreeNode, children = frame.directives) => {
|
||||
let position: ElementPosition | undefined;
|
||||
if (node.component) {
|
||||
position = observer.getDirectivePosition(node.component.instance);
|
||||
} else {
|
||||
position = observer.getDirectivePosition(node.directives[0].instance);
|
||||
}
|
||||
if (position === undefined) {
|
||||
return;
|
||||
}
|
||||
const directives = node.directives.map((d) => {
|
||||
return {
|
||||
isComponent: false,
|
||||
isElement: false,
|
||||
name: getDirectiveName(d.instance),
|
||||
lifecycle: {},
|
||||
};
|
||||
});
|
||||
if (node.component) {
|
||||
directives.push({
|
||||
isElement: node.component.isElement,
|
||||
isComponent: true,
|
||||
lifecycle: {},
|
||||
name: getDirectiveName(node.component.instance),
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
children: [],
|
||||
directives,
|
||||
};
|
||||
children[position[position.length - 1]] = result;
|
||||
node.children.forEach((n) => traverse(n, result.children));
|
||||
};
|
||||
directiveForest.forEach((n) => traverse(n));
|
||||
return frame;
|
||||
};
|
||||
|
||||
const flushBuffer = (obs: DirectiveForestObserver, source: string = '') => {
|
||||
const items = Array.from(eventMap.keys());
|
||||
const positions: ElementPosition[] = [];
|
||||
const positionDirective = new Map<ElementPosition, any>();
|
||||
items.forEach((dir) => {
|
||||
const position = obs.getDirectivePosition(dir);
|
||||
if (position === undefined) {
|
||||
return;
|
||||
}
|
||||
positions.push(position);
|
||||
positionDirective.set(position, dir);
|
||||
});
|
||||
positions.sort(lexicographicOrder);
|
||||
|
||||
const result = prepareInitialFrame(source, frameDuration);
|
||||
frameDuration = 0;
|
||||
|
||||
positions.forEach((position) => {
|
||||
const dir = positionDirective.get(position);
|
||||
insertElementProfile(result.directives, position, eventMap.get(dir));
|
||||
});
|
||||
eventMap = new Map<any, DirectiveProfile>();
|
||||
return result;
|
||||
};
|
||||
|
||||
const getChangeDetectionSource = () => {
|
||||
const zone = (window as any).Zone;
|
||||
if (!zone || !zone.currentTask) {
|
||||
return '';
|
||||
}
|
||||
return zone.currentTask.source;
|
||||
};
|
||||
|
||||
const lexicographicOrder = (a: ElementPosition, b: ElementPosition) => {
|
||||
if (a.length < b.length) {
|
||||
return -1;
|
||||
}
|
||||
if (a.length > b.length) {
|
||||
return 1;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] < b[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[i] > b[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
|
@ -1,255 +1,53 @@
|
|||
import { DirectiveForestObserver } from './observer';
|
||||
import { ElementPosition, ProfilerFrame, ElementProfile, DirectiveProfile, LifecycleProfile } from 'protocol';
|
||||
import { runOutsideAngular, isCustomElement } from '../utils';
|
||||
import { getDirectiveName } from '../highlighter';
|
||||
import { ComponentTreeNode } from '../component-tree';
|
||||
import { DirectiveForestObserver } from './observer';
|
||||
import { LifecycleProfile } from 'protocol';
|
||||
|
||||
let observer: DirectiveForestObserver;
|
||||
let inProgress = false;
|
||||
let inChangeDetection = false;
|
||||
let eventMap: Map<any, DirectiveProfile>;
|
||||
let frameDuration = 0;
|
||||
const markName = (s: string, method: Method) => `🅰️ ${s}#${method}`;
|
||||
|
||||
export const start = (onFrame: (frame: ProfilerFrame) => void): void => {
|
||||
if (inProgress) {
|
||||
throw new Error('Recording already in progress');
|
||||
const supportsPerformance = globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function';
|
||||
|
||||
type Method = keyof LifecycleProfile | 'changeDetection';
|
||||
|
||||
const recordMark = (s: string, method: Method) => {
|
||||
if (supportsPerformance) {
|
||||
performance.mark(`${markName(s, method)}_start`);
|
||||
}
|
||||
};
|
||||
|
||||
const endMark = (nodeName: string, method: Method) => {
|
||||
if (supportsPerformance) {
|
||||
const name = markName(nodeName, method);
|
||||
const start = `${name}_start`;
|
||||
const end = `${name}_end`;
|
||||
if (performance.getEntriesByName(start).length > 0) {
|
||||
performance.mark(end);
|
||||
performance.measure(name, start, end);
|
||||
}
|
||||
performance.clearMarks(start);
|
||||
performance.clearMarks(end);
|
||||
performance.clearMeasures(name);
|
||||
}
|
||||
};
|
||||
|
||||
export let observer: DirectiveForestObserver;
|
||||
export const getDirectiveForestObserver = () => {
|
||||
if (observer) {
|
||||
return observer;
|
||||
}
|
||||
eventMap = new Map<any, DirectiveProfile>();
|
||||
inProgress = true;
|
||||
let changeDetectionStart = 0;
|
||||
let lifecycleHookStart = 0;
|
||||
observer = new DirectiveForestObserver({
|
||||
// We flush here because it's possible the current node to overwrite
|
||||
// an existing removed node.
|
||||
onCreate(directive: any, node: Node, _: number, isComponent: boolean, position: ElementPosition): void {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
});
|
||||
onChangeDetectionStart(component: any): void {
|
||||
recordMark(getDirectiveName(component), 'changeDetection');
|
||||
},
|
||||
onChangeDetectionStart(component: any, node: Node): void {
|
||||
changeDetectionStart = performance.now();
|
||||
if (!inChangeDetection) {
|
||||
inChangeDetection = true;
|
||||
const source = getChangeDetectionSource();
|
||||
runOutsideAngular(() => {
|
||||
setTimeout(() => {
|
||||
inChangeDetection = false;
|
||||
onFrame(flushBuffer(observer, source));
|
||||
});
|
||||
});
|
||||
}
|
||||
if (!eventMap.has(component)) {
|
||||
eventMap.set(component, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(component),
|
||||
isComponent: true,
|
||||
changeDetection: 0,
|
||||
lifecycle: {},
|
||||
});
|
||||
}
|
||||
onChangeDetectionEnd(component: any): void {
|
||||
endMark(getDirectiveName(component), 'changeDetection');
|
||||
},
|
||||
onChangeDetectionEnd(component: any, node: Node): void {
|
||||
const profile = eventMap.get(component);
|
||||
if (profile) {
|
||||
let current = profile.changeDetection;
|
||||
if (current === undefined) {
|
||||
current = 0;
|
||||
}
|
||||
const duration = performance.now() - changeDetectionStart;
|
||||
profile.changeDetection = current + duration;
|
||||
frameDuration += duration;
|
||||
} else {
|
||||
console.warn('Could not find profile for', component);
|
||||
}
|
||||
onLifecycleHookStart(component: any, lifecyle: keyof LifecycleProfile): void {
|
||||
recordMark(getDirectiveName(component), lifecyle);
|
||||
},
|
||||
onDestroy(directive: any, node: Node, _: number, isComponent: boolean, __: ElementPosition): void {
|
||||
// Make sure we reflect such directives in the report.
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isComponent && isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
});
|
||||
}
|
||||
},
|
||||
onLifecycleHookStart(directive: any, node: Node, _: number, isComponent: boolean): void {
|
||||
if (!eventMap.has(directive)) {
|
||||
eventMap.set(directive, {
|
||||
isElement: isCustomElement(node),
|
||||
name: getDirectiveName(directive),
|
||||
isComponent,
|
||||
lifecycle: {},
|
||||
});
|
||||
}
|
||||
lifecycleHookStart = performance.now();
|
||||
},
|
||||
onLifecycleHookEnd(directive: any, _: Node, __: number, ___: boolean, hook: keyof LifecycleProfile): void {
|
||||
const dir = eventMap.get(directive);
|
||||
if (!dir) {
|
||||
console.warn('Could not find directive in onLifecycleHook callback', directive, hook);
|
||||
return;
|
||||
}
|
||||
const duration = performance.now() - lifecycleHookStart;
|
||||
dir.lifecycle[hook] = (dir.lifecycle[hook] || 0) + duration;
|
||||
frameDuration += duration;
|
||||
onLifecycleHookEnd(component: any, lifecyle: keyof LifecycleProfile): void {
|
||||
endMark(getDirectiveName(component), lifecyle);
|
||||
},
|
||||
});
|
||||
observer.initialize();
|
||||
};
|
||||
|
||||
export const stop = (): ProfilerFrame => {
|
||||
const result = flushBuffer(observer);
|
||||
// We want to garbage collect the records;
|
||||
observer.destroy();
|
||||
inProgress = false;
|
||||
return result;
|
||||
};
|
||||
|
||||
const insertOrMerge = (lastFrame: ElementProfile, profile: DirectiveProfile) => {
|
||||
let exists = false;
|
||||
lastFrame.directives.forEach((d) => {
|
||||
if (d.name === profile.name) {
|
||||
exists = true;
|
||||
let current = d.changeDetection;
|
||||
if (current === undefined) {
|
||||
current = 0;
|
||||
}
|
||||
d.changeDetection = current + (profile.changeDetection ?? 0);
|
||||
for (const key of Object.keys(profile.lifecycle)) {
|
||||
if (!d.lifecycle[key]) {
|
||||
d.lifecycle[key] = 0;
|
||||
}
|
||||
d.lifecycle[key] += profile.lifecycle[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!exists) {
|
||||
lastFrame.directives.push(profile);
|
||||
}
|
||||
};
|
||||
|
||||
const insertElementProfile = (frames: ElementProfile[], position: ElementPosition, profile?: DirectiveProfile) => {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
const original = frames;
|
||||
for (let i = 0; i < position.length - 1; i++) {
|
||||
const pos = position[i];
|
||||
if (!frames[pos]) {
|
||||
// TODO(mgechev): consider how to ensure we don't hit this case
|
||||
console.warn('Unable to find parent node for', profile, original);
|
||||
return;
|
||||
}
|
||||
frames = frames[pos].children;
|
||||
}
|
||||
const lastIdx = position[position.length - 1];
|
||||
let lastFrame: ElementProfile = {
|
||||
children: [],
|
||||
directives: [],
|
||||
};
|
||||
if (frames[lastIdx]) {
|
||||
lastFrame = frames[lastIdx];
|
||||
} else {
|
||||
frames[lastIdx] = lastFrame;
|
||||
}
|
||||
insertOrMerge(lastFrame, profile);
|
||||
};
|
||||
|
||||
const prepareInitialFrame = (source: string, duration: number) => {
|
||||
const frame: ProfilerFrame = {
|
||||
source,
|
||||
duration,
|
||||
directives: [],
|
||||
};
|
||||
const directiveForest = observer.getDirectiveForest();
|
||||
const traverse = (node: ComponentTreeNode, children = frame.directives) => {
|
||||
let position: ElementPosition | undefined;
|
||||
if (node.component) {
|
||||
position = observer.getDirectivePosition(node.component.instance);
|
||||
} else {
|
||||
position = observer.getDirectivePosition(node.directives[0].instance);
|
||||
}
|
||||
if (position === undefined) {
|
||||
return;
|
||||
}
|
||||
const directives = node.directives.map((d) => {
|
||||
return {
|
||||
isComponent: false,
|
||||
isElement: false,
|
||||
name: getDirectiveName(d.instance),
|
||||
lifecycle: {},
|
||||
};
|
||||
});
|
||||
if (node.component) {
|
||||
directives.push({
|
||||
isElement: node.component.isElement,
|
||||
isComponent: true,
|
||||
lifecycle: {},
|
||||
name: getDirectiveName(node.component.instance),
|
||||
});
|
||||
}
|
||||
const result = {
|
||||
children: [],
|
||||
directives,
|
||||
};
|
||||
children[position[position.length - 1]] = result;
|
||||
node.children.forEach((n) => traverse(n, result.children));
|
||||
};
|
||||
directiveForest.forEach((n) => traverse(n));
|
||||
return frame;
|
||||
};
|
||||
|
||||
const flushBuffer = (obs: DirectiveForestObserver, source: string = '') => {
|
||||
const items = Array.from(eventMap.keys());
|
||||
const positions: ElementPosition[] = [];
|
||||
const positionDirective = new Map<ElementPosition, any>();
|
||||
items.forEach((dir) => {
|
||||
const position = obs.getDirectivePosition(dir);
|
||||
if (position === undefined) {
|
||||
return;
|
||||
}
|
||||
positions.push(position);
|
||||
positionDirective.set(position, dir);
|
||||
});
|
||||
positions.sort(lexicographicOrder);
|
||||
|
||||
const result = prepareInitialFrame(source, frameDuration);
|
||||
frameDuration = 0;
|
||||
|
||||
positions.forEach((position) => {
|
||||
const dir = positionDirective.get(position);
|
||||
insertElementProfile(result.directives, position, eventMap.get(dir));
|
||||
});
|
||||
eventMap = new Map<any, DirectiveProfile>();
|
||||
return result;
|
||||
};
|
||||
|
||||
const getChangeDetectionSource = () => {
|
||||
const zone = (window as any).Zone;
|
||||
if (!zone || !zone.currentTask) {
|
||||
return '';
|
||||
}
|
||||
return zone.currentTask.source;
|
||||
};
|
||||
|
||||
const lexicographicOrder = (a: ElementPosition, b: ElementPosition) => {
|
||||
if (a.length < b.length) {
|
||||
return -1;
|
||||
}
|
||||
if (a.length > b.length) {
|
||||
return 1;
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] < b[i]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[i] > b[i]) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return observer;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ElementPosition, LifecycleProfile } from 'protocol';
|
|||
import { componentMetadata } from '../utils';
|
||||
import { IdentityTracker, IndexedNode } from './identity-tracker';
|
||||
import { getLViewFromDirectiveOrElementInstance, getDirectiveHostElement } from '../lview-transform';
|
||||
import { DEV_TOOLS_HIGHLIGHT_NODE_ID, getDirectiveName } from '../highlighter';
|
||||
import { DEV_TOOLS_HIGHLIGHT_NODE_ID } from '../highlighter';
|
||||
|
||||
export type CreationCallback = (
|
||||
componentOrDirective: any,
|
||||
|
|
@ -14,18 +14,18 @@ export type CreationCallback = (
|
|||
|
||||
export type LifecycleStartCallback = (
|
||||
componentOrDirective: any,
|
||||
hook: keyof LifecycleProfile | 'unknown',
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
hook: keyof LifecycleProfile | 'unknown'
|
||||
isComponent: boolean
|
||||
) => void;
|
||||
|
||||
export type LifecycleEndCallback = (
|
||||
componentOrDirective: any,
|
||||
hook: keyof LifecycleProfile | 'unknown',
|
||||
node: Node,
|
||||
id: number,
|
||||
isComponent: boolean,
|
||||
hook: keyof LifecycleProfile | 'unknown'
|
||||
isComponent: boolean
|
||||
) => void;
|
||||
|
||||
export type ChangeDetectionStartCallback = (component: any, node: Node, id: number, position: ElementPosition) => void;
|
||||
|
|
@ -40,7 +40,7 @@ export type DestroyCallback = (
|
|||
position: ElementPosition
|
||||
) => void;
|
||||
|
||||
export interface Config {
|
||||
export interface Callbacks {
|
||||
onCreate: CreationCallback;
|
||||
onDestroy: DestroyCallback;
|
||||
onChangeDetectionStart: ChangeDetectionStartCallback;
|
||||
|
|
@ -103,7 +103,11 @@ export class DirectiveForestObserver {
|
|||
private _tracker = new IdentityTracker();
|
||||
private _forest: IndexedNode[] = [];
|
||||
|
||||
constructor(private _config: Partial<Config>) {}
|
||||
private _callbacks: Partial<Callbacks>[] = [];
|
||||
|
||||
constructor(config: Partial<Callbacks>) {
|
||||
this._callbacks.push(config);
|
||||
}
|
||||
|
||||
getDirectivePosition(dir: any): ElementPosition | undefined {
|
||||
const result = this._tracker.getDirectivePosition(dir);
|
||||
|
|
@ -153,12 +157,8 @@ export class DirectiveForestObserver {
|
|||
const { newNodes, removedNodes, indexedForest } = this._tracker.index();
|
||||
this._forest = indexedForest;
|
||||
newNodes.forEach((node) => {
|
||||
if (this._config.onLifecycleHookStart || this._config.onLifecycleHookEnd) {
|
||||
this._observeLifecycle(node.directive, node.isComponent);
|
||||
}
|
||||
if (node.isComponent && (this._config.onChangeDetectionStart || this._config.onChangeDetectionEnd)) {
|
||||
this._observeComponent(node.directive);
|
||||
}
|
||||
this._observeLifecycle(node.directive, node.isComponent);
|
||||
this._observeComponent(node.directive);
|
||||
this._fireCreationCallback(node.directive, node.isComponent);
|
||||
});
|
||||
removedNodes.forEach((node) => {
|
||||
|
|
@ -167,6 +167,14 @@ export class DirectiveForestObserver {
|
|||
});
|
||||
}
|
||||
|
||||
subscribe(config: Partial<Callbacks>): void {
|
||||
this._callbacks.push(config);
|
||||
}
|
||||
|
||||
unsubscribe(config: Partial<Callbacks>): void {
|
||||
this._callbacks.splice(this._callbacks.indexOf(config), 1);
|
||||
}
|
||||
|
||||
private _onMutation(records: MutationRecord[]): void {
|
||||
if (this._isDevToolsMutation(records)) {
|
||||
return;
|
||||
|
|
@ -175,37 +183,22 @@ export class DirectiveForestObserver {
|
|||
}
|
||||
|
||||
private _fireCreationCallback(component: any, isComponent: boolean): void {
|
||||
if (!this._config.onCreate) {
|
||||
return;
|
||||
}
|
||||
const position = this._tracker.getDirectivePosition(component);
|
||||
if (position === undefined) {
|
||||
return;
|
||||
}
|
||||
const id = this._tracker.getDirectiveId(component);
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
this._config.onCreate(component, getDirectiveHostElement(component), id, isComponent, position);
|
||||
this._onCreate(component, getDirectiveHostElement(component), id, isComponent, position);
|
||||
}
|
||||
|
||||
private _fireDestroyCallback(component: any, isComponent: boolean): void {
|
||||
if (!this._config.onDestroy) {
|
||||
return;
|
||||
}
|
||||
const position = this._tracker.getDirectivePosition(component);
|
||||
if (position === undefined) {
|
||||
return;
|
||||
}
|
||||
const id = this._tracker.getDirectiveId(component);
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
this._config.onDestroy(component, getDirectiveHostElement(component), id, isComponent, position);
|
||||
this._onDestroy(component, getDirectiveHostElement(component), id, isComponent, position);
|
||||
}
|
||||
|
||||
private _observeComponent(cmp: any): void {
|
||||
const declarations = componentMetadata(cmp);
|
||||
if (!declarations) {
|
||||
return;
|
||||
}
|
||||
const original = declarations.template;
|
||||
const self = this;
|
||||
if (original.patched) {
|
||||
|
|
@ -216,14 +209,10 @@ export class DirectiveForestObserver {
|
|||
const start = performance.now();
|
||||
const id = self._tracker.getDirectiveId(component);
|
||||
|
||||
if (self._config.onChangeDetectionStart && id !== undefined && position !== undefined) {
|
||||
self._config.onChangeDetectionStart(component, getDirectiveHostElement(component), id, position);
|
||||
}
|
||||
self._onChangeDetectionStart(component, getDirectiveHostElement(component), id, position);
|
||||
original.apply(this, arguments);
|
||||
if (self._tracker.hasDirective(component) && id !== undefined && position !== undefined) {
|
||||
if (self._config.onChangeDetectionEnd) {
|
||||
self._config.onChangeDetectionEnd(component, getDirectiveHostElement(component), id, position);
|
||||
}
|
||||
self._onChangeDetectionEnd(component, getDirectiveHostElement(component), id, position);
|
||||
} else {
|
||||
self._lastChangeDetection.set(component, performance.now() - start);
|
||||
}
|
||||
|
|
@ -253,13 +242,9 @@ export class DirectiveForestObserver {
|
|||
const id = self._tracker.getDirectiveId(this);
|
||||
const lifecycleHookName = getLifeCycleName(this, el);
|
||||
const element = getDirectiveHostElement(this);
|
||||
if (self._config.onLifecycleHookStart && id !== undefined) {
|
||||
self._config.onLifecycleHookStart(this, element, id, isComponent, lifecycleHookName);
|
||||
}
|
||||
self._onLifecycleHookStart(this, lifecycleHookName, element, id, isComponent);
|
||||
const result = el.apply(this, arguments);
|
||||
if (self._config.onLifecycleHookEnd && id !== undefined) {
|
||||
self._config.onLifecycleHookEnd(this, element, id, isComponent, lifecycleHookName);
|
||||
}
|
||||
self._onLifecycleHookEnd(this, lifecycleHookName, element, id, isComponent);
|
||||
return result;
|
||||
};
|
||||
current[idx].patched = true;
|
||||
|
|
@ -282,6 +267,86 @@ export class DirectiveForestObserver {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _onCreate(
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
___: boolean,
|
||||
position: ElementPosition | undefined
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
this._invokeCallback('onCreate', arguments);
|
||||
}
|
||||
|
||||
private _onDestroy(
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
___: boolean,
|
||||
position: ElementPosition | undefined
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
this._invokeCallback('onDestroy', arguments);
|
||||
}
|
||||
|
||||
private _onChangeDetectionStart(
|
||||
_: any,
|
||||
__: Node,
|
||||
id: number | undefined,
|
||||
position: ElementPosition | undefined
|
||||
): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
this._invokeCallback('onChangeDetectionStart', arguments);
|
||||
}
|
||||
|
||||
private _onChangeDetectionEnd(_: any, __: Node, id: number | undefined, position: ElementPosition | undefined): void {
|
||||
if (id === undefined || position === undefined) {
|
||||
return;
|
||||
}
|
||||
this._invokeCallback('onChangeDetectionEnd', arguments);
|
||||
}
|
||||
|
||||
private _onLifecycleHookStart(
|
||||
_: any,
|
||||
__: keyof LifecycleProfile | 'unknown',
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
this._invokeCallback('onLifecycleHookStart', arguments);
|
||||
}
|
||||
|
||||
private _onLifecycleHookEnd(
|
||||
_: any,
|
||||
__: keyof LifecycleProfile | 'unknown',
|
||||
___: Node,
|
||||
id: number | undefined,
|
||||
____: boolean
|
||||
): void {
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
this._invokeCallback('onLifecycleHookEnd', arguments);
|
||||
}
|
||||
|
||||
private _invokeCallback(name: keyof Callbacks, args: IArguments): void {
|
||||
this._callbacks.forEach((config) => {
|
||||
const cb = config[name];
|
||||
if (cb) {
|
||||
cb.apply(null, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const containsInternalElements = (nodes: NodeList): boolean => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue