Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<ng-container *ngIf="useTraceViewerV2; else traceViewV1">
<div class="error-overlay" *ngIf="traceViewerV2LoadingStatus === TraceViewerV2LoadingStatus.ERROR">
<mat-icon>error</mat-icon>
<div class="error-message">{{'Error, please use the old frontend. ' + (traceViewerV2ErrorMessage ?? '')}}</div>
</div>
<div class="loading-overlay" *ngIf="traceViewerV2LoadingStatus !== TraceViewerV2LoadingStatus.IDLE && traceViewerV2LoadingStatus !== TraceViewerV2LoadingStatus.ERROR">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
<div class="loading-status">{{traceViewerV2LoadingStatus}}</div>
<div class="tutorial">{{tutorials[currentTutorialIndex]}}</div>
</div>
-->
<canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()"></canvas>
-->
<div *ngIf="selectedEvent" #drawer class="drawer">
<div>Name: {{selectedEvent.name}}</div>
<div>Start Time: {{selectedEvent.startUsFormatted}}</div>
<div>Duration: {{selectedEvent.durationUsFormatted}}</div>
</div>
</ng-container>
<ng-template #traceViewV1>
<iframe #tvIframe [src]="url | safe" onload="focus();"></iframe>
</ng-template>
18 changes: 18 additions & 0 deletions frontend/app/components/trace_viewer/trace_viewer_container.scss
Original file line number Diff line number Diff line change
@@ -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%;
}
208 changes: 208 additions & 0 deletions frontend/app/components/trace_viewer/trace_viewer_container.ts
Original file line number Diff line number Diff line change
@@ -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<LoadingStatusUpdateEventDetail> {
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<HTMLIFrameElement>;

readonly TraceViewerV2LoadingStatus = TraceViewerV2LoadingStatus;
traceViewerV2LoadingStatus: TraceViewerV2LoadingStatus =
TraceViewerV2LoadingStatus.IDLE;
traceViewerV2ErrorMessage?: string;
search$ = new Subject<string>();
currentSearchQuery = '';
searchResultCountText = '';
readonly tutorials = TUTORIALS;
currentTutorialIndex = 0;
tutorialSubscription?: Subscription;

/** Handles on-destroy Subject, used to unsubscribe. */
private readonly destroyed = new ReplaySubject<void>(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}`;
}
}
24 changes: 24 additions & 0 deletions frontend/app/components/trace_viewer_v2/METADATA
Original file line number Diff line number Diff line change
@@ -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"
}
}
}