xref: /aosp_15_r20/external/perfetto/docs/contributing/ui-plugins.md (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1# UI plugins
2The Perfetto UI can be extended with plugins. These plugins are shipped part of
3Perfetto.
4
5## Create a plugin
6The guide below explains how to create a plugin for the Perfetto UI. You can
7browse the public plugin API [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
8
9### Prepare for UI development
10First we need to prepare the UI development environment. You will need to use a
11MacOS or Linux machine. Follow the steps below or see the [Getting
12Started](./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`. Search for all instances
25of `SKELETON: <instruction>` in the file and follow the instructions.
26
27Notes on naming:
28- Don't name the directory `XyzPlugin` just `Xyz`.
29- The `pluginId` and directory name must match.
30- Plugins should be prefixed with the reversed components of a domain name you
31  control. For example if `example.com` is your domain your plugin should be
32  named `com.example.Foo`.
33- Core plugins maintained by the Perfetto team should use `dev.perfetto.Foo`.
34- Commands should have ids with the pattern `example.com#DoSomething`
35- Command's ids should be prefixed with the id of the plugin which provides
36  them.
37- Command names should have the form "Verb something something", and should be
38  in normal sentence case. I.e. don't capitalize the first letter of each word.
39  - Good: "Pin janky frame timeline tracks"
40  - Bad: "Tracks are Displayed if Janky"
41
42### Start the dev server
43```sh
44./ui/run-dev-server
45```
46Now navigate to [localhost:10000](http://localhost:10000/)
47
48### Enable your plugin
49- Navigate to the plugins page:
50  [localhost:10000/#!/plugins](http://localhost:10000/#!/plugins).
51- Ctrl-F for your plugin name and enable it.
52- Enabling/disabling plugins requires a restart of the UI, so refresh the page
53  to start your plugin.
54
55Later you can request for your plugin to be enabled by default. Follow the
56[default plugins](#default-plugins) section for this.
57
58### Upload your plugin for review
59- Update `ui/src/plugins/<your-plugin-name>/OWNERS` to include your email.
60- Follow the [Contributing](./getting-started#contributing) instructions to
61  upload your CL to the codereview tool.
62- Once uploaded add `[email protected]` as a reviewer for your CL.
63
64## Plugin Lifecycle
65To demonstrate the plugin's lifecycle, this is a minimal plugin that implements
66the key lifecycle hooks:
67
68```ts
69default export class implements PerfettoPlugin {
70  static readonly id = 'com.example.MyPlugin';
71
72  static onActivate(app: App): void {
73    // Called once on app startup
74    console.log('MyPlugin::onActivate()', app.pluginId);
75    // Note: It's rare that plugins would need this hook as most plugins are
76    // interested in trace details. Thus, this function can usually be omitted.
77  }
78
79  constructor(trace: Trace) {
80    // Called each time a trace is loaded
81    console.log('MyPlugin::constructor()', trace.traceInfo.traceTitle);
82  }
83
84  async onTraceLoad(trace: Trace): Promise<void> {
85    // Called each time a trace is loaded
86    console.log('MyPlugin::onTraceLoad()', trace.traceInfo.traceTitle);
87    // Note this function returns a promise, so any any async calls should be
88    // completed before this promise resolves as the app using this promise for
89    // timing and plugin synchronization.
90  }
91}
92```
93
94You can run this plugin with devtools to see the log messages in the console,
95which should give you a feel for the plugin lifecycle. Try opening a few traces
96one after another.
97
98`onActivate()` runs shortly after Perfetto starts up, before a trace is loaded.
99This is where the you'll configure your plugin's capabilities that aren't trace
100dependent. At this point the plugin's class is not instantiated, so you'll
101notice `onActivate()` hook is a static class member. `onActivate()` is only ever
102called once, regardless of the number of traces loaded.
103
104`onActivate()` is passed an `App` object which the plugin can use to configure
105core capabilities such as commands, sidebar items and pages. Capabilities
106registered on the App interface are persisted throughout the lifetime of the app
107(practically forever until the tab is closed), in contrast to what happens for
108the same methods on the `Trace` object (see below).
109
110The plugin class in instantiated when a trace is loaded (a new plugin instance
111is created for each trace). `onTraceLoad()` is called immediately after the
112class is instantiated, which is where you'll configure your plugin's trace
113dependent capabilities.
114
115`onTraceLoad()` is passed a `Trace` object which the plugin can use to configure
116entities that are scoped to a specific trace, such as tracks and tabs. `Trace`
117is a superset of `App`, so anything you can do with `App` you can also do with
118`Trace`, however, capabilities registered on `Trace` will typically be discarded
119when a new trace is loaded.
120
121A plugin will typically register capabilities with the core and return quickly.
122But these capabilities usually contain objects and callbacks which are called
123into later by the core during the runtime of the app. Most capabilities require
124a `Trace` or an `App` to do anything useful so these are usually bound into the
125capabilities at registration time using JavaScript classes or closures.
126
127```ts
128// Toy example: Code will not compile.
129async onTraceLoad(trace: Trace) {
130  // `trace` is captured in the closure and used later by the app
131  trace.regsterXYZ(() => trace.xyz);
132}
133```
134
135That way, the callback is bound to a specific trace object which and the trace
136object can outlive the runtime of the `onTraceLoad()` function, which is a very
137common pattern in Perfetto plugins.
138
139> Note: Some capabilities can be registered on either the `App` or the `Trace`
140> object (i.e. in `onActivate()` or in `onTraceLoad()`), if in doubt about which
141> one to use, use `onTraceLoad()` as this is more than likely the one you want.
142> Most plugins add tracks and tabs that depend on the trace. You'd usually have
143> to be doing something out of the ordinary if you need to use `onActivate()`.
144
145### Performance
146`onActivate()` and `onTraceLoad()` should generally complete as quickly as
147possible, however sometimes `onTraceLoad()` may need to perform async operations
148on trace processor such as performing queries and/or creating views and tables.
149Thus, `onTraceLoad()` should return a promise (or you can simply make it an
150async function). When this promise resolves it tells the core that the plugin is
151fully initialized.
152
153> Note: It's important that any async operations done in onTraceLoad() are
154> awaited so that all async operations are completed by the time the promise is
155> resolved. This is so that plugins can be properly timed and synchronized.
156
157
158```ts
159// GOOD
160async onTraceLoad(trace: Trace) {
161  await trace.engine.query(...);
162}
163
164// BAD
165async onTraceLoad(trace: Trace) {
166  // Note the missing await!
167  trace.engine.query(...);
168}
169```
170
171## Extension Points
172Plugins can extend functionality of Perfetto by registering capabilities via
173extension points on the `App` or `Trace` objects.
174
175The following sections delve into more detail on each extension point and
176provide examples of how they can be used.
177
178### Commands
179Commands are user issuable shortcuts for actions in the UI. They are invoked via
180the command palette which can be opened by pressing Ctrl+Shift+P (or Cmd+Shift+P
181on Mac), or by typing a '>' into the omnibox.
182
183To add a command, add a call to `registerCommand()` on either your
184`onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
185commands in `onTraceLoad()` by default unless you very specifically want the
186command to be available before a trace has loaded.
187
188Example of a command that doesn't require a trace.
189```ts
190default export class implements PerfettoPlugin {
191  static readonly id = 'com.example.MyPlugin';
192  static onActivate(app: App) {
193    app.commands.registerCommand({
194      id: `${app.pluginId}#SayHello`,
195      name: 'Say hello',
196      callback: () => console.log('Hello, world!'),
197    });
198  }
199}
200```
201
202Example of a command that requires a trace object - in this case the trace
203title.
204```ts
205default export class implements PerfettoPlugin {
206  static readonly id = 'com.example.MyPlugin';
207  async onTraceLoad(trace: Trace) {
208    trace.commands.registerCommand({
209      id: `${trace.pluginId}#LogTraceTitle`,
210      name: 'Log trace title',
211      callback: () => console.log(trace.info.traceTitle),
212    });
213  }
214}
215```
216
217> Notice that the trace object is captured in the closure, so it can be used
218> after the onTraceLoad() function has returned. This is a very common pattern
219> in Perfetto plugins.
220
221Command arguments explained:
222- `id` is a unique string which identifies this command. The `id` should be
223prefixed with the plugin id followed by a `#`. All command `id`s must be unique
224system-wide.
225- `name` is a human readable name for the command, which is shown in the command
226palette.
227- `callback()` is the callback which actually performs the action.
228
229#### Async commands
230It's common that commands will perform async operations in their callbacks. It's
231recommended to use async/await for this rather than `.then().catch()`. The
232easiest way to do this is to make the callback an async function.
233
234```ts
235default export class implements PerfettoPlugin {
236  static readonly id = 'com.example.MyPlugin';
237  async onTraceLoad(trace: Trace) {
238    trace.commands.registerCommand({
239      id: `${trace.pluginId}#QueryTraceProcessor`,
240      name: 'Query trace processor',
241      callback: async () => {
242        const results = await trace.engine.query(...);
243        // use results...
244      },
245    });
246  }
247}
248```
249
250If the callback is async (i.e. it returns a promise), nothing special happens.
251The command is still fire-n-forget as far as the core is concerned.
252
253Examples:
254- [com.example.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts).
255- [perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core_plugins/commands/index.ts).
256- [com.example.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleState/index.ts).
257
258### Hotkeys
259A hotkey may be associated with a command at registration time.
260
261```typescript
262ctx.commands.registerCommand({
263  ...
264  defaultHotkey: 'Shift+H',
265});
266```
267
268Despite the fact that the hotkey is a string, its format is checked at compile
269time using typescript's [template literal
270types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
271
272See
273[hotkey.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/hotkeys.ts)
274for more details on how the hotkey syntax works, and for the available keys and
275modifiers.
276
277Note this is referred to as the 'default' hotkey because we may introduce a
278feature in the future where users can modify their hotkeys, though this doesn't
279exist at the moment.
280
281### Tracks
282In order to add a new track to the timeline, you'll need to create two entities:
283- A track 'renderer' which controls what the track looks like and how it fetches
284  data from trace processor.
285- A track 'node' controls where the track appears in the workspace.
286
287Track renderers are powerful but complex, so it's, so it's strongly advised not
288to create your own. Instead, by far the easiest way to get started with tracks
289is to use the `createQuerySliceTrack` and `createQueryCounterTrack` helpers.
290
291Example:
292```ts
293import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
294
295default export class implements PerfettoPlugin {
296  static readonly id = 'com.example.MyPlugin';
297  async onTraceLoad(trace: Trace) {
298    const title = 'My Track';
299    const uri = `${trace.pluginId}#MyTrack`;
300    const query = 'select * from slice where track_id = 123';
301
302    // Create a new track renderer based on a query
303    const track = await createQuerySliceTrack({
304      trace,
305      uri,
306      data: {
307        sqlSource: query,
308      },
309    });
310
311    // Register the track renderer with the core
312    trace.tracks.registerTrack({uri, title, track});
313
314    // Create a track node that references the track renderer using its uri
315    const trackNode = new TrackNode({uri, title});
316
317    // Add the track node to the current workspace
318    trace.workspace.addChildInOrder(trackNode);
319  }
320}
321```
322
323See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/components/tracks/query_slice_track.ts)
324for detailed usage.
325
326You can also add a counter track using `createQueryCounterTrack` which works in
327a similar way.
328
329```ts
330import {createQueryCounterTrack} from '../../components/tracks/query_counter_track';
331
332default export class implements PerfettoPlugin {
333  static readonly id = 'com.example.MyPlugin';
334  async onTraceLoad(trace: Trace) {
335    const title = 'My Counter Track';
336    const uri = `${trace.pluginId}#MyCounterTrack`;
337    const query = 'select * from counter where track_id = 123';
338
339    // Create a new track renderer based on a query
340    const track = await createQueryCounterTrack({
341      trace,
342      uri,
343      data: {
344        sqlSource: query,
345      },
346    });
347
348    // Register the track renderer with the core
349    trace.tracks.registerTrack({uri, title, track});
350
351    // Create a track node that references the track renderer using its uri
352    const trackNode = new TrackNode({uri, title});
353
354    // Add the track node to the current workspace
355    trace.workspace.addChildInOrder(trackNode);
356  }
357}
358```
359
360See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/components/tracks/query_counter_track.ts)
361for detailed usage.
362
363#### Grouping Tracks
364Any track can have children. Just add child nodes any `TrackNode` object using
365its `addChildXYZ()` methods. Nested tracks are rendered as a collapsible tree.
366
367```ts
368const group = new TrackNode({title: 'Group'});
369trace.workspace.addChildInOrder(group);
370group.addChildLast(new TrackNode({title: 'Child Track A'}));
371group.addChildLast(new TrackNode({title: 'Child Track B'}));
372group.addChildLast(new TrackNode({title: 'Child Track C'}));
373```
374
375Tracks nodes with children can be collapsed and expanded manually by the user at
376runtime, or programmatically using their `expand()` and `collapse()` methods. By
377default tracks are collapsed, so to have tracks automatically expanded on
378startup you'll need to call `expand()` after adding the track node.
379
380```ts
381group.expand();
382```
383
384![Nested tracks](../images/ui-plugins/nested_tracks.png)
385
386Summary tracks are behave slightly differently to ordinary tracks. Summary
387tracks:
388- Are rendered with a light blue background when collapsed, dark blue when
389  expanded.
390- Stick to the top of the viewport when scrolling.
391- Area selections made on the track apply to child tracks instead of the summary
392  track itself.
393
394To create a summary track, set the `isSummary: true` option in its initializer
395list at creation time or set its `isSummary` property to true after creation.
396
397```ts
398const group = new TrackNode({title: 'Group', isSummary: true});
399// ~~~ or ~~~
400group.isSummary = true;
401```
402
403![Summary track](../images/ui-plugins/summary_track.png)
404
405Examples
406- [com.example.ExampleNestedTracks](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleNestedTracks/index.ts).
407
408#### Track Ordering
409Tracks can be manually reordered using the `addChildXYZ()` functions available on
410the track node api, including `addChildFirst()`, `addChildLast()`,
411`addChildBefore()`, and `addChildAfter()`.
412
413See [the workspace source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/workspace.ts) for detailed usage.
414
415However, when several plugins add tracks to the same node or the workspace, no
416single plugin has complete control over the sorting of child nodes within this
417node. Thus, the sortOrder property is be used to decentralize the sorting logic
418between plugins.
419
420In order to do this we simply give the track a `sortOrder` and call
421`addChildInOrder()` on the parent node and the track will be placed before the
422first track with a higher `sortOrder` in the list. (i.e. lower `sortOrder`s appear
423higher in the stack).
424
425```ts
426// PluginA
427workspace.addChildInOrder(new TrackNode({title: 'Foo', sortOrder: 10}));
428
429// Plugin B
430workspace.addChildInOrder(new TrackNode({title: 'Bar', sortOrder: -10}));
431```
432
433Now it doesn't matter which order plugin are initialized, track `Bar` will
434appear above track `Foo` (unless reordered later).
435
436If no `sortOrder` is defined, the track assumes a `sortOrder` of 0.
437
438> It is recommended to always use `addChildInOrder()` in plugins when adding
439> tracks to the `workspace`, especially if you want your plugin to be enabled by
440> default, as this will ensure it respects the sortOrder of other plugins.
441
442
443### Tabs
444Tabs are a useful way to display contextual information about the trace, the
445current selection, or to show the results of an operation.
446
447To register a tab from a plugin, use the `Trace.registerTab` method.
448
449```ts
450class MyTab implements Tab {
451  render(): m.Children {
452    return m('div', 'Hello from my tab');
453  }
454
455  getTitle(): string {
456    return 'My Tab';
457  }
458}
459
460default export class implements PerfettoPlugin {
461  static readonly id = 'com.example.MyPlugin';
462  async onTraceLoad(trace: Trace) {
463    trace.registerTab({
464      uri: `${trace.pluginId}#MyTab`,
465      content: new MyTab(),
466    });
467  }
468}
469```
470
471You'll need to pass in a tab-like object, something that implements the `Tab`
472interface. Tabs only need to define their title and a render function which
473specifies how to render the tab.
474
475Registered tabs don't appear immediately - we need to show it first. All
476registered tabs are displayed in the tab dropdown menu, and can be shown or
477hidden by clicking on the entries in the drop down menu.
478
479Tabs can also be hidden by clicking the little x in the top right of their
480handle.
481
482Alternatively, tabs may be shown or hidden programmatically using the tabs API.
483
484```ts
485trace.tabs.showTab(`${trace.pluginId}#MyTab`);
486trace.tabs.hideTab(`${trace.pluginId}#MyTab`);
487```
488
489Tabs have the following properties:
490- Each tab has a unique URI.
491- Only once instance of the tab may be open at a time. Calling showTab multiple
492  times with the same URI will only activate the tab, not add a new instance of
493  the tab to the tab bar.
494
495#### Ephemeral Tabs
496
497By default, tabs are registered as 'permanent' tabs. These tabs have the
498following additional properties:
499- They appear in the tab dropdown.
500- They remain once closed. The plugin controls the lifetime of the tab object.
501
502Ephemeral tabs, by contrast, have the following properties:
503- They do not appear in the tab dropdown.
504- When they are hidden, they will be automatically unregistered.
505
506Ephemeral tabs can be registered by setting the `isEphemeral` flag when
507registering the tab.
508
509```ts
510trace.registerTab({
511  isEphemeral: true,
512  uri: `${trace.pluginId}#MyTab`,
513  content: new MyEphemeralTab(),
514});
515```
516
517Ephemeral tabs are usually added as a result of some user action, such as
518running a command. Thus, it's common pattern to register a tab and show the tab
519simultaneously.
520
521Motivating example:
522```ts
523import m from 'mithril';
524import {uuidv4} from '../../base/uuid';
525
526class MyNameTab implements Tab {
527  constructor(private name: string) {}
528  render(): m.Children {
529    return m('h1', `Hello, ${this.name}!`);
530  }
531  getTitle(): string {
532    return 'My Name Tab';
533  }
534}
535
536default export class implements PerfettoPlugin {
537  static readonly id = 'com.example.MyPlugin';
538  async onTraceLoad(trace: Trace): Promise<void> {
539    trace.registerCommand({
540      id: `${trace.pluginId}#AddNewEphemeralTab`,
541      name: 'Add new ephemeral tab',
542      callback: () => handleCommand(trace),
543    });
544  }
545}
546
547function handleCommand(trace: Trace): void {
548  const name = prompt('What is your name');
549  if (name) {
550    const uri = `${trace.pluginId}#MyName${uuidv4()}`;
551    // This makes the tab available to perfetto
552    ctx.registerTab({
553      isEphemeral: true,
554      uri,
555      content: new MyNameTab(name),
556    });
557
558    // This opens the tab in the tab bar
559    ctx.tabs.showTab(uri);
560  }
561}
562```
563
564### Details Panels & The Current Selection Tab
565The "Current Selection" tab is a special tab that cannot be hidden. It remains
566permanently in the left-most tab position in the tab bar. Its purpose is to
567display details about the current selection.
568
569Plugins may register interest in providing content for this tab using the
570`PluginContentTrace.registerDetailsPanel()` method.
571
572For example:
573
574```ts
575default export class implements PerfettoPlugin {
576  static readonly id = 'com.example.MyPlugin';
577  async onTraceLoad(trace: Trace) {
578    trace.registerDetailsPanel({
579      render(selection: Selection) {
580        if (canHandleSelection(selection)) {
581          return m('div', 'Details for selection');
582        } else {
583          return undefined;
584        }
585      }
586    });
587  }
588}
589```
590
591This function takes an object that implements the `DetailsPanel` interface,
592which only requires a render function to be implemented that takes the current
593selection object and returns either mithril vnodes or a falsy value.
594
595Every render cycle, render is called on all registered details panels, and the
596first registered panel to return a truthy value will be used.
597
598Currently the winning details panel takes complete control over this tab. Also,
599the order that these panels are called in is not defined, so if we have multiple
600details panels competing for the same selection, the one that actually shows up
601is undefined. This is a limitation of the current approach and will be updated
602to a more democratic contribution model in the future.
603
604### Sidebar Menu Items
605Plugins can add new entries to the sidebar menu which appears on the left hand
606side of the UI. These entries can include:
607- Commands
608- Links
609- Arbitrary Callbacks
610
611#### Commands
612If a command is referenced, the command name and hotkey are displayed on the
613sidebar item.
614```ts
615trace.commands.registerCommand({
616  id: 'sayHi',
617  name: 'Say hi',
618  callback: () => window.alert('hi'),
619  defaultHotkey: 'Shift+H',
620});
621
622trace.sidebar.addMenuItem({
623  commandId: 'sayHi',
624  section: 'support',
625  icon: 'waving_hand',
626});
627```
628
629#### Links
630If an href is present, the sidebar will be used as a link. This can be an
631internal link to a page, or an external link.
632```ts
633trace.sidebar.addMenuItem({
634  section: 'navigation',
635  text: 'Plugins',
636  href: '#!/plugins',
637});
638```
639
640#### Callbacks
641Sidebar items can be instructed to execute arbitrary callbacks when the button
642is clicked.
643```ts
644trace.sidebar.addMenuItem({
645  section: 'current_trace',
646  text: 'Copy secrets to clipboard',
647  action: () => copyToClipboard('...'),
648});
649```
650
651If the action returns a promise, the sidebar item will show a little spinner
652animation until the promise returns.
653
654```ts
655trace.sidebar.addMenuItem({
656  section: 'current_trace',
657  text: 'Prepare the data...',
658  action: () => new Promise((r) => setTimeout(r, 1000)),
659});
660```
661Optional params for all types of sidebar items:
662- `icon` - A material design icon to be displayed next to the sidebar menu item.
663  See full list [here](https://fonts.google.com/icons).
664- `tooltip` - Displayed on hover
665- `section` - Where to place the menu item.
666  - `navigation`
667  - `current_trace`
668  - `convert_trace`
669  - `example_traces`
670  - `support`
671- `sortOrder` - The higher the sortOrder the higher the bar.
672
673See the [sidebar source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/sidebar.ts)
674for more detailed usage.
675
676### Pages
677Pages are entities that can be routed via the URL args, and whose content take
678up the entire available space to the right of the sidebar and underneath the
679topbar. Examples of pages are the timeline, record page, and query page, just to
680name a few common examples.
681
682E.g.
683```
684http://ui.perfetto.dev/#!/viewer <-- 'viewer' is is the current page.
685```
686
687Pages are added from a plugin by calling the `pages.registerPage` function.
688
689Pages can be trace-less or trace-ful. Trace-less pages are pages that are to be
690displayed when no trace is loaded - i.e. the record page. Trace-ful pages are
691displayed only when a trace is loaded, as they typically require a trace to work
692with.
693
694You'll typically register trace-less pages in your plugin's `onActivate()`
695function and trace-full pages in either `onActivate()` or `onTraceLoad()`. If
696users navigate to a trace-ful page before a trace is loaded the homepage will be
697shown instead.
698
699> Note: You don't need to bind the `Trace` object for pages unlike other
700> extension points, Perfetto will inject a trace object for you.
701
702Pages should be mithril components that accept `PageWithTraceAttrs` for
703trace-ful pages or `PageAttrs` for trace-less pages.
704
705Example of a trace-less page:
706```ts
707import m from 'mithril';
708import {PageAttrs} from '../../public/page';
709
710class MyPage implements m.ClassComponent<PageAttrs> {
711  view(vnode: m.CVnode<PageAttrs>) {
712    return `The trace title is: ${vnode.attrs.trace.traceInfo.traceTitle}`;
713  }
714}
715
716// ~~~ snip ~~~
717
718app.pages.registerPage({route: '/mypage', page: MyPage, traceless: true});
719```
720
721```ts
722import m from 'mithril';
723import {PageWithTraceAttrs} from '../../public/page';
724
725class MyPage implements m.ClassComponent<PageWithTraceAttrs> {
726  view(_vnode_: m.CVnode<PageWithTraceAttrs>) {
727    return 'Hello from my page';
728  }
729}
730
731// ~~~ snip ~~~
732
733app.pages.registerPage({route: '/mypage', page: MyPage});
734```
735
736Examples:
737- [dev.perfetto.ExplorePage](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExplorePage/index.ts).
738
739
740### Metric Visualisations
741TBD
742
743Examples:
744- [dev.perfetto.AndroidBinderViz](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts).
745
746### State
747NOTE: It is important to consider version skew when using persistent state.
748
749Plugins can persist information into permalinks. This allows plugins to
750gracefully handle permalinking and is an opt-in - not automatic - mechanism.
751
752Persistent plugin state works using a `Store<T>` where `T` is some JSON
753serializable object. `Store` is implemented
754[here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/base/store.ts).
755`Store` allows for reading and writing `T`. Reading:
756```typescript
757interface Foo {
758  bar: string;
759}
760
761const store: Store<Foo> = getFooStoreSomehow();
762
763// store.state is immutable and must not be edited.
764const foo = store.state.foo;
765const bar = foo.bar;
766
767console.log(bar);
768```
769
770Writing:
771```typescript
772interface Foo {
773  bar: string;
774}
775
776const store: Store<Foo> = getFooStoreSomehow();
777
778store.edit((draft) => {
779  draft.foo.bar = 'Hello, world!';
780});
781
782console.log(store.state.foo.bar);
783// > Hello, world!
784```
785
786First define an interface for your specific plugin state.
787```typescript
788interface MyState {
789  favouriteSlices: MySliceInfo[];
790}
791```
792
793To access permalink state, call `mountStore()` on your `Trace`
794object, passing in a migration function.
795```typescript
796default export class implements PerfettoPlugin {
797  static readonly id = 'com.example.MyPlugin';
798  async onTraceLoad(trace: Trace): Promise<void> {
799    const store = trace.mountStore(migrate);
800  }
801}
802
803function migrate(initialState: unknown): MyState {
804  // ...
805}
806```
807
808When it comes to migration, there are two cases to consider:
809- Loading a new trace
810- Loading from a permalink
811
812In case of a new trace, your migration function is called with `undefined`. In
813this case you should return a default version of `MyState`:
814```typescript
815const DEFAULT = {favouriteSlices: []};
816
817function migrate(initialState: unknown): MyState {
818  if (initialState === undefined) {
819    // Return default version of MyState.
820    return DEFAULT;
821  } else {
822    // Migrate old version here.
823  }
824}
825```
826
827In the permalink case, your migration function is called with the state of the
828plugin store at the time the permalink was generated. This may be from an older
829or newer version of the plugin.
830
831**Plugins must not make assumptions about the contents of `initialState`!**
832
833In this case you need to carefully validate the state object. This could be
834achieved in several ways, none of which are particularly straight forward. State
835migration is difficult!
836
837One brute force way would be to use a version number.
838
839```typescript
840interface MyState {
841  version: number;
842  favouriteSlices: MySliceInfo[];
843}
844
845const VERSION = 3;
846const DEFAULT = {favouriteSlices: []};
847
848function migrate(initialState: unknown): MyState {
849  if (initialState && (initialState as {version: any}).version === VERSION) {
850    // Version number checks out, assume the structure is correct.
851    return initialState as State;
852  } else {
853    // Null, undefined, or bad version number - return default value.
854    return DEFAULT;
855  }
856}
857```
858
859You'll need to remember to update your version number when making changes!
860Migration should be unit-tested to ensure compatibility.
861
862Examples:
863- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
864
865## Guide to the plugin API
866The plugin interfaces are defined in
867[ui/src/public/](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
868
869## Default plugins
870Some plugins are enabled by default. These plugins are held to a higher quality
871than non-default plugins since changes to those plugins effect all users of the
872UI. The list of default plugins is specified at
873[ui/src/core/default_plugins.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core/default_plugins.ts).
874
875## Misc notes
876- Plugins must be licensed under
877  [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) the same as all other
878  code in the repository.
879- Plugins are the responsibility of the OWNERS of that plugin to maintain, not
880  the responsibility of the Perfetto team. All efforts will be made to keep the
881  plugin API stable and existing plugins working however plugins that remain
882  unmaintained for long periods of time will be disabled and ultimately deleted.
883
884