Merge "ui: Fix bug where flows were not being shown for frames tracks" into main
diff --git a/Android.bp b/Android.bp
index a7bda88..69c599d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -6776,6 +6776,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
@@ -7205,6 +7206,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
@@ -7296,6 +7298,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.gen.cc",
@@ -7387,6 +7390,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.gen.h",
@@ -7474,6 +7478,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
@@ -7564,6 +7569,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pb.cc",
@@ -7654,6 +7660,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pb.h",
@@ -7741,6 +7748,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
@@ -7832,6 +7840,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pbzero.cc",
@@ -7923,6 +7932,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/clk.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cma.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/compaction.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/cpm_trace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cpuhp.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/cros_ec.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/dcvsh.pbzero.h",
@@ -13644,6 +13654,7 @@
         "src/trace_processor/perfetto_sql/stdlib/slices/flow.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql",
         "src/trace_processor/perfetto_sql/stdlib/stack_trace/jit.sql",
         "src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql",
@@ -15127,6 +15138,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
@@ -16467,6 +16479,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
diff --git a/BUILD b/BUILD
index ca9ec70..66c9ff0 100644
--- a/BUILD
+++ b/BUILD
@@ -3119,6 +3119,7 @@
         "src/trace_processor/perfetto_sql/stdlib/slices/flow.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/hierarchy.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/slices.sql",
+        "src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql",
     ],
 )
@@ -5544,6 +5545,7 @@
         "protos/perfetto/trace/ftrace/clk.proto",
         "protos/perfetto/trace/ftrace/cma.proto",
         "protos/perfetto/trace/ftrace/compaction.proto",
+        "protos/perfetto/trace/ftrace/cpm_trace.proto",
         "protos/perfetto/trace/ftrace/cpuhp.proto",
         "protos/perfetto/trace/ftrace/cros_ec.proto",
         "protos/perfetto/trace/ftrace/dcvsh.proto",
diff --git a/docs/analysis/common-queries.md b/docs/analysis/common-queries.md
index 6675bc0..e69de29 100644
--- a/docs/analysis/common-queries.md
+++ b/docs/analysis/common-queries.md
@@ -1,102 +0,0 @@
-# PerfettoSQL Common Queries
-
-This page acts as a reference guide for queries which often appear when
-performing ad-hoc analysis.
-
-## Computing CPU time for slices
-If collecting traces which including scheduling information (i.e. from ftrace)
-as well as userspace slices (i.e. from atrace), the actual time spent running
-on a CPU for each userspace slice can be computed: this is commonly known as
-the "CPU time" for a slice.
-
-Firstly, setup the views to simplify subsequent queries:
-```
-DROP VIEW IF EXISTS slice_with_utid;
-CREATE VIEW slice_with_utid AS
-SELECT
-  ts,
-  dur,
-  slice.name as slice_name,
-  slice.id as slice_id, utid,
-  thread.name as thread_name
-FROM slice
-JOIN thread_track ON thread_track.id = slice.track_id
-JOIN thread USING (utid);
-
-DROP TABLE IF EXISTS slice_thread_state_breakdown;
-CREATE VIRTUAL TABLE slice_thread_state_breakdown
-USING SPAN_LEFT_JOIN(
-  slice_with_utid PARTITIONED utid,
-  thread_state PARTITIONED utid
-);
-```
-
-Then, to compute the CPU time for all slices in the trace:
-```
-SELECT slice_id, slice_name, SUM(dur) AS cpu_time
-FROM slice_thread_state_breakdown
-WHERE state = 'Running'
-GROUP BY slice_id;
-```
-
-You can also compute CPU time for a specific slice:
-```
-SELECT slice_name, SUM(dur) AS cpu_time
-FROM slice_thread_state_breakdown
-WHERE slice_id = <your slice id> AND state = 'Running';
-```
-
-These queries can be varied easily to compute other similar metrics.
-For example to get the time spent "runnable" and in "uninterruptible sleep":
-```
-SELECT
-  slice_id,
-  slice_name,
-  SUM(IIF(state = 'R', dur, 0)) AS runnable_time,
-  SUM(IIF(state = 'D', dur, 0)) AS uninterruptible_time
-FROM slice_thread_state_breakdown
-GROUP BY slice_id;
-```
-
-## Computing scheduling time by woken threads
-A given thread might cause other threads to wake up i.e. because work was
-scheduled on them. For a given thread, the amount of time threads it
-woke up ran for can be a good proxy to understand how much work is being
-spawned.
-
-To compute this, the following query can be used:
-```
-SELECT
-  SUM((
-    SELECT dur FROM sched
-    WHERE
-      sched.ts > wakee_runnable.ts AND
-      wakee_runnable.utid = wakee_runnable.utid
-    ORDER BY ts
-    LIMIT 1
-  )) AS scheduled_dur
-FROM thread AS waker
-JOIN thread_state AS wakee_runnable ON waker.utid = wakee_runnable.waker_utid
-WHERE waker.name = <your waker thread name here>
-```
-
-To do this for all the threads in the trace simultaenously:
-```
-SELECT
-  waker_process.name AS process_name,
-  waker.name AS thread_name,
-  SUM((
-    SELECT dur FROM sched
-    WHERE
-      sched.ts > wakee_runnable.ts AND
-      sched.utid = wakee_runnable.utid
-    ORDER BY ts
-    LIMIT 1
-  )) AS scheduled_dur
-FROM thread AS waker
-JOIN process AS waker_process USING (upid)
-JOIN thread_state AS wakee_runnable ON waker.utid = wakee_runnable.waker_utid
-WHERE waker.utid != 0
-GROUP BY 1, 2
-ORDER BY 3 desc
-```
diff --git a/docs/contributing/ui-plugins.md b/docs/contributing/ui-plugins.md
index 60a6796..cc65f43 100644
--- a/docs/contributing/ui-plugins.md
+++ b/docs/contributing/ui-plugins.md
@@ -3,7 +3,8 @@
 Perfetto.
 
 ## Create a plugin
-The guide below explains how to create a plugin for the Perfetto UI.
+The guide below explains how to create a plugin for the Perfetto UI. You can
+browse the public plugin API [here](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public).
 
 ### Prepare for UI development
 First we need to prepare the UI development environment. You will need to use a
