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:
mgechev 2020-04-30 16:28:25 -07:00 committed by AleksanderBodurri
parent 2eb4771ea6
commit ccee9302a0
6 changed files with 437 additions and 385 deletions

View file

@ -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;
});

View file

@ -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));
}
}

View file

@ -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);
};

View 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;
};

View file

@ -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;
};

View file

@ -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 => {