diff --git a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/record-formatter/record-formatter.ts b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/record-formatter/record-formatter.ts index e5d14eab7b8..d211d00c420 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/record-formatter/record-formatter.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/record-formatter/record-formatter.ts @@ -10,6 +10,11 @@ export interface AppEntry { source: string; } +export interface GraphNode extends AppEntry { + toolTip: string; + style: any; +} + export abstract class RecordFormatter { abstract format(records: ProfilerFrame[]): TimelineView; abstract insertTimelineRecord(result: AppEntry[], record: ProfilerFrame): void; diff --git a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.css b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.css index dcdf385193c..48e6c1c5a3b 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.css +++ b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.css @@ -12,6 +12,11 @@ padding: 5px; } +.controls mat-form-field { + margin-left: 15px; + margin-right: 15px; +} + .controls label { font-weight: 500; } @@ -31,3 +36,42 @@ .warning-high { color: red; } + +.bar-graph-container { + padding: 2px; + background-color: #f3f3f3; + height: 54px; + display: flex; + align-items: flex-end; + border-top: 1px solid #d0d0d0; + border-bottom: 1px solid #d0d0d0; + margin-bottom: 4px; +} + +.bar-graph-container button { + margin-bottom: 5px; +} + +.bar-container { + max-width: calc(100vw - 150px); + display: flex; + align-items: baseline; + overflow-x: auto; +} + +.bar-container::-webkit-scrollbar { + display: none; +} + +.selected { + border: 1px solid #00000050; +} + +.txt-frames { + font-weight: 500; + line-height: 50px; + padding: 0px; + margin: 0px 10px; + width: 35px; + text-align: center; +} diff --git a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.html b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.html index a12126b04cd..e163286cf35 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.html @@ -14,7 +14,6 @@
- Select View Flamegraph @@ -25,18 +24,6 @@ - - -   - - - -
+
+

{{ currentView + 1 }}/{{ graphData.length }}

+ +
+
+
+ +
diff --git a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.spec.ts b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.spec.ts index 929748efcbf..da44c71a20c 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.spec.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.spec.ts @@ -2,9 +2,10 @@ import { TimelineComponent } from './timeline.component'; describe('TimelineComponent', () => { let comp: TimelineComponent; + const sanitizer = {}; beforeEach(() => { - comp = new TimelineComponent(); + comp = new TimelineComponent(sanitizer as any); }); it('should calculate the framerate from passed duration', () => { diff --git a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.ts index c9ceee3c789..0ea1c4d1540 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.component.ts @@ -1,9 +1,9 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { MatSlider, MatSliderChange } from '@angular/material/slider'; +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; import { ProfilerFrame } from 'protocol'; import { FlamegraphFormatter, FlamegraphNode } from './record-formatter/flamegraph-formatter'; -import { AppEntry, TimelineView } from './record-formatter/record-formatter'; +import { AppEntry, TimelineView, GraphNode } from './record-formatter/record-formatter'; import { WebtreegraphFormatter, WebtreegraphNode } from './record-formatter/webtreegraph-formatter'; +import { DomSanitizer } from '@angular/platform-browser'; export enum VisualizationMode { FlameGraph, @@ -15,6 +15,8 @@ const formatters = { [VisualizationMode.WebTreeGraph]: new WebtreegraphFormatter(), }; +const MAX_HEIGHT = 50; + @Component({ selector: 'ng-recording-timeline', templateUrl: './timeline.component.html', @@ -22,6 +24,8 @@ const formatters = { changeDetection: ChangeDetectionStrategy.OnPush, }) export class TimelineComponent { + @ViewChild('barContainer') barContainer: ElementRef; + @Input() set records(data: ProfilerFrame[]) { this.unFormattedRecords = data; this.formatRecords(); @@ -29,17 +33,18 @@ export class TimelineComponent { @Output() exportProfile = new EventEmitter(); - @ViewChild(MatSlider) slider: MatSlider; - @Input() unFormattedRecords: ProfilerFrame[]; profileRecords: TimelineView | TimelineView = { timeline: [], }; - currentView = 1; + currentView = 0; cmpVisualizationModes = VisualizationMode; visualizationMode = VisualizationMode.FlameGraph; + graphData: GraphNode[] = []; + + constructor(private sanitizer: DomSanitizer) {} get formatter(): FlamegraphFormatter | WebtreegraphFormatter { return formatters[this.visualizationMode]; @@ -54,23 +59,53 @@ export class TimelineComponent { return Math.floor(64 / 2 ** multiplier); } - updateView($event: MatSliderChange): void { - if ($event.value === undefined || $event.value > this.profileRecords.timeline.length) { - return; + move(value: number): void { + const newVal = this.currentView + value; + if (newVal > -1 && newVal < this.profileRecords.timeline.length) { + this.currentView = newVal; + this.barContainer.nativeElement.children[newVal].scrollIntoView({ + behavior: 'auto', + block: 'end', + inline: 'nearest', + }); } - this.currentView = $event.value; - this.slider.value = this.currentView; } - move(number: number): void { - const newVal = this.currentView + number; - if (newVal > 0 && newVal < this.profileRecords.timeline.length) { - this.currentView = newVal; - this.slider.value = this.currentView; - } + updateView(value: number): void { + this.currentView = value; } formatRecords(): void { this.profileRecords = this.formatter.format(this.unFormattedRecords); + this.renderBarChart(this.profileRecords.timeline); + } + + renderBarChart(timeline: AppEntry[]): void { + const maxValue = timeline.reduce( + (acc: number, node: AppEntry) => Math.max(acc, node.timeSpent), + 0 + ); + const multiplicationFactor = parseFloat((MAX_HEIGHT / maxValue).toFixed(2)); + this.graphData = timeline.map(d => { + const height = d.timeSpent * multiplicationFactor; + let backgroundColor = 'rgb(237, 213, 94)'; + if (height > 33) { + backgroundColor = 'rgb(240, 117, 117)'; + } else if (height > 16) { + backgroundColor = 'rgb(238, 189, 99)'; + } + + const style = { + 'margin-left': '1px', + 'margin-right': '1px', + 'background-color': backgroundColor, + cursor: 'pointer', + 'min-width': '25px', + width: '25px', + height: `${height}px`, + }; + const toolTip = `${d.source} TimeSpent: ${d.timeSpent.toFixed(3)}ms`; + return { ...d, style, toolTip }; + }); } } diff --git a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.module.ts b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.module.ts index dbc7957d1f8..377aa6cb812 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.module.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/profiler/recording/timeline/timeline.module.ts @@ -2,11 +2,12 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TimelineComponent } from './timeline.component'; import { RecordingVisualizerModule } from './recording-visualizer/recording-visualizer.module'; -import { MatSliderModule } from '@angular/material/slider'; import { MatButtonModule } from '@angular/material/button'; import { FormsModule } from '@angular/forms'; import { NgxFlamegraphModule } from 'ngx-flamegraph'; import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; @NgModule({ declarations: [TimelineComponent], @@ -14,8 +15,9 @@ import { MatSelectModule } from '@angular/material/select'; CommonModule, FormsModule, RecordingVisualizerModule, - MatSliderModule, MatButtonModule, + MatTooltipModule, + MatIconModule, NgxFlamegraphModule, MatSelectModule, ],