@@ -48,6 +49,8 @@
 - Navigate to the plugins page:
   [localhost:10000/#!/plugins](http://localhost:10000/#!/plugins).
 - Ctrl-F for your plugin name and enable it.
+- Enabling/disabling plugins requires a restart of the UI, so refresh the page
+  to start your plugin.
 
 Later you can request for your plugin to be enabled by default. Follow the
 [default plugins](#default-plugins) section for this.
@@ -58,81 +61,212 @@
   upload your CL to the codereview tool.
 - Once uploaded add `stevegolton@google.com` as a reviewer for your CL.
 
-## Plugin extension points
-Plugins can extend a handful of specific places in the UI. The sections below
-show these extension points and give examples of how they can be used.
+## Plugin Lifecycle
+To demonstrate the plugin's lifecycle, this is a minimal plugin that implements
+the key lifecycle hooks:
 
-### Commands
-Commands are user issuable shortcuts for actions in the UI. They can be accessed
-via the omnibox.
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
 
-Follow the [create a plugin](#create-a-plugin) to get an initial skeleton for
-your plugin.
-
-To add your first command, add a call to `ctx.registerCommand()` in either your
-`onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
-commands in `onActivate()` by default unless they require something from
-`PluginContextTrace` which is not available on `PluginContext`.
-
-The tradeoff is that commands registered in `onTraceLoad()` are only available
-while a trace is loaded, whereas commands registered in `onActivate()` are
-available all the time the plugin is active.
-
-```typescript
-class MyPlugin implements PerfettoPlugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerCommand(
-       {
-         id: 'dev.perfetto.ExampleSimpleCommand#LogHelloPlugin',
-         name: 'Log "Hello, plugin!"',
-         callback: () => console.log('Hello, plugin!'),
-       },
-    );
+  static onActivate(app: App): void {
+    // Called once on app startup
+    console.log('MyPlugin::onActivate()', app.pluginId);
+    // Note: It's rare that plugins would need this hook as most plugins are
+    // interested in trace details. Thus, this function can usually be omitted.
   }
 
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerCommand(
-       {
-         id: 'dev.perfetto.ExampleSimpleTraceCommand#LogHelloTrace',
-         name: 'Log "Hello, trace!"',
-         callback: () => console.log('Hello, trace!'),
-       },
-    );
+  constructor(trace: Trace) {
+    // Called each time a trace is loaded
+    console.log('MyPlugin::constructor()', trace.traceInfo.traceTitle);
+  }
+
+  async onTraceLoad(trace: Trace): Promise<void> {
+    // Called each time a trace is loaded
+    console.log('MyPlugin::onTraceLoad()', trace.traceInfo.traceTitle);
+    // Note this function returns a promise, so any any async calls should be
+    // completed before this promise resolves as the app using this promise for
+    // timing and plugin synchronization.
   }
 }
 ```
 
-Here `id` is a unique string which identifies this command. The `id` should be
-prefixed with the plugin id followed by a `#`. All command `id`s must be unique
-system-wide. `name` is a human readable name for the command, which is shown in
-the command palette. Finally `callback()` is the callback which actually
-performs the action.
+You can run this plugin with devtools to see the log messages in the console,
+which should give you a feel for the plugin lifecycle. Try opening a few traces
+one after another.
 
-Commands are removed automatically when their context disappears. Commands
-registered with the `PluginContext` are removed when the plugin is deactivated,
-and commands registered with the `PluginContextTrace` are removed when the trace
-is unloaded.
+`onActivate()` runs shortly after Perfetto starts up, before a trace is loaded.
+This is where the you'll configure your plugin's capabilities that aren't trace
+dependent. At this point the plugin's class is not instantiated, so you'll
+notice `onActivate()` hook is a static class member. `onActivate()` is only ever
+called once, regardless of the number of traces loaded.
+
+`onActivate()` is passed an `App` object which the plugin can use to configure
+core capabilities such as commands, sidebar items and pages. Capabilities
+registered on the App interface are persisted throughout the lifetime of the app
+(practically forever until the tab is closed), in contrast to what happens for
+the same methods on the `Trace` object (see below).
+
+The plugin class in instantiated when a trace is loaded (a new plugin instance
+is created for each trace). `onTraceLoad()` is called immediately after the
+class is instantiated, which is where you'll configure your plugin's trace
+dependent capabilities.
+
+`onTraceLoad()` is passed a `Trace` object which the plugin can use to configure
+entities that are scoped to a specific trace, such as tracks and tabs. `Trace`
+is a superset of `App`, so anything you can do with `App` you can also do with
+`Trace`, however, capabilities registered on `Trace` will typically be discarded
+when a new trace is loaded.
+
+A plugin will typically register capabilities with the core and return quickly.
+But these capabilities usually contain objects and callbacks which are called
+into later by the core during the runtime of the app. Most capabilities require
+a `Trace` or an `App` to do anything useful so these are usually bound into the
+capabilities at registration time using JavaScript classes or closures.
+
+```ts
+// Toy example: Code will not compile.
+async onTraceLoad(trace: Trace) {
+  // `trace` is captured in the closure and used later by the app
+  trace.regsterXYZ(() => trace.xyz);
+}
+```
+
+That way, the callback is bound to a specific trace object which and the trace
+object can outlive the runtime of the `onTraceLoad()` function, which is a very
+common pattern in Perfetto plugins.
+
+> Note: Some capabilities can be registered on either the `App` or the `Trace`
+> object (i.e. in `onActivate()` or in `onTraceLoad()`), if in doubt about which
+> one to use, use `onTraceLoad()` as this is more than likely the one you want.
+> Most plugins add tracks and tabs that depend on the trace. You'd usually have
+> to be doing something out of the ordinary if you need to use `onActivate()`.
+
+### Performance
+`onActivate()` and `onTraceLoad()` should generally complete as quickly as
+possible, however sometimes `onTraceLoad()` may need to perform async operations
+on trace processor such as performing queries and/or creating views and tables.
+Thus, `onTraceLoad()` should return a promise (or you can simply make it an
+async function). When this promise resolves it tells the core that the plugin is
+fully initialized.
+
+> Note: It's important that any async operations done in onTraceLoad() are
+> awaited so that all async operations are completed by the time the promise is
+> resolved. This is so that plugins can be properly timed and synchronized.
+
+
+```ts
+// GOOD
+async onTraceLoad(trace: Trace) {
+  await trace.engine.query(...);
+}
+
+// BAD
+async onTraceLoad(trace: Trace) {
+  // Note the missing await!
+  trace.engine.query(...);
+}
+```
+
+## Extension Points
+Plugins can extend functionality of Perfetto by registering capabilities via
+extension points on the `App` or `Trace` objects.
+
+The following sections delve into more detail on each extension point and
+provide examples of how they can be used.
+
+### Commands
+Commands are user issuable shortcuts for actions in the UI. They are invoked via
+the command palette which can be opened by pressing Ctrl+Shift+P (or Cmd+Shift+P
+on Mac), or by typing a '>' into the omnibox.
+
+To add a command, add a call to `registerCommand()` on either your
+`onActivate()` or `onTraceLoad()` hooks. The recommendation is to register
+commands in `onTraceLoad()` by default unless you very specifically want the
+command to be available before a trace has loaded.
+
+Example of a command that doesn't require a trace.
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  static onActivate(app: App) {
+    app.commands.registerCommand({
+      id: `${app.pluginId}#SayHello`,
+      name: 'Say hello',
+      callback: () => console.log('Hello, world!'),
+    });
+  }
+}
+```
+
+Example of a command that requires a trace object - in this case the trace
+title.
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.commands.registerCommand({
+      id: `${trace.pluginId}#LogTraceTitle`,
+      name: 'Log trace title',
+      callback: () => console.log(trace.info.traceTitle),
+    });
+  }
+}
+```
+
+> Notice that the trace object is captured in the closure, so it can be used
+> after the onTraceLoad() function has returned. This is a very common pattern
+> in Perfetto plugins.
+
+Command arguments explained:
+- `id` is a unique string which identifies this command. The `id` should be
+prefixed with the plugin id followed by a `#`. All command `id`s must be unique
+system-wide.
+- `name` is a human readable name for the command, which is shown in the command
+palette.
+- `callback()` is the callback which actually performs the action.
+
+#### Async commands
+It's common that commands will perform async operations in their callbacks. It's
+recommended to use async/await for this rather than `.then().catch()`. The
+easiest way to do this is to make the callback an async function.
+
+```ts
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.commands.registerCommand({
+      id: `${trace.pluginId}#QueryTraceProcessor`,
+      name: 'Query trace processor',
+      callback: async () => {
+        const results = await trace.engine.query(...);
+        // use results...
+      },
+    });
+  }
+}
+```
+
+If the callback is async (i.e. it returns a promise), nothing special happens.
+The command is still fire-n-forget as far as the core is concerned.
 
 Examples:
-- [dev.perfetto.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts).
+- [com.example.ExampleSimpleCommand](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleSimpleCommand/index.ts).
 - [perfetto.CoreCommands](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/core_plugins/commands/index.ts).
-- [dev.perfetto.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExampleState/index.ts).
+- [com.example.ExampleState](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleState/index.ts).
 
-#### Hotkeys
-
-A default hotkey may be provided when registering a command.
+### Hotkeys
+A hotkey may be associated with a command at registration time.
 
 ```typescript
