The Perfetto UI can be extended with plugins. These plugins are shipped part of Perfetto.
The guide below explains how to create a plugin for the Perfetto UI.
First we need to prepare the UI development environment. You will need to use a MacOS or Linux machine. Follow the steps below or see the Getting Started guide for more detail.
git clone https://android.googlesource.com/platform/external/perfetto/ cd perfetto ./tool/install-build-deps --ui
cp -r ui/src/plugins/com.example.Skeleton ui/src/plugins/<your-plugin-name>
Now edit ui/src/plugins/<your-plugin-name>/index.ts
. Search for all instances of SKELETON: <instruction>
in the file and follow the instructions.
Notes on naming:
XyzPlugin
just Xyz
.pluginId
and directory name must match.example.com
is your domain your plugin should be named com.example.Foo
.dev.perfetto.Foo
.example.com#DoSomething
./ui/run-dev-server
ui/src/plugins/<your-plugin-name>/OWNERS
to include your email.hjd@google.com
as a reviewer for your CL.Plugins can extend a handful of specific places in the UI. The sections below show these extension points and give examples of how they can be used.
Commands are user issuable shortcuts for actions in the UI. They can be accessed via the omnibox.
Follow the create a plugin to get an initial skeleton for your plugin. To add your first command edit either the commands()
or traceCommands()
methods.
class MyPlugin implements Plugin { // ... commands(ctx: PluginContext): Command[] { return [ { id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld', name: 'Log hello world', callback: () => console.log('Hello, world!'), }, ]; } traceCommands(ctx: TracePluginContext): Command[] { return [ { id: 'dev.perfetto.ExampleSimpleTraceCommand#LogHelloWorld', name: 'Log hello trace', callback: () => console.log('Hello, trace!'), }, ]; } }
Commands are polled whenever the command list must be updated. When no trace is loaded, only the commands()
method is called, whereas when a trace is loaded, both the commands()
and the traceCommands()
methods are called, and their outputs are concatenated.
The difference between the two is that commands defined in commands()
only have access to the viewer API, whereas commands defined in traceCommands()
may access the store and the engine in addition to the viewer API.
The tradeoff is that commands defined in traceCommands()
are only available when a trace is loaded, whereas commands defined in commands()
are available all the time.
Here id
is a unique string which identifies this command. The id
should be prefixed with the plugin id followed by a #
. name
is a human readable name for the command. Finally callback()
is the callback which actually performs the action.
Examples:
TBD
TBD
TBD
Examples:
NOTE: It is important to consider version skew when using persistent state.
Plugins can persist information into permalinks. This allows plugins to gracefully handle permalinking and is an opt-in - not automatic - mechanism.
Persistent plugin state works using a Store<T>
where T
is some JSON serializable object. Store
is implemented here. Store
allows for reading and writing T
. Reading:
interface Foo { bar: string; } const store: Store<Foo> = getFooStoreSomehow(); // store.state is immutable and must not be edited. const foo = store.state.foo; const bar = foo.bar; console.log(bar);
Writing:
interface Foo { bar: string; } const store: Store<Foo> = getFooStoreSomehow(); store.edit((draft) => { draft.foo.bar = 'Hello, world!'; }); console.log(store.state.foo.bar); // > Hello, world!
First define an interface for your specific plugin state.
interface MyState { favouriteSlices: MySliceInfo[]; }
This interface will be used as type parameter to the Plugin
and TracePluginContext
interfaces.
class MyPlugin implements Plugin<MyState> { migrate(initialState: unknown): MyState { // ... } async onTraceLoad(ctx: TracePluginContext<MyState>): Promise<void> { // You can access the store on ctx.store } async onTraceUnload(ctx: TracePluginContext<MyState>): Promise<void> { // You can access the store on ctx.store } // ... }
migrate()
is called after onActivate()
just before onTraceLoad()
. There are two cases to consider:
In case of a new trace migrate()
is called with undefined
. In this case you should return a default version of MyState
:
class MyPlugin implements Plugin<MyState> { migrate(initialState: unknown): MyState { if (initialState === undefined) { return { favouriteSlices: []; }; } // ... } // ... }
In the permalink case migrate()
is called with the state of the plugin store at the time the permalink was generated. This may be from a older or newer version of the plugin. Plugin's must not make assumptions about the contents of initialState
.
In this case you need to carefully validate the state object.
TODO: Add validation example.
Examples:
The plugin interfaces are defined in ui/src/public/index.ts.
TBD