• 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 {assertExists} from '../base/logging';
16import {Registry} from '../base/registry';
17import {App} from '../public/app';
18import {
19  MetricVisualisation,
20  PerfettoPlugin,
21  PerfettoPluginStatic,
22} from '../public/plugin';
23import {Trace} from '../public/trace';
24import {defaultPlugins} from './default_plugins';
25import {featureFlags} from './feature_flags';
26import {Flag} from '../public/feature_flag';
27import {TraceImpl} from './trace_impl';
28
29// The pseudo plugin id used for the core instance of AppImpl.
30export const CORE_PLUGIN_ID = '__core__';
31
32function makePlugin(
33  desc: PerfettoPluginStatic<PerfettoPlugin>,
34  trace: Trace,
35): PerfettoPlugin {
36  const PluginClass = desc;
37  return new PluginClass(trace);
38}
39
40// This interface injects AppImpl's methods into PluginManager to avoid
41// circular dependencies between PluginManager and AppImpl.
42export interface PluginAppInterface {
43  forkForPlugin(pluginId: string): App;
44  get trace(): TraceImpl | undefined;
45}
46
47// Contains information about a plugin.
48export interface PluginWrapper {
49  // A reference to the plugin descriptor
50  readonly desc: PerfettoPluginStatic<PerfettoPlugin>;
51
52  // The feature flag used to allow users to change whether this plugin should
53  // be enabled or not.
54  readonly enableFlag: Flag;
55
56  // Record whether this plugin was enabled for this session, regardless of the
57  // current flag setting. I.e. this captures the state of the enabled flag at
58  // boot time.
59  readonly enabled: boolean;
60
61  // Keeps track of whether this plugin is active. A plugin can be active even
62  // if it's disabled, if another plugin depends on it.
63  //
64  // In summary, a plugin can be in one of three states:
65  // - Inactive: Disabled and no active plugins depend on it.
66  // - Transitively active: Disabled but active because another plugin depends
67  //   on it.
68  // - Explicitly active: Active because it was explicitly enabled by the user.
69  active?: boolean;
70
71  // If a trace has been loaded, this object stores the relevant trace-scoped
72  // plugin data
73  traceContext?: {
74    // The concrete plugin instance, created on trace load.
75    readonly instance: PerfettoPlugin;
76
77    // How long it took for the plugin's onTraceLoad() function to run.
78    readonly loadTimeMs: number;
79  };
80}
81
82export class PluginManagerImpl {
83  private readonly registry = new Registry<PluginWrapper>((x) => x.desc.id);
84  private orderedPlugins: Array<PluginWrapper> = [];
85
86  constructor(private readonly app: PluginAppInterface) {}
87
88  registerPlugin(desc: PerfettoPluginStatic<PerfettoPlugin>) {
89    const flagId = `plugin_${desc.id}`;
90    const name = `Plugin: ${desc.id}`;
91    const flag = featureFlags.register({
92      id: flagId,
93      name,
94      description: `Overrides '${desc.id}' plugin.`,
95      defaultValue: defaultPlugins.includes(desc.id),
96    });
97    this.registry.register({
98      desc,
99      enableFlag: flag,
100      enabled: flag.get(),
101    });
102  }
103
104  /**
105   * Activates all registered plugins that have not already been registered.
106   *
107   * @param enableOverrides - The list of plugins that are enabled regardless of
108   * the current flag setting.
109   */
110  activatePlugins(enableOverrides: ReadonlyArray<string> = []) {
111    const enabledPlugins = this.registry
112      .valuesAsArray()
113      .filter((p) => p.enableFlag.get() || enableOverrides.includes(p.desc.id));
114
115    this.orderedPlugins = this.sortPluginsTopologically(enabledPlugins);
116
117    this.orderedPlugins.forEach((p) => {
118      if (p.active) return;
119      const app = this.app.forkForPlugin(p.desc.id);
120      p.desc.onActivate?.(app);
121      p.active = true;
122    });
123  }
124
125  async onTraceLoad(
126    traceCore: TraceImpl,
127    beforeEach?: (id: string) => void,
128  ): Promise<void> {
129    // Awaiting all plugins in parallel will skew timing data as later plugins
130    // will spend most of their time waiting for earlier plugins to load.
131    // Running in parallel will have very little performance benefit assuming
132    // most plugins use the same engine, which can only process one query at a
133    // time.
134    for (const p of this.orderedPlugins) {
135      if (p.active) {
136        beforeEach?.(p.desc.id);
137        const trace = traceCore.forkForPlugin(p.desc.id);
138        const before = performance.now();
139        const instance = makePlugin(p.desc, trace);
140        await instance.onTraceLoad?.(trace);
141        const loadTimeMs = performance.now() - before;
142        p.traceContext = {
143          instance,
144          loadTimeMs,
145        };
146        traceCore.trash.defer(() => {
147          p.traceContext = undefined;
148        });
149      }
150    }
151  }
152
153  metricVisualisations(): MetricVisualisation[] {
154    return this.registry.valuesAsArray().flatMap((plugin) => {
155      if (!plugin.active) return [];
156      return plugin.desc.metricVisualisations?.() ?? [];
157    });
158  }
159
160  getAllPlugins() {
161    return this.registry.valuesAsArray();
162  }
163
164  getPluginContainer(id: string): PluginWrapper | undefined {
165    return this.registry.tryGet(id);
166  }
167
168  getPlugin<T extends PerfettoPlugin>(
169    pluginDescriptor: PerfettoPluginStatic<T>,
170  ): T {
171    const plugin = this.registry.get(pluginDescriptor.id);
172    return assertExists(plugin.traceContext).instance as T;
173  }
174
175  /**
176   * Sort plugins in dependency order, ensuring that if a plugin depends on
177   * other plugins, those plugins will appear fist in the list.
178   */
179  private sortPluginsTopologically(
180    plugins: ReadonlyArray<PluginWrapper>,
181  ): Array<PluginWrapper> {
182    const orderedPlugins = new Array<PluginWrapper>();
183    const visiting = new Set<string>();
184
185    const visit = (p: PluginWrapper) => {
186      // Continue if we've already added this plugin, there's no need to add it
187      // again
188      if (orderedPlugins.includes(p)) {
189        return;
190      }
191
192      // Detect circular dependencies
193      if (visiting.has(p.desc.id)) {
194        const cycle = Array.from(visiting).concat(p.desc.id);
195        throw new Error(
196          `Cyclic plugin dependency detected: ${cycle.join(' -> ')}`,
197        );
198      }
199
200      // Temporarily push this plugin onto the visiting stack while visiting
201      // dependencies, to allow circular dependencies to be detected
202      visiting.add(p.desc.id);
203
204      // Recursively visit dependencies
205      p.desc.dependencies?.forEach((d) => {
206        visit(this.registry.get(d.id));
207      });
208
209      visiting.delete(p.desc.id);
210
211      // Finally add this plugin to the ordered list
212      orderedPlugins.push(p);
213    };
214
215    plugins.forEach((p) => visit(p));
216
217    return orderedPlugins;
218  }
219}
220