docs: Improve UI plugin documentation
Change-Id: I5ec26ee19f74d1c6af70d4cb6d35f7ea52ec397f
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 7a674e8..fbe67d3 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -33,6 +33,12 @@
plugin should be named `com.example.Foo`.
- Core plugins maintained by the Perfetto team should use
`dev.perfetto.Foo`.
+- Commands should have ids with the pattern `example.com#DoSomething`
+- Command's ids should be prefixed with the id of the plugin which
+ provides them.
+- Commands names should have the form "Verb something something".
+ Good: "Pin janky frame timeline tracks"
+ Bad: "Tracks are Displayed if Janky"
### Start the dev server
```sh
@@ -52,19 +58,156 @@
used.
### Commands
-TBD
+Commands are user issuable shortcuts for actions in the UI.
+They can be accessed via the omnibox.
+
+Follow the [create a plugin](#create-a-plugin) to get an initial
+skeleton for your plugin.
+To add your first command edit the command method.
+
+```typescript
+class MyPlugin implements TracePlugin {
+ // ...
+
+ commands(): Command[] {
+ return [
+ {
+ id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
+ name: 'Log hello world',
+ callback: () => console.log('Hello, world!'),
+ },
+ ];
+ }
+}
+```
+
+Here `id` is a unique string which identifies this command.
+The `id` should be prefixed with the plugin id followed by a `#`.
+`name` is a human readable name for the command.
+Finally `callback()` is the callback which actually performs the
+action.
+
+Examples:
+- [dev.perfetto.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts).
+- [dev.perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.CoreCommands/index.ts).
+- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
### Tracks
TBD
-### Detail tabs
+### Tabs
TBD
### Metric Visualisations
TBD
+Examples:
+- [dev.perfetto.AndroidBinderViz](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts).
+
+### State
+NOTE: It is important to consider version skew when using persistent state.
+
+Plugins can persist information into permalinks. This allows plugins
+to gracefully handle permalinking and is an opt-in - not automatic -
+mechanism.
+
+Persistent plugin state works using a `Store<T>` where `T` is some JSON
+serializable object.
+`Store` is implemented [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/frontend/store.ts).
+`Store` allows for reading and writing `T`.
+Reading:
+```typescript
+interface Foo {
+ bar: string;
+}
+
+const store: Store<Foo> = getFooStoreSomehow();
+
+// store.state is immutable and must not be edited.
+const foo = store.state.foo;
+const bar = foo.bar;
+
+console.log(bar);
+```
+
+Writing:
+```typescript
+interface Foo {
+ bar: string;
+}
+
+const store: Store<Foo> = getFooStoreSomehow();
+
+store.edit((draft) => {
+ draft.foo.bar = 'Hello, world!';
+});
+
+console.log(store.state.foo.bar);
+// > Hello, world!
+```
+
+First define an interface for your specific plugin state.
+```typescript
+interface MyState {
+ favouriteSlices: MySliceInfo[];
+}
+```
+
+This interface will be used as type parameter in two methods on your
+`TracePlugin`.
+```typescript
+class MyPlugin implements TracePlugin {
+
+ static migrate(initialState: unknown): MyState {
+ // ...
+ }
+
+ constructor(store: Store<MyState>, [...]) {
+ // ...
+ }
+
+ // ...
+}
+```
+
+`migrate()` is called after trace load but before construction of
+`TracePlugin`. There are two cases to consider:
+- Loading a new trace
+- Loading from a permalink
+
+In case of a new trace `migrate()` is called with `undefined`. In this
+case you should return a default version of `MyState`:
+```
+class MyPlugin implements TracePlugin {
+
+ static migrate(initialState: unknown): MyState {
+ if (initialState === undefined) {
+ return {
+ favouriteSlices: [];
+ };
+ }
+ // ...
+ }
+
+ // ...
+}
+```
+
+In the permalink case `migrate()` is called with the state of the plugin
+store at the time the permalink was generated. This may be from a
+older or newer version of the plugin.
+**Plugin's must not make assumptions about the contents of `initialState`**.
+
+In this case you need to carefully validate the state object.
+
+TODO: Add validation example.
+
+Examples:
+- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
+
## Guide to the plugin API
-TBD
+The plugin interfaces are defined in [ui/src/public/index.ts](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/index.ts).
+
## Default plugins
TBD
diff --git a/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts b/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts
deleted file mode 100644
index 261ad39..0000000
--- a/ui/src/plugins/dev.perfetto.ExamplePlugin/index.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {
- Command,
- EngineProxy,
- PluginContext,
- Store,
- TracePlugin,
-} from '../../public';
-
-interface ExampleState {
- counter: number;
-}
-
-// This is just an example plugin, used to prove that the plugin system works.
-class ExamplePlugin implements TracePlugin {
- static migrate(_initialState: unknown): ExampleState {
- return {counter: 0};
- }
-
- constructor(_store: Store<ExampleState>, _engine: EngineProxy) {
- // No-op
- }
-
- dispose(): void {
- // No-op
- }
-
- commands(): Command[] {
- // Example return value:
- // return [
- // {
- // id: 'dev.perfetto.ExampleCommand',
- // name: 'Example Command',
- // callback: () => console.log('Hello from example command'),
- // },
- // ];
- return [];
- }
-}
-
-function activate(ctx: PluginContext) {
- ctx.registerTracePluginFactory(ExamplePlugin);
-}
-
-export const plugin = {
- pluginId: 'dev.perfetto.ExamplePlugin',
- activate,
-};
diff --git a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
new file mode 100644
index 0000000..549f4e6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
@@ -0,0 +1,56 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+ Command,
+ EngineProxy,
+ PluginContext,
+ Store,
+ TracePlugin,
+ Viewer,
+} from '../../public';
+
+interface State {}
+
+// This is just an example plugin, used to prove that the plugin system works.
+class ExampleSimpleCommand implements TracePlugin {
+ static migrate(_initialState: unknown): State {
+ return {};
+ }
+
+ constructor(_store: Store<State>, _engine: EngineProxy, _viewer: Viewer) {
+ // No-op
+ }
+
+ dispose(): void {
+ // No-op
+ }
+
+ commands(): Command[] {
+ return [
+ {
+ id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
+ name: 'Log "Hello, world!"',
+ callback: () => console.log('Hello, world!'),
+ },
+ ];
+ }
+}
+
+export const plugin = {
+ pluginId: 'dev.perfetto.ExampleSimpleCommand',
+ activate(ctx: PluginContext) {
+ ctx.registerTracePluginFactory(ExampleSimpleCommand);
+ },
+};
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
new file mode 100644
index 0000000..04bc3f6
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
@@ -0,0 +1,71 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+ Command,
+ EngineProxy,
+ PluginContext,
+ Store,
+ TracePlugin,
+ Viewer,
+} from '../../public';
+
+interface State {
+ counter: number;
+}
+
+// This example plugin shows using state that is persisted in the
+// permalink.
+class ExampleState implements TracePlugin {
+ static migrate(_initialState: unknown): State {
+ // TODO(hjd): Show validation example.
+
+ return {
+ counter: 0,
+ };
+ }
+
+ private store: Store<State>;
+ private viewer: Viewer;
+
+ constructor(store: Store<State>, _engine: EngineProxy, viewer: Viewer) {
+ this.store = store;
+ this.viewer = viewer;
+ }
+
+ dispose(): void {
+ // No-op
+ }
+
+ commands(): Command[] {
+ return [
+ {
+ id: 'dev.perfetto.ExampleState#ShowCounter',
+ name: 'Show ExampleState counter',
+ callback: () => {
+ const counter = this.store.state.counter;
+ this.viewer.tabs.openQuery(
+ `SELECT ${counter} as counter;`, `Show counter ${counter}`);
+ },
+ },
+ ];
+ }
+}
+
+export const plugin = {
+ pluginId: 'dev.perfetto.ExampleState',
+ activate(ctx: PluginContext) {
+ ctx.registerTracePluginFactory(ExampleState);
+ },
+};