| // 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 {ensureExists} from '../base/assert'; | |
| import {Registry} from '../base/registry'; | |
| import type {PerfettoPlugin, PerfettoPluginStatic} from '../public/plugin'; | |
| import type {Trace} from '../public/trace'; | |
| import {featureFlags} from './feature_flags'; | |
| import type {Flag} from '../public/feature_flag'; | |
| import type {TraceImpl} from './trace_impl'; | |
| import type {AppImpl} from './app_impl'; | |
| import {createProxy} from '../base/utils'; | |
| import type {RouteArgs} from '../public/route_schema'; | |
| import type {SettingsManagerImpl} from './settings_manager'; | |
| import type {PageManager} from '../public/page'; | |
| function makePlugin( | |
| desc: PerfettoPluginStatic<PerfettoPlugin>, | |
| trace: Trace, | |
| ): PerfettoPlugin { | |
| const PluginClass = desc; | |
| return new PluginClass(trace); | |
| } | |
| // Contains information about a plugin. | |
| export interface PluginWrapper { | |
| // A reference to the plugin descriptor | |
| readonly desc: PerfettoPluginStatic<PerfettoPlugin>; | |
| // 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; | |
| // Whether this is a core plugin (part of CORE_PLUGINS) or not. | |
| readonly isCore: 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<PluginWrapper>((x) => x.desc.id); | |
| private orderedPlugins: Array<PluginWrapper> = []; | |
| constructor(private readonly defaultPluginIds: ReadonlyArray<string>) {} | |
| registerPlugin(desc: PerfettoPluginStatic<PerfettoPlugin>, isCore = false) { | |
| const flagId = `plugin_${desc.id}`; | |
| const name = `Plugin: ${desc.id}`; | |
| const flag = featureFlags.register({ | |
| id: flagId, | |
| name, | |
| description: `Overrides '${desc.id}' plugin.`, | |
| defaultValue: this.defaultPluginIds.includes(desc.id), | |
| }); | |
| this.registry.register({ | |
| desc, | |
| enableFlag: flag, | |
| enabled: flag.get(), | |
| isCore, | |
| }); | |
| } | |
| /** | |
| * Activates all registered plugins that have not already been registered. | |
| * | |
| * @param app - The application instance. | |
| * @param enableOverrides - The list of plugins that are enabled regardless of | |
| * the current flag setting. | |
| */ | |
| activatePlugins(app: AppImpl, enableOverrides: ReadonlyArray<string> = []) { | |
| 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 appProxy = createAppProxy(app, p.desc.id); | |
| const pluginArgs = getPluginArgs(app, p.desc.id); | |
| p.desc.onActivate?.(appProxy, pluginArgs); | |
| p.active = true; | |
| }); | |
| } | |
| async onTraceLoad( | |
| trace: TraceImpl, | |
| beforeEach?: (id: string) => void, | |
| ): Promise<void> { | |
| // 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 traceProxy = createTraceProxy(trace, p.desc.id); | |
| const instance = makePlugin(p.desc, traceProxy); | |
| const args = getOpenerArgs(trace, p.desc.id); | |
| const before = performance.now(); | |
| await instance.onTraceLoad?.(traceProxy, args); | |
| const loadTimeMs = performance.now() - before; | |
| p.traceContext = { | |
| instance, | |
| loadTimeMs, | |
| }; | |
| trace.trash.defer(() => { | |
| p.traceContext = undefined; | |
| }); | |
| } | |
| } | |
| } | |
| getAllPlugins() { | |
| return this.registry.valuesAsArray(); | |
| } | |
| getPluginContainer(id: string): PluginWrapper | undefined { | |
| return this.registry.tryGet(id); | |
| } | |
| isPluginEnabled(id: string): boolean { | |
| const plugin = this.registry.tryGet(id); | |
| return plugin?.traceContext !== undefined; | |
| } | |
| getPlugin<T extends PerfettoPlugin>( | |
| pluginDescriptor: PerfettoPluginStatic<T>, | |
| ): T { | |
| const plugin = this.registry.get(pluginDescriptor.id); | |
| return ensureExists(plugin.traceContext).instance as T; | |
| } | |
| isCorePlugin(pluginId: string): boolean { | |
| const plugin = this.registry.tryGet(pluginId); | |
| return plugin?.isCore ?? false; | |
| } | |
| /** | |
| * 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<PluginWrapper>, | |
| ): Array<PluginWrapper> { | |
| const orderedPlugins = new Array<PluginWrapper>(); | |
| const visiting = new Set<string>(); | |
| 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; | |
| } | |
| } | |
| /** | |
| * Creates a plugin-scoped proxy for the App instance. | |
| * | |
| * This proxy automatically injects the plugin's ID into any pages or settings | |
| * registered by the plugin, ensuring proper attribution and enabling cleanup | |
| * when the plugin is unloaded. It also recursively proxies the trace property | |
| * if one is loaded. | |
| */ | |
| function createAppProxy(app: AppImpl, pluginId: string): AppImpl { | |
| return createProxy(app, { | |
| get trace() { | |
| if (app.trace) { | |
| return createTraceProxy(app.trace, pluginId); | |
| } else { | |
| return undefined; | |
| } | |
| }, | |
| get pages() { | |
| return createPagesProxy(app.pages, pluginId); | |
| }, | |
| get settings() { | |
| return createSettingsProxy(app.settings, pluginId); | |
| }, | |
| }); | |
| } | |
| /** | |
| * Creates a plugin-scoped proxy for the Trace instance. | |
| * | |
| * This proxy automatically injects the plugin's ID into any pages, settings, | |
| * and tracks registered by the plugin. This ensures that all trace-scoped | |
| * resources created by the plugin are properly attributed and can be | |
| * automatically cleaned up when the trace is closed. It also proxies | |
| * the trace property back to itself. | |
| */ | |
| function createTraceProxy(trace: TraceImpl, pluginId: string): TraceImpl { | |
| const traceProxy = createProxy(trace, { | |
| get engine() { | |
| return trace.engine.getProxy(pluginId); | |
| }, | |
| get trace(): TraceImpl { | |
| return traceProxy; // Return this proxy. | |
| }, | |
| get pages() { | |
| return createPagesProxy(trace.pages, pluginId); | |
| }, | |
| get settings() { | |
| return createSettingsProxy(trace.settings, pluginId); | |
| }, | |
| get tracks() { | |
| return createProxy(trace.tracks, { | |
| registerTrack(track) { | |
| return trace.tracks.registerTrack({ | |
| ...track, | |
| pluginId, | |
| }); | |
| }, | |
| }); | |
| }, | |
| }); | |
| return traceProxy; | |
| } | |
| /** | |
| * Creates a proxy for the PageManager that automatically injects the pluginId | |
| * into any registered pages. | |
| */ | |
| function createPagesProxy<T extends PageManager>( | |
| pages: T, | |
| pluginId: string, | |
| ): T { | |
| return createProxy(pages, { | |
| registerPage(page) { | |
| return pages.registerPage({ | |
| ...page, | |
| pluginId, | |
| }); | |
| }, | |
| } as Partial<T>); | |
| } | |
| /** | |
| * Creates a proxy for the SettingsManager that automatically injects the | |
| * pluginId into any registered settings. | |
| */ | |
| function createSettingsProxy<T extends SettingsManagerImpl>( | |
| settings: T, | |
| pluginId: string, | |
| ): T { | |
| return createProxy(settings, { | |
| register(setting) { | |
| return settings.register(setting, pluginId); | |
| }, | |
| } as Partial<T>); | |
| } | |
| function getPluginArgs(app: AppImpl, pluginId: string): RouteArgs { | |
| return Object.entries(app.initialRouteArgs).reduce((result, [key, value]) => { | |
| // Create a regex to match keys starting with pluginId | |
| const regex = new RegExp(`^${pluginId}:(.+)$`); | |
| const match = key.match(regex); | |
| // Only include entries that match the regex | |
| if (match) { | |
| const newKey = match[1]; | |
| // Use the capture group (what comes after the prefix) as the new key | |
| result[newKey] = value; | |
| } | |
| return result; | |
| }, {} as RouteArgs); | |
| } | |
| function getOpenerArgs( | |
| trace: TraceImpl, | |
| pluginId: string, | |
| ): {[key: string]: unknown} | undefined { | |
| const traceSource = trace.traceInfo.source; | |
| if (traceSource.type !== 'ARRAY_BUFFER') { | |
| return undefined; | |
| } | |
| const pluginArgs = traceSource.pluginArgs; | |
| return (pluginArgs ?? {})[pluginId]; | |
| } |