refactor(devtools): use signal apis in visualizer and devtools-tabs (#57192)

Refactor the visualizer and devtools-tabs components to use signal apis, in future we can make the components onPush and zoneless

PR Close #57192
This commit is contained in:
Sheik Althaf
2024-07-30 12:55:46 +05:30
committed by Jessica Janiuk
parent e3919e7b9d
commit 4fa25cf328
15 changed files with 229 additions and 241 deletions
@@ -1,7 +1,7 @@
<nav class="devtools-nav" #navBar mat-tab-nav-bar [color]="'accent'" [tabPanel]="tabPanel">
<nav class="devtools-nav" #navBar mat-tab-nav-bar mat-stretch-tabs="false" [disablePagination]="true" [color]="'accent'" [tabPanel]="tabPanel">
<div id="nav-buttons">
<button (click)="toggleInspector()" matTooltip="Inspect element">
<mat-icon [class.inspector-active]="inspectorRunning"> pin_end </mat-icon>
<mat-icon [class.inspector-active]="inspectorRunning()"> pin_end </mat-icon>
</button>
<button [matMenuTriggerFor]="menu" matTooltip="Open settings">
<mat-icon> settings </mat-icon>
@@ -25,8 +25,8 @@
}
</select>
@for (tab of tabs; track $index) {
<a class="mat-tab-link" mat-tab-link (click)="changeTab(tab)" [active]="activeTab === tab">
@for (tab of tabs(); track $index) {
<a class="mat-tab-link" mat-tab-link (click)="changeTab(tab)" [active]="activeTab() === tab">
{{ tab }}
</a>
}
@@ -39,10 +39,10 @@
{{ angularVersion() }}
</span>
} @else {
<span
<span
id="version-number"
matTooltip="
Angular Devtools supports Angular versions 12 and above. Some DevTools features may be available in
Angular Devtools supports Angular versions 12 and above. Some DevTools features may be available in
older versions of Angular, but it is not officially supported.
"
class="unsupported-version"
@@ -51,7 +51,7 @@
</span>
}
| DevTools: {{ extensionVersion }}
| DevTools: {{ extensionVersion() }}
</section>
}
</nav>
@@ -60,35 +60,38 @@
@if (!applicationEnvironment.frameSelectorEnabled || frameManager.selectedFrame !== null) {
<div class="tab-content">
<ng-directive-explorer
[showCommentNodes]="showCommentNodes"
[isHydrationEnabled]="isHydrationEnabled"
[class.hidden]="activeTab !== 'Components'"
[showCommentNodes]="showCommentNodes()"
[isHydrationEnabled]="isHydrationEnabled()"
[class.hidden]="activeTab() !== 'Components'"
(toggleInspector)="toggleInspector()"
/>
<ng-profiler [class.hidden]="activeTab !== 'Profiler'"/>
<ng-router-tree [routes]="routes" [class.hidden]="activeTab !== 'Router Tree'"/>
<ng-injector-tree [class.hidden]="activeTab !== 'Injector Tree'"/>
<ng-profiler [class.hidden]="activeTab() !== 'Profiler'"/>
<ng-router-tree [routes]="routes()" [class.hidden]="activeTab() !== 'Router Tree'"/>
<ng-injector-tree [class.hidden]="activeTab() !== 'Injector Tree'"/>
</div>
}
</mat-tab-nav-panel>
<mat-menu #menu="matMenu">
@if (!profilingNotificationsSupported) {
<div mat-menu-item disableRipple (click)="$event.stopPropagation(); toggleTimingAPI()">
<mat-slide-toggle [checked]="timingAPIEnabled">
Enable timing API
<div (click)="$event.stopPropagation()">
@if (!profilingNotificationsSupported) {
<label mat-menu-item disableRipple>
<mat-slide-toggle [checked]="timingAPIEnabled()" (change)="toggleTimingAPI()">
Enable timing API
</mat-slide-toggle>
</label>
}
<label mat-menu-item disableRipple>
@let currentTheme = themeService.currentTheme();
<mat-slide-toggle [checked]="currentTheme === 'dark-theme'" (click)="themeService.toggleDarkMode(currentTheme === 'light-theme')">
Dark Mode
</mat-slide-toggle>
</div>
}
<div mat-menu-item disableRipple (click)="$event.stopPropagation(); themeService.toggleDarkMode(currentTheme === 'light-theme')">
<mat-slide-toggle [checked]="currentTheme === 'dark-theme'">
Dark Mode
</mat-slide-toggle>
</div>
<div mat-menu-item disableRipple (click)="$event.stopPropagation(); showCommentNodes = $event.checked">
<mat-slide-toggle [checked]="showCommentNodes">
Show comment nodes
</mat-slide-toggle>
</label>
<label mat-menu-item disableRipple>
<mat-slide-toggle [checked]="showCommentNodes()" (change)="showCommentNodes.set($event.checked)">
Show comment nodes
</mat-slide-toggle>
</label>
</div>
</mat-menu>
@@ -6,19 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
AfterViewInit,
Component,
computed,
EventEmitter,
inject,
input,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Component, computed, inject, input, output, signal} from '@angular/core';
import {MatIcon} from '@angular/material/icon';
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
import {MatSlideToggle} from '@angular/material/slide-toggle';
@@ -60,32 +48,31 @@ type Tabs = 'Components' | 'Profiler' | 'Router Tree' | 'Injector Tree';
],
providers: [TabUpdate],
})
export class DevToolsTabsComponent implements OnInit, AfterViewInit {
@Input() isHydrationEnabled = false;
export class DevToolsTabsComponent {
readonly isHydrationEnabled = input(false);
readonly frameSelected = output<Frame>();
@Output() frameSelected = new EventEmitter<Frame>();
@ViewChild(DirectiveExplorerComponent) directiveExplorer!: DirectiveExplorerComponent;
@ViewChild('navBar', {static: true}) navbar!: MatTabNav;
readonly applicationEnvironment = inject(ApplicationEnvironment);
readonly activeTab = signal<Tabs>('Components');
readonly inspectorRunning = signal(false);
readonly showCommentNodes = signal(false);
readonly timingAPIEnabled = signal(false);
readonly routes = signal<Route[]>([]);
readonly frameManager = inject(FrameManager);
readonly tabs = computed<Tabs[]>(() => {
const alwaysShown: Tabs[] = ['Components', 'Profiler', 'Injector Tree'];
return this.routes().length === 0 ? alwaysShown : [...alwaysShown, 'Router Tree'];
});
applicationEnvironment = inject(ApplicationEnvironment);
activeTab: Tabs = 'Components';
inspectorRunning = false;
routerTreeEnabled = false;
showCommentNodes = false;
timingAPIEnabled = false;
profilingNotificationsSupported = Boolean(
(window.chrome?.devtools as any)?.performance?.onProfilingStarted,
);
currentTheme!: Theme;
routes: Route[] = [];
frameManager = inject(FrameManager);
TOP_LEVEL_FRAME_ID = TOP_LEVEL_FRAME_ID;
angularVersion = input<string | undefined>(undefined);
majorAngularVersion = computed(() => {
readonly angularVersion = input<string | undefined>(undefined);
readonly majorAngularVersion = computed(() => {
const version = this.angularVersion();
if (!version) {
return -1;
@@ -93,46 +80,29 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
return parseInt(version.toString().split('.')[0], 10);
});
extensionVersion = 'Development Build';
readonly extensionVersion = signal('Development Build');
constructor(
public tabUpdate: TabUpdate,
public themeService: ThemeService,
private _messageBus: MessageBus<Events>,
) {
this.themeService.currentTheme
.pipe(takeUntilDestroyed())
.subscribe((theme) => (this.currentTheme = theme));
public tabUpdate = inject(TabUpdate);
public themeService = inject(ThemeService);
private _messageBus = inject<MessageBus<Events>>(MessageBus);
constructor() {
this._messageBus.on('updateRouterTree', (routes) => {
this.routes = routes || [];
this.routes.set(routes || []);
});
if (typeof chrome !== 'undefined' && chrome.runtime !== undefined) {
this.extensionVersion.set(chrome.runtime.getManifest().version);
}
}
emitSelectedFrame(frameId: string): void {
const frame = this.frameManager.frames.find((frame) => frame.id === parseInt(frameId, 10));
this.frameSelected.emit(frame);
}
ngOnInit(): void {
this.navbar.stretchTabs = false;
if (chrome !== undefined && chrome.runtime !== undefined) {
this.extensionVersion = chrome.runtime.getManifest().version;
}
}
get tabs(): Tabs[] {
const alwaysShown: Tabs[] = ['Components', 'Profiler', 'Injector Tree'];
return this.routes.length === 0 ? alwaysShown : [...alwaysShown, 'Router Tree'];
}
ngAfterViewInit(): void {
this.navbar.disablePagination = true;
this.frameSelected.emit(frame!);
}
changeTab(tab: Tabs): void {
this.activeTab = tab;
this.activeTab.set(tab);
this.tabUpdate.notify();
if (tab === 'Router Tree') {
this._messageBus.emit('getRoutes');
@@ -145,7 +115,7 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
}
emitInspectorEvent(): void {
if (this.inspectorRunning) {
if (this.inspectorRunning()) {
this._messageBus.emit('inspectorStart');
} else {
this._messageBus.emit('inspectorEnd');
@@ -154,12 +124,12 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
}
toggleInspectorState(): void {
this.inspectorRunning = !this.inspectorRunning;
this.inspectorRunning.update((state) => !state);
}
toggleTimingAPI(): void {
this.timingAPIEnabled = !this.timingAPIEnabled;
this.timingAPIEnabled
this.timingAPIEnabled.update((state) => !state);
this.timingAPIEnabled()
? this._messageBus.emit('enableTimingAPI')
: this._messageBus.emit('disableTimingAPI');
}
@@ -61,11 +61,11 @@ describe('DevtoolsTabsComponent', () => {
});
it('toggles inspector flag', () => {
expect(comp.inspectorRunning).toBe(false);
expect(comp.inspectorRunning()).toBe(false);
comp.toggleInspectorState();
expect(comp.inspectorRunning).toBe(true);
expect(comp.inspectorRunning()).toBe(true);
comp.toggleInspectorState();
expect(comp.inspectorRunning).toBe(false);
expect(comp.inspectorRunning()).toBe(false);
});
it('emits inspector event', () => {
@@ -1,10 +1,10 @@
<div class="wrapper" @stagger>
@for (bar of internalData; track $index) {
@for (bar of internalData(); track $index) {
<div
@appear
(click)="barClick.emit(originalData[$index])"
(click)="barClick.emit(data()[$index])"
class="bar"
[style.backgroundColor]="color"
[style.backgroundColor]="color()"
[style.width.%]="bar.width"
[matTooltip]="bar.text">
<span>{{ bar.text }}</span>
@@ -15,7 +15,7 @@ import {
transition,
trigger,
} from '@angular/animations';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {ChangeDetectionStrategy, Component, computed, input, output} from '@angular/core';
import {BargraphNode} from '../record-formatter/bargraph-formatter/bargraph-formatter';
import {MatTooltip} from '@angular/material/tooltip';
@@ -41,15 +41,17 @@ interface BarData {
],
standalone: true,
imports: [MatTooltip],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BarChartComponent {
@Input()
set data(nodes: BargraphNode[]) {
this.originalData = nodes;
this.internalData = [];
readonly data = input<BargraphNode[]>([]);
readonly internalData = computed(() => {
const nodes = this.data() ?? [];
const values: BarData[] = [];
const max = nodes.reduce((a: number, c) => Math.max(a, c.value), -Infinity);
for (const node of nodes) {
this.internalData.push({
values.push({
label: node.label,
count: node.count ?? 1,
width: (node.value / max) * 100,
@@ -57,12 +59,11 @@ export class BarChartComponent {
text: createBarText(node),
});
}
}
@Input({required: true}) color!: string;
@Output() barClick = new EventEmitter<BargraphNode>();
return values;
});
originalData!: BargraphNode[];
internalData: BarData[] = [];
readonly color = input.required<string>();
readonly barClick = output<BargraphNode>();
}
export function createBarText(bar: BargraphNode) {
@@ -1,3 +1,3 @@
<div class="level-profile-wrapper">
<ng-bar-chart (barClick)="selectNode($event)" [color]="barColor" [data]="profileRecords"> </ng-bar-chart>
<ng-bar-chart (barClick)="selectNode($event)" [color]="barColor()" [data]="profileRecords()"> </ng-bar-chart>
</div>
@@ -6,11 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, EventEmitter, Input, OnDestroy, Output} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ChangeDetectionStrategy, Component, computed, inject, input, output} from '@angular/core';
import {ProfilerFrame} from 'protocol';
import {Theme, ThemeService} from '../../../../theme-service';
import {ThemeService} from '../../../../theme-service';
import {BarGraphFormatter, BargraphNode} from '../record-formatter/bargraph-formatter/index';
import {formatDirectiveProfile} from './profile-formatter';
@@ -23,25 +22,20 @@ import {BarChartComponent} from './bar-chart.component';
styleUrls: ['./bargraph-visualizer.component.scss'],
standalone: true,
imports: [BarChartComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BargraphVisualizerComponent {
barColor!: string;
profileRecords!: BargraphNode[];
public themeService = inject(ThemeService);
readonly barColor = computed(() => {
return this.themeService.currentTheme() === 'dark-theme' ? '#073d69' : '#cfe8fc';
});
@Output() nodeSelect = new EventEmitter<SelectedEntry>();
readonly nodeSelect = output<SelectedEntry>();
private _formatter = new BarGraphFormatter();
private readonly _formatter = new BarGraphFormatter();
frame = input.required<ProfilerFrame>();
@Input()
set frame(data: ProfilerFrame) {
this.profileRecords = this._formatter.formatFrame(data);
}
constructor(public themeService: ThemeService) {
this.themeService.currentTheme.pipe(takeUntilDestroyed()).subscribe((theme) => {
this.barColor = theme === 'dark-theme' ? '#073d69' : '#cfe8fc';
});
}
profileRecords = computed(() => this._formatter.formatFrame(this.frame()));
formatEntryData(bargraphNode: BargraphNode): SelectedDirective[] {
return formatDirectiveProfile(bargraphNode.directives ?? []);
@@ -5,7 +5,7 @@
<th class="value">Time</th>
</thead>
<tbody>
@for (entry of data; track $index) {
@for (entry of data(); track $index) {
<tr>
<td class="name">
{{ entry.directive }}
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Input} from '@angular/core';
import {ChangeDetectionStrategy, Component, input} from '@angular/core';
import {SelectedDirective} from './timeline-visualizer.component';
@@ -15,7 +15,8 @@ import {SelectedDirective} from './timeline-visualizer.component';
templateUrl: './execution-details.component.html',
styleUrls: ['./execution-details.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExecutionDetailsComponent {
@Input({required: true}) data!: SelectedDirective[];
readonly data = input.required<SelectedDirective[]>();
}
@@ -1,7 +1,7 @@
<div class="level-profile-wrapper">
<ngx-flamegraph
(frameClick)="selectFrame($event)"
[config]="{ data: profilerBars, color: colors }"
[config]="{ data: profilerBars(), color: colors() }"
siblingLayout="equal"
>
</ngx-flamegraph>
@@ -6,12 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
import {ChangeDetectionStrategy, Component, computed, inject, input, output} from '@angular/core';
import {Color, RawData} from 'ngx-flamegraph/lib/utils';
import {ProfilerFrame} from 'protocol';
import {Subscription} from 'rxjs';
import {Theme, ThemeService} from '../../../../theme-service';
import {ThemeService} from '../../../../theme-service';
import {
FlamegraphFormatter,
FlamegraphNode,
@@ -28,56 +27,41 @@ import {NgxFlamegraphModule} from 'ngx-flamegraph';
styleUrls: ['./flamegraph-visualizer.component.scss'],
standalone: true,
imports: [NgxFlamegraphModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FlamegraphVisualizerComponent implements OnInit, OnDestroy {
profilerBars: FlamegraphNode[] = [];
export class FlamegraphVisualizerComponent {
public themeService = inject(ThemeService);
readonly profilerBars = computed(() => {
return [
this._formatter.formatFrame(
this.frame(),
this.changeDetection(),
this.themeService.currentTheme(),
),
];
});
view: [number, number] = [235, 200];
colors!: Color;
private _formatter = new FlamegraphFormatter();
private _showChangeDetection = false;
private _frame!: ProfilerFrame;
private _currentThemeSubscription!: Subscription;
currentTheme!: Theme;
private readonly _formatter = new FlamegraphFormatter();
readonly colors = computed<Color>(() => {
return this.themeService.currentTheme() === 'dark-theme'
? {
hue: [210, 90],
saturation: [90, 90],
lightness: [25, 25],
}
: {
hue: [50, 15],
saturation: [100, 100],
lightness: [75, 75],
};
});
@Output() nodeSelect = new EventEmitter<SelectedEntry>();
readonly nodeSelect = output<SelectedEntry>();
@Input()
set frame(frame: ProfilerFrame) {
this._frame = frame;
this._selectFrame();
}
readonly frame = input.required<ProfilerFrame>();
@Input()
set changeDetection(changeDetection: boolean) {
this._showChangeDetection = changeDetection;
this._selectFrame();
}
constructor(public themeService: ThemeService) {}
ngOnInit(): void {
this._currentThemeSubscription = this.themeService.currentTheme.subscribe((theme) => {
this.currentTheme = theme;
this.colors =
theme === 'dark-theme'
? {
hue: [210, 90],
saturation: [90, 90],
lightness: [25, 25],
}
: {
hue: [50, 15],
saturation: [100, 100],
lightness: [75, 75],
};
this._selectFrame();
});
}
ngOnDestroy(): void {
this._currentThemeSubscription.unsubscribe();
}
readonly changeDetection = input.required<boolean>();
selectFrame(frame: RawData): void {
if (frame.label === ROOT_LEVEL_ELEMENT_LABEL) {
@@ -96,10 +80,4 @@ export class FlamegraphVisualizerComponent implements OnInit, OnDestroy {
formatEntryData(flameGraphNode: FlamegraphNode): SelectedDirective[] {
return formatDirectiveProfile(flameGraphNode.original.directives);
}
private _selectFrame(): void {
this.profilerBars = [
this._formatter.formatFrame(this._frame, this._showChangeDetection, this.currentTheme),
];
}
}
@@ -1,37 +1,37 @@
<as-split unit="percent" [gutterSize]="9">
<as-split-area size="75">
@switch (_visualizationMode) {
@switch (visualizationMode()) {
@case (cmpVisualizationModes.FlameGraph) {
<ng-flamegraph-visualizer
[frame]="frame"
[changeDetection]="changeDetection"
[frame]="frame()"
[changeDetection]="changeDetection()"
(nodeSelect)="handleNodeSelect($event)"
/>
}
@case (cmpVisualizationModes.TreeMap) {
<ng-tree-map-visualizer [frame]="frame"/>
<ng-tree-map-visualizer [frame]="frame()"/>
}
@case (cmpVisualizationModes.BarGraph) {
<ng-bargraph-visualizer [frame]="frame" (nodeSelect)="handleNodeSelect($event)"/>
<ng-bargraph-visualizer [frame]="frame()" (nodeSelect)="handleNodeSelect($event)"/>
}
}
</as-split-area>
@if (selectedEntry) {
@if (selectedEntry(); as entry) {
<as-split-area size="25" minSize="15">
<mat-card class="selected-entry">
<mat-toolbar>{{ selectedEntry.label }} details</mat-toolbar>
<mat-toolbar>{{ entry.label }} details</mat-toolbar>
<section class="entry-statistics">
<div class="txt-total-time">
<label>Total time spent:</label><span> {{ selectedEntry.value | number }} ms</span>
<label>Total time spent:</label><span> {{ entry.value | number }} ms</span>
</div>
@if (selectedEntry.value > 0) {
<ng-execution-details [data]="selectedDirectives"/>
@if (entry.value > 0) {
<ng-execution-details [data]="selectedDirectives()"/>
}
@if (parentHierarchy.length > 0) {
@if (parentHierarchy().length > 0) {
<div>
<div class="txt-total-time"><label>Parent Hierarchy</label></div>
<ul>
@for (parent of parentHierarchy; track $index) {
@for (parent of parentHierarchy(); track $index) {
<li>{{ parent.name }}</li>
}
</ul>
@@ -6,7 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Input} from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
input,
signal,
untracked,
} from '@angular/core';
import {ProfilerFrame} from 'protocol';
import {BargraphNode} from '../record-formatter/bargraph-formatter';
@@ -34,6 +42,11 @@ export interface SelectedDirective {
value: number;
}
interface SelectedNode {
visualizationMode: VisualizationMode;
node: SelectedEntry;
}
@Component({
selector: 'ng-timeline-visualizer',
templateUrl: './timeline-visualizer.component.html',
@@ -50,30 +63,42 @@ export interface SelectedDirective {
ExecutionDetailsComponent,
DecimalPipe,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TimelineVisualizerComponent {
@Input()
set visualizationMode(mode: VisualizationMode) {
this._visualizationMode = mode;
this.selectedEntry = null;
this.selectedDirectives = [];
this.parentHierarchy = [];
}
@Input({required: true}) frame!: ProfilerFrame;
@Input({required: true}) changeDetection!: boolean;
readonly visualizationMode = input.required<VisualizationMode>();
readonly frame = input.required<ProfilerFrame>();
readonly changeDetection = input.required<boolean>();
cmpVisualizationModes = VisualizationMode;
private readonly selectedNodeWithVizMode = signal<SelectedNode | null>(null);
private readonly selectedNode = computed(() => {
const nodeWithMode = this.selectedNodeWithVizMode();
selectedEntry: BargraphNode | FlamegraphNode | null = null;
selectedDirectives: SelectedDirective[] = [];
parentHierarchy: {name: string}[] = [];
if (this.visualizationMode() !== nodeWithMode?.visualizationMode) return null;
/** @internal */
_visualizationMode!: VisualizationMode;
return nodeWithMode.node;
});
readonly selectedEntry = computed(() => this.selectedNode()?.entry ?? null);
readonly selectedDirectives = computed(() => this.selectedNode()?.selectedDirectives ?? []);
readonly parentHierarchy = computed(() => this.selectedNode()?.parentHierarchy ?? []);
handleNodeSelect({entry, parentHierarchy, selectedDirectives}: SelectedEntry): void {
this.selectedEntry = entry;
this.selectedDirectives = selectedDirectives;
this.parentHierarchy = parentHierarchy ?? [];
constructor() {
effect(
() => {
const nodeWithMode = untracked(this.selectedNodeWithVizMode);
if (nodeWithMode?.visualizationMode !== this.visualizationMode()) {
this.selectedNodeWithVizMode.set(null);
}
},
{allowSignalWrites: true},
);
}
handleNodeSelect(selected: SelectedEntry): void {
this.selectedNodeWithVizMode.set({
visualizationMode: this.visualizationMode(),
node: selected,
});
}
}
@@ -6,7 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {
afterNextRender,
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
inject,
input,
NgZone,
OnDestroy,
viewChild,
} from '@angular/core';
import {ProfilerFrame} from 'protocol';
import {Subject, Subscription} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
@@ -19,20 +31,14 @@ import {TreeMapFormatter, TreeMapNode} from '../record-formatter/tree-map-format
templateUrl: './tree-map-visualizer.component.html',
styleUrls: ['./tree-map-visualizer.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeMapVisualizerComponent implements OnInit, OnDestroy {
export class TreeMapVisualizerComponent implements OnDestroy {
private _formatter = new TreeMapFormatter();
@Input()
set frame(frame: ProfilerFrame) {
// first element in data is the Application node
this.treeMapRecords = this._formatter.formatFrame(frame);
if (this.tree) {
this._renderTree();
}
}
readonly frame = input.required<ProfilerFrame>();
constructor(private _ngZone: NgZone) {}
private _ngZone = inject(NgZone);
private resize$ = new Subject<void>();
private _throttledResizeSubscription!: Subscription;
@@ -40,20 +46,31 @@ export class TreeMapVisualizerComponent implements OnInit, OnDestroy {
private _resizeObserver: ResizeObserver = new ResizeObserver(() =>
this._ngZone.run(() => this.resize$.next()),
);
private treeMapRecords!: TreeMapNode;
private readonly treeMapRecords = computed<TreeMapNode>(() => {
// first element in data is the Application node
return this._formatter.formatFrame(this.frame());
});
@ViewChild('webTree', {static: true}) tree!: ElementRef<HTMLElement>;
readonly tree = viewChild.required<ElementRef<HTMLElement>>('webTree');
ngOnInit(): void {
this._throttledResizeSubscription = this.resize$
.pipe(debounceTime(100))
.subscribe(() => this._renderTree());
this._resizeObserver.observe(this.tree.nativeElement);
constructor() {
effect(() => {
if (this.tree()) this._renderTree();
});
afterNextRender({
read: () => {
this._throttledResizeSubscription = this.resize$
.pipe(debounceTime(100))
.subscribe(() => this._renderTree());
this._resizeObserver.observe(this.tree().nativeElement);
},
});
}
ngOnDestroy(): void {
this._throttledResizeSubscription.unsubscribe();
this._resizeObserver.unobserve(this.tree.nativeElement);
this._resizeObserver.unobserve(this.tree().nativeElement);
}
private _renderTree(): void {
@@ -62,11 +79,11 @@ export class TreeMapVisualizerComponent implements OnInit, OnDestroy {
}
private _removeTree(): void {
Array.from(this.tree.nativeElement.children).forEach((child) => child.remove());
Array.from(this.tree().nativeElement.children).forEach((child) => child.remove());
}
private _createTree(): void {
render(this.tree.nativeElement, this.treeMapRecords, {
render(this.tree().nativeElement, this.treeMapRecords(), {
padding: [20, 5, 5, 5],
caption: (node) => `${node.id}: ${node.size.toFixed(3)} ms`,
showNode: () => true,
@@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable, Renderer2, RendererFactory2} from '@angular/core';
import {ReplaySubject, Subject} from 'rxjs';
import {Injectable, Renderer2, RendererFactory2, signal} from '@angular/core';
export type Theme = 'dark-theme' | 'light-theme';
@@ -16,7 +15,7 @@ export type Theme = 'dark-theme' | 'light-theme';
})
export class ThemeService {
private renderer: Renderer2;
currentTheme: Subject<Theme> = new ReplaySubject();
readonly currentTheme = signal<Theme>('light-theme');
constructor(private _rendererFactory: RendererFactory2) {
this.renderer = this._rendererFactory.createRenderer(null, null);
@@ -28,7 +27,7 @@ export class ThemeService {
const addClass = !isDark ? 'light-theme' : 'dark-theme';
this.renderer.removeClass(document.body, removeClass);
this.renderer.addClass(document.body, addClass);
this.currentTheme.next(addClass);
this.currentTheme.set(addClass);
}
initializeThemeWatcher(): void {