From ac2cd3757b64b0e8a213c2f0ea4e6accc741f596 Mon Sep 17 00:00:00 2001 From: mgechev Date: Mon, 4 May 2020 16:40:55 -0700 Subject: [PATCH] feat(devtools): introduce streamed visualization of the profiling data --- .../profiler/profiler.component.html | 7 +- .../profiler/profiler.component.ts | 47 ++++--- .../devtools-tabs/profiler/profiler.module.ts | 15 +-- .../frame-selector.component.html | 15 +-- .../frame-selector.component.scss | 7 +- .../frame-selector.component.ts | 60 +++++++-- .../recording-dialog.component.html | 2 +- .../recording-dialog.component.scss | 0 .../recording-dialog.component.ts | 0 .../recording-modal.component.html | 2 +- .../recording-modal.component.scss | 2 +- .../recording-modal.component.ts | 12 +- .../timeline-controls.component.html | 16 +-- .../timeline-controls.component.scss | 5 + .../timeline-controls.component.ts | 3 +- .../timeline/timeline.component.html | 56 +++++---- .../timeline/timeline.component.scss | 13 ++ .../recording/timeline/timeline.component.ts | 116 +++++++++++++----- .../recording/timeline/timeline.module.ts | 16 ++- 19 files changed, 256 insertions(+), 138 deletions(-) rename projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/{ => timeline}/recording-modal/recording-dialog/recording-dialog.component.html (62%) rename projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/{ => timeline}/recording-modal/recording-dialog/recording-dialog.component.scss (100%) rename projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/{ => timeline}/recording-modal/recording-dialog/recording-dialog.component.ts (100%) rename projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/{ => timeline}/recording-modal/recording-modal.component.html (54%) rename projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/{ => timeline}/recording-modal/recording-modal.component.scss (75%) rename projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/{ => timeline}/recording-modal/recording-modal.component.ts (55%) diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.html b/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.html index 8e6a94fd6b4..bac99b97c05 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.html @@ -15,11 +15,8 @@

-

- -

-

- +

+

diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.ts index 332310a2097..4127a02db79 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.component.ts @@ -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(); - @ViewChildren(RecordingModalComponent) recordingRef: QueryList; + // 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(); this.state = 'idle'; + this._buffer = []; } } diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.module.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.module.ts index 2042b334997..277648d71b9 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.module.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/profiler.module.ts @@ -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], }) diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.html b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.html index dff65cf72dc..7634562cada 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.html @@ -1,18 +1,19 @@
-

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

- -
+ +
-
-
diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.scss b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.scss index 7d858b6c01c..a891e62aa7e 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.scss +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.scss @@ -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; diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.ts index c714c686f47..6e134561644 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/frame-selector/frame-selector.component.ts @@ -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) { + this._graphData$ = graphData; + this._graphDataSubscription = this._graphData$.subscribe((items) => + setTimeout(() => { + this.viewPort.scrollToIndex(items.length); + }) + ); + } + + get graphData$(): Observable { + return this._graphData$; + } + @Output() move = new EventEmitter(); @Output() selectFrame = new EventEmitter(); + @ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport; currentFrameIndex: number; + + get itemWidth(): number { + return ITEM_WIDTH; + } + + private _graphData$: Observable; + 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 }); + } + } } diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.html b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.html similarity index 62% rename from projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.html rename to projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.html index 98d26a8f56e..71b83fbc584 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.html @@ -1,5 +1,5 @@
-

Recording

+

Interact with your app to preview change detection

diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.scss b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.scss similarity index 100% rename from projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.scss rename to projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.scss diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.ts similarity index 100% rename from projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-dialog/recording-dialog.component.ts rename to projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-dialog/recording-dialog.component.ts diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.html b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.html similarity index 54% rename from projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.html rename to projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.html index d95cbc94059..a16311c881c 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.html @@ -1,3 +1,3 @@ -
+
diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.scss b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.scss similarity index 75% rename from projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.scss rename to projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.scss index c19bcc65235..f42bc8573d5 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.scss +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.scss @@ -1,4 +1,4 @@ -@import '../../../../../../../../node_modules/@angular/cdk/overlay-prebuilt.css'; +@import '../../../../../../../../../node_modules/@angular/cdk/overlay-prebuilt.css'; :host { overflow: hidden; diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.ts similarity index 55% rename from projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.ts rename to projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.ts index c1ba2a832e7..285f10a261f 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/recording-modal/recording-modal.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/recording-modal/recording-modal.component.ts @@ -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 {} diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.html b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.html index ac7c47d8c42..3a4e1f62382 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.html @@ -1,6 +1,6 @@
- - + + Flame graph @@ -14,12 +14,14 @@ - + -
diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.scss b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.scss index 77cd3c98daa..4779ca773a0 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.scss +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.scss @@ -1,3 +1,8 @@ +:host { + height: 60px; + display: block; +} + .controls { top: 0; left: 300px; diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.ts index f9a7f2d6a3a..171e9750e6f 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline-controls/timeline-controls.component.ts @@ -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(); @Output() exportProfile = new EventEmitter(); diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.html b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.html index ea9bee87825..d50666695e4 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.html +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.html @@ -1,26 +1,34 @@ - - - + - - Nothing was profiled - +

+ There's no information to show. +

- - - - - + + + + +

+ Select a bar to preview a particular change detection cycle. +

+ + diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.scss b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.scss index d3bf526af5d..f32c655a37d 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.scss +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.scss @@ -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; +} diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.ts index c19344c2296..c1fc43950dd 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.component.ts @@ -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) { + 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(); visualizationMode = VisualizationMode.BarGraph; - graphData: GraphNode[] = []; - currentFrameIndex = 0; + currentFrameIndex = -1; + + private _maxDuration = -Infinity; + private _subscription: Subscription; + private _allRecords: ProfilerFrame[] = []; + private _graphDataSubject = new BehaviorSubject([]); + 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 }; } } diff --git a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.module.ts b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.module.ts index 1221cd91251..b0f65ed9d14 100644 --- a/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.module.ts +++ b/projects/ng-devtools/src/lib/devtools-tabs/profiler/recording/timeline/timeline.module.ts @@ -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,