| # UI plugins |
| The Perfetto UI can be extended with plugins. These plugins are shipped |
| part of Perfetto. |
| |
| ## Create a plugin |
| The guide below explains how to create a plugin for the Perfetto UI. |
| |
| ### Prepare for UI development |
| 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](./getting-started) guide for more detail. |
| |
| ```sh |
| git clone https://android.googlesource.com/platform/external/perfetto/ |
| cd perfetto |
| ./tool/install-build-deps --ui |
| ``` |
| |
| ### Copy the plugin skeleton |
| ```sh |
| 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: |
| - Don't name the directory `XyzPlugin` just `Xyz`. |
| - The `pluginId` and directory name must match. |
| - Plugins should be prefixed with the reversed components of a domain |
| name you control. For example if `example.com` is your domain your |
| plugin should be named `com.example.Foo`. |
| - Core plugins maintained by the Perfetto team should use |
| `dev.perfetto.Foo`. |
| - Commands should have ids with the pattern `example.com#DoSomething` |
| - Command's ids should be prefixed with the id of the plugin which |
| provides them. |
| - Commands names should have the form "Verb something something". |
| Good: "Pin janky frame timeline tracks" |
| Bad: "Tracks are Displayed if Janky" |
| |
| ### Start the dev server |
| ```sh |
| ./ui/run-dev-server |
| ``` |
| Now navigate to [](http://localhost:10000/settings) |
| |
| ### Upload your plugin for review |
| - Update `ui/src/plugins/<your-plugin-name>/OWNERS` to include your email. |
| - Follow the [Contributing](./getting-started#contributing) |
| instructions to upload your CL to the codereview tool. |
| - Once uploaded add `hjd@google.com` as a reviewer for your CL. |
| |
| ## Plugin extension points |
| 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 |
| Commands are user issuable shortcuts for actions in the UI. |
| They can be accessed via the omnibox. |
| |
| Follow the [create a plugin](#create-a-plugin) to get an initial |
| skeleton for your plugin. |
| |
| To add your first command, add a call to `ctx.addCommand()` in either your |
| `onActivate()` or `onTraceLoad()` hooks. The recommendation is to register |
| commands in `onActivate()` by default unless they require something from |
| `TracePluginContext` which is not available on `PluginContext`. |
| |
| The tradeoff is that commands registered in `onTraceLoad()` are only available |
| while a trace is loaded, whereas commands registered in `onActivate()` are |
| available all the time the plugin is active. |
| |
| ```typescript |
| class MyPlugin implements Plugin { |
| onActivate(ctx: PluginContext): void { |
| ctx.addCommand( |
| { |
| id: 'dev.perfetto.ExampleSimpleCommand#LogHelloPlugin', |
| name: 'Log "Hello, plugin!"', |
| callback: () => console.log('Hello, plugin!'), |
| }, |
| ); |
| } |
| |
| onTraceLoad(ctx: TracePluginContext): void { |
| ctx.addCommand( |
| { |
| id: 'dev.perfetto.ExampleSimpleTraceCommand#LogHelloTrace', |
| name: 'Log "Hello, trace!"', |
| callback: () => console.log('Hello, trace!'), |
| }, |
| ); |
| } |
| } |
| ``` |
| |
| Here `id` is a unique string which identifies this command. |
| The `id` should be prefixed with the plugin id followed by a `#`. All command |
| `id`s must be unique system-wide. |
| `name` is a human readable name for the command, which is shown in the command |
| palette. |
| Finally `callback()` is the callback which actually performs the |
| action. |
| |
| Commands are removed automatically when their context disappears. Commands |
| registered with the `PluginContext` are removed when the plugin is deactivated, |
| and commands registered with the `TracePluginContext` are removed when the trace |
| is unloaded. |
| |
| Examples: |
| - [dev.perfetto.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts). |
| - [dev.perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.CoreCommands/index.ts). |
| - [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts). |
| |
| ### Tracks |
| TBD |
| |
| ### Tabs |
| TBD |
| |
| ### Metric Visualisations |
| TBD |
| |
| Examples: |
| - [dev.perfetto.AndroidBinderViz](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts). |
| |
| ### State |
| 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](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/frontend/store.ts). |
| `Store` allows for reading and writing `T`. |
| Reading: |
| ```typescript |
| 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: |
| ```typescript |
| 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. |
| ```typescript |
| interface MyState { |
| favouriteSlices: MySliceInfo[]; |
| } |
| ``` |
| |
| This interface will be used as type parameter to the `Plugin` and |
| `TracePluginContext` interfaces. |
| ```typescript |
| 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: |
| - Loading a new trace |
| - Loading from a permalink |
| |
| In case of a new trace `migrate()` is called with `undefined`. In this |
| case you should return a default version of `MyState`: |
| ```typescript |
| 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: |
| - [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts). |
| |
| ## Guide to the plugin API |
| The plugin interfaces are defined in [ui/src/public/index.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/index.ts). |
| |
| |
| ## Default plugins |
| TBD |
| |
| ## Misc notes |
| - Plugins must be licensed under |
| [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) |
| the same as all other code in the repository. |
| - Plugins are the responsibility of the OWNERS of that plugin to |
| maintain, not the responsibility of the Perfetto team. All |
| efforts will be made to keep the plugin API stable and existing |
| plugins working however plugins that remain unmaintained for long |
| periods of time will be disabled and ultimately deleted. |
| |