1// Copyright (C) 2022 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 {v4 as uuidv4} from 'uuid'; 16 17import {Disposable, DisposableStack} from '../base/disposable'; 18import {Registry} from '../base/registry'; 19import {Span, duration, time} from '../base/time'; 20import {TraceContext, globals} from '../frontend/globals'; 21import { 22 Command, 23 LegacyDetailsPanel, 24 MetricVisualisation, 25 Migrate, 26 Plugin, 27 PluginContext, 28 PluginContextTrace, 29 PluginDescriptor, 30 PrimaryTrackSortKey, 31 Store, 32 TabDescriptor, 33 TrackDescriptor, 34 TrackPredicate, 35 GroupPredicate, 36 TrackRef, 37} from '../public'; 38import {EngineBase, Engine} from '../trace_processor/engine'; 39 40import {Actions} from './actions'; 41import {SCROLLING_TRACK_GROUP} from './state'; 42import {addQueryResultsTab} from '../frontend/query_result_tab'; 43import {Flag, featureFlags} from '../core/feature_flags'; 44import {assertExists} from '../base/logging'; 45import {raf} from '../core/raf_scheduler'; 46import {defaultPlugins} from '../core/default_plugins'; 47import {HighPrecisionTimeSpan} from './high_precision_time'; 48import {PromptOption} from '../frontend/omnibox_manager'; 49 50// Every plugin gets its own PluginContext. This is how we keep track 51// what each plugin is doing and how we can blame issues on particular 52// plugins. 53// The PluginContext exists for the whole duration a plugin is active. 54export class PluginContextImpl implements PluginContext, Disposable { 55 private trash = new DisposableStack(); 56 private alive = true; 57 58 readonly sidebar = { 59 hide() { 60 globals.dispatch( 61 Actions.setSidebar({ 62 visible: false, 63 }), 64 ); 65 }, 66 show() { 67 globals.dispatch( 68 Actions.setSidebar({ 69 visible: true, 70 }), 71 ); 72 }, 73 isVisible() { 74 return globals.state.sidebarVisible; 75 }, 76 }; 77 78 registerCommand(cmd: Command): void { 79 // Silently ignore if context is dead. 80 if (!this.alive) return; 81 82 const disposable = globals.commandManager.registerCommand(cmd); 83 this.trash.use(disposable); 84 } 85 86 // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 runCommand(id: string, ...args: any[]): any { 88 return globals.commandManager.runCommand(id, ...args); 89 } 90 91 constructor(readonly pluginId: string) {} 92 93 dispose(): void { 94 this.trash.dispose(); 95 this.alive = false; 96 } 97} 98 99// This PluginContextTrace implementation provides the plugin access to trace 100// related resources, such as the engine and the store. 101// The PluginContextTrace exists for the whole duration a plugin is active AND a 102// trace is loaded. 103class PluginContextTraceImpl implements PluginContextTrace, Disposable { 104 private trash = new DisposableStack(); 105 private alive = true; 106 readonly engine: Engine; 107 108 constructor(private ctx: PluginContext, engine: EngineBase) { 109 const engineProxy = engine.getProxy(ctx.pluginId); 110 this.trash.use(engineProxy); 111 this.engine = engineProxy; 112 } 113 114 registerCommand(cmd: Command): void { 115 // Silently ignore if context is dead. 116 if (!this.alive) return; 117 118 const dispose = globals.commandManager.registerCommand(cmd); 119 this.trash.use(dispose); 120 } 121 122 registerTrack(trackDesc: TrackDescriptor): void { 123 // Silently ignore if context is dead. 124 if (!this.alive) return; 125 126 const dispose = globals.trackManager.registerTrack(trackDesc); 127 this.trash.use(dispose); 128 } 129 130 addDefaultTrack(track: TrackRef): void { 131 // Silently ignore if context is dead. 132 if (!this.alive) return; 133 134 const dispose = globals.trackManager.addPotentialTrack(track); 135 this.trash.use(dispose); 136 } 137 138 registerStaticTrack(track: TrackDescriptor & TrackRef): void { 139 this.registerTrack(track); 140 this.addDefaultTrack(track); 141 } 142 143 // eslint-disable-next-line @typescript-eslint/no-explicit-any 144 runCommand(id: string, ...args: any[]): any { 145 return this.ctx.runCommand(id, ...args); 146 } 147 148 registerTab(desc: TabDescriptor): void { 149 if (!this.alive) return; 150 151 const unregister = globals.tabManager.registerTab(desc); 152 this.trash.use(unregister); 153 } 154 155 addDefaultTab(uri: string): void { 156 const remove = globals.tabManager.addDefaultTab(uri); 157 this.trash.use(remove); 158 } 159 160 registerDetailsPanel(detailsPanel: LegacyDetailsPanel): void { 161 if (!this.alive) return; 162 163 const tabMan = globals.tabManager; 164 const unregister = tabMan.registerLegacyDetailsPanel(detailsPanel); 165 this.trash.use(unregister); 166 } 167 168 get sidebar() { 169 return this.ctx.sidebar; 170 } 171 172 readonly tabs = { 173 openQuery: (query: string, title: string) => { 174 addQueryResultsTab({query, title}); 175 }, 176 177 showTab(uri: string): void { 178 globals.dispatch(Actions.showTab({uri})); 179 }, 180 181 hideTab(uri: string): void { 182 globals.dispatch(Actions.hideTab({uri})); 183 }, 184 }; 185 186 get pluginId(): string { 187 return this.ctx.pluginId; 188 } 189 190 readonly timeline = { 191 // Add a new track to the timeline, returning its key. 192 addTrack(uri: string, displayName: string): string { 193 const trackKey = uuidv4(); 194 globals.dispatch( 195 Actions.addTrack({ 196 key: trackKey, 197 uri, 198 name: displayName, 199 trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK, 200 trackGroup: SCROLLING_TRACK_GROUP, 201 }), 202 ); 203 return trackKey; 204 }, 205 206 removeTrack(key: string): void { 207 globals.dispatch(Actions.removeTracks({trackKeys: [key]})); 208 }, 209 210 pinTrack(key: string) { 211 if (!isPinned(key)) { 212 globals.dispatch(Actions.toggleTrackPinned({trackKey: key})); 213 } 214 }, 215 216 unpinTrack(key: string) { 217 if (isPinned(key)) { 218 globals.dispatch(Actions.toggleTrackPinned({trackKey: key})); 219 } 220 }, 221 222 pinTracksByPredicate(predicate: TrackPredicate) { 223 const tracks = Object.values(globals.state.tracks); 224 const groups = globals.state.trackGroups; 225 for (const track of tracks) { 226 const tags = { 227 name: track.name, 228 groupName: (track.trackGroup ? groups[track.trackGroup] : undefined) 229 ?.name, 230 }; 231 if (predicate(tags) && !isPinned(track.key)) { 232 globals.dispatch( 233 Actions.toggleTrackPinned({ 234 trackKey: track.key, 235 }), 236 ); 237 } 238 } 239 }, 240 241 unpinTracksByPredicate(predicate: TrackPredicate) { 242 const tracks = Object.values(globals.state.tracks); 243 for (const track of tracks) { 244 const tags = { 245 name: track.name, 246 }; 247 if (predicate(tags) && isPinned(track.key)) { 248 globals.dispatch( 249 Actions.toggleTrackPinned({ 250 trackKey: track.key, 251 }), 252 ); 253 } 254 } 255 }, 256 257 removeTracksByPredicate(predicate: TrackPredicate) { 258 const trackKeysToRemove = Object.values(globals.state.tracks) 259 .filter((track) => { 260 const tags = { 261 name: track.name, 262 }; 263 return predicate(tags); 264 }) 265 .map((trackState) => trackState.key); 266 267 globals.dispatch(Actions.removeTracks({trackKeys: trackKeysToRemove})); 268 }, 269 270 expandGroupsByPredicate(predicate: GroupPredicate) { 271 const groups = globals.state.trackGroups; 272 const groupsToExpand = Object.values(groups) 273 .filter((group) => group.collapsed) 274 .filter((group) => { 275 const ref = { 276 displayName: group.name, 277 collapsed: group.collapsed, 278 }; 279 return predicate(ref); 280 }) 281 .map((group) => group.key); 282 283 for (const groupKey of groupsToExpand) { 284 globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey})); 285 } 286 }, 287 288 collapseGroupsByPredicate(predicate: GroupPredicate) { 289 const groups = globals.state.trackGroups; 290 const groupsToCollapse = Object.values(groups) 291 .filter((group) => !group.collapsed) 292 .filter((group) => { 293 const ref = { 294 displayName: group.name, 295 collapsed: group.collapsed, 296 }; 297 return predicate(ref); 298 }) 299 .map((group) => group.key); 300 301 for (const groupKey of groupsToCollapse) { 302 globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey})); 303 } 304 }, 305 306 get tracks(): TrackRef[] { 307 const tracks = Object.values(globals.state.tracks); 308 const pinnedTracks = globals.state.pinnedTracks; 309 const groups = globals.state.trackGroups; 310 return tracks.map((trackState) => { 311 const group = trackState.trackGroup 312 ? groups[trackState.trackGroup] 313 : undefined; 314 return { 315 displayName: trackState.name, 316 uri: trackState.uri, 317 key: trackState.key, 318 groupName: group?.name, 319 isPinned: pinnedTracks.includes(trackState.key), 320 }; 321 }); 322 }, 323 324 panToTimestamp(ts: time): void { 325 globals.panToTimestamp(ts); 326 }, 327 328 setViewportTime(start: time, end: time): void { 329 const interval = HighPrecisionTimeSpan.fromTime(start, end); 330 globals.timeline.updateVisibleTime(interval); 331 }, 332 333 get viewport(): Span<time, duration> { 334 return globals.timeline.visibleTimeSpan; 335 }, 336 }; 337 338 dispose(): void { 339 this.trash.dispose(); 340 this.alive = false; 341 } 342 343 mountStore<T>(migrate: Migrate<T>): Store<T> { 344 return globals.store.createSubStore(['plugins', this.pluginId], migrate); 345 } 346 347 get trace(): TraceContext { 348 return globals.traceContext; 349 } 350 351 get openerPluginArgs(): {[key: string]: unknown} | undefined { 352 if (globals.state.engine?.source.type !== 'ARRAY_BUFFER') { 353 return undefined; 354 } 355 const pluginArgs = globals.state.engine?.source.pluginArgs; 356 return (pluginArgs ?? {})[this.pluginId]; 357 } 358 359 async prompt( 360 text: string, 361 options?: PromptOption[] | undefined, 362 ): Promise<string> { 363 return globals.omnibox.prompt(text, options); 364 } 365} 366 367function isPinned(trackId: string): boolean { 368 return globals.state.pinnedTracks.includes(trackId); 369} 370 371// 'Static' registry of all known plugins. 372export class PluginRegistry extends Registry<PluginDescriptor> { 373 constructor() { 374 super((info) => info.pluginId); 375 } 376} 377 378export interface PluginDetails { 379 plugin: Plugin; 380 context: PluginContext & Disposable; 381 traceContext?: PluginContextTraceImpl; 382 previousOnTraceLoadTimeMillis?: number; 383} 384 385function makePlugin(info: PluginDescriptor): Plugin { 386 const {plugin} = info; 387 388 // Class refs are functions, concrete plugins are not 389 if (typeof plugin === 'function') { 390 const PluginClass = plugin; 391 return new PluginClass(); 392 } else { 393 return plugin; 394 } 395} 396 397export class PluginManager { 398 private registry: PluginRegistry; 399 private _plugins: Map<string, PluginDetails>; 400 private engine?: EngineBase; 401 private flags = new Map<string, Flag>(); 402 403 constructor(registry: PluginRegistry) { 404 this.registry = registry; 405 this._plugins = new Map(); 406 } 407 408 get plugins(): Map<string, PluginDetails> { 409 return this._plugins; 410 } 411 412 // Must only be called once on startup 413 async initialize(): Promise<void> { 414 // Shuffle the order of plugins to weed out any implicit inter-plugin 415 // dependencies. 416 const pluginsShuffled = Array.from(pluginRegistry.values()) 417 .map(({pluginId}) => ({pluginId, sort: Math.random()})) 418 .sort((a, b) => a.sort - b.sort); 419 420 for (const {pluginId} of pluginsShuffled) { 421 const flagId = `plugin_${pluginId}`; 422 const name = `Plugin: ${pluginId}`; 423 const flag = featureFlags.register({ 424 id: flagId, 425 name, 426 description: `Overrides '${pluginId}' plugin.`, 427 defaultValue: defaultPlugins.includes(pluginId), 428 }); 429 this.flags.set(pluginId, flag); 430 if (flag.get()) { 431 await this.activatePlugin(pluginId); 432 } 433 } 434 } 435 436 /** 437 * Enable plugin flag - i.e. configure a plugin to start on boot. 438 * @param id The ID of the plugin. 439 * @param now Optional: If true, also activate the plugin now. 440 */ 441 async enablePlugin(id: string, now?: boolean): Promise<void> { 442 const flag = this.flags.get(id); 443 if (flag) { 444 flag.set(true); 445 } 446 now && (await this.activatePlugin(id)); 447 } 448 449 /** 450 * Disable plugin flag - i.e. configure a plugin not to start on boot. 451 * @param id The ID of the plugin. 452 * @param now Optional: If true, also deactivate the plugin now. 453 */ 454 async disablePlugin(id: string, now?: boolean): Promise<void> { 455 const flag = this.flags.get(id); 456 if (flag) { 457 flag.set(false); 458 } 459 now && (await this.deactivatePlugin(id)); 460 } 461 462 /** 463 * Start a plugin just for this session. This setting is not persisted. 464 * @param id The ID of the plugin to start. 465 */ 466 async activatePlugin(id: string): Promise<void> { 467 if (this.isActive(id)) { 468 return; 469 } 470 471 const pluginInfo = this.registry.get(id); 472 const plugin = makePlugin(pluginInfo); 473 474 const context = new PluginContextImpl(id); 475 476 plugin.onActivate?.(context); 477 478 const pluginDetails: PluginDetails = { 479 plugin, 480 context, 481 }; 482 483 // If a trace is already loaded when plugin is activated, make sure to 484 // call onTraceLoad(). 485 if (this.engine) { 486 await doPluginTraceLoad(pluginDetails, this.engine); 487 } 488 489 this._plugins.set(id, pluginDetails); 490 491 raf.scheduleFullRedraw(); 492 } 493 494 /** 495 * Stop a plugin just for this session. This setting is not persisted. 496 * @param id The ID of the plugin to stop. 497 */ 498 async deactivatePlugin(id: string): Promise<void> { 499 const pluginDetails = this.getPluginContext(id); 500 if (pluginDetails === undefined) { 501 return; 502 } 503 const {context, plugin} = pluginDetails; 504 505 await doPluginTraceUnload(pluginDetails); 506 507 plugin.onDeactivate && plugin.onDeactivate(context); 508 context.dispose(); 509 510 this._plugins.delete(id); 511 512 raf.scheduleFullRedraw(); 513 } 514 515 /** 516 * Restore all plugins enable/disabled flags to their default values. 517 * @param now Optional: Also activates/deactivates plugins to match flag 518 * settings. 519 */ 520 async restoreDefaults(now?: boolean): Promise<void> { 521 for (const plugin of pluginRegistry.values()) { 522 const pluginId = plugin.pluginId; 523 const flag = assertExists(this.flags.get(pluginId)); 524 flag.reset(); 525 if (now) { 526 if (flag.get()) { 527 await this.activatePlugin(plugin.pluginId); 528 } else { 529 await this.deactivatePlugin(plugin.pluginId); 530 } 531 } 532 } 533 } 534 535 isActive(pluginId: string): boolean { 536 return this.getPluginContext(pluginId) !== undefined; 537 } 538 539 isEnabled(pluginId: string): boolean { 540 return Boolean(this.flags.get(pluginId)?.get()); 541 } 542 543 getPluginContext(pluginId: string): PluginDetails | undefined { 544 return this._plugins.get(pluginId); 545 } 546 547 async onTraceLoad( 548 engine: EngineBase, 549 beforeEach?: (id: string) => void, 550 ): Promise<void> { 551 this.engine = engine; 552 553 // Shuffle the order of plugins to weed out any implicit inter-plugin 554 // dependencies. 555 const pluginsShuffled = Array.from(this._plugins.entries()) 556 .map(([id, plugin]) => ({id, plugin, sort: Math.random()})) 557 .sort((a, b) => a.sort - b.sort); 558 559 // Awaiting all plugins in parallel will skew timing data as later plugins 560 // will spend most of their time waiting for earlier plugins to load. 561 // Running in parallel will have very little performance benefit assuming 562 // most plugins use the same engine, which can only process one query at a 563 // time. 564 for (const {id, plugin} of pluginsShuffled) { 565 beforeEach?.(id); 566 await doPluginTraceLoad(plugin, engine); 567 } 568 } 569 570 onTraceClose() { 571 for (const pluginDetails of this._plugins.values()) { 572 doPluginTraceUnload(pluginDetails); 573 } 574 this.engine = undefined; 575 } 576 577 metricVisualisations(): MetricVisualisation[] { 578 return Array.from(this._plugins.values()).flatMap((ctx) => { 579 const tracePlugin = ctx.plugin; 580 if (tracePlugin.metricVisualisations) { 581 return tracePlugin.metricVisualisations(ctx.context); 582 } else { 583 return []; 584 } 585 }); 586 } 587} 588 589async function doPluginTraceLoad( 590 pluginDetails: PluginDetails, 591 engine: EngineBase, 592): Promise<void> { 593 const {plugin, context} = pluginDetails; 594 595 const traceCtx = new PluginContextTraceImpl(context, engine); 596 pluginDetails.traceContext = traceCtx; 597 598 const startTime = performance.now(); 599 const result = await Promise.resolve(plugin.onTraceLoad?.(traceCtx)); 600 const loadTime = performance.now() - startTime; 601 pluginDetails.previousOnTraceLoadTimeMillis = loadTime; 602 603 raf.scheduleFullRedraw(); 604 605 return result; 606} 607 608async function doPluginTraceUnload( 609 pluginDetails: PluginDetails, 610): Promise<void> { 611 const {traceContext, plugin} = pluginDetails; 612 613 if (traceContext) { 614 plugin.onTraceUnload && (await plugin.onTraceUnload(traceContext)); 615 traceContext.dispose(); 616 pluginDetails.traceContext = undefined; 617 } 618} 619 620// TODO(hjd): Sort out the story for global singletons like these: 621export const pluginRegistry = new PluginRegistry(); 622export const pluginManager = new PluginManager(pluginRegistry); 623