diff --git a/frontend/app/components/trace_viewer/BUILD b/frontend/app/components/trace_viewer/BUILD index d7b2751d..af372e5a 100644 --- a/frontend/app/components/trace_viewer/BUILD +++ b/frontend/app/components/trace_viewer/BUILD @@ -24,6 +24,7 @@ xprof_ng_module( "@org_xprof//frontend/app/common/angular:angular_material_progress_bar", "@org_xprof//frontend/app/common/constants", "@org_xprof//frontend/app/common/interfaces", + "@org_xprof//frontend/app/components/trace_viewer_container", "@org_xprof//frontend/app/components/trace_viewer_v2:app_bundle_ts", "@org_xprof//frontend/app/pipes", "@org_xprof//frontend/app/services/communication_service", diff --git a/frontend/app/components/trace_viewer_container/BUILD b/frontend/app/components/trace_viewer_container/BUILD index bf5eb6e7..7cc9f030 100644 --- a/frontend/app/components/trace_viewer_container/BUILD +++ b/frontend/app/components/trace_viewer_container/BUILD @@ -15,9 +15,15 @@ xprof_ng_module( deps = [ "@npm//@angular/common", "@npm//@angular/core", + "@npm//@angular/forms", "@npm//rxjs", + "@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/angular:angular_material_table", "@org_xprof//frontend/app/components/trace_viewer_v2:app_bundle_ts", "@org_xprof//frontend/app/pipes", ], diff --git a/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html b/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html index e8ef5bbb..b45593d3 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html @@ -1,20 +1,57 @@ -
- error -
{{'Error, please use the old frontend. ' + (traceViewerV2ErrorMessage ?? '')}}
-
-
- -
{{traceViewerV2LoadingStatus}}
-
{{tutorials[currentTutorialIndex]}}
+ + +
+ error +
{{'Error, please use the old frontend. ' + (traceViewerV2ErrorMessage ?? '')}}
+
+
+ +
+ +
{{traceViewerV2LoadingStatus}}
+
{{tutorials[currentTutorialIndex]}}
+
+
+
+ + + +
{{searchResultCountText}}
+ + +
+
+
Loading more data...
+ +
--> -->
-
Name: {{selectedEvent.name}}
-
Start Time: {{selectedEvent.startUsFormatted}}
-
Duration: {{selectedEvent.durationUsFormatted}}
+
+ + + + + + + + + + + + +
{{element.property}} {{element.value}}
+
+
+
+
diff --git a/frontend/app/components/trace_viewer_container/trace_viewer_container.scss b/frontend/app/components/trace_viewer_container/trace_viewer_container.scss index b71e2661..4c34404c 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.scss +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.scss @@ -1,18 +1,148 @@ -:host { - display: block; +/** CSS for a trace viewer component. */ + +$menu-bar-height: 64px; +$filter-bar-height: 50px; +$search-bar-height: 50px; +$drawer-height: 300px; + +iframe { + position: absolute; width: 100%; - height: 100%; + height: calc(100% - $menu-bar-height - $filter-bar-height); + box-sizing: border-box; + border-style: unset; + transition: height 0.05s ease-in-out; } -.emscripten { +canvas { + position: absolute; width: 100%; - height: 100%; - display: block; + height: calc(100% - $menu-bar-height - $filter-bar-height - $search-bar-height); + box-sizing: border-box; + border-style: unset; + // Add a smooth transition when the height changes (e.g., when the drawer + // opens/closes). + // This is to make the canvas rendering more smooth. + transition: height 0.05s ease-in-out; } -iframe { - border: 0; - display: block; - height: 100%; +.search-container { + display: flex; + align-items: center; + margin-left: 16px; + height: $search-bar-height; + 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 { + margin: 0 8px; +} + +.drawer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: $drawer-height; + background-color: white; + border-top: 1px solid black; + padding: 10px; + box-sizing: border-box; + z-index: 100; + overflow: auto; + + .event-detail-table-container { + width: 800px; + } + + table.mat-mdc-table { + width: 100%; + table-layout: fixed; + + th.mat-mdc-header-cell, + td.mat-mdc-cell { + border-bottom: none; + font-size: 12px; + padding-top: 0; + padding-bottom: 0; + } + + td.mat-column-property { + width: 30%; + } + + tr.mat-mdc-header-row { + height: 30px; + } + + tr.mat-mdc-row { + height: 24px; + } + } +} + +.error-overlay { + position: absolute; + width: 100%; + height: calc(100% - $menu-bar-height - $filter-bar-height - $search-bar-height); + 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: calc(100% - $menu-bar-height - $filter-bar-height - $search-bar-height); + 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%; + } +} + +.spacer { + flex-grow: 1; +} + +.incremental-loading { + display: flex; + align-items: center; + margin-right: 16px; + + .loading-status { + margin-right: 8px; + } +} + +:host(.drawer-open) { + canvas { + height: calc(100% - $menu-bar-height - $filter-bar-height - $search-bar-height - $drawer-height); + } } 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 e97947dc..cad69748 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts @@ -1,12 +1,56 @@ import {CommonModule} from '@angular/common'; -import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; import {MatProgressBarModule} from '@angular/material/progress-bar'; -import {LOADING_STATUS_UPDATE_EVENT_NAME, TraceViewerV2LoadingStatus, type TraceViewerV2Module} from 'org_xprof/frontend/app/components/trace_viewer_v2/main'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTableModule} from '@angular/material/table'; +import {isSearchEventsEvent, LOADING_STATUS_UPDATE_EVENT_NAME, SEARCH_EVENTS_EVENT_NAME, type SearchEventsEventDetail, TraceViewerV2LoadingStatus, 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'; + +/** + * The name of the event selected custom event, dispatched from WASM in Trace + * Viewer v2. + */ +export const EVENT_SELECTED_EVENT_NAME = 'eventselected'; + +/** + * The detail of an 'EntrySelected' custom event. The properties are quoted to + * prevent renaming during minification. + */ +export declare interface EntrySelectedEventDetail { + eventIndex: number; + name: string; + startUs: number; + durationUs: number; + startUsFormatted: string; + durationUsFormatted: string; + pid?: number; + uid?: string; + hloModuleName?: string; + hloOpName?: string; +} + +// Type guard for the 'EntrySelected' custom event. +function isEntrySelectedEvent( + event: Event, + ): event is CustomEvent { + return ( + event instanceof CustomEvent && event.detail && + typeof event.detail.eventIndex === 'number' && + typeof event.detail.name === 'string' && + typeof event.detail.startUs === 'number' && + typeof event.detail.durationUs === 'number' && + typeof event.detail.startUsFormatted === 'string' && + typeof event.detail.durationUsFormatted === 'string'); +} + /** * The interface for a selected event. */ @@ -14,6 +58,17 @@ export interface SelectedEvent { name: string; startUsFormatted?: string; durationUsFormatted?: string; + stackTraceLinkHtml?: string; + rooflineModelLinkHtml?: string; + graphViewerLinkHtml?: string; +} + +/** + * The interface for selected event property. + */ +export interface SelectedEventProperty { + property: string; + value: string|undefined; } // The tutorials to display while the trace viewer is loading. @@ -43,6 +98,12 @@ function isLoadingStatusUpdateEvent( Object.values(TraceViewerV2LoadingStatus).includes(event.detail.status)); } +declare interface TrackView extends Element { + onEndPanScan_(event: Event): Function; + onEndSelection_(event: Event): Function; + onEndZoom_(event: Event): Function; +} + /** A trace viewer container component. */ @Component({ standalone: true, @@ -54,14 +115,26 @@ function isLoadingStatusUpdateEvent( MatIconModule, MatProgressBarModule, PipesModule, + FormsModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + MatProgressSpinnerModule, + MatTableModule, ], }) -export class TraceViewerContainer implements OnInit, OnDestroy { +export class TraceViewerContainer implements OnInit, OnDestroy, AfterViewInit { @Input() traceViewerModule: TraceViewerV2Module|null = null; @Input() url = ''; @Input() useTraceViewerV2 = true; @Input() selectedEvent?: SelectedEvent|null; - @Input() isInitialLoading = false; + @Input() searching = false; + isInitialLoading = true; + @Input() selectedEventProperties: SelectedEventProperty[] = []; + @Input() eventDetailColumns: string[] = []; + @Output() + readonly eventSelected = new EventEmitter(); + @Output() readonly searchEvents = new EventEmitter(); @ViewChild('tvIframe') tvIframe?: ElementRef; @@ -69,6 +142,7 @@ export class TraceViewerContainer implements OnInit, OnDestroy { traceViewerV2LoadingStatus: TraceViewerV2LoadingStatus = TraceViewerV2LoadingStatus.IDLE; traceViewerV2ErrorMessage?: string; + searchQuery = ''; search$ = new Subject(); currentSearchQuery = ''; searchResultCountText = ''; @@ -97,6 +171,21 @@ export class TraceViewerContainer implements OnInit, OnDestroy { LOADING_STATUS_UPDATE_EVENT_NAME, this.loadingStatusUpdateEventListener, ); + window.addEventListener( + EVENT_SELECTED_EVENT_NAME, + this.eventSelectedEventListener, + ); + window.addEventListener( + SEARCH_EVENTS_EVENT_NAME, + this.searchEventsEventListener, + ); + } + + ngAfterViewInit() { + if (!this.useTraceViewerV2) { + window.addEventListener('mouseup', this.mouseUpEventListener); + window.addEventListener('keydown', this.keyDownEventListener); + } } ngOnDestroy() { @@ -104,12 +193,57 @@ export class TraceViewerContainer implements OnInit, OnDestroy { LOADING_STATUS_UPDATE_EVENT_NAME, this.loadingStatusUpdateEventListener, ); + window.removeEventListener( + EVENT_SELECTED_EVENT_NAME, + this.eventSelectedEventListener, + ); + window.removeEventListener( + SEARCH_EVENTS_EVENT_NAME, + this.searchEventsEventListener, + ); + if (!this.useTraceViewerV2) { + window.removeEventListener('mouseup', this.mouseUpEventListener); + window.removeEventListener('keydown', this.keyDownEventListener); + } // Unsubscribes all pending subscriptions. this.destroyed.next(); this.destroyed.complete(); this.stopTutorialRotation(); } + private readonly keyDownEventListener = (event: KeyboardEvent) => { + // Disable hotkey listening when typing in the input box + const el = event.target as HTMLInputElement; + if (el.type === 'text') return; + switch (event.key) { + case 'a': + case 'd': + case 's': + case 'w': + case '1': + case '2': + case '3': + case '4': + this.tvIframe?.nativeElement?.focus(); + break; + default: + break; + } + }; + + private readonly mouseUpEventListener = (event: Event) => { + const trackView: TrackView|null|undefined = + this.tvIframe?.nativeElement?.contentDocument?.querySelector( + 'tr-ui-timeline-track-view', + ); + try { + trackView?.onEndPanScan_(event); + trackView?.onEndSelection_(event); + trackView?.onEndZoom_(event); + } catch (e) { + } + }; + private readonly loadingStatusUpdateEventListener = (event: Event) => { if (!isLoadingStatusUpdateEvent(event)) { return; @@ -124,6 +258,20 @@ export class TraceViewerContainer implements OnInit, OnDestroy { } }; + private readonly eventSelectedEventListener = (e: Event) => { + if (!isEntrySelectedEvent(e)) { + return; + } + this.eventSelected.emit(e.detail); + }; + + private readonly searchEventsEventListener = (e: Event) => { + if (!isSearchEventsEvent(e)) { + return; + } + this.searchEvents.emit(e.detail); + }; + /** * Updates the loading status and starts/stops the tutorial rotation * accordingly. @@ -142,6 +290,7 @@ export class TraceViewerContainer implements OnInit, OnDestroy { this.traceViewerV2LoadingStatus === TraceViewerV2LoadingStatus.ERROR) { // Stop the tutorial rotation when loading is finished or failed. this.stopTutorialRotation(); + this.isInitialLoading = false; } else { // Start the tutorial rotation when loading is in progress. this.startTutorialRotation();