// Copyright (C) 2022 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {assertExists} from '../base/logging'; import {Registry} from '../base/registry'; import {App} from '../public/app'; import { MetricVisualisation, PerfettoPlugin, PerfettoPluginStatic, } from '../public/plugin'; import {Trace} from '../public/trace'; import {defaultPlugins} from './default_plugins'; import {featureFlags} from './feature_flags'; import {Flag} from '../public/feature_flag'; import {TraceImpl} from './trace_impl'; // The pseudo plugin id used for the core instance of AppImpl. export const CORE_PLUGIN_ID = '__core__'; function makePlugin( desc: PerfettoPluginStatic, trace: Trace, ): PerfettoPlugin { const PluginClass = desc; return new PluginClass(trace); } // This interface injects AppImpl's methods into PluginManager to avoid // circular dependencies between PluginManager and AppImpl. export interface PluginAppInterface { forkForPlugin(pluginId: string): App; get trace(): TraceImpl | undefined; } // Contains information about a plugin. export interface PluginWrapper { // A reference to the plugin descriptor readonly desc: PerfettoPluginStatic; // The feature flag used to allow users to change whether this plugin should // be enabled or not. readonly enableFlag: Flag; // Record whether this plugin was enabled for this session, regardless of the // current flag setting. I.e. this captures the state of the enabled flag at // boot time. readonly enabled: boolean; // Keeps track of whether this plugin is active. A plugin can be active even // if it's disabled, if another plugin depends on it. // // In summary, a plugin can be in one of three states: // - Inactive: Disabled and no active plugins depend on it. // - Transitively active: Disabled but active because another plugin depends // on it. // - Explicitly active: Active because it was explicitly enabled by the user. active?: boolean; // If a trace has been loaded, this object stores the relevant trace-scoped // plugin data traceContext?: { // The concrete plugin instance, created on trace load. readonly instance: PerfettoPlugin; // How long it took for the plugin's onTraceLoad() function to run. readonly loadTimeMs: number; }; } export class PluginManagerImpl { private readonly registry = new Registry((x) => x.desc.id); private orderedPlugins: Array = []; constructor(private readonly app: PluginAppInterface) {} registerPlugin(desc: PerfettoPluginStatic) { const flagId = `plugin_${desc.id}`; const name = `Plugin: ${desc.id}`; const flag = featureFlags.register({ id: flagId, name, description: `Overrides '${desc.id}' plugin.`, defaultValue: defaultPlugins.includes(desc.id), }); this.registry.register({ desc, enableFlag: flag, enabled: flag.get(), }); } /** * Activates all registered plugins that have not already been registered. * * @param enableOverrides - The list of plugins that are enabled regardless of * the current flag setting. */ activatePlugins(enableOverrides: ReadonlyArray = []) { const enabledPlugins = this.registry .valuesAsArray() .filter((p) => p.enableFlag.get() || enableOverrides.includes(p.desc.id)); this.orderedPlugins = this.sortPluginsTopologically(enabledPlugins); this.orderedPlugins.forEach((p) => { if (p.active) return; const app = this.app.forkForPlugin(p.desc.id); p.desc.onActivate?.(app); p.active = true; }); } async onTraceLoad( traceCore: TraceImpl, beforeEach?: (id: string) => void, ): Promise { // Awaiting all plugins in parallel will skew timing data as later plugins // will spend most of their time waiting for earlier plugins to load. // Running in parallel will have very little performance benefit assuming // most plugins use the same engine, which can only process one query at a // time. for (const p of this.orderedPlugins) { if (p.active) { beforeEach?.(p.desc.id); const trace = traceCore.forkForPlugin(p.desc.id); const before = performance.now(); const instance = makePlugin(p.desc, trace); await instance.onTraceLoad?.(trace); const loadTimeMs = performance.now() - before; p.traceContext = { instance, loadTimeMs, }; traceCore.trash.defer(() => { p.traceContext = undefined; }); } } } metricVisualisations(): MetricVisualisation[] { return this.registry.valuesAsArray().flatMap((plugin) => { if (!plugin.active) return []; return plugin.desc.metricVisualisations?.() ?? []; }); } getAllPlugins() { return this.registry.valuesAsArray(); } getPluginContainer(id: string): PluginWrapper | undefined { return this.registry.tryGet(id); } getPlugin( pluginDescriptor: PerfettoPluginStatic, ): T { const plugin = this.registry.get(pluginDescriptor.id); return assertExists(plugin.traceContext).instance as T; } /** * Sort plugins in dependency order, ensuring that if a plugin depends on * other plugins, those plugins will appear fist in the list. */ private sortPluginsTopologically( plugins: ReadonlyArray, ): Array { const orderedPlugins = new Array(); const visiting = new Set(); const visit = (p: PluginWrapper) => { // Continue if we've already added this plugin, there's no need to add it // again if (orderedPlugins.includes(p)) { return; } // Detect circular dependencies if (visiting.has(p.desc.id)) { const cycle = Array.from(visiting).concat(p.desc.id); throw new Error( `Cyclic plugin dependency detected: ${cycle.join(' -> ')}`, ); } // Temporarily push this plugin onto the visiting stack while visiting // dependencies, to allow circular dependencies to be detected visiting.add(p.desc.id); // Recursively visit dependencies p.desc.dependencies?.forEach((d) => { visit(this.registry.get(d.id)); }); visiting.delete(p.desc.id); // Finally add this plugin to the ordered list orderedPlugins.push(p); }; plugins.forEach((p) => visit(p)); return orderedPlugins; } }