diff --git a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel index 1581f435e133..5bd44297df42 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/BUILD.bazel @@ -1,6 +1,6 @@ load("//devtools/tools:defaults.bzl", "ts_project", "ts_test_library", "zoneless_web_test_suite") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//devtools:__subpackages__"]) ts_project( name = "lib", @@ -9,8 +9,8 @@ ts_project( ":client_event_subscribers", "//devtools/projects/ng-devtools-backend/src/lib/component-inspector", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", - "//devtools/projects/ng-devtools-backend/src/lib/hooks", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", + "//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", "//devtools/projects/protocol", ], @@ -130,8 +130,8 @@ ts_test_library( ], deps = [ ":client_event_subscribers", - "//:node_modules/rxjs", - "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:identity-tracker", + "//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler", "//devtools/projects/protocol", "//devtools/projects/shared-utils", ], @@ -149,8 +149,10 @@ ts_project( "//:node_modules/rxjs", "//devtools/projects/ng-devtools-backend/src/lib/component-inspector", "//devtools/projects/ng-devtools-backend/src/lib/component-tree", - "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", + "//devtools/projects/ng-devtools-backend/src/lib/profiling", + "//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler", "//devtools/projects/ng-devtools-backend/src/lib/state-serializer", "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts index 132c1a63abef..a2220b712161 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.spec.ts @@ -9,8 +9,8 @@ import {Events, MessageBus} from '../../../protocol'; import {subscribeToClientEvents} from './client-event-subscribers'; import {appIsAngular, appIsAngularIvy, appIsSupportedAngularVersion} from '../../../shared-utils'; -import {DirectiveForestHooks} from './hooks/hooks'; -import {of} from 'rxjs'; +import {Profiler} from './profiling/profiler'; +import {NodeArray} from './directive-forest/identity-tracker'; describe('ClientEventSubscriber', () => { let messageBusMock: MessageBus; @@ -43,7 +43,7 @@ describe('ClientEventSubscriber', () => { }); it('should setup inspector', () => { - subscribeToClientEvents(messageBusMock, {directiveForestHooks: MockDirectiveForestHooks}); + subscribeToClientEvents(messageBusMock, {profiler: MockProfiler}); expect(messageBusMock.on).toHaveBeenCalledWith('inspectorStart', jasmine.any(Function)); expect(messageBusMock.on).toHaveBeenCalledWith('inspectorEnd', jasmine.any(Function)); @@ -66,10 +66,8 @@ function mockAngular() { return appNode; } -class MockDirectiveForestHooks extends DirectiveForestHooks { - profiler = { - subscribe: () => {}, - changeDetection$: of(), - } as any as DirectiveForestHooks['profiler']; - initialize = () => {}; +class MockProfiler extends Profiler { + override destroy(): void {} + + override onIndexForest(_: NodeArray, __: NodeArray): void {} } diff --git a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts index 20ac9360e2c3..aae0b6e500c4 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/client-event-subscribers.ts @@ -51,9 +51,9 @@ import { updateState, } from './component-tree/component-tree'; import {unHighlight} from './highlighter'; -import {disableTimingAPI, enableTimingAPI, initializeOrGetDirectiveForestHooks} from './hooks'; -import {start as startProfiling, stop as stopProfiling} from './hooks/capture'; -import {DirectiveForestHooks} from './hooks/hooks'; +import {start as startProfiling, stop as stopProfiling} from './profiling/capture'; +import {disableTimingAPI, enableTimingAPI} from './profiling/timing-api'; +import {getProfiler, Profiler} from './profiling/profiler'; import {ComponentTreeNode} from './interfaces'; import {ngDebugClient, ngDebugDependencyInjectionApiIsSupported} from './ng-debug-api/ng-debug-api'; import {getSupportedApis} from './ng-debug-api/supported-apis'; @@ -63,13 +63,14 @@ import {serializeDirectiveState, serializeValue} from './state-serializer/state- import {runOutsideAngular, unwrapSignal} from './utils/general'; import {sanitizeObject} from './utils/serialization'; import {SignalGraphRef} from './utils/signal-graph-ref'; +import {getDirectiveForestManager} from './directive-forest/manager'; type InspectorRef = {ref: ComponentInspector | null}; export const subscribeToClientEvents = ( messageBus: MessageBus, depsForTestOnly?: { - directiveForestHooks?: typeof DirectiveForestHooks; + profiler?: new (...args: any[]) => Profiler; }, ): void => { const inspector: InspectorRef = {ref: null}; @@ -125,8 +126,8 @@ export const subscribeToClientEvents = ( // update requests, instead we want to request an update at most // once every 250ms runOutsideAngular(() => { - initializeOrGetDirectiveForestHooks(depsForTestOnly) - .profiler.changeDetection$.pipe(debounceTime(250)) + getProfiler(depsForTestOnly) + .changeDetection$.pipe(debounceTime(250)) .subscribe(() => messageBus.emit('componentTreeDirty')); }); } @@ -145,10 +146,10 @@ const getLatestComponentExplorerViewCallback = // We want to force re-indexing of the component tree. // Pressing the refresh button means the user saw stuck UI. - initializeOrGetDirectiveForestHooks().indexForest(); + getDirectiveForestManager().indexForest(); const forest = prepareForestForSerialization( - initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), + getDirectiveForestManager().getIndexedDirectiveForest(), ngDebugDependencyInjectionApiIsSupported(), ); @@ -157,10 +158,7 @@ const getLatestComponentExplorerViewCallback = return; } - const state = getLatestComponentState( - query, - initializeOrGetDirectiveForestHooks().getDirectiveForest(), - ); + const state = getLatestComponentState(query, getDirectiveForestManager().getDirectiveForest()); if (state) { const {directiveProperties} = state; @@ -214,7 +212,7 @@ const stopProfilingCallback = (messageBus: MessageBus) => () => { const selectedComponentCallback = (inspector: InspectorRef) => (position: ElementPosition) => { const node = queryDirectiveForest( position, - initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), + getDirectiveForestManager().getIndexedDirectiveForest(), ); setConsoleReference({node, position}); inspector.ref?.highlightByPosition(position); @@ -225,7 +223,7 @@ const getNestedPropertiesCallback = const emitEmpty = () => messageBus.emit('nestedProperties', [position, {props: {}}, propPath]); const node = queryDirectiveForest( position.element, - initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), + getDirectiveForestManager().getIndexedDirectiveForest(), ); if (!node) { return emitEmpty(); @@ -256,7 +254,7 @@ const getSignalNestedPropertiesCallback = messageBus.emit('signalNestedProperties', [position, {props: {}}, propPath]); const node = queryDirectiveForest( position.element, - initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), + getDirectiveForestManager().getIndexedDirectiveForest(), ); if (!node || !node.nativeElement) { return emitEmpty(); @@ -340,7 +338,7 @@ const checkForAngular = (messageBus: MessageBus): void => { } if (appIsIvy && appIsAngularInDevMode() && appIsSupportedAngularVersion()) { - initializeOrGetDirectiveForestHooks(); + getDirectiveForestManager(); } const devMode = appIsAngularInDevMode(); @@ -402,7 +400,7 @@ export interface SerializableComponentTreeNode extends DevToolsNode< } function getRouterInstance() { - const forest = initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(); + const forest = getDirectiveForestManager().getIndexedDirectiveForest(); const rootNode = forest[0]; if (!rootNode || !rootNode.nativeElement) { @@ -433,12 +431,12 @@ const prepareForestForSerialization = ( ? { name: node.component.name, isElement: node.component.isElement, - id: initializeOrGetDirectiveForestHooks().getDirectiveId(node.component.instance)!, + id: getDirectiveForestManager().getDirectiveId(node.component.instance)!, } : null, directives: node.directives?.map((d) => ({ name: d.name, - id: initializeOrGetDirectiveForestHooks().getDirectiveId(d.instance)!, + id: getDirectiveForestManager().getDirectiveId(d.instance)!, })), children: prepareForestForSerialization(node.children, includeResolutionPath), hydration: node.hydration, @@ -582,7 +580,7 @@ const logProvider = ( const getTransferStateCallback = (messageBus: MessageBus) => () => { const ng = ngDebugClient(); - const forest = initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(); + const forest = getDirectiveForestManager().getIndexedDirectiveForest(); if (forest.length === 0) { messageBus.emit('transferStateData', [null]); return; @@ -641,7 +639,7 @@ const getSignalGraphCallback = (messageBus: MessageBus) => (element: Ele // get injector from position const node = queryDirectiveForest( element, - initializeOrGetDirectiveForestHooks().getIndexedDirectiveForest(), + getDirectiveForestManager().getIndexedDirectiveForest(), ); if (!node) { messageBus.emit('latestSignalGraph', [null]); diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel index 1a2169d23291..0e538bea8537 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/BUILD.bazel @@ -1,6 +1,6 @@ load("//devtools/tools:defaults.bzl", "ts_project", "ts_test_library", "zoneless_web_test_suite") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//devtools:__subpackages__"]) ts_project( name = "component-inspector", @@ -9,7 +9,8 @@ ts_project( "//devtools/projects/ng-devtools-backend/src/lib:highlighter", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", "//devtools/projects/ng-devtools-backend/src/lib/component-tree", - "//devtools/projects/ng-devtools-backend/src/lib/hooks", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager", + "//devtools/projects/ng-devtools-backend/src/lib/profiling", "//devtools/projects/protocol", ], ) @@ -21,13 +22,7 @@ ts_test_library( ], deps = [ ":component-inspector", - "//:node_modules/@angular/core", - "//devtools/projects/ng-devtools-backend/src/lib:highlighter", - "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib/component-tree", - "//devtools/projects/ng-devtools-backend/src/lib/hooks", - "//devtools/projects/ng-devtools-backend/src/lib/utils", - "//devtools/projects/protocol", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager", ], ) diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts index d7268880eee9..8e4e8c64ed52 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {initializeOrGetDirectiveForestHooks} from '../hooks'; +import {getDirectiveForestManager} from '../directive-forest/manager'; import {ComponentInspector} from './component-inspector'; describe('ComponentInspector', () => { @@ -45,6 +45,6 @@ describe('ComponentInspector', () => { }); it('should always retrieve the same forest hook', () => { - expect(initializeOrGetDirectiveForestHooks()).toBe(initializeOrGetDirectiveForestHooks()); + expect(getDirectiveForestManager()).toBe(getDirectiveForestManager()); }); }); diff --git a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts index 4412d5cc67f6..e5393770d8a1 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/component-inspector/component-inspector.ts @@ -16,12 +16,13 @@ import { removeHydrationHighlights, unHighlight, } from '../highlighter'; -import {initializeOrGetDirectiveForestHooks} from '../hooks'; +import {getDirectiveForestManager} from '../directive-forest/manager'; import {ComponentTreeNode} from '../interfaces'; interface Type extends Function { new (...args: any[]): T; } + export interface ComponentInspectorOptions { onComponentEnter: (id: number) => void; onComponentSelect: (id: number) => void; @@ -65,7 +66,7 @@ export class ComponentInspector { if (this._selectedComponent.component && this._selectedComponent.host) { this._onComponentSelect( - initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!, + getDirectiveForestManager().getDirectiveId(this._selectedComponent.component)!, ); } } @@ -82,7 +83,7 @@ export class ComponentInspector { if (this._selectedComponent.component && this._selectedComponent.host) { highlightSelectedElement(this._selectedComponent.host); this._onComponentEnter( - initializeOrGetDirectiveForestHooks().getDirectiveId(this._selectedComponent.component)!, + getDirectiveForestManager().getDirectiveId(this._selectedComponent.component)!, ); } } @@ -102,7 +103,7 @@ export class ComponentInspector { } highlightByPosition(position: ElementPosition): void { - const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest(); + const forest: ComponentTreeNode[] = getDirectiveForestManager().getDirectiveForest(); const elementToHighlight: HTMLElement | null = findNodeInForest(position, forest); if (elementToHighlight) { highlightSelectedElement(elementToHighlight); @@ -110,7 +111,7 @@ export class ComponentInspector { } highlightHydrationNodes(): void { - const forest: ComponentTreeNode[] = initializeOrGetDirectiveForestHooks().getDirectiveForest(); + const forest: ComponentTreeNode[] = getDirectiveForestManager().getDirectiveForest(); // drop the root nodes, we don't want to highlight it const forestWithoutRoots = forest.flatMap((rootNode) => rootNode.children); diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel index 8f972bfd63a5..98c146688a79 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/BUILD.bazel @@ -1,12 +1,16 @@ load("//devtools/tools:defaults.bzl", "ts_project", "ts_test_library", "zoneless_web_test_suite") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//devtools:__subpackages__"]) ts_project( name = "directive-forest", srcs = glob( include = ["*.ts"], - exclude = ["*.spec.ts"], + exclude = [ + "*.spec.ts", + "identity-tracker.ts", + "manager.ts", + ], ), deps = [ "//:node_modules/@angular/core", @@ -22,13 +26,40 @@ ts_project( ], ) +ts_project( + name = "identity-tracker", + srcs = [ + "identity-tracker.ts", + ], + deps = [ + "//devtools/projects/ng-devtools-backend/src/lib:interfaces", + "//devtools/projects/ng-devtools-backend/src/lib/component-tree", + "//devtools/projects/protocol", + ], +) + +ts_project( + name = "manager", + srcs = [ + "manager.ts", + ], + deps = [ + ":identity-tracker", + "//devtools/projects/ng-devtools-backend/src/lib:interfaces", + "//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler", + "//devtools/projects/protocol", + ], +) + ts_test_library( name = "test_lib", srcs = [ + "identity-tracker.spec.ts", "render-tree.spec.ts", ], deps = [ ":directive-forest", + ":identity-tracker", "//:node_modules/@angular/core", "//devtools/projects/ng-devtools-backend/src/lib:interfaces", "//devtools/projects/ng-devtools-backend/src/lib/utils", diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.spec.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker.spec.ts similarity index 67% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.spec.ts rename to devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker.spec.ts index 467b4484ac5e..fd415a8c665e 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.spec.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker.spec.ts @@ -12,41 +12,41 @@ describe('IdentityTracker', () => { let tracker: IdentityTracker; beforeEach(() => { - (IdentityTracker as any)._instance = undefined; + (IdentityTracker as any).instance = undefined; tracker = IdentityTracker.getInstance(); }); afterEach(() => { - (IdentityTracker as any)._instance = undefined; + (IdentityTracker as any).instance = undefined; document.querySelectorAll('[ng-version]').forEach((el) => el.remove()); }); function seedDirective(opts: {dir: object; id: number; isComponent?: boolean}): void { const internal = tracker as any; - internal._currentDirectiveId.set(opts.dir, opts.id); - internal._currentDirectivePosition.set(opts.dir, [opts.id]); + internal.currentDirectiveId.set(opts.dir, opts.id); + internal.currentDirectivePosition.set(opts.dir, [opts.id]); internal.isComponent.set(opts.dir, opts.isComponent ?? false); } - describe('setProfilingActive', () => { - it('removes pending directives from all maps when profiling stops', () => { + describe('selectMode', () => { + it('removes pending directives from all maps when the mode is changed back to `normal`', () => { const dir = {}; seedDirective({dir, id: 0, isComponent: true}); - (tracker as any)._pendingRemovals.add(dir); + (tracker as any).pendingRemovals.add(dir); - tracker.setProfilingActive(false); + tracker.selectMode('normal'); expect(tracker.hasDirective(dir)).toBeFalse(); expect(tracker.getDirectiveId(dir)).toBeUndefined(); expect(tracker.getDirectivePosition(dir)).toBeUndefined(); }); - it('does not flush pending removals when profiling starts', () => { + it('does not flush pending removals when the `preservation` mode is selected', () => { const dir = {}; seedDirective({dir, id: 0, isComponent: true}); - (tracker as any)._pendingRemovals.add(dir); + (tracker as any).pendingRemovals.add(dir); - tracker.setProfilingActive(true); + tracker.selectMode('preservation'); expect(tracker.hasDirective(dir)).toBeTrue(); expect(tracker.getDirectiveId(dir)).toBe(0); @@ -55,35 +55,35 @@ describe('IdentityTracker', () => { it('clears the pending set after flushing', () => { const dir = {}; - (tracker as any)._pendingRemovals.add(dir); + (tracker as any).pendingRemovals.add(dir); - tracker.setProfilingActive(false); + tracker.selectMode('normal'); - expect((tracker as any)._pendingRemovals.size).toBe(0); + expect((tracker as any).pendingRemovals.size).toBe(0); }); it('handles an empty pending set gracefully', () => { - expect(() => tracker.setProfilingActive(false)).not.toThrow(); + expect(() => tracker.selectMode('normal')).not.toThrow(); }); it('flushes multiple pending directives at once', () => { const dirs = [{}, {}, {}]; dirs.forEach((dir, i) => { seedDirective({dir, id: i}); - (tracker as any)._pendingRemovals.add(dir); + (tracker as any).pendingRemovals.add(dir); }); - tracker.setProfilingActive(false); + tracker.selectMode('normal'); dirs.forEach((dir) => { expect(tracker.hasDirective(dir)).toBeFalse(); }); - expect((tracker as any)._pendingRemovals.size).toBe(0); + expect((tracker as any).pendingRemovals.size).toBe(0); }); }); describe('index() cleanup behavior', () => { - it('immediately removes a stale directive from all maps when not profiling', () => { + it('immediately removes a stale directive from all maps when the mode is set to `normal`', () => { const dir = {}; seedDirective({dir, id: 0}); @@ -94,34 +94,34 @@ describe('IdentityTracker', () => { expect(tracker.getDirectivePosition(dir)).toBeUndefined(); }); - it('keeps a stale directive in maps during profiling, staging it for deferred cleanup', () => { + it('keeps a stale directive in maps during `preservation` mode, staging it for deferred cleanup', () => { const dir = {}; seedDirective({dir, id: 0}); - tracker.setProfilingActive(true); + tracker.selectMode('preservation'); tracker.index(); expect(tracker.hasDirective(dir)).toBeTrue(); expect(tracker.getDirectiveId(dir)).toBe(0); expect(tracker.getDirectivePosition(dir)).toEqual([0]); - expect((tracker as any)._pendingRemovals.has(dir)).toBeTrue(); + expect((tracker as any).pendingRemovals.has(dir)).toBeTrue(); }); - it('removes deferred directives from maps once profiling stops', () => { + it('removes deferred directives from maps once the mode is reverted back to `normal`', () => { const dir = {}; seedDirective({dir, id: 0}); - tracker.setProfilingActive(true); + tracker.selectMode('preservation'); tracker.index(); expect(tracker.hasDirective(dir)).toBeTrue(); - tracker.setProfilingActive(false); + tracker.selectMode('normal'); expect(tracker.hasDirective(dir)).toBeFalse(); }); - it('includes removed directives in the returned removedNodes regardless of profiling state', () => { + it('includes removed directives in the returned removedNodes regardless of selected mode', () => { const dir = {}; seedDirective({dir, id: 0, isComponent: true}); @@ -136,7 +136,7 @@ describe('IdentityTracker', () => { const dir = {}; seedDirective({dir, id: 0}); - tracker.setProfilingActive(true); + tracker.selectMode('preservation'); tracker.index(); const {removedNodes} = tracker.index(); @@ -144,16 +144,16 @@ describe('IdentityTracker', () => { expect(removedNodes.some((n) => n.directive === dir)).toBeTrue(); }); - it('grows maps monotonically during profiling and fully clears on stop', () => { + it('grows maps monotonically during `preservation` mode and fully clears on stop', () => { const dirs = [{}, {}, {}]; dirs.forEach((dir, i) => seedDirective({dir, id: i})); - tracker.setProfilingActive(true); + tracker.selectMode('preservation'); tracker.index(); dirs.forEach((dir) => expect(tracker.hasDirective(dir)).toBeTrue()); - tracker.setProfilingActive(false); + tracker.selectMode('normal'); dirs.forEach((dir) => expect(tracker.hasDirective(dir)).toBeFalse()); }); diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker.ts new file mode 100644 index 000000000000..9f9fb69bf230 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/identity-tracker.ts @@ -0,0 +1,203 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {DevToolsNode, ElementPosition} from '../../../../protocol'; + +import {buildDirectiveForest} from '../component-tree/component-tree'; +import { + ComponentInstance, + ComponentInstanceType, + ComponentTreeNode, + DirectiveInstance, + DirectiveInstanceType, +} from '../interfaces'; + +export declare interface Type extends Function { + new (...args: any[]): T; +} + +interface TreeNode { + parent: TreeNode; + directive?: Type; + children: TreeNode[]; +} + +export type NodeArray = { + directive: DirectiveInstance; + isComponent: boolean; +}[]; + +export type IndexingOutput = { + newNodes: NodeArray; + removedNodes: NodeArray; + indexedForest: IndexedNode[]; + directiveForest: ComponentTreeNode[]; +}; + +/** + * Operating mode of the IdentityTracker. + * + * - `normal` (default) – normal operation; the data is kept only during the lifetime of directives + * - `preservation` – preserve the IDs of destroyed directives for look up; changing the mode cleans the data + */ +export type IdentityTrackerOpMode = 'normal' | 'preservation'; + +/** + * Indexes the directive forest by adding IDs and node positions. + */ +export class IdentityTracker { + private static instance: IdentityTracker; + + private directiveIdCounter = 0; + private currentDirectivePosition = new Map(); + private currentDirectiveId = new Map(); + private mode: IdentityTrackerOpMode = 'normal'; + + // References of removed/destroyed directives. Used during `preservation` mode. + private pendingRemovals = new Set(); + + isComponent = new Map(); + + // private constructor for Singleton Pattern + private constructor() {} + + static getInstance(): IdentityTracker { + if (!IdentityTracker.instance) { + IdentityTracker.instance = new IdentityTracker(); + } + return IdentityTracker.instance; + } + + getDirectivePosition(dir: DirectiveInstance): ElementPosition | undefined { + return this.currentDirectivePosition.get(dir); + } + + getDirectiveId(dir: DirectiveInstance): number | undefined { + return this.currentDirectiveId.get(dir); + } + + hasDirective(dir: DirectiveInstance): boolean { + return this.currentDirectiveId.has(dir); + } + + /** + * Change/select the operating mode of the `IdentityTracker`. + * + * Refer to `IdentityTrackerOpMode` for detailed information + * about the different modes. + */ + selectMode(mode: IdentityTrackerOpMode) { + this.mode = mode; + if (mode !== 'preservation') { + this.flushPendingRemovals(); + } + } + + index(): IndexingOutput { + const directiveForest = buildDirectiveForest(); + const indexedForest = indexForest(directiveForest); + const newNodes: NodeArray = []; + const removedNodes: NodeArray = []; + const allNodes = new Set(); + indexedForest.forEach((root) => this.indexInternal(root, null, newNodes, allNodes)); + this.currentDirectiveId.forEach((_: number, dir: DirectiveInstance) => { + if (!allNodes.has(dir)) { + removedNodes.push({directive: dir, isComponent: !!this.isComponent.get(dir)}); + if (this.mode === 'preservation') { + this.pendingRemovals.add(dir); + } else { + this.cleanupDirective(dir); + } + } + }); + return {newNodes, removedNodes, indexedForest, directiveForest}; + } + + private indexInternal( + node: IndexedNode, + parent: TreeNode | null, + newNodes: {directive: DirectiveInstance; isComponent: boolean}[], + allNodes: Set, + ): void { + if (node.component) { + allNodes.add(node.component.instance); + this.isComponent.set(node.component.instance, true); + this.indexNode(node.component.instance, node.position, newNodes); + } + (node.directives || []).forEach((dir) => { + allNodes.add(dir.instance); + this.isComponent.set(dir.instance, false); + this.indexNode(dir.instance, node.position, newNodes); + }); + if (node.controlFlowBlock) { + this.indexNode(node.controlFlowBlock, node.position, newNodes); + } + node.children.forEach((child) => this.indexInternal(child, parent, newNodes, allNodes)); + } + + private indexNode( + directive: DirectiveInstance, + position: ElementPosition, + newNodes: NodeArray, + ): void { + this.currentDirectivePosition.set(directive, position); + if (!this.currentDirectiveId.has(directive)) { + newNodes.push({directive, isComponent: !!this.isComponent.get(directive)}); + this.currentDirectiveId.set(directive, this.directiveIdCounter++); + } + } + + private cleanupDirective(dir: DirectiveInstance): void { + this.currentDirectiveId.delete(dir); + this.currentDirectivePosition.delete(dir); + this.isComponent.delete(dir); + } + + private flushPendingRemovals(): void { + for (const dir of this.pendingRemovals) { + this.cleanupDirective(dir); + } + this.pendingRemovals.clear(); + } + + destroy(): void { + this.currentDirectivePosition = new Map(); + this.currentDirectiveId = new Map(); + this.isComponent = new Map(); + this.pendingRemovals.clear(); + this.mode = 'normal'; + } +} + +export interface IndexedNode extends DevToolsNode { + position: ElementPosition; + children: IndexedNode[]; +} + +const indexTree = >( + node: T, + idx: number, + parentPosition: number[] = [], +): IndexedNode => { + const position = parentPosition.concat([idx]); + return { + position, + tagName: node.tagName, + component: node.component, + directives: node.directives?.map((d) => ({position, ...d})), + children: node.children.map((n, i) => indexTree(n, i, position)), + nativeElement: node.nativeElement, + hydration: node.hydration, + controlFlowBlock: node.controlFlowBlock, + injector: node.injector, + } as IndexedNode; +}; + +export const indexForest = >( + forest: T[], +): IndexedNode[] => forest.map((n, i) => indexTree(n, i)); diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/index.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/index.ts index 33126ded27b5..04bdf1963d0c 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/index.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/index.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ComponentTreeNode} from '../interfaces'; import {LTreeStrategy} from './ltree'; import {RTreeStrategy} from './render-tree'; diff --git a/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts new file mode 100644 index 000000000000..0376de18c553 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/directive-forest/manager.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {ElementPosition} from '../../../../protocol'; +import {ComponentTreeNode, DirectiveInstance} from '../interfaces'; +import {getProfiler} from '../profiling/profiler'; +import {IdentityTracker, IndexedNode, IndexingOutput} from './identity-tracker'; + +// Global reference. +let directiveForestManager: DirectiveForestManager; + +/** + * Exposes latest directive forest state. + * Delegates forest indexing to IdentityTracker Singleton. + */ +export class DirectiveForestManager { + private _tracker = IdentityTracker.getInstance(); + private _forest: ComponentTreeNode[] = []; + private _indexedForest: IndexedNode[] = []; + private _indexForestCbs: ((output: IndexingOutput) => void)[] = []; + + getDirectivePosition(dir: DirectiveInstance): ElementPosition | undefined { + const result = this._tracker.getDirectivePosition(dir); + if (result === undefined) { + console.warn('Unable to find position of', dir); + } + return result; + } + + getDirectiveId(dir: DirectiveInstance): number | undefined { + const result = this._tracker.getDirectiveId(dir); + if (result === undefined) { + console.warn('Unable to find ID of', result); + } + return result; + } + + getIndexedDirectiveForest(): IndexedNode[] { + return this._indexedForest; + } + + getDirectiveForest(): ComponentTreeNode[] { + return this._forest; + } + + initialize(): void { + this.indexForest(); + } + + indexForest(): void { + const output = this._tracker.index(); + this._indexedForest = output.indexedForest; + this._forest = output.directiveForest; + + for (const cb of this._indexForestCbs) { + cb(output); + } + } + + /** + * Listen to forest indexing event. + * + * @param cb + * @returns An unlisten function. + */ + onIndexForest(cb: (output: IndexingOutput) => void) { + this._indexForestCbs.push(cb); + + return () => { + const idx = this._indexForestCbs.indexOf(cb); + if (idx > -1) { + this._indexForestCbs.splice(idx, 1); + } + }; + } +} + +/** + * Get the directive forest manager. + * Initializes the manager if it wasn't requested before. + */ +export function getDirectiveForestManager(): DirectiveForestManager { + if (directiveForestManager) { + return directiveForestManager; + } else { + directiveForestManager = new DirectiveForestManager(); + } + + directiveForestManager.onIndexForest(({newNodes, removedNodes}) => { + getProfiler().onIndexForest(newNodes, removedNodes); + }); + directiveForestManager.initialize(); + + return directiveForestManager; +} diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel deleted file mode 100644 index 29e1936ed211..000000000000 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/BUILD.bazel +++ /dev/null @@ -1,32 +0,0 @@ -load("//devtools/tools:defaults.bzl", "ts_project") - -package(default_visibility = ["//visibility:public"]) - -ts_project( - name = "hooks", - srcs = glob( - include = ["*.ts"], - exclude = [ - "*.spec.ts", - "identity-tracker.ts", - ], - ), - deps = [ - ":identity_tracker", - "//devtools/projects/ng-devtools-backend/src/lib:highlighter", - "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib/hooks/profiler", - "//devtools/projects/ng-devtools-backend/src/lib/utils", - "//devtools/projects/protocol", - ], -) - -ts_project( - name = "identity_tracker", - srcs = ["identity-tracker.ts"], - deps = [ - "//devtools/projects/ng-devtools-backend/src/lib:interfaces", - "//devtools/projects/ng-devtools-backend/src/lib/component-tree", - "//devtools/projects/protocol", - ], -) diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/hooks.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/hooks.ts deleted file mode 100644 index 01e21ed6ef52..000000000000 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/hooks.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {ElementPosition} from '../../../../protocol'; - -import {ComponentTreeNode} from '../interfaces'; - -import {IdentityTracker, IndexedNode} from './identity-tracker'; -import {Profiler, selectProfilerStrategy} from './profiler'; - -/** - * Class to hook into directive forest. - * - * Exposes latest directive forest state. - * - * Delegates profiling to a Profiler instance. - * Delegates forest indexing to IdentityTracker Singleton - */ -export class DirectiveForestHooks { - private _tracker = IdentityTracker.getInstance(); - private _forest: ComponentTreeNode[] = []; - private _indexedForest: IndexedNode[] = []; - - profiler: Profiler = selectProfilerStrategy(); - - getDirectivePosition(dir: any): ElementPosition | undefined { - const result = this._tracker.getDirectivePosition(dir); - if (result === undefined) { - console.warn('Unable to find position of', dir); - } - return result; - } - - getDirectiveId(dir: any): number | undefined { - const result = this._tracker.getDirectiveId(dir); - if (result === undefined) { - console.warn('Unable to find ID of', result); - } - return result; - } - - getIndexedDirectiveForest(): IndexedNode[] { - return this._indexedForest; - } - - getDirectiveForest(): ComponentTreeNode[] { - return this._forest; - } - - initialize(): void { - this.indexForest(); - } - - indexForest(): void { - const {newNodes, removedNodes, indexedForest, directiveForest} = this._tracker.index(); - this._indexedForest = indexedForest; - this._forest = directiveForest; - this.profiler.onIndexForest(newNodes, removedNodes); - } -} diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts deleted file mode 100644 index 6ee4f58c389f..000000000000 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/identity-tracker.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {DevToolsNode, ElementPosition} from '../../../../protocol'; - -import {buildDirectiveForest} from '../component-tree/component-tree'; -import {ComponentInstanceType, ComponentTreeNode, DirectiveInstanceType} from '../interfaces'; - -export declare interface Type extends Function { - new (...args: any[]): T; -} - -interface TreeNode { - parent: TreeNode; - directive?: Type; - children: TreeNode[]; -} - -export type NodeArray = { - directive: any; - isComponent: boolean; -}[]; - -export class IdentityTracker { - private static _instance: IdentityTracker; - - private _directiveIdCounter = 0; - private _currentDirectivePosition = new Map(); - private _currentDirectiveId = new Map(); - isComponent = new Map(); - - /** - * Directives that were removed while profiling was active. - * Cleanup is deferred until profiling stops so that the profiler - * can still look up IDs / positions of destroyed components. - */ - private _pendingRemovals = new Set(); - private _isProfiling = false; - - // private constructor for Singleton Pattern - private constructor() {} - - static getInstance(): IdentityTracker { - if (!IdentityTracker._instance) { - IdentityTracker._instance = new IdentityTracker(); - } - return IdentityTracker._instance; - } - - getDirectivePosition(dir: any): ElementPosition | undefined { - return this._currentDirectivePosition.get(dir); - } - - getDirectiveId(dir: any): number | undefined { - return this._currentDirectiveId.get(dir); - } - - hasDirective(dir: any): boolean { - return this._currentDirectiveId.has(dir); - } - - /** - * Toggle profiling state. While profiling is active, removed directive - * entries are kept so the profiler can still resolve IDs and positions. - * When profiling stops, deferred removals are flushed. - */ - setProfilingActive(active: boolean): void { - this._isProfiling = active; - if (!active) { - this._flushPendingRemovals(); - } - } - - index(): { - newNodes: NodeArray; - removedNodes: NodeArray; - indexedForest: IndexedNode[]; - directiveForest: ComponentTreeNode[]; - } { - const directiveForest = buildDirectiveForest(); - const indexedForest = indexForest(directiveForest); - const newNodes: NodeArray = []; - const removedNodes: NodeArray = []; - const allNodes = new Set(); - indexedForest.forEach((root) => this._index(root, null, newNodes, allNodes)); - this._currentDirectiveId.forEach((_: number, dir: any) => { - if (!allNodes.has(dir)) { - removedNodes.push({directive: dir, isComponent: !!this.isComponent.get(dir)}); - if (this._isProfiling) { - this._pendingRemovals.add(dir); - } else { - this._cleanupDirective(dir); - } - } - }); - return {newNodes, removedNodes, indexedForest, directiveForest}; - } - - private _index( - node: IndexedNode, - parent: TreeNode | null, - newNodes: {directive: any; isComponent: boolean}[], - allNodes: Set, - ): void { - if (node.component) { - allNodes.add(node.component.instance); - this.isComponent.set(node.component.instance, true); - this._indexNode(node.component.instance, node.position, newNodes); - } - (node.directives || []).forEach((dir) => { - allNodes.add(dir.instance); - this.isComponent.set(dir.instance, false); - this._indexNode(dir.instance, node.position, newNodes); - }); - if (node.controlFlowBlock) { - this._indexNode(node.controlFlowBlock, node.position, newNodes); - } - node.children.forEach((child) => this._index(child, parent, newNodes, allNodes)); - } - - private _indexNode(directive: any, position: ElementPosition, newNodes: NodeArray): void { - this._currentDirectivePosition.set(directive, position); - if (!this._currentDirectiveId.has(directive)) { - newNodes.push({directive, isComponent: !!this.isComponent.get(directive)}); - this._currentDirectiveId.set(directive, this._directiveIdCounter++); - } - } - - private _cleanupDirective(dir: any): void { - this._currentDirectiveId.delete(dir); - this._currentDirectivePosition.delete(dir); - this.isComponent.delete(dir); - } - - private _flushPendingRemovals(): void { - for (const dir of this._pendingRemovals) { - this._cleanupDirective(dir); - } - this._pendingRemovals.clear(); - } - - destroy(): void { - this._currentDirectivePosition = new Map(); - this._currentDirectiveId = new Map(); - this.isComponent = new Map(); - this._pendingRemovals.clear(); - this._isProfiling = false; - } -} - -export interface IndexedNode extends DevToolsNode { - position: ElementPosition; - children: IndexedNode[]; -} - -const indexTree = >( - node: T, - idx: number, - parentPosition: number[] = [], -): IndexedNode => { - const position = parentPosition.concat([idx]); - return { - position, - tagName: node.tagName, - component: node.component, - directives: node.directives?.map((d) => ({position, ...d})), - children: node.children.map((n, i) => indexTree(n, i, position)), - nativeElement: node.nativeElement, - hydration: node.hydration, - controlFlowBlock: node.controlFlowBlock, - injector: node.injector, - } as IndexedNode; -}; - -export const indexForest = >( - forest: T[], -): IndexedNode[] => forest.map((n, i) => indexTree(n, i)); diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts b/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts deleted file mode 100644 index 1cec578c1ed9..000000000000 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {LifecycleProfile} from '../../../../protocol'; - -import {getDirectiveName} from '../highlighter'; - -import {DirectiveForestHooks} from './hooks'; - -const markName = (s: string, method: Method) => `🅰️ ${s}#${method}`; - -const supportsPerformance = - globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function'; - -type Method = keyof LifecycleProfile | 'changeDetection' | string; - -const recordMark = (s: string, method: Method) => { - if (supportsPerformance) { - // tslint:disable-next-line:ban - 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) { - // tslint:disable-next-line:ban - performance.mark(end); - - const measureOptions = { - start, - end, - detail: { - devtools: { - dataType: 'track-entry', - color: 'primary', - track: '🅰️ Angular DevTools', - }, - }, - }; - performance.measure(name, measureOptions); - } - performance.clearMarks(start); - performance.clearMarks(end); - performance.clearMeasures(name); - } -}; - -let timingAPIFlag = false; - -export const enableTimingAPI = () => (timingAPIFlag = true); -export const disableTimingAPI = () => (timingAPIFlag = false); - -const timingAPIEnabled = () => timingAPIFlag; - -let directiveForestHooks: DirectiveForestHooks; - -export const initializeOrGetDirectiveForestHooks = ( - depsForTestOnly: { - directiveForestHooks?: typeof DirectiveForestHooks; - } = {}, -) => { - // Allow for overriding the DirectiveForestHooks implementation for testing purposes. - if (depsForTestOnly.directiveForestHooks) { - directiveForestHooks = new depsForTestOnly.directiveForestHooks(); - } - - if (directiveForestHooks) { - return directiveForestHooks; - } else { - directiveForestHooks = new DirectiveForestHooks(); - } - - directiveForestHooks.profiler.subscribe({ - onChangeDetectionStart(component: any): void { - if (!timingAPIEnabled()) { - return; - } - recordMark(getDirectiveName(component), 'changeDetection'); - }, - onChangeDetectionEnd(component: any): void { - if (!timingAPIEnabled()) { - return; - } - endMark(getDirectiveName(component), 'changeDetection'); - }, - onLifecycleHookStart(component: any, lifecyle: keyof LifecycleProfile): void { - if (!timingAPIEnabled()) { - return; - } - recordMark(getDirectiveName(component), lifecyle); - }, - onLifecycleHookEnd(component: any, lifecyle: keyof LifecycleProfile): void { - if (!timingAPIEnabled()) { - return; - } - endMark(getDirectiveName(component), lifecyle); - }, - onOutputStart(component: any, output: string): void { - if (!timingAPIEnabled()) { - return; - } - recordMark(getDirectiveName(component), output); - }, - onOutputEnd(component: any, output: string): void { - if (!timingAPIEnabled()) { - return; - } - endMark(getDirectiveName(component), output); - }, - }); - directiveForestHooks.initialize(); - return directiveForestHooks; -}; diff --git a/devtools/projects/ng-devtools-backend/src/lib/interfaces.ts b/devtools/projects/ng-devtools-backend/src/lib/interfaces.ts index 9997676e8e79..381e06d54304 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/interfaces.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/interfaces.ts @@ -9,22 +9,29 @@ import {DevToolsNode} from '../../../protocol'; export interface DebuggingAPI { - getComponent(node: Node): any; - getDirectives(node: Node): any[]; - getHostElement(cmp: any): HTMLElement; + getComponent(node: Node): ComponentInstance; + getDirectives(node: Node): DirectiveInstance[]; + getHostElement(cmp: ComponentInstance): HTMLElement; } + +export type DirectiveInstance = any; + +export type ComponentInstance = DirectiveInstance; + export interface DirectiveInstanceType { - instance: any; + instance: DirectiveInstance; name: string; } export interface ComponentInstanceType { - instance: any; + instance: ComponentInstance; name: string; isElement: boolean; } -export interface ComponentTreeNode - extends DevToolsNode { +export interface ComponentTreeNode extends DevToolsNode< + DirectiveInstanceType, + ComponentInstanceType +> { children: ComponentTreeNode[]; } diff --git a/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel new file mode 100644 index 000000000000..596f7fbf9696 --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/BUILD.bazel @@ -0,0 +1,22 @@ +load("//devtools/tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//devtools:__subpackages__"]) + +ts_project( + name = "profiling", + srcs = glob( + include = ["*.ts"], + exclude = [ + "*.spec.ts", + ], + ), + deps = [ + "//devtools/projects/ng-devtools-backend/src/lib:highlighter", + "//devtools/projects/ng-devtools-backend/src/lib:interfaces", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:identity-tracker", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:manager", + "//devtools/projects/ng-devtools-backend/src/lib/profiling/profiler", + "//devtools/projects/ng-devtools-backend/src/lib/utils", + "//devtools/projects/protocol", + ], +) diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts similarity index 80% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts rename to devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts index dc23aab837d5..eb9fa0c42368 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/capture.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/capture.ts @@ -16,13 +16,12 @@ import { } from '../../../../protocol'; import {getDirectiveName} from '../highlighter'; -import {ComponentTreeNode} from '../interfaces'; +import {ComponentInstance, ComponentTreeNode, DirectiveInstance} from '../interfaces'; import {isCustomElement, runOutsideAngular} from '../utils/general'; -import {initializeOrGetDirectiveForestHooks} from '.'; -import {DirectiveForestHooks} from './hooks'; -import {IdentityTracker} from './identity-tracker'; -import {Hooks} from './profiler'; +import {DirectiveForestManager, getDirectiveForestManager} from '../directive-forest/manager'; +import {IdentityTracker} from '../directive-forest/identity-tracker'; +import {getProfiler, Hooks} from './profiler'; let inProgress = false; let inChangeDetection = false; @@ -41,28 +40,39 @@ export const start = (onFrame: (frame: ProfilerFrame) => void): void => { } eventMap = new Map(); inProgress = true; - IdentityTracker.getInstance().setProfilingActive(true); + + // Enable preservation mode. While it is active, removed directive + // entries are kept so the profiler can still resolve IDs and positions. + // When profiling stops, the mode is changed back to `normal` and + // deferred removals are flushed. + IdentityTracker.getInstance().selectMode('preservation'); + hooks = getHooks(onFrame); - initializeOrGetDirectiveForestHooks().profiler.subscribe(hooks); + getProfiler().subscribe(hooks); }; export const stop = (): ProfilerFrame => { - const directiveForestHooks = initializeOrGetDirectiveForestHooks(); - const result = flushBuffer(directiveForestHooks); - initializeOrGetDirectiveForestHooks().profiler.unsubscribe(hooks); - hooks = {}; + const directiveForestManager = getDirectiveForestManager(); + const result = flushBuffer(directiveForestManager); + getProfiler().unsubscribe(hooks); inProgress = false; - IdentityTracker.getInstance().setProfilingActive(false); + hooks = {}; + IdentityTracker.getInstance().selectMode('normal'); + return result; }; -const startEvent = (map: Record, directive: any, label: string) => { +const startEvent = (map: Record, directive: DirectiveInstance, label: string) => { const name = getDirectiveName(directive); const key = `${name}#${label}`; map[key] = performance.now(); }; -const getEventStart = (map: Record, directive: any, label: string) => { +const getEventStart = ( + map: Record, + directive: DirectiveInstance, + label: string, +) => { const name = getDirectiveName(directive); const key = `${name}#${label}`; return map[key]; @@ -73,13 +83,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { 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 { + onCreate(directive: DirectiveInstance, node: Node, _: number, isComponent: boolean): void { eventMap.set(directive, { isElement: isCustomElement(node), name: getDirectiveName(directive), @@ -88,7 +92,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { outputs: {}, }); }, - onChangeDetectionStart(component: any, node: Node): void { + onChangeDetectionStart(component: ComponentInstance, node: Node): void { startEvent(timeStartMap, component, 'changeDetection'); if (!inChangeDetection) { inChangeDetection = true; @@ -96,7 +100,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { runOutsideAngular(() => { Promise.resolve().then(() => { inChangeDetection = false; - onFrame(flushBuffer(initializeOrGetDirectiveForestHooks(), source)); + onFrame(flushBuffer(getDirectiveForestManager(), source)); }); }); } @@ -111,7 +115,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { }); } }, - onChangeDetectionEnd(component: any): void { + onChangeDetectionEnd(component: ComponentInstance): void { const profile = eventMap.get(component); if (profile) { @@ -131,7 +135,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { } }, onDestroy( - directive: any, + directive: DirectiveInstance, node: Node, _: number, isComponent: boolean, @@ -149,10 +153,10 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { } }, onLifecycleHookStart( - directive: any, + directive: DirectiveInstance, hookName: keyof LifecycleProfile, node: Node, - id: number, + _: number, isComponent: boolean, ): void { startEvent(timeStartMap, directive, hookName); @@ -167,7 +171,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { } }, onLifecycleHookEnd( - directive: any, + directive: DirectiveInstance, hookName: keyof LifecycleProfile, _: Node, __: number, @@ -187,10 +191,10 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { frameDuration += duration; }, onOutputStart( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, outputName: string, node: Node, - id: number | undefined, + _: number | undefined, isComponent: boolean, ): void { startEvent(timeStartMap, componentOrDirective, outputName); @@ -204,7 +208,7 @@ const getHooks = (onFrame: (frame: ProfilerFrame) => void): Partial => { }); } }, - onOutputEnd(componentOrDirective: any, outputName: string): void { + onOutputEnd(componentOrDirective: DirectiveInstance, outputName: string): void { const name = outputName; const entry = eventMap.get(componentOrDirective); const startTimestamp = getEventStart(timeStartMap, componentOrDirective, name); @@ -293,16 +297,16 @@ const prepareInitialFrame = (source: string, duration: number) => { duration, directives: [], }; - const directiveForestHooks = initializeOrGetDirectiveForestHooks(); - const directiveForest = directiveForestHooks.getIndexedDirectiveForest(); + const directiveForestManager = getDirectiveForestManager(); + const directiveForest = directiveForestManager.getIndexedDirectiveForest(); const traverse = (node: ComponentTreeNode, children = frame.directives) => { let position: ElementPosition | undefined; if (node.component) { - position = directiveForestHooks.getDirectivePosition(node.component.instance); + position = directiveForestManager.getDirectivePosition(node.component.instance); } else if (node.directives?.[0]) { - position = directiveForestHooks.getDirectivePosition(node.directives[0].instance); + position = directiveForestManager.getDirectivePosition(node.directives[0].instance); } else if (node.controlFlowBlock) { - position = directiveForestHooks.getDirectivePosition(node.controlFlowBlock); + position = directiveForestManager.getDirectivePosition(node.controlFlowBlock); } if (position === undefined) { @@ -339,12 +343,12 @@ const prepareInitialFrame = (source: string, duration: number) => { return frame; }; -const flushBuffer = (directiveForestHooks: DirectiveForestHooks, source: string = '') => { +const flushBuffer = (directiveForestManager: DirectiveForestManager, source: string = '') => { const items = Array.from(eventMap.keys()); const positions: ElementPosition[] = []; - const positionDirective = new Map(); + const positionDirective = new Map(); items.forEach((dir) => { - const position = directiveForestHooks.getDirectivePosition(dir); + const position = directiveForestManager.getDirectivePosition(dir); if (position === undefined) { return; } diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/BUILD.bazel similarity index 69% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel rename to devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/BUILD.bazel index 07dea802c5ac..c0939427ed80 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/BUILD.bazel +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/BUILD.bazel @@ -1,6 +1,6 @@ load("//devtools/tools:defaults.bzl", "ts_project") -package(default_visibility = ["//visibility:public"]) +package(default_visibility = ["//devtools:__subpackages__"]) ts_project( name = "profiler", @@ -11,8 +11,9 @@ ts_project( deps = [ "//:node_modules/@angular/core", "//:node_modules/rxjs", + "//devtools/projects/ng-devtools-backend/src/lib:interfaces", "//devtools/projects/ng-devtools-backend/src/lib/directive-forest", - "//devtools/projects/ng-devtools-backend/src/lib/hooks:identity_tracker", + "//devtools/projects/ng-devtools-backend/src/lib/directive-forest:identity-tracker", "//devtools/projects/ng-devtools-backend/src/lib/ng-debug-api", "//devtools/projects/ng-devtools-backend/src/lib/utils", "//devtools/projects/protocol", diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/index.ts similarity index 56% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts rename to devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/index.ts index e10c03682d00..8f5cda5b9626 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/index.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/index.ts @@ -14,13 +14,35 @@ import {Profiler} from './shared'; export {type Hooks, Profiler} from './shared'; +// Global reference. +let profiler: Profiler; + /** * Factory method for creating profiler object. * Gives priority to NgProfiler, falls back on PatchingProfiler if framework APIs are not present. */ -export const selectProfilerStrategy = (): Profiler => { +const selectProfilerStrategy = (): Profiler => { if (typeof ngDebugClient().ɵsetProfiler === 'function') { return new NgProfiler(); } return new PatchingProfiler(); }; + +/** + * Get the Profiler reference. + * Initializes the Profiler if it wasn't requested before. + */ +export function getProfiler( + depsForTestOnly: { + profiler?: new (...args: any[]) => Profiler; + } = {}, +): Profiler { + // Allow for overriding the Profiler implementation for testing purposes. + if (depsForTestOnly.profiler) { + profiler = new depsForTestOnly.profiler(); + } + if (!profiler) { + profiler = selectProfilerStrategy(); + } + return profiler; +} diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/native.ts similarity index 71% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts rename to devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/native.ts index 6c415a0599ac..d0a80c2f7842 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/native.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/native.ts @@ -10,9 +10,10 @@ import {ɵProfilerEvent} from '@angular/core'; import {getDirectiveHostElement} from '../../directive-forest'; import {ngDebugClient} from '../../ng-debug-api/ng-debug-api'; import {runOutsideAngular} from '../../utils/general'; -import {IdentityTracker, NodeArray} from '../identity-tracker'; +import {IdentityTracker, NodeArray} from '../../directive-forest/identity-tracker'; import {getLifeCycleName, Hooks, Profiler} from './shared'; +import {DirectiveInstance} from '../../interfaces'; type ProfilerCallback = (event: ɵProfilerEvent, instanceOrLView: {} | null, eventFn: any) => void; @@ -67,107 +68,107 @@ export class NgProfiler extends Profiler { }); } - [ɵProfilerEvent.BootstrapApplicationStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.BootstrapApplicationStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.BootstrapApplicationEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.BootstrapApplicationEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.BootstrapComponentStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.BootstrapComponentStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.BootstrapComponentEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.BootstrapComponentEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.ChangeDetectionStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.ChangeDetectionStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.ChangeDetectionEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.ChangeDetectionEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.ChangeDetectionSyncStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.ChangeDetectionSyncStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.ChangeDetectionSyncEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.ChangeDetectionSyncEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.AfterRenderHooksStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.AfterRenderHooksStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.AfterRenderHooksEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.AfterRenderHooksEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.ComponentStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.ComponentStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.ComponentEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.ComponentEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.DeferBlockStateStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.DeferBlockStateStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.DeferBlockStateEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.DeferBlockStateEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.DynamicComponentStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.DynamicComponentStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.DynamicComponentEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.DynamicComponentEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.HostBindingsUpdateStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.HostBindingsUpdateStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.HostBindingsUpdateEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.HostBindingsUpdateEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.TemplateCreateStart](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.TemplateCreateStart](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.TemplateCreateEnd](_directive: any, _eventFn: any): void { + [ɵProfilerEvent.TemplateCreateEnd](_directive: DirectiveInstance, _eventFn: any): void { // todo: implement return; } - [ɵProfilerEvent.TemplateUpdateStart](context: any, _eventFn: any): void { + [ɵProfilerEvent.TemplateUpdateStart](context: DirectiveInstance, _eventFn: any): void { if (!this._inChangeDetection) { this._inChangeDetection = true; runOutsideAngular(() => { @@ -218,7 +219,7 @@ export class NgProfiler extends Profiler { ); } - [ɵProfilerEvent.LifecycleHookStart](directive: any, hook: any): void { + [ɵProfilerEvent.LifecycleHookStart](directive: DirectiveInstance, hook: any): void { const id = this._tracker.getDirectiveId(directive); const element = getDirectiveHostElement(directive); const lifecycleHookName = getLifeCycleName(directive, hook); @@ -227,7 +228,7 @@ export class NgProfiler extends Profiler { this._onLifecycleHookStart(directive, lifecycleHookName, element, id, isComponent); } - [ɵProfilerEvent.LifecycleHookEnd](directive: any, hook: any): void { + [ɵProfilerEvent.LifecycleHookEnd](directive: DirectiveInstance, hook: any): void { const id = this._tracker.getDirectiveId(directive); const element = getDirectiveHostElement(directive); const lifecycleHookName = getLifeCycleName(directive, hook); @@ -236,14 +237,17 @@ export class NgProfiler extends Profiler { this._onLifecycleHookEnd(directive, lifecycleHookName, element, id, isComponent); } - [ɵProfilerEvent.OutputStart](componentOrDirective: any, listener: () => void): void { + [ɵProfilerEvent.OutputStart]( + componentOrDirective: DirectiveInstance, + listener: () => void, + ): void { const isComponent = !!this._tracker.isComponent.get(componentOrDirective); const node = getDirectiveHostElement(componentOrDirective); const id = this._tracker.getDirectiveId(componentOrDirective); this._onOutputStart(componentOrDirective, listener.name, node, id, isComponent); } - [ɵProfilerEvent.OutputEnd](componentOrDirective: any, listener: () => void): void { + [ɵProfilerEvent.OutputEnd](componentOrDirective: DirectiveInstance, listener: () => void): void { const isComponent = !!this._tracker.isComponent.get(componentOrDirective); const node = getDirectiveHostElement(componentOrDirective); const id = this._tracker.getDirectiveId(componentOrDirective); diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/polyfill.ts similarity index 85% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts rename to devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/polyfill.ts index 837dabe6718c..c0055c4f6564 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/polyfill.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/polyfill.ts @@ -12,9 +12,9 @@ import { METADATA_PROPERTY_NAME, } from '../../directive-forest'; import {runOutsideAngular} from '../../utils/general'; -import {IdentityTracker, NodeArray} from '../identity-tracker'; - +import {IdentityTracker, NodeArray} from '../../directive-forest/identity-tracker'; import {getLifeCycleName, Profiler} from './shared'; +import {ComponentInstance, DirectiveInstance} from '../../interfaces'; const hookTViewProperties = [ 'preOrderHooks', @@ -27,14 +27,14 @@ const hookTViewProperties = [ ]; // Only used in older Angular versions prior to the introduction of `getDirectiveMetadata` -const componentMetadata = (instance: any) => instance?.constructor?.ɵcmp; +const componentMetadata = (instance: ComponentInstance) => instance?.constructor?.ɵcmp; /** * Implementation of Profiler that uses monkey patching of directive templates and lifecycle * methods to fire profiler hooks. */ export class PatchingProfiler extends Profiler { - private _patched = new Map void>(); + private _patched = new Map void>(); private _undoLifecyclePatch: (() => void)[] = []; private _tracker = IdentityTracker.getInstance(); @@ -47,7 +47,7 @@ export class PatchingProfiler extends Profiler { meta.tView.template = template; } - this._patched = new Map void>(); + this._patched = new Map void>(); this._undoLifecyclePatch.forEach((p) => p()); this._undoLifecyclePatch = []; } @@ -64,19 +64,19 @@ export class PatchingProfiler extends Profiler { }); } - private _fireCreationCallback(component: any, isComponent: boolean): void { + private _fireCreationCallback(component: ComponentInstance, isComponent: boolean): void { const position = this._tracker.getDirectivePosition(component); const id = this._tracker.getDirectiveId(component); this._onCreate(component, getDirectiveHostElement(component), id, isComponent, position); } - private _fireDestroyCallback(component: any, isComponent: boolean): void { + private _fireDestroyCallback(component: ComponentInstance, isComponent: boolean): void { const position = this._tracker.getDirectivePosition(component); const id = this._tracker.getDirectiveId(component); this._onDestroy(component, getDirectiveHostElement(component), id, isComponent, position); } - private _observeComponent(cmp: any): void { + private _observeComponent(cmp: ComponentInstance): void { const declarations = componentMetadata(cmp); if (!declarations) { return; @@ -86,7 +86,7 @@ export class PatchingProfiler extends Profiler { if (original.patched) { return; } - declarations.tView.template = function (_: any, component: any): void { + declarations.tView.template = function (_: any, component: ComponentInstance): void { if (!self._inChangeDetection) { self._inChangeDetection = true; runOutsideAngular(() => { @@ -109,7 +109,7 @@ export class PatchingProfiler extends Profiler { this._patched.set(cmp, original); } - private _observeLifecycle(directive: any, isComponent: boolean): void { + private _observeLifecycle(directive: DirectiveInstance, isComponent: boolean): void { const ctx = getLViewFromDirectiveOrElementInstance(directive); if (!ctx) { return; diff --git a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/shared.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/shared.ts similarity index 89% rename from devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/shared.ts rename to devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/shared.ts index f20f99473194..2bd0a33c7b6f 100644 --- a/devtools/projects/ng-devtools-backend/src/lib/hooks/profiler/shared.ts +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/profiler/shared.ts @@ -9,10 +9,11 @@ import {ElementPosition, LifecycleProfile} from '../../../../../protocol'; import {Subject} from 'rxjs'; -import {NodeArray} from '../identity-tracker'; +import {NodeArray} from '../../directive-forest/identity-tracker'; +import {ComponentInstance, DirectiveInstance} from '../../interfaces'; type CreationHook = ( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, node: Node, id: number, isComponent: boolean, @@ -20,7 +21,7 @@ type CreationHook = ( ) => void; type LifecycleStartHook = ( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, hook: keyof LifecycleProfile, node: Node, id: number, @@ -28,7 +29,7 @@ type LifecycleStartHook = ( ) => void; type LifecycleEndHook = ( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, hook: keyof LifecycleProfile, node: Node, id: number, @@ -36,21 +37,21 @@ type LifecycleEndHook = ( ) => void; type ChangeDetectionStartHook = ( - component: any, + component: ComponentInstance, node: Node, id: number, position: ElementPosition, ) => void; type ChangeDetectionEndHook = ( - component: any, + component: ComponentInstance, node: Node, id: number, position: ElementPosition, ) => void; type DestroyHook = ( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, node: Node, id: number, isComponent: boolean, @@ -58,14 +59,15 @@ type DestroyHook = ( ) => void; type OutputStartHook = ( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, outputName: string, node: Node, id: number | undefined, isComponent: boolean, ) => void; + type OutputEndHook = ( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, outputName: string, node: Node, id: number | undefined, @@ -113,7 +115,7 @@ export abstract class Profiler { /** @internal */ protected _onCreate( - _: any, + _: DirectiveInstance, hook: Node, id: number | undefined, node: boolean, @@ -127,7 +129,7 @@ export abstract class Profiler { /** @internal */ protected _onDestroy( - _: any, + _: DirectiveInstance, hook: Node, id: number | undefined, node: boolean, @@ -141,7 +143,7 @@ export abstract class Profiler { /** @internal */ protected _onChangeDetectionStart( - _: any, + _: ComponentInstance, hook: Node, id: number | undefined, position: ElementPosition | undefined, @@ -154,7 +156,7 @@ export abstract class Profiler { /** @internal */ protected _onChangeDetectionEnd( - _: any, + _: ComponentInstance, hook: Node, id: number | undefined, position: ElementPosition | undefined, @@ -167,7 +169,7 @@ export abstract class Profiler { /** @internal */ protected _onLifecycleHookStart( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, hook: keyof LifecycleProfile | 'unknown', node: Node, id: number | undefined, @@ -188,7 +190,7 @@ export abstract class Profiler { /** @internal */ protected _onLifecycleHookEnd( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, hook: keyof LifecycleProfile | 'unknown', node: Node, id: number | undefined, @@ -202,7 +204,7 @@ export abstract class Profiler { /** @internal */ protected _onOutputStart( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, hook: string, node: Node, id: number | undefined, @@ -216,7 +218,7 @@ export abstract class Profiler { /** @internal */ protected _onOutputEnd( - componentOrDirective: any, + componentOrDirective: DirectiveInstance, hook: string, node: Node, id: number | undefined, diff --git a/devtools/projects/ng-devtools-backend/src/lib/profiling/timing-api.ts b/devtools/projects/ng-devtools-backend/src/lib/profiling/timing-api.ts new file mode 100644 index 000000000000..672868f8b5fc --- /dev/null +++ b/devtools/projects/ng-devtools-backend/src/lib/profiling/timing-api.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {LifecycleProfile} from '../../../../protocol'; +import {getDirectiveName} from '../highlighter'; +import {getProfiler} from './profiler'; +import type {ComponentInstance, DirectiveInstance} from '../interfaces'; + +type Method = keyof LifecycleProfile | 'changeDetection' | string; + +// Timing API global flag. +let timingAPIFlag = false; + +export const enableTimingAPI = () => (timingAPIFlag = true); +export const disableTimingAPI = () => (timingAPIFlag = false); + +const timingAPIEnabled = () => timingAPIFlag; + +const markName = (s: string, method: Method) => `🅰️ ${s}#${method}`; + +const supportsPerformance = + globalThis.performance && typeof globalThis.performance.getEntriesByName === 'function'; + +const recordMark = (s: string, method: Method) => { + if (supportsPerformance) { + // tslint:disable-next-line:ban + 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) { + // tslint:disable-next-line:ban + performance.mark(end); + + const measureOptions = { + start, + end, + detail: { + devtools: { + dataType: 'track-entry', + color: 'primary', + track: '🅰️ Angular DevTools', + }, + }, + }; + performance.measure(name, measureOptions); + } + performance.clearMarks(start); + performance.clearMarks(end); + performance.clearMeasures(name); + } +}; + +getProfiler().subscribe({ + onChangeDetectionStart(component: ComponentInstance): void { + if (!timingAPIEnabled()) { + return; + } + recordMark(getDirectiveName(component), 'changeDetection'); + }, + onChangeDetectionEnd(component: ComponentInstance): void { + if (!timingAPIEnabled()) { + return; + } + endMark(getDirectiveName(component), 'changeDetection'); + }, + onLifecycleHookStart(component: DirectiveInstance, lifecyle: keyof LifecycleProfile): void { + if (!timingAPIEnabled()) { + return; + } + recordMark(getDirectiveName(component), lifecyle); + }, + onLifecycleHookEnd(component: DirectiveInstance, lifecyle: keyof LifecycleProfile): void { + if (!timingAPIEnabled()) { + return; + } + endMark(getDirectiveName(component), lifecyle); + }, + onOutputStart(component: DirectiveInstance, output: string): void { + if (!timingAPIEnabled()) { + return; + } + recordMark(getDirectiveName(component), output); + }, + onOutputEnd(component: DirectiveInstance, output: string): void { + if (!timingAPIEnabled()) { + return; + } + endMark(getDirectiveName(component), output); + }, +});