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