blob: cc65f43eedd1418676dea81f434ab191ebb75d55 [file] [log] [blame] [view]
# 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. You can
browse the public plugin API [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
### 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
./tools/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.
- Command names should have the form "Verb something something", and should be
in normal sentence case. I.e. don't capitalize the first letter of each word.
- Good: "Pin janky frame timeline tracks"
- Bad: "Tracks are Displayed if Janky"
### Start the dev server
```sh
./ui/run-dev-server
```
Now navigate to [localhost:10000](http://localhost:10000/)
### Enable your plugin
- Navigate to the plugins page:
[localhost:10000/#!/plugins](http://localhost:10000/#!/plugins).
- Ctrl-F for your plugin name and enable it.
- Enabling/disabling plugins requires a restart of the UI, so refresh the page
to start your plugin.
Later you can request for your plugin to be enabled by default. Follow the
[default plugins](#default-plugins) section for this.
### 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 `stevegolton@google.com` as a reviewer for your CL.
## Plugin Lifecycle
To demonstrate the plugin's lifecycle, this is a minimal plugin that implements
the key lifecycle hooks:
```ts
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
static onActivate(app: App): void {
// Called once on app startup
console.log('MyPlugin::onActivate()', app.pluginId);
// Note: It's rare that plugins would need this hook as most plugins are
// interested in trace details. Thus, this function can usually be omitted.
}
constructor(trace: Trace) {
// Called each time a trace is loaded
console.log('MyPlugin::constructor()', trace.traceInfo.traceTitle);
}
async onTraceLoad(trace: Trace): Promise<void> {
// Called each time a trace is loaded
console.log('MyPlugin::onTraceLoad()', trace.traceInfo.traceTitle);
// Note this function returns a promise, so any any async calls should be
// completed before this promise resolves as the app using this promise for
// timing and plugin synchronization.
}
}
```
You can run this plugin with devtools to see the log messages in the console,
which should give you a feel for the plugin lifecycle. Try opening a few traces
one after another.
`onActivate()` runs shortly after Perfetto starts up, before a trace is loaded.
This is where the you'll configure your plugin's capabilities that aren't trace
dependent. At this point the plugin's class is not instantiated, so you'll
notice `onActivate()` hook is a static class member. `onActivate()` is only ever
called once, regardless of the number of traces loaded.
`onActivate()` is passed an `App` object which the plugin can use to configure
core capabilities such as commands, sidebar items and pages. Capabilities
registered on the App interface are persisted throughout the lifetime of the app
(practically forever until the tab is closed), in contrast to what happens for
the same methods on the `Trace` object (see below).
The plugin class in instantiated when a trace is loaded (a new plugin instance
is created for each trace). `onTraceLoad()` is called immediately after the
class is instantiated, which is where you'll configure your plugin's trace
dependent capabilities.
`onTraceLoad()` is passed a `Trace` object which the plugin can use to configure
entities that are scoped to a specific trace, such as tracks and tabs. `Trace`
is a superset of `App`, so anything you can do with `App` you can also do with
`Trace`, however, capabilities registered on `Trace` will typically be discarded
when a new trace is loaded.
A plugin will typically register capabilities with the core and return quickly.
But these capabilities usually contain objects and callbacks which are called
into later by the core during the runtime of the app. Most capabilities require
a `Trace` or an `App` to do anything useful so these are usually bound into the
capabilities at registration time using JavaScript classes or closures.
```ts
// Toy example: Code will not compile.
async onTraceLoad(trace: Trace) {
// `trace` is captured in the closure and used later by the app
trace.regsterXYZ(() => trace.xyz);
}
```
That way, the callback is bound to a specific trace object which and the trace
object can outlive the runtime of the `onTraceLoad()` function, which is a very
common pattern in Perfetto plugins.
> Note: Some capabilities can be registered on either the `App` or the `Trace`
> object (i.e. in `onActivate()` or in `onTraceLoad()`), if in doubt about which
> one to use, use `onTraceLoad()` as this is more than likely the one you want.
> Most plugins add tracks and tabs that depend on the trace. You'd usually have
> to be doing something out of the ordinary if you need to use `onActivate()`.
### Performance
`onActivate()` and `onTraceLoad()` should generally complete as quickly as
possible, however sometimes `onTraceLoad()` may need to perform async operations
on trace processor such as performing queries and/or creating views and tables.
Thus, `onTraceLoad()` should return a promise (or you can simply make it an
async function). When this promise resolves it tells the core that the plugin is
fully initialized.
> Note: It's important that any async operations done in onTraceLoad() are
> awaited so that all async operations are completed by the time the promise is
> resolved. This is so that plugins can be properly timed and synchronized.
```ts
// GOOD
async onTraceLoad(trace: Trace) {
await trace.engine.query(...);
}
// BAD
async onTraceLoad(trace: Trace) {
// Note the missing await!
trace.engine.query(...);
}
```
## Extension Points
Plugins can extend functionality of Perfetto by registering capabilities via
extension points on the `App` or `Trace` objects.
The following sections delve into more detail on each extension point and
provide examples of how they can be used.
### Commands
Commands are user issuable shortcuts for actions in the UI. They are invoked via
the command palette which can be opened by pressing Ctrl+Shift+P (or Cmd+Shift+P
on Mac), or by typing a '>' into the omnibox.
To add a command, add a call to `registerCommand()` on either your
`onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
commands in `onTraceLoad()` by default unless you very specifically want the
command to be available before a trace has loaded.
Example of a command that doesn't require a trace.
```ts
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
static onActivate(app: App) {
app.commands.registerCommand({
id: `${app.pluginId}#SayHello`,
name: 'Say hello',
callback: () => console.log('Hello, world!'),
});
}
}
```
Example of a command that requires a trace object - in this case the trace
title.
```ts
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace) {
trace.commands.registerCommand({
id: `${trace.pluginId}#LogTraceTitle`,
name: 'Log trace title',
callback: () => console.log(trace.info.traceTitle),
});
}
}
```
> Notice that the trace object is captured in the closure, so it can be used
> after the onTraceLoad() function has returned. This is a very common pattern
> in Perfetto plugins.
Command arguments explained:
- `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.
- `callback()` is the callback which actually performs the action.
#### Async commands
It's common that commands will perform async operations in their callbacks. It's
recommended to use async/await for this rather than `.then().catch()`. The
easiest way to do this is to make the callback an async function.
```ts
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace) {
trace.commands.registerCommand({
id: `${trace.pluginId}#QueryTraceProcessor`,
name: 'Query trace processor',
callback: async () => {
const results = await trace.engine.query(...);
// use results...
},
});
}
}
```
If the callback is async (i.e. it returns a promise), nothing special happens.
The command is still fire-n-forget as far as the core is concerned.
Examples:
- [com.example.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts).
- [perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core_plugins/commands/index.ts).
- [com.example.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleState/index.ts).
### Hotkeys
A hotkey may be associated with a command at registration time.
```typescript
ctx.commands.registerCommand({
...
defaultHotkey: 'Shift+H',
});
```
Despite the fact that the hotkey is a string, its format is checked at compile
time using typescript's [template literal
types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
See
[hotkey.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/hotkeys.ts)
for more details on how the hotkey syntax works, and for the available keys and
modifiers.
Note this is referred to as the 'default' hotkey because we may introduce a
feature in the future where users can modify their hotkeys, though this doesn't
exist at the moment.
### Tracks
In order to add a new track to the timeline, you'll need to create two entities:
- A track 'renderer' which controls what the track looks like and how it fetches
data from trace processor.
- A track 'node' controls where the track appears in the workspace.
Track renderers are powerful but complex, so it's, so it's strongly advised not
to create your own. Instead, by far the easiest way to get started with tracks
is to use the `createQuerySliceTrack` and `createQueryCounterTrack` helpers.
Example:
```ts
import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace) {
const title = 'My Track';
const uri = `${trace.pluginId}#MyTrack`;
const query = 'select * from slice where track_id = 123';
// Create a new track renderer based on a query
const track = await createQuerySliceTrack({
trace,
uri,
data: {
sqlSource: query,
},
});
// Register the track renderer with the core
trace.tracks.registerTrack({uri, title, track});
// Create a track node that references the track renderer using its uri
const track = new TrackNode({uri, title});
// Add the track node to the current workspace
trace.workspace.addChildInOrder(track);
}
}
```
See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/lib/tracks/query_slice_track.ts)
for detailed usage.
You can also add a counter track using `createQueryCounterTrack` which works in
a similar way.
```ts
import {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace) {
const title = 'My Counter Track';
const uri = `${trace.pluginId}#MyCounterTrack`;
const query = 'select * from counter where track_id = 123';
// Create a new track renderer based on a query
const track = await createQueryCounterTrack({
trace,
uri,
data: {
sqlSource: query,
},
});
// Register the track renderer with the core
trace.tracks.registerTrack({uri, title, track});
// Create a track node that references the track renderer using its uri
const track = new TrackNode({uri, title});
// Add the track node to the current workspace
trace.workspace.addChildInOrder(track);
}
}
```
See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/lib/tracks/query_counter_track.ts)
for detailed usage.
#### Grouping Tracks
Any track can have children. Just add child nodes any `TrackNode` object using
its `addChildXYZ()` methods. Nested tracks are rendered as a collapsible tree.
```ts
const group = new TrackNode({title: 'Group'});
trace.workspace.addChildInOrder(group);
group.addChildLast(new TrackNode({title: 'Child Track A'}));
group.addChildLast(new TrackNode({title: 'Child Track B'}));
group.addChildLast(new TrackNode({title: 'Child Track C'}));
```
Tracks nodes with children can be collapsed and expanded manually by the user at
runtime, or programmatically using their `expand()` and `collapse()` methods. By
default tracks are collapsed, so to have tracks automatically expanded on
startup you'll need to call `expand()` after adding the track node.
```ts
group.expand();
```
![Nested tracks](../images/ui-plugins/nested_tracks.png)
Summary tracks are behave slightly differently to ordinary tracks. Summary
tracks:
- Are rendered with a light blue background when collapsed, dark blue when
expanded.
- Stick to the top of the viewport when scrolling.
- Area selections made on the track apply to child tracks instead of the summary
track itself.
To create a summary track, set the `isSummary: true` option in its initializer
list at creation time or set its `isSummary` property to true after creation.
```ts
const group = new TrackNode({title: 'Group', isSummary: true});
// ~~~ or ~~~
group.isSummary = true;
```
![Summary track](../images/ui-plugins/summary_track.png)
Examples
- [com.example.ExampleNestedTracks](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleNestedTracks/index.ts).
#### Track Ordering
Tracks can be manually reordered using the `addChildXYZ()` functions available on
the track node api, including `addChildFirst()`, `addChildLast()`,
`addChildBefore()`, and `addChildAfter()`.
See [the workspace source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/workspace.ts) for detailed usage.
However, when several plugins add tracks to the same node or the workspace, no
single plugin has complete control over the sorting of child nodes within this
node. Thus, the sortOrder property is be used to decentralize the sorting logic
between plugins.
In order to do this we simply give the track a `sortOrder` and call
`addChildInOrder()` on the parent node and the track will be placed before the
first track with a higher `sortOrder` in the list. (i.e. lower `sortOrder`s appear
higher in the stack).
```ts
// PluginA
workspace.addChildInOrder(new TrackNode({title: 'Foo', sortOrder: 10}));
// Plugin B
workspace.addChildInOrder(new TrackNode({title: 'Bar', sortOrder: -10}));
```
Now it doesn't matter which order plugin are initialized, track `Bar` will
appear above track `Foo` (unless reordered later).
If no `sortOrder` is defined, the track assumes a `sortOrder` of 0.
> It is recommended to always use `addChildInOrder()` in plugins when adding
> tracks to the `workspace`, especially if you want your plugin to be enabled by
> default, as this will ensure it respects the sortOrder of other plugins.
### Tabs
Tabs are a useful way to display contextual information about the trace, the
current selection, or to show the results of an operation.
To register a tab from a plugin, use the `Trace.registerTab` method.
```ts
class MyTab implements Tab {
render(): m.Children {
return m('div', 'Hello from my tab');
}
getTitle(): string {
return 'My Tab';
}
}
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace) {
trace.registerTab({
uri: `${trace.pluginId}#MyTab`,
content: new MyTab(),
});
}
}
```
You'll need to pass in a tab-like object, something that implements the `Tab`
interface. Tabs only need to define their title and a render function which
specifies how to render the tab.
Registered tabs don't appear immediately - we need to show it first. All
registered tabs are displayed in the tab dropdown menu, and can be shown or
hidden by clicking on the entries in the drop down menu.
Tabs can also be hidden by clicking the little x in the top right of their
handle.
Alternatively, tabs may be shown or hidden programmatically using the tabs API.
```ts
trace.tabs.showTab(`${trace.pluginId}#MyTab`);
trace.tabs.hideTab(`${trace.pluginId}#MyTab`);
```
Tabs have the following properties:
- Each tab has a unique URI.
- Only once instance of the tab may be open at a time. Calling showTab multiple
times with the same URI will only activate the tab, not add a new instance of
the tab to the tab bar.
#### Ephemeral Tabs
By default, tabs are registered as 'permanent' tabs. These tabs have the
following additional properties:
- They appear in the tab dropdown.
- They remain once closed. The plugin controls the lifetime of the tab object.
Ephemeral tabs, by contrast, have the following properties:
- They do not appear in the tab dropdown.
- When they are hidden, they will be automatically unregistered.
Ephemeral tabs can be registered by setting the `isEphemeral` flag when
registering the tab.
```ts
trace.registerTab({
isEphemeral: true,
uri: `${trace.pluginId}#MyTab`,
content: new MyEphemeralTab(),
});
```
Ephemeral tabs are usually added as a result of some user action, such as
running a command. Thus, it's common pattern to register a tab and show the tab
simultaneously.
Motivating example:
```ts
import m from 'mithril';
import {uuidv4} from '../../base/uuid';
class MyNameTab implements Tab {
constructor(private name: string) {}
render(): m.Children {
return m('h1', `Hello, ${this.name}!`);
}
getTitle(): string {
return 'My Name Tab';
}
}
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace): Promise<void> {
trace.registerCommand({
id: `${trace.pluginId}#AddNewEphemeralTab`,
name: 'Add new ephemeral tab',
callback: () => handleCommand(trace),
});
}
}
function handleCommand(trace: Trace): void {
const name = prompt('What is your name');
if (name) {
const uri = `${trace.pluginId}#MyName${uuidv4()}`;
// This makes the tab available to perfetto
ctx.registerTab({
isEphemeral: true,
uri,
content: new MyNameTab(name),
});
// This opens the tab in the tab bar
ctx.tabs.showTab(uri);
}
}
```
### Details Panels & The Current Selection Tab
The "Current Selection" tab is a special tab that cannot be hidden. It remains
permanently in the left-most tab position in the tab bar. Its purpose is to
display details about the current selection.
Plugins may register interest in providing content for this tab using the
`PluginContentTrace.registerDetailsPanel()` method.
For example:
```ts
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace) {
trace.registerDetailsPanel({
render(selection: Selection) {
if (canHandleSelection(selection)) {
return m('div', 'Details for selection');
} else {
return undefined;
}
}
});
}
}
```
This function takes an object that implements the `DetailsPanel` interface,
which only requires a render function to be implemented that takes the current
selection object and returns either mithril vnodes or a falsy value.
Every render cycle, render is called on all registered details panels, and the
first registered panel to return a truthy value will be used.
Currently the winning details panel takes complete control over this tab. Also,
the order that these panels are called in is not defined, so if we have multiple
details panels competing for the same selection, the one that actually shows up
is undefined. This is a limitation of the current approach and will be updated
to a more democratic contribution model in the future.
### Sidebar Menu Items
Plugins can add new entries to the sidebar menu which appears on the left hand
side of the UI. These entries can include:
- Commands
- Links
- Arbitrary Callbacks
#### Commands
If a command is referenced, the command name and hotkey are displayed on the
sidebar item.
```ts
trace.commands.registerCommand({
id: 'sayHi',
name: 'Say hi',
callback: () => window.alert('hi'),
defaultHotkey: 'Shift+H',
});
trace.sidebar.addMenuItem({
commandId: 'sayHi',
section: 'support',
icon: 'waving_hand',
});
```
#### Links
If an href is present, the sidebar will be used as a link. This can be an
internal link to a page, or an external link.
```ts
trace.sidebar.addMenuItem({
section: 'navigation',
text: 'Plugins',
href: '#!/plugins',
});
```
#### Callbacks
Sidebar items can be instructed to execute arbitrary callbacks when the button
is clicked.
```ts
trace.sidebar.addMenuItem({
section: 'current_trace',
text: 'Copy secrets to clipboard',
action: () => copyToClipboard('...'),
});
```
If the action returns a promise, the sidebar item will show a little spinner
animation until the promise returns.
```ts
trace.sidebar.addMenuItem({
section: 'current_trace',
text: 'Prepare the data...',
action: () => new Promise((r) => setTimeout(r, 1000)),
});
```
Optional params for all types of sidebar items:
- `icon` - A material design icon to be displayed next to the sidebar menu item.
See full list [here](https://fonts.google.com/icons).
- `tooltip` - Displayed on hover
- `section` - Where to place the menu item.
- `navigation`
- `current_trace`
- `convert_trace`
- `example_traces`
- `support`
- `sortOrder` - The higher the sortOrder the higher the bar.
See the [sidebar source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/sidebar.ts)
for more detailed usage.
### Pages
Pages are entities that can be routed via the URL args, and whose content take
up the entire available space to the right of the sidebar and underneath the
topbar. Examples of pages are the timeline, record page, and query page, just to
name a few common examples.
E.g.
```
http://ui.perfetto.dev/#!/viewer <-- 'viewer' is is the current page.
```
Pages are added from a plugin by calling the `pages.registerPage` function.
Pages can be trace-less or trace-ful. Trace-less pages are pages that are to be
displayed when no trace is loaded - i.e. the record page. Trace-ful pages are
displayed only when a trace is loaded, as they typically require a trace to work
with.
You'll typically register trace-less pages in your plugin's `onActivate()`
function and trace-full pages in either `onActivate()` or `onTraceLoad()`. If
users navigate to a trace-ful page before a trace is loaded the homepage will be
shown instead.
> Note: You don't need to bind the `Trace` object for pages unlike other
> extension points, Perfetto will inject a trace object for you.
Pages should be mithril components that accept `PageWithTraceAttrs` for
trace-ful pages or `PageAttrs` for trace-less pages.
Example of a trace-less page:
```ts
import m from 'mithril';
import {PageAttrs} from '../../public/page';
class MyPage implements m.ClassComponent<PageAttrs> {
view(vnode: m.CVnode<PageAttrs>) {
return `The trace title is: ${vnode.attrs.trace.traceInfo.traceTitle}`;
}
}
// ~~~ snip ~~~
app.pages.registerPage({route: '/mypage', page: MyPage, traceless: true});
```
```ts
import m from 'mithril';
import {PageWithTraceAttrs} from '../../public/page';
class MyPage implements m.ClassComponent<PageWithTraceAttrs> {
view(_vnode_: m.CVnode<PageWithTraceAttrs>) {
return 'Hello from my page';
}
}
// ~~~ snip ~~~
app.pages.registerPage({route: '/mypage', page: MyPage});
```
Examples:
- [dev.perfetto.ExplorePage](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExplorePage/index.ts).
### 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/base/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[];
}
```
To access permalink state, call `mountStore()` on your `Trace`
object, passing in a migration function.
```typescript
default export class implements PerfettoPlugin {
static readonly id = 'com.example.MyPlugin';
async onTraceLoad(trace: Trace): Promise<void> {
const store = trace.mountStore(migrate);
}
}
function migrate(initialState: unknown): MyState {
// ...
}
```
When it comes to migration, there are two cases to consider:
- Loading a new trace
- Loading from a permalink
In case of a new trace, your migration function is called with `undefined`. In
this case you should return a default version of `MyState`:
```typescript
const DEFAULT = {favouriteSlices: []};
function migrate(initialState: unknown): MyState {
if (initialState === undefined) {
// Return default version of MyState.
return DEFAULT;
} else {
// Migrate old version here.
}
}
```
In the permalink case, your migration function is called with the state of the
plugin store at the time the permalink was generated. This may be from an older
or newer version of the plugin.
**Plugins must not make assumptions about the contents of `initialState`!**
In this case you need to carefully validate the state object. This could be
achieved in several ways, none of which are particularly straight forward. State
migration is difficult!
One brute force way would be to use a version number.
```typescript
interface MyState {
version: number;
favouriteSlices: MySliceInfo[];
}
const VERSION = 3;
const DEFAULT = {favouriteSlices: []};
function migrate(initialState: unknown): MyState {
if (initialState && (initialState as {version: any}).version === VERSION) {
// Version number checks out, assume the structure is correct.
return initialState as State;
} else {
// Null, undefined, or bad version number - return default value.
return DEFAULT;
}
}
```
You'll need to remember to update your version number when making changes!
Migration should be unit-tested to ensure compatibility.
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/](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
## Default plugins
Some plugins are enabled by default. These plugins are held to a higher quality
than non-default plugins since changes to those plugins effect all users of the
UI. The list of default plugins is specified at
[ui/src/core/default_plugins.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core/default_plugins.ts).
## 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.