blob: 01ce63b3649c7d6938694b78719859c92c36c9f3 [file]
// 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/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 assertExists(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];
}