mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
feat(devtools): introduce streamed visualization of the profiling data
This commit is contained in:
parent
7df0c4d3e9
commit
ac2cd3757b
19 changed files with 256 additions and 138 deletions
|
|
@ -15,11 +15,8 @@
|
|||
<input type="file" (change)="importProfilerResults($event)" placeholder="Upload file" accept=".json" />
|
||||
</span>
|
||||
</p>
|
||||
<p class="recording" [class.hidden]="state !== 'recording'">
|
||||
<ng-recording-modal></ng-recording-modal>
|
||||
</p>
|
||||
<p class="visualization" *ngIf="state === 'visualizing'">
|
||||
<ng-recording-timeline [records]="stream" (exportProfile)="exportProfilerResults()"> </ng-recording-timeline>
|
||||
<p class="visualization" *ngIf="state !== 'idle'">
|
||||
<ng-recording-timeline [stream]="stream" (exportProfile)="exportProfilerResults()"> </ng-recording-timeline>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, ViewChildren, QueryList, OnInit, OnDestroy } from '@angular/core';
|
||||
import { RecordingModalComponent } from './recording/recording-modal/recording-modal.component';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { MessageBus, Events, ProfilerFrame } from 'protocol';
|
||||
import { FileApiService } from '../../file-api-service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ProfilerImportDialogComponent } from './profiler-import-dialog/profiler-import-dialog.component';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
type State = 'idle' | 'recording' | 'visualizing';
|
||||
|
||||
|
|
@ -17,10 +17,10 @@ const PROFILER_VERSION = 1;
|
|||
})
|
||||
export class ProfilerComponent implements OnInit, OnDestroy {
|
||||
state: State = 'idle';
|
||||
stream: ProfilerFrame[] = [];
|
||||
buffer: ProfilerFrame[] = [];
|
||||
stream = new Subject<ProfilerFrame[]>();
|
||||
|
||||
@ViewChildren(RecordingModalComponent) recordingRef: QueryList<RecordingModalComponent>;
|
||||
// We collect this buffer so we can have it available for export.
|
||||
private _buffer: ProfilerFrame[] = [];
|
||||
|
||||
constructor(
|
||||
private _fileApiService: FileApiService,
|
||||
|
|
@ -30,23 +30,26 @@ export class ProfilerComponent implements OnInit, OnDestroy {
|
|||
|
||||
startRecording(): void {
|
||||
this.state = 'recording';
|
||||
this.recordingRef.forEach((r) => r.start());
|
||||
this._messageBus.emit('startProfiling');
|
||||
}
|
||||
|
||||
stopRecording(): void {
|
||||
this.state = 'idle';
|
||||
this.recordingRef.forEach((r) => r.stop());
|
||||
this.state = 'visualizing';
|
||||
this._messageBus.emit('stopProfiling');
|
||||
this.stream.complete();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._messageBus.on('profilerResults', (remainingRecords) => {
|
||||
this._profilerFinished(remainingRecords);
|
||||
if (remainingRecords.duration > 0 && remainingRecords.source) {
|
||||
this.stream.next([remainingRecords]);
|
||||
this._buffer.push(remainingRecords);
|
||||
}
|
||||
});
|
||||
|
||||
this._messageBus.on('sendProfilerChunk', (chunkOfRecords: ProfilerFrame) => {
|
||||
this.buffer.push(chunkOfRecords);
|
||||
this.stream.next([chunkOfRecords]);
|
||||
this._buffer.push(chunkOfRecords);
|
||||
});
|
||||
|
||||
this._fileApiService.uploadedData.subscribe((importedFile) => {
|
||||
|
|
@ -69,34 +72,27 @@ export class ProfilerComponent implements OnInit, OnDestroy {
|
|||
|
||||
processDataDialog.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this._viewProfilerData(importedFile.stream);
|
||||
this.state = 'visualizing';
|
||||
this._buffer = importedFile.buffer;
|
||||
setTimeout(() => this.stream.next(importedFile.buffer));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._viewProfilerData(importedFile.stream);
|
||||
this.state = 'visualizing';
|
||||
this._buffer = importedFile.buffer;
|
||||
setTimeout(() => this.stream.next(importedFile.buffer));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _profilerFinished(remainingRecords: ProfilerFrame): void {
|
||||
const flattenedBuffer = [].concat.apply([], this.buffer);
|
||||
this._viewProfilerData([...flattenedBuffer, remainingRecords]);
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._fileApiService.uploadedData.unsubscribe();
|
||||
}
|
||||
|
||||
private _viewProfilerData(stream: ProfilerFrame[]): void {
|
||||
this.state = 'visualizing';
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
exportProfilerResults(): void {
|
||||
const fileToExport = {
|
||||
version: PROFILER_VERSION,
|
||||
stream: this.stream,
|
||||
buffer: this._buffer,
|
||||
};
|
||||
this._fileApiService.saveObjectAsJSON(fileToExport);
|
||||
}
|
||||
|
|
@ -106,7 +102,8 @@ export class ProfilerComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
discardRecording(): void {
|
||||
this.stream = [];
|
||||
this.stream = new Subject<ProfilerFrame[]>();
|
||||
this.state = 'idle';
|
||||
this._buffer = [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,17 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { ProfilerComponent } from './profiler.component';
|
||||
import { RecordingModalComponent } from './recording/recording-modal/recording-modal.component';
|
||||
import { TimelineModule } from './recording/timeline/timeline.module';
|
||||
import { RecordingDialogComponent } from './recording/recording-modal/recording-dialog/recording-dialog.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { ProfilerImportDialogComponent } from './profiler-import-dialog/profiler-import-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProfilerComponent, RecordingModalComponent, RecordingDialogComponent, ProfilerImportDialogComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
FormsModule,
|
||||
MatProgressBarModule,
|
||||
TimelineModule,
|
||||
MatButtonModule,
|
||||
],
|
||||
declarations: [ProfilerComponent, ProfilerImportDialogComponent],
|
||||
imports: [CommonModule, MatDialogModule, MatSelectModule, FormsModule, TimelineModule, MatButtonModule],
|
||||
exports: [ProfilerComponent],
|
||||
entryComponents: [ProfilerImportDialogComponent],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
<div class="bar-graph-container">
|
||||
<p class="txt-frames">{{ currentFrameIndex + 1 }}/{{ graphData.length }}</p>
|
||||
<button mat-icon-button (click)="move.emit(-1)" [disabled]="currentFrameIndex == 0">
|
||||
<p class="txt-frames">{{ currentFrameIndex + 1 }}/{{ (graphData$ | async)?.length }}</p>
|
||||
<button mat-icon-button (click)="move.emit(-1)" [disabled]="currentFrameIndex <= 0">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<div #barContainer class="bar-container">
|
||||
|
||||
<cdk-virtual-scroll-viewport #barContainer orientation="horizontal" [itemSize]="itemWidth" class="bar-container">
|
||||
<div
|
||||
*ngFor="let d of graphData; let i = index"
|
||||
[matTooltip]="d.toolTip"
|
||||
*cdkVirtualFor="let d of graphData$ | async; let i = index"
|
||||
[ngStyle]="d.style"
|
||||
[class.selected]="i === currentFrameIndex"
|
||||
(click)="selectFrame.emit(i)"
|
||||
></div>
|
||||
</div>
|
||||
<button mat-icon-button (click)="move.emit(1)" [disabled]="currentFrameIndex == graphData.length - 1">
|
||||
</cdk-virtual-scroll-viewport>
|
||||
|
||||
<button mat-icon-button (click)="move.emit(1)" [disabled]="currentFrameIndex >= (graphData$ | async)?.length - 1">
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@
|
|||
|
||||
.bar-container {
|
||||
max-width: calc(100vw - 150px);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
::ng-deep .cdk-virtual-scroll-content-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,67 @@
|
|||
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild, OnDestroy } from '@angular/core';
|
||||
import { GraphNode } from '../record-formatter/record-formatter';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
|
||||
const ITEM_WIDTH = 29;
|
||||
|
||||
@Component({
|
||||
selector: 'ng-frame-selector',
|
||||
templateUrl: './frame-selector.component.html',
|
||||
styleUrls: ['./frame-selector.component.scss'],
|
||||
})
|
||||
export class FrameSelectorComponent {
|
||||
export class FrameSelectorComponent implements OnDestroy {
|
||||
@ViewChild('barContainer') barContainer: ElementRef;
|
||||
@Input() set currentFrame(value: number) {
|
||||
this.currentFrameIndex = value;
|
||||
this.barContainer?.nativeElement?.children?.[value]?.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'end',
|
||||
inline: 'nearest',
|
||||
});
|
||||
this._ensureVisible(value);
|
||||
}
|
||||
@Input() graphData: GraphNode[];
|
||||
@Input() profilerFramesLength: number;
|
||||
@Input() set graphData$(graphData: Observable<GraphNode[]>) {
|
||||
this._graphData$ = graphData;
|
||||
this._graphDataSubscription = this._graphData$.subscribe((items) =>
|
||||
setTimeout(() => {
|
||||
this.viewPort.scrollToIndex(items.length);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get graphData$(): Observable<GraphNode[]> {
|
||||
return this._graphData$;
|
||||
}
|
||||
|
||||
@Output() move = new EventEmitter<number>();
|
||||
@Output() selectFrame = new EventEmitter<number>();
|
||||
@ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
|
||||
|
||||
currentFrameIndex: number;
|
||||
|
||||
get itemWidth(): number {
|
||||
return ITEM_WIDTH;
|
||||
}
|
||||
|
||||
private _graphData$: Observable<GraphNode[]>;
|
||||
private _graphDataSubscription: Subscription;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this._graphDataSubscription) {
|
||||
this._graphDataSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private _ensureVisible(index: number): void {
|
||||
if (!this.viewPort) {
|
||||
return;
|
||||
}
|
||||
const scrollParent = this.viewPort.elementRef.nativeElement;
|
||||
// The left most point we see an element
|
||||
const left = scrollParent.scrollLeft;
|
||||
// That's the right most point we currently see an element.
|
||||
const right = left + scrollParent.offsetWidth;
|
||||
const itemLeft = index * this.itemWidth;
|
||||
if (itemLeft < left) {
|
||||
scrollParent.scrollTo({ left: itemLeft });
|
||||
} else if (right < itemLeft + this.itemWidth) {
|
||||
scrollParent.scrollTo({ left: itemLeft - scrollParent.offsetWidth + this.itemWidth });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<section class="main-wrapper">
|
||||
<h2>Recording</h2>
|
||||
<h2>Interact with your app to preview change detection</h2>
|
||||
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</section>
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
<section *ngIf="visible" id="recorder-wrapper">
|
||||
<section id="recorder-wrapper">
|
||||
<ng-recording-dialog></ng-recording-dialog>
|
||||
</section>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../../../../../../../node_modules/@angular/cdk/overlay-prebuilt.css';
|
||||
@import '../../../../../../../../../node_modules/@angular/cdk/overlay-prebuilt.css';
|
||||
|
||||
:host {
|
||||
overflow: hidden;
|
||||
|
|
@ -5,14 +5,4 @@ import { Component } from '@angular/core';
|
|||
templateUrl: './recording-modal.component.html',
|
||||
styleUrls: ['./recording-modal.component.scss'],
|
||||
})
|
||||
export class RecordingModalComponent {
|
||||
visible = false;
|
||||
|
||||
stop(): void {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
start(): void {
|
||||
this.visible = true;
|
||||
}
|
||||
}
|
||||
export class RecordingModalComponent {}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="controls">
|
||||
<button mat-stroked-button (click)="exportProfile.emit()">Export to JSON</button>
|
||||
<mat-form-field>
|
||||
<button *ngIf="!empty" mat-stroked-button (click)="exportProfile.emit()">Export to JSON</button>
|
||||
<mat-form-field *ngIf="record">
|
||||
<mat-select [value]="visualizationMode" (selectionChange)="this.changeVisualizationMode.emit($event.value)">
|
||||
<mat-option [value]="flameGraphMode">
|
||||
Flame graph
|
||||
|
|
@ -14,12 +14,14 @@
|
|||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<label *ngIf="estimatedFrameRate >= 60"> Time spent: {{ record.duration | number }} ms </label>
|
||||
<label *ngIf="estimatedFrameRate >= 60 && record"> Time spent: {{ record?.duration | number }} ms </label>
|
||||
|
||||
<label [style.color]="frameColor" *ngIf="estimatedFrameRate < 60">
|
||||
Time spent: {{ record.duration | number }} ms
|
||||
<label [style.color]="frameColor" *ngIf="estimatedFrameRate < 60 && record">
|
||||
Time spent: {{ record?.duration | number }} ms
|
||||
</label>
|
||||
|
||||
<label [style.color]="frameColor" *ngIf="estimatedFrameRate < 60"> Frame rate: {{ estimatedFrameRate }} fps </label>
|
||||
<label *ngIf="record.source">Source: {{ record.source }}</label>
|
||||
<label [style.color]="frameColor" *ngIf="estimatedFrameRate < 60 && record">
|
||||
Frame rate: {{ estimatedFrameRate }} fps
|
||||
</label>
|
||||
<label *ngIf="record?.source && record">Source: {{ record?.source }}</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
:host {
|
||||
height: 60px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
top: 0;
|
||||
left: 300px;
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ import { ProfilerFrame } from 'protocol';
|
|||
styleUrls: ['./timeline-controls.component.scss'],
|
||||
})
|
||||
export class TimelineControlsComponent {
|
||||
@Input() record: ProfilerFrame;
|
||||
@Input() record: ProfilerFrame | undefined;
|
||||
@Input() estimatedFrameRate: number;
|
||||
@Input() frameColor: string;
|
||||
@Input() visualizationMode: VisualizationMode;
|
||||
@Input() empty: boolean;
|
||||
@Output() changeVisualizationMode = new EventEmitter<VisualizationMode>();
|
||||
@Output() exportProfile = new EventEmitter<void>();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,34 @@
|
|||
<ng-container *ngIf="profilerFrames.length > 0; else noRecords">
|
||||
<ng-container *ngTemplateOutlet="recordView; context: { record: frame }"></ng-container>
|
||||
</ng-container>
|
||||
<ng-recording-modal *ngIf="!hasFrames && !visualizing"></ng-recording-modal>
|
||||
|
||||
<ng-template #noRecords>
|
||||
Nothing was profiled
|
||||
</ng-template>
|
||||
<p class="info" *ngIf="!hasFrames && visualizing">
|
||||
There's no information to show.
|
||||
</p>
|
||||
|
||||
<ng-template let-record="record" #recordView>
|
||||
<ng-timeline-controls
|
||||
[frameColor]="getColorByFrameRate(estimateFrameRate(record.duration))"
|
||||
[record]="record"
|
||||
[estimatedFrameRate]="estimateFrameRate(record.duration)"
|
||||
[visualizationMode]="visualizationMode"
|
||||
(changeVisualizationMode)="visualizationMode = $event"
|
||||
(exportProfile)="exportProfile.emit($event)"
|
||||
></ng-timeline-controls>
|
||||
<ng-frame-selector
|
||||
(move)="move($event)"
|
||||
(selectFrame)="selectFrame($event)"
|
||||
[profilerFramesLength]="profilerFrames.length"
|
||||
[graphData]="graphData"
|
||||
[currentFrame]="currentFrameIndex"
|
||||
></ng-frame-selector>
|
||||
<ng-timeline-visualizer [visualizationMode]="visualizationMode" [frame]="record"></ng-timeline-visualizer>
|
||||
</ng-template>
|
||||
<ng-timeline-controls
|
||||
[class.hidden]="!hasFrames"
|
||||
[record]="frame"
|
||||
[empty]="!hasFrames"
|
||||
[frameColor]="getColorByFrameRate(estimateFrameRate(frame?.duration))"
|
||||
[estimatedFrameRate]="estimateFrameRate(frame?.duration)"
|
||||
[visualizationMode]="visualizationMode"
|
||||
(changeVisualizationMode)="visualizationMode = $event"
|
||||
(exportProfile)="exportProfile.emit($event)"
|
||||
></ng-timeline-controls>
|
||||
|
||||
<ng-frame-selector
|
||||
[class.hidden]="!hasFrames"
|
||||
(move)="move($event)"
|
||||
(selectFrame)="selectFrame($event)"
|
||||
[graphData$]="graphData$"
|
||||
[currentFrame]="currentFrameIndex"
|
||||
></ng-frame-selector>
|
||||
|
||||
<p class="info" *ngIf="hasFrames && currentFrameIndex < 0">
|
||||
Select a bar to preview a particular change detection cycle.
|
||||
</p>
|
||||
|
||||
<ng-timeline-visualizer
|
||||
*ngIf="hasFrames && currentFrameIndex >= 0"
|
||||
[visualizationMode]="visualizationMode"
|
||||
[frame]="frame"
|
||||
></ng-timeline-visualizer>
|
||||
|
|
|
|||
|
|
@ -4,3 +4,16 @@
|
|||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
/*
|
||||
intentionally using visibility: hidden
|
||||
display: none breaks the virtual scroll
|
||||
*/
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, Output, OnDestroy } from '@angular/core';
|
||||
import { ProfilerFrame } from 'protocol';
|
||||
import { GraphNode } from './record-formatter/record-formatter';
|
||||
import { Observable, Subscription, BehaviorSubject } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
|
||||
export enum VisualizationMode {
|
||||
FlameGraph,
|
||||
|
|
@ -14,22 +16,41 @@ const MAX_HEIGHT = 50;
|
|||
selector: 'ng-recording-timeline',
|
||||
templateUrl: './timeline.component.html',
|
||||
styleUrls: ['./timeline.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TimelineComponent {
|
||||
@Input() set records(data: ProfilerFrame[]) {
|
||||
this.profilerFrames = data.filter((frame) => frame.duration > 0);
|
||||
this.renderBarChart(this.profilerFrames);
|
||||
export class TimelineComponent implements OnDestroy {
|
||||
@Input() set stream(data: Observable<ProfilerFrame[]>) {
|
||||
if (this._subscription) {
|
||||
this._subscription.unsubscribe();
|
||||
}
|
||||
this._allRecords = [];
|
||||
this._maxDuration = -Infinity;
|
||||
this._subscription = data.subscribe({
|
||||
next: (frames: ProfilerFrame[]): void => {
|
||||
this._processFrames(frames);
|
||||
},
|
||||
complete: (): void => {
|
||||
this.visualizing = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
@Input() profilerFrames: ProfilerFrame[] = [];
|
||||
@Output() exportProfile = new EventEmitter<void>();
|
||||
|
||||
visualizationMode = VisualizationMode.BarGraph;
|
||||
graphData: GraphNode[] = [];
|
||||
currentFrameIndex = 0;
|
||||
currentFrameIndex = -1;
|
||||
|
||||
private _maxDuration = -Infinity;
|
||||
private _subscription: Subscription;
|
||||
private _allRecords: ProfilerFrame[] = [];
|
||||
private _graphDataSubject = new BehaviorSubject<GraphNode[]>([]);
|
||||
visualizing = false;
|
||||
graphData$ = this._graphDataSubject.asObservable().pipe(share());
|
||||
|
||||
get hasFrames(): boolean {
|
||||
return this._allRecords.length > 0;
|
||||
}
|
||||
|
||||
get frame(): ProfilerFrame {
|
||||
return this.profilerFrames[this.currentFrameIndex];
|
||||
return this._allRecords[this.currentFrameIndex];
|
||||
}
|
||||
|
||||
estimateFrameRate(timeSpent: number): number {
|
||||
|
|
@ -39,7 +60,7 @@ export class TimelineComponent {
|
|||
|
||||
move(value: number): void {
|
||||
const newVal = this.currentFrameIndex + value;
|
||||
if (newVal > -1 && newVal < this.profilerFrames.length) {
|
||||
if (newVal > -1 && newVal < this._allRecords.length) {
|
||||
this.currentFrameIndex = newVal;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,26 +80,59 @@ export class TimelineComponent {
|
|||
return 'red';
|
||||
}
|
||||
|
||||
renderBarChart(records: ProfilerFrame[]): void {
|
||||
const maxValue = records.reduce((acc: number, frame: ProfilerFrame) => Math.max(acc, frame.duration), 0);
|
||||
const multiplicationFactor = parseFloat((MAX_HEIGHT / maxValue).toFixed(2));
|
||||
this.graphData = records.map((r) => {
|
||||
const height = r.duration * multiplicationFactor;
|
||||
const colorPercentage = Math.round((height / MAX_HEIGHT) * 100);
|
||||
const backgroundColor = this.getColorByFrameRate(this.estimateFrameRate(r.duration));
|
||||
ngOnDestroy(): void {
|
||||
if (this._subscription) {
|
||||
this._subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
const style = {
|
||||
'margin-left': '1px',
|
||||
'margin-right': '1px',
|
||||
background: `-webkit-linear-gradient(bottom, ${backgroundColor} ${colorPercentage}%, #f3f3f3 ${colorPercentage}%)`,
|
||||
border: '1px solid #d0d0d0',
|
||||
cursor: 'pointer',
|
||||
'min-width': '25px',
|
||||
width: '25px',
|
||||
height: '50px',
|
||||
};
|
||||
const toolTip = `${r.source} TimeSpent: ${r.duration.toFixed(3)}ms`;
|
||||
return { style, toolTip };
|
||||
});
|
||||
private _processFrames(frames: ProfilerFrame[]): void {
|
||||
let regenerate = false;
|
||||
for (const frame of frames) {
|
||||
if (frame.duration >= this._maxDuration) {
|
||||
regenerate = true;
|
||||
}
|
||||
this._allRecords.push(frame);
|
||||
}
|
||||
if (regenerate) {
|
||||
this._graphDataSubject.next(this._generateBars());
|
||||
return;
|
||||
}
|
||||
const multiplicationFactor = parseFloat((MAX_HEIGHT / this._maxDuration).toFixed(2));
|
||||
frames.forEach((frame) => this._graphDataSubject.value.push(this._getBarStyles(frame, multiplicationFactor)));
|
||||
|
||||
// We need to pass a new reference, because the CDK virtual scroll
|
||||
// has OnPush strategy, so it doesn't update the UI otherwise.
|
||||
// If this turns out ot be a bottleneck, we can easily create an immutable reference.
|
||||
this._graphDataSubject.next(this._graphDataSubject.value.slice());
|
||||
}
|
||||
|
||||
private _generateBars(): GraphNode[] {
|
||||
const maxValue = this._allRecords.reduce((acc: number, frame: ProfilerFrame) => Math.max(acc, frame.duration), 0);
|
||||
const multiplicationFactor = parseFloat((MAX_HEIGHT / maxValue).toFixed(2));
|
||||
this._maxDuration = Math.max(this._maxDuration, maxValue);
|
||||
return this._allRecords.map((r) => this._getBarStyles(r, multiplicationFactor));
|
||||
}
|
||||
|
||||
private _getBarStyles(
|
||||
record: ProfilerFrame,
|
||||
multiplicationFactor: number
|
||||
): { style: { [key: string]: string }; toolTip: string } {
|
||||
const height = record.duration * multiplicationFactor;
|
||||
const colorPercentage = Math.round((height / MAX_HEIGHT) * 100);
|
||||
const backgroundColor = this.getColorByFrameRate(this.estimateFrameRate(record.duration));
|
||||
|
||||
const style = {
|
||||
'margin-left': '1px',
|
||||
'margin-right': '1px',
|
||||
background: `-webkit-linear-gradient(bottom, ${backgroundColor} ${colorPercentage}%, #f3f3f3 ${colorPercentage}%)`,
|
||||
border: '1px solid #d0d0d0',
|
||||
cursor: 'pointer',
|
||||
'min-width': '25px',
|
||||
width: '25px',
|
||||
height: '50px',
|
||||
};
|
||||
const toolTip = `${record.source} TimeSpent: ${record.duration.toFixed(3)}ms`;
|
||||
return { style, toolTip };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,27 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
|||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { FrameSelectorComponent } from './frame-selector/frame-selector.component';
|
||||
import { TimelineControlsComponent } from './timeline-controls/timeline-controls.component';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { RecordingDialogComponent } from './recording-modal/recording-dialog/recording-dialog.component';
|
||||
import { RecordingModalComponent } from './recording-modal/recording-modal.component';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
@NgModule({
|
||||
declarations: [TimelineComponent, FrameSelectorComponent, TimelineControlsComponent],
|
||||
declarations: [
|
||||
TimelineComponent,
|
||||
RecordingDialogComponent,
|
||||
RecordingModalComponent,
|
||||
FrameSelectorComponent,
|
||||
TimelineControlsComponent,
|
||||
],
|
||||
imports: [
|
||||
ScrollingModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RecordingVisualizerModule,
|
||||
MatDialogModule,
|
||||
MatProgressBarModule,
|
||||
MatButtonModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
|
|
|
|||
Loading…
Reference in a new issue