diff --git a/frontend/app/components/trace_viewer/trace_viewer.ng.html b/frontend/app/components/trace_viewer/trace_viewer.ng.html index 1b1ec856..98e0cbec 100644 --- a/frontend/app/components/trace_viewer/trace_viewer.ng.html +++ b/frontend/app/components/trace_viewer/trace_viewer.ng.html @@ -1 +1,9 @@ - + + diff --git a/frontend/app/components/trace_viewer/trace_viewer.ts b/frontend/app/components/trace_viewer/trace_viewer.ts index a04ad165..ad287410 100644 --- a/frontend/app/components/trace_viewer/trace_viewer.ts +++ b/frontend/app/components/trace_viewer/trace_viewer.ts @@ -1,15 +1,29 @@ import {PlatformLocation} from '@angular/common'; -import {Component, inject, Injector, OnDestroy} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {AfterViewInit, Component, inject, Injector, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {Store} from '@ngrx/store'; import {API_PREFIX, DATA_API, PLUGIN_NAME} from 'org_xprof/frontend/app/common/constants/constants'; import {HostMetadata} from 'org_xprof/frontend/app/common/interfaces/hosts'; import {NavigationEvent} from 'org_xprof/frontend/app/common/interfaces/navigation_event'; +import {EntrySelectedEventDetail, SelectedEvent, SelectedEventProperty, TraceViewerContainer, } from 'org_xprof/frontend/app/components/trace_viewer_container/trace_viewer_container'; +import {TraceData as MainTraceData, traceViewerV2Main, TraceViewerV2Module} from 'org_xprof/frontend/app/components/trace_viewer_v2/main'; import {SOURCE_CODE_SERVICE_INTERFACE_TOKEN} from 'org_xprof/frontend/app/services/source_code_service/source_code_service_interface'; import {getHostsState} from 'org_xprof/frontend/app/store/selectors'; import {combineLatest, ReplaySubject} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; +interface TraceData { + traceEvents?: Array<{[key: string]: unknown}>; + [key: string]: unknown; +} + +/** + * The name of the event selected custom event, dispatched from WASM in Trace + * Viewer v2. + */ +export const EVENT_SELECTED_EVENT_NAME = 'eventselected'; + /** A trace viewer component. */ @Component({ standalone: false, @@ -17,9 +31,10 @@ import {takeUntil} from 'rxjs/operators'; templateUrl: './trace_viewer.ng.html', styleUrls: ['./trace_viewer.css'] }) -export class TraceViewer implements OnDestroy { +export class TraceViewer implements OnInit, AfterViewInit, OnDestroy { /** Handles on-destroy Subject, used to unsubscribe. */ private readonly destroyed = new ReplaySubject(1); + private navigationEvent: NavigationEvent = {}; private readonly injector = inject(Injector); private readonly store = inject(Store<{}>); @@ -27,8 +42,20 @@ export class TraceViewer implements OnDestroy { pathPrefix = ''; sourceCodeServiceIsAvailable = false; hostList: string[] = []; + useTraceViewerV2 = true; + traceViewerModule: TraceViewerV2Module|null = null; + selectedEvent: SelectedEvent|null = null; + selectedEventProperties: SelectedEventProperty[] = []; + eventDetailColumns: string[] = ['property', 'value']; + private readonly eventArgsCache = new Map(); + private queryString = ''; + searching = false; + + @ViewChild(TraceViewerContainer, {static: false}) + container?: TraceViewerContainer; constructor( + private readonly httpClient: HttpClient, platformLocation: PlatformLocation, route: ActivatedRoute, ) { @@ -44,7 +71,8 @@ export class TraceViewer implements OnDestroy { this.hostList = hostsMetadata.map((host: HostMetadata) => host.hostname); } - this.update(params as NavigationEvent); + this.navigationEvent = {...params, ...queryParams}; + this.update(this.navigationEvent); }); // We don't need the source code service to be persistently available. @@ -59,36 +87,63 @@ export class TraceViewer implements OnDestroy { }); } + ngOnInit() { + // Event listeners are handled by TraceViewerContainer. + } + + ngAfterViewInit() { + if (this.useTraceViewerV2) { + // Use setTimeout to defer initialization to the next tick to avoid + // ExpressionChangedAfterItHasBeenCheckedError when setting 'loading' + // state. + setTimeout(() => { + this.initializeWasmApp(); + }); + } + } + + async initializeWasmApp() { + this.traceViewerModule = await traceViewerV2Main(); + this.update(this.navigationEvent); + } + update(event: NavigationEvent) { const isStreaming = (event.tag === 'trace_viewer@'); const run = event.run || ''; const tag = event.tag || ''; const runPath = event.run_path || ''; const sessionPath = event.session_path || ''; - let queryString = `run=${run}&tag=${tag}`; + this.queryString = `run=${run}&tag=${tag}`; if (sessionPath) { - queryString += `&session_path=${sessionPath}`; + this.queryString += `&session_path=${sessionPath}`; } else if (runPath) { - queryString += `&run_path=${runPath}`; + this.queryString += `&run_path=${runPath}`; } if (event.hosts && typeof event.hosts === 'string') { // Since event.hosts is a comma-separated string, we can use it directly. - queryString += `&hosts=${event.hosts}`; + this.queryString += `&hosts=${event.hosts}`; } else if (event.host) { - queryString += `&host=${event.host}`; + this.queryString += `&host=${event.host}`; } else { - queryString += + this.queryString += `&host=${this.hostList.length > 0 ? this.hostList[0] : ''}`; } - const traceDataUrl = `${this.pathPrefix}${DATA_API}?${queryString}`; - this.url = `${this.pathPrefix}${API_PREFIX}${ - PLUGIN_NAME}/trace_viewer_index.html?is_streaming=${ - isStreaming}&is_oss=true&trace_data_url=${ - encodeURIComponent(traceDataUrl)}&source_code_service=${ - this.sourceCodeServiceIsAvailable}`; + const traceDataUrl = `${this.pathPrefix}${DATA_API}?${this.queryString}`; + + if (this.useTraceViewerV2) { + if (this.traceViewerModule && this.traceViewerModule.loadJsonData) { + this.traceViewerModule.loadJsonData(traceDataUrl); + } + } else { + this.url = `${this.pathPrefix}${API_PREFIX}${ + PLUGIN_NAME}/trace_viewer_index.html?is_streaming=${ + isStreaming}&is_oss=true&trace_data_url=${ + encodeURIComponent(traceDataUrl)}&source_code_service=${ + this.sourceCodeServiceIsAvailable}`; + } } ngOnDestroy() { @@ -96,4 +151,105 @@ export class TraceViewer implements OnDestroy { this.destroyed.next(); this.destroyed.complete(); } + + onEventSelected(event: EntrySelectedEventDetail|null) { + if (!event) { + this.selectedEvent = null; + this.selectedEventProperties = []; + return; + } + + this.selectedEvent = { + name: event.name, + startUsFormatted: event.startUsFormatted, + durationUsFormatted: event.durationUsFormatted, + }; + + const properties: SelectedEventProperty[] = []; + properties.push({property: 'Name', value: event.name}); + properties.push({property: 'Start Time', value: event.startUsFormatted}); + properties.push({property: 'Duration', value: event.durationUsFormatted}); + if (event.hloModuleName) { + properties.push({property: 'HLO Module', value: event.hloModuleName}); + } + if (event.hloOpName) { + properties.push({property: 'HLO Op', value: event.hloOpName}); + } + this.selectedEventProperties = properties; + + if (event.uid) { + this.maybeFetchEventArgs( + event.name, event.startUs, event.durationUs, event.uid); + } + } + + onSearchEvents(query: string) { + if (!query) { + this.traceViewerModule?.setSearchResultsInWasm({traceEvents: []}); + this.container?.updateSearchResultCountText(); + return; + } + this.searching = true; + const searchParams = new URLSearchParams(this.queryString); + searchParams.set('search_prefix', query); + this.httpClient + .get( + `${this.pathPrefix}${DATA_API}?${searchParams.toString()}`) + .pipe(takeUntil(this.destroyed)) + .subscribe((data) => { + this.searching = false; + if (this.traceViewerModule && data) { + this.traceViewerModule.setSearchResultsInWasm({ + ...data, + traceEvents: data.traceEvents || [], + } as MainTraceData); + this.container?.updateSearchResultCountText(); + } + }); + } + + private maybeFetchEventArgs( + name: string, startUs: number, durationUs: number, uid: string) { + const key = `${name}-${startUs}-${durationUs}`; + if (this.eventArgsCache.has(key)) { + if (this.selectedEvent) { + this.addArgsToSelectedEvent(this.eventArgsCache.get(key)!); + } + return; + } + + const params = new URLSearchParams(this.queryString); + params.set('event_name', name); + params.set('start_time_ms', (startUs / 1000).toString()); + params.set('duration_ms', (durationUs / 1000).toString()); + params.set('unique_id', Math.floor(Number(uid)).toString()); + + this.httpClient + .get(`${this.pathPrefix}${DATA_API}?${params.toString()}`) + .pipe(takeUntil(this.destroyed)) + .subscribe((data) => { + const traceData = data as TraceData; + if (!traceData || !traceData.traceEvents || + traceData.traceEvents.length === 0) { + return; + } + const lastEvent = + traceData.traceEvents[traceData.traceEvents.length - 1]; + if (lastEvent['ph'] === 'X' && this.selectedEvent && + lastEvent['args']) { + const args = lastEvent['args'] as {[key: string]: string}; + this.eventArgsCache.set(key, args); + this.addArgsToSelectedEvent(args); + } + }); + } + + private addArgsToSelectedEvent(args: {[key: string]: string}) { + if (!this.selectedEvent) return; + const properties = [...this.selectedEventProperties]; + for (const key of Object.keys(args)) { + properties.push({property: key, value: args[key]}); + } + this.selectedEventProperties = properties; + } } diff --git a/frontend/app/components/trace_viewer/trace_viewer_module.ts b/frontend/app/components/trace_viewer/trace_viewer_module.ts index 41291bab..a72ab9b7 100644 --- a/frontend/app/components/trace_viewer/trace_viewer_module.ts +++ b/frontend/app/components/trace_viewer/trace_viewer_module.ts @@ -1,7 +1,9 @@ import {CommonModule} from '@angular/common'; +import {HttpClientModule} from '@angular/common/http'; import {NgModule} from '@angular/core'; import {MatIconModule} from '@angular/material/icon'; import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {TraceViewerContainer} from 'org_xprof/frontend/app/components/trace_viewer_container/trace_viewer_container'; import {PipesModule} from 'org_xprof/frontend/app/pipes/pipes_module'; import {TraceViewer} from './trace_viewer'; @@ -11,9 +13,11 @@ import {TraceViewer} from './trace_viewer'; declarations: [TraceViewer], imports: [ CommonModule, + HttpClientModule, MatIconModule, MatProgressBarModule, PipesModule, + TraceViewerContainer, ], exports: [TraceViewer] }) 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 cad69748..49a588ea 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts @@ -8,7 +8,7 @@ import {MatInputModule} from '@angular/material/input'; import {MatProgressBarModule} from '@angular/material/progress-bar'; 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 {isSearchEventsEvent, LOADING_STATUS_UPDATE_EVENT_NAME, SEARCH_EVENTS_EVENT_NAME, 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'; @@ -133,8 +133,8 @@ export class TraceViewerContainer implements OnInit, OnDestroy, AfterViewInit { @Input() selectedEventProperties: SelectedEventProperty[] = []; @Input() eventDetailColumns: string[] = []; @Output() - readonly eventSelected = new EventEmitter(); - @Output() readonly searchEvents = new EventEmitter(); + readonly eventSelected = new EventEmitter(); + @Output() readonly searchEvents = new EventEmitter(); @ViewChild('tvIframe') tvIframe?: ElementRef; @@ -262,14 +262,18 @@ export class TraceViewerContainer implements OnInit, OnDestroy, AfterViewInit { if (!isEntrySelectedEvent(e)) { return; } - this.eventSelected.emit(e.detail); + if (e.detail.eventIndex === -1) { + this.eventSelected.emit(null); + } else { + this.eventSelected.emit(e.detail); + } }; private readonly searchEventsEventListener = (e: Event) => { if (!isSearchEventsEvent(e)) { return; } - this.searchEvents.emit(e.detail); + this.searchEvents.emit(e.detail.events_query); }; /** @@ -332,6 +336,7 @@ export class TraceViewerContainer implements OnInit, OnDestroy, AfterViewInit { onSearchEvent(query: string) { this.search$.next(query); + this.searchEvents.emit(query); } nextSearchResult() { diff --git a/frontend/app/components/trace_viewer_v2/BUILD b/frontend/app/components/trace_viewer_v2/BUILD index 542e8e94..98046744 100644 --- a/frontend/app/components/trace_viewer_v2/BUILD +++ b/frontend/app/components/trace_viewer_v2/BUILD @@ -126,6 +126,7 @@ wasm_cc_library( BINARY_LINKOPTS = [ "-sASYNCIFY", "-sMODULARIZE", + "-sMINIMAL_RUNTIME=0", "-sENVIRONMENT=web", "-sALLOW_MEMORY_GROWTH", "-sEXPORT_NAME=loadWasmTraceViewerModule", @@ -188,7 +189,6 @@ filegroup( srcs = [ "index.html", ":app_bundle.js", - ":trace_viewer_v2_wasm/trace_viewer_v2.js", - ":trace_viewer_v2_wasm/trace_viewer_v2.wasm", + ":trace_viewer_v2_wasm", ], ) diff --git a/frontend/app/components/trace_viewer_v2/main.ts b/frontend/app/components/trace_viewer_v2/main.ts index 6fcf8b23..c41290ba 100644 --- a/frontend/app/components/trace_viewer_v2/main.ts +++ b/frontend/app/components/trace_viewer_v2/main.ts @@ -209,7 +209,38 @@ async function loadAndStartWasm( return traceviewerModule; } +async function ensureWasmModuleIsLoaded(): Promise { + // tslint:disable-next-line:no-any + if (typeof (window as any).loadWasmTraceViewerModule !== 'undefined') { + return; + } + return new Promise((resolve, reject) => { + const existingScript = document.querySelector( + 'script[src*="trace_viewer_v2.js"]', + ); + if (existingScript) { + existingScript.addEventListener('load', () => { + resolve(); + }); + existingScript.addEventListener('error', () => { + reject(new Error('Failed to load WASM module.')); + }); + return; + } + const script = document.createElement('script'); + script.src = 'trace_viewer_v2.js'; + script.onload = () => { + resolve(); + }; + script.onerror = () => { + reject(new Error('Failed to load WASM module.')); + }; + document.body.appendChild(script); + }); +} + async function initGpuAndStartWasmApp(): Promise { + await ensureWasmModuleIsLoaded(); const canvas = document.querySelector('#canvas') as HTMLCanvasElement; if (!canvas) { throw new Error('Could not find canvas element with id="canvas"'); diff --git a/plugin/xprof/profile_plugin.py b/plugin/xprof/profile_plugin.py index 25ca4f07..a74776e9 100644 --- a/plugin/xprof/profile_plugin.py +++ b/plugin/xprof/profile_plugin.py @@ -76,6 +76,8 @@ MATERIALICONS_WOFF2_ROUTE = '/materialicons.woff2' TRACE_VIEWER_INDEX_HTML_ROUTE = '/trace_viewer_index.html' TRACE_VIEWER_INDEX_JS_ROUTE = '/trace_viewer_index.js' +TRACE_VIEWER_V2_JS_ROUTE = '/trace_viewer_v2.js' +TRACE_VIEWER_V2_WASM_ROUTE = '/trace_viewer_v2.wasm' ZONE_JS_ROUTE = '/zone.js' DATA_ROUTE = '/data' DATA_CSV_ROUTE = '/data_csv' @@ -895,6 +897,8 @@ def get_plugin_apps( MATERIALICONS_WOFF2_ROUTE: self.static_file_route, TRACE_VIEWER_INDEX_HTML_ROUTE: self.static_file_route, TRACE_VIEWER_INDEX_JS_ROUTE: self.static_file_route, + TRACE_VIEWER_V2_JS_ROUTE: self.static_file_route, + TRACE_VIEWER_V2_WASM_ROUTE: self.static_file_route, ZONE_JS_ROUTE: self.static_file_route, RUNS_ROUTE: self.runs_route, RUN_TOOLS_ROUTE: self.run_tools_route, @@ -963,6 +967,8 @@ def static_file_route(self, request: wrappers.Request) -> wrappers.Response: mimetype = 'text/css' elif extention == '.js': mimetype = 'application/javascript' + elif extention == '.wasm': + mimetype = 'application/wasm' else: mimetype = 'application/octet-stream' try: