/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Component, OnInit} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {Events, MessageBus, ProfilerFrame} from 'protocol'; import {Subject} from 'rxjs'; import {FileApiService} from './file-api-service'; import {ProfilerImportDialogComponent} from './profiler-import-dialog.component'; import {TimelineComponent} from './timeline/timeline.component'; import {MatIcon} from '@angular/material/icon'; import {MatTooltip} from '@angular/material/tooltip'; import {MatIconButton} from '@angular/material/button'; import {MatCard} from '@angular/material/card'; type State = 'idle' | 'recording' | 'visualizing'; const SUPPORTED_VERSIONS = [1]; const PROFILER_VERSION = 1; @Component({ selector: 'ng-profiler', templateUrl: './profiler.component.html', styleUrls: ['./profiler.component.scss'], standalone: true, imports: [MatCard, MatIconButton, MatTooltip, MatIcon, TimelineComponent], }) export class ProfilerComponent implements OnInit { state: State = 'idle'; stream = new Subject(); // We collect this buffer so we can have it available for export. private _buffer: ProfilerFrame[] = []; constructor( private _fileApiService: FileApiService, private _messageBus: MessageBus, public dialog: MatDialog, ) { this._fileApiService.uploadedData.subscribe((importedFile) => { if (importedFile.error) { console.error('Could not process uploaded file'); console.error(importedFile.error); this.dialog.open(ProfilerImportDialogComponent, { width: '600px', data: {status: 'ERROR', errorMessage: importedFile.error}, }); return; } if (!SUPPORTED_VERSIONS.includes(importedFile.version)) { const processDataDialog = this.dialog.open(ProfilerImportDialogComponent, { width: '600px', data: { importedVersion: importedFile.version, profilerVersion: PROFILER_VERSION, status: 'INVALID_VERSION', }, }); processDataDialog.afterClosed().subscribe((result) => { if (result) { this.state = 'visualizing'; this._buffer = importedFile.buffer; setTimeout(() => this.stream.next(importedFile.buffer)); } }); } else { this.state = 'visualizing'; this._buffer = importedFile.buffer; setTimeout(() => this.stream.next(importedFile.buffer)); } }); } startRecording(): void { this.state = 'recording'; this._messageBus.emit('startProfiling'); } stopRecording(): void { this.state = 'visualizing'; this._messageBus.emit('stopProfiling'); this.stream.complete(); } ngOnInit(): void { this._messageBus.on('profilerResults', (remainingRecords) => { if (remainingRecords.duration > 0 && remainingRecords.source) { this.stream.next([remainingRecords]); this._buffer.push(remainingRecords); } }); this._messageBus.on('sendProfilerChunk', (chunkOfRecords: ProfilerFrame) => { this.stream.next([chunkOfRecords]); this._buffer.push(chunkOfRecords); }); } exportProfilerResults(): void { const fileToExport = { version: PROFILER_VERSION, buffer: this._buffer, }; this._fileApiService.saveObjectAsJSON(fileToExport); } importProfilerResults(event: InputEvent): void { this._fileApiService.publishFileUpload(event); } discardRecording(): void { this.stream = new Subject(); this.state = 'idle'; this._buffer = []; } }