• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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