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);
+  },
+};