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