From ccee9302a08d23ff8a026efbc01c6e012004c6e4 Mon Sep 17 00:00:00 2001 From: mgechev Date: Thu, 30 Apr 2020 16:28:25 -0700 Subject: [PATCH] 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. --- .../src/lib/client-event-subscribers.ts | 36 +-- .../component-inspector.ts | 6 +- .../src/lib/component-tree-identifiers.ts | 73 ----- .../src/lib/observer/capture.ts | 268 +++++++++++++++++ .../src/lib/observer/index.ts | 284 +++--------------- .../src/lib/observer/observer.ts | 155 +++++++--- 6 files changed, 437 insertions(+), 385 deletions(-) delete mode 100644 projects/ng-devtools-backend/src/lib/component-tree-identifiers.ts create mode 100644 projects/ng-devtools-backend/src/lib/observer/capture.ts diff --git a/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index 6e128c844fb..10beaa40ee8 100644 --- a/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -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): void => { messageBus.on('shutdown', shutdownCallback(messageBus)); @@ -71,18 +65,18 @@ const getLatestComponentExplorerViewCallback = (messageBus: MessageBus) ) => { // 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) => () => { }; 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) => ( 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, 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; }); diff --git a/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts b/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts index 4a9843ef745..9220e2ae1e1 100644 --- a/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts +++ b/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts @@ -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)); } } diff --git a/projects/ng-devtools-backend/src/lib/component-tree-identifiers.ts b/projects/ng-devtools-backend/src/lib/component-tree-identifiers.ts deleted file mode 100644 index 28d628bed90..00000000000 --- a/projects/ng-devtools-backend/src/lib/component-tree-identifiers.ts +++ /dev/null @@ -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); -}; diff --git a/projects/ng-devtools-backend/src/lib/observer/capture.ts b/projects/ng-devtools-backend/src/lib/observer/capture.ts new file mode 100644 index 00000000000..7c1561da530 --- /dev/null +++ b/projects/ng-devtools-backend/src/lib/observer/capture.ts @@ -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; +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(); + 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(); + 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(); + 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; +}; diff --git a/projects/ng-devtools-backend/src/lib/observer/index.ts b/projects/ng-devtools-backend/src/lib/observer/index.ts index cbf81191a50..624f3da0ae1 100644 --- a/projects/ng-devtools-backend/src/lib/observer/index.ts +++ b/projects/ng-devtools-backend/src/lib/observer/index.ts @@ -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; -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(); - 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(); - 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(); - 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; }; diff --git a/projects/ng-devtools-backend/src/lib/observer/observer.ts b/projects/ng-devtools-backend/src/lib/observer/observer.ts index 4f39ee7c776..f7bc7e0a979 100644 --- a/projects/ng-devtools-backend/src/lib/observer/observer.ts +++ b/projects/ng-devtools-backend/src/lib/observer/observer.ts @@ -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) {} + private _callbacks: Partial[] = []; + + constructor(config: Partial) { + 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): void { + this._callbacks.push(config); + } + + unsubscribe(config: Partial): 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 => {