mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(devtools): adding bar chart for timeline
This commit is contained in:
committed by
Minko Gechev
parent
75590e6c07
commit
75cb030a84
+5
@@ -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;
|
||||
|
||||
+44
@@ -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;
|
||||
}
|
||||
|
||||
+18
-13
@@ -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>
|
||||
|
||||
<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
-1
@@ -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', () => {
|
||||
|
||||
+52
-17
@@ -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 };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user