feat(devtools): adding bar chart for timeline

This commit is contained in:
Sumit Arora
2020-03-19 09:32:22 -04:00
committed by Minko Gechev
parent 75590e6c07
commit 75cb030a84
6 changed files with 125 additions and 33 deletions
@@ -10,6 +10,11 @@ export interface AppEntry<T> {
source: string;
}
export interface GraphNode<T> extends AppEntry<T> {
toolTip: string;
style: any;
}
export abstract class RecordFormatter<T> {
abstract format(records: ProfilerFrame[]): TimelineView<T>;
abstract insertTimelineRecord(result: AppEntry<T>[], record: ProfilerFrame): void;
@@ -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;
}
@@ -14,7 +14,6 @@
<div class="controls">
<button mat-stroked-button (click)="exportProfile.emit()">Export to JSON</button>
<mat-form-field>
<mat-label>Select View</mat-label>
<mat-select (selectionChange)="formatRecords()" [(ngModel)]="visualizationMode">
<mat-option [value]="cmpVisualizationModes.FlameGraph">
Flamegraph
@@ -25,18 +24,6 @@
</mat-select>
</mat-form-field>
<ng-container *ngIf="showControls">
<mat-slider
[max]="profileRecords.timeline.length - 1"
[min]="1"
[(ngModel)]="currentView"
(change)="updateView($event)"
></mat-slider>
&nbsp;
<button mat-stroked-button (click)="move(-1)"><</button>
<button mat-stroked-button (click)="move(1)">></button>
</ng-container>
<label
[class.warning-no]="record.timeSpent < 16"
[class.warning-low]="record.timeSpent >= 16"
@@ -57,5 +44,23 @@
</label>
<label *ngIf="record.source">Source: {{ record.source }}</label>
</div>
<div class="bar-graph-container">
<p class="txt-frames">{{ currentView + 1 }}/{{ graphData.length }}</p>
<button mat-icon-button (click)="move(-1)" [disabled]="currentView == 0">
<mat-icon>chevron_left</mat-icon>
</button>
<div #barContainer class="bar-container">
<div
*ngFor="let d of graphData; let i = index"
[matTooltip]="d.toolTip"
[ngStyle]="d.style"
[class.selected]="i == currentView"
(click)="updateView(i)"
></div>
</div>
<button mat-icon-button (click)="move(1)" [disabled]="currentView == graphData.length - 1">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<ng-timeline-visualizer [visualizationMode]="visualizationMode" [records]="record.app"></ng-timeline-visualizer>
</ng-template>
@@ -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', () => {
@@ -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<void>();
@ViewChild(MatSlider) slider: MatSlider;
@Input() unFormattedRecords: ProfilerFrame[];
profileRecords: TimelineView<FlamegraphNode> | TimelineView<WebtreegraphNode> = {
timeline: [],
};
currentView = 1;
currentView = 0;
cmpVisualizationModes = VisualizationMode;
visualizationMode = VisualizationMode.FlameGraph;
graphData: GraphNode<FlamegraphNode | WebtreegraphNode>[] = [];
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<FlamegraphNode | WebtreegraphNode>[]): void {
const maxValue = timeline.reduce(
(acc: number, node: AppEntry<FlamegraphNode | WebtreegraphNode>) => 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 };
});
}
}
@@ -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,
],