From e91142afe2acbb90d27cb2335632de376c278f73 Mon Sep 17 00:00:00 2001 From: Profiler Team Date: Mon, 2 Feb 2026 16:47:03 -0800 Subject: [PATCH] Refactor: Extract Trace Viewer rendering and loading logic into TraceViewerContainer PiperOrigin-RevId: 864578402 --- frontend/app/components/trace_viewer/BUILD | 19 +- .../trace_viewer/trace_viewer_container.css | 5 + .../trace_viewer_container.ng.html | 40 ++++ .../trace_viewer/trace_viewer_container.scss | 94 ++++++++ .../trace_viewer/trace_viewer_container.ts | 208 ++++++++++++++++++ .../trace_viewer/trace_viewer_module.ts | 23 +- frontend/app/components/trace_viewer_v2/BUILD | 76 +------ 7 files changed, 385 insertions(+), 80 deletions(-) create mode 100644 frontend/app/components/trace_viewer/trace_viewer_container.css create mode 100644 frontend/app/components/trace_viewer/trace_viewer_container.ng.html create mode 100644 frontend/app/components/trace_viewer/trace_viewer_container.scss create mode 100644 frontend/app/components/trace_viewer/trace_viewer_container.ts diff --git a/frontend/app/components/trace_viewer/BUILD b/frontend/app/components/trace_viewer/BUILD index f23dacdc..51ae73a3 100644 --- a/frontend/app/components/trace_viewer/BUILD +++ b/frontend/app/components/trace_viewer/BUILD @@ -7,11 +7,14 @@ 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", @@ -19,11 +22,16 @@ xprof_ng_module( "@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", ], @@ -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, +) diff --git a/frontend/app/components/trace_viewer/trace_viewer_container.css b/frontend/app/components/trace_viewer/trace_viewer_container.css new file mode 100644 index 00000000..d4b23470 --- /dev/null +++ b/frontend/app/components/trace_viewer/trace_viewer_container.css @@ -0,0 +1,5 @@ +: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..4d798e77 --- /dev/null +++ b/frontend/app/components/trace_viewer/trace_viewer_container.ng.html @@ -0,0 +1,40 @@ +
+ + + +
{{searchResultCountText}}
+ + +
+ +
+ error +
{{'Error, please use the old frontend. ' + (traceViewerV2ErrorMessage ?? '')}}
+
+ +
+ +
{{traceViewerV2LoadingStatus}}
+
{{tutorials[currentTutorialIndex]}}
+
+
+
Loading more data...
+ +
+
+ --> + + --> +
+
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..f7076312 --- /dev/null +++ b/frontend/app/components/trace_viewer/trace_viewer_container.scss @@ -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; +} 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/trace_viewer_module.ts b/frontend/app/components/trace_viewer/trace_viewer_module.ts index ef5e050c..f5e0ffb7 100644 --- a/frontend/app/components/trace_viewer/trace_viewer_module.ts +++ b/frontend/app/components/trace_viewer/trace_viewer_module.ts @@ -1,13 +1,30 @@ +import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +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 {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {PipesModule} from 'org_xprof/frontend/app/pipes/pipes_module'; import {TraceViewer} from './trace_viewer'; +import {TraceViewerContainer} from './trace_viewer_container'; /** A trace viewer module. */ @NgModule({ - declarations: [TraceViewer], - imports: [PipesModule], - exports: [TraceViewer] + declarations: [TraceViewer, TraceViewerContainer], + imports: [ + CommonModule, + MatButtonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatProgressBarModule, + MatProgressSpinnerModule, + PipesModule, + ], + exports: [TraceViewer, TraceViewerContainer] }) export class TraceViewerModule { } diff --git a/frontend/app/components/trace_viewer_v2/BUILD b/frontend/app/components/trace_viewer_v2/BUILD index 1f72282c..df56c3ce 100644 --- a/frontend/app/components/trace_viewer_v2/BUILD +++ b/frontend/app/components/trace_viewer_v2/BUILD @@ -1,28 +1,11 @@ -load("//javascript/tools/jscompiler/builddefs:flags.bzl", "RECOMMENDED_FLAGS") -load("//javascript/typescript:build_defs.bzl", "ts_declaration", "ts_development_sources", "ts_library") - # load("//third_party/bazel_rules/rules_cc/cc:cc_binary.bzl", "cc_binary") # load("//third_party/bazel_rules/rules_cc/cc:cc_library.bzl", "cc_library") # load("//third_party/bazel_rules/rules_cc/cc:cc_test.bzl", "cc_test") -# load("//third_party/bazel_rules/rules_wasm/wasm:defs.bzl", "wasm_cc_binary") -load("//tools/build_defs/js:rules.bzl", "js_binary") -load("//tools/build_defs/js/devserver:web_dev_server.bzl", "web_dev_server") -load("//tools/build_defs/testing:bzl_library.bzl", "bzl_library") + load(":build_defs.bzl", "wasm_cc_library", "wasm_cc_test") package(default_visibility = ["//third_party/xprof/frontend:__subpackages__"]) -bzl_library( - name = "build_defs_bzl", - srcs = ["build_defs.bzl"], - parse_tests = False, - visibility = ["//visibility:private"], - deps = [ - "//third_party/bazel_rules/rules_cc/cc:core_rules", - "//third_party/bazel_rules/rules_wasm/wasm:bzl_lib", - ], -) - cc_library( name = "animation", hdrs = ["animation.h"], @@ -182,60 +165,3 @@ cc_binary( "@org_xprof//frontend/app/components/trace_viewer_v2/trace_helper:trace_event_parser", ], ) - -wasm_cc_binary( - name = "trace_viewer_v2_wasm", - cc_target = ":trace_viewer_v2", - generate_ts_defs = "embind", -) - -ts_declaration( - name = "wasm_declared_types", - srcs = [":trace_viewer_v2_wasm/trace_viewer_v2.d.ts"], - deps = ["//third_party/javascript/typings/emscripten"], -) - -ts_library( - name = "app_bundle_ts", - srcs = ["main.ts"], - deps = [ - ":wasm_declared_types", - "//third_party/javascript/webgpu_types", - ], -) - -ts_development_sources( - name = "devsrcs", - deps = [":app_bundle_ts"], -) - -JS_COMPILER_FLAGS = [] - -js_binary( - name = "app_bundle", - defs = JS_COMPILER_FLAGS + RECOMMENDED_FLAGS, - deps = [":app_bundle_ts"], -) - -filegroup( - name = "static_files", - srcs = [ - "index.html", - ":app_bundle.js", - ":trace_viewer_v2_wasm/trace_viewer_v2.js", - ":trace_viewer_v2_wasm/trace_viewer_v2.wasm", - ], -) - -web_dev_server( - name = "dev_server", - concatjs_routes = {":devsrcs": "/app_bundle.js"}, - static_files = [ - "index.html", - ":trace_viewer_v2_wasm", - ], - static_files_path_aliases = { - "/trace_viewer_v2.js": "third_party/xprof/frontend/app/components/trace_viewer_v2/trace_viewer_v2_wasm/trace_viewer_v2.js", - "/trace_viewer_v2.wasm": "third_party/xprof/frontend/app/components/trace_viewer_v2/trace_viewer_v2_wasm/trace_viewer_v2.wasm", - }, -)