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",
- },
-)