• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# UI plugins
2The Perfetto UI can be extended with plugins. These plugins are shipped
3part of Perfetto.
4
5## Create a plugin
6The guide below explains how to create a plugin for the Perfetto UI.
7
8### Prepare for UI development
9First we need to prepare the UI development environment.
10You will need to use a MacOS or Linux machine.
11Follow the steps below or see the
12[Getting Started](./getting-started) guide for more detail.
13
14```sh
15git clone https://android.googlesource.com/platform/external/perfetto/
16cd perfetto
17./tools/install-build-deps --ui
18```
19
20### Copy the plugin skeleton
21```sh
22cp -r ui/src/plugins/com.example.Skeleton ui/src/plugins/<your-plugin-name>
23```
24Now edit `ui/src/plugins/<your-plugin-name>/index.ts`.
25Search for all instances of `SKELETON: <instruction>` in the file and
26follow the instructions.
27
28Notes on naming:
29- Don't name the directory `XyzPlugin` just `Xyz`.
30- The `pluginId` and directory name must match.
31- Plugins should be prefixed with the reversed components of a domain
32  name you control. For example if `example.com` is your domain your
33  plugin should be named `com.example.Foo`.
34- Core plugins maintained by the Perfetto team should use
35  `dev.perfetto.Foo`.
36- Commands should have ids with the pattern `example.com#DoSomething`
37- Command's ids should be prefixed with the id of the plugin which
38  provides them.
39- Command names should have the form "Verb something something", and should be
40  in normal sentence case. I.e. don't capitalize the first letter of each word.
41  - Good: "Pin janky frame timeline tracks"
42  - Bad: "Tracks are Displayed if Janky"
43
44### Start the dev server
45```sh
46./ui/run-dev-server
47```
48Now navigate to [localhost:10000](http://localhost:10000/)
49
50### Enable your plugin
51- Navigate to the plugins page: [localhost:10000/#!/plugins](http://localhost:10000/#!/plugins).
52- Ctrl-F for your plugin name and enable it.
53
54Later you can request for your plugin to be enabled by default.
55Follow the [default plugins](#default-plugins) section for this.
56
57### Upload your plugin for review
58- Update `ui/src/plugins/<your-plugin-name>/OWNERS` to include your email.
59- Follow the [Contributing](./getting-started#contributing)
60  instructions to upload your CL to the codereview tool.
61- Once uploaded add `stevegolton@google.com` as a reviewer for your CL.
62
63## Plugin extension points
64Plugins can extend a handful of specific places in the UI. The sections
65below show these extension points and give examples of how they can be
66used.
67
68### Commands
69Commands are user issuable shortcuts for actions in the UI.
70They can be accessed via the omnibox.
71
72Follow the [create a plugin](#create-a-plugin) to get an initial
73skeleton for your plugin.
74
75To add your first command, add a call to `ctx.registerCommand()` in either
76your `onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
77commands in `onActivate()` by default unless they require something from
78`PluginContextTrace` which is not available on `PluginContext`.
79
80The tradeoff is that commands registered in `onTraceLoad()` are only available
81while a trace is loaded, whereas commands registered in `onActivate()` are
82available all the time the plugin is active.
83
84```typescript
85class MyPlugin implements Plugin {
86  onActivate(ctx: PluginContext): void {
87    ctx.registerCommand(
88       {
89         id: 'dev.perfetto.ExampleSimpleCommand#LogHelloPlugin',
90         name: 'Log "Hello, plugin!"',
91         callback: () => console.log('Hello, plugin!'),
92       },
93    );
94  }
95
96  onTraceLoad(ctx: PluginContextTrace): void {
97    ctx.registerCommand(
98       {
99         id: 'dev.perfetto.ExampleSimpleTraceCommand#LogHelloTrace',
100         name: 'Log "Hello, trace!"',
101         callback: () => console.log('Hello, trace!'),
102       },
103    );
104  }
105}
106```
107
108Here `id` is a unique string which identifies this command.
109The `id` should be prefixed with the plugin id followed by a `#`. All command
110`id`s must be unique system-wide.
111`name` is a human readable name for the command, which is shown in the command
112palette.
113Finally `callback()` is the callback which actually performs the
114action.
115
116Commands are removed automatically when their context disappears. Commands
117registered with the `PluginContext` are removed when the plugin is deactivated,
118and commands registered with the `PluginContextTrace` are removed when the trace
119is unloaded.
120
121Examples:
122- [dev.perfetto.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts).
123- [dev.perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.CoreCommands/index.ts).
124- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
125
126#### Hotkeys
127
128A default hotkey may be provided when registering a command.
129
130```typescript
131ctx.registerCommand({
132  id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
133  name: 'Log "Hello, World!"',
134  callback: () => console.log('Hello, World!'),
135  defaultHotkey: 'Shift+H',
136});
137```
138
139Even though the hotkey is a string, it's format checked at compile time using
140typescript's [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
141
142See [hotkey.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/hotkeys.ts)
143for more details on how the hotkey syntax works, and for the available keys and
144modifiers.
145
146### Tracks
147#### Defining Tracks
148Tracks describe how to render a track and how to respond to mouse interaction.
149However, the interface is a WIP and should be considered unstable.
150This documentation will be added to over the next few months after the design is
151finalised.
152
153#### Reusing Existing Tracks
154Creating tracks from scratch is difficult and the API is currently a WIP, so it
155is strongly recommended to use one of our existing base classes which do a lot
156of the heavy lifting for you. These base classes also provide a more stable
157layer between your track and the (currently unstable) track API.
158
159For example, if your track needs to show slices from a given a SQL expression (a
160very common pattern), extend the `NamedSliceTrack` abstract base class and
161implement `getSqlSource()`, which should return a query with the following
162columns:
163
164- `id: INTEGER`: A unique ID for the slice.
165- `ts: INTEGER`: The timestamp of the start of the slice.
166- `dur: INTEGER`: The duration of the slice.
167- `depth: INTEGER`: Integer value defining how deep the slice should be drawn in
168    the track, 0 being rendered at the top of the track, and increasing numbers
169    being drawn towards the bottom of the track.
170- `name: TEXT`: Text to be rendered on the slice and in the popup.
171
172For example, the following track describes a slice track that displays all
173slices that begin with the letter 'a'.
174```ts
175class MyTrack extends NamedSliceTrack {
176  getSqlSource(): string {
177    return `
178    SELECT
179      id,
180      ts,
181      dur,
182      depth,
183      name
184    from slice
185    where name like 'a%'
186    `;
187  }
188}
189```
190
191#### Registering Tracks
192Plugins may register tracks with Perfetto using
193`PluginContextTrace.registerTrack()`, usually in their `onTraceLoad` function.
194
195```ts
196class MyPlugin implements Plugin {
197  onTraceLoad(ctx: PluginContextTrace): void {
198    ctx.registerTrack({
199      uri: 'dev.MyPlugin#ExampleTrack',
200      displayName: 'My Example Track',
201      trackFactory: ({trackKey}) => {
202        return new MyTrack({engine: ctx.engine, trackKey});
203      },
204    });
205  }
206}
207```
208
209#### Default Tracks
210The "default" tracks are a list of tracks that are added to the timeline when a
211fresh trace is loaded (i.e. **not** when loading a trace from a permalink).
212This list is copied into the timeline after the trace has finished loading, at
213which point control is handed over to the user, allowing them add, remove and
214reorder tracks as they please.
215Thus it only makes sense to add default tracks in your plugin's `onTraceLoad`
216function, as adding a default track later will have no effect.
217
218```ts
219class MyPlugin implements Plugin {
220  onTraceLoad(ctx: PluginContextTrace): void {
221    ctx.registerTrack({
222      // ... as above ...
223    });
224
225    ctx.addDefaultTrack({
226      uri: 'dev.MyPlugin#ExampleTrack',
227      displayName: 'My Example Track',
228      sortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
229    });
230  }
231}
232```
233
234Registering and adding a default track is such a common pattern that there is a
235shortcut for doing both in one go: `PluginContextTrace.registerStaticTrack()`,
236which saves having to repeat the URI and display name.
237
238```ts
239class MyPlugin implements Plugin {
240  onTraceLoad(ctx: PluginContextTrace): void {
241    ctx.registerStaticTrack({
242      uri: 'dev.MyPlugin#ExampleTrack',
243      displayName: 'My Example Track',
244      trackFactory: ({trackKey}) => {
245        return new MyTrack({engine: ctx.engine, trackKey});
246      },
247      sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
248    });
249  }
250}
251```
252
253#### Adding Tracks Directly
254Sometimes plugins might want to add a track to the timeline immediately, usually
255as a result of a command or on some other user action such as a button click.
256We can do this using `PluginContext.timeline.addTrack()`.
257
258```ts
259class MyPlugin implements Plugin {
260  onTraceLoad(ctx: PluginContextTrace): void {
261    ctx.registerTrack({
262      // ... as above ...
263    });
264
265    // Register a command that directly adds a new track to the timeline
266    ctx.registerCommand({
267      id: 'dev.MyPlugin#AddMyTrack',
268      name: 'Add my track',
269      callback: () => {
270        ctx.timeline.addTrack(
271          'dev.MyPlugin#ExampleTrack',
272          'My Example Track'
273        );
274      },
275    });
276  }
277}
278```
279
280### Tabs
281Tabs are a useful way to display contextual information about the trace, the
282current selection, or to show the results of an operation.
283
284To register a tab from a plugin, use the `PluginContextTrace.registerTab`
285method.
286
287```ts
288import m from 'mithril';
289import {Tab, Plugin, PluginContext, PluginContextTrace} from '../../public';
290
291class MyTab implements Tab {
292  render(): m.Children {
293    return m('div', 'Hello from my tab');
294  }
295
296  getTitle(): string {
297    return 'My Tab';
298  }
299}
300
301class MyPlugin implements Plugin {
302  onActivate(_: PluginContext): void {}
303  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
304    ctx.registerTab({
305      uri: 'dev.MyPlugin#MyTab',
306      content: new MyTab(),
307    });
308  }
309}
310```
311
312You'll need to pass in a tab-like object, something that implements the `Tab`
313interface. Tabs only need to define their title and a render function which
314specifies how to render the tab.
315
316Registered tabs don't appear immediately - we need to show it first. All
317registered tabs are displayed in the tab dropdown menu, and can be shown or
318hidden by clicking on the entries in the drop down menu.
319
320Tabs can also be hidden by clicking the little x in the top right of their
321handle.
322
323Alternatively, tabs may be shown or hidden programmatically using the tabs API.
324
325```ts
326ctx.tabs.showTab('dev.MyPlugin#MyTab');
327ctx.tabs.hideTab('dev.MyPlugin#MyTab');
328```
329
330Tabs have the following properties:
331- Each tab has a unique URI.
332- Only once instance of the tab may be open at a time. Calling showTab multiple
333  times with the same URI will only activate the tab, not add a new instance of
334  the tab to the tab bar.
335
336#### Ephemeral Tabs
337
338By default, tabs are registered as 'permanent' tabs. These tabs have the
339following additional properties:
340- They appear in the tab dropdown.
341- They remain once closed. The plugin controls the lifetime of the tab object.
342
343Ephemeral tabs, by contrast, have the following properties:
344- They do not appear in the tab dropdown.
345- When they are hidden, they will be automatically unregistered.
346
347Ephemeral tabs can be registered by setting the `isEphemeral` flag when
348registering the tab.
349
350```ts
351ctx.registerTab({
352  isEphemeral: true,
353  uri: 'dev.MyPlugin#MyTab',
354  content: new MyEphemeralTab(),
355});
356```
357
358Ephemeral tabs are usually added as a result of some user action, such as
359running a command. Thus, it's common pattern to register a tab and show the tab
360simultaneously.
361
362Motivating example:
363```ts
364import m from 'mithril';
365import {uuidv4} from '../../base/uuid';
366import {
367  Plugin,
368  PluginContext,
369  PluginContextTrace,
370  PluginDescriptor,
371  Tab,
372} from '../../public';
373
374class MyNameTab implements Tab {
375  constructor(private name: string) {}
376  render(): m.Children {
377    return m('h1', `Hello, ${this.name}!`);
378  }
379  getTitle(): string {
380    return 'My Name Tab';
381  }
382}
383
384class MyPlugin implements Plugin {
385  onActivate(_: PluginContext): void {}
386  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
387    ctx.registerCommand({
388      id: 'dev.MyPlugin#AddNewEphemeralTab',
389      name: 'Add new ephemeral tab',
390      callback: () => handleCommand(ctx),
391    });
392  }
393}
394
395function handleCommand(ctx: PluginContextTrace): void {
396  const name = prompt('What is your name');
397  if (name) {
398    const uri = 'dev.MyPlugin#MyName' + uuidv4();
399    // This makes the tab available to perfetto
400    ctx.registerTab({
401      isEphemeral: true,
402      uri,
403      content: new MyNameTab(name),
404    });
405
406    // This opens the tab in the tab bar
407    ctx.tabs.showTab(uri);
408  }
409}
410
411export const plugin: PluginDescriptor = {
412  pluginId: 'dev.MyPlugin',
413  plugin: MyPlugin,
414};
415```
416
417### Details Panels & The Current Selection Tab
418The "Current Selection" tab is a special tab that cannot be hidden. It remains
419permanently in the left-most tab position in the tab bar. Its purpose is to
420display details about the current selection.
421
422Plugins may register interest in providing content for this tab using the
423`PluginContentTrace.registerDetailsPanel()` method.
424
425For example:
426
427```ts
428class MyPlugin implements Plugin {
429  onActivate(_: PluginContext): void {}
430  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
431    ctx.registerDetailsPanel({
432      render(selection: Selection) {
433        if (canHandleSelection(selection)) {
434          return m('div', 'Details for selection');
435        } else {
436          return undefined;
437        }
438      }
439    });
440  }
441}
442```
443
444This function takes an object that implements the `DetailsPanel` interface,
445which only requires a render function to be implemented that takes the current
446selection object and returns either mithril vnodes or a falsy value.
447
448Every render cycle, render is called on all registered details panels, and the
449first registered panel to return a truthy value will be used.
450
451Currently the winning details panel takes complete control over this tab. Also,
452the order that these panels are called in is not defined, so if we have multiple
453details panels competing for the same selection, the one that actually shows up
454is undefined. This is a limitation of the current approach and will be updated
455to a more democratic contribution model in the future.
456
457### Metric Visualisations
458TBD
459
460Examples:
461- [dev.perfetto.AndroidBinderViz](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts).
462
463### State
464NOTE: It is important to consider version skew when using persistent state.
465
466Plugins can persist information into permalinks. This allows plugins
467to gracefully handle permalinking and is an opt-in - not automatic -
468mechanism.
469
470Persistent plugin state works using a `Store<T>` where `T` is some JSON
471serializable object.
472`Store` is implemented [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/store.ts).
473`Store` allows for reading and writing `T`.
474Reading:
475```typescript
476interface Foo {
477  bar: string;
478}
479
480const store: Store<Foo> = getFooStoreSomehow();
481
482// store.state is immutable and must not be edited.
483const foo = store.state.foo;
484const bar = foo.bar;
485
486console.log(bar);
487```
488
489Writing:
490```typescript
491interface Foo {
492  bar: string;
493}
494
495const store: Store<Foo> = getFooStoreSomehow();
496
497store.edit((draft) => {
498  draft.foo.bar = 'Hello, world!';
499});
500
501console.log(store.state.foo.bar);
502// > Hello, world!
503```
504
505First define an interface for your specific plugin state.
506```typescript
507interface MyState {
508  favouriteSlices: MySliceInfo[];
509}
510```
511
512To access permalink state, call `mountStore()` on your `PluginContextTrace`
513object, passing in a migration function.
514```typescript
515class MyPlugin implements Plugin {
516  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
517    const store = ctx.mountStore(migrate);
518  }
519}
520
521function migrate(initialState: unknown): MyState {
522  // ...
523}
524```
525
526When it comes to migration, there are two cases to consider:
527- Loading a new trace
528- Loading from a permalink
529
530In case of a new trace, your migration function is called with `undefined`. In
531this case you should return a default version of `MyState`:
532```typescript
533const DEFAULT = {favouriteSlices: []};
534
535function migrate(initialState: unknown): MyState {
536  if (initialState === undefined) {
537    // Return default version of MyState.
538    return DEFAULT;
539  } else {
540    // Migrate old version here.
541  }
542}
543```
544
545In the permalink case, your migration function is called with the state of the
546plugin store at the time the permalink was generated. This may be from an older
547or newer version of the plugin.
548
549**Plugins must not make assumptions about the contents of `initialState`!**
550
551In this case you need to carefully validate the state object. This could be
552achieved in several ways, none of which are particularly straight forward. State
553migration is difficult!
554
555One brute force way would be to use a version number.
556
557```typescript
558interface MyState {
559  version: number;
560  favouriteSlices: MySliceInfo[];
561}
562
563const VERSION = 3;
564const DEFAULT = {favouriteSlices: []};
565
566function migrate(initialState: unknown): MyState {
567  if (initialState && (initialState as {version: any}).version === VERSION) {
568    // Version number checks out, assume the structure is correct.
569    return initialState as State;
570  } else {
571    // Null, undefined, or bad version number - return default value.
572    return DEFAULT;
573  }
574}
575```
576
577You'll need to remember to update your version number when making changes!
578Migration should be unit-tested to ensure compatibility.
579
580Examples:
581- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
582
583## Guide to the plugin API
584The 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).
585
586## Default plugins
587Some plugins are enabled by default.
588These plugins are held to a higher quality than non-default plugins since changes to those plugins effect all users of the UI.
589The 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/common/default_plugins.ts).
590
591## Misc notes
592- Plugins must be licensed under
593  [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
594  the same as all other code in the repository.
595- Plugins are the responsibility of the OWNERS of that plugin to
596  maintain, not the responsibility of the Perfetto team. All
597  efforts will be made to keep the plugin API stable and existing
598  plugins working however plugins that remain unmaintained for long
599  periods of time will be disabled and ultimately deleted.
600
601