1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {DisposableStack} from '../base/disposable_stack'; 16import {createStore, Migrate, Store} from '../base/store'; 17import {TimelineImpl} from './timeline'; 18import {Command} from '../public/command'; 19import {Trace} from '../public/trace'; 20import {ScrollToArgs, setScrollToFunction} from '../public/scroll_helper'; 21import {TrackDescriptor} from '../public/track'; 22import {EngineBase, EngineProxy} from '../trace_processor/engine'; 23import {CommandManagerImpl} from './command_manager'; 24import {NoteManagerImpl} from './note_manager'; 25import {OmniboxManagerImpl} from './omnibox_manager'; 26import {SearchManagerImpl} from './search_manager'; 27import {SelectionManagerImpl} from './selection_manager'; 28import {SidebarManagerImpl} from './sidebar_manager'; 29import {TabManagerImpl} from './tab_manager'; 30import {TrackManagerImpl} from './track_manager'; 31import {WorkspaceManagerImpl} from './workspace_manager'; 32import {SidebarMenuItem} from '../public/sidebar'; 33import {ScrollHelper} from './scroll_helper'; 34import {Selection, SelectionOpts} from '../public/selection'; 35import {SearchResult} from '../public/search'; 36import {PivotTableManager} from './pivot_table_manager'; 37import {FlowManager} from './flow_manager'; 38import {AppContext, AppImpl} from './app_impl'; 39import {PluginManagerImpl} from './plugin_manager'; 40import {RouteArgs} from '../public/route_schema'; 41import {CORE_PLUGIN_ID} from './plugin_manager'; 42import {Analytics} from '../public/analytics'; 43import {getOrCreate} from '../base/utils'; 44import {fetchWithProgress} from '../base/http_utils'; 45import {TraceInfoImpl} from './trace_info_impl'; 46import {PageHandler, PageManager} from '../public/page'; 47import {createProxy} from '../base/utils'; 48import {PageManagerImpl} from './page_manager'; 49import {FeatureFlagManager, FlagSettings} from '../public/feature_flag'; 50import {featureFlags} from './feature_flags'; 51import {SerializedAppState} from './state_serialization_schema'; 52import {PostedTrace} from './trace_source'; 53import {PerfManager} from './perf_manager'; 54import {EvtSource} from '../base/events'; 55 56/** 57 * Handles the per-trace state of the UI 58 * There is an instance of this class per each trace loaded, and typically 59 * between 0 and 1 instances in total (% brief moments while we swap traces). 60 * 90% of the app state live here, including the Engine. 61 * This is the underlying storage for AppImpl, which instead has one instance 62 * per trace per plugin. 63 */ 64export class TraceContext implements Disposable { 65 private readonly pluginInstances = new Map<string, TraceImpl>(); 66 readonly appCtx: AppContext; 67 readonly engine: EngineBase; 68 readonly omniboxMgr = new OmniboxManagerImpl(); 69 readonly searchMgr: SearchManagerImpl; 70 readonly selectionMgr: SelectionManagerImpl; 71 readonly tabMgr = new TabManagerImpl(); 72 readonly timeline: TimelineImpl; 73 readonly traceInfo: TraceInfoImpl; 74 readonly trackMgr = new TrackManagerImpl(); 75 readonly workspaceMgr = new WorkspaceManagerImpl(); 76 readonly noteMgr = new NoteManagerImpl(); 77 readonly flowMgr: FlowManager; 78 readonly pluginSerializableState = createStore<{[key: string]: {}}>({}); 79 readonly scrollHelper: ScrollHelper; 80 readonly pivotTableMgr; 81 readonly trash = new DisposableStack(); 82 readonly onTraceReady = new EvtSource<void>(); 83 84 // List of errors that were encountered while loading the trace by the TS 85 // code. These are on top of traceInfo.importErrors, which is a summary of 86 // what TraceProcessor reports on the stats table at import time. 87 readonly loadingErrors: string[] = []; 88 89 constructor(gctx: AppContext, engine: EngineBase, traceInfo: TraceInfoImpl) { 90 this.appCtx = gctx; 91 this.engine = engine; 92 this.trash.use(engine); 93 this.traceInfo = traceInfo; 94 this.timeline = new TimelineImpl(traceInfo); 95 96 this.scrollHelper = new ScrollHelper( 97 this.traceInfo, 98 this.timeline, 99 this.workspaceMgr.currentWorkspace, 100 this.trackMgr, 101 ); 102 103 this.selectionMgr = new SelectionManagerImpl( 104 this.engine, 105 this.trackMgr, 106 this.noteMgr, 107 this.scrollHelper, 108 this.onSelectionChange.bind(this), 109 ); 110 111 this.noteMgr.onNoteDeleted = (noteId) => { 112 if ( 113 this.selectionMgr.selection.kind === 'note' && 114 this.selectionMgr.selection.id === noteId 115 ) { 116 this.selectionMgr.clear(); 117 } 118 }; 119 120 this.pivotTableMgr = new PivotTableManager( 121 engine.getProxy('PivotTableManager'), 122 ); 123 124 this.flowMgr = new FlowManager( 125 engine.getProxy('FlowManager'), 126 this.trackMgr, 127 this.selectionMgr, 128 ); 129 130 this.searchMgr = new SearchManagerImpl({ 131 timeline: this.timeline, 132 trackManager: this.trackMgr, 133 engine: this.engine, 134 workspace: this.workspaceMgr.currentWorkspace, 135 onResultStep: this.onResultStep.bind(this), 136 }); 137 } 138 139 // This method wires up changes to selection to side effects on search and 140 // tabs. This is to avoid entangling too many dependencies between managers. 141 private onSelectionChange(selection: Selection, opts: SelectionOpts) { 142 const {clearSearch = true, switchToCurrentSelectionTab = true} = opts; 143 if (clearSearch) { 144 this.searchMgr.reset(); 145 } 146 if (switchToCurrentSelectionTab && selection.kind !== 'empty') { 147 this.tabMgr.showCurrentSelectionTab(); 148 } 149 150 if (selection.kind === 'area') { 151 this.pivotTableMgr.setSelectionArea(selection); 152 } 153 154 this.flowMgr.updateFlows(selection); 155 } 156 157 private onResultStep(searchResult: SearchResult) { 158 this.selectionMgr.selectSearchResult(searchResult); 159 } 160 161 // Gets or creates an instance of TraceImpl backed by the current TraceContext 162 // for the given plugin. 163 forPlugin(pluginId: string) { 164 return getOrCreate(this.pluginInstances, pluginId, () => { 165 const appForPlugin = this.appCtx.forPlugin(pluginId); 166 return new TraceImpl(appForPlugin, this); 167 }); 168 } 169 170 // Called by AppContext.closeCurrentTrace(). 171 [Symbol.dispose]() { 172 this.trash.dispose(); 173 } 174} 175 176/** 177 * This implementation provides the plugin access to trace related resources, 178 * such as the engine and the store. This exists for the whole duration a plugin 179 * is active AND a trace is loaded. 180 * There are N+1 instances of this for each trace, one for each plugin plus one 181 * for the core. 182 */ 183export class TraceImpl implements Trace { 184 private readonly appImpl: AppImpl; 185 private readonly traceCtx: TraceContext; 186 187 // This is not the original Engine base, rather an EngineProxy based on the 188 // same engineBase. 189 private readonly engineProxy: EngineProxy; 190 private readonly trackMgrProxy: TrackManagerImpl; 191 private readonly commandMgrProxy: CommandManagerImpl; 192 private readonly sidebarProxy: SidebarManagerImpl; 193 private readonly pageMgrProxy: PageManagerImpl; 194 195 // This is called by TraceController when loading a new trace, soon after the 196 // engine has been set up. It obtains a new TraceImpl for the core. From that 197 // we can fork sibling instances (i.e. bound to the same TraceContext) for 198 // the various plugins. 199 static createInstanceForCore( 200 appImpl: AppImpl, 201 engine: EngineBase, 202 traceInfo: TraceInfoImpl, 203 ): TraceImpl { 204 const traceCtx = new TraceContext( 205 appImpl.__appCtxForTrace, 206 engine, 207 traceInfo, 208 ); 209 return traceCtx.forPlugin(CORE_PLUGIN_ID); 210 } 211 212 // Only called by TraceContext.forPlugin(). 213 constructor(appImpl: AppImpl, ctx: TraceContext) { 214 const pluginId = appImpl.pluginId; 215 this.appImpl = appImpl; 216 this.traceCtx = ctx; 217 const traceUnloadTrash = ctx.trash; 218 219 // Invalidate all the engine proxies when the TraceContext is destroyed. 220 this.engineProxy = ctx.engine.getProxy(pluginId); 221 traceUnloadTrash.use(this.engineProxy); 222 223 // Intercept the registerTrack() method to inject the pluginId into tracks. 224 this.trackMgrProxy = createProxy(ctx.trackMgr, { 225 registerTrack(trackDesc: TrackDescriptor): Disposable { 226 return ctx.trackMgr.registerTrack({...trackDesc, pluginId}); 227 }, 228 }); 229 230 // CommandManager is global. Here we intercept the registerCommand() because 231 // we want any commands registered via the Trace interface to be 232 // unregistered when the trace unloads (before a new trace is loaded) to 233 // avoid ending up with duplicate commands. 234 this.commandMgrProxy = createProxy(ctx.appCtx.commandMgr, { 235 registerCommand(cmd: Command): Disposable { 236 const disposable = appImpl.commands.registerCommand(cmd); 237 traceUnloadTrash.use(disposable); 238 return disposable; 239 }, 240 }); 241 242 // Likewise, remove all trace-scoped sidebar entries when the trace unloads. 243 this.sidebarProxy = createProxy(ctx.appCtx.sidebarMgr, { 244 addMenuItem(menuItem: SidebarMenuItem): Disposable { 245 const disposable = appImpl.sidebar.addMenuItem(menuItem); 246 traceUnloadTrash.use(disposable); 247 return disposable; 248 }, 249 }); 250 251 this.pageMgrProxy = createProxy(ctx.appCtx.pageMgr, { 252 registerPage(pageHandler: PageHandler): Disposable { 253 const disposable = appImpl.pages.registerPage({ 254 ...pageHandler, 255 pluginId: appImpl.pluginId, 256 }); 257 traceUnloadTrash.use(disposable); 258 return disposable; 259 }, 260 }); 261 262 // TODO(primiano): remove this injection once we plumb Trace everywhere. 263 setScrollToFunction((x: ScrollToArgs) => ctx.scrollHelper.scrollTo(x)); 264 } 265 266 scrollTo(where: ScrollToArgs): void { 267 this.traceCtx.scrollHelper.scrollTo(where); 268 } 269 270 // Creates an instance of TraceImpl backed by the same TraceContext for 271 // another plugin. This is effectively a way to "fork" the core instance and 272 // create the N instances for plugins. 273 forkForPlugin(pluginId: string) { 274 return this.traceCtx.forPlugin(pluginId); 275 } 276 277 mountStore<T>(migrate: Migrate<T>): Store<T> { 278 return this.traceCtx.pluginSerializableState.createSubStore( 279 [this.pluginId], 280 migrate, 281 ); 282 } 283 284 getPluginStoreForSerialization() { 285 return this.traceCtx.pluginSerializableState; 286 } 287 288 async getTraceFile(): Promise<Blob> { 289 const src = this.traceInfo.source; 290 if (this.traceInfo.downloadable) { 291 if (src.type === 'ARRAY_BUFFER') { 292 return new Blob([src.buffer]); 293 } else if (src.type === 'FILE') { 294 return src.file; 295 } else if (src.type === 'URL') { 296 return await fetchWithProgress(src.url, (progressPercent: number) => 297 this.omnibox.showStatusMessage( 298 `Downloading trace ${progressPercent}%`, 299 ), 300 ); 301 } 302 } 303 // Not available in HTTP+RPC mode. Rather than propagating an undefined, 304 // show a graceful error (the ERR:trace_src will be intercepted by 305 // error_dialog.ts). We expect all users of this feature to not be able to 306 // do anything useful if we returned undefined (other than showing the same 307 // dialog). 308 // The caller was supposed to check that traceInfo.downloadable === true 309 // before calling this. Throwing while downloadable is true is a bug. 310 throw new Error(`Cannot getTraceFile(${src.type})`); 311 } 312 313 get openerPluginArgs(): {[key: string]: unknown} | undefined { 314 const traceSource = this.traceCtx.traceInfo.source; 315 if (traceSource.type !== 'ARRAY_BUFFER') { 316 return undefined; 317 } 318 const pluginArgs = traceSource.pluginArgs; 319 return (pluginArgs ?? {})[this.pluginId]; 320 } 321 322 get trace() { 323 return this; 324 } 325 326 get engine() { 327 return this.engineProxy; 328 } 329 330 get timeline() { 331 return this.traceCtx.timeline; 332 } 333 334 get tracks() { 335 return this.trackMgrProxy; 336 } 337 338 get tabs() { 339 return this.traceCtx.tabMgr; 340 } 341 342 get workspace() { 343 return this.traceCtx.workspaceMgr.currentWorkspace; 344 } 345 346 get workspaces() { 347 return this.traceCtx.workspaceMgr; 348 } 349 350 get search() { 351 return this.traceCtx.searchMgr; 352 } 353 354 get selection() { 355 return this.traceCtx.selectionMgr; 356 } 357 358 get traceInfo(): TraceInfoImpl { 359 return this.traceCtx.traceInfo; 360 } 361 362 get notes() { 363 return this.traceCtx.noteMgr; 364 } 365 366 get pivotTable() { 367 return this.traceCtx.pivotTableMgr; 368 } 369 370 get flows() { 371 return this.traceCtx.flowMgr; 372 } 373 374 get loadingErrors(): ReadonlyArray<string> { 375 return this.traceCtx.loadingErrors; 376 } 377 378 addLoadingError(err: string) { 379 this.traceCtx.loadingErrors.push(err); 380 } 381 382 // App interface implementation. 383 384 get pluginId(): string { 385 return this.appImpl.pluginId; 386 } 387 388 get commands(): CommandManagerImpl { 389 return this.commandMgrProxy; 390 } 391 392 get sidebar(): SidebarManagerImpl { 393 return this.sidebarProxy; 394 } 395 396 get pages(): PageManager { 397 return this.pageMgrProxy; 398 } 399 400 get omnibox(): OmniboxManagerImpl { 401 return this.appImpl.omnibox; 402 } 403 404 get plugins(): PluginManagerImpl { 405 return this.appImpl.plugins; 406 } 407 408 get analytics(): Analytics { 409 return this.appImpl.analytics; 410 } 411 412 get initialRouteArgs(): RouteArgs { 413 return this.appImpl.initialRouteArgs; 414 } 415 416 get featureFlags(): FeatureFlagManager { 417 return { 418 register: (settings: FlagSettings) => featureFlags.register(settings), 419 }; 420 } 421 422 scheduleFullRedraw(): void { 423 this.appImpl.scheduleFullRedraw(); 424 } 425 426 navigate(newHash: string): void { 427 this.appImpl.navigate(newHash); 428 } 429 430 openTraceFromFile(file: File): void { 431 this.appImpl.openTraceFromFile(file); 432 } 433 434 openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) { 435 this.appImpl.openTraceFromUrl(url, serializedAppState); 436 } 437 438 openTraceFromBuffer(args: PostedTrace): void { 439 this.appImpl.openTraceFromBuffer(args); 440 } 441 442 get onTraceReady() { 443 return this.traceCtx.onTraceReady; 444 } 445 446 get perfDebugging(): PerfManager { 447 return this.appImpl.perfDebugging; 448 } 449 450 get trash(): DisposableStack { 451 return this.traceCtx.trash; 452 } 453 454 // Nothing other than AppImpl should ever refer to this, hence the __ name. 455 get __traceCtxForApp() { 456 return this.traceCtx; 457 } 458} 459 460// A convenience interface to inject the App in Mithril components. 461export interface TraceImplAttrs { 462 trace: TraceImpl; 463} 464 465export interface OptionalTraceImplAttrs { 466 trace?: TraceImpl; 467} 468