diff --git a/frontend/app/components/trace_viewer/trace_viewer_container.css b/frontend/app/components/trace_viewer/trace_viewer_container.css deleted file mode 100644 index d4b23470..00000000 --- a/frontend/app/components/trace_viewer/trace_viewer_container.css +++ /dev/null @@ -1,5 +0,0 @@ -:host { - display: block; - width: 100%; - height: 100%; -} diff --git a/frontend/app/components/trace_viewer/trace_viewer_container.ng.html b/frontend/app/components/trace_viewer/trace_viewer_container.ng.html new file mode 100644 index 00000000..e8ef5bbb --- /dev/null +++ b/frontend/app/components/trace_viewer/trace_viewer_container.ng.html @@ -0,0 +1,22 @@ + + + error + {{'Error, please use the old frontend. ' + (traceViewerV2ErrorMessage ?? '')}} + + + + {{traceViewerV2LoadingStatus}} + {{tutorials[currentTutorialIndex]}} + + --> + + --> + + Name: {{selectedEvent.name}} + Start Time: {{selectedEvent.startUsFormatted}} + Duration: {{selectedEvent.durationUsFormatted}} + + + + + diff --git a/frontend/app/components/trace_viewer/trace_viewer_container.scss b/frontend/app/components/trace_viewer/trace_viewer_container.scss new file mode 100644 index 00000000..b71e2661 --- /dev/null +++ b/frontend/app/components/trace_viewer/trace_viewer_container.scss @@ -0,0 +1,18 @@ +:host { + display: block; + width: 100%; + height: 100%; +} + +.emscripten { + width: 100%; + height: 100%; + display: block; +} + +iframe { + border: 0; + display: block; + height: 100%; + width: 100%; +} diff --git a/frontend/app/components/trace_viewer/trace_viewer_container.ts b/frontend/app/components/trace_viewer/trace_viewer_container.ts new file mode 100644 index 00000000..8e55328f --- /dev/null +++ b/frontend/app/components/trace_viewer/trace_viewer_container.ts @@ -0,0 +1,208 @@ +import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {LOADING_STATUS_UPDATE_EVENT_NAME, TraceViewerV2LoadingStatus, type TraceViewerV2Module} from 'org_xprof/frontend/app/components/trace_viewer_v2/main'; +import {interval, ReplaySubject, Subject, Subscription} from 'rxjs'; +import {debounceTime, takeUntil} from 'rxjs/operators'; + +/** + * The interface for a selected event. + */ +export interface SelectedEvent { + name: string; + startUsFormatted?: string; + durationUsFormatted?: string; +} + +// The tutorials to display while the trace viewer is loading. +const TUTORIALS = Object.freeze([ + 'Pan: A/D or Shift+Scroll or Drag', + 'Zoom: W/S or Ctrl+Scroll', + 'Scroll: Up/Down Arrow or Scroll', +]); + +// The interval at which to rotate the tutorials. +const TUTORIAL_ROTATION_INTERVAL_MS = 3_000; + +/** + * The detail of a 'LoadingStatusUpdate' custom event. + */ +declare interface LoadingStatusUpdateEventDetail { + status: TraceViewerV2LoadingStatus; + message?: string; +} + +// Type guard for the 'LoadingStatusUpdate' custom event. +function isLoadingStatusUpdateEvent( + event: Event, + ): event is CustomEvent { + return ( + event instanceof CustomEvent && event.detail && event.detail.status && + Object.values(TraceViewerV2LoadingStatus).includes(event.detail.status)); +} + +/** A trace viewer container component. */ +@Component({ + standalone: false, + selector: 'trace-viewer-container', + templateUrl: './trace_viewer_container.ng.html', + styleUrls: ['./trace_viewer_container.css'], +}) +export class TraceViewerContainer implements OnInit, OnDestroy { + @Input() traceViewerModule: TraceViewerV2Module|null = null; + @Input() url = ''; + @Input() useTraceViewerV2 = true; + @Input() selectedEvent?: SelectedEvent|null; + @Input() isInitialLoading = false; + + @ViewChild('tvIframe') tvIframe?: ElementRef; + + readonly TraceViewerV2LoadingStatus = TraceViewerV2LoadingStatus; + traceViewerV2LoadingStatus: TraceViewerV2LoadingStatus = + TraceViewerV2LoadingStatus.IDLE; + traceViewerV2ErrorMessage?: string; + search$ = new Subject(); + currentSearchQuery = ''; + searchResultCountText = ''; + readonly tutorials = TUTORIALS; + currentTutorialIndex = 0; + tutorialSubscription?: Subscription; + + /** Handles on-destroy Subject, used to unsubscribe. */ + private readonly destroyed = new ReplaySubject(1); + + constructor() { + this.search$.pipe(debounceTime(300), takeUntil(this.destroyed)) + .subscribe((query) => { + this.currentSearchQuery = query; + if (this.traceViewerModule) { + this.traceViewerModule.Application.Instance().setSearchQuery(query); + this.updateSearchResultCountText(); + } else if (!query) { + this.searchResultCountText = ''; + } + }); + } + + ngOnInit() { + window.addEventListener( + LOADING_STATUS_UPDATE_EVENT_NAME, + this.loadingStatusUpdateEventListener, + ); + } + + ngOnDestroy() { + window.removeEventListener( + LOADING_STATUS_UPDATE_EVENT_NAME, + this.loadingStatusUpdateEventListener, + ); + // Unsubscribes all pending subscriptions. + this.destroyed.next(); + this.destroyed.complete(); + this.stopTutorialRotation(); + } + + private readonly loadingStatusUpdateEventListener = (event: Event) => { + if (!isLoadingStatusUpdateEvent(event)) { + return; + } + + this.updateLoadingStatus(event.detail.status); + + if (event.detail.status !== TraceViewerV2LoadingStatus.ERROR) { + this.traceViewerV2ErrorMessage = undefined; + } else { + this.traceViewerV2ErrorMessage = event.detail.message; + } + }; + + /** + * Updates the loading status and starts/stops the tutorial rotation + * accordingly. + * + * If the status changes to IDLE or ERROR, the tutorial rotation is stopped. + * Otherwise (e.g., INITIALIZING, LOADING_DATA), the tutorial rotation is + * started to provide user feedback. + */ + private updateLoadingStatus(status: TraceViewerV2LoadingStatus) { + if (this.traceViewerV2LoadingStatus === status) { + return; + } + this.traceViewerV2LoadingStatus = status; + + if (this.traceViewerV2LoadingStatus === TraceViewerV2LoadingStatus.IDLE || + this.traceViewerV2LoadingStatus === TraceViewerV2LoadingStatus.ERROR) { + // Stop the tutorial rotation when loading is finished or failed. + this.stopTutorialRotation(); + } else { + // Start the tutorial rotation when loading is in progress. + this.startTutorialRotation(); + } + } + + /** + * Starts the tutorial rotation. + * + * This method initializes the `tutorialSubscription` to rotate through + * tutorials at a set interval. It ensures only one subscription is active at + * a time. The subscription lifecycle is managed here and will be terminated + * when `stopTutorialRotation` is called or when the component is destroyed. + */ + private startTutorialRotation() { + if (this.tutorialSubscription) return; + + this.tutorialSubscription = + interval(TUTORIAL_ROTATION_INTERVAL_MS) + .pipe(takeUntil(this.destroyed)) + .subscribe(() => { + this.currentTutorialIndex = + (this.currentTutorialIndex + 1) % this.tutorials.length; + }); + } + + /** + * Stops the tutorial rotation. + * + * This method unsubscribes from the `tutorialSubscription` and clears the + * reference, stopping the interval timer. + */ + private stopTutorialRotation() { + if (this.tutorialSubscription) { + this.tutorialSubscription.unsubscribe(); + this.tutorialSubscription = undefined; + } + } + + onSearchEvent(query: string) { + this.search$.next(query); + } + + nextSearchResult() { + if (this.traceViewerModule) { + this.traceViewerModule.Application.Instance() + .navigateToNextSearchResult(); + this.updateSearchResultCountText(); + } + } + + prevSearchResult() { + if (this.traceViewerModule) { + this.traceViewerModule.Application.Instance() + .navigateToPrevSearchResult(); + this.updateSearchResultCountText(); + } + } + + updateSearchResultCountText() { + if (!this.traceViewerModule || !this.currentSearchQuery) { + this.searchResultCountText = ''; + return; + } + const instance = this.traceViewerModule.Application.Instance(); + const count = instance.getSearchResultsCount(); + const index = instance.getCurrentSearchResultIndex(); + if (count === 0) { + this.searchResultCountText = '0 / 0'; + return; + } + this.searchResultCountText = `${index === -1 ? 1 : index + 1} / ${count}`; + } +} diff --git a/frontend/app/components/trace_viewer_v2/METADATA b/frontend/app/components/trace_viewer_v2/METADATA new file mode 100644 index 00000000..d15d0e5a --- /dev/null +++ b/frontend/app/components/trace_viewer_v2/METADATA @@ -0,0 +1,24 @@ +# proto-file: devtools/metadata/metadata.proto +# proto-message: MetaData + +ide_config: { + build: { + language: CPP + glob: "**.cc" + glob: "**.h" + preset: { + name: "Emscripten" + flag: "--config=wasm" + } + } +} + +tricorder: { + options: { + builder: { + # Wasm modules require --config=wasm for emscripten headers. + target_tag: "requires_wasm_config" + config: "wasm" + } + } +}