diff --git a/frontend/app/components/trace_viewer/BUILD b/frontend/app/components/trace_viewer/BUILD index 410726497..edadadbe7 100644 --- a/frontend/app/components/trace_viewer/BUILD +++ b/frontend/app/components/trace_viewer/BUILD @@ -22,6 +22,7 @@ xprof_ng_module( deps = [ "@npm//@angular/common", "@npm//@angular/core", + "@npm//@angular/core_rxjs_interop", "@npm//@angular/forms", "@npm//@angular/router", "@npm//@ngrx/store", diff --git a/frontend/app/components/trace_viewer/trace_viewer.ts b/frontend/app/components/trace_viewer/trace_viewer.ts index 8b66004df..86c39b8fb 100644 --- a/frontend/app/components/trace_viewer/trace_viewer.ts +++ b/frontend/app/components/trace_viewer/trace_viewer.ts @@ -5,6 +5,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, + DestroyRef, inject, Injector, OnDestroy, @@ -12,6 +13,7 @@ import { TemplateRef, ViewChild, } from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {MatDialog, MatDialogConfig} from '@angular/material/dialog'; import {ActivatedRoute, Router} from '@angular/router'; import {Store} from '@ngrx/store'; @@ -55,7 +57,6 @@ import { distinctUntilChanged, finalize, switchMap, - takeUntil, tap, } from 'rxjs/operators'; import { @@ -160,8 +161,8 @@ function loadFeatureFlagsFromStorage(): FeatureFlagWithValue[] { styleUrls: ['./trace_viewer.css'], }) export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { - private readonly destroyed = new ReplaySubject(1); - private isDestroyed = false; + private readonly destroyRef = inject(DestroyRef); + isDestroyed = false; private isInitializing = false; private navigationEvent: NavigationEvent = {}; private readonly injector = inject(Injector); @@ -404,7 +405,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { this.route.queryParams, this.store.select(getHostsState), ]) - .pipe(takeUntil(this.destroyed)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(([params, queryParams, hostsMetadata]) => { if (hostsMetadata && hostsMetadata.length > 0) { this.hostList = hostsMetadata.map( @@ -440,7 +441,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { // UI accordingly. sourceCodeService ?.isAvailable() - .pipe(takeUntil(this.destroyed)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((isAvailable) => { this.sourceCodeServiceIsAvailable = isAvailable; }); @@ -449,7 +450,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { ngOnInit(): void { this.searchQuery .pipe( - takeUntil(this.destroyed), + takeUntilDestroyed(this.destroyRef), debounceTime(300), distinctUntilChanged(), tap(() => { @@ -482,6 +483,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { }), ) .subscribe((data) => { + if (this.isDestroyed) return; this.searching = false; if (this.traceViewerModule && data) { // Type contract is guaranteed by the backend response structure. @@ -525,7 +527,8 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { this.traceViewerModule && this.traceViewerModule.getAllFlowCategories ) { - this.allFlowCategories = this.traceViewerModule.getAllFlowCategories(); + this.allFlowCategories = + this.traceViewerModule.getAllFlowCategories() as FlowCategory[]; this.selectAllFlowCategories(); } this.update(this.navigationEvent); @@ -599,6 +602,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { if (this.useTraceViewerV2) { if (this.traceViewerModule && this.traceViewerModule.loadTraceData) { this.traceViewerModule.loadTraceData(traceDataUrl).then(() => { + if (this.isDestroyed) return; this.updateFlowCategories(); this.updateWasmFlowCategories(); this.updateWasmProcessMappings(); @@ -623,9 +627,6 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { this.traceViewerModule = null; } } finally { - // Unsubscribes all pending subscriptions. - this.destroyed.next(); - this.destroyed.complete(); window.removeEventListener( DETAILS_RECEIVED_EVENT_NAME, this.detailsReceivedEventListener, @@ -872,7 +873,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { host, params, ) - .pipe(takeUntil(this.destroyed)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => { const traceData = data as TraceData; if ( @@ -944,7 +945,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { params, ) .pipe( - takeUntil(this.destroyed), + takeUntilDestroyed(this.destroyRef), finalize(() => { this.pendingAdjacentNodesFetches.delete(key); }), @@ -1243,7 +1244,7 @@ export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { customEvent.detail.status === TraceViewerV2LoadingStatus.IDLE ) { setTimeout(() => { - if (!this.destroyed.isStopped) { + if (!this.isDestroyed) { this.showColorOnboarding = true; } }, 2000); // Delay 2 seconds after load complete diff --git a/frontend/app/components/trace_viewer_container/BUILD b/frontend/app/components/trace_viewer_container/BUILD index d08e919a3..1483ab0ab 100644 --- a/frontend/app/components/trace_viewer_container/BUILD +++ b/frontend/app/components/trace_viewer_container/BUILD @@ -15,6 +15,7 @@ xprof_ng_module( deps = [ "@npm//@angular/common", "@npm//@angular/core", + "@npm//@angular/core_rxjs_interop", "@npm//@angular/forms", "@npm//angular-split", "@npm//rxjs", diff --git a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts index c70905a06..3845ef556 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts @@ -5,8 +5,10 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, + DestroyRef, ElementRef, EventEmitter, + inject, Input, OnChanges, OnDestroy, @@ -15,6 +17,7 @@ import { SimpleChanges, ViewChild, } from '@angular/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {FormsModule} from '@angular/forms'; import {MatButtonModule} from '@angular/material/button'; import {MatFormFieldModule} from '@angular/material/form-field'; @@ -35,8 +38,8 @@ import { type TraceViewerV2Module, } from 'org_xprof/frontend/app/components/trace_viewer_v2/main'; import {PipesModule} from 'org_xprof/frontend/app/pipes/pipes_module'; -import {interval, ReplaySubject, Subject, Subscription} from 'rxjs'; -import {debounceTime, takeUntil} from 'rxjs/operators'; +import {interval, Subject, Subscription} from 'rxjs'; +import {debounceTime} from 'rxjs/operators'; const DEPRECATED_STORAGE_KEYS = ['trace_viewer_timing_prompted']; @@ -379,12 +382,11 @@ export class TraceViewerContainer timelineHeightPercent = 100; detailHeightPercent = 0; - /** Handles on-destroy Subject, used to unsubscribe. */ - private readonly destroyed = new ReplaySubject(1); + private readonly destroyRef = inject(DestroyRef); constructor() { this.search$ - .pipe(debounceTime(300), takeUntil(this.destroyed)) + .pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef)) .subscribe((query) => { this.currentSearchQuery = query; this.searchEvents.emit({events_query: query}); @@ -456,9 +458,6 @@ export class TraceViewerContainer window.removeEventListener('mouseup', this.mouseUpEventListener); window.removeEventListener('keydown', this.keyDownEventListener); } - // Unsubscribes all pending subscriptions. - this.destroyed.next(); - this.destroyed.complete(); this.stopTutorialRotation(); } @@ -622,7 +621,7 @@ export class TraceViewerContainer if (this.tutorialSubscription) return; this.tutorialSubscription = interval(TUTORIAL_ROTATION_INTERVAL_MS) - .pipe(takeUntil(this.destroyed)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.currentTutorialIndex = (this.currentTutorialIndex + 1) % this.tutorials.length; diff --git a/frontend/app/components/trace_viewer_v2/main.ts b/frontend/app/components/trace_viewer_v2/main.ts index d65ff3ebf..0019e5b5c 100644 --- a/frontend/app/components/trace_viewer_v2/main.ts +++ b/frontend/app/components/trace_viewer_v2/main.ts @@ -320,8 +320,7 @@ async function getWebGpuDevice(): Promise { 'WebGPU cannot be initialized - failed to get WebGPU device.', ); } - // tslint:disable-next-line:no-any - (device as any).lost + void (device as unknown as {lost: Promise}).lost .then(() => { throw new Error('WebGPU Cannot be initialized - Device has been lost'); }) @@ -512,6 +511,13 @@ async function decompressGzip(data: Uint8Array): Promise { } } +/** + * Checks if the URL represents a compressed protobuf file. + */ +export function isCompressedProto(urlObj: URL): boolean { + return urlObj.pathname.endsWith('.pb'); +} + function isPerfettoTrace(data: Uint8Array, fileName: string): boolean { return ( fileName.endsWith('.pftrace') || @@ -799,7 +805,7 @@ async function fetchAndProcessTraceData({ updateUrlWithResolution(urlObj, traceviewerModule.canvas); try { - if (urlObj.pathname.endsWith('.pb')) { + if (isCompressedProto(urlObj)) { const buffer = await loadCompressedTraceDataInternal(urlObj.toString()); if (isAbortRequested()) return; @@ -1104,7 +1110,7 @@ export async function traceViewerV2Main( return; } - if (urlObj.pathname.endsWith('.pb')) { + if (isCompressedProto(urlObj)) { const buffer = await loadCompressedTraceDataInternal(urlObj.toString()); traceviewerModule.setCompressedSearchResultsInWasm( new Uint8Array(buffer),