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 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 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