mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jessica Janiuk
parent
e3919e7b9d
commit
4fa25cf328
@@ -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', () => {
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+12
-11
@@ -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
-1
@@ -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>
|
||||
|
||||
+11
-17
@@ -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 ?? []);
|
||||
|
||||
+1
-1
@@ -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 }}
|
||||
|
||||
+3
-2
@@ -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
-1
@@ -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>
|
||||
|
||||
+31
-53
@@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -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>
|
||||
|
||||
+44
-19
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+38
-21
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user