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
19 changes: 17 additions & 2 deletions frontend/app/components/trace_viewer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,31 @@ xprof_ng_module(
name = "trace_viewer",
srcs = [
"trace_viewer.ts",
"trace_viewer_container.ts",
"trace_viewer_module.ts",
],
assets = [
":trace_viewer_css",
":trace_viewer_container_css",
"trace_viewer.ng.html",
"trace_viewer_container.ng.html",
],
deps = [
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/router",
"@npm//@ngrx/store",
"@npm//rxjs",
"@org_xprof//frontend/app/common/angular:angular_common_http",
"@org_xprof//frontend/app/common/angular:angular_material_button",
"@org_xprof//frontend/app/common/angular:angular_material_form_field",
"@org_xprof//frontend/app/common/angular:angular_material_icon",
"@org_xprof//frontend/app/common/angular:angular_material_input",
"@org_xprof//frontend/app/common/angular:angular_material_progress_bar",
"@org_xprof//frontend/app/common/angular:angular_material_progress_spinner",
"@org_xprof//frontend/app/common/constants",
"@org_xprof//frontend/app/common/interfaces",
"@org_xprof//frontend/app/components/trace_viewer_v2:app_bundle_ts",
"@org_xprof//frontend/app/pipes",
"@org_xprof//frontend/app/services/communication_service",
"@org_xprof//frontend/app/services/source_code_service:source_code_service_interface",
"@org_xprof//frontend/app/store",
],
Expand All @@ -35,3 +43,10 @@ sass_binary(
# stack = False,
sourcemap = False,
)

sass_binary(
name = "trace_viewer_container_css",
src = "trace_viewer_container.scss",
# stack = False,
sourcemap = False,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host {
display: block;
width: 100%;
height: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<div *ngIf="useTraceViewerV2" class="search-container">
<mat-form-field appearance="outline">
<input matInput #searchBox placeholder="Search events" aria-label="Search trace events" (input)="onSearchEvent(searchBox.value)" />
</mat-form-field>
<div *ngIf="currentSearchQuery" class="search-result-count">{{searchResultCountText}}</div>
<button mat-icon-button aria-label="Previous search result" (click)="prevSearchResult()" [disabled]="!currentSearchQuery">
<mat-icon>chevron_left</mat-icon>
</button>
<button mat-icon-button aria-label="Next search result" (click)="nextSearchResult()" [disabled]="!currentSearchQuery">
<mat-icon>chevron_right</mat-icon>
</button>
</div>
<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>
<ng-container *ngIf="traceViewerV2LoadingStatus !== TraceViewerV2LoadingStatus.IDLE && traceViewerV2LoadingStatus !== TraceViewerV2LoadingStatus.ERROR">
<div *ngIf="isInitialLoading" class="loading-overlay">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
<div class="loading-status">{{traceViewerV2LoadingStatus}}</div>
<div class="tutorial">{{tutorials[currentTutorialIndex]}}</div>
</div>
<div *ngIf="!isInitialLoading" class="incremental-loading-spinner">
<div class="loading-status">Loading more data...</div>
<mat-progress-spinner mode="indeterminate" diameter="32"></mat-progress-spinner>
</div>
</ng-container>
-->
<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>
94 changes: 94 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,94 @@
$menu-bar-height: 64px;
$filter-bar-height: 50px;
$search-bar-height: 50px;

:host {
display: block;
width: 100%;
height: 100%;
position: relative;
}

.emscripten {
width: 100%;
height: 100%;
display: block;
}

iframe {
border: 0;
display: block;
height: 100%;
width: 100%;
}

.error-overlay {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
background-color: white;
color: red;

mat-icon {
margin-right: 10px;
}
}

.loading-overlay {
position: absolute;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.5);
z-index: 1000;

.loading-status,
.tutorial {
margin-top: 10px;
}

mat-progress-bar {
width: 40%;
}
}

.incremental-loading-spinner {
position: absolute;
top: 1px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: row;
align-items: center;

.loading-status {
margin-right: 10px;
}
}

.search-container {
display: flex;
align-items: center;
margin-left: 16px;
height: 50px;
border-bottom: 1px solid black;
box-sizing: border-box;
}

.search-container ::ng-deep .mat-mdc-form-field-infix {
min-height: 30px;
padding-top: 4px;
padding-bottom: 4px;
align-items: center;
}

.search-result-count {
padding: 0 8px;
}
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}`;
}
}
Loading