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 // Keeps track of whether the plugin has been activated or not. 57 active?: boolean; 58 59 // If a trace has been loaded, this object stores the relevant trace-scoped 60 // plugin data 61 traceContext?: { 62 // The concrete plugin instance, created on trace load. 63 readonly instance: PerfettoPlugin; 64 65 // How long it took for the plugin's onTraceLoad() function to run. 66 readonly loadTimeMs: number; 67 }; 68} 69 70export class PluginManagerImpl { 71 private readonly registry = new Registry<PluginWrapper>((x) => x.desc.id); 72 private orderedPlugins: Array<PluginWrapper> = []; 73 74 constructor(private readonly app: PluginAppInterface) {} 75 76 registerPlugin(desc: PerfettoPluginStatic<PerfettoPlugin>) { 77 const flagId = `plugin_${desc.id}`; 78 const name = `Plugin: ${desc.id}`; 79 const flag = featureFlags.register({ 80 id: flagId, 81 name, 82 description: `Overrides '${desc.id}' plugin.`, 83 defaultValue: defaultPlugins.includes(desc.id), 84 }); 85 this.registry.register({ 86 desc, 87 enableFlag: flag, 88 }); 89 } 90 91 /** 92 * Activates all registered plugins that have not already been registered. 93 * 94 * @param enableOverrides - The list of plugins that are enabled regardless of 95 * the current flag setting. 96 */ 97 activatePlugins(enableOverrides: ReadonlyArray<string> = []) { 98 const enabledPlugins = this.registry 99 .valuesAsArray() 100 .filter((p) => p.enableFlag.get() || enableOverrides.includes(p.desc.id)); 101 102 this.orderedPlugins = this.sortPluginsTopologically(enabledPlugins); 103 104 this.orderedPlugins.forEach((p) => { 105 if (p.active) return; 106 const app = this.app.forkForPlugin(p.desc.id); 107 p.desc.onActivate?.(app); 108 p.active = true; 109 }); 110 } 111 112 async onTraceLoad( 113 traceCore: TraceImpl, 114 beforeEach?: (id: string) => void, 115 ): Promise<void> { 116 // Awaiting all plugins in parallel will skew timing data as later plugins 117 // will spend most of their time waiting for earlier plugins to load. 118 // Running in parallel will have very little performance benefit assuming 119 // most plugins use the same engine, which can only process one query at a 120 // time. 121 for (const p of this.orderedPlugins) { 122 if (p.active) { 123 beforeEach?.(p.desc.id); 124 const trace = traceCore.forkForPlugin(p.desc.id); 125 const before = performance.now(); 126 const instance = makePlugin(p.desc, trace); 127 await instance.onTraceLoad?.(trace); 128 const loadTimeMs = performance.now() - before; 129 p.traceContext = { 130 instance, 131 loadTimeMs, 132 }; 133 traceCore.trash.defer(() => { 134 p.traceContext = undefined; 135 }); 136 } 137 } 138 } 139 140 metricVisualisations(): MetricVisualisation[] { 141 return this.registry.valuesAsArray().flatMap((plugin) => { 142 if (!plugin.active) return []; 143 return plugin.desc.metricVisualisations?.() ?? []; 144 }); 145 } 146 147 getAllPlugins() { 148 return this.registry.valuesAsArray(); 149 } 150 151 getPluginContainer(id: string): PluginWrapper | undefined { 152 return this.registry.tryGet(id); 153 } 154 155 getPlugin<T extends PerfettoPlugin>( 156 pluginDescriptor: PerfettoPluginStatic<T>, 157 ): T { 158 const plugin = this.registry.get(pluginDescriptor.id); 159 return assertExists(plugin.traceContext).instance as T; 160 } 161 162 /** 163 * Sort plugins in dependency order, ensuring that if a plugin depends on 164 * other plugins, those plugins will appear fist in the list. 165 */ 166 private sortPluginsTopologically( 167 plugins: ReadonlyArray<PluginWrapper>, 168 ): Array<PluginWrapper> { 169 const orderedPlugins = new Array<PluginWrapper>(); 170 const visiting = new Set<string>(); 171 172 const visit = (p: PluginWrapper) => { 173 // Continue if we've already added this plugin, there's no need to add it 174 // again 175 if (orderedPlugins.includes(p)) { 176 return; 177 } 178 179 // Detect circular dependencies 180 if (visiting.has(p.desc.id)) { 181 const cycle = Array.from(visiting).concat(p.desc.id); 182 throw new Error( 183 `Cyclic plugin dependency detected: ${cycle.join(' -> ')}`, 184 ); 185 } 186 187 // Temporarily push this plugin onto the visiting stack while visiting 188 // dependencies, to allow circular dependencies to be detected 189 visiting.add(p.desc.id); 190 191 // Recursively visit dependencies 192 p.desc.dependencies?.forEach((d) => { 193 visit(this.registry.get(d.id)); 194 }); 195 196 visiting.delete(p.desc.id); 197 198 // Finally add this plugin to the ordered list 199 orderedPlugins.push(p); 200 }; 201 202 plugins.forEach((p) => visit(p)); 203 204 return orderedPlugins; 205 } 206} 207