diff --git a/frontend/app/BUILD b/frontend/app/BUILD index 1a89128d3..7b9f10623 100644 --- a/frontend/app/BUILD +++ b/frontend/app/BUILD @@ -15,6 +15,7 @@ xprof_ng_module( ], deps = [ "@npm//@angular/core", + "@npm//@angular/forms", "@npm//@angular/platform-browser", "@npm//@ngrx/store", "@npm//rxjs", diff --git a/frontend/app/app.ts b/frontend/app/app.ts index 921bc8fb5..37e75c7f9 100644 --- a/frontend/app/app.ts +++ b/frontend/app/app.ts @@ -40,11 +40,31 @@ export class App implements OnInit { this.loading = false; return; } + + let filteredRuns = runs; + + const config = await firstValueFrom(this.dataService.getConfig()); + if (config?.filterSessions) { + try { + const regex = new RegExp(config.filterSessions, 'i'); + filteredRuns = runs.filter(run => regex.test(run)); + } catch (e) { + console.error('Invalid regex for session filter:', e); + // fallback to no filtering if regex is invalid + filteredRuns = runs; + } + + // fallback to no filtering if regex finds nothing + if (filteredRuns.length === 0) { + filteredRuns = runs; + } + } + this.dataFound = true; - this.store.dispatch(actions.setCurrentRunAction({currentRun: runs[0]})); + this.store.dispatch(actions.setCurrentRunAction({currentRun: filteredRuns[0]})); const tools = - await firstValueFrom(this.dataService.getRunTools(runs[0])) as string[]; - const runToolsMap: RunToolsMap = {[runs[0]]: tools}; + await firstValueFrom(this.dataService.getRunTools(filteredRuns[0])) as string[]; + const runToolsMap: RunToolsMap = {[filteredRuns[0]]: tools}; for (let i = 1; i < runs.length; i++) { runToolsMap[runs[i]] = []; } diff --git a/frontend/app/app_module.ts b/frontend/app/app_module.ts index 0028df9fc..9e5e8997b 100644 --- a/frontend/app/app_module.ts +++ b/frontend/app/app_module.ts @@ -1,5 +1,6 @@ import {HttpClientModule} from '@angular/common/http'; import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; import {MatProgressBarModule} from '@angular/material/progress-bar'; import {BrowserModule} from '@angular/platform-browser'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; @@ -21,6 +22,7 @@ import {App} from './app'; imports: [ BrowserModule, HttpClientModule, + FormsModule, MatProgressBarModule, EmptyPageModule, MainPageModule, diff --git a/frontend/app/components/controls/view_architecture/view_architecture.ts b/frontend/app/components/controls/view_architecture/view_architecture.ts index 278a63c40..2d700fa34 100644 --- a/frontend/app/components/controls/view_architecture/view_architecture.ts +++ b/frontend/app/components/controls/view_architecture/view_architecture.ts @@ -20,6 +20,7 @@ export class ViewArchitecture implements OnInit, OnDestroy { private readonly dataService: DataServiceV2Interface = inject(DATA_SERVICE_INTERFACE_TOKEN); hideViewArchitectureButton = true; + filterSessions = ''; private readonly destroyed = new ReplaySubject(1); ngOnInit() { @@ -28,6 +29,7 @@ export class ViewArchitecture implements OnInit, OnDestroy { .subscribe((config) => { this.hideViewArchitectureButton = config?.hideCaptureProfileButton || false; + this.filterSessions = config?.filterSessions || ''; }); } diff --git a/frontend/app/components/sidenav/BUILD b/frontend/app/components/sidenav/BUILD index 0b4d8f590..4a8d98d3e 100644 --- a/frontend/app/components/sidenav/BUILD +++ b/frontend/app/components/sidenav/BUILD @@ -15,6 +15,7 @@ xprof_ng_module( ], deps = [ "@npm//@angular/core", + "@npm//@angular/forms", "@npm//@angular/router", "@npm//@ngrx/store", "@npm//rxjs", @@ -22,7 +23,10 @@ xprof_ng_module( "@org_xprof//frontend/app/common/angular:angular_material_checkbox", "@org_xprof//frontend/app/common/angular:angular_material_core", "@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_select", + "@org_xprof//frontend/app/common/angular:angular_material_tooltip", "@org_xprof//frontend/app/common/constants", "@org_xprof//frontend/app/common/interfaces", "@org_xprof//frontend/app/components/capture_profile", diff --git a/frontend/app/components/sidenav/sidenav.ng.html b/frontend/app/components/sidenav/sidenav.ng.html index 2a3a25d97..09981b424 100644 --- a/frontend/app/components/sidenav/sidenav.ng.html +++ b/frontend/app/components/sidenav/sidenav.ng.html @@ -3,14 +3,40 @@ +
+
+ Filter Sessions + + info + +
+ + + search + + +
+
- Sessions ({{runs.length}}) + Sessions ({{filteredRuns.length}}/{{runs.length}})
- + {{run}} diff --git a/frontend/app/components/sidenav/sidenav.scss b/frontend/app/components/sidenav/sidenav.scss index 6925c9e35..3c5ae1133 100644 --- a/frontend/app/components/sidenav/sidenav.scss +++ b/frontend/app/components/sidenav/sidenav.scss @@ -10,6 +10,17 @@ padding: 20px 20px 0 20px; } +.tooltip-icon { + color: #e8710a; + font-size: 25px; + vertical-align: bottom; + cursor: pointer; +} + +.mat-form-field { + margin-bottom: 10px; +} + .mat-subheading-2 { margin-bottom: 0; } @@ -21,7 +32,6 @@ // Target the custom panel class defined in sidenav.ng.html .multi-host-select-panel { - .full-width { width: 100%; } diff --git a/frontend/app/components/sidenav/sidenav.ts b/frontend/app/components/sidenav/sidenav.ts index bbc36c186..e76c7f2a6 100644 --- a/frontend/app/components/sidenav/sidenav.ts +++ b/frontend/app/components/sidenav/sidenav.ts @@ -27,6 +27,7 @@ export class SideNav implements OnInit, OnDestroy { runToolsMap: RunToolsMap = {}; runs: string[] = []; + filteredRuns: string[] = []; tags: string[] = []; hosts: string[] = []; moduleList: string[] = []; @@ -41,6 +42,7 @@ export class SideNav implements OnInit, OnDestroy { allHostsSelected = false; hideCaptureProfileButton = false; + filterSessions = ''; constructor( private readonly router: Router, @@ -58,6 +60,7 @@ export class SideNav implements OnInit, OnDestroy { this.runToolsMap$.subscribe((runTools: RunToolsMap) => { this.runToolsMap = runTools; this.runs = Object.keys(this.runToolsMap); + this.filterRuns(); }); this.currentRun$.subscribe(run => { if (run && !this.selectedRunInternal) { @@ -66,6 +69,21 @@ export class SideNav implements OnInit, OnDestroy { }); } + filterRuns(): void { + if (!this.filterSessions) { + this.filteredRuns = this.runs; + return; + } + try { + const regex = new RegExp(this.filterSessions, 'i'); + this.filteredRuns = this.runs.filter(run => regex.test(run)); + } catch (e) { + console.error('Invalid regex for session filter:', e); + // fallback to no filtering or previous state if regex is invalid + this.filteredRuns = this.runs; + } + } + get is_hlo_tool() { return HLO_TOOLS.includes(this.selectedTag); } @@ -77,8 +95,9 @@ export class SideNav implements OnInit, OnDestroy { // Getter for valid run given url router or user selection. get selectedRun() { - return this.runs.find(validRun => validRun === this.selectedRunInternal) || - this.runs[0] || ''; + return this.filteredRuns.find( + validRun => validRun === this.selectedRunInternal) || + this.filteredRuns[0] || ''; } // Getter for valid tag given url router or user selection. @@ -165,7 +184,8 @@ export class SideNav implements OnInit, OnDestroy { const config = await firstValueFrom( this.dataService.getConfig().pipe(takeUntil(this.destroyed))); if (config) { - this.hideCaptureProfileButton = config.hideCaptureProfileButton; + this.filterSessions = config.filterSessions; + this.filterRuns(); } } @@ -246,6 +266,12 @@ export class SideNav implements OnInit, OnDestroy { return response.split(','); } + applyFilterAndSelectFirst() { + if (this.filteredRuns.length > 0) { + this.onRunSelectionChange(this.filteredRuns[0]); + } + } + onRunSelectionChange(run: string) { this.selectedRunInternal = run; this.afterUpdateRun(); diff --git a/frontend/app/components/sidenav/sidenav_module.ts b/frontend/app/components/sidenav/sidenav_module.ts index 3c9c7f819..2c6e31da8 100644 --- a/frontend/app/components/sidenav/sidenav_module.ts +++ b/frontend/app/components/sidenav/sidenav_module.ts @@ -1,10 +1,14 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; import {MatButtonModule} from '@angular/material/button'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatOptionModule} from '@angular/material/core'; import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; import {MatSelectModule} from '@angular/material/select'; +import {MatTooltipModule} from '@angular/material/tooltip'; import {CaptureProfileModule} from 'org_xprof/frontend/app/components/capture_profile/capture_profile_module'; import {BufferDetailsModule} from 'org_xprof/frontend/app/components/memory_viewer/buffer_details/buffer_details_module'; import {OpDetailsModule} from 'org_xprof/frontend/app/components/op_profile/op_details/op_details_module'; @@ -18,10 +22,14 @@ import {SideNav} from './sidenav'; imports: [ CommonModule, MatButtonModule, + FormsModule, MatCheckboxModule, MatFormFieldModule, + MatIconModule, + MatInputModule, MatSelectModule, MatOptionModule, + MatTooltipModule, BufferDetailsModule, CaptureProfileModule, OpDetailsModule, diff --git a/frontend/app/services/data_service_v2/data_service_v2_interface.ts b/frontend/app/services/data_service_v2/data_service_v2_interface.ts index 6e582f586..3ccce4ab5 100644 --- a/frontend/app/services/data_service_v2/data_service_v2_interface.ts +++ b/frontend/app/services/data_service_v2/data_service_v2_interface.ts @@ -14,6 +14,7 @@ import {type SmartSuggestionReport} from 'org_xprof/frontend/app/common/interfac /** A serializable object with profiler configuration details. */ export interface ProfilerConfig { hideCaptureProfileButton: boolean; + filterSessions: string; } /** The data service class that calls API and return response. */ diff --git a/plugin/xprof/profile_plugin.py b/plugin/xprof/profile_plugin.py index 0de22d282..87178b715 100644 --- a/plugin/xprof/profile_plugin.py +++ b/plugin/xprof/profile_plugin.py @@ -474,6 +474,7 @@ def __init__(self, context): self.hide_capture_profile_button = getattr( context, 'hide_capture_profile_button', False ) + self.filter_sessions = getattr(context, 'filter_sessions', '') # Whether the plugin is active. This is an expensive computation, so we # compute this asynchronously and cache positive results indefinitely. @@ -535,6 +536,7 @@ def config_route(self, request: wrappers.Request) -> wrappers.Response: logger.info('config_route: %s', self.logdir) config_data = { 'hideCaptureProfileButton': self.hide_capture_profile_button, + 'filterSessions': self.filter_sessions, } return respond(config_data, 'application/json') diff --git a/plugin/xprof/server.py b/plugin/xprof/server.py index 46e7e796a..191f7f720 100644 --- a/plugin/xprof/server.py +++ b/plugin/xprof/server.py @@ -50,6 +50,7 @@ class ServerConfig: grpc_port: int worker_service_address: str hide_capture_profile_button: bool + filter_sessions: str def make_wsgi_app(plugin): @@ -147,6 +148,7 @@ def _launch_server( config.logdir, DataProvider(config.logdir), TBContext.Flags(False) ) context.hide_capture_profile_button = config.hide_capture_profile_button + context.filter_sessions = config.filter_sessions loader = ProfilePluginLoader() plugin = loader.load(context) run_server(plugin, _get_wildcard_address(config.port), config.port) @@ -254,6 +256,15 @@ def _create_argument_parser() -> argparse.ArgumentParser: " main server port (--port)." ), ) + + parser.add_argument( + "-fs", + "--filter_sessions", + type=str, + default="", + help="A regex to filter the available sessions (defaults to no filter).", + ) + return parser @@ -289,6 +300,7 @@ def main() -> int: grpc_port=args.grpc_port, worker_service_address=worker_service_address, hide_capture_profile_button=args.hide_capture_profile_button, + filter_sessions=args.filter_sessions, ) print("Attempting to start XProf server:") @@ -296,6 +308,7 @@ def main() -> int: print(f" Port: {config.port}") print(f" Worker Service Address: {config.worker_service_address}") print(f" Hide Capture Button: {config.hide_capture_profile_button}") + print(f" Filter Sessions: {config.filter_sessions}") if logdir and not epath.Path(logdir).exists(): print(