-ctx.registerCommand({
-  id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
-  name: 'Log "Hello, World!"',
-  callback: () => console.log('Hello, World!'),
+ctx.commands.registerCommand({
+  ...
   defaultHotkey: 'Shift+H',
 });
 ```
 
-Even though the hotkey is a string, it's format checked at compile time using
-typescript's [template literal
+Despite the fact that the hotkey is a string, its format is checked at compile
+time using typescript's [template literal
 types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html).
 
 See
@@ -140,151 +274,179 @@
 for more details on how the hotkey syntax works, and for the available keys and
 modifiers.
 
+Note this is referred to as the 'default' hotkey because we may introduce a
+feature in the future where users can modify their hotkeys, though this doesn't
+exist at the moment.
+
 ### Tracks
-#### Defining Tracks
-Tracks describe how to render a track and how to respond to mouse interaction.
-However, the interface is a WIP and should be considered unstable. This
-documentation will be added to over the next few months after the design is
-finalised.
+In order to add a new track to the timeline, you'll need to create two entities:
+- A track 'renderer' which controls what the track looks like and how it fetches
+  data from trace processor.
+- A track 'node' controls where the track appears in the workspace.
 
-#### Reusing Existing Tracks
-Creating tracks from scratch is difficult and the API is currently a WIP, so it
-is strongly recommended to use one of our existing base classes which do a lot
-of the heavy lifting for you. These base classes also provide a more stable
-layer between your track and the (currently unstable) track API.
+Track renderers are powerful but complex, so it's, so it's strongly advised not
+to create your own. Instead, by far the easiest way to get started with tracks
+is to use the `createQuerySliceTrack` and `createQueryCounterTrack` helpers.
 
-For example, if your track needs to show slices from a given a SQL expression (a
-very common pattern), extend the `NamedSliceTrack` abstract base class and
-implement `getSqlSource()`, which should return a query with the following
-columns:
-
-- `id: INTEGER`: A unique ID for the slice.
-- `ts: INTEGER`: The timestamp of the start of the slice.
-- `dur: INTEGER`: The duration of the slice.
-- `depth: INTEGER`: Integer value defining how deep the slice should be drawn in
-    the track, 0 being rendered at the top of the track, and increasing numbers
-    being drawn towards the bottom of the track.
-- `name: TEXT`: Text to be rendered on the slice and in the popup.
-
-For example, the following track describes a slice track that displays all
-slices that begin with the letter 'a'.
+Example:
 ```ts
-class MyTrack extends NamedSliceTrack {
-  getSqlSource(): string {
-    return `
-    SELECT
-      id,
-      ts,
-      dur,
-      depth,
-      name
-    from slice
-    where name like 'a%'
-    `;
-  }
-}
-```
+import {createQuerySliceTrack} from '../../public/lib/tracks/query_slice_track';
 
-#### Registering Tracks
-Plugins may register tracks with Perfetto using
-`PluginContextTrace.registerTrack()`, usually in their `onTraceLoad` function.
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    const title = 'My Track';
+    const uri = `${trace.pluginId}#MyTrack`;
+    const query = 'select * from slice where track_id = 123';
 
-```ts
-class MyPlugin implements PerfettoPlugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerTrack({
-      uri: 'dev.MyPlugin#ExampleTrack',
-      displayName: 'My Example Track',
-      trackFactory: ({trackKey}) => {
-        return new MyTrack({engine: ctx.engine, trackKey});
+    // Create a new track renderer based on a query
+    const track = await createQuerySliceTrack({
+      trace,
+      uri,
+      data: {
+        sqlSource: query,
       },
     });
+
+    // Register the track renderer with the core
+    trace.tracks.registerTrack({uri, title, track});
+
+    // Create a track node that references the track renderer using its uri
+    const track = new TrackNode({uri, title});
+
+    // Add the track node to the current workspace
+    trace.workspace.addChildInOrder(track);
   }
 }
 ```
 
-#### Default Tracks
-The "default" tracks are a list of tracks that are added to the timeline when a
-fresh trace is loaded (i.e. **not** when loading a trace from a permalink). This
-list is copied into the timeline after the trace has finished loading, at which
-point control is handed over to the user, allowing them add, remove and reorder
-tracks as they please. Thus it only makes sense to add default tracks in your
-plugin's `onTraceLoad` function, as adding a default track later will have no
-effect.
+See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/lib/tracks/query_slice_track.ts)
+for detailed usage.
+
+You can also add a counter track using `createQueryCounterTrack` which works in
+a similar way.
 
 ```ts
-class MyPlugin implements PerfettoPlugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerTrack({
-      // ... as above ...
-    });
+import {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
 
-    ctx.addDefaultTrack({
-      uri: 'dev.MyPlugin#ExampleTrack',
-      displayName: 'My Example Track',
-      sortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-  }
-}
-```
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    const title = 'My Counter Track';
+    const uri = `${trace.pluginId}#MyCounterTrack`;
+    const query = 'select * from counter where track_id = 123';
 
-Registering and adding a default track is such a common pattern that there is a
-shortcut for doing both in one go: `PluginContextTrace.registerStaticTrack()`,
-which saves having to repeat the URI and display name.
-
-```ts
-class MyPlugin implements PerfettoPlugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerStaticTrack({
-      uri: 'dev.MyPlugin#ExampleTrack',
-      displayName: 'My Example Track',
-      trackFactory: ({trackKey}) => {
-        return new MyTrack({engine: ctx.engine, trackKey});
-      },
-      sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-    });
-  }
-}
-```
-
-#### Adding Tracks Directly
-Sometimes plugins might want to add a track to the timeline immediately, usually
-as a result of a command or on some other user action such as a button click. We
-can do this using `PluginContext.timeline.addTrack()`.
-
-```ts
-class MyPlugin implements PerfettoPlugin {
-  onTraceLoad(ctx: PluginContextTrace): void {
-    ctx.registerTrack({
-      // ... as above ...
-    });
-
-    // Register a command that directly adds a new track to the timeline
-    ctx.registerCommand({
-      id: 'dev.MyPlugin#AddMyTrack',
-      name: 'Add my track',
-      callback: () => {
-        ctx.timeline.addTrack(
-          'dev.MyPlugin#ExampleTrack',
-          'My Example Track'
-        );
+    // Create a new track renderer based on a query
+    const track = await createQueryCounterTrack({
+      trace,
+      uri,
+      data: {
+        sqlSource: query,
       },
     });
+
+    // Register the track renderer with the core
+    trace.tracks.registerTrack({uri, title, track});
+
+    // Create a track node that references the track renderer using its uri
+    const track = new TrackNode({uri, title});
+
+    // Add the track node to the current workspace
+    trace.workspace.addChildInOrder(track);
   }
 }
 ```
 
+See [the source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/lib/tracks/query_counter_track.ts)
+for detailed usage.
+
+#### Grouping Tracks
+Any track can have children. Just add child nodes any `TrackNode` object using
+its `addChildXYZ()` methods. Nested tracks are rendered as a collapsible tree.
+
+```ts
+const group = new TrackNode({title: 'Group'});
+trace.workspace.addChildInOrder(group);
+group.addChildLast(new TrackNode({title: 'Child Track A'}));
+group.addChildLast(new TrackNode({title: 'Child Track B'}));
+group.addChildLast(new TrackNode({title: 'Child Track C'}));
+```
+
+Tracks nodes with children can be collapsed and expanded manually by the user at
+runtime, or programmatically using their `expand()` and `collapse()` methods. By
+default tracks are collapsed, so to have tracks automatically expanded on
+startup you'll need to call `expand()` after adding the track node.
+
+```ts
+group.expand();
+```
+
+![Nested tracks](../images/ui-plugins/nested_tracks.png)
+
+Summary tracks are behave slightly differently to ordinary tracks. Summary
+tracks:
+- Are rendered with a light blue background when collapsed, dark blue when
+  expanded.
+- Stick to the top of the viewport when scrolling.
+- Area selections made on the track apply to child tracks instead of the summary
+  track itself.
+
+To create a summary track, set the `isSummary: true` option in its initializer
+list at creation time or set its `isSummary` property to true after creation.
+
+```ts
+const group = new TrackNode({title: 'Group', isSummary: true});
+// ~~~ or ~~~
+group.isSummary = true;
+```
+
+![Summary track](../images/ui-plugins/summary_track.png)
+
+Examples
+- [com.example.ExampleNestedTracks](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/com.example.ExampleNestedTracks/index.ts).
+
+#### Track Ordering
+Tracks can be manually reordered using the `addChildXYZ()` functions available on
+the track node api, including `addChildFirst()`, `addChildLast()`,
+`addChildBefore()`, and `addChildAfter()`.
+
+See [the workspace source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/workspace.ts) for detailed usage.
+
+However, when several plugins add tracks to the same node or the workspace, no
+single plugin has complete control over the sorting of child nodes within this
+node. Thus, the sortOrder property is be used to decentralize the sorting logic
+between plugins.
+
+In order to do this we simply give the track a `sortOrder` and call
+`addChildInOrder()` on the parent node and the track will be placed before the
+first track with a higher `sortOrder` in the list. (i.e. lower `sortOrder`s appear
+higher in the stack).
+
+```ts
+// PluginA
+workspace.addChildInOrder(new TrackNode({title: 'Foo', sortOrder: 10}));
+
+// Plugin B
+workspace.addChildInOrder(new TrackNode({title: 'Bar', sortOrder: -10}));
+```
+
+Now it doesn't matter which order plugin are initialized, track `Bar` will
+appear above track `Foo` (unless reordered later).
+
+If no `sortOrder` is defined, the track assumes a `sortOrder` of 0.
+
+> It is recommended to always use `addChildInOrder()` in plugins when adding
+> tracks to the `workspace`, especially if you want your plugin to be enabled by
+> default, as this will ensure it respects the sortOrder of other plugins.
+
+
 ### Tabs
 Tabs are a useful way to display contextual information about the trace, the
 current selection, or to show the results of an operation.
 
-To register a tab from a plugin, use the `PluginContextTrace.registerTab`
-method.
+To register a tab from a plugin, use the `Trace.registerTab` method.
 
 ```ts
-import m from 'mithril';
-import {Tab, Plugin, PluginContext, PluginContextTrace} from '../../public';
-
 class MyTab implements Tab {
   render(): m.Children {
     return m('div', 'Hello from my tab');
@@ -295,11 +457,11 @@
   }
 }
 
-class MyPlugin implements PerfettoPlugin {
-  onActivate(_: PluginContext): void {}
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerTab({
-      uri: 'dev.MyPlugin#MyTab',
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.registerTab({
+      uri: `${trace.pluginId}#MyTab`,
       content: new MyTab(),
     });
   }
@@ -320,8 +482,8 @@
 Alternatively, tabs may be shown or hidden programmatically using the tabs API.
 
 ```ts
-ctx.tabs.showTab('dev.MyPlugin#MyTab');
-ctx.tabs.hideTab('dev.MyPlugin#MyTab');
+trace.tabs.showTab(`${trace.pluginId}#MyTab`);
+trace.tabs.hideTab(`${trace.pluginId}#MyTab`);
 ```
 
 Tabs have the following properties:
@@ -345,9 +507,9 @@
 registering the tab.
 
 ```ts
-ctx.registerTab({
+trace.registerTab({
   isEphemeral: true,
-  uri: 'dev.MyPlugin#MyTab',
+  uri: `${trace.pluginId}#MyTab`,
   content: new MyEphemeralTab(),
 });
 ```
@@ -360,13 +522,6 @@
 ```ts
 import m from 'mithril';
 import {uuidv4} from '../../base/uuid';
-import {
-  Plugin,
-  PluginContext,
-  PluginContextTrace,
-  PluginDescriptor,
-  Tab,
-} from '../../public';
 
 class MyNameTab implements Tab {
   constructor(private name: string) {}
@@ -378,21 +533,21 @@
   }
 }
 
-class MyPlugin implements PerfettoPlugin {
-  onActivate(_: PluginContext): void {}
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerCommand({
-      id: 'dev.MyPlugin#AddNewEphemeralTab',
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace): Promise<void> {
+    trace.registerCommand({
+      id: `${trace.pluginId}#AddNewEphemeralTab`,
       name: 'Add new ephemeral tab',
-      callback: () => handleCommand(ctx),
+      callback: () => handleCommand(trace),
     });
   }
 }
 
-function handleCommand(ctx: PluginContextTrace): void {
+function handleCommand(trace: Trace): void {
   const name = prompt('What is your name');
   if (name) {
-    const uri = 'dev.MyPlugin#MyName' + uuidv4();
+    const uri = `${trace.pluginId}#MyName${uuidv4()}`;
     // This makes the tab available to perfetto
     ctx.registerTab({
       isEphemeral: true,
@@ -404,11 +559,6 @@
     ctx.tabs.showTab(uri);
   }
 }
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'dev.MyPlugin',
-  plugin: MyPlugin,
-};
 ```
 
 ### Details Panels & The Current Selection Tab
@@ -422,10 +572,10 @@
 For example:
 
 ```ts
-class MyPlugin implements PerfettoPlugin {
-  onActivate(_: PluginContext): void {}
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    ctx.registerDetailsPanel({
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace) {
+    trace.registerDetailsPanel({
       render(selection: Selection) {
         if (canHandleSelection(selection)) {
           return m('div', 'Details for selection');
@@ -451,6 +601,142 @@
 is undefined. This is a limitation of the current approach and will be updated
 to a more democratic contribution model in the future.
 
+### Sidebar Menu Items
+Plugins can add new entries to the sidebar menu which appears on the left hand
+side of the UI. These entries can include:
+- Commands
+- Links
+- Arbitrary Callbacks
+
+#### Commands
+If a command is referenced, the command name and hotkey are displayed on the
+sidebar item.
+```ts
+trace.commands.registerCommand({
+  id: 'sayHi',
+  name: 'Say hi',
+  callback: () => window.alert('hi'),
+  defaultHotkey: 'Shift+H',
+});
+
+trace.sidebar.addMenuItem({
+  commandId: 'sayHi',
+  section: 'support',
+  icon: 'waving_hand',
+});
+```
+
+#### Links
+If an href is present, the sidebar will be used as a link. This can be an
+internal link to a page, or an external link.
+```ts
+trace.sidebar.addMenuItem({
+  section: 'navigation',
+  text: 'Plugins',
+  href: '#!/plugins',
+});
+```
+
+#### Callbacks
+Sidebar items can be instructed to execute arbitrary callbacks when the button
+is clicked.
+```ts
+trace.sidebar.addMenuItem({
+  section: 'current_trace',
+  text: 'Copy secrets to clipboard',
+  action: () => copyToClipboard('...'),
+});
+```
+
+If the action returns a promise, the sidebar item will show a little spinner
+animation until the promise returns.
+
+```ts
+trace.sidebar.addMenuItem({
+  section: 'current_trace',
+  text: 'Prepare the data...',
+  action: () => new Promise((r) => setTimeout(r, 1000)),
+});
+```
+Optional params for all types of sidebar items:
+- `icon` - A material design icon to be displayed next to the sidebar menu item.
+  See full list [here](https://fonts.google.com/icons).
+- `tooltip` - Displayed on hover
+- `section` - Where to place the menu item.
+  - `navigation`
+  - `current_trace`
+  - `convert_trace`
+  - `example_traces`
+  - `support`
+- `sortOrder` - The higher the sortOrder the higher the bar.
+
+See the [sidebar source](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/public/sidebar.ts)
+for more detailed usage.
+
+### Pages
+Pages are entities that can be routed via the URL args, and whose content take
+up the entire available space to the right of the sidebar and underneath the
+topbar. Examples of pages are the timeline, record page, and query page, just to
+name a few common examples.
+
+E.g.
+```
+http://ui.perfetto.dev/#!/viewer <-- 'viewer' is is the current page.
+```
+
+Pages are added from a plugin by calling the `pages.registerPage` function.
+
+Pages can be trace-less or trace-ful. Trace-less pages are pages that are to be
+displayed when no trace is loaded - i.e. the record page. Trace-ful pages are
+displayed only when a trace is loaded, as they typically require a trace to work
+with.
+
+You'll typically register trace-less pages in your plugin's `onActivate()`
+function and trace-full pages in either `onActivate()` or `onTraceLoad()`. If
+users navigate to a trace-ful page before a trace is loaded the homepage will be
+shown instead.
+
+> Note: You don't need to bind the `Trace` object for pages unlike other
+> extension points, Perfetto will inject a trace object for you.
+
+Pages should be mithril components that accept `PageWithTraceAttrs` for
+trace-ful pages or `PageAttrs` for trace-less pages.
+
+Example of a trace-less page:
+```ts
+import m from 'mithril';
+import {PageAttrs} from '../../public/page';
+
+class MyPage implements m.ClassComponent<PageAttrs> {
+  view(vnode: m.CVnode<PageAttrs>) {
+    return `The trace title is: ${vnode.attrs.trace.traceInfo.traceTitle}`;
+  }
+}
+
+// ~~~ snip ~~~
+
+app.pages.registerPage({route: '/mypage', page: MyPage, traceless: true});
+```
+
+```ts
+import m from 'mithril';
+import {PageWithTraceAttrs} from '../../public/page';
+
+class MyPage implements m.ClassComponent<PageWithTraceAttrs> {
+  view(_vnode_: m.CVnode<PageWithTraceAttrs>) {
+    return 'Hello from my page';
+  }
+}
+
+// ~~~ snip ~~~
+
+app.pages.registerPage({route: '/mypage', page: MyPage});
+```
+
+Examples:
+- [dev.perfetto.ExplorePage](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/ui/src/plugins/dev.perfetto.ExplorePage/index.ts).
+
+
 ### Metric Visualisations
 TBD
 
@@ -504,12 +790,13 @@
 }
 ```
 
-To access permalink state, call `mountStore()` on your `PluginContextTrace`
+To access permalink state, call `mountStore()` on your `Trace`
 object, passing in a migration function.
 ```typescript
-class MyPlugin implements PerfettoPlugin {
-  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const store = ctx.mountStore(migrate);
+default export class implements PerfettoPlugin {
+  static readonly id = 'com.example.MyPlugin';
+  async onTraceLoad(trace: Trace): Promise<void> {
+    const store = trace.mountStore(migrate);
   }
 }
 
diff --git a/docs/images/ui-plugins/nested_tracks.png b/docs/images/ui-plugins/nested_tracks.png
new file mode 100644
index 0000000..a9e87bc
--- /dev/null
+++ b/docs/images/ui-plugins/nested_tracks.png
Binary files differ
diff --git a/docs/images/ui-plugins/summary_track.png b/docs/images/ui-plugins/summary_track.png
new file mode 100644
index 0000000..96999dc
--- /dev/null
+++ b/docs/images/ui-plugins/summary_track.png
Binary files differ
diff --git a/docs/toc.md b/docs/toc.md
index 01be2af..deaeb5d 100644
--- a/docs/toc.md
+++ b/docs/toc.md
@@ -48,7 +48,6 @@
     * [Standard Library](analysis/stdlib-docs.autogen)
     * [Syntax](analysis/perfetto-sql-syntax.md)
     * [Prelude tables](analysis/sql-tables.autogen)
-    * [Common Queries](analysis/common-queries.md)
     * [Built-ins](analysis/builtin.md)
   * [Analysis at scale](#)
     * [Batch Trace Processor](analysis/batch-trace-processor.md)
diff --git a/gn/proto_library.gni b/gn/proto_library.gni
index 6bc1760..af540c2 100644
--- a/gn/proto_library.gni
+++ b/gn/proto_library.gni
@@ -107,6 +107,7 @@
                              "generator_plugin_options",
                              "include_dirs",
                              "proto_data_sources",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "sources",
@@ -153,6 +154,7 @@
                              "defines",
                              "generator_plugin_options",
                              "include_dirs",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "sources",
@@ -205,6 +207,7 @@
                              "defines",
                              "extra_configs",
                              "include_dirs",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "generator_plugin_options",
@@ -286,40 +289,6 @@
   # build generators and for generating descriptors.
   source_set_target_name =
       string_replace(target_name, expansion_token, "source_set")
-  group(source_set_target_name) {
-    public_deps_ = []
-    if (defined(invoker.public_deps)) {
-      foreach(dep, invoker.public_deps) {
-        # Get the absolute target path
-        mapped_dep = string_replace(dep, expansion_token, "source_set")
-        public_deps_ += [ mapped_dep ]
-      }
-    }
-
-    deps = []
-    if (defined(invoker.deps)) {
-      foreach(dep, invoker.deps) {
-        mapped_dep = string_replace(dep, expansion_token, "source_set")
-        deps += [ mapped_dep ]
-      }
-    }
-    deps += public_deps_
-
-    sources = []
-    foreach(source, invoker.sources) {
-      sources += [ get_path_info(source, "abspath") ]
-    }
-
-    metadata = {
-      proto_library_sources = sources
-      proto_import_dirs = import_dirs_
-      exports = []
-      foreach(dep, public_deps_) {
-        exports +=
-            [ get_label_info(dep, "dir") + ":" + get_label_info(dep, "name") ]
-      }
-    }
-  }
 
   # This config is necessary for Chrome proto_library build rule to work
   # correctly.
@@ -328,6 +297,42 @@
     inputs = invoker.sources
   }
 
+  group(source_set_target_name) {
+    # To propagate indirect inputs dependencies to descendant tareget, we use
+    # public_deps and public_configs in this target.
+    public_deps = []
+    exports_ = []
+    if (defined(invoker.public_deps)) {
+      foreach(dep, invoker.public_deps) {
+        # Get the absolute target path
+        mapped_dep = string_replace(dep, expansion_token, "source_set")
+        public_deps += [ mapped_dep ]
+        exports_ += [ get_label_info(mapped_dep, "dir") + ":" +
+                      get_label_info(mapped_dep, "name") ]
+      }
+    }
+
+    if (defined(invoker.deps)) {
+      foreach(dep, invoker.deps) {
+        mapped_dep = string_replace(dep, expansion_token, "source_set")
+        public_deps += [ mapped_dep ]
+      }
+    }
+
+    sources = []
+    foreach(source, invoker.sources) {
+      sources += [ get_path_info(source, "abspath") ]
+    }
+
+    public_configs = [ ":${source_set_input_config_name}" ]
+
+    metadata = {
+      proto_library_sources = sources
+      proto_import_dirs = import_dirs_
+      exports = exports_
+    }
+  }
+
   # Generate the descriptor if the option is set.
   if (defined(invoker.generate_descriptor)) {
     target_name_ = string_replace(target_name, expansion_token, "descriptor")
@@ -385,6 +390,7 @@
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=pbzero"
         deps = all_deps_
+        proto_deps = [ ":$source_set_target_name" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
         forward_variables_from(invoker, vars_to_forward)
@@ -395,6 +401,7 @@
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=gen"
         deps = all_deps_
+        proto_deps = [ ":$source_set_target_name" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
         forward_variables_from(invoker, vars_to_forward)
@@ -405,6 +412,7 @@
         proto_in_dir = proto_path
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=gen"
+        proto_deps = [ ":$source_set_target_name" ]
         deps = all_deps_ + [ ":$cpp_target_name_" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
@@ -419,6 +427,7 @@
         cc_generator_options = "lite=true:"
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
+        proto_deps = [ ":${source_set_target_name}" ]
         forward_variables_from(invoker, vars_to_forward)
       }
     } else {
diff --git a/gn/standalone/proto_library.gni b/gn/standalone/proto_library.gni
index 93ed9c9..ed86c02 100644
--- a/gn/standalone/proto_library.gni
+++ b/gn/standalone/proto_library.gni
@@ -22,6 +22,10 @@
 
 template("proto_library") {
   assert(defined(invoker.sources))
+
+  # This is used in chromium build.
+  not_needed(invoker, [ "proto_deps" ])
+
   proto_sources = invoker.sources
 
   # All the proto imports should be relative to the project root.
diff --git a/infra/perfetto.dev/BUILD.gn b/infra/perfetto.dev/BUILD.gn
index 25ff547..b81fa3d 100644
--- a/infra/perfetto.dev/BUILD.gn
+++ b/infra/perfetto.dev/BUILD.gn
@@ -358,6 +358,22 @@
   mdtargets += [ ":mdfile_${source}" ]
 }
 
+# Files which have been removed/renamed/moved and now have HTTP redirections in
+# src/assets/script.js
+removed_renamed_moved_files = [ "analysis/common-queries.md" ]
+
+foreach(source, removed_renamed_moved_files) {
+  filename = rebase_path(string_replace(source, ".md", ""),
+                         rebase_path("../../docs", root_build_dir))
+  md_to_html("mdfile_${source}") {
+    markdown = "src/empty.md"
+    html_template = "src/template_markdown.html"
+    out_html = "docs/${filename}"
+    deps = [ ":gen_toc" ]
+  }
+  mdtargets += [ ":mdfile_${source}" ]
+}
+
 group("all_mdfiles") {
   deps = mdtargets
 }
diff --git a/infra/perfetto.dev/src/assets/script.js b/infra/perfetto.dev/src/assets/script.js
index aa368bc..44adbf6 100644
--- a/infra/perfetto.dev/src/assets/script.js
+++ b/infra/perfetto.dev/src/assets/script.js
@@ -21,26 +21,6 @@
 let tocEventHandlersInstalled = false;
 let resizeObserver = undefined;
 
-// Handles redirects from the old docs.perfetto.dev.
-const legacyRedirectMap = {
-  '#/contributing': '/docs/contributing/getting-started#community',
-  '#/build-instructions': '/docs/contributing/build-instructions',
-  '#/testing': '/docs/contributing/testing',
-  '#/app-instrumentation': '/docs/instrumentation/tracing-sdk',
-  '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording',
-  '#/running': '/docs/quickstart/android-tracing',
-  '#/long-traces': '/docs/concepts/config#long-traces',
-  '#/detached-mode': '/docs/concepts/detached-mode',
-  '#/heapprofd': '/docs/data-sources/native-heap-profiler',
-  '#/java-hprof': '/docs/data-sources/java-heap-profiler',
-  '#/trace-processor': '/docs/analysis/trace-processor',
-  '#/analysis': '/docs/analysis/trace-processor#annotations',
-  '#/metrics': '/docs/analysis/metrics',
-  '#/traceconv': '/docs/quickstart/traceconv',
-  '#/clock-sync': '/docs/concepts/clock-sync',
-  '#/architecture': '/docs/concepts/service-model',
-};
-
 function doAfterLoadEvent(action) {
   if (onloadFired) {
     return action();
@@ -345,7 +325,40 @@
   document.documentElement.style.setProperty('--anim-enabled', '1')
 });
 
+// Handles redirects from the old docs.perfetto.dev.
+const legacyRedirectMap = {
+  '#/contributing': '/docs/contributing/getting-started#community',
+  '#/build-instructions': '/docs/contributing/build-instructions',
+  '#/testing': '/docs/contributing/testing',
+  '#/app-instrumentation': '/docs/instrumentation/tracing-sdk',
+  '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording',
+  '#/running': '/docs/quickstart/android-tracing',
+  '#/long-traces': '/docs/concepts/config#long-traces',
+  '#/detached-mode': '/docs/concepts/detached-mode',
+  '#/heapprofd': '/docs/data-sources/native-heap-profiler',
+  '#/java-hprof': '/docs/data-sources/java-heap-profiler',
+  '#/trace-processor': '/docs/analysis/trace-processor',
+  '#/analysis': '/docs/analysis/trace-processor#annotations',
+  '#/metrics': '/docs/analysis/metrics',
+  '#/traceconv': '/docs/quickstart/traceconv',
+  '#/clock-sync': '/docs/concepts/clock-sync',
+  '#/architecture': '/docs/concepts/service-model',
+};
+
 const fragment = location.hash.split('?')[0].replace('.md', '');
 if (fragment in legacyRedirectMap) {
   location.replace(legacyRedirectMap[fragment]);
-}
\ No newline at end of file
+}
+
+// Pages which have been been removed/renamed/moved and need to be redirected
+// to their new home.
+const redirectMap = {
+  // stdlib docs is not a perfect replacement but is good enough until we write
+  // a proper, Android specific query codelab page.
+  // TODO(lalitm): switch to that page when it's ready.
+  '/docs/analysis/common-queries': '/docs/analysis/stdlib-docs',
+};
+
+if (location.pathname in redirectMap) {
+  location.replace(redirectMap[location.pathname]);
+}
diff --git a/infra/perfetto.dev/src/empty.md b/infra/perfetto.dev/src/empty.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/infra/perfetto.dev/src/empty.md
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 47da583..9ab7eda 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -78,11 +78,13 @@
 import "protos/perfetto/metrics/android/wattson_tasks_attribution.proto";
 
 // Trace processor metadata
+// Next id: 17
 message TraceMetadata {
   reserved 1;
   optional int64 trace_duration_ns = 2;
   optional string trace_uuid = 3;
   optional string android_build_fingerprint = 4;
+  optional string android_device_manufacturer = 16;
   optional int64 statsd_triggering_subscription_id = 5;
   optional int64 trace_size_bytes = 6;
   repeated string trace_trigger = 7;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index a2763d9..275e373 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -3009,11 +3009,13 @@
 // Begin of protos/perfetto/metrics/metrics.proto
 
 // Trace processor metadata
+// Next id: 17
 message TraceMetadata {
   reserved 1;
   optional int64 trace_duration_ns = 2;
   optional string trace_uuid = 3;
   optional string android_build_fingerprint = 4;
+  optional string android_device_manufacturer = 16;
   optional int64 statsd_triggering_subscription_id = 5;
   optional int64 trace_size_bytes = 6;
   repeated string trace_trigger = 7;
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 59148eb..7319dd4 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -28,6 +28,7 @@
   "clk.proto",
   "cma.proto",
   "compaction.proto",
+  "cpm_trace.proto",
   "cpuhp.proto",
   "cros_ec.proto",
   "dcvsh.proto",
diff --git a/protos/perfetto/trace/ftrace/cpm_trace.proto b/protos/perfetto/trace/ftrace/cpm_trace.proto
new file mode 100644
index 0000000..f19f0f8
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/cpm_trace.proto
@@ -0,0 +1,12 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message ParamSetValueCpmFtraceEvent {
+  optional string body = 1;
+  optional uint32 value = 2;
+  optional int64 timestamp = 3;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 7858418..b363eb0 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -28,6 +28,7 @@
 import "protos/perfetto/trace/ftrace/clk.proto";
 import "protos/perfetto/trace/ftrace/cma.proto";
 import "protos/perfetto/trace/ftrace/compaction.proto";
+import "protos/perfetto/trace/ftrace/cpm_trace.proto";
 import "protos/perfetto/trace/ftrace/cpuhp.proto";
 import "protos/perfetto/trace/ftrace/cros_ec.proto";
 import "protos/perfetto/trace/ftrace/dcvsh.proto";
@@ -681,5 +682,6 @@
     SchedWakeupTaskAttrFtraceEvent sched_wakeup_task_attr = 540;
     DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
     KprobeEvent kprobe_event = 542;
+    ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
   }
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index f77915c..5e07a0c 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -7538,6 +7538,16 @@
 
 // End of protos/perfetto/trace/ftrace/compaction.proto
 
+// Begin of protos/perfetto/trace/ftrace/cpm_trace.proto
+
+message ParamSetValueCpmFtraceEvent {
+  optional string body = 1;
+  optional uint32 value = 2;
+  optional int64 timestamp = 3;
+}
+
+// End of protos/perfetto/trace/ftrace/cpm_trace.proto
+
 // Begin of protos/perfetto/trace/ftrace/cpuhp.proto
 
 message CpuhpExitFtraceEvent {
@@ -11413,6 +11423,7 @@
     SchedWakeupTaskAttrFtraceEvent sched_wakeup_task_attr = 540;
     DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
     KprobeEvent kprobe_event = 542;
+    ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
   }
 }
 
diff --git a/python/generators/sql_processing/utils.py b/python/generators/sql_processing/utils.py
index 6d58286..edb6b95 100644
--- a/python/generators/sql_processing/utils.py
+++ b/python/generators/sql_processing/utils.py
@@ -113,7 +113,7 @@
     'chrome/util': ['cr'],
     'intervals': ['interval'],
     'graphs': ['graph'],
-    'slices': ['slice'],
+    'slices': ['slice', 'thread_slice', 'process_slice'],
     'linux': ['cpu', 'memory'],
     'stacks': ['cpu_profiling'],
 }
@@ -121,8 +121,6 @@
 # Allows for nonstandard object names.
 OBJECT_NAME_ALLOWLIST = {
     'graphs/partition.sql': ['tree_structural_partition_by_group'],
-    'slices/with_context.sql': ['process_slice', 'thread_slice'],
-    'slices/cpu_time.sql': ['thread_slice_cpu_time', 'thread_slice_cpu_cycles']
 }
 
 
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index a2da25e..728882b 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index dc6d6b9..8c99f49 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -536,3 +536,4 @@
 pixel_mm/pixel_mm_kswapd_done
 sched/sched_wakeup_task_attr
 devfreq/devfreq_frequency
+cpm_trace/param_set_value_cpm
diff --git a/src/trace_processor/importers/common/tracks.h b/src/trace_processor/importers/common/tracks.h
index 3245a6b..ce4fbcd 100644
--- a/src/trace_processor/importers/common/tracks.h
+++ b/src/trace_processor/importers/common/tracks.h
@@ -56,6 +56,7 @@
   F(legacy_chrome_global_instants)               \
   F(linux_device_frequency)                      \
   F(linux_rpm)                                   \
+  F(pixel_cpm_trace)                             \
   F(pkvm_hypervisor)                             \
   F(softirq_counter)                             \
   F(thread)                                      \
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 1d31d7d..53085f9 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
 namespace trace_processor {
 namespace {
 
-std::array<FtraceMessageDescriptor, 543> descriptors{{
+std::array<FtraceMessageDescriptor, 544> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -6006,6 +6006,16 @@
             {"type", ProtoSchemaType::kInt32},
         },
     },
+    {
+        "param_set_value_cpm",
+        3,
+        {
+            {},
+            {"body", ProtoSchemaType::kString},
+            {"value", ProtoSchemaType::kUint32},
+            {"timestamp", ProtoSchemaType::kInt64},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 51ab54d..d9b130e 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -71,6 +71,7 @@
 #include "protos/perfetto/trace/ftrace/bcl_exynos.pbzero.h"
 #include "protos/perfetto/trace/ftrace/binder.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cma.pbzero.h"
+#include "protos/perfetto/trace/ftrace/cpm_trace.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cpuhp.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cros_ec.pbzero.h"
 #include "protos/perfetto/trace/ftrace/dcvsh.pbzero.h"
@@ -1372,6 +1373,10 @@
         ParseKprobe(ts, pid, fld_bytes);
         break;
       }
+      case FtraceEvent::kParamSetValueCpmFieldNumber: {
+        ParseParamSetValueCpm(fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -3836,4 +3841,16 @@
                                        track_id);
 }
 
+void FtraceParser::ParseParamSetValueCpm(protozero::ConstBytes blob) {
+  protos::pbzero::ParamSetValueCpmFtraceEvent::Decoder event(blob);
+  TrackTracker::DimensionsBuilder dims_builder =
+      context_->track_tracker->CreateDimensionsBuilder();
+  // Store event body which denotes the name of the track.
+  dims_builder.AppendName(context_->storage->InternString(event.body()));
+  TrackId track_id = context_->track_tracker->InternTrack(
+      tracks::pixel_cpm_trace, std::move(dims_builder).Build());
+  context_->event_tracker->PushCounter(static_cast<int64_t>(event.timestamp()),
+                                       event.value(), track_id);
+}
+
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index 335649d..6a08f65 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -317,6 +317,7 @@
   void ParseGoogleIccEvent(int64_t timestamp, protozero::ConstBytes);
   void ParseGoogleIrmEvent(int64_t timestamp, protozero::ConstBytes);
   void ParseDeviceFrequency(int64_t ts, protozero::ConstBytes blob);
+  void ParseParamSetValueCpm(protozero::ConstBytes blob);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
index 5c6c2e3..50a85da 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
@@ -43,6 +43,7 @@
 #include "src/trace_processor/util/status_macros.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
+#include "protos/perfetto/trace/ftrace/cpm_trace.pbzero.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
 #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
 #include "protos/perfetto/trace/ftrace/power.pbzero.h"
@@ -279,6 +280,11 @@
     TokenizeFtraceThermalExynosAcpmBulk(cpu, std::move(event),
                                         std::move(state));
     return;
+  } else if (PERFETTO_UNLIKELY(event_id ==
+                               protos::pbzero::FtraceEvent::
+                                   kParamSetValueCpmFieldNumber)) {
+    TokenizeFtraceParamSetValueCpm(cpu, std::move(event), std::move(state));
+    return;
   }
 
   auto timestamp = context_->clock_tracker->ToTraceTime(
@@ -448,19 +454,12 @@
     RefPtr<PacketSequenceStateGeneration> state) {
   // Special handling of valid gpu_work_period tracepoint events which contain
   // timestamp values for the GPU time period nested inside the event data.
-  const uint8_t* data = event.data();
-  const size_t length = event.length();
-
-  ProtoDecoder decoder(data, length);
-  auto ts_field =
-      decoder.FindField(protos::pbzero::FtraceEvent::kGpuWorkPeriodFieldNumber);
-  if (!ts_field.valid()) {
-    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
-    return;
-  }
+  auto ts_field = GetFtraceEventField(
+      protos::pbzero::FtraceEvent::kGpuWorkPeriodFieldNumber, event);
+  if (!ts_field.has_value()) return;
 
   protos::pbzero::GpuWorkPeriodFtraceEvent::Decoder gpu_work_event(
-      ts_field.data(), ts_field.size());
+      ts_field.value().data(), ts_field.value().size());
   if (!gpu_work_event.has_start_time_ns()) {
     context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
     return;
@@ -490,19 +489,13 @@
     RefPtr<PacketSequenceStateGeneration> state) {
   // Special handling of valid thermal_exynos_acpm_bulk tracepoint events which
   // contains the right timestamp value nested inside the event data.
-  const uint8_t* data = event.data();
-  const size_t length = event.length();
-
-  ProtoDecoder decoder(data, length);
-  auto ts_field = decoder.FindField(
-      protos::pbzero::FtraceEvent::kThermalExynosAcpmBulkFieldNumber);
-  if (!ts_field.valid()) {
-    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
-    return;
-  }
+  auto ts_field = GetFtraceEventField(
+      protos::pbzero::FtraceEvent::kThermalExynosAcpmBulkFieldNumber, event);
+  if (!ts_field.has_value()) return;
 
   protos::pbzero::ThermalExynosAcpmBulkFtraceEvent::Decoder
-      thermal_exynos_acpm_bulk_event(ts_field.data(), ts_field.size());
+      thermal_exynos_acpm_bulk_event(ts_field.value().data(),
+                                     ts_field.value().size());
   if (!thermal_exynos_acpm_bulk_event.has_timestamp()) {
     context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
     return;
@@ -513,5 +506,42 @@
                                     std::move(state), context_->machine_id());
 }
 
+void FtraceTokenizer::TokenizeFtraceParamSetValueCpm(
+    uint32_t cpu, TraceBlobView event,
+    RefPtr<PacketSequenceStateGeneration> state) {
+  // Special handling of valid param_set_value_cpm tracepoint events which
+  // contains the right timestamp value nested inside the event data.
+  auto ts_field = GetFtraceEventField(
+      protos::pbzero::FtraceEvent::kParamSetValueCpmFieldNumber, event);
+  if (!ts_field.has_value()) return;
+
+  protos::pbzero::ParamSetValueCpmFtraceEvent::Decoder
+      param_set_value_cpm_event(ts_field.value().data(),
+                                ts_field.value().size());
+  if (!param_set_value_cpm_event.has_timestamp()) {
+    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
+    return;
+  }
+  int64_t timestamp =
+      static_cast<int64_t>(param_set_value_cpm_event.timestamp());
+  context_->sorter->PushFtraceEvent(cpu, timestamp, std::move(event),
+                                    std::move(state), context_->machine_id());
+}
+
+std::optional<protozero::Field> FtraceTokenizer::GetFtraceEventField(
+    uint32_t event_id, const TraceBlobView& event) {
+  //  Extract ftrace event field by decoding event trace blob.
+  const uint8_t* data = event.data();
+  const size_t length = event.length();
+
+  ProtoDecoder decoder(data, length);
+  auto ts_field = decoder.FindField(event_id);
+  if (!ts_field.valid()) {
+    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
+    return std::nullopt;
+  }
+  return ts_field;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.h b/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
index 6aff47d..d8780b1 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_FTRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_FTRACE_TOKENIZER_H_
 
+#include <optional>
 #include <vector>
 
 #include "perfetto/trace_processor/trace_blob_view.h"
@@ -68,6 +69,11 @@
       uint32_t cpu,
       TraceBlobView event,
       RefPtr<PacketSequenceStateGeneration> state);
+  void TokenizeFtraceParamSetValueCpm(
+      uint32_t cpu, TraceBlobView event,
+      RefPtr<PacketSequenceStateGeneration> state);
+  std::optional<protozero::Field> GetFtraceEventField(
+      uint32_t event_id, const TraceBlobView& event);
 
   void DlogWithLimit(const base::Status& status) {
     static std::atomic<uint32_t> dlog_count(0);
diff --git a/src/trace_processor/metrics/sql/trace_metadata.sql b/src/trace_processor/metrics/sql/trace_metadata.sql
index 90c112d..1e45573 100644
--- a/src/trace_processor/metrics/sql/trace_metadata.sql
+++ b/src/trace_processor/metrics/sql/trace_metadata.sql
@@ -23,6 +23,9 @@
   'android_build_fingerprint', (
     SELECT str_value FROM metadata WHERE name = 'android_build_fingerprint'
   ),
+  'android_device_manufacturer', (
+    SELECT str_value FROM metadata WHERE name = 'android_device_manufacturer'
+  ),
   'statsd_triggering_subscription_id', (
     SELECT int_value FROM metadata
     WHERE name = 'statsd_triggering_subscription_id'
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
index b882627..ebc8f8b 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
@@ -19,7 +19,9 @@
   -- Step name (ChromeLatencyInfo.step).
   step STRING,
   -- Input type.
-  input_type STRING
+  input_type STRING,
+  -- Start time of the parent Chrome scheduler task (if any) of this step.
+  task_start_time_ts INT
 ) AS
 SELECT
   EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.trace_id') AS latency_id,
@@ -28,7 +30,8 @@
   dur,
   utid,
   EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.step') AS step,
-  EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.input_type') AS input_type
+  EXTRACT_ARG(thread_slice.arg_set_id, 'chrome_latency_info.input_type') AS input_type,
+  ts - (EXTRACT_ARG(thread_slice.arg_set_id, 'current_task.event_offset_from_task_start_time_us') * 1000) AS task_start_time_ts
 FROM
   thread_slice
 WHERE
@@ -69,7 +72,9 @@
   -- Step name (ChromeLatencyInfo.step).
   step STRING,
   -- Input type.
-  input_type STRING
+  input_type STRING,
+  -- Start time of the parent Chrome scheduler task (if any) of this step.
+  task_start_time_ts INT
 ) AS
 SELECT
   latency_id,
@@ -78,7 +83,8 @@
   dur,
   utid,
   step,
-  chrome_inputs.input_type AS input_type
+  chrome_inputs.input_type AS input_type,
+  task_start_time_ts
 FROM
   chrome_inputs
 LEFT JOIN
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql b/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql
index e627991..80696a1 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/time_in_state.sql
@@ -93,36 +93,58 @@
 GROUP BY utid;
 
 -- Time the thread spent each state in a given interval.
+--
+-- This function is only designed to run over a small number of intervals
+-- (10-100 at most). It will be *very slow* for large sets of intervals.
+--
+-- Specifically for any non-trivial subset of thread slices, prefer using
+-- `thread_slice_time_in_state` in the `slices.time_in_state` module for this
+-- purpose instead.
 CREATE PERFETTO FUNCTION sched_time_in_state_for_thread_in_interval(
   -- The start of the interval.
   ts INT,
   -- The duration of the interval.
   dur INT,
   -- The utid of the thread.
-  utid INT)
+  utid INT
+)
 RETURNS TABLE(
-  -- Thread state (from the `thread_state` table).
-  -- Use `sched_state_to_human_readable_string` function to get full name.
-  state INT,
+  -- The scheduling state (from the `thread_state` table).
+  --
+  -- Use the `sched_state_to_human_readable_string` function in the `sched`
+  -- package to get full name.
+  state STRING,
   -- A (posssibly NULL) boolean indicating, if the device was in uninterruptible
   -- sleep, if it was an IO sleep.
   io_wait BOOL,
-  -- Some states can specify the blocked function. Usually NULL.
+  -- If the `state` is uninterruptible sleep, `io_wait` indicates if it was
+  -- an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if
+  -- we cannot tell if it was an IO sleep or not.
+  --
+  -- Only available on Android when
+  -- `sched/sched_blocked_reason` ftrace tracepoint is enabled.
   blocked_function INT,
-  -- Total time spent with this state, cpu and blocked function.
-  dur INT) AS
+  -- The duration of time the threads slice spent for each
+  -- (state, io_wait, blocked_function) tuple.
+  dur INT
+) AS
 SELECT
   state,
   io_wait,
   blocked_function,
   sum(ii.dur) as dur
 FROM thread_state
-JOIN
-  (SELECT * FROM _interval_intersect_single!(
+JOIN (
+  SELECT *
+  FROM _interval_intersect_single!(
     $ts, $dur,
-    (SELECT id, ts, dur
-    FROM thread_state
-    WHERE utid = $utid AND dur > 0))) ii USING (id)
+    (
+      SELECT id, ts, dur
+      FROM thread_state
+      WHERE utid = $utid AND dur > 0
+    )
+  )
+) ii USING (id)
 GROUP BY 1, 2, 3
 ORDER BY 4 DESC;
 
@@ -137,7 +159,7 @@
 RETURNS TABLE(
   -- Thread state (from the `thread_state` table).
   -- Use `sched_state_to_human_readable_string` function to get full name.
-  state INT,
+  state STRING,
   -- A (posssibly NULL) boolean indicating, if the device was in uninterruptible
   -- sleep, if it was an IO sleep.
   io_wait BOOL,
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn
index 2e58615..4d616ea 100644
--- a/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/BUILD.gn
@@ -21,6 +21,7 @@
     "flow.sql",
     "hierarchy.sql",
     "slices.sql",
+    "time_in_state.sql",
     "with_context.sql",
   ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql b/src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql
new file mode 100644
index 0000000..142a664
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/slices/time_in_state.sql
@@ -0,0 +1,77 @@
+--
+-- Copyright 2024 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
+--
+--     https://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.
+
+INCLUDE PERFETTO MODULE intervals.intersect;
+INCLUDE PERFETTO MODULE slices.with_context;
+
+-- For each thread slice, returns the sum of the time it spent in various
+-- scheduling states.
+--
+-- Requires scheduling data to be available in the trace.
+CREATE PERFETTO TABLE thread_slice_time_in_state(
+  -- Id of a slice. Alias of `slice.id`.
+  id INT,
+  -- Name of the slice.
+  name STRING,
+  -- Id of the thread the slice is running on. Alias of `thread.id`.
+  utid INT,
+  -- Name of the thread.
+  thread_name STRING,
+  -- Id of the process the slice is running on. Alias of `process.id`.
+  upid INT,
+  -- Name of the process.
+  process_name STRING,
+  -- The scheduling state (from the `thread_state` table).
+  --
+  -- Use the `sched_state_to_human_readable_string` function in the `sched`
+  -- package to get full name.
+  state STRING,
+  -- If the `state` is uninterruptible sleep, `io_wait` indicates if it was
+  -- an IO sleep. Will be null if `state` is *not* uninterruptible sleep or if
+  -- we cannot tell if it was an IO sleep or not.
+  --
+  -- Only available on Android when
+  -- `sched/sched_blocked_reason` ftrace tracepoint is enabled.
+  io_wait BOOL,
+  -- If in uninterruptible sleep (D), the kernel function on which was blocked.
+  -- Only available on userdebug Android builds when
+  -- `sched/sched_blocked_reason` ftrace tracepoint is enabled.
+  blocked_function INT,
+  -- The duration of time the threads slice spent for each
+  -- (state, io_wait, blocked_function) tuple.
+  dur INT
+) AS
+SELECT
+  ii.id_0 AS id,
+  ts.name,
+  ts.utid,
+  ts.thread_name,
+  ts.upid,
+  ts.process_name,
+  tstate.state,
+  tstate.io_wait,
+  tstate.blocked_function,
+  SUM(ii.dur) AS dur
+FROM _interval_intersect!(
+  (
+    (SELECT * FROM thread_slice WHERE utid > 0 AND dur > 0),
+    (SELECT * FROM thread_state WHERE dur > 0)
+  ),
+  (utid)
+) ii
+JOIN thread_slice ts ON ts.id = ii.id_0
+JOIN thread_state tstate ON tstate.id = ii.id_1
+GROUP BY ii.id_0, tstate.state, tstate.io_wait, tstate.blocked_function
+ORDER BY ii.id_0;
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
index 42639ea..da2f15c 100644
--- a/src/trace_processor/util/proto_to_args_parser.cc
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -482,7 +482,11 @@
       pool_.descriptors()[*opt_enum_descriptor_idx].FindEnumString(value);
   if (!opt_enum_string) {
     // Fall back to the integer representation of the field.
-    delegate.AddInteger(key_prefix_, value);
+    // We add the string representation of the int value here in order that
+    // EXTRACT_ARG() should return consistent types under error conditions and
+    // that CREATE PERFETTO TABLE AS EXTRACT_ARG(...) should be generally safe
+    // to use.
+    delegate.AddString(key_prefix_, std::to_string(value));
     return base::OkStatus();
   }
   delegate.AddString(
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index b152d02..2cd9b20 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -1294,6 +1294,22 @@
        kUnsetFtraceId,
        112,
        kUnsetSize},
+      {"param_set_value_cpm",
+       "cpm_trace",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "body", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "value", 2, ProtoSchemaType::kUint32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "timestamp", 3, ProtoSchemaType::kInt64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       543,
+       kUnsetSize},
       {"cpuhp_exit",
        "cpuhp",
        {
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/cpm_trace/param_set_value_cpm/format b/src/traced/probes/ftrace/test/data/synthetic/events/cpm_trace/param_set_value_cpm/format
new file mode 100644
index 0000000..a7a2876
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/cpm_trace/param_set_value_cpm/format
@@ -0,0 +1,13 @@
+name: param_set_value_cpm
+ID: 1125
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:__data_loc char[] body;	offset:8;	size:4;	signed:0;
+	field:unsigned int value;	offset:12;	size:4;	signed:0;
+	field:long long timestamp;	offset:16;	size:8;	signed:1;
+
+print fmt: "%s state=%u timestamp=%lld", __get_str(body), REC->value, REC->timestamp
diff --git a/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
index bb42920..083354a 100755
--- a/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/stdlib/chrome/tests_scroll_jank.py
@@ -393,6 +393,36 @@
         -2143831735395280246,"GESTURE_SCROLL_UPDATE_EVENT","STEP_SEND_INPUT_EVENT_UI,STEP_HANDLE_INPUT_EVENT_IMPL,STEP_DID_HANDLE_INPUT_AND_OVERSCROLL,STEP_GESTURE_EVENT_HANDLED"
         """))
 
+  def test_task_start_time(self):
+    return DiffTestBlueprint(
+        trace=DataPath('scroll_m131.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.input;
+
+        SELECT
+          latency_id,
+          step,
+          task_start_time_ts
+        FROM chrome_input_pipeline_steps
+        ORDER BY latency_id
+        LIMIT 10;
+        """,
+        # STEP_SEND_INPUT_EVENT_UI does not run in a task,
+        # so its task_start_time_ts will be NULL.
+        out=Csv("""
+        "latency_id","step","task_start_time_ts"
+        -2143831735395280256,"STEP_SEND_INPUT_EVENT_UI","[NULL]"
+        -2143831735395280256,"STEP_HANDLE_INPUT_EVENT_IMPL",1292554143003210
+        -2143831735395280256,"STEP_DID_HANDLE_INPUT_AND_OVERSCROLL",1292554153539210
+        -2143831735395280256,"STEP_GESTURE_EVENT_HANDLED",1292554154651257
+        -2143831735395280254,"STEP_SEND_INPUT_EVENT_UI","[NULL]"
+        -2143831735395280254,"STEP_HANDLE_INPUT_EVENT_IMPL",1292554155188210
+        -2143831735395280254,"STEP_DID_HANDLE_INPUT_AND_OVERSCROLL",1292554164359210
+        -2143831735395280254,"STEP_GESTURE_EVENT_HANDLED",1292554165141257
+        -2143831735395280250,"STEP_SEND_INPUT_EVENT_UI","[NULL]"
+        -2143831735395280250,"STEP_HANDLE_INPUT_EVENT_IMPL",1292554131865210
+        """))
+
   def test_chrome_coalesced_inputs(self):
         return DiffTestBlueprint(
         trace=DataPath('scroll_m131.pftrace'),
diff --git a/test/trace_processor/diff_tests/stdlib/slices/tests.py b/test/trace_processor/diff_tests/stdlib/slices/tests.py
index 625659e..88b430f 100644
--- a/test/trace_processor/diff_tests/stdlib/slices/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/slices/tests.py
@@ -177,3 +177,27 @@
         8,46926
         9,17865
         """))
+
+  def test_thread_slice_time_in_state(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE slices.time_in_state;
+
+        SELECT id, name, state, io_wait, blocked_function, dur
+        FROM thread_slice_time_in_state
+        LIMIT 10;
+        """,
+        out=Csv("""
+          "id","name","state","io_wait","blocked_function","dur"
+          0,"Deoptimization JIT inline cache","Running","[NULL]","[NULL]",178646
+          1,"Deoptimization JIT inline cache","Running","[NULL]","[NULL]",119740
+          2,"Lock contention on thread list lock (owner tid: 0)","Running","[NULL]","[NULL]",58073
+          3,"Lock contention on thread list lock (owner tid: 0)","Running","[NULL]","[NULL]",98698
+          3,"Lock contention on thread list lock (owner tid: 0)","S","[NULL]","[NULL]",56302
+          4,"monitor contention with owner InputReader (1421) at void com.android.server.power.PowerManagerService.acquireWakeLockInternal(android.os.IBinder, int, java.lang.String, java.lang.String, android.os.WorkSource, java.lang.String, int, int)(PowerManagerService.java:1018) waiters=0 blocking from void com.android.server.power.PowerManagerService.handleSandman()(PowerManagerService.java:2280)","Running","[NULL]","[NULL]",121979
+          4,"monitor contention with owner InputReader (1421) at void com.android.server.power.PowerManagerService.acquireWakeLockInternal(android.os.IBinder, int, java.lang.String, java.lang.String, android.os.WorkSource, java.lang.String, int, int)(PowerManagerService.java:1018) waiters=0 blocking from void com.android.server.power.PowerManagerService.handleSandman()(PowerManagerService.java:2280)","S","[NULL]","[NULL]",51198
+          5,"monitor contention with owner main (1204) at void com.android.server.am.ActivityManagerService.onWakefulnessChanged(int)(ActivityManagerService.java:7244) waiters=0 blocking from void com.android.server.am.ActivityManagerService$3.handleMessage(android.os.Message)(ActivityManagerService.java:1704)","Running","[NULL]","[NULL]",45000
+          5,"monitor contention with owner main (1204) at void com.android.server.am.ActivityManagerService.onWakefulnessChanged(int)(ActivityManagerService.java:7244) waiters=0 blocking from void com.android.server.am.ActivityManagerService$3.handleMessage(android.os.Message)(ActivityManagerService.java:1704)","S","[NULL]","[NULL]",20164377
+          6,"monitor contention with owner main (1204) at void com.android.server.am.ActivityManagerService.onWakefulnessChanged(int)(ActivityManagerService.java:7244) waiters=1 blocking from com.android.server.wm.ActivityTaskManagerInternal$SleepToken com.android.server.am.ActivityTaskManagerService.acquireSleepToken(java.lang.String, int)(ActivityTaskManagerService.java:5048)","Running","[NULL]","[NULL]",35104
+        """))
diff --git a/ui/src/common/gcs_uploader.ts b/ui/src/base/gcs_uploader.ts
similarity index 93%
rename from ui/src/common/gcs_uploader.ts
rename to ui/src/base/gcs_uploader.ts
index d2012ee..b2f2bd5 100644
--- a/ui/src/common/gcs_uploader.ts
+++ b/ui/src/base/gcs_uploader.ts
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer} from '../base/deferred';
-import {Time} from '../base/time';
-import {TraceFileStream} from '../core/trace_stream';
+import {defer} from './deferred';
+import {Time} from './time';
 
 export const BUCKET_NAME = 'perfetto-ui-data';
 export const MIME_JSON = 'application/json; charset=utf-8';
@@ -184,13 +183,16 @@
  * @returns A hex-encoded string containing the hash of the file.
  */
 async function hashFileStreaming(file: Blob): Promise<string> {
-  const fileStream = new TraceFileStream(file);
+  const CHUNK_SIZE = 32 * 1024 * 1024; // 32MB
+  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
   let chunkDigests = '';
-  for (;;) {
-    const chunk = await fileStream.readChunk();
-    const digest = await crypto.subtle.digest('SHA-1', chunk.data);
+
+  for (let i = 0; i < totalChunks; i++) {
+    const start = i * CHUNK_SIZE;
+    const end = Math.min(start + CHUNK_SIZE, file.size);
+    const chunk = await file.slice(start, end).arrayBuffer();
+    const digest = await crypto.subtle.digest('SHA-1', chunk);
     chunkDigests += digestToHex(digest);
-    if (chunk.eof) break;
   }
   return sha1(chunkDigests);
 }
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index c30256a..d0ee370 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -22,6 +22,7 @@
 // - Be approved by one of Perfetto UI owners.
 export const defaultPlugins = [
   'com.android.GpuWorkPeriod',
+  'com.google.PixelCpmTrace',
   'com.google.PixelMemory',
   'dev.perfetto.AndroidBinderVizPlugin',
   'dev.perfetto.AndroidClientServer',
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index dbe4a02..affbe70 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 import {ErrorDetails} from '../base/logging';
 import {EXTENSION_URL} from '../common/recordingV2/recording_utils';
-import {GcsUploader} from '../common/gcs_uploader';
+import {GcsUploader} from '../base/gcs_uploader';
 import {RECORDING_V2_FLAG} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
 import {VERSION} from '../gen/perfetto_version';
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index 73c9a9d..b69916f 100644
--- a/ui/src/frontend/permalink.ts
+++ b/ui/src/frontend/permalink.ts
@@ -24,7 +24,7 @@
   MIME_BINARY,
   MIME_JSON,
   GcsUploader,
-} from '../common/gcs_uploader';
+} from '../base/gcs_uploader';
 import {
   SERIALIZED_STATE_VERSION,
   SerializedAppState,
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index aed42e3..6d7a579 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -52,7 +52,7 @@
 import {EtwSettings} from './recording/etw_settings';
 import {AppImpl} from '../core/app_impl';
 import {RecordingManager} from '../controller/recording_manager';
-import {BUCKET_NAME, GcsUploader, MIME_JSON} from '../common/gcs_uploader';
+import {BUCKET_NAME, GcsUploader, MIME_JSON} from '../base/gcs_uploader';
 import {showModal} from '../widgets/modal';
 import {CopyableLink} from '../widgets/copyable_link';
 
diff --git a/ui/src/plugins/com.example.Skeleton/index.ts b/ui/src/plugins/com.example.Skeleton/index.ts
index d737940..5746950 100644
--- a/ui/src/plugins/com.example.Skeleton/index.ts
+++ b/ui/src/plugins/com.example.Skeleton/index.ts
@@ -16,19 +16,12 @@
 import {App} from '../../public/app';
 import {MetricVisualisation} from '../../public/plugin';
 import {PerfettoPlugin} from '../../public/plugin';
-import {createStore, Store} from '../../base/store';
-
-interface State {
-  foo: string;
-}
 
 // SKELETON: Rename this class to match your plugin.
 export default class implements PerfettoPlugin {
   // SKELETON: Update pluginId to match the directory of the plugin.
   static readonly id = 'com.example.Skeleton';
 
-  private store: Store<State> = createStore({foo: 'foo'});
-
   /**
    * This hook is called when the plugin is activated manually, or when the UI
    * starts up with this plugin enabled. This is typically before a trace has
@@ -38,8 +31,8 @@
    * This hook should be used for adding commands that don't depend on the
    * trace.
    */
-  static onActivate(_: App): void {
-    //
+  static onActivate(app: App): void {
+    console.log('SkeletonPlugin::onActivate()', app.pluginId);
   }
 
   /**
@@ -50,26 +43,23 @@
    * It should not be used for finding tracks from other plugins as there is no
    * guarantee those tracks will have been added yet.
    */
-  async onTraceLoad(ctx: Trace): Promise<void> {
-    this.store = ctx.mountStore((_: unknown): State => {
-      return {foo: 'bar'};
-    });
-
-    this.store.edit((state) => {
-      state.foo = 'baz';
-    });
+  async onTraceLoad(trace: Trace): Promise<void> {
+    console.log('SkeletonPlugin::onTraceLoad()', trace.traceInfo.traceTitle);
 
     // This is an example of how to access the pluginArgs pushed by the
     // postMessage when deep-linking to the UI.
-    if (ctx.openerPluginArgs !== undefined) {
-      console.log(`Postmessage args for ${ctx.pluginId}`, ctx.openerPluginArgs);
+    if (trace.openerPluginArgs !== undefined) {
+      console.log(
+        `Postmessage args for ${trace.pluginId}`,
+        trace.openerPluginArgs,
+      );
     }
 
     /**
-     * This hook is called when the trace has finished loading, and all plugins
-     * have returned from their onTraceLoad calls. The UI can be considered
-     * 'ready' at this point. All tracks and commands should now be available,
-     * and the timeline is ready to use.
+     * The 'traceready' event is fired when the trace has finished loading, and
+     * all plugins have returned from their onTraceLoad calls. The UI can be
+     * considered 'ready' at this point. All tracks and commands should now be
+     * available, and the timeline is ready to use.
      *
      * This is where any automations should be done - things that you would
      * usually do manually after the trace has loaded but you'd like to automate
@@ -94,8 +84,8 @@
      * TODO(stevegolton): Update this comment if the semantics of track adding
      * changes.
      */
-    ctx.addEventListener('traceready', async () => {
-      console.log('onTraceReady called');
+    trace.addEventListener('traceready', async () => {
+      console.log('SkeletonPlugin::traceready');
     });
   }
 
diff --git a/ui/src/plugins/com.google.PixelCpmTrace/OWNERS b/ui/src/plugins/com.google.PixelCpmTrace/OWNERS
new file mode 100644
index 0000000..9833dcb
--- /dev/null
+++ b/ui/src/plugins/com.google.PixelCpmTrace/OWNERS
@@ -0,0 +1,2 @@
+sashwinbalaji@google.com
+spirani@google.com
diff --git a/ui/src/plugins/com.google.PixelCpmTrace/index.ts b/ui/src/plugins/com.google.PixelCpmTrace/index.ts
new file mode 100644
index 0000000..dcb3934
--- /dev/null
+++ b/ui/src/plugins/com.google.PixelCpmTrace/index.ts
@@ -0,0 +1,74 @@
+// Copyright (C) 2024 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 {createQueryCounterTrack} from '../../public/lib/tracks/query_counter_track';
+import {PerfettoPlugin} from '../../public/plugin';
+import {Trace} from '../../public/trace';
+import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
+import {TrackNode} from '../../public/workspace';
+import {NUM, STR} from '../../trace_processor/query_result';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'com.google.PixelCpmTrace';
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
+    const group = new TrackNode({
+      title: 'Central Power Manager',
+      isSummary: true,
+    });
+
+    const {engine} = ctx;
+    const result = await engine.query(`
+      select
+        id AS trackId,
+        extract_arg(dimension_arg_set_id, 'name') AS trackName
+      FROM track
+      WHERE classification = 'pixel_cpm_trace'
+      ORDER BY trackName
+    `);
+
+    const it = result.iter({trackId: NUM, trackName: STR});
+    for (let group_added = false; it.valid(); it.next()) {
+      const {trackId, trackName} = it;
+      const uri = `/cpm_trace_${trackName}`;
+      const track = await createQueryCounterTrack({
+        trace: ctx,
+        uri,
+        data: {
+          sqlSource: `
+             select ts, value
+             from counter
+             where track_id = ${trackId}
+           `,
+          columns: ['ts', 'value'],
+        },
+        columns: {ts: 'ts', value: 'value'},
+      });
+      ctx.tracks.registerTrack({
+        uri,
+        title: trackName,
+        tags: {
+          kind: COUNTER_TRACK_KIND,
+          trackIds: [trackId],
+        },
+        track,
+      });
+      group.addChildInOrder(new TrackNode({uri, title: trackName}));
+      if (!group_added) {
+        ctx.workspace.addChildInOrder(group);
+        group_added = true;
+      }
+    }
+  }
+}
diff --git a/ui/src/widgets/flamegraph.ts b/ui/src/widgets/flamegraph.ts
index a7252f5..3c831dd 100644
--- a/ui/src/widgets/flamegraph.ts
+++ b/ui/src/widgets/flamegraph.ts
@@ -966,7 +966,7 @@
   }
   return addFilter(state, {
     kind: 'SHOW_STACK',
-    filter: filter.split(': ', 2)[1],
+    filter: filter,
   });
 }