Merge "Add dim bounds proto to perfetto" into main
diff --git a/Android.bp b/Android.bp
index b820aef..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",
@@ -13599,19 +13609,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/winscope/windowmanager.sql",
         "src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/**/*.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
         "src/trace_processor/perfetto_sql/stdlib/counters/intervals.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql",
         "src/trace_processor/perfetto_sql/stdlib/export/to_firefox_profile.sql",
         "src/trace_processor/perfetto_sql/stdlib/graphs/critical_path.sql",
         "src/trace_processor/perfetto_sql/stdlib/graphs/dominator_tree.sql",
@@ -13656,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",
@@ -15139,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",
@@ -16479,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 a23b86c..d4f1f1b 100644
--- a/BUILD
+++ b/BUILD
@@ -502,6 +502,72 @@
     linkstatic = True,
 )
 
+# GN target: //src/traceconv:libpprofbuilder
+perfetto_cc_library(
+    name = "libpprofbuilder",
+    srcs = [
+        ":src_profiling_deobfuscator",
+        ":src_profiling_symbolizer_symbolize_database",
+        ":src_profiling_symbolizer_symbolizer",
+        ":src_trace_processor_util_build_id",
+        ":src_traceconv_pprofbuilder",
+        ":src_traceconv_utils",
+    ],
+    hdrs = [
+        ":include_perfetto_base_base",
+        ":include_perfetto_ext_base_base",
+        ":include_perfetto_profiling_pprof_builder",
+        ":include_perfetto_protozero_protozero",
+        ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_base",
+        ":include_perfetto_public_protozero",
+        ":include_perfetto_trace_processor_basic_types",
+        ":include_perfetto_trace_processor_storage",
+        ":include_perfetto_trace_processor_trace_processor",
+    ],
+    visibility = PERFETTO_CONFIG.public_visibility,
+    deps = [
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_config_android_zero",
+        ":protos_perfetto_config_ftrace_zero",
+        ":protos_perfetto_config_gpu_zero",
+        ":protos_perfetto_config_inode_file_zero",
+        ":protos_perfetto_config_interceptors_zero",
+        ":protos_perfetto_config_power_zero",
+        ":protos_perfetto_config_process_stats_zero",
+        ":protos_perfetto_config_profiling_zero",
+        ":protos_perfetto_config_statsd_zero",
+        ":protos_perfetto_config_sys_stats_zero",
+        ":protos_perfetto_config_system_info_zero",
+        ":protos_perfetto_config_track_event_zero",
+        ":protos_perfetto_config_zero",
+        ":protos_perfetto_trace_android_winscope_common_zero",
+        ":protos_perfetto_trace_android_winscope_regular_zero",
+        ":protos_perfetto_trace_android_zero",
+        ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
+        ":protos_perfetto_trace_filesystem_zero",
+        ":protos_perfetto_trace_ftrace_zero",
+        ":protos_perfetto_trace_gpu_zero",
+        ":protos_perfetto_trace_interned_data_zero",
+        ":protos_perfetto_trace_minimal_zero",
+        ":protos_perfetto_trace_non_minimal_zero",
+        ":protos_perfetto_trace_perfetto_zero",
+        ":protos_perfetto_trace_power_zero",
+        ":protos_perfetto_trace_profiling_zero",
+        ":protos_perfetto_trace_ps_zero",
+        ":protos_perfetto_trace_statsd_zero",
+        ":protos_perfetto_trace_sys_stats_zero",
+        ":protos_perfetto_trace_system_info_zero",
+        ":protos_perfetto_trace_track_event_zero",
+        ":protos_perfetto_trace_translation_zero",
+        ":protos_third_party_pprof_zero",
+        ":protozero",
+        ":src_trace_processor_containers_containers",
+    ] + PERFETTO_CONFIG.deps.zlib,
+    linkstatic = True,
+)
+
 # GN target: //test:client_api_example
 perfetto_cc_binary(
     name = "client_api_example",
@@ -2974,19 +3040,6 @@
     srcs = glob(["src/trace_processor/perfetto_sql/stdlib/chrome/**/*.sql"]),
 )
 
-# GN target: //src/trace_processor/perfetto_sql/stdlib/common:common
-perfetto_filegroup(
-    name = "src_trace_processor_perfetto_sql_stdlib_common_common",
-    srcs = [
-        "src/trace_processor/perfetto_sql/stdlib/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql",
-    ],
-)
-
 # GN target: //src/trace_processor/perfetto_sql/stdlib/counters:counters
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_counters_counters",
@@ -2995,19 +3048,6 @@
     ],
 )
 
-# GN target: //src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common:common
-perfetto_filegroup(
-    name = "src_trace_processor_perfetto_sql_stdlib_deprecated_v42_common_common",
-    srcs = [
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql",
-        "src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql",
-    ],
-)
-
 # GN target: //src/trace_processor/perfetto_sql/stdlib/export:export
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_export_export",
@@ -3145,6 +3185,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",
     ],
 )
@@ -3242,9 +3283,7 @@
         ":src_trace_processor_perfetto_sql_stdlib_android_winscope_winscope",
         ":src_trace_processor_perfetto_sql_stdlib_callstacks_callstacks",
         ":src_trace_processor_perfetto_sql_stdlib_chrome_chrome_sql",
-        ":src_trace_processor_perfetto_sql_stdlib_common_common",
         ":src_trace_processor_perfetto_sql_stdlib_counters_counters",
-        ":src_trace_processor_perfetto_sql_stdlib_deprecated_v42_common_common",
         ":src_trace_processor_perfetto_sql_stdlib_export_export",
         ":src_trace_processor_perfetto_sql_stdlib_graphs_graphs",
         ":src_trace_processor_perfetto_sql_stdlib_intervals_intervals",
@@ -5572,6 +5611,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",
@@ -6890,74 +6930,6 @@
            PERFETTO_CONFIG.deps.demangle_wrapper,
 )
 
-# GN target: //src/traceconv:libpprofbuilder
-perfetto_cc_library(
-    name = "libpprofbuilder",
-    srcs = [
-        ":src_profiling_deobfuscator",
-        ":src_profiling_symbolizer_symbolize_database",
-        ":src_profiling_symbolizer_symbolizer",
-        ":src_trace_processor_util_build_id",
-        ":src_traceconv_pprofbuilder",
-        ":src_traceconv_utils",
-    ],
-    hdrs = [
-        ":include_perfetto_base_base",
-        ":include_perfetto_ext_base_base",
-        ":include_perfetto_profiling_pprof_builder",
-        ":include_perfetto_protozero_protozero",
-        ":include_perfetto_public_abi_base",
-        ":include_perfetto_public_base",
-        ":include_perfetto_public_protozero",
-        ":include_perfetto_trace_processor_basic_types",
-        ":include_perfetto_trace_processor_storage",
-        ":include_perfetto_trace_processor_trace_processor",
-    ],
-    visibility = [
-        "//visibility:public",
-    ],
-    deps = [
-        ":protos_perfetto_common_zero",
-        ":protos_perfetto_config_android_zero",
-        ":protos_perfetto_config_ftrace_zero",
-        ":protos_perfetto_config_gpu_zero",
-        ":protos_perfetto_config_inode_file_zero",
-        ":protos_perfetto_config_interceptors_zero",
-        ":protos_perfetto_config_power_zero",
-        ":protos_perfetto_config_process_stats_zero",
-        ":protos_perfetto_config_profiling_zero",
-        ":protos_perfetto_config_statsd_zero",
-        ":protos_perfetto_config_sys_stats_zero",
-        ":protos_perfetto_config_system_info_zero",
-        ":protos_perfetto_config_track_event_zero",
-        ":protos_perfetto_config_zero",
-        ":protos_perfetto_trace_android_winscope_common_zero",
-        ":protos_perfetto_trace_android_winscope_regular_zero",
-        ":protos_perfetto_trace_android_zero",
-        ":protos_perfetto_trace_chrome_zero",
-        ":protos_perfetto_trace_etw_zero",
-        ":protos_perfetto_trace_filesystem_zero",
-        ":protos_perfetto_trace_ftrace_zero",
-        ":protos_perfetto_trace_gpu_zero",
-        ":protos_perfetto_trace_interned_data_zero",
-        ":protos_perfetto_trace_minimal_zero",
-        ":protos_perfetto_trace_non_minimal_zero",
-        ":protos_perfetto_trace_perfetto_zero",
-        ":protos_perfetto_trace_power_zero",
-        ":protos_perfetto_trace_profiling_zero",
-        ":protos_perfetto_trace_ps_zero",
-        ":protos_perfetto_trace_statsd_zero",
-        ":protos_perfetto_trace_sys_stats_zero",
-        ":protos_perfetto_trace_system_info_zero",
-        ":protos_perfetto_trace_track_event_zero",
-        ":protos_perfetto_trace_translation_zero",
-        ":protos_third_party_pprof_zero",
-        ":protozero",
-        ":src_trace_processor_containers_containers",
-    ] + PERFETTO_CONFIG.deps.zlib,
-    linkstatic = True,
-)
-
 # GN target: //src/traceconv:traceconv
 perfetto_cc_binary(
     name = "traceconv",
diff --git a/CHANGELOG b/CHANGELOG
index 60f31dc..2a33ef5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -24,6 +24,10 @@
       inserted directly into it.
     * Removed the `uid_track` table. It was redundant as no data was
       inserted directly into it.
+    * Removed `common` package. It has been deprecated since `v42` and most
+      functionality has been moved into other packages. Notably, the time
+      conversion functions can be found in `time.conversion` module, and
+      `thread_slice` is available with `slices.with_context`.
   Trace Processor:
     *
   UI:
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/android/startup_metric.proto b/protos/perfetto/metrics/android/startup_metric.proto
index 1de0b47..86c206c 100644
--- a/protos/perfetto/metrics/android/startup_metric.proto
+++ b/protos/perfetto/metrics/android/startup_metric.proto
@@ -303,8 +303,8 @@
     // sorted by the duration in descending order.
     // By checking out the top slices/threads, developers can identify specific
     // slices or threads for further investigation.
-    repeated TraceSliceSection trace_slice_sections = 7;
-    repeated TraceThreadSection trace_thread_sections = 8;
+    optional TraceSliceSectionInfo trace_slice_sections = 7;
+    optional TraceThreadSectionInfo trace_thread_sections = 8;
 
     // Details specific for a reason.
     optional string additional_info = 9;
@@ -355,6 +355,13 @@
     optional uint32 thread_tid = 6;
   }
 
+  // Information for the SliceSections
+  message TraceSliceSectionInfo {
+    repeated TraceSliceSection slice_section = 1;
+    optional int64 start_timestamp = 2;
+    optional int64 end_timestamp = 3;
+  }
+
   // Contains information for a section of a thread.
   message TraceThreadSection {
     optional int64 start_timestamp = 1;
@@ -371,6 +378,13 @@
     optional uint32 thread_tid = 6;
   }
 
+  // Information for the ThreadSections
+  message TraceThreadSectionInfo {
+    repeated TraceThreadSection thread_section = 1;
+    optional int64 start_timestamp  = 2;
+    optional int64 end_timestamp = 3;
+  }
+
   // Next id: 26
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
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..e2fc3a3 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -2541,8 +2541,8 @@
     // sorted by the duration in descending order.
     // By checking out the top slices/threads, developers can identify specific
     // slices or threads for further investigation.
-    repeated TraceSliceSection trace_slice_sections = 7;
-    repeated TraceThreadSection trace_thread_sections = 8;
+    optional TraceSliceSectionInfo trace_slice_sections = 7;
+    optional TraceThreadSectionInfo trace_thread_sections = 8;
 
     // Details specific for a reason.
     optional string additional_info = 9;
@@ -2593,6 +2593,13 @@
     optional uint32 thread_tid = 6;
   }
 
+  // Information for the SliceSections
+  message TraceSliceSectionInfo {
+    repeated TraceSliceSection slice_section = 1;
+    optional int64 start_timestamp = 2;
+    optional int64 end_timestamp = 3;
+  }
+
   // Contains information for a section of a thread.
   message TraceThreadSection {
     optional int64 start_timestamp = 1;
@@ -2609,6 +2616,13 @@
     optional uint32 thread_tid = 6;
   }
 
+  // Information for the ThreadSections
+  message TraceThreadSectionInfo {
+    repeated TraceThreadSection thread_section = 1;
+    optional int64 start_timestamp  = 2;
+    optional int64 end_timestamp = 3;
+  }
+
   // Next id: 26
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
@@ -3009,11 +3023,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..ed15b47 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 2695450..37c1a95 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -115,15 +115,9 @@
     (['/public/lib/colorizer'], '/core/feature_flags'),
 
     # TODO(primiano): Record page-related technical debt.
-    ('/frontend/record*', '/controller/*'),
-    ('/frontend/permalink', '/controller/*'),
-    ('/common/*', '/controller/record_config_types'),
-    ('/controller/index', '/common/recordingV2/target_factories/index'),
-    ('/common/recordingV2/*', '/controller/*'),
-    ('/controller/record_controller*', '*'),
-    ('/controller/adb_*', '*'),
-    ('/chrome_extension/chrome_tracing_controller', '/controller/*'),
-    ('/chrome_extension/chrome_tracing_controller', '/core/trace_config_utils'),
+    ('/plugins/dev.perfetto.RecordTrace/*', '/frontend/globals'),
+    ('/chrome_extension/chrome_tracing_controller',
+     '/plugins/dev.perfetto.RecordTrace/*'),
 
     # TODO(primiano): query-table tech debt.
     (
@@ -150,9 +144,6 @@
     # Bigtrace deps.
     ('/bigtrace/*', ['/base/*', '/widgets/*', '/trace_processor/*']),
 
-    # TODO(primiano): rationalize recordingv2. RecordingV2 is a mess of subdirs.
-    ('/common/recordingV2/*', '/common/recordingV2/*'),
-
     # TODO(primiano): misc tech debt.
     ('/public/lib/extensions', '/frontend/*'),
     ('/bigtrace/index', ['/core/live_reload', '/core/raf_scheduler']),
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/android/jank/frames.sql b/src/trace_processor/metrics/sql/android/jank/frames.sql
index 8f72e96..481bb4b 100644
--- a/src/trace_processor/metrics/sql/android/jank/frames.sql
+++ b/src/trace_processor/metrics/sql/android/jank/frames.sql
@@ -122,30 +122,55 @@
 -- the commit/composite slices on the main thread.
 DROP TABLE IF EXISTS android_jank_cuj_sf_frame;
 CREATE PERFETTO TABLE android_jank_cuj_sf_frame AS
+WITH android_jank_cuj_timeline_sf_frame AS (
+    SELECT DISTINCT
+      cuj_id,
+      CAST(timeline.name AS INTEGER) AS vsync,
+      timeline.display_frame_token
+    FROM android_jank_cuj_vsync_boundary boundary
+    JOIN actual_frame_timeline_slice timeline
+      ON
+        boundary.upid = timeline.upid
+        AND CAST(timeline.name AS INTEGER) >= vsync_min
+        AND CAST(timeline.name AS INTEGER) <= vsync_max
+    WHERE
+        boundary.layer_id IS NULL
+      OR (
+        timeline.layer_name GLOB '*#*'
+        AND boundary.layer_id = CAST(STR_SPLIT(timeline.layer_name, '#', 1) AS INTEGER))
+),
+android_jank_cuj_sf_frame_base AS (
+    SELECT DISTINCT
+      boundary.cuj_id,
+      boundary.vsync,
+      boundary.ts,
+      boundary.ts_main_thread_start,
+      boundary.ts_end,
+      boundary.dur,
+      actual_timeline.jank_tag = 'Self Jank' AS sf_missed,
+      NULL AS app_missed, -- for simplicity align schema with android_jank_cuj_frame
+      jank_tag,
+      jank_type,
+      prediction_type,
+      present_type,
+      gpu_composition,
+      -- In case expected timeline is missing, as a fallback we use the typical frame deadline
+      -- for 60Hz.
+      -- See similar expression in android_jank_cuj_frame_timeline.
+      COALESCE(expected_timeline.dur, 16600000) AS dur_expected
+    FROM android_jank_cuj_sf_main_thread_frame_boundary boundary
+    JOIN android_jank_cuj_sf_process sf_process
+    JOIN actual_frame_timeline_slice actual_timeline
+      ON actual_timeline.upid = sf_process.upid
+        AND boundary.vsync = CAST(actual_timeline.name AS INTEGER)
+    JOIN android_jank_cuj_timeline_sf_frame ft
+      ON CAST(actual_timeline.name AS INTEGER) = ft.display_frame_token
+        AND boundary.cuj_id = ft.cuj_id
+    LEFT JOIN expected_frame_timeline_slice expected_timeline
+      ON expected_timeline.upid = actual_timeline.upid
+        AND expected_timeline.name = actual_timeline.name
+)
 SELECT
-  cuj_id,
-  ROW_NUMBER() OVER (PARTITION BY cuj_id ORDER BY vsync ASC) AS frame_number,
-  vsync,
-  boundary.ts,
-  boundary.ts_main_thread_start,
-  boundary.ts_end,
-  boundary.dur,
-  actual_timeline.jank_tag = 'Self Jank' AS sf_missed,
-  NULL AS app_missed, -- for simplicity align schema with android_jank_cuj_frame
-  jank_tag,
-  jank_type,
-  prediction_type,
-  present_type,
-  gpu_composition,
-  -- In case expected timeline is missing, as a fallback we use the typical frame deadline
-  -- for 60Hz.
-  -- See similar expression in android_jank_cuj_frame_timeline.
-  COALESCE(expected_timeline.dur, 16600000) AS dur_expected
-FROM android_jank_cuj_sf_main_thread_frame_boundary boundary
-JOIN android_jank_cuj_sf_process sf_process
-JOIN actual_frame_timeline_slice actual_timeline
-  ON actual_timeline.upid = sf_process.upid
-    AND boundary.vsync = CAST(actual_timeline.name AS INTEGER)
-LEFT JOIN expected_frame_timeline_slice expected_timeline
-  ON expected_timeline.upid = actual_timeline.upid
-    AND expected_timeline.name = actual_timeline.name;
+ *,
+ ROW_NUMBER() OVER (PARTITION BY cuj_id ORDER BY vsync ASC) AS frame_number
+FROM android_jank_cuj_sf_frame_base;
diff --git a/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql b/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
index 9c43318..536dea7 100644
--- a/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
+++ b/src/trace_processor/metrics/sql/android/jank/relevant_slices.sql
@@ -67,6 +67,7 @@
 -- Ignore child slice e.g. "Choreographer#doFrame - resynced to 1234 in 20.0ms"
   AND slice.name not GLOB '*resynced*'
   AND slice.dur > 0
+  AND vsync > 0
   AND (vsync >= begin_vsync OR begin_vsync is NULL)
   AND (vsync <= end_vsync OR end_vsync is NULL)
   -- In some malformed traces we see nested doFrame slices.
diff --git a/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql b/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
index 2d5b646..8a4a7dd 100644
--- a/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
+++ b/src/trace_processor/metrics/sql/android/startup/slow_start_reasons.sql
@@ -46,10 +46,13 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_in_runnable_state(
   startup_id LONG, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_tid', tid, 'process_pid', pid,
-    'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
     SELECT p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -62,10 +65,13 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_and_state(
   startup_id LONG, state STRING, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_tid', tid, 'process_pid', pid,
-    'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
     SELECT p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -78,10 +84,13 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_time_for_launch_state_and_io_wait(
   startup_id INT, state STRING, io_wait BOOL, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_tid', tid, 'process_pid', pid,
-    'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
     SELECT p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -95,10 +104,13 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_thread_time_for_launch_state_and_thread(
   startup_id INT, state STRING, thread_name STRING, num_threads INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceThreadSection(
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'thread_tid', tid, 'process_pid', pid,
-    'thread_name', thread_name))
+  SELECT AndroidStartupMetric_TraceThreadSectionInfo(
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur),
+    'thread_section', RepeatedField(AndroidStartupMetric_TraceThreadSection(
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'thread_tid', tid, 'process_pid', pid,
+      'thread_name', thread_name)))
   FROM (
     SELECT p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -111,13 +123,16 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_missing_baseline_profile_for_launch(
   startup_id LONG, pkg_name STRING)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', pid,
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME($startup_id,
@@ -135,13 +150,16 @@
 
 CREATE OR REPLACE PERFETTO FUNCTION get_run_from_apk(startup_id LONG)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', pid,
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups l, android_startup_processes p
@@ -157,13 +175,16 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_unlock_running_during_launch_slice(startup_id LONG,
   pid INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', $pid,
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', $pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT tid, slice.ts as slice_ts, slice.dur as slice_dur,
       slice.id as slice_id, slice.name as slice_name
@@ -180,13 +201,16 @@
 
 CREATE OR REPLACE PERFETTO FUNCTION get_gc_activity(startup_id LONG, num_slices INT)
 RETURNS PROTO  AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', pid,
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups slice, android_startup_processes p
@@ -204,13 +228,16 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_dur_on_main_thread_for_startup_and_slice(
   startup_id LONG, slice_name STRING, num_slices INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', pid,
-    'start_timestamp', slice_ts,
-    'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id,
-    'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts,
+      'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id,
+      'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups l,
@@ -223,11 +250,14 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_main_thread_binder_transactions_blocked(
   startup_id LONG, threshold DOUBLE, num_slices INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', pid,
-    'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id, 'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', pid,
+      'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id, 'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT pid, request.tid as tid, request.slice_ts as slice_ts, request.slice_dur as slice_dur,
       request.id as slice_id, request.slice_name as slice_name
@@ -253,11 +283,14 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_slices_concurrent_to_launch(
   startup_id INT, slice_glob STRING, num_slices INT, pid INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', $pid,
-    'start_timestamp', ts, 'end_timestamp', ts + dur,
-    'slice_id', id, 'slice_name', name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', $pid,
+      'start_timestamp', ts, 'end_timestamp', ts + dur,
+      'slice_id', id, 'slice_name', name)),
+    'start_timestamp', MIN(ts),
+    'end_timestamp', MAX(ts + dur))
   FROM (
     SELECT thread.tid, s.ts as ts, dur, s.id, s.name FROM slice s
     JOIN thread_track t ON s.track_id = t.id
@@ -275,11 +308,14 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_slices_for_startup_and_slice_name(
   startup_id INT, slice_name STRING, num_slices INT, pid int)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', $pid,
-    'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
-    'slice_id', slice_id, 'slice_name', slice_name))
+  SELECT AndroidStartupMetric_TraceSliceSectionInfo(
+    'slice_section', RepeatedField(AndroidStartupMetric_TraceSliceSection(
+      'thread_tid', tid,
+      'process_pid', $pid,
+      'start_timestamp', slice_ts, 'end_timestamp', slice_ts + slice_dur,
+      'slice_id', slice_id, 'slice_name', slice_name)),
+    'start_timestamp', MIN(slice_ts),
+    'end_timestamp', MAX(slice_ts + slice_dur))
   FROM (
     SELECT tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql b/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql
index 788ff3b..3545234 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql
@@ -55,22 +55,6 @@
           ELSE "unknown" END)
   ELSE "regular" END AS delay_type;
 
--- Checks if slice has a descendant with provided name.
-CREATE OR REPLACE PERFETTO FUNCTION _has_descendant_slice_with_name(
-  -- Id of the slice to check descendants of.
-  id INT,
-  -- Name of potential descendant slice.
-  descendant_name STRING
-)
--- Whether `descendant_name` is a name of an descendant slice.
-RETURNS BOOL AS
-SELECT EXISTS(
-  SELECT 1
-  FROM descendant_slice($id)
-  WHERE name = $descendant_name
-  LIMIT 1
-);
-
 -- Get all EventLatency events for scroll updates to use their
 -- flows later on to decide how much time we waited from queueing the event
 -- until we started processing it.
@@ -87,10 +71,16 @@
   {{slice_table_name}} AS s JOIN args USING(arg_set_id)
 WHERE
   NAME = "EventLatency"
-  AND (args.string_value GLOB "*GESTURE_SCROLL_UPDATE"
-  OR args.string_value = "GESTURE_SCROLL_END")
-  AND _has_descendant_slice_with_name(
-    s.id, "SubmitCompositorFrameToPresentationCompositorFrame")
+  AND EXISTS(
+    SELECT 1
+    FROM descendant_slice(s.id)
+    WHERE name = "SubmitCompositorFrameToPresentationCompositorFrame"
+    LIMIT 1
+    )
+    AND (
+      args.string_value GLOB "*GESTURE_SCROLL_UPDATE"
+      OR args.string_value = "GESTURE_SCROLL_END"
+    )
 ORDER BY trace_id;
 
 -- Get all chrome_latency_info_for_gesture_slices where trace_ids are not -1,
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql b/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
index 94d2794..62d84af 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_scroll_inputs_per_frame.sql
@@ -21,7 +21,22 @@
 -- The numbers mentioned above are estimates in the ideal case scenario.
 
 INCLUDE PERFETTO MODULE chrome.scroll_jank.utils;
-INCLUDE PERFETTO MODULE common.slices;
+
+-- Checks if slice has a descendant with provided name.
+CREATE OR REPLACE PERFETTO FUNCTION _has_descendant_slice_with_name(
+  -- Id of the slice to check descendants of.
+  id INT,
+  -- Name of potential descendant slice.
+  descendant_name STRING
+)
+-- Whether `descendant_name` is a name of an descendant slice.
+RETURNS BOOL AS
+SELECT EXISTS(
+  SELECT 1
+  FROM descendant_slice($id)
+  WHERE name = $descendant_name
+  LIMIT 1
+);
 
 -- Grab all GestureScrollUpdate slices.
 DROP VIEW IF EXISTS chrome_all_scroll_updates;
@@ -29,7 +44,7 @@
 SELECT
   S.id,
   chrome_get_most_recent_scroll_begin_id(ts) AS scroll_id,
-  has_descendant_slice_with_name(S.id, "SubmitCompositorFrameToPresentationCompositorFrame")
+  _has_descendant_slice_with_name(S.id, "SubmitCompositorFrameToPresentationCompositorFrame")
   AS is_presented,
   ts,
   dur,
diff --git a/src/trace_processor/metrics/sql/common/parent_slice.sql b/src/trace_processor/metrics/sql/common/parent_slice.sql
index d5c6f24..5478d93 100644
--- a/src/trace_processor/metrics/sql/common/parent_slice.sql
+++ b/src/trace_processor/metrics/sql/common/parent_slice.sql
@@ -13,5 +13,3 @@
 -- See the License for the specific language governing permissions and
 -- limitations under the License.
 --
-
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
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/engine/perfetto_sql_engine.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
index 21216b6..36bd391 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -704,8 +704,18 @@
   }
 
   std::string package_name = sql_modules::GetPackageName(key);
+
   auto* package = FindPackage(package_name);
   if (!package) {
+    if (package_name == "common") {
+      return base::ErrStatus(
+          "INCLUDE: Package `common` has been removed and most of the "
+          "functionality has been moved to other packages. Check "
+          "`slices.with_context` for replacement for `common.slices` and "
+          "`time.conversion` for replacement for `common.timestamps`. The "
+          "documentation for Perfetto standard library can be found at "
+          "https://perfetto.dev/docs/analysis/stdlib-docs.");
+    }
     return base::ErrStatus("INCLUDE: Package '%s' not found", key.c_str());
   }
   return IncludePackageImpl(*package, key, parser);
diff --git a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
index bf51d9e..adaeb73 100644
--- a/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/BUILD.gn
@@ -22,9 +22,7 @@
     "android",
     "callstacks",
     "chrome:chrome_sql",
-    "common",
     "counters",
-    "deprecated/v42/common",
     "export",
     "graphs",
     "intervals",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
index 90697fa..66d2d01 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
@@ -39,6 +39,8 @@
   OR $name GLOB 'NotificationStackScrollLayout#onMeasure'
   OR $name GLOB 'ExpNotRow#*'
   OR $name GLOB 'GC: Wait For*'
+  OR $name GLOB 'Recomposer:*'
+  OR $name GLOB 'Compose:*'
   OR (
     -- Some top level handler slices
     $depth = 0
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
index 62b1d88..0d704db 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/event_latency.sql
@@ -2,8 +2,6 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
-
 -- Finds the start timestamp for a given slice's descendant with a given name.
 -- If there are multiple descendants with a given name, the function will return
 -- the first one, so it's most useful when working with a timeline broken down
@@ -42,6 +40,22 @@
 WHERE s.name GLOB $child_name
 LIMIT 1;
 
+-- Checks if slice has a descendant with provided name.
+CREATE PERFETTO FUNCTION _has_descendant_slice_with_name(
+  -- Id of the slice to check descendants of.
+  id INT,
+  -- Name of potential descendant slice.
+  descendant_name STRING
+)
+-- Whether `descendant_name` is a name of an descendant slice.
+RETURNS BOOL AS
+SELECT EXISTS(
+  SELECT 1
+  FROM descendant_slice($id)
+  WHERE name = $descendant_name
+  LIMIT 1
+);
+
 -- Returns the presentation timestamp for a given EventLatency slice.
 -- This is either the end of
 -- SwapEndToPresentationCompositorFrame (if it exists),
@@ -101,7 +115,7 @@
   slice.ts,
   slice.dur,
   EXTRACT_arg(arg_set_id, 'event_latency.event_latency_id') AS scroll_update_id,
-  has_descendant_slice_with_name(
+  _has_descendant_slice_with_name(
     slice.id,
     'SubmitCompositorFrameToPresentationCompositorFrame')
     AS is_presented,
@@ -142,7 +156,7 @@
   event_type GLOB '*GESTURE_SCROLL*'
   -- Pinches are only relevant if the frame was presented.
   OR (event_type GLOB '*GESTURE_PINCH_UPDATE'
-    AND has_descendant_slice_with_name(
+    AND _has_descendant_slice_with_name(
       id,
       'SubmitCompositorFrameToPresentationCompositorFrame')
   )
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql
index 909619c..eae5bd5 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/graphics_pipeline.sql
@@ -2,7 +2,7 @@
 -- Use of this source code is governed by a BSD-style license that can be
 -- found in the LICENSE file.
 
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
+INCLUDE PERFETTO MODULE slices.with_context;
 
 -- `Graphics.Pipeline` steps corresponding to work done by a Viz client to
 -- produce a frame (i.e. before surface aggregation). Covers steps:
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/common/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
deleted file mode 100644
index 73db0a7..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/BUILD.gn
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (C) 2022 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("../../../../../gn/perfetto_sql.gni")
-
-perfetto_sql_source_set("common") {
-  sources = [
-    "args.sql",
-    "counters.sql",
-    "metadata.sql",
-    "percentiles.sql",
-    "slices.sql",
-    "timestamps.sql",
-  ]
-}
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/OWNERS b/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
deleted file mode 100644
index 0a16b3f..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/OWNERS
+++ /dev/null
@@ -1,8 +0,0 @@
-set noparent
-
-# Please prefer sending to one of the following people
-mayzner@google.com
-lalitm@google.com
-
-# For emergency reviews
-primiano@google.com
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/args.sql b/src/trace_processor/perfetto_sql/stdlib/common/args.sql
deleted file mode 100644
index 3d1e793..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/args.sql
+++ /dev/null
@@ -1,20 +0,0 @@
---
--- Copyright 2023 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/counters.sql b/src/trace_processor/perfetto_sql/stdlib/common/counters.sql
deleted file mode 100644
index f0c1ce6..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/counters.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 2023 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.counters;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/metadata.sql b/src/trace_processor/perfetto_sql/stdlib/common/metadata.sql
deleted file mode 100644
index bd1a0fd..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/metadata.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 2022 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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.metadata;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql b/src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql
deleted file mode 100644
index 525c95c..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/percentiles.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 2023 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.percentiles;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/slices.sql b/src/trace_processor/perfetto_sql/stdlib/common/slices.sql
deleted file mode 100644
index d5d70c9..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/slices.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 2022 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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql b/src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql
deleted file mode 100644
index 8f91d3b..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/common/timestamps.sql
+++ /dev/null
@@ -1,21 +0,0 @@
---
--- Copyright 2022 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.
-
--- No new changes allowed. Will be removed after v45 of Perfetto.
---
--- We decided to move away from the generalised `common` module and migrate the
--- most useful functionality into specialised modules.
-INCLUDE PERFETTO MODULE deprecated.v42.common.args;
-INCLUDE PERFETTO MODULE deprecated.v42.common.timestamps;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/BUILD.gn
deleted file mode 100644
index a99b51b..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/BUILD.gn
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (C) 2022 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("../../../../../../../gn/perfetto_sql.gni")
-
-perfetto_sql_source_set("common") {
-  sources = [
-    "args.sql",
-    "counters.sql",
-    "metadata.sql",
-    "percentiles.sql",
-    "slices.sql",
-    "timestamps.sql",
-  ]
-}
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql
deleted file mode 100644
index df0615a..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/args.sql
+++ /dev/null
@@ -1,31 +0,0 @@
---
--- Copyright 2023 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     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.
-
--- Returns the formatted value of a given argument.
--- Similar to EXTRACT_ARG, but instead of returning the raw value, it returns
--- the value formatted according to the 'value_type' column (e.g. for booleans,
--- EXTRACT_ARG will return 0 or 1, while FORMATTED_ARG will return 'true' or
--- 'false').
-CREATE PERFETTO FUNCTION formatted_arg(
-  -- Id of the arg set.
-  arg_set_id INT,
-  -- Key of the argument.
-  arg_key STRING
-)
--- Formatted value of the argument.
-RETURNS STRING AS
-SELECT display_value
-FROM args
-WHERE arg_set_id = $arg_set_id AND key = $arg_key;
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql
deleted file mode 100644
index 7923c52..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/counters.sql
+++ /dev/null
@@ -1,101 +0,0 @@
---
--- Copyright 2023 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     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 deprecated.v42.common.timestamps;
-
--- Timestamp of first counter value in a counter.
-CREATE PERFETTO FUNCTION earliest_timestamp_for_counter_track(
-  -- Id of a counter track with a counter.
-  counter_track_id INT)
--- Timestamp of first counter value. Null if doesn't exist.
-RETURNS LONG AS
-SELECT MIN(ts) FROM counter WHERE counter.track_id = $counter_track_id;
-
--- Counter values with details of counter track with calculated duration of each counter value.
--- Duration is calculated as time from counter to the next counter.
-CREATE PERFETTO FUNCTION counter_with_dur_for_track(
-  -- Id of track counter track.
-  counter_track_id INT)
-RETURNS TABLE(
-    -- Timestamp of the counter value.
-    ts LONG,
-    -- Duration of the counter value.
-    dur LONG,
-    -- Counter value.
-    value DOUBLE,
-    -- Id of the counter track.
-    track_id INT,
-    -- Name of the counter track.
-    track_name STRING,
-    -- Counter track set id.
-    track_arg_set_id INT,
-    -- Counter arg set id.
-    arg_set_id INT
-) AS
-SELECT
-  ts,
-  LEAD(ts, 1, trace_end()) OVER(ORDER BY ts) - ts AS dur,
-  value,
-  track.id AS track_id,
-  track.name AS track_name,
-  track.source_arg_set_id AS track_arg_set_id,
-  counter.arg_set_id AS arg_set_id
-FROM counter
-JOIN counter_track track ON track.id = counter.track_id
-WHERE track.id = $counter_track_id;
-
--- COUNTER_WITH_DUR_FOR_TRACK but in a specified time.
--- Does calculation over the table ends - creates an artificial counter value at
--- the start if needed and chops the duration of the last timestamps in range.
-CREATE PERFETTO FUNCTION counter_for_time_range(
-  -- Id of track counter track.
-  counter_track_id INT,
-  -- Timestamp of the timerange start.
-  -- Can be earlier than the first counter value.
-  start_ts LONG,
-  -- Timestamp of the timerange end.
-  end_ts LONG)
-RETURNS TABLE(
-  -- Timestamp of the counter value.
-  ts LONG,
-  -- Duration of the counter value.
-  dur LONG,
-  -- Counter value.
-  value DOUBLE,
-  -- If of the counter track.
-  track_id INT,
-  -- Name of the counter track.
-  track_name STRING,
-  -- Counter track set id.
-  track_arg_set_id INT,
-  -- Counter arg set id.
-  arg_set_id INT
-) AS
-SELECT
-  IIF(ts < $start_ts, $start_ts, ts) AS ts,
-  IIF(
-    ts < $start_ts,
-    dur - ($start_ts - ts),
-    IIF(ts + dur > $end_ts, $end_ts - ts, dur)) AS dur,
-  value,
-  track_id,
-  track_name,
-  track_arg_set_id,
-  arg_set_id
-FROM counter_with_dur_for_track($counter_track_id)
-WHERE TRUE
-  AND ts + dur >= $start_ts
-  AND ts < $end_ts
-ORDER BY ts ASC;
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql
deleted file mode 100644
index e667477..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/metadata.sql
+++ /dev/null
@@ -1,22 +0,0 @@
---
--- Copyright 2022 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.
-
--- Extracts an int value with the given name from the metadata table.
-CREATE PERFETTO FUNCTION extract_int_metadata(
-  -- The name of the metadata entry.
-  name STRING)
--- int_value for the given name. NULL if there's no such entry.
-RETURNS LONG AS
-SELECT int_value FROM metadata WHERE name = ($name);
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql
deleted file mode 100644
index e807a78..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/percentiles.sql
+++ /dev/null
@@ -1,169 +0,0 @@
---
--- Copyright 2023 The Android Open Source Project
---
--- Licensed under the Apache License, Version 2.0 (the "License");
--- you may not use this file except in compliance with the License.
--- You may obtain a copy of the License at
---
---     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 deprecated.v42.common.counters;
-INCLUDE PERFETTO MODULE deprecated.v42.common.timestamps;
-
-CREATE PERFETTO FUNCTION _number_generator(upper_limit INT)
-RETURNS TABLE(num INT) AS
-WITH nums AS
-    (SELECT 1 num UNION SELECT num + 1
-    from NUMS
-    WHERE num < $upper_limit)
-SELECT num FROM nums;
-
-CREATE PERFETTO FUNCTION _earliest_timestamp_for_counter_track(
-  -- Id of a counter track with a counter.
-  counter_track_id INT)
--- Timestamp of first counter value. Null if doesn't exist.
-RETURNS LONG AS
-SELECT MIN(ts) FROM counter WHERE counter.track_id = $counter_track_id;
-
--- COUNTER_WITH_DUR_FOR_TRACK but in a specified time.
--- Does calculation over the table ends - creates an artificial counter value at
--- the start if needed and chops the duration of the last timestamps in range.
-CREATE PERFETTO FUNCTION _counter_for_time_range(
-  -- Id of track counter track.
-  counter_track_id INT,
-  -- Timestamp of the timerange start.
-  -- Can be earlier than the first counter value.
-  start_ts LONG,
-  -- Timestamp of the timerange end.
-  end_ts LONG)
-RETURNS TABLE(
-  -- Timestamp of the counter value.
-  ts LONG,
-  -- Duration of the counter value.
-  dur LONG,
-  -- Counter value.
-  value DOUBLE,
-  -- If of the counter track.
-  track_id INT,
-  -- Name of the counter track.
-  track_name STRING,
-  -- Counter track set id.
-  track_arg_set_id INT,
-  -- Counter arg set id.
-  arg_set_id INT
-) AS
-SELECT
-  IIF(ts < $start_ts, $start_ts, ts) AS ts,
-  IIF(
-    ts < $start_ts,
-    dur - ($start_ts - ts),
-    IIF(ts + dur > $end_ts, $end_ts - ts, dur)) AS dur,
-  value,
-  track_id,
-  track_name,
-  track_arg_set_id,
-  arg_set_id
-FROM counter_with_dur_for_track($counter_track_id)
-WHERE TRUE
-  AND ts + dur >= $start_ts
-  AND ts < $end_ts
-ORDER BY ts ASC;
-
---
--- Get durations for percentile
---
-
--- All percentiles (range 1-100) for counter track ID in a given time range.
---
--- Percentiles are calculated by:
--- 1. Dividing the sum of duration in time range for each value in the counter
--- by duration of the counter in range. This gives us `percentile_for)value` (DOUBLE).
--- 2. Fetching each percentile by taking floor of each `percentile_for_value`, grouping by
--- resulting `percentile` and MIN from value for each grouping. As we are rounding down,
--- taking MIN assures most reliable data.
--- 3. Filling the possible gaps in percentiles by getting the minimal value from higher
--- percentiles for each gap.
-CREATE PERFETTO FUNCTION counter_percentiles_for_time_range(
-  -- Id of the counter track.
-  counter_track_id INT,
-  -- Timestamp of start of time range.
-  start_ts LONG,
-  -- Timestamp of end of time range.
-  end_ts LONG)
-RETURNS TABLE(
-  -- All of the numbers from 1 to 100.
-  percentile INT,
-  -- Value for the percentile.
-  value DOUBLE
-) AS
-WITH percentiles_for_value AS (
-    SELECT
-        value,
-        (CAST(SUM(dur) OVER(ORDER BY value ASC) AS DOUBLE) /
-            ($end_ts - MAX($start_ts, _earliest_timestamp_for_counter_track($counter_track_id)))) * 100
-        AS percentile_for_value
-    FROM _COUNTER_FOR_TIME_RANGE($counter_track_id, $start_ts, $end_ts)
-    ORDER BY value ASC
-),
-with_gaps AS (
-    SELECT
-        CAST(percentile_for_value AS INT) AS percentile,
-        MIN(value) AS value
-    FROM percentiles_for_value
-    GROUP BY percentile
-    ORDER BY percentile ASC)
-SELECT
-    num AS percentile,
-    IFNULL(value, MIN(value) OVER (ORDER BY percentile DESC)) AS value
-FROM _NUMBER_GENERATOR(100) AS nums
-LEFT JOIN with_gaps ON with_gaps.percentile = nums.num
-ORDER BY percentile DESC;
-
--- All percentiles (range 1-100) for counter track ID.
-CREATE PERFETTO FUNCTION counter_percentiles_for_track(
-  -- Id of the counter track.
-  counter_track_id INT)
-RETURNS TABLE(
-  -- All of the numbers from 1 to 100.
-  percentile INT,
-  -- Value for the percentile.
-  value DOUBLE
-) AS
-SELECT *
-FROM counter_percentiles_for_time_range(
-  $counter_track_id, trace_start(), trace_end());
-
--- Value for specific percentile (range 1-100) for counter track ID in time range.
-CREATE PERFETTO FUNCTION counter_track_percentile_for_time(
-  -- Id of the counter track.
-  counter_track_id INT,
-  -- Any of the numbers from 1 to 100.
-  percentile INT,
-  -- Timestamp of start of time range.
-  start_ts LONG,
-  -- Timestamp of end of time range.
-  end_ts LONG)
--- Value for the percentile.
-RETURNS DOUBLE AS
-SELECT value
-FROM counter_percentiles_for_time_range($counter_track_id, $start_ts, $end_ts)
-WHERE percentile = $percentile;
-
--- Value for specific percentile (range 1-100) for counter track ID.
-CREATE PERFETTO FUNCTION counter_track_percentile(
-  -- Id of the counter track.
-  counter_track_id INT,
-  -- Any of the numbers from 1 to 100.
-  percentile INT)
--- Value for the percentile.
-RETURNS DOUBLE AS
-SELECT counter_track_percentile_for_time($counter_track_id,
-                                         $percentile,
-                                         trace_start(),
-                                         trace_end());
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql
deleted file mode 100644
index 05b6b21..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/slices.sql
+++ /dev/null
@@ -1,133 +0,0 @@
---
--- Copyright 2022 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 slices.with_context;
-
--- Checks if slice has an ancestor with provided name.
-CREATE PERFETTO FUNCTION has_parent_slice_with_name(
-  -- Id of the slice to check parents of.
-  id INT,
-  -- Name of potential ancestor slice.
-  parent_name STRING)
--- Whether `parent_name` is a name of an ancestor slice.
-RETURNS BOOL AS
-SELECT EXISTS(
-  SELECT 1
-  FROM ancestor_slice($id)
-  WHERE name = $parent_name
-  LIMIT 1
-);
-
--- Checks if slice has a descendant with provided name.
-CREATE PERFETTO FUNCTION has_descendant_slice_with_name(
-  -- Id of the slice to check descendants of.
-  id INT,
-  -- Name of potential descendant slice.
-  descendant_name STRING
-)
--- Whether `descendant_name` is a name of an descendant slice.
-RETURNS BOOL AS
-SELECT EXISTS(
-  SELECT 1
-  FROM descendant_slice($id)
-  WHERE name = $descendant_name
-  LIMIT 1
-);
-
--- Finds the end timestamp for a given slice's descendant with a given name.
--- If there are multiple descendants with a given name, the function will return the
--- first one, so it's most useful when working with a timeline broken down into phases,
--- where each subphase can happen only once.
-CREATE PERFETTO FUNCTION descendant_slice_end(
-  -- Id of the parent slice.
-  parent_id INT,
-  -- Name of the child with the desired end TS.
-  child_name STRING
-)
--- End timestamp of the child or NULL if it doesn't exist.
-RETURNS INT AS
-SELECT
-  CASE WHEN s.dur
-    IS NOT -1 THEN s.ts + s.dur
-    ELSE NULL
-  END
-FROM descendant_slice($parent_id) s
-WHERE s.name = $child_name
-LIMIT 1;
-
--- Finds all slices with a direct parent with the given parent_id.
-CREATE PERFETTO FUNCTION direct_children_slice(
-  -- Id of the parent slice.
-  parent_id LONG)
-RETURNS TABLE(
-  -- Alias for `slice.id`.
-  id LONG,
-  -- Alias for `slice.type`.
-  type STRING,
-  -- Alias for `slice.ts`.
-  ts LONG,
-  -- Alias for `slice.dur`.
-  dur LONG,
-  -- Alias for `slice.category`.
-  category LONG,
-  -- Alias for `slice.name`.
-  name STRING,
-  -- Alias for `slice.track_id`.
-  track_id LONG,
-  -- Alias for `slice.depth`.
-  depth LONG,
-  -- Alias for `slice.parent_id`.
-  parent_id LONG,
-  -- Alias for `slice.arg_set_id`.
-  arg_set_id LONG,
-  -- Alias for `slice.thread_ts`.
-  thread_ts LONG,
-  -- Alias for `slice.thread_dur`.
-  thread_dur LONG
-) AS
-SELECT
-  slice.id,
-  slice.type,
-  slice.ts,
-  slice.dur,
-  slice.category,
-  slice.name,
-  slice.track_id,
-  slice.depth,
-  slice.parent_id,
-  slice.arg_set_id,
-  slice.thread_ts,
-  slice.thread_dur
-FROM slice
-WHERE parent_id = $parent_id;
-
--- Given a slice id, returns the name of the slice.
-CREATE PERFETTO FUNCTION slice_name_from_id(
-  -- The slice id which we need the name for.
-  id LONG
-)
--- The name of slice with the given id.
-RETURNS STRING AS
-SELECT
-  name
-FROM slice
-WHERE $id = id;
-
-CREATE PERFETTO FUNCTION slice_count(
-  -- Name of the slices to counted.
-  slice_glob STRING)
--- Number of slices with the name.
-RETURNS INT AS
-SELECT COUNT(1) FROM slice WHERE name GLOB $slice_glob;
diff --git a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql b/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql
deleted file mode 100644
index bff333f..0000000
--- a/src/trace_processor/perfetto_sql/stdlib/deprecated/v42/common/timestamps.sql
+++ /dev/null
@@ -1,72 +0,0 @@
---
--- Copyright 2022 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 time.conversion;
-
-CREATE PERFETTO FUNCTION is_spans_overlapping(
-  ts1 LONG,
-  ts_end1 LONG,
-  ts2 LONG,
-  ts_end2 LONG)
-RETURNS BOOL AS
-SELECT (IIF($ts1 < $ts2, $ts2, $ts1)
-      < IIF($ts_end1 < $ts_end2, $ts_end1, $ts_end2));
-
-CREATE PERFETTO FUNCTION spans_overlapping_dur(
-  ts1 LONG,
-  dur1 LONG,
-  ts2 LONG,
-  dur2 LONG
-)
-RETURNS INT AS
-SELECT
-  CASE
-    WHEN $dur1 = -1 OR $dur2 = -1 THEN 0
-    WHEN $ts1 + $dur1 < $ts2 OR $ts2 + $dur2 < $ts1 THEN 0
-    WHEN ($ts1 >= $ts2) AND ($ts1 + $dur1 <= $ts2 + $dur2) THEN $dur1
-    WHEN ($ts1 < $ts2) AND ($ts1 + $dur1 < $ts2 + $dur2) THEN $ts1 + $dur1 - $ts2
-    WHEN ($ts1 > $ts2) AND ($ts1 + $dur1 > $ts2 + $dur2) THEN $ts2 + $dur2 - $ts1
-    ELSE $dur2
-  END;
-
--- Renames
-
-CREATE PERFETTO FUNCTION ns(nanos INT)
-RETURNS INT AS
-SELECT time_from_ns($nanos);
-
-CREATE PERFETTO FUNCTION us(micros INT)
-RETURNS INT AS
-SELECT time_from_us($micros);
-
-CREATE PERFETTO FUNCTION ms(millis INT)
-RETURNS INT AS
-SELECT time_from_ms($millis);
-
-CREATE PERFETTO FUNCTION seconds(seconds INT)
-RETURNS INT AS
-SELECT time_from_s($seconds);
-
-CREATE PERFETTO FUNCTION minutes(minutes INT)
-RETURNS INT AS
-SELECT time_from_min($minutes);
-
-CREATE PERFETTO FUNCTION hours(hours INT)
-RETURNS INT AS
-SELECT time_from_hours($hours);
-
-CREATE PERFETTO FUNCTION days(days INT)
-RETURNS INT AS
-SELECT time_from_days($days);
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql b/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql
index e7081ee..50331e6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql
@@ -37,6 +37,24 @@
 FROM intervals_overlap_count!(runnable, ts, dur)
 ORDER BY ts;
 
+-- The count of threads in uninterruptible sleep over time.
+CREATE PERFETTO TABLE sched_uninterruptible_sleep_thread_count(
+  -- Timestamp when the thread count changed to the current value.
+  ts INT,
+  -- Number of threads in uninterrutible sleep, covering the range from this timestamp to the
+  -- next row's timestamp.
+  uninterruptible_sleep_thread_count INT
+) AS
+WITH
+uninterruptible_sleep AS (
+  SELECT ts, dur FROM thread_state
+  where state = 'D'
+)
+SELECT
+  ts, value as uninterruptible_sleep_thread_count
+FROM intervals_overlap_count!(uninterruptible_sleep, ts, dur)
+ORDER BY ts;
+
 -- The count of active CPUs over time.
 CREATE PERFETTO TABLE sched_active_cpu_count(
   -- Timestamp when the number of active CPU changed.
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/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index 8397d8d..b9d4c47 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -501,7 +501,7 @@
   for (int repeat = 0; repeat < 3; repeat++) {
     ASSERT_EQ(RestoreInitialTables(), 0u);
     {
-      auto it = Query("INCLUDE PERFETTO MODULE common.timestamps;");
+      auto it = Query("INCLUDE PERFETTO MODULE time.conversion;");
       it.Next();
       ASSERT_TRUE(it.Status().ok());
     }
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/cmdline_integrationtest.cc b/test/cmdline_integrationtest.cc
index 60cce1f..c4a6fc7 100644
--- a/test/cmdline_integrationtest.cc
+++ b/test/cmdline_integrationtest.cc
@@ -47,6 +47,7 @@
 
 using ::testing::ContainsRegex;
 using ::testing::Each;
+using ::testing::ElementsAre;
 using ::testing::ElementsAreArray;
 using ::testing::Eq;
 using ::testing::HasSubstr;
@@ -99,6 +100,52 @@
   return trace_config;
 }
 
+// For the regular tests.
+TraceConfig CreateTraceConfigForTest(uint32_t test_msg_count = 11,
+                                     uint32_t test_msg_size = 32) {
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(1024);
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("android.perfetto.FakeProducer");
+  ds_config->mutable_for_testing()->set_message_count(test_msg_count);
+  ds_config->mutable_for_testing()->set_message_size(test_msg_size);
+  return trace_config;
+}
+
+void ExpectTraceContainsTestMessages(const protos::gen::Trace& trace,
+                                     uint32_t count) {
+  ssize_t actual_test_packets_count = std::count_if(
+      trace.packet().begin(), trace.packet().end(),
+      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
+  EXPECT_EQ(count, static_cast<uint32_t>(actual_test_packets_count));
+}
+
+void ExpectTraceContainsTestMessagesWithSize(const protos::gen::Trace& trace,
+                                             uint32_t message_size) {
+  for (const auto& packet : trace.packet()) {
+    if (packet.has_for_testing()) {
+      EXPECT_EQ(message_size, packet.for_testing().str().size());
+    }
+  }
+}
+
+void ExpectTraceContainsConfigWithTriggerMode(
+    const protos::gen::Trace& trace,
+    protos::gen::TraceConfig::TriggerConfig::TriggerMode trigger_mode) {
+  // GTest three level nested Property matcher is hard to read, so we use
+  // 'find_if' with lambda to ensure the trace config properly includes the
+  // trigger mode we set.
+  auto found =
+      std::find_if(trace.packet().begin(), trace.packet().end(),
+                   [trigger_mode](const protos::gen::TracePacket& tp) {
+                     return tp.has_trace_config() &&
+                            tp.trace_config().trigger_config().trigger_mode() ==
+                                trigger_mode;
+                   });
+  EXPECT_NE(found, trace.packet().end())
+      << "Trace config doesn't include expected trigger mode.";
+}
+
 class ScopedFileRemove {
  public:
   explicit ScopedFileRemove(const std::string& path) : path_(path) {}
@@ -106,6 +153,27 @@
   std::string path_;
 };
 
+bool ParseNotEmptyTraceFromFile(const std::string& trace_path,
+                                protos::gen::Trace& out) {
+  std::string trace_str;
+  if (!base::ReadFile(trace_path, &trace_str))
+    return false;
+  if (trace_str.empty())
+    return false;
+  return out.ParseFromString(trace_str);
+}
+
+std::vector<std::string> GetReceivedTriggerNames(
+    const protos::gen::Trace& trace) {
+  std::vector<std::string> triggers;
+  for (const protos::gen::TracePacket& packet : trace.packet()) {
+    if (packet.has_trigger()) {
+      triggers.push_back(packet.trigger().trigger_name());
+    }
+  }
+  return triggers;
+}
+
 class PerfettoCmdlineTest : public ::testing::Test {
  public:
   void StartServiceIfRequiredNoNewExecsAfterThis() {
@@ -190,11 +258,8 @@
       // Read the trace written in the fixed location
       // (/data/misc/perfetto-traces/ on Android, /tmp/ on Linux/Mac) and make
       // sure it has the right contents.
-      std::string trace_str;
-      base::ReadFile(trace_path, &trace_str);
-      ASSERT_FALSE(trace_str.empty());
       protos::gen::Trace trace;
-      ASSERT_TRUE(trace.ParseFromString(trace_str));
+      ASSERT_TRUE(ParseNotEmptyTraceFromFile(trace_path, trace));
       uint32_t test_packets = 0;
       for (const auto& p : trace.packet())
         test_packets += p.has_for_testing() ? 1 : 0;
@@ -212,6 +277,11 @@
   std::string stderr_;
   base::TestTaskRunner task_runner_;
 
+  // We use these two constants to set test data payload parameters and assert
+  // it was correctly written to the trace.
+  static constexpr size_t kTestMessageCount = 11;
+  static constexpr size_t kTestMessageSize = 32;
+
  private:
   bool exec_allowed_ = true;
   TestHelper test_helper_{&task_runner_};
@@ -350,15 +420,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, StartTracingTrigger) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::START_TRACING);
@@ -404,53 +467,25 @@
   test_helper().WaitForProducerSetup();
   EXPECT_EQ(0, trigger_proc.Run(&stderr_));
 
-  // Wait for the producer to start, and then write out 11 packets.
+  // Wait for the producer to start, and then write out some test packets.
   test_helper().WaitForProducerEnabled();
   auto on_data_written = task_runner_.CreateCheckpoint("data_written");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written");
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  size_t for_testing_packets = 0;
-  size_t trigger_packets = 0;
-  size_t trace_config_packets = 0;
-  for (const auto& packet : trace.packet()) {
-    if (packet.has_trace_config()) {
-      // Ensure the trace config properly includes the trigger mode we set.
-      auto kStartTrig = protos::gen::TraceConfig::TriggerConfig::START_TRACING;
-      EXPECT_EQ(kStartTrig,
-                packet.trace_config().trigger_config().trigger_mode());
-      ++trace_config_packets;
-    } else if (packet.has_trigger()) {
-      // validate that the triggers are properly added to the trace.
-      EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
-      ++trigger_packets;
-    } else if (packet.has_for_testing()) {
-      // Make sure that the data size is correctly set based on what we
-      // requested.
-      EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
-      ++for_testing_packets;
-    }
-  }
-  EXPECT_EQ(trace_config_packets, 1u);
-  EXPECT_EQ(trigger_packets, 1u);
-  EXPECT_EQ(for_testing_packets, kMessageCount);
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsConfigWithTriggerMode(
+      trace, protos::gen::TraceConfig::TriggerConfig::START_TRACING);
+  EXPECT_THAT(GetReceivedTriggerNames(trace), ElementsAre("trigger_name"));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 TEST_F(PerfettoCmdlineTest, StopTracingTrigger) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -497,8 +532,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -507,56 +542,23 @@
 
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  bool seen_first_trigger = false;
-  size_t for_testing_packets = 0;
-  size_t trigger_packets = 0;
-  size_t trace_config_packets = 0;
-  for (const auto& packet : trace.packet()) {
-    if (packet.has_trace_config()) {
-      // Ensure the trace config properly includes the trigger mode we set.
-      auto kStopTrig = protos::gen::TraceConfig::TriggerConfig::STOP_TRACING;
-      EXPECT_EQ(kStopTrig,
-                packet.trace_config().trigger_config().trigger_mode());
-      ++trace_config_packets;
-    } else if (packet.has_trigger()) {
-      // validate that the triggers are properly added to the trace.
-      if (!seen_first_trigger) {
-        EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
-        seen_first_trigger = true;
-      } else {
-        EXPECT_EQ("trigger_name_3", packet.trigger().trigger_name());
-      }
-      ++trigger_packets;
-    } else if (packet.has_for_testing()) {
-      // Make sure that the data size is correctly set based on what we
-      // requested.
-      EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
-      ++for_testing_packets;
-    }
-  }
-  EXPECT_EQ(trace_config_packets, 1u);
-  EXPECT_EQ(trigger_packets, 2u);
-  EXPECT_EQ(for_testing_packets, kMessageCount);
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsConfigWithTriggerMode(
+      trace, protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+  EXPECT_THAT(GetReceivedTriggerNames(trace),
+              ElementsAre("trigger_name", "trigger_name_3"));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 // Dropbox on the commandline client only works on android builds. So disable
 // this test on all other builds.
 TEST_F(PerfettoCmdlineTest, AndroidOnly(NoDataNoFileWithoutTrigger)) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* incident_config = trace_config.mutable_incident_report_config();
   incident_config->set_destination_package("foo.bar.baz");
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -601,15 +603,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, StopTracingTriggerFromConfig) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -666,8 +661,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -676,44 +671,20 @@
 
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
-  bool seen_first_trigger = false;
-  for (const auto& packet : trace.packet()) {
-    if (packet.has_trace_config()) {
-      // Ensure the trace config properly includes the trigger mode we set.
-      auto kStopTrig = protos::gen::TraceConfig::TriggerConfig::STOP_TRACING;
-      EXPECT_EQ(kStopTrig,
-                packet.trace_config().trigger_config().trigger_mode());
-    } else if (packet.has_trigger()) {
-      // validate that the triggers are properly added to the trace.
-      if (!seen_first_trigger) {
-        EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
-        seen_first_trigger = true;
-      } else {
-        EXPECT_EQ("trigger_name_3", packet.trigger().trigger_name());
-      }
-    } else if (packet.has_for_testing()) {
-      // Make sure that the data size is correctly set based on what we
-      // requested.
-      EXPECT_EQ(kMessageSize, packet.for_testing().str().size());
-    }
-  }
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
+  ExpectTraceContainsConfigWithTriggerMode(
+      trace, protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+  EXPECT_THAT(GetReceivedTriggerNames(trace),
+              ElementsAre("trigger_name", "trigger_name_3"));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 TEST_F(PerfettoCmdlineTest, TriggerFromConfigStopsFileOpening) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 11;
-  constexpr size_t kMessageSize = 32;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -772,15 +743,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, AndroidOnly(CmdTriggerWithUploadFlag)) {
-  // See |message_count| and |message_size| in the TraceConfig above.
-  constexpr size_t kMessageCount = 2;
-  constexpr size_t kMessageSize = 2;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
@@ -831,8 +795,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -841,11 +805,11 @@
 
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+  EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
   EXPECT_THAT(trace.packet(),
               Contains(Property(&protos::gen::TracePacket::trigger,
                                 Property(&protos::gen::Trigger::trigger_name,
@@ -853,14 +817,8 @@
 }
 
 TEST_F(PerfettoCmdlineTest, TriggerCloneSnapshot) {
-  constexpr size_t kMessageCount = 2;
-  constexpr size_t kMessageSize = 2;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   auto* trigger_cfg = trace_config.mutable_trigger_config();
   trigger_cfg->set_trigger_mode(
       protos::gen::TraceConfig::TriggerConfig::CLONE_SNAPSHOT);
@@ -910,8 +868,8 @@
   });
 
   test_helper().WaitForProducerEnabled();
-  // Wait for the producer to start, and then write out 11 packets, before the
-  // trace actually starts (the trigger is seen).
+  // Wait for the producer to start, and then write out some test packets,
+  // before the trace actually starts (the trigger is seen).
   auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
   fake_producer->ProduceEventBatch(test_helper().WrapTask(on_data_written));
   task_runner_.RunUntilCheckpoint("data_written_1");
@@ -931,11 +889,11 @@
   perfetto_proc.SendSigterm();
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(snapshot_path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(snapshot_path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
+  EXPECT_LT(static_cast<int>(kTestMessageCount), trace.packet_size());
   EXPECT_THAT(trace.packet(),
               Contains(Property(&protos::gen::TracePacket::trigger,
                                 Property(&protos::gen::Trigger::trigger_name,
@@ -961,14 +919,9 @@
 }
 
 TEST_F(PerfettoCmdlineTest, CloneByName) {
-  constexpr size_t kMessageCount = 2;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   trace_config.set_unique_session_name("my_unique_session_name");
-  auto* ds_config = trace_config.add_data_sources()->mutable_config();
-  ds_config->set_name("android.perfetto.FakeProducer");
-  ds_config->mutable_for_testing()->set_message_count(kMessageCount);
-  ds_config->mutable_for_testing()->set_message_size(2);
 
   // We have to construct all the processes we want to fork before we start the
   // service with |StartServiceIfRequired()|. this is because it is unsafe
@@ -1026,26 +979,18 @@
   EXPECT_EQ(0, perfetto_proc_clone_2.Run(&stderr_)) << "stderr: " << stderr_;
   EXPECT_FALSE(base::FileExists(path_cloned_2));
 
-  std::string cloned_trace_str;
-  base::ReadFile(path_cloned, &cloned_trace_str);
   protos::gen::Trace cloned_trace;
-  ASSERT_TRUE(cloned_trace.ParseFromString(cloned_trace_str));
-  ssize_t cloned_num_test_packets = std::count_if(
-      cloned_trace.packet().begin(), cloned_trace.packet().end(),
-      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
-  EXPECT_EQ(cloned_num_test_packets, static_cast<ssize_t>(kMessageCount));
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path_cloned, cloned_trace));
+  ExpectTraceContainsTestMessages(cloned_trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(cloned_trace, kTestMessageSize);
 
   perfetto_proc.SendSigterm();
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(cloned_trace_str));
-  ssize_t num_test_packets = std::count_if(
-      trace.packet().begin(), trace.packet().end(),
-      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
-  EXPECT_EQ(num_test_packets, static_cast<ssize_t>(kMessageCount));
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 // Regression test for b/279753347: --save-for-bugreport would create an empty
@@ -1185,10 +1130,8 @@
   auto check_trace = [&](std::string fname, int expected_score) {
     std::string fpath = GetBugreportTraceDir() + "/" + fname;
     ASSERT_TRUE(base::FileExists(fpath)) << fpath;
-    std::string trace_str;
-    base::ReadFile(fpath, &trace_str);
     protos::gen::Trace trace;
-    ASSERT_TRUE(trace.ParseFromString(trace_str)) << fpath;
+    ASSERT_TRUE(ParseNotEmptyTraceFromFile(fpath, trace)) << fpath;
     EXPECT_THAT(
         trace.packet(),
         Contains(Property(&protos::gen::TracePacket::trace_config,
@@ -1211,8 +1154,9 @@
   auto remove_on_exit = base::OnScopeExit(remove_br_files);
 
   const uint32_t kMsgCount = 10000;
+  const uint32_t kMsgSize = 1024;
   TraceConfig cfg = CreateTraceConfigForBugreportTest(
-      /*score=*/1, /*add_filter=*/false, kMsgCount, /*msg_size=*/1024);
+      /*score=*/1, /*add_filter=*/false, kMsgCount, kMsgSize);
 
   auto session_name = "bugreport_test_" +
                       std::to_string(base::GetWallTimeNs().count() % 1000000);
@@ -1263,14 +1207,10 @@
 
   std::string fpath = GetBugreportTraceDir() + "/systrace.pftrace";
   ASSERT_TRUE(base::FileExists(fpath)) << fpath;
-  std::string trace_str;
-  base::ReadFile(fpath, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str)) << fpath;
-  ssize_t num_test_packets = std::count_if(
-      trace.packet().begin(), trace.packet().end(),
-      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
-  EXPECT_EQ(num_test_packets, static_cast<ssize_t>(kMsgCount));
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(fpath, trace)) << fpath;
+  ExpectTraceContainsTestMessages(trace, kMsgCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kMsgSize);
 }
 
 }  // namespace perfetto
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 92108de..69c1bc7 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -117,8 +117,6 @@
 from diff_tests.stdlib.android.startups_tests import Startups
 from diff_tests.stdlib.android.tests import AndroidStdlib
 from diff_tests.stdlib.chrome.chrome_stdlib_testsuites import CHROME_STDLIB_TESTSUITES
-from diff_tests.stdlib.common.tests import StdlibCommon
-from diff_tests.stdlib.common.tests import StdlibCommon
 from diff_tests.stdlib.counters.tests import StdlibCounterIntervals
 from diff_tests.stdlib.dynamic_tables.tests import DynamicTables
 from diff_tests.stdlib.export.tests import ExportTests
@@ -326,7 +324,6 @@
       *Pkvm(index_path, 'stdlib/pkvm', 'Pkvm').fetch(),
       *PreludeSlices(index_path, 'stdlib/prelude', 'PreludeSlices').fetch(),
       *StdlibSmoke(index_path, 'stdlib', 'StdlibSmoke').fetch(),
-      *StdlibCommon(index_path, 'stdlib/common', 'StdlibCommon').fetch(),
       *Slices(index_path, 'stdlib/slices', 'Slices').fetch(),
       *SpanJoinLeftJoin(index_path, 'stdlib/span_join',
                         'SpanJoinLeftJoin').fetch(),
@@ -335,7 +332,6 @@
       *SpanJoinRegression(index_path, 'stdlib/span_join',
                           'SpanJoinRegression').fetch(),
       *SpanJoinSmoke(index_path, 'stdlib/span_join', 'SpanJoinSmoke').fetch(),
-      *StdlibCommon(index_path, 'stdlib/common', 'StdlibCommon').fetch(),
       *StdlibIntervals(index_path, 'stdlib/intervals',
                        'StdlibIntervals').fetch(),
       *IntervalsIntersect(index_path, 'stdlib/intervals',
diff --git a/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py
index 597cdcd..1ce67ab 100644
--- a/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/metrics/chrome/tests_scroll_jank.py
@@ -492,7 +492,7 @@
         INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_v3;
 
         SELECT
-          HAS_DESCENDANT_SLICE_WITH_NAME(
+          _HAS_DESCENDANT_SLICE_WITH_NAME(
             (SELECT id from slice where dur = 60156000),
             'SwapEndToPresentationCompositorFrame') AS has_descendant;
         """,
@@ -510,7 +510,7 @@
         INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_v3;
 
         SELECT
-          HAS_DESCENDANT_SLICE_WITH_NAME(
+          _HAS_DESCENDANT_SLICE_WITH_NAME(
             (SELECT id from slice where dur = 77247000),
             'SwapEndToPresentationCompositorFrame') AS has_descendant;
         """,
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup.out b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
index 42368b3..ec2b075 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
@@ -78,11 +78,15 @@
       }
       launch_dur: 108
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 130
+          end_timestamp: 210
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 130
         end_timestamp: 210
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     startup_type: "warm"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
index 2d6070c..8b53348 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
@@ -148,12 +148,16 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 340
+          end_timestamp: 390
+          slice_id: 20
+          slice_name: "CollectorTransition mark sweep GC"
+          process_pid: 3
+          thread_tid: 5
+        }
         start_timestamp: 340
         end_timestamp: 390
-        slice_id: 20
-        slice_name: "CollectorTransition mark sweep GC"
-        process_pid: 3
-        thread_tid: 5
       }
     }
     slow_start_reason_with_details {
@@ -171,20 +175,24 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
-        start_timestamp: 170
-        end_timestamp: 500000000
-        slice_id: 9
-        slice_name: "OpenDexFilesFromOat(something else)"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 170
+          end_timestamp: 500000000
+          slice_id: 9
+          slice_name: "OpenDexFilesFromOat(something else)"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 150
+          end_timestamp: 165
+          slice_id: 5
+          slice_name: "OpenDexFilesFromOat(something)"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 150
-        end_timestamp: 165
-        slice_id: 5
-        slice_name: "OpenDexFilesFromOat(something)"
-        process_pid: 3
-        thread_tid: 3
+        end_timestamp: 500000000
       }
     }
     slow_start_reason_with_details {
@@ -200,12 +208,16 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 10000000
+          end_timestamp: 50000000
+          slice_id: 21
+          slice_name: "binder transaction"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 10000000
         end_timestamp: 50000000
-        slice_id: 21
-        slice_name: "binder transaction"
-        process_pid: 3
-        thread_tid: 3
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
index a98505f..a4e1074 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
@@ -107,12 +107,16 @@
       }
       launch_dur: 999999900000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 340000000000
+          end_timestamp: 390000000000
+          slice_id: 91
+          slice_name: "CollectorTransition mark sweep GC"
+          process_pid: 3
+          thread_tid: 5
+        }
         start_timestamp: 340000000000
         end_timestamp: 390000000000
-        slice_id: 91
-        slice_name: "CollectorTransition mark sweep GC"
-        process_pid: 3
-        thread_tid: 5
       }
     }
     slow_start_reason_with_details {
@@ -129,25 +133,29 @@
       }
       launch_dur: 999999900000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 155000000000
+          end_timestamp: 165000000000
+          thread_name: "Jit thread pool"
+          process_pid: 3
+          thread_tid: 4
+        }
+        thread_section {
+          start_timestamp: 170000000000
+          end_timestamp: 175000000000
+          thread_name: "Jit thread pool"
+          process_pid: 3
+          thread_tid: 4
+        }
+        thread_section {
+          start_timestamp: 185000000000
+          end_timestamp: 190000000000
+          thread_name: "Jit thread pool"
+          process_pid: 3
+          thread_tid: 4
+        }
         start_timestamp: 155000000000
-        end_timestamp: 165000000000
-        thread_name: "Jit thread pool"
-        thread_tid: 4
-        process_pid: 3
-      }
-      trace_thread_sections {
-        start_timestamp: 170000000000
-        end_timestamp: 175000000000
-        thread_name: "Jit thread pool"
-        thread_tid: 4
-        process_pid: 3
-      }
-      trace_thread_sections {
-        start_timestamp: 185000000000
         end_timestamp: 190000000000
-        thread_name: "Jit thread pool"
-        thread_tid: 4
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -164,28 +172,32 @@
       }
       launch_dur: 999999900000000000
       trace_slice_sections {
-        start_timestamp: 200000000000
-        end_timestamp: 210000000000
-        slice_id: 84
-        slice_name: "JIT compiling nothing"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 200000000000
+          end_timestamp: 210000000000
+          slice_id: 84
+          slice_name: "JIT compiling nothing"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 100000000000
+          end_timestamp: 101000000000
+          slice_id: 9
+          slice_name: "JIT compiling something"
+          process_pid: 3
+          thread_tid: 4
+        }
+        slice_section {
+          start_timestamp: 101000000000
+          end_timestamp: 102000000000
+          slice_id: 10
+          slice_name: "JIT compiling something"
+          process_pid: 3
+          thread_tid: 4
+        }
         start_timestamp: 100000000000
-        end_timestamp: 101000000000
-        slice_id: 9
-        slice_name: "JIT compiling something"
-        process_pid: 3
-        thread_tid: 4
-      }
-      trace_slice_sections {
-        start_timestamp: 101000000000
-        end_timestamp: 102000000000
-        slice_id: 10
-        slice_name: "JIT compiling something"
-        process_pid: 3
-        thread_tid: 4
+        end_timestamp: 210000000000
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
index 2405d7e..04b8915 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
@@ -128,12 +128,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 204000000000
+          end_timestamp: 205000000000
+          slice_id: 13
+          slice_name: "location=/system/framework/oat/arm/com.google.android.calendar.odex status=up-to-date filter=speed reason=install-dm"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 204000000000
         end_timestamp: 205000000000
-        slice_id: 13
-        slice_name: "location=/system/framework/oat/arm/com.google.android.calendar.odex status=up-to-date filter=speed reason=install-dm"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -149,12 +153,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 200000000000
+          end_timestamp: 202000000000
+          slice_id: 12
+          slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 200000000000
         end_timestamp: 202000000000
-        slice_id: 12
-        slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -171,12 +179,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 185000000000
+          end_timestamp: 187000000000
+          slice_id: 4
+          slice_name: "bindApplication"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 185000000000
         end_timestamp: 187000000000
-        slice_id: 4
-        slice_name: "bindApplication"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -193,20 +205,24 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 188000000000
+          end_timestamp: 189000000000
+          slice_id: 6
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 191000000000
+          end_timestamp: 192000000000
+          slice_id: 8
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 188000000000
-        end_timestamp: 189000000000
-        slice_id: 6
-        slice_name: "inflate"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
-        start_timestamp: 191000000000
         end_timestamp: 192000000000
-        slice_id: 8
-        slice_name: "inflate"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -223,12 +239,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 188000000000
+          end_timestamp: 189000000000
+          slice_id: 7
+          slice_name: "ResourcesManager#getResources"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 188000000000
         end_timestamp: 189000000000
-        slice_id: 7
-        slice_name: "ResourcesManager#getResources"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -245,11 +265,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 205000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 205000000000
         end_timestamp: 210000000000
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
index 3dbb2aa..f2c7123 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
@@ -127,12 +127,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 200000000000
+          end_timestamp: 202000000000
+          slice_id: 12
+          slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 200000000000
         end_timestamp: 202000000000
-        slice_id: 12
-        slice_name: "location=error status=io-error-no-oat filter=run-from-apk reason=unknown"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -149,12 +153,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 185000000000
+          end_timestamp: 195000000000
+          slice_id: 4
+          slice_name: "bindApplication"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 185000000000
         end_timestamp: 195000000000
-        slice_id: 4
-        slice_name: "bindApplication"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -171,20 +179,24 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
-        start_timestamp: 190000000000
-        end_timestamp: 192000000000
-        slice_id: 8
-        slice_name: "inflate"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 190000000000
+          end_timestamp: 192000000000
+          slice_id: 8
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 188000000000
+          end_timestamp: 189000000000
+          slice_id: 7
+          slice_name: "inflate"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 188000000000
-        end_timestamp: 189000000000
-        slice_id: 7
-        slice_name: "inflate"
-        process_pid: 3
-        thread_tid: 3
+        end_timestamp: 192000000000
       }
     }
     slow_start_reason_with_details {
@@ -201,12 +213,16 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 187000000000
+          end_timestamp: 192000000000
+          slice_id: 5
+          slice_name: "ResourcesManager#getResources"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 187000000000
         end_timestamp: 192000000000
-        slice_id: 5
-        slice_name: "ResourcesManager#getResources"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -223,11 +239,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 205000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 205000000000
         end_timestamp: 210000000000
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
index 4913852..6cb97e0 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
@@ -46,25 +46,29 @@
       }
       launch_dur: 100
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 105
+          end_timestamp: 106
+          slice_id: 6
+          slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+          thread_tid: 1
+        }
+        slice_section {
+          start_timestamp: 106
+          end_timestamp: 107
+          slice_id: 8
+          slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+          thread_tid: 1
+        }
+        slice_section {
+          start_timestamp: 107
+          end_timestamp: 108
+          slice_id: 10
+          slice_name: "Broadcast dispatched from android (2005:system/1000) x"
+          thread_tid: 1
+        }
         start_timestamp: 105
-        end_timestamp: 106
-        slice_id: 6
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-        thread_tid: 1
-      }
-      trace_slice_sections {
-        start_timestamp: 106
-        end_timestamp: 107
-        slice_id: 8
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-        thread_tid: 1
-      }
-      trace_slice_sections {
-        start_timestamp: 107
         end_timestamp: 108
-        slice_id: 10
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-        thread_tid: 1
       }
     }
     slow_start_reason_with_details {
@@ -81,25 +85,29 @@
       }
       launch_dur: 100
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 100
+          end_timestamp: 101
+          slice_id: 1
+          slice_name: "broadcastReceiveReg: x"
+          thread_tid: 2
+        }
+        slice_section {
+          start_timestamp: 101
+          end_timestamp: 102
+          slice_id: 2
+          slice_name: "broadcastReceiveReg: x"
+          thread_tid: 2
+        }
+        slice_section {
+          start_timestamp: 102
+          end_timestamp: 103
+          slice_id: 3
+          slice_name: "broadcastReceiveReg: x"
+          thread_tid: 2
+        }
         start_timestamp: 100
-        end_timestamp: 101
-        slice_id: 1
-        slice_name: "broadcastReceiveReg: x"
-        thread_tid: 2
-      }
-      trace_slice_sections {
-        start_timestamp: 101
-        end_timestamp: 102
-        slice_id: 2
-        slice_name: "broadcastReceiveReg: x"
-        thread_tid: 2
-      }
-      trace_slice_sections {
-        start_timestamp: 102
         end_timestamp: 103
-        slice_id: 3
-        slice_name: "broadcastReceiveReg: x"
-        thread_tid: 2
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
index dabbb5f..f64dceb 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
@@ -82,12 +82,16 @@
       }
       launch_dur: 100000000000
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 112000000000
+          end_timestamp: 115000000000
+          slice_id: 1
+          slice_name: "bindApplication"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 112000000000
         end_timestamp: 115000000000
-        slice_id: 1
-        slice_name: "bindApplication"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -105,28 +109,32 @@
       }
       launch_dur: 100000000000
       trace_slice_sections {
-        start_timestamp: 140000000000
-        end_timestamp: 157000000000
-        slice_id: 5
-        slice_name: "Lock contention on a monitor lock (owner tid: 2)"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
+        slice_section {
+          start_timestamp: 140000000000
+          end_timestamp: 157000000000
+          slice_id: 5
+          slice_name: "Lock contention on a monitor lock (owner tid: 2)"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 120000000000
+          end_timestamp: 130000000000
+          slice_id: 4
+          slice_name: "Lock contention on thread list lock (owner tid: 2)"
+          process_pid: 3
+          thread_tid: 3
+        }
+        slice_section {
+          start_timestamp: 155000000000
+          end_timestamp: 160000000000
+          slice_id: 6
+          slice_name: "Lock contention on a monitor lock (owner tid: 3)"
+          process_pid: 3
+          thread_tid: 4
+        }
         start_timestamp: 120000000000
-        end_timestamp: 130000000000
-        slice_id: 4
-        slice_name: "Lock contention on thread list lock (owner tid: 2)"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
-        start_timestamp: 155000000000
         end_timestamp: 160000000000
-        slice_id: 6
-        slice_name: "Lock contention on a monitor lock (owner tid: 3)"
-        process_pid: 3
-        thread_tid: 4
       }
     }
     slow_start_reason_with_details {
@@ -144,20 +152,24 @@
      }
      launch_dur: 100000000000
      trace_slice_sections {
+       slice_section {
+         start_timestamp: 140000000000
+         end_timestamp: 157000000000
+         slice_id: 5
+         slice_name: "Lock contention on a monitor lock (owner tid: 2)"
+         process_pid: 3
+         thread_tid: 3
+       }
+       slice_section {
+         start_timestamp: 155000000000
+         end_timestamp: 160000000000
+         slice_id: 6
+         slice_name: "Lock contention on a monitor lock (owner tid: 3)"
+         process_pid: 3
+         thread_tid: 4
+       }
        start_timestamp: 140000000000
-       end_timestamp: 157000000000
-       slice_id: 5
-       slice_name: "Lock contention on a monitor lock (owner tid: 2)"
-       process_pid: 3
-       thread_tid: 3
-     }
-     trace_slice_sections {
-       start_timestamp: 155000000000
        end_timestamp: 160000000000
-       slice_id: 6
-       slice_name: "Lock contention on a monitor lock (owner tid: 3)"
-       process_pid: 3
-       thread_tid: 4
      }
     }
     startup_type: "cold"
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
index ded275e..17d426d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
@@ -77,11 +77,15 @@
       }
       launch_dur: 7
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 103
+          end_timestamp: 107
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 103
         end_timestamp: 107
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
   }
@@ -164,11 +168,15 @@
       }
       launch_dur: 7
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 203
+          end_timestamp: 207
+          thread_name: "com.google.android.calendar"
+          process_pid: 4
+          thread_tid: 4
+        }
         start_timestamp: 203
         end_timestamp: 207
-        thread_name: "com.google.android.calendar"
-        thread_tid: 4
-        process_pid: 4
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
index 942037e..0ae5b29 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
@@ -81,11 +81,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 130000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 130000000000
         end_timestamp: 210000000000
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -102,11 +106,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 120000000000
+          end_timestamp: 125000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 120000000000
         end_timestamp: 125000000000
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -123,11 +131,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 125000000000
+          end_timestamp: 130000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 125000000000
         end_timestamp: 130000000000
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -144,11 +156,15 @@
       }
       launch_dur: 108000000000
       trace_thread_sections {
+        thread_section {
+          start_timestamp: 130000000000
+          end_timestamp: 210000000000
+          thread_name: "com.google.android.calendar"
+          process_pid: 3
+          thread_tid: 3
+        }
         start_timestamp: 130000000000
         end_timestamp: 210000000000
-        thread_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
index 3f52432..1f2000d 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
@@ -44,11 +44,15 @@
       }
       launch_dur: 100
       trace_slice_sections {
+        slice_section {
+          start_timestamp: 130
+          end_timestamp: 133
+          slice_id: 1
+          slice_name: "KeyguardUpdateMonitor#onAuthenticationSucceeded"
+          thread_tid: 2
+        }
         start_timestamp: 130
         end_timestamp: 133
-        slice_id: 1
-        slice_name: "KeyguardUpdateMonitor#onAuthenticationSucceeded"
-        thread_tid: 2
       }
     }
   }
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/common/tests.py b/test/trace_processor/diff_tests/stdlib/common/tests.py
deleted file mode 100644
index d103320..0000000
--- a/test/trace_processor/diff_tests/stdlib/common/tests.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License a
-#
-#      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.
-
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
-from python.generators.diff_tests.testing import TestSuite
-
-
-class StdlibCommon(TestSuite):
-
-  def test_spans_overlapping_dur_intersect_edge(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 2, 1, 2) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_intersect_edge_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(1, 2, 0, 2) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_intersect_all(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 3, 1, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_intersect_all_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(1, 1, 0, 3) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        1
-        """))
-
-  def test_spans_overlapping_dur_no_intersect(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 1, 2, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
-
-  def test_spans_overlapping_dur_no_intersect_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(2, 1, 0, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
-
-  def test_spans_overlapping_dur_negative_dur(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, -1, 0, 1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
-
-  def test_spans_overlapping_dur_negative_dur_reversed(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-
-        """),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT SPANS_OVERLAPPING_DUR(0, 1, 0, -1) AS dur
-        """,
-        out=Csv("""
-        "dur"
-        0
-        """))
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/test/trace_processor/diff_tests/stdlib/timestamps/tests.py b/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
index a029ce5..d5cc34b 100644
--- a/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
@@ -22,86 +22,60 @@
 
 class Timestamps(TestSuite):
 
-  def test_ns(self):
+  def test_to_time(self):
     return DiffTestBlueprint(
         trace=TextProto(""),
         query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT ns(4) as result;
+        INCLUDE PERFETTO MODULE time.conversion;
+
+        WITH data(unit, time) AS (
+          VALUES
+            ('ns', time_to_ns(cast_int!(1e14))),
+            ('us', time_to_us(cast_int!(1e14))),
+            ('ms', time_to_ms(cast_int!(1e14))),
+            ('s', time_to_s(cast_int!(1e14))),
+            ('min', time_to_min(cast_int!(1e14))),
+            ('h', time_to_hours(cast_int!(1e14))),
+            ('days', time_to_days(cast_int!(1e14)))
+        )
+        SELECT * FROM data
       """,
         out=Csv("""
-        "result"
-        4
+        "unit","time"
+        "ns",100000000000000
+        "us",100000000000
+        "ms",100000000
+        "s",100000
+        "min",1666
+        "h",27
+        "days",1
       """))
 
-  def test_us(self):
+  def test_from_time(self):
     return DiffTestBlueprint(
         trace=TextProto(""),
         query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT us(4) as result;
-      """,
-        out=Csv("""
-        "result"
-        4000
-      """))
+        INCLUDE PERFETTO MODULE time.conversion;
 
-  def test_ms(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT ms(4) as result;
+        WITH data(unit, time) AS (
+          VALUES
+            ('ns', time_from_ns(1)),
+            ('us', time_from_us(1)),
+            ('ms', time_from_ms(1)),
+            ('s', time_from_s(1)),
+            ('min', time_from_min(1)),
+            ('h', time_from_hours(1)),
+            ('days', time_from_days(1))
+        )
+        SELECT * FROM data
       """,
         out=Csv("""
-        "result"
-        4000000
-      """))
-
-  def test_seconds(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT seconds(4) as result;
-      """,
-        out=Csv("""
-        "result"
-        4000000000
-      """))
-
-  def test_minutes(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT minutes(1) as result;
-      """,
-        out=Csv("""
-        "result"
-        60000000000
-      """))
-
-  def test_hours(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT hours(1) as result;
-      """,
-        out=Csv("""
-        "result"
-        3600000000000
-      """))
-
-  def test_days(self):
-    return DiffTestBlueprint(
-        trace=TextProto(""),
-        query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
-        SELECT days(1) as result;
-      """,
-        out=Csv("""
-        "result"
-        86400000000000
-      """))
+        "unit","time"
+        "ns",1
+        "us",1000
+        "ms",1000000
+        "s",1000000000
+        "min",60000000000
+        "h",3600000000000
+        "days",86400000000000
+      """))
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/syntax/include_tests.py b/test/trace_processor/diff_tests/syntax/include_tests.py
index 6c4d88a..6316ae6 100644
--- a/test/trace_processor/diff_tests/syntax/include_tests.py
+++ b/test/trace_processor/diff_tests/syntax/include_tests.py
@@ -23,121 +23,40 @@
 
   def test_import(self):
     return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 1
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|13\n"
-              }
-            }
-            event {
-              timestamp: 4000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|20\n"
-              }
-            }
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.audio|1\n"
-              }
-            }
-          }
-        }
-        """),
+        trace=TextProto(''),
         query="""
-        SELECT IMPORT('common.timestamps');
+        SELECT IMPORT('time.conversion');
 
-        SELECT TRACE_START();
+        SELECT 1 AS x;
         """,
         out=Csv("""
-        "TRACE_START()"
-        1000
+        "x"
+        1
         """))
 
   def test_include_perfetto_module(self):
     return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 1
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|13\n"
-              }
-            }
-            event {
-              timestamp: 4000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|20\n"
-              }
-            }
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.audio|1\n"
-              }
-            }
-          }
-        }
-        """),
+        trace=TextProto(''),
         query="""
-        INCLUDE PERFETTO MODULE common.timestamps;
+        INCLUDE PERFETTO MODULE time.conversion;
 
-        SELECT TRACE_START();
+        SELECT time_to_ns(1) AS x
         """,
         out=Csv("""
-        "TRACE_START()"
-        1000
+        "x"
+        1
         """))
 
   def test_include_and_import(self):
     return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 1
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|13\n"
-              }
-            }
-            event {
-              timestamp: 4000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.data_conn|20\n"
-              }
-            }
-            event {
-              timestamp: 1000
-              pid: 1
-              print {
-                buf: "C|1000|battery_stats.audio|1\n"
-              }
-            }
-          }
-        }
-        """),
+        trace=TextProto(''),
         query="""
-        SELECT IMPORT('common.timestamps');
-        INCLUDE PERFETTO MODULE common.timestamps;
+        SELECT IMPORT('time.conversion');
+        INCLUDE PERFETTO MODULE time.conversion;
 
-        SELECT TRACE_START();
+        SELECT 1 AS x
         """,
         out=Csv("""
-        "TRACE_START()"
-        1000
+        "x"
+        1
         """))
diff --git a/tools/check_sql_modules.py b/tools/check_sql_modules.py
index f787143..ed8ddc3 100755
--- a/tools/check_sql_modules.py
+++ b/tools/check_sql_modules.py
@@ -106,7 +106,10 @@
 
       if (include_package == "common"):
         errors.append(
-            "Common module has been deprecated in the standard library.")
+            "Common module has been deprecated in the standard library. "
+            "Please check `slices.with_context` for a replacement for "
+            "`common.slices` and `time.conversion` for replacement for "
+            "`common.timestamps`")
 
       if (package != "viz" and include_package == "viz"):
         errors.append("No modules can depend on 'viz' outside 'viz' package.")
diff --git a/tools/gen_bazel b/tools/gen_bazel
index a30f5ca..a5776ba 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -83,12 +83,12 @@
     '//src/trace_processor:trace_processor_shell',
     '//src/trace_processor:trace_processor',
     '//src/traceconv:traceconv',
-    '//src/traceconv:libpprofbuilder',
 ]
 
 # These targets will be exported with visibility only to our allowlist.
 allowlist_public_targets = [
     '//src/shared_lib:libperfetto_c',
+    '//src/traceconv:libpprofbuilder',
 ]
 
 # These targets are required by internal build rules but don't need to be
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/base/mithril_utils.ts b/ui/src/base/mithril_utils.ts
index b450f64..9b0615d 100644
--- a/ui/src/base/mithril_utils.ts
+++ b/ui/src/base/mithril_utils.ts
@@ -52,3 +52,36 @@
     );
   },
 };
+
+/**
+ * Utility function to pre-bind some mithril attrs of a component, and leave
+ * the others unbound and passed at run-time.
+ * Example use case: the Page API Passes to the registered page a PageAttrs,
+ * which is {subpage:string}. Imagine you write a MyPage component that takes
+ * some extra input attrs (e.g. the App object) and you want to bind them
+ * onActivate(). The results looks like this:
+ *
+ * interface MyPageAttrs extends PageAttrs { app: App; }
+ *
+ * class MyPage extends m.classComponent<MyPageAttrs> {... view() {...} }
+ *
+ * onActivate(app: App) {
+ *   pages.register(... bindMithrilApps(MyPage, {app: app});
+ * }
+ *
+ * The return value of bindMithrilApps is a mithril component that takes in
+ * input only a {subpage: string} and passes down to MyPage the combination
+ * of pre-bound and runtime attrs, that is {subpage, app}.
+ */
+export function bindMithrilAttrs<BaseAttrs, Attrs>(
+  component: m.ComponentTypes<Attrs>,
+  boundArgs: Omit<Attrs, keyof BaseAttrs>,
+): m.Component<BaseAttrs> {
+  return {
+    view(vnode: m.Vnode<BaseAttrs>) {
+      const attrs = {...vnode.attrs, ...boundArgs} as Attrs;
+      const emptyAttrs: m.CommonAttributes<Attrs, {}> = {}; // Keep tsc happy.
+      return m<Attrs, {}>(component, {...attrs, ...emptyAttrs});
+    },
+  };
+}
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 916fca9..de15873 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -21,13 +21,13 @@
   ConsumerPortResponse,
   GetTraceStatsResponse,
   ReadBuffersResponse,
-} from '../controller/consumer_port_types';
-import {RpcConsumerPort} from '../controller/record_controller_interfaces';
+} from '../plugins/dev.perfetto.RecordTrace/consumer_port_types';
+import {RpcConsumerPort} from '../plugins/dev.perfetto.RecordTrace/record_controller_interfaces';
 import {
   browserSupportsPerfettoConfig,
   extractTraceConfig,
   hasSystemDataSourceConfig,
-} from '../core/trace_config_utils';
+} from '../plugins/dev.perfetto.RecordTrace/trace_config_utils';
 import {ITraceStats, TraceConfig} from '../protos';
 
 import {DevToolsSocket} from './devtools_socket';
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
deleted file mode 100644
index cc10366..0000000
--- a/ui/src/common/constants.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (C) 2021 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.
-
-export const TRACE_SUFFIX = '.perfetto-trace';
diff --git a/ui/src/common/recordingV2/recording_error_handling.ts b/ui/src/common/recordingV2/recording_error_handling.ts
deleted file mode 100644
index ffec467..0000000
--- a/ui/src/common/recordingV2/recording_error_handling.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-// Copyright (C) 2022 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 {getErrorMessage} from '../../base/errors';
-import {
-  showAllowUSBDebugging,
-  showConnectionLostError,
-  showExtensionNotInstalled,
-  showFailedToPushBinary,
-  showIssueParsingTheTracedResponse,
-  showNoDeviceSelected,
-  showWebsocketConnectionIssue,
-  showWebUSBErrorV2,
-} from '../../frontend/error_dialog';
-import {OnMessageCallback} from './recording_interfaces_v2';
-import {
-  ALLOW_USB_DEBUGGING,
-  BINARY_PUSH_FAILURE,
-  BINARY_PUSH_UNKNOWN_RESPONSE,
-  EXTENSION_NOT_INSTALLED,
-  NO_DEVICE_SELECTED,
-  PARSING_UNABLE_TO_DECODE_METHOD,
-  PARSING_UNKNWON_REQUEST_ID,
-  PARSING_UNRECOGNIZED_MESSAGE,
-  PARSING_UNRECOGNIZED_PORT,
-  WEBSOCKET_UNABLE_TO_CONNECT,
-} from './recording_utils';
-
-// The pattern for handling recording error can have the following nesting in
-// case of errors:
-// A. wrapRecordingError -> wraps a promise
-// B. onFailure -> has user defined logic and calls showRecordingModal
-// C. showRecordingModal -> shows UX for a given error; this is not called
-//    directly by wrapRecordingError, because we want the caller (such as the
-//    UI) to dictate the UX
-
-// This method takes a promise and a callback to be execute in case the promise
-// fails. It then awaits the promise and executes the callback in case of
-// failure. In the recording code it is used to wrap:
-// 1. Acessing the WebUSB API.
-// 2. Methods returning promises which can be rejected. For instance:
-// a) When the user clicks 'Add a new device' but then doesn't select a valid
-//    device.
-// b) When the user starts a tracing session, but cancels it before they
-//    authorize the session on the device.
-export async function wrapRecordingError<T>(
-  promise: Promise<T>,
-  onFailure: OnMessageCallback,
-): Promise<T | undefined> {
-  try {
-    return await promise;
-  } catch (e) {
-    // Sometimes the message is wrapped in an Error object, sometimes not, so
-    // we make sure we transform it into a string.
-    const errorMessage = getErrorMessage(e);
-    onFailure(errorMessage);
-    return undefined;
-  }
-}
-
-// Shows a modal for every known type of error which can arise during recording.
-// In this way, errors occuring at different levels of the recording process
-// can be handled in a central location.
-export function showRecordingModal(message: string): void {
-  if (
-    [
-      'Unable to claim interface.',
-      'The specified endpoint is not part of a claimed and selected ' +
-        'alternate interface.',
-      // thrown when calling the 'reset' method on a WebUSB device.
-      'Unable to reset the device.',
-    ].some((partOfMessage) => message.includes(partOfMessage))
-  ) {
-    showWebUSBErrorV2();
-  } else if (
-    [
-      'A transfer error has occurred.',
-      'The device was disconnected.',
-      'The transfer was cancelled.',
-    ].some((partOfMessage) => message.includes(partOfMessage)) ||
-    isDeviceDisconnectedError(message)
-  ) {
-    showConnectionLostError();
-  } else if (message === ALLOW_USB_DEBUGGING) {
-    showAllowUSBDebugging();
-  } else if (
-    isMessageComposedOf(message, [
-      BINARY_PUSH_FAILURE,
-      BINARY_PUSH_UNKNOWN_RESPONSE,
-    ])
-  ) {
-    showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
-  } else if (message === NO_DEVICE_SELECTED) {
-    showNoDeviceSelected();
-  } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
-    showWebsocketConnectionIssue(message);
-  } else if (message === EXTENSION_NOT_INSTALLED) {
-    showExtensionNotInstalled();
-  } else if (
-    isMessageComposedOf(message, [
-      PARSING_UNKNWON_REQUEST_ID,
-      PARSING_UNABLE_TO_DECODE_METHOD,
-      PARSING_UNRECOGNIZED_PORT,
-      PARSING_UNRECOGNIZED_MESSAGE,
-    ])
-  ) {
-    showIssueParsingTheTracedResponse(message);
-  } else {
-    throw new Error(`${message}`);
-  }
-}
-
-function isDeviceDisconnectedError(message: string) {
-  return (
-    message.includes('Device with serial') &&
-    message.includes('was disconnected.')
-  );
-}
-
-function isMessageComposedOf(message: string, issues: string[]) {
-  for (const issue of issues) {
-    if (message.includes(issue)) {
-      return true;
-    }
-  }
-  return false;
-}
-
-// Exception thrown by the Recording logic.
-export class RecordingError extends Error {}
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index ab9718a..3336045 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -34,6 +34,8 @@
 import {PageHandler} from '../public/page';
 import {setPerfHooks} from './perf';
 import {ServiceWorkerController} from '../frontend/service_worker_controller';
+import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
+import {featureFlags} from './feature_flags';
 
 // The args that frontend/index.ts passes when calling AppImpl.initialize().
 // This is to deal with injections that would otherwise cause circular deps.
@@ -77,19 +79,31 @@
   // The currently open trace.
   currentTrace?: TraceContext;
 
+  private static _instance: AppContext;
+
+  static initialize(initArgs: AppInitArgs): AppContext {
+    assertTrue(AppContext._instance === undefined);
+    return (AppContext._instance = new AppContext(initArgs));
+  }
+
+  static get instance(): AppContext {
+    return assertExists(AppContext._instance);
+  }
+
   // This constructor is invoked only once, when frontend/index.ts invokes
   // AppMainImpl.initialize().
-  constructor(initArgs: AppInitArgs) {
+  private constructor(initArgs: AppInitArgs) {
     this.initArgs = initArgs;
     this.initialRouteArgs = initArgs.initialRouteArgs;
-    this.sidebarMgr = new SidebarManagerImpl({
-      sidebarEnabled: !this.initialRouteArgs.hideSidebar,
-    });
     this.serviceWorkerController = new ServiceWorkerController();
     this.embeddedMode = this.initialRouteArgs.mode === 'embedded';
     this.testingMode =
       self.location !== undefined &&
       self.location.search.indexOf('testing=1') >= 0;
+    this.sidebarMgr = new SidebarManagerImpl({
+      disabled: this.embeddedMode,
+      hidden: this.initialRouteArgs.hideSidebar,
+    });
     this.analytics = initAnalytics(this.testingMode, this.embeddedMode);
     this.pluginMgr = new PluginManagerImpl({
       forkForPlugin: (pluginId) => this.forPlugin(pluginId),
@@ -141,19 +155,16 @@
   private readonly appCtx: AppContext;
   private readonly pageMgrProxy: PageManagerImpl;
 
+  // Invoked by frontend/index.ts.
+  static initialize(args: AppInitArgs) {
+    AppContext.initialize(args).forPlugin(CORE_PLUGIN_ID);
+  }
+
   // Gets access to the one instance that the core can use. Note that this is
   // NOT the only instance, as other AppImpl instance will be created for each
   // plugin.
-  private static _instance: AppImpl;
-
-  // Invoked by frontend/index.ts.
-  static initialize(args: AppInitArgs) {
-    assertTrue(AppImpl._instance === undefined);
-    AppImpl._instance = new AppContext(args).forPlugin(CORE_PLUGIN_ID);
-  }
-
   static get instance(): AppImpl {
-    return assertExists(AppImpl._instance);
+    return AppContext.instance.forPlugin(CORE_PLUGIN_ID);
   }
 
   // Only called by AppContext.forPlugin().
@@ -171,6 +182,10 @@
     });
   }
 
+  forPlugin(pluginId: string): AppImpl {
+    return this.appCtx.forPlugin(pluginId);
+  }
+
   get commands(): CommandManagerImpl {
     return this.appCtx.commandMgr;
   }
@@ -211,6 +226,12 @@
     return this.appCtx.initialRouteArgs;
   }
 
+  get featureFlags(): FeatureFlagManager {
+    return {
+      register: (settings: FlagSettings) => featureFlags.register(settings),
+    };
+  }
+
   openTraceFromFile(file: File): void {
     this.openTrace({type: 'FILE', file});
   }
@@ -228,7 +249,6 @@
   }
 
   private async openTrace(src: TraceSource) {
-    assertTrue(this.pluginId === CORE_PLUGIN_ID);
     this.appCtx.closeCurrentTrace();
     this.appCtx.isLoadingTrace = true;
     try {
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index c30256a..4791e8e 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',
@@ -55,6 +56,7 @@
   'dev.perfetto.ProcessSummary',
   'dev.perfetto.ProcessThreadGroups',
   'dev.perfetto.QueryPage',
+  'dev.perfetto.RecordTrace',
   'dev.perfetto.RestorePinnedTrack',
   'dev.perfetto.Sched',
   'dev.perfetto.Screenshots',
diff --git a/ui/src/core/feature_flags.ts b/ui/src/core/feature_flags.ts
index 4e745b9..4e60a61 100644
--- a/ui/src/core/feature_flags.ts
+++ b/ui/src/core/feature_flags.ts
@@ -16,20 +16,7 @@
 // ~everywhere and the are "statically" initialized (i.e. files construct Flags
 // at import time) if this file starts importing anything we will quickly run
 // into issues with initialization order which will be a pain.
-
-interface FlagSettings {
-  id: string;
-  defaultValue: boolean;
-  description: string;
-  name?: string;
-  devOnly?: boolean;
-}
-
-export enum OverrideState {
-  DEFAULT = 'DEFAULT',
-  TRUE = 'OVERRIDE_TRUE',
-  FALSE = 'OVERRIDE_FALSE',
-}
+import {Flag, FlagSettings, OverrideState} from '../public/feature_flag';
 
 export interface FlagStore {
   load(): object;
@@ -135,39 +122,6 @@
   }
 }
 
-export interface Flag {
-  // A unique identifier for this flag ("magicSorting")
-  readonly id: string;
-
-  // The name of the flag the user sees ("New track sorting algorithm")
-  readonly name: string;
-
-  // A longer description which is displayed to the user.
-  // "Sort tracks using an embedded tfLite model based on your expression
-  // while waiting for the trace to load."
-  readonly description: string;
-
-  // Whether the flag defaults to true or false.
-  // If !flag.isOverridden() then flag.get() === flag.defaultValue
-  readonly defaultValue: boolean;
-
-  // Get the current value of the flag.
-  get(): boolean;
-
-  // Override the flag and persist the new value.
-  set(value: boolean): void;
-
-  // If the flag has been overridden.
-  // Note: A flag can be overridden to its default value.
-  isOverridden(): boolean;
-
-  // Reset the flag to its default setting.
-  reset(): void;
-
-  // Get the current state of the flag.
-  overriddenState(): OverrideState;
-}
-
 class FlagImpl implements Flag {
   registry: Flags;
   state: OverrideState;
@@ -248,10 +202,3 @@
 
 export const FlagsForTesting = Flags;
 export const featureFlags = new Flags(new LocalStorageStore());
-
-export const RECORDING_V2_FLAG = featureFlags.register({
-  id: 'recordingv2',
-  name: 'Recording V2',
-  description: 'Record using V2 interface',
-  defaultValue: false,
-});
diff --git a/ui/src/core/plugin_manager.ts b/ui/src/core/plugin_manager.ts
index 55b5f4a..f444618 100644
--- a/ui/src/core/plugin_manager.ts
+++ b/ui/src/core/plugin_manager.ts
@@ -22,7 +22,8 @@
 } from '../public/plugin';
 import {Trace} from '../public/trace';
 import {defaultPlugins} from './default_plugins';
-import {featureFlags, Flag} from './feature_flags';
+import {featureFlags} from './feature_flags';
+import {Flag} from '../public/feature_flag';
 import {TraceImpl} from './trace_impl';
 
 // The pseudo plugin id used for the core instance of AppImpl.
diff --git a/ui/src/core/sidebar_manager.ts b/ui/src/core/sidebar_manager.ts
index 11c12dd..9de9b90 100644
--- a/ui/src/core/sidebar_manager.ts
+++ b/ui/src/core/sidebar_manager.ts
@@ -27,9 +27,9 @@
 
   readonly menuItems = new Registry<SidebarMenuItemInternal>((m) => m.id);
 
-  constructor(args: {sidebarEnabled: boolean}) {
-    this.enabled = args.sidebarEnabled;
-    this._visible = args.sidebarEnabled;
+  constructor(args: {disabled?: boolean; hidden?: boolean}) {
+    this.enabled = !args.disabled;
+    this._visible = !args.hidden;
   }
 
   addMenuItem(item: SidebarMenuItem): Disposable {
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index 01e0070..2ae2d16 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -46,6 +46,10 @@
 import {PageHandler, PageManager} from '../public/page';
 import {createProxy} from '../base/utils';
 import {PageManagerImpl} from './page_manager';
+import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
+import {featureFlags} from './feature_flags';
+import {SerializedAppState} from './state_serialization_schema';
+import {PostedTrace} from './trace_source';
 
 /**
  * Handles the per-trace state of the UI
@@ -407,6 +411,12 @@
     return this.appImpl.initialRouteArgs;
   }
 
+  get featureFlags(): FeatureFlagManager {
+    return {
+      register: (settings: FlagSettings) => featureFlags.register(settings),
+    };
+  }
+
   scheduleFullRedraw(): void {
     this.appImpl.scheduleFullRedraw();
   }
@@ -415,6 +425,18 @@
     this.appImpl.navigate(newHash);
   }
 
+  openTraceFromFile(file: File): void {
+    this.appImpl.openTraceFromFile(file);
+  }
+
+  openTraceFromUrl(url: string, serializedAppState?: SerializedAppState) {
+    this.appImpl.openTraceFromUrl(url, serializedAppState);
+  }
+
+  openTraceFromBuffer(args: PostedTrace): void {
+    this.appImpl.openTraceFromBuffer(args);
+  }
+
   addEventListener<T extends keyof EventListeners>(
     event: T,
     callback: EventListeners[T],
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index 732a5d6..19a0c70 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -21,7 +21,8 @@
 import {TopLevelScrollTrack} from './scroll_track';
 import {ScrollJankCauseMap} from './scroll_jank_cause_map';
 import {TrackNode} from '../../public/workspace';
-import {featureFlags, OverrideState} from '../../core/feature_flags';
+import {featureFlags} from '../../core/feature_flags';
+import {OverrideState} from '../../public/feature_flag';
 
 // Before plugins were a thing, this plugin was enabled using a feature flag.
 // However, nowadays, plugins themselves can be selectively enabled and
diff --git a/ui/src/core_plugins/flags_page/flags_page.ts b/ui/src/core_plugins/flags_page/flags_page.ts
index 4f275c9..d8cfdf6 100644
--- a/ui/src/core_plugins/flags_page/flags_page.ts
+++ b/ui/src/core_plugins/flags_page/flags_page.ts
@@ -14,7 +14,8 @@
 
 import m from 'mithril';
 import {channelChanged, getNextChannel, setChannel} from '../../core/channels';
-import {featureFlags, Flag, OverrideState} from '../../core/feature_flags';
+import {featureFlags} from '../../core/feature_flags';
+import {Flag, OverrideState} from '../../public/feature_flag';
 import {raf} from '../../core/raf_scheduler';
 import {PageAttrs} from '../../public/page';
 import {Router} from '../../core/router';
diff --git a/ui/src/frontend/clipboard.ts b/ui/src/frontend/clipboard.ts
deleted file mode 100644
index 8d0b06c..0000000
--- a/ui/src/frontend/clipboard.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2018 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 {copyToClipboard} from '../base/clipboard';
-import {AppImpl} from '../core/app_impl';
-
-export function onClickCopy(url: string) {
-  return (e: Event) => {
-    e.preventDefault();
-    copyToClipboard(url);
-    AppImpl.instance.omnibox.showStatusMessage(
-      'Link copied into the clipboard',
-    );
-  };
-}
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index dbe4a02..99d4157 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -14,9 +14,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 {RECORDING_V2_FLAG} from '../core/feature_flags';
+import {GcsUploader} from '../base/gcs_uploader';
 import {raf} from '../core/raf_scheduler';
 import {VERSION} from '../gen/perfetto_version';
 import {getCurrentModalKey, showModal} from '../widgets/modal';
@@ -47,22 +45,20 @@
     return;
   }
 
-  if (!RECORDING_V2_FLAG.get()) {
-    if (err.message.includes('Unable to claim interface')) {
-      showWebUSBError();
-      timeLastReport = now;
-      return;
-    }
+  if (err.message.includes('Unable to claim interface')) {
+    showWebUSBError();
+    timeLastReport = now;
+    return;
+  }
 
-    if (
-      err.message.includes('A transfer error has occurred') ||
-      err.message.includes('The device was disconnected') ||
-      err.message.includes('The transfer was cancelled')
-    ) {
-      showConnectionLostError();
-      timeLastReport = now;
-      return;
-    }
+  if (
+    err.message.includes('A transfer error has occurred') ||
+    err.message.includes('The device was disconnected') ||
+    err.message.includes('The transfer was cancelled')
+  ) {
+    showConnectionLostError();
+    timeLastReport = now;
+    return;
   }
 
   if (err.message.includes('(ERR:fmt)')) {
@@ -356,135 +352,6 @@
   });
 }
 
-export function showWebUSBErrorV2() {
-  showModal({
-    title: 'A WebUSB error occurred',
-    content: m(
-      'div',
-      m(
-        'span',
-        `Is adb already running on the host? Run this command and
-      try again.`,
-      ),
-      m('br'),
-      m('.modal-bash', '> adb kill-server'),
-      m('br'),
-      // The statement below covers the following edge case:
-      // 1. 'adb server' is running on the device.
-      // 2. The user selects the new Android target, so we try to fetch the
-      // OS version and do QSS.
-      // 3. The error modal is shown.
-      // 4. The user runs 'adb kill-server'.
-      // At this point we don't have a trigger to try fetching the OS version
-      // + QSS again. Therefore, the user will need to refresh the page.
-      m(
-        'span',
-        "If after running 'adb kill-server', you don't see " +
-          "a 'Start Recording' button on the page and you don't see " +
-          "'Allow USB debugging' on the device, " +
-          'you will need to reload this page.',
-      ),
-      m('br'),
-      m('br'),
-      m('span', 'For details see '),
-      m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
-    ),
-  });
-}
-
-export function showConnectionLostError(): void {
-  showModal({
-    title: 'Connection with the ADB device lost',
-    content: m(
-      'div',
-      m('span', `Please connect the device again to restart the recording.`),
-      m('br'),
-    ),
-  });
-}
-
-export function showAllowUSBDebugging(): void {
-  showModal({
-    title: 'Could not connect to the device',
-    content: m(
-      'div',
-      m('span', 'Please allow USB debugging on the device.'),
-      m('br'),
-    ),
-  });
-}
-
-export function showNoDeviceSelected(): void {
-  showModal({
-    title: 'No device was selected for recording',
-    content: m(
-      'div',
-      m(
-        'span',
-        `If you want to connect to an ADB device,
-           please select it from the list.`,
-      ),
-      m('br'),
-    ),
-  });
-}
-
-export function showExtensionNotInstalled(): void {
-  showModal({
-    title: 'Perfetto Chrome extension not installed',
-    content: m(
-      'div',
-      m(
-        '.note',
-        `To trace Chrome from the Perfetto UI, you need to install our `,
-        m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
-        ' and then reload this page.',
-      ),
-      m('br'),
-    ),
-  });
-}
-
-export function showWebsocketConnectionIssue(message: string): void {
-  showModal({
-    title: 'Unable to connect to the device via websocket',
-    content: m(
-      'div',
-      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
-      m('pre', message),
-    ),
-  });
-}
-
-export function showIssueParsingTheTracedResponse(message: string): void {
-  showModal({
-    title:
-      'A problem was encountered while connecting to' +
-      ' the Perfetto tracing service',
-    content: m('div', m('span', message), m('br')),
-  });
-}
-
-export function showFailedToPushBinary(message: string): void {
-  showModal({
-    title: 'Failed to push a binary to the device',
-    content: m(
-      'div',
-      m(
-        'span',
-        'This can happen if your Android device has an OS version lower ' +
-          'than Q. Perfetto tried to push the latest version of its ' +
-          'embedded binary but failed.',
-      ),
-      m('br'),
-      m('br'),
-      m('span', 'Error message:'),
-      m('br'),
-      m('span', message),
-    ),
-  });
-}
-
 function showRpcSequencingError() {
   showModal({
     title: 'A TraceProcessor RPC error occurred',
@@ -534,3 +401,25 @@
     ],
   });
 }
+
+function showWebsocketConnectionIssue(message: string): void {
+  showModal({
+    title: 'Unable to connect to the device via websocket',
+    content: m(
+      'div',
+      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+      m('pre', message),
+    ),
+  });
+}
+
+function showConnectionLostError(): void {
+  showModal({
+    title: 'Connection with the ADB device lost',
+    content: m(
+      'div',
+      m('span', `Please connect the device again to restart the recording.`),
+      m('br'),
+    ),
+  });
+}
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 198f08e..67aa000 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -20,7 +20,7 @@
 import m from 'mithril';
 import {defer} from '../base/deferred';
 import {addErrorHandler, reportError} from '../base/logging';
-import {RECORDING_V2_FLAG, featureFlags} from '../core/feature_flags';
+import {featureFlags} from '../core/feature_flags';
 import {initLiveReload} from '../core/live_reload';
 import {raf} from '../core/raf_scheduler';
 import {initWasm} from '../trace_processor/wasm_engine_proxy';
@@ -33,8 +33,6 @@
 import {globals} from './globals';
 import {HomePage} from './home_page';
 import {postMessageHandler} from './post_message_handler';
-import {RecordPage} from './record_page';
-import {RecordPageV2} from './record_page_v2';
 import {Route, Router} from '../core/router';
 import {CheckHttpRpcConnection} from './rpc_http_dialog';
 import {maybeOpenTraceFromRoute} from './trace_url_handler';
@@ -224,8 +222,6 @@
   const pages = AppImpl.instance.pages;
   const traceless = true;
   pages.registerPage({route: '/', traceless, page: HomePage});
-  const recordPage = RECORDING_V2_FLAG.get() ? RecordPageV2 : RecordPage;
-  pages.registerPage({route: '/record', traceless, page: recordPage});
   pages.registerPage({route: '/viewer', page: ViewerPage});
   const router = new Router();
   router.onRouteChanged = routeChange;
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index 5ab7059..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,
@@ -32,9 +32,7 @@
 import {z} from 'zod';
 import {showModal} from '../widgets/modal';
 import {AppImpl} from '../core/app_impl';
-import {Router} from '../core/router';
-import {onClickCopy} from './clipboard';
-import {RecordingManager} from '../controller/recording_manager';
+import {CopyableLink} from '../widgets/copyable_link';
 
 // Permalink serialization has two layers:
 // 1. Serialization of the app state (state_serialization.ts):
@@ -57,69 +55,54 @@
   // 1. parseAppState() does further semantic checks (e.g. version checking).
   // 2. We want to still load the traceUrl even if the app state is invalid.
   appState: z.any().optional(),
-
-  // This is for the very unusual case of clicking on "Share settings" in the
-  // recording page. In this case there is no trace or app state. We just
-  // create a permalink with the recording state.
-  recordingOpts: z.any().optional(),
 });
 
 type PermalinkState = z.infer<typeof PERMALINK_SCHEMA>;
 
-export interface PermalinkOptions {
-  mode: 'APP_STATE' | 'RECORDING_OPTS';
-}
-
-export async function createPermalink(opts: PermalinkOptions): Promise<void> {
-  const hash = await createPermalinkInternal(opts);
+export async function createPermalink(): Promise<void> {
+  const hash = await createPermalinkInternal();
   showPermalinkDialog(hash);
 }
 
 // Returns the file name, not the full url (i.e. the name of the GCS object).
-async function createPermalinkInternal(
-  opts: PermalinkOptions,
-): Promise<string> {
+async function createPermalinkInternal(): Promise<string> {
   const permalinkData: PermalinkState = {};
 
-  if (opts.mode === 'RECORDING_OPTS') {
-    permalinkData.recordingOpts = RecordingManager.instance.state.recordConfig;
-  } else if (opts.mode === 'APP_STATE') {
-    // Check if we need to upload the trace file, before serializing the app
-    // state.
-    let alreadyUploadedUrl = '';
-    const trace = assertExists(AppImpl.instance.trace);
-    const traceSource = trace.traceInfo.source;
-    let dataToUpload: File | ArrayBuffer | undefined = undefined;
-    let traceName = trace.traceInfo.traceTitle || 'trace';
-    if (traceSource.type === 'FILE') {
-      dataToUpload = traceSource.file;
-      traceName = dataToUpload.name;
-    } else if (traceSource.type === 'ARRAY_BUFFER') {
-      dataToUpload = traceSource.buffer;
-    } else if (traceSource.type === 'URL') {
-      alreadyUploadedUrl = traceSource.url;
-    } else {
-      throw new Error(`Cannot share trace ${JSON.stringify(traceSource)}`);
-    }
-
-    // Upload the trace file, unless it's already uploaded (type == 'URL').
-    // Internally TraceGcsUploader will skip the upload if an object with the
-    // same hash exists already.
-    if (alreadyUploadedUrl) {
-      permalinkData.traceUrl = alreadyUploadedUrl;
-    } else if (dataToUpload !== undefined) {
-      updateStatus(`Uploading ${traceName}`);
-      const uploader: GcsUploader = new GcsUploader(dataToUpload, {
-        mimeType: MIME_BINARY,
-        onProgress: () => reportUpdateProgress(uploader),
-      });
-      await uploader.waitForCompletion();
-      permalinkData.traceUrl = uploader.uploadedUrl;
-    }
-
-    permalinkData.appState = serializeAppState(trace);
+  // Check if we need to upload the trace file, before serializing the app
+  // state.
+  let alreadyUploadedUrl = '';
+  const trace = assertExists(AppImpl.instance.trace);
+  const traceSource = trace.traceInfo.source;
+  let dataToUpload: File | ArrayBuffer | undefined = undefined;
+  let traceName = trace.traceInfo.traceTitle || 'trace';
+  if (traceSource.type === 'FILE') {
+    dataToUpload = traceSource.file;
+    traceName = dataToUpload.name;
+  } else if (traceSource.type === 'ARRAY_BUFFER') {
+    dataToUpload = traceSource.buffer;
+  } else if (traceSource.type === 'URL') {
+    alreadyUploadedUrl = traceSource.url;
+  } else {
+    throw new Error(`Cannot share trace ${JSON.stringify(traceSource)}`);
   }
 
+  // Upload the trace file, unless it's already uploaded (type == 'URL').
+  // Internally TraceGcsUploader will skip the upload if an object with the
+  // same hash exists already.
+  if (alreadyUploadedUrl) {
+    permalinkData.traceUrl = alreadyUploadedUrl;
+  } else if (dataToUpload !== undefined) {
+    updateStatus(`Uploading ${traceName}`);
+    const uploader: GcsUploader = new GcsUploader(dataToUpload, {
+      mimeType: MIME_BINARY,
+      onProgress: () => reportUpdateProgress(uploader),
+    });
+    await uploader.waitForCompletion();
+    permalinkData.traceUrl = uploader.uploadedUrl;
+  }
+
+  permalinkData.appState = serializeAppState(trace);
+
   // Serialize the permalink with the app state (or recording state) and upload.
   updateStatus(`Creating permalink...`);
   const permalinkJson = JsonSerialize(permalinkData);
@@ -166,13 +149,6 @@
     }
   }
 
-  if (permalink.recordingOpts !== undefined) {
-    // This permalink state only contains a RecordConfig. Show the
-    // recording page with the config, but keep other state as-is.
-    RecordingManager.instance.setRecordConfig(permalink.recordingOpts);
-    Router.navigate('#!/record');
-    return;
-  }
   let serializedAppState: SerializedAppState | undefined = undefined;
   if (permalink.appState !== undefined) {
     // This is the most common case where the permalink contains the app state
@@ -266,15 +242,8 @@
 }
 
 function showPermalinkDialog(hash: string) {
-  const url = `${self.location.origin}/#!/?s=${hash}`;
-  const linkProps = {title: 'Click to copy the URL', onclick: onClickCopy(url)};
   showModal({
     title: 'Permalink',
-    content: m(
-      'div',
-      m(`a[href=${url}]`, linkProps, url),
-      m('br'),
-      m('i', 'Click on the URL to copy it into the clipboard'),
-    ),
+    content: m(CopyableLink, {url: `${self.location.origin}/#!/?s=${hash}`}),
   });
 }
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 9f71bcc..8ece2d3 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {getCurrentChannel} from '../core/channels';
-import {TRACE_SUFFIX} from '../common/constants';
+import {TRACE_SUFFIX} from '../public/trace';
 import {
   disableMetatracingAndGetTrace,
   enableMetatracing,
@@ -554,13 +554,6 @@
   // TODO(primiano): The Open file / Open with legacy entries are registered by
   // the 'perfetto.CoreCommands' plugins. Make things consistent.
   app.sidebar.addMenuItem({
-    section: 'navigation',
-    text: 'Record new trace',
-    href: '#!/record',
-    icon: 'fiber_smart_record',
-    sortOrder: 2,
-  });
-  app.sidebar.addMenuItem({
     section: 'support',
     text: 'Keyboard shortcuts',
     action: toggleHelp,
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index 40f2012..b803148 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -28,6 +28,12 @@
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {addEphemeralTab} from '../common/add_ephemeral_tab';
 import {Tab} from '../public/tab';
+import {addChartTab} from './widgets/charts/chart_tab';
+import {
+  ChartOption,
+  createChartConfigFromSqlTableState,
+} from './widgets/charts/chart';
+import {AddChartMenuItem} from './widgets/charts/add_chart_menu';
 
 export interface AddSqlTableTabParams {
   table: SqlTableDescription;
@@ -122,6 +128,16 @@
       },
       m(SqlTable, {
         state: this.state,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              this.state,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => addChartTab(chart),
+          }),
       }),
     );
   }
diff --git a/ui/src/frontend/trace_share_utils.ts b/ui/src/frontend/trace_share_utils.ts
index 418fe51..cf8f185 100644
--- a/ui/src/frontend/trace_share_utils.ts
+++ b/ui/src/frontend/trace_share_utils.ts
@@ -16,11 +16,11 @@
 import {TraceUrlSource} from '../core/trace_source';
 import {createPermalink} from './permalink';
 import {showModal} from '../widgets/modal';
-import {onClickCopy} from './clipboard';
 import {globals} from './globals';
 import {AppImpl} from '../core/app_impl';
 import {Trace} from '../public/trace';
 import {TraceImpl} from '../core/trace_impl';
+import {CopyableLink} from '../widgets/copyable_link';
 
 export function isShareable(trace: Trace) {
   return globals.isInternalUser && trace.traceInfo.downloadable;
@@ -43,7 +43,7 @@
     if (traceUrl) {
       msg.push(m('p', 'By using the URL below you can open this trace again.'));
       msg.push(m('p', 'Clicking will copy the URL into the clipboard.'));
-      msg.push(createTraceLink(traceUrl, traceUrl));
+      msg.push(m(CopyableLink, {url: traceUrl}));
     }
 
     showModal({
@@ -61,19 +61,6 @@
   );
   if (result) {
     AppImpl.instance.analytics.logEvent('Trace Actions', 'Create permalink');
-    return await createPermalink({mode: 'APP_STATE'});
+    return await createPermalink();
   }
 }
-
-export function createTraceLink(title: string, url: string) {
-  if (url === '') {
-    return m('a.trace-file-name', title);
-  }
-  const linkProps = {
-    href: url,
-    title: 'Click to copy the URL',
-    target: '_blank',
-    onclick: onClickCopy(url),
-  };
-  return m('a.trace-file-name', linkProps, title);
-}
diff --git a/ui/src/frontend/widgets/charts/add_chart_menu.ts b/ui/src/frontend/widgets/charts/add_chart_menu.ts
new file mode 100644
index 0000000..cb3bb17
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/add_chart_menu.ts
@@ -0,0 +1,53 @@
+// 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 m from 'mithril';
+import {MenuItem} from '../../../widgets/menu';
+import {Icons} from '../../../base/semantic_icons';
+import {Chart, ChartConfig, ChartOption, toTitleCase} from './chart';
+
+interface AddChartMenuItemAttrs {
+  readonly chartConfig: ChartConfig;
+  readonly chartOptions: Array<ChartOption>;
+  readonly addChart: (chart: Chart) => void;
+}
+
+export class AddChartMenuItem
+  implements m.ClassComponent<AddChartMenuItemAttrs>
+{
+  private renderAddChartOptions(
+    config: ChartConfig,
+    chartOptions: Array<ChartOption>,
+    addChart: (chart: Chart) => void,
+  ): m.Children {
+    return chartOptions.map((option) => {
+      return m(MenuItem, {
+        label: toTitleCase(option),
+        onclick: () => addChart({option, config}),
+      });
+    });
+  }
+
+  view({attrs}: m.Vnode<AddChartMenuItemAttrs>) {
+    return m(
+      MenuItem,
+      {label: 'Add chart', icon: Icons.Chart},
+      this.renderAddChartOptions(
+        attrs.chartConfig,
+        attrs.chartOptions,
+        attrs.addChart,
+      ),
+    );
+  }
+}
diff --git a/ui/src/frontend/widgets/charts/chart.ts b/ui/src/frontend/widgets/charts/chart.ts
index 1a78c41..124a52e 100644
--- a/ui/src/frontend/widgets/charts/chart.ts
+++ b/ui/src/frontend/widgets/charts/chart.ts
@@ -11,9 +11,13 @@
 // 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 m from 'mithril';
 import {Row} from '../../../trace_processor/query_result';
 import {Engine} from '../../../trace_processor/engine';
-import {TableColumn, TableColumnSet} from '../sql/table/column';
+import {Filter, TableColumn, TableColumnSet} from '../sql/table/column';
+import {Histogram} from './histogram/histogram';
+import {SqlTableState} from '../sql/table/state';
+import {columnTitle} from '../sql/table/table';
 
 export interface VegaLiteChartSpec {
   $schema: string;
@@ -41,6 +45,26 @@
   };
 }
 
+// Holds the various chart types and human readable string
+export enum ChartOption {
+  HISTOGRAM = 'histogram',
+}
+
+export interface ChartConfig {
+  readonly engine: Engine;
+  readonly columnTitle: string; // Human readable column name (ex: Duration)
+  readonly sqlColumn: string[]; // SQL column name (ex: dur)
+  readonly filters?: Filter[]; // Filters applied to SQL table
+  readonly tableDisplay?: string; // Human readable table name (ex: slices)
+  readonly query: string; // SQL query for the underlying data
+  readonly aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
+}
+
+export interface Chart {
+  readonly option: ChartOption;
+  readonly config: ChartConfig;
+}
+
 export interface ChartData {
   readonly rows: Row[];
   readonly error?: string;
@@ -65,3 +89,32 @@
 
   return words.join(' ');
 }
+
+// renderChartComponent will take a chart option and config and map
+// to the corresponding chart class component.
+export function renderChartComponent(chart: Chart) {
+  switch (chart.option) {
+    case ChartOption.HISTOGRAM:
+      return m(Histogram, chart.config);
+    default:
+      return;
+  }
+}
+
+export function createChartConfigFromSqlTableState(
+  column: TableColumn,
+  columnAlias: string,
+  sqlTableState: SqlTableState,
+) {
+  return {
+    engine: sqlTableState.trace.engine,
+    columnTitle: columnTitle(column),
+    sqlColumn: [columnAlias],
+    filters: sqlTableState?.getFilters(),
+    tableDisplay: sqlTableState.config.displayName ?? sqlTableState.config.name,
+    query: sqlTableState.getSqlQuery(
+      Object.fromEntries([[columnAlias, column.primaryColumn()]]),
+    ),
+    aggregationType: column.aggregation?.().dataType,
+  };
+}
diff --git a/ui/src/frontend/widgets/charts/chart_tab.ts b/ui/src/frontend/widgets/charts/chart_tab.ts
new file mode 100644
index 0000000..6d802e6
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/chart_tab.ts
@@ -0,0 +1,54 @@
+// 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 m from 'mithril';
+import {DetailsShell} from '../../../widgets/details_shell';
+import {filterTitle} from '../sql/table/column';
+import {addEphemeralTab} from '../../../common/add_ephemeral_tab';
+import {Tab} from '../../../public/tab';
+import {Chart, renderChartComponent, toTitleCase} from './chart';
+
+export function addChartTab(chart: Chart): void {
+  addEphemeralTab('histogramTab', new ChartTab(chart));
+}
+
+export class ChartTab implements Tab {
+  constructor(private readonly chart: Chart) {}
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: this.getTitle(),
+        description: this.getDescription(),
+      },
+      renderChartComponent(this.chart),
+    );
+  }
+
+  getTitle(): string {
+    return `${toTitleCase(this.chart.config.columnTitle)} Histogram`;
+  }
+
+  private getDescription(): string {
+    let desc = `Count distribution for ${this.chart.config.tableDisplay ?? ''} table`;
+
+    if (this.chart.config.filters && this.chart.config.filters.length > 0) {
+      desc += ' where ';
+      desc += this.chart.config.filters.map((f) => filterTitle(f)).join(', ');
+    }
+
+    return desc;
+  }
+}
diff --git a/ui/src/frontend/widgets/charts/histogram/histogram.ts b/ui/src/frontend/widgets/charts/histogram/histogram.ts
index e4dd0d0..38f4a65 100644
--- a/ui/src/frontend/widgets/charts/histogram/histogram.ts
+++ b/ui/src/frontend/widgets/charts/histogram/histogram.ts
@@ -15,25 +15,14 @@
 import m from 'mithril';
 import {stringifyJsonWithBigints} from '../../../../base/json_utils';
 import {VegaView} from '../../../../widgets/vega_view';
-import {Filter} from '../../../widgets/sql/table/column';
 import {HistogramState} from './state';
 import {Spinner} from '../../../../widgets/spinner';
-import {Engine} from '../../../../trace_processor/engine';
+import {ChartConfig} from '../chart';
 
-export interface HistogramConfig {
-  engine: Engine;
-  columnTitle: string; // Human readable column name (ex: Duration)
-  sqlColumn: string[]; // SQL column name (ex: dur)
-  filters?: Filter[]; // Filters applied to SQL table
-  tableDisplay?: string; // Human readable table name (ex: slices)
-  query: string; // SQL query for the underlying data
-  aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
-}
-
-export class Histogram implements m.ClassComponent<HistogramConfig> {
+export class Histogram implements m.ClassComponent<ChartConfig> {
   private readonly state: HistogramState;
 
-  constructor({attrs}: m.Vnode<HistogramConfig>) {
+  constructor({attrs}: m.Vnode<ChartConfig>) {
     this.state = new HistogramState(
       attrs.engine,
       attrs.query,
diff --git a/ui/src/frontend/widgets/charts/histogram/tab.ts b/ui/src/frontend/widgets/charts/histogram/tab.ts
deleted file mode 100644
index 096245e..0000000
--- a/ui/src/frontend/widgets/charts/histogram/tab.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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 m from 'mithril';
-import {DetailsShell} from '../../../../widgets/details_shell';
-import {filterTitle} from '../../../widgets/sql/table/column';
-import {addEphemeralTab} from '../../../../common/add_ephemeral_tab';
-import {Tab} from '../../../../public/tab';
-import {Histogram, HistogramConfig} from './histogram';
-import {toTitleCase} from '../chart';
-
-export function addHistogramTab(config: HistogramConfig): void {
-  addEphemeralTab('histogramTab', new HistogramTab(config));
-}
-
-export class HistogramTab implements Tab {
-  constructor(private readonly config: HistogramConfig) {}
-
-  render() {
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-        description: this.getDescription(),
-      },
-      m(Histogram, this.config),
-    );
-  }
-
-  getTitle(): string {
-    return `${toTitleCase(this.config.columnTitle)} Histogram`;
-  }
-
-  private getDescription(): string {
-    let desc = `Count distribution for ${this.config.tableDisplay ?? ''} table`;
-
-    if (this.config.filters && this.config.filters.length > 0) {
-      desc += ' where ';
-      desc += this.config.filters.map((f) => filterTitle(f)).join(', ');
-    }
-
-    return desc;
-  }
-}
diff --git a/ui/src/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index acd5ffe..761a32c 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -40,14 +40,20 @@
 import {SqlTableState} from './state';
 import {SqlTableDescription} from './table_description';
 import {Intent} from '../../../../widgets/common';
-import {addHistogramTab} from '../../charts/histogram/tab';
 import {Form} from '../../../../widgets/form';
 import {TextInput} from '../../../../widgets/text_input';
 
 export interface SqlTableConfig {
   readonly state: SqlTableState;
+  // For additional menu items to add to the column header menus
+  readonly addColumnMenuItems?: (
+    column: TableColumn,
+    columnAlias: string,
+  ) => m.Children;
 }
 
+type AdditionalColumnMenuItems = Record<string, m.Children>;
+
 function renderCell(
   column: TableColumn,
   row: Row,
@@ -65,7 +71,7 @@
   return column.renderCell(sqlValue, getTableManager(state), additionalValues);
 }
 
-function columnTitle(column: TableColumn): string {
+export function columnTitle(column: TableColumn): string {
   if (column.getTitle !== undefined) {
     const title = column.getTitle();
     if (title !== undefined) return title;
@@ -274,7 +280,11 @@
     );
   }
 
-  renderColumnHeader(column: TableColumn, index: number) {
+  renderColumnHeader(
+    column: TableColumn,
+    index: number,
+    additionalColumnHeaderMenuItems?: m.Children,
+  ) {
     const sorted = this.state.isSortedBy(column);
     const icon =
       sorted === 'ASC'
@@ -327,27 +337,7 @@
         {label: 'Add filter', icon: Icons.Filter},
         this.renderColumnFilterOptions(column),
       ),
-      m(MenuItem, {
-        label: 'Create histogram',
-        icon: Icons.Chart,
-        onclick: () => {
-          const columnAlias =
-            this.state.getCurrentRequest().columns[
-              sqlColumnId(column.primaryColumn())
-            ];
-          addHistogramTab({
-            engine: this.state.trace.engine,
-            sqlColumn: [columnAlias],
-            columnTitle: columnTitle(column),
-            filters: this.state.getFilters(),
-            tableDisplay: this.table.displayName ?? this.table.name,
-            query: this.state.getSqlQuery(
-              Object.fromEntries([[columnAlias, column.primaryColumn()]]),
-            ),
-            aggregationType: column.aggregation?.().dataType,
-          });
-        },
-      }),
+      additionalColumnHeaderMenuItems,
       // Menu items before divider apply to selected column
       m(MenuDivider),
       // Menu items after divider apply to entire table
@@ -355,13 +345,49 @@
     );
   }
 
-  view() {
+  getAdditionalColumnMenuItems(
+    addColumnMenuItems?: (
+      column: TableColumn,
+      columnAlias: string,
+    ) => m.Children,
+  ) {
+    if (addColumnMenuItems === undefined) return;
+
+    const additionalColumnMenuItems: AdditionalColumnMenuItems = {};
+    this.state.getSelectedColumns().forEach((column) => {
+      const columnAlias =
+        this.state.getCurrentRequest().columns[
+          sqlColumnId(column.primaryColumn())
+        ];
+
+      additionalColumnMenuItems[columnAlias] = addColumnMenuItems(
+        column,
+        columnAlias,
+      );
+    });
+
+    return additionalColumnMenuItems;
+  }
+
+  view({attrs}: m.Vnode<SqlTableConfig>) {
     const rows = this.state.getDisplayedRows();
+    const additionalColumnMenuItems = this.getAdditionalColumnMenuItems(
+      attrs.addColumnMenuItems,
+    );
 
     const columns = this.state.getSelectedColumns();
     const columnDescriptors = columns.map((column, i) => {
       return {
-        title: this.renderColumnHeader(column, i),
+        title: this.renderColumnHeader(
+          column,
+          i,
+          additionalColumnMenuItems &&
+            additionalColumnMenuItems[
+              this.state.getCurrentRequest().columns[
+                sqlColumnId(column.primaryColumn())
+              ]
+            ],
+        ),
         render: (row: Row) => renderCell(column, row, this.state),
       };
     });
diff --git a/ui/src/frontend/widgets/thread_state.ts b/ui/src/frontend/widgets/thread_state.ts
index d6c0f7b..b7cfd69 100644
--- a/ui/src/frontend/widgets/thread_state.ts
+++ b/ui/src/frontend/widgets/thread_state.ts
@@ -58,6 +58,6 @@
   if (state.thread === undefined) return null;
 
   return m(ThreadStateRef, {
-    id: state.threadStateSqlId,
+    id: state.id,
   });
 }
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/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
index 5b1d0d4..1c60d4f 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
@@ -33,8 +33,15 @@
 import {Button} from '../../widgets/button';
 import {Icons} from '../../base/semantic_icons';
 import {DetailsShell} from '../../widgets/details_shell';
+import {
+  Chart,
+  ChartOption,
+  createChartConfigFromSqlTableState,
+  renderChartComponent,
+} from '../../frontend/widgets/charts/chart';
+import {AddChartMenuItem} from '../../frontend/widgets/charts/add_chart_menu';
 
-interface ExplorePageState {
+interface ExploreTableState {
   sqlTableState?: SqlTableState;
   selectedTable?: ExplorableTable;
 }
@@ -46,13 +53,12 @@
 }
 
 export class ExplorePage implements m.ClassComponent<PageWithTraceAttrs> {
-  private readonly state: ExplorePageState;
+  private readonly state: ExploreTableState;
+  private readonly charts: Chart[];
 
   constructor() {
-    this.state = {
-      sqlTableState: undefined,
-      selectedTable: undefined,
-    };
+    this.charts = [];
+    this.state = {};
   }
 
   // Show menu with standard library tables
@@ -115,7 +121,7 @@
 
           this.state.selectedTable = table;
 
-          const sqlTableState = new SqlTableState(
+          this.state.sqlTableState = new SqlTableState(
             trace,
             {
               name: table.name,
@@ -123,7 +129,6 @@
             },
             {imports: [table.module]},
           );
-          this.state.sqlTableState = sqlTableState;
         },
       });
     });
@@ -162,6 +167,16 @@
       },
       m(SqlTable, {
         state: sqlTableState,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              sqlTableState,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => this.charts.push(chart),
+          }),
       }),
     );
   }
@@ -170,6 +185,7 @@
     return m(
       '.explore-page',
       m(Menu, this.renderSelectableTablesMenuItems(attrs.trace)),
+      this.charts.map((chart) => renderChartComponent(chart)),
       this.state.selectedTable && this.renderSqlTable(),
     );
   }
diff --git a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
index 03be4f2..d75dd77 100644
--- a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
@@ -20,6 +20,7 @@
 import {STR_NULL} from '../../trace_processor/query_result';
 import {Slice} from '../../public/track';
 import {Trace} from '../../public/trace';
+import {TrackEventDetails} from '../../public/selection';
 
 // color named and defined based on Material Design color palettes
 // 500 colors indicate a timeline slice is not a partial jank (not a jank or
@@ -90,6 +91,17 @@
       colorScheme: getColorSchemeForJank(row.jankTag, row.jankSeverityType),
     };
   }
+
+  override async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (!baseDetails) return undefined;
+    return {
+      ...baseDetails,
+      tableName: 'slice',
+    };
+  }
 }
 
 function getColorSchemeForJank(
diff --git a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
index bd41be4..ff04311 100644
--- a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
@@ -22,6 +22,7 @@
 import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
 import {Slice} from '../../public/track';
 import {Trace} from '../../public/trace';
+import {TrackEventDetails} from '../../public/selection';
 
 const GREEN = makeColorScheme(new HSLColor('#4CAF50')); // Green 500
 
@@ -61,4 +62,15 @@
   getRowSpec(): NamedRow {
     return NAMED_ROW;
   }
+
+  override async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const baseDetails = await super.getSelectionDetails(id);
+    if (!baseDetails) return undefined;
+    return {
+      ...baseDetails,
+      tableName: 'slice',
+    };
+  }
 }
diff --git a/ui/src/controller/adb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
similarity index 98%
rename from ui/src/controller/adb.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
index e188ea7..5197d23 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../base/logging';
-import {isString} from '../base/object_utils';
-import {utf8Decode, utf8Encode} from '../base/string_utils';
+import {assertExists} from '../../base/logging';
+import {isString} from '../../base/object_utils';
+import {utf8Decode, utf8Encode} from '../../base/string_utils';
 import {Adb, AdbMsg, AdbStream, CmdType} from './adb_interfaces';
 
 export const VERSION_WITH_CHECKSUM = 0x01000000;
diff --git a/ui/src/controller/adb_base_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
similarity index 96%
rename from ui/src/controller/adb_base_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
index 2a72a33..c447df5 100644
--- a/ui/src/controller/adb_base_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {exists} from '../base/utils';
-import {RecordingState, RecordingTarget, isAdbTarget} from '../common/state';
+import {exists} from '../../base/utils';
+import {RecordingState, RecordingTarget, isAdbTarget} from './state';
 import {
   extractDurationFromTraceConfig,
   extractTraceConfig,
-} from '../core/trace_config_utils';
+} from './trace_config_utils';
 import {Adb} from './adb_interfaces';
 import {ReadBuffersResponse} from './consumer_port_types';
 import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
diff --git a/ui/src/controller/adb_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_interfaces.ts
similarity index 100%
rename from ui/src/controller/adb_interfaces.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_interfaces.ts
diff --git a/ui/src/controller/adb_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
similarity index 97%
rename from ui/src/controller/adb_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
index 9f51a97..1d228a5 100644
--- a/ui/src/controller/adb_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
@@ -19,7 +19,7 @@
   DEFAULT_MAX_PAYLOAD_BYTES,
   VERSION_WITH_CHECKSUM,
 } from './adb';
-import {utf8Encode} from '../base/string_utils';
+import {utf8Encode} from '../../base/string_utils';
 
 test('startAuthentication', async () => {
   const adb = new AdbOverWebUsb();
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
similarity index 95%
rename from ui/src/controller/adb_record_controller_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
index e404397..6078a59 100644
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
@@ -13,12 +13,12 @@
 // limitations under the License.
 
 import {dingus} from 'dingusjs';
-import {utf8Encode} from '../base/string_utils';
-import {EnableTracingRequest, TraceConfig} from '../protos';
+import {utf8Encode} from '../../base/string_utils';
+import {EnableTracingRequest, TraceConfig} from '../../protos';
 import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
 import {AdbConsumerPort} from './adb_shell_controller';
 import {Consumer} from './record_controller_interfaces';
-import {createEmptyState} from '../common/empty_state';
+import {createEmptyState} from './empty_state';
 
 function generateMockConsumer(): Consumer {
   return {
diff --git a/ui/src/controller/adb_shell_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
similarity index 96%
rename from ui/src/controller/adb_shell_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
index 5d1d156..623dc5d 100644
--- a/ui/src/controller/adb_shell_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {base64Encode, utf8Decode} from '../base/string_utils';
-import {RecordingState} from '../common/state';
-import {extractTraceConfig} from '../core/trace_config_utils';
+import {base64Encode, utf8Decode} from '../../base/string_utils';
+import {RecordingState} from './state';
+import {extractTraceConfig} from './trace_config_utils';
 import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
 import {Adb, AdbStream} from './adb_interfaces';
 import {ReadBuffersResponse} from './consumer_port_types';
diff --git a/ui/src/controller/adb_socket_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
similarity index 98%
rename from ui/src/controller/adb_socket_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
index 715be0d..a676747 100644
--- a/ui/src/controller/adb_socket_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
@@ -20,14 +20,14 @@
   GetTraceStatsResponse,
   IPCFrame,
   ReadBuffersResponse,
-} from '../protos';
+} from '../../protos';
 import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
 import {Adb, AdbStream} from './adb_interfaces';
 import {isReadBuffersResponse} from './consumer_port_types';
 import {Consumer} from './record_controller_interfaces';
-import {exists} from '../base/utils';
-import {assertTrue} from '../base/logging';
-import {RecordingState} from '../common/state';
+import {exists} from '../../base/utils';
+import {assertTrue} from '../../base/logging';
+import {RecordingState} from './state';
 
 enum SocketState {
   DISCONNECTED,
diff --git a/ui/src/frontend/recording/advanced_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/advanced_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
index d762338..35e6fe2 100644
--- a/ui/src/frontend/recording/advanced_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 const FTRACE_CATEGORIES = new Map<string, string>();
diff --git a/ui/src/frontend/recording/android_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
similarity index 98%
rename from ui/src/frontend/recording/android_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
index 4154fa7..7c0d741 100644
--- a/ui/src/frontend/recording/android_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
@@ -14,9 +14,9 @@
 
 import m from 'mithril';
 import {AtomId, DataSourceDescriptor} from '../../protos';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
-import {RecordConfig} from '../../controller/record_config_types';
+import {RecordConfig} from './record_config_types';
 
 const PUSH_ATOM_IDS = new Map<string, string>();
 const PULL_ATOM_IDS = new Map<string, string>();
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
similarity index 96%
rename from ui/src/controller/chrome_proxy_record_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
index d1e1b63..ef0b999 100644
--- a/ui/src/controller/chrome_proxy_record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {binaryDecode, binaryEncode} from '../base/string_utils';
-import {TRACE_SUFFIX} from '../common/constants';
+import {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
 import {
   ConsumerPortResponse,
   hasProperty,
diff --git a/ui/src/frontend/recording/chrome_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/chrome_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
index 8ac14cd..fd09d82 100644
--- a/ui/src/frontend/recording/chrome_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
@@ -13,19 +13,19 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
+import {DataSource} from './recordingV2/recording_interfaces_v2';
 import {
   RecordingState,
   getBuiltinChromeCategoryList,
   isChromeTarget,
-} from '../../common/state';
+} from './state';
 import {
   MultiSelect,
   MultiSelectDiff,
   Option as MultiSelectOption,
 } from '../../widgets/multiselect';
 import {Section} from '../../widgets/section';
-import {CategoryGetter, CompactProbe, Toggle} from '../record_widgets';
+import {CategoryGetter, CompactProbe, Toggle} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 function extractChromeCategories(
diff --git a/ui/src/controller/consumer_port_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
similarity index 98%
rename from ui/src/controller/consumer_port_types.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
index 973205f..732e9e8 100644
--- a/ui/src/controller/consumer_port_types.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
@@ -18,7 +18,7 @@
   IFreeBuffersResponse,
   IGetTraceStatsResponse,
   IReadBuffersResponse,
-} from '../protos';
+} from '../../protos';
 
 export interface Typed {
   type: string;
diff --git a/ui/src/frontend/recording/cpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/cpu_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
index ba12267..06b2713 100644
--- a/ui/src/frontend/recording/cpu_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe, Slider} from '../record_widgets';
+import {Probe, Slider} from './record_widgets';
 import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
 
 export class CpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/common/empty_state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
similarity index 92%
rename from ui/src/common/empty_state.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
index c356b00..bfafe3f 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
@@ -12,10 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  autosaveConfigStore,
-  recordTargetStore,
-} from '../frontend/record_config';
+import {autosaveConfigStore, recordTargetStore} from './record_config';
 import {RecordingState} from './state';
 
 export function createEmptyState(): RecordingState {
diff --git a/ui/src/frontend/recording/etw_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/etw_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
index 70c1ab1..eefb8ac 100644
--- a/ui/src/frontend/recording/etw_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe} from '../record_widgets';
+import {Probe} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 export class EtwSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/frontend/recording/gpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/gpu_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
index 745af99..1040f75 100644
--- a/ui/src/frontend/recording/gpu_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe} from '../record_widgets';
+import {Probe} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 export class GpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
new file mode 100644
index 0000000..e0c5a1f
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/index.ts
@@ -0,0 +1,56 @@
+// 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 m from 'mithril';
+import {RecordPage} from './record_page';
+import {RecordPageV2} from './record_page_v2';
+import {App} from '../../public/app';
+import {PerfettoPlugin} from '../../public/plugin';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
+import {RecordingManager} from './recording_manager';
+import {PageAttrs} from '../../public/page';
+import {bindMithrilAttrs} from '../../base/mithril_utils';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.RecordTrace';
+
+  static onActivate(app: App) {
+    app.sidebar.addMenuItem({
+      section: 'navigation',
+      text: 'Record new trace',
+      href: '#!/record',
+      icon: 'fiber_smart_record',
+      sortOrder: 2,
+    });
+
+    const RECORDING_V2_FLAG = app.featureFlags.register({
+      id: 'recordingv2',
+      name: 'Recording V2',
+      description: 'Record using V2 interface',
+      defaultValue: false,
+    });
+    const useRecordingV2 = RECORDING_V2_FLAG.get();
+
+    const recMgr = new RecordingManager(app, useRecordingV2);
+    let page: m.ClassComponent<PageAttrs>;
+    if (useRecordingV2) {
+      const recCtl = new RecordingPageController(app, recMgr);
+      recCtl.initFactories();
+      page = bindMithrilAttrs(RecordPageV2, {app, recCtl, recMgr});
+    } else {
+      page = bindMithrilAttrs(RecordPage, {app, recMgr});
+    }
+    app.pages.registerPage({route: '/record', traceless: true, page});
+  }
+}
diff --git a/ui/src/frontend/recording/linux_perf_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/linux_perf_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
index 7f9c6d4..a0fcf9f 100644
--- a/ui/src/frontend/recording/linux_perf_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe, Slider, Textarea} from '../record_widgets';
+import {Probe, Slider, Textarea} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 const PLACEHOLDER_TEXT = `Filters for processes to profile, one per line e.g.:
diff --git a/ui/src/frontend/recording/memory_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
similarity index 98%
rename from ui/src/frontend/recording/memory_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
index ccfa9ef..231306f 100644
--- a/ui/src/frontend/recording/memory_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {MeminfoCounters, VmstatCounters} from '../../protos';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
 import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
 
 class HeapSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/frontend/recording/power_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
similarity index 93%
rename from ui/src/frontend/recording/power_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
index be2f6ed..bf88217 100644
--- a/ui/src/frontend/recording/power_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {globals} from '../globals';
-import {Probe, Slider} from '../record_widgets';
+import {globals} from '../../frontend/globals';
+import {Probe, Slider} from './record_widgets';
 import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
 
 export class PowerSettings implements m.ClassComponent<RecordingSectionAttrs> {
@@ -34,6 +34,7 @@
         m('span', ')'),
       ),
     ];
+    // TODO(primiano): figure out a better story for isInternalUser.
     if (globals.isInternalUser) {
       descr.push(
         m(
diff --git a/ui/src/frontend/record_config.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
similarity index 97%
rename from ui/src/frontend/record_config.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
index f3bf3c5..ae41d9c 100644
--- a/ui/src/frontend/record_config.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {exists} from '../base/utils';
-import {getDefaultRecordingTargets, RecordingTarget} from '../common/state';
+import {exists} from '../../base/utils';
+import {getDefaultRecordingTargets, RecordingTarget} from './state';
 import {
   createEmptyRecordConfig,
   NamedRecordConfig,
   NAMED_RECORD_CONFIG_SCHEMA,
   RecordConfig,
   RECORD_CONFIG_SCHEMA,
-} from '../controller/record_config_types';
+} from './record_config_types';
 
 const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
 const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
similarity index 100%
rename from ui/src/controller/record_config_types.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
diff --git a/ui/src/controller/record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
similarity index 94%
rename from ui/src/controller/record_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
index 844a7a4..8062650 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
@@ -13,19 +13,19 @@
 // limitations under the License.
 
 import {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
-import {isString} from '../base/object_utils';
-import {base64Encode} from '../base/string_utils';
-import {TRACE_SUFFIX} from '../common/constants';
-import {genTraceConfig} from '../common/recordingV2/recording_config_utils';
-import {TargetInfo} from '../common/recordingV2/recording_interfaces_v2';
+import {isString} from '../../base/object_utils';
+import {base64Encode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
+import {genTraceConfig} from './recordingV2/recording_config_utils';
+import {TargetInfo} from './recordingV2/recording_interfaces_v2';
 import {
   AdbRecordingTarget,
   isAdbTarget,
   isChromeTarget,
   isWindowsTarget,
   RecordingTarget,
-} from '../common/state';
-import {ConsumerPort, TraceConfig} from '../protos';
+} from './state';
+import {ConsumerPort, TraceConfig} from '../../protos';
 import {AdbOverWebUsb} from './adb';
 import {AdbConsumerPort} from './adb_shell_controller';
 import {AdbSocketConsumerPort} from './adb_socket_controller';
@@ -41,9 +41,9 @@
 } from './consumer_port_types';
 import {RecordConfig} from './record_config_types';
 import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
-import {AppImpl} from '../core/app_impl';
 import {RecordingManager} from './recording_manager';
-import {raf} from '../core/raf_scheduler';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
 
 type RPCImplMethod = Method | rpc.ServiceMethod<Message<{}>, Message<{}>>;
 
@@ -189,6 +189,7 @@
 }
 
 export class RecordController implements Consumer {
+  private app: App;
   private recMgr: RecordingManager;
   private config: RecordConfig | null = null;
   private readonly extensionPort: MessagePort;
@@ -206,7 +207,8 @@
   // char, it is the 'targetOS'
   private controllerPromises = new Map<string, Promise<RpcConsumerPort>>();
 
-  constructor(recMgr: RecordingManager, extensionPort: MessagePort) {
+  constructor(app: App, recMgr: RecordingManager, extensionPort: MessagePort) {
+    this.app = app;
     this.recMgr = recMgr;
     this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
     this.extensionPort = extensionPort;
@@ -219,7 +221,7 @@
   refreshOnStateChange() {
     // TODO(eseckler): Use ConsumerPort's QueryServiceState instead
     // of posting a custom extension message to retrieve the category list.
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
     if (this.state.fetchChromeCategories && !this.fetchedCategories) {
       this.fetchedCategories = true;
       if (this.state.extensionInstalled) {
@@ -227,12 +229,7 @@
       }
       this.recMgr.setFetchChromeCategories(false);
     }
-    if (
-      this.state.recordConfig === this.config &&
-      this.state.recordingInProgress === this.recordingInProgress
-    ) {
-      return;
-    }
+
     this.config = this.state.recordConfig;
 
     const configProto = genConfigProto(this.config, this.state.recordingTarget);
@@ -325,7 +322,7 @@
       return;
     }
     const trace = this.generateTrace();
-    AppImpl.instance.openTraceFromBuffer({
+    this.app.openTraceFromBuffer({
       title: 'Recorded trace',
       buffer: trace.buffer,
       fileName: `recorded_trace${this.recordedTraceSuffix}`,
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
similarity index 97%
rename from ui/src/controller/record_controller_interfaces.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
index e9662fd..f29940a 100644
--- a/ui/src/controller/record_controller_interfaces.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TRACE_SUFFIX} from '../common/constants';
+import {TRACE_SUFFIX} from '../../public/trace';
 import {ConsumerPortResponse} from './consumer_port_types';
 
 export type ErrorCallback = (_: string) => void;
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
similarity index 99%
rename from ui/src/controller/record_controller_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
index 442e4b8..1035369 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../base/logging';
-import {TraceConfig} from '../protos';
+import {assertExists} from '../../base/logging';
+import {TraceConfig} from '../../protos';
 import {createEmptyRecordConfig} from './record_config_types';
 import {genConfigProto, toPbtxt} from './record_controller';
 
diff --git a/ui/src/frontend/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
similarity index 84%
rename from ui/src/frontend/record_page.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
index bb161b0..021a5db 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
@@ -26,38 +26,32 @@
   LoadedConfig,
   MAX_TIME,
   RecordingTarget,
-} from '../common/state';
-import {AdbOverWebUsb} from '../controller/adb';
-import {RecordConfig} from '../controller/record_config_types';
-import {featureFlags} from '../core/feature_flags';
-import {raf} from '../core/raf_scheduler';
-import {PageAttrs} from '../public/page';
+} from './state';
+import {AdbOverWebUsb} from './adb';
+import {RECORD_CONFIG_SCHEMA, RecordConfig} from './record_config_types';
+import {PageAttrs} from '../../public/page';
 import {
   autosaveConfigStore,
   recordConfigStore,
   recordTargetStore,
 } from './record_config';
 import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './recording/advanced_settings';
-import {AndroidSettings} from './recording/android_settings';
-import {ChromeSettings} from './recording/chrome_settings';
-import {CpuSettings} from './recording/cpu_settings';
-import {GpuSettings} from './recording/gpu_settings';
-import {LinuxPerfSettings} from './recording/linux_perf_settings';
-import {MemorySettings} from './recording/memory_settings';
-import {PowerSettings} from './recording/power_settings';
-import {RecordingSettings} from './recording/recording_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {createPermalink} from './permalink';
-import {AppImpl} from '../core/app_impl';
-import {RecordingManager} from '../controller/recording_manager';
-
-export const PERSIST_CONFIG_FLAG = featureFlags.register({
-  id: 'persistConfigsUI',
-  name: 'Config persistence UI',
-  description: 'Show experimental config persistence UI on the record page.',
-  defaultValue: true,
-});
+import {AdvancedSettings} from './advanced_settings';
+import {AndroidSettings} from './android_settings';
+import {ChromeSettings} from './chrome_settings';
+import {CpuSettings} from './cpu_settings';
+import {GpuSettings} from './gpu_settings';
+import {LinuxPerfSettings} from './linux_perf_settings';
+import {MemorySettings} from './memory_settings';
+import {PowerSettings} from './power_settings';
+import {RecordingSettings} from './recording_settings';
+import {EtwSettings} from './etw_settings';
+import {RecordingManager} from './recording_manager';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
+import {GcsUploader, BUCKET_NAME, MIME_JSON} from '../../base/gcs_uploader';
+import {showModal} from '../../widgets/modal';
+import {CopyableLink} from '../../widgets/copyable_link';
 
 export const RECORDING_SECTIONS = [
   'buffers',
@@ -158,22 +152,20 @@
 
   recMgr.setRecordingTarget(recordingTarget);
   recordTargetStore.save(target);
-  raf.scheduleFullRedraw();
+  scheduleFullRedraw();
 }
 
 function Instructions(recMgr: RecordingManager, cssClass: string) {
   return m(
     `.record-section.instructions${cssClass}`,
     m('header', 'Recording command'),
-    PERSIST_CONFIG_FLAG.get()
-      ? m(
-          'button.permalinkconfig',
-          {
-            onclick: () => createPermalink({mode: 'RECORDING_OPTS'}),
-          },
-          'Share recording settings',
-        )
-      : null,
+    m(
+      'button.permalinkconfig',
+      {
+        onclick: () => uploadRecordingConfig(recMgr.state.recordConfig),
+      },
+      'Share recording settings',
+    ),
     RecordingSnippet(recMgr),
     BufferUsageProgressBar(recMgr),
     m('.buttons', StopCancelButtons(recMgr)),
@@ -203,7 +195,7 @@
       disabled: loadedConfigEqual(configType, recMgr.state.lastLoadedConfig),
       onclick: () => {
         recMgr.setRecordConfig(config, configType);
-        raf.scheduleFullRedraw();
+        scheduleFullRedraw();
       },
     },
     m('i.material-icons', 'file_upload'),
@@ -249,7 +241,7 @@
                   type: 'NAMED',
                   name: item.title,
                 });
-                raf.scheduleFullRedraw();
+                scheduleFullRedraw();
               }
             },
           },
@@ -262,7 +254,7 @@
             title: 'Remove configuration',
             onclick: () => {
               recordConfigStore.delete(item.key);
-              raf.scheduleFullRedraw();
+              scheduleFullRedraw();
             },
           },
           m('i.material-icons', 'delete'),
@@ -297,7 +289,7 @@
         placeholder: 'Title for config',
         oninput() {
           ConfigTitleState.setTitle(this.value);
-          raf.scheduleFullRedraw();
+          scheduleFullRedraw();
         },
       }),
       m(
@@ -313,7 +305,7 @@
               recMgr.state.recordConfig,
               ConfigTitleState.getTitle(),
             );
-            raf.scheduleFullRedraw();
+            scheduleFullRedraw();
             ConfigTitleState.clearTitle();
           },
         },
@@ -331,7 +323,7 @@
               )
             ) {
               recMgr.clearRecordConfig();
-              raf.scheduleFullRedraw();
+              scheduleFullRedraw();
             }
           },
         },
@@ -578,7 +570,7 @@
 
 function onStartRecordingPressed(recMgr: RecordingManager) {
   location.href = '#!/record/instructions';
-  raf.scheduleFullRedraw();
+  scheduleFullRedraw();
   autosaveConfigStore.save(recMgr.state.recordConfig);
 
   const target = recMgr.state.recordingTarget;
@@ -587,7 +579,7 @@
     isChromeTarget(target) ||
     isWindowsTarget(target)
   ) {
-    AppImpl.instance.analytics.logEvent(
+    recMgr.app.analytics.logEvent(
       'Record Trace',
       `Record trace (${target.os})`,
     );
@@ -747,7 +739,7 @@
     '.record-menu',
     {
       class: recInProgress ? 'disabled' : '',
-      onclick: () => raf.scheduleFullRedraw(),
+      onclick: () => scheduleFullRedraw(),
     },
     m('header', 'Trace config'),
     m(
@@ -770,22 +762,20 @@
           m('.sub', 'Manually record trace'),
         ),
       ),
-      PERSIST_CONFIG_FLAG.get()
-        ? m(
-            'a[href="#!/record/config"]',
-            {
-              onclick: () => {
-                recordConfigStore.reloadFromLocalStorage();
-              },
-            },
-            m(
-              `li${routePage === 'config' ? '.active' : ''}`,
-              m('i.material-icons', 'save'),
-              m('.title', 'Saved configs'),
-              m('.sub', 'Manage local configs'),
-            ),
-          )
-        : null,
+      m(
+        'a[href="#!/record/config"]',
+        {
+          onclick: () => {
+            recordConfigStore.reloadFromLocalStorage();
+          },
+        },
+        m(
+          `li${routePage === 'config' ? '.active' : ''}`,
+          m('i.material-icons', 'save'),
+          m('.title', 'Saved configs'),
+          m('.sub', 'Manage local configs'),
+        ),
+      ),
     ),
     m('header', 'Probes'),
     m('ul', probes),
@@ -796,10 +786,36 @@
   return routePage === section ? '.active' : '';
 }
 
-export class RecordPage implements m.ClassComponent<PageAttrs> {
-  private readonly recMgr = RecordingManager.instance;
+export interface RecordPageAttrs extends PageAttrs {
+  app: App;
+  recMgr: RecordingManager;
+}
 
-  view({attrs}: m.CVnode<PageAttrs>) {
+export class RecordPage implements m.ClassComponent<RecordPageAttrs> {
+  private readonly recMgr: RecordingManager;
+  private lastSubpage: string | undefined = undefined;
+
+  constructor({attrs}: m.CVnode<RecordPageAttrs>) {
+    this.recMgr = attrs.recMgr;
+  }
+
+  oninit({attrs}: m.CVnode<RecordPageAttrs>) {
+    this.lastSubpage = attrs.subpage;
+    if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
+      const hash = attrs.subpage.substring(7);
+      loadRecordConfig(this.recMgr, hash);
+      attrs.app.navigate('#!/record/instructions');
+    }
+  }
+
+  view({attrs}: m.CVnode<RecordPageAttrs>) {
+    if (attrs.subpage !== this.lastSubpage) {
+      this.lastSubpage = attrs.subpage;
+      // TODO(primiano): this is a hack necesasry to retrigger the generation of
+      // the record cmdline. Refactor this code once record v1 vs v2 is gone.
+      this.recMgr.setRecordConfig(this.recMgr.state.recordConfig);
+    }
+
     const pages: m.Children = [];
     // we need to remove the `/` character from the route
     let routePage = attrs.subpage ? attrs.subpage.substr(1) : '';
@@ -862,3 +878,35 @@
     );
   }
 }
+
+export async function uploadRecordingConfig(recordConfig: RecordConfig) {
+  const json = JSON.stringify(recordConfig);
+  const uploader: GcsUploader = new GcsUploader(json, {
+    mimeType: MIME_JSON,
+  });
+  await uploader.waitForCompletion();
+  const hash = uploader.uploadedFileName;
+  const url = `${self.location.origin}/#!/record/share/${hash}`;
+  showModal({
+    title: 'Shareable record settings',
+    content: m(CopyableLink, {url}),
+  });
+}
+
+export async function loadRecordConfig(recMgr: RecordingManager, hash: string) {
+  const url = `https://storage.googleapis.com/${BUCKET_NAME}/${hash}`;
+  const response = await fetch(url);
+  if (!response.ok) {
+    showModal({title: 'Load failed', content: `Could not fetch ${url}`});
+    return;
+  }
+  const text = await response.text();
+  const json = JSON.parse(text);
+  const res = RECORD_CONFIG_SCHEMA.safeParse(json);
+  if (!res.success) {
+    throw new Error(
+      'Failed to deserialize record settings ' + res.error.toString(),
+    );
+  }
+  recMgr.setRecordConfig(res.data);
+}
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
similarity index 76%
rename from ui/src/frontend/record_page_v2.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
index 5fbeac0..3559332 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
@@ -14,60 +14,52 @@
 
 import m from 'mithril';
 import {Attributes} from 'mithril';
-import {assertExists} from '../base/logging';
-import {RecordingConfigUtils} from '../common/recordingV2/recording_config_utils';
+import {assertExists} from '../../base/logging';
+import {RecordingConfigUtils} from './recordingV2/recording_config_utils';
 import {
   ChromeTargetInfo,
   RecordingTargetV2,
   TargetInfo,
-} from '../common/recordingV2/recording_interfaces_v2';
+} from './recordingV2/recording_interfaces_v2';
 import {
   RecordingPageController,
   RecordingState,
-} from '../common/recordingV2/recording_page_controller';
-import {
-  EXTENSION_NAME,
-  EXTENSION_URL,
-} from '../common/recordingV2/recording_utils';
-import {targetFactoryRegistry} from '../common/recordingV2/target_factory_registry';
-import {raf} from '../core/raf_scheduler';
-import {PageAttrs} from '../public/page';
+} from './recordingV2/recording_page_controller';
+import {EXTENSION_NAME, EXTENSION_URL} from './recordingV2/recording_utils';
+import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
+import {PageAttrs} from '../../public/page';
 import {recordConfigStore} from './record_config';
 import {
   Configurations,
+  loadRecordConfig,
   maybeGetActiveCss,
-  PERSIST_CONFIG_FLAG,
   RECORDING_SECTIONS,
+  uploadRecordingConfig,
 } from './record_page';
 import {CodeSnippet} from './record_widgets';
-import {AdvancedSettings} from './recording/advanced_settings';
-import {AndroidSettings} from './recording/android_settings';
-import {ChromeSettings} from './recording/chrome_settings';
-import {CpuSettings} from './recording/cpu_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {GpuSettings} from './recording/gpu_settings';
-import {LinuxPerfSettings} from './recording/linux_perf_settings';
-import {MemorySettings} from './recording/memory_settings';
-import {PowerSettings} from './recording/power_settings';
-import {RecordingSettings} from './recording/recording_settings';
-import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
-import {showAddNewTargetModal} from './recording/reset_target_modal';
-import {createPermalink} from './permalink';
-import {RecordingManager} from '../controller/recording_manager';
-import {RecordConfig} from '../controller/record_config_types';
+import {AdvancedSettings} from './advanced_settings';
+import {AndroidSettings} from './android_settings';
+import {ChromeSettings} from './chrome_settings';
+import {CpuSettings} from './cpu_settings';
+import {EtwSettings} from './etw_settings';
+import {GpuSettings} from './gpu_settings';
+import {LinuxPerfSettings} from './linux_perf_settings';
+import {MemorySettings} from './memory_settings';
+import {PowerSettings} from './power_settings';
+import {RecordingSettings} from './recording_settings';
+import {FORCE_RESET_MESSAGE} from './recording_ui_utils';
+import {showAddNewTargetModal} from './reset_target_modal';
+import {RecordingManager} from './recording_manager';
+import {RecordConfig} from './record_config_types';
+import {App} from '../../public/app';
+import {scheduleFullRedraw} from '../../widgets/raf';
 
 const START_RECORDING_MESSAGE = 'Start Recording';
 
 // TODO(primiano): this is needs to be rewritten, but then i'm going to rewrite
 // the whole record_page_v2 so not worth cleaning up now.
-let _controller: RecordingPageController;
-function controller(): RecordingPageController {
-  if (_controller === undefined) {
-    _controller = new RecordingPageController(RecordingManager.instance);
-  }
-  return _controller;
-}
-const recordConfigUtils = new RecordingConfigUtils();
+let controller: RecordingPageController;
+let recordConfigUtils: RecordingConfigUtils;
 
 // Options for displaying a target selection menu.
 export interface TargetSelectionOptions {
@@ -106,13 +98,13 @@
 
 function RecordingPlatformSelection() {
   // Don't show the platform selector while we are recording a trace.
-  if (controller().getState() >= RecordingState.RECORDING) return undefined;
+  if (controller.getState() >= RecordingState.RECORDING) return undefined;
 
   return m(
     '.target',
     m(
       '.chip',
-      {onclick: () => showAddNewTargetModal(controller())},
+      {onclick: () => showAddNewTargetModal(controller)},
       m('button', 'Add new recording target'),
       m('i.material-icons', 'add'),
     ),
@@ -121,13 +113,13 @@
 }
 
 export function targetSelection(): m.Vnode | undefined {
-  if (!controller().shouldShowTargetSelection()) {
+  if (!controller.shouldShowTargetSelection()) {
     return undefined;
   }
 
   const targets: RecordingTargetV2[] = targetFactoryRegistry.listTargets();
   const targetNames = [];
-  const targetInfo = controller().getTargetInfo();
+  const targetInfo = controller.getTargetInfo();
   if (!targetInfo) {
     targetNames.push(m('option', 'PLEASE_SELECT_TARGET'));
   }
@@ -149,7 +141,7 @@
       {
         selectedIndex,
         onchange: (e: Event) => {
-          controller().onTargetSelection((e.target as HTMLSelectElement).value);
+          controller.onTargetSelection((e.target as HTMLSelectElement).value);
         },
         onupdate: (select) => {
           // Work around mithril bug
@@ -178,24 +170,22 @@
 }
 
 function Instructions(recCfg: RecordConfig, cssClass: string) {
-  if (controller().getState() < RecordingState.TARGET_SELECTED) {
+  if (controller.getState() < RecordingState.TARGET_SELECTED) {
     return undefined;
   }
   // We will have a valid target at this step because we checked the state.
-  const targetInfo = assertExists(controller().getTargetInfo());
+  const targetInfo = assertExists(controller.getTargetInfo());
 
   return m(
     `.record-section.instructions${cssClass}`,
     m('header', 'Recording command'),
-    PERSIST_CONFIG_FLAG.get()
-      ? m(
-          'button.permalinkconfig',
-          {
-            onclick: () => createPermalink({mode: 'RECORDING_OPTS'}),
-          },
-          'Share recording settings',
-        )
-      : null,
+    m(
+      'button.permalinkconfig',
+      {
+        onclick: () => uploadRecordingConfig(recCfg),
+      },
+      'Share recording settings',
+    ),
     RecordingSnippet(recCfg, targetInfo),
     BufferUsageProgressBar(),
     m('.buttons', StopCancelButtons()),
@@ -204,13 +194,13 @@
 
 function BufferUsageProgressBar() {
   // Show the Buffer Usage bar only after we start recording a trace.
-  if (controller().getState() !== RecordingState.RECORDING) {
+  if (controller.getState() !== RecordingState.RECORDING) {
     return undefined;
   }
 
-  controller().fetchBufferUsage();
+  controller.fetchBufferUsage();
 
-  const bufferUsage = controller().getBufferUsagePercentage();
+  const bufferUsage = controller.getBufferUsagePercentage();
   // Buffer usage is not available yet on Android.
   if (bufferUsage === 0) return undefined;
 
@@ -222,11 +212,11 @@
 }
 
 function RecordingNotes(recCfg: RecordConfig) {
-  if (controller().getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
+  if (controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
     return undefined;
   }
   // We will have a valid target at this step because we checked the state.
-  const targetInfo = assertExists(controller().getTargetInfo());
+  const targetInfo = assertExists(controller.getTargetInfo());
 
   const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
   const cmdlineUrl =
@@ -321,7 +311,7 @@
 function RecordingSnippet(recCfg: RecordConfig, targetInfo: TargetInfo) {
   // We don't need commands to start tracing on chrome
   if (isChromeTargetInfo(targetInfo)) {
-    if (controller().getState() > RecordingState.AUTH_P2) {
+    if (controller.getState() > RecordingState.AUTH_P2) {
       // If the UI has started tracing, don't display a message guiding the user
       // to start recording.
       return undefined;
@@ -373,14 +363,14 @@
 
 function RecordingButton(recCfg: RecordConfig) {
   if (
-    controller().getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
-    !controller().canCreateTracingSession()
+    controller.getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
+    !controller.canCreateTracingSession()
   ) {
     return undefined;
   }
 
   // We know we have a target because we checked the state.
-  const targetInfo = assertExists(controller().getTargetInfo());
+  const targetInfo = assertExists(controller.getTargetInfo());
   const hasDataSources = recordConfigUtils.fetchLatestRecordCommand(
     recCfg,
     targetInfo,
@@ -395,7 +385,7 @@
       'button',
       {
         class: 'selected',
-        onclick: () => controller().onStartRecordingPressed(),
+        onclick: () => controller.onStartRecordingPressed(),
       },
       START_RECORDING_MESSAGE,
     ),
@@ -404,21 +394,17 @@
 
 function StopCancelButtons() {
   // Show the Stop/Cancel buttons only while we are recording a trace.
-  if (!controller().shouldShowStopCancelButtons()) {
+  if (!controller.shouldShowStopCancelButtons()) {
     return undefined;
   }
 
   const stop = m(
     `button.selected`,
-    {onclick: () => controller().onStop()},
+    {onclick: () => controller.onStop()},
     'Stop',
   );
 
-  const cancel = m(
-    `button`,
-    {onclick: () => controller().onCancel()},
-    'Cancel',
-  );
+  const cancel = m(`button`, {onclick: () => controller.onCancel()}, 'Cancel');
 
   return [stop, cancel];
 }
@@ -508,7 +494,7 @@
 
   // We only display the probes when we have a valid target, so it's not
   // possible for the target to be undefined here.
-  const targetType = assertExists(controller().getTargetInfo()).targetType;
+  const targetType = assertExists(controller.getTargetInfo()).targetType;
   const probes = [];
   if (targetType === 'LINUX') {
     probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
@@ -533,10 +519,10 @@
     '.record-menu',
     {
       class:
-        controller().getState() > RecordingState.TARGET_INFO_DISPLAYED
+        controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
           ? 'disabled'
           : '',
-      onclick: () => raf.scheduleFullRedraw(),
+      onclick: () => scheduleFullRedraw(),
     },
     m('header', 'Trace config'),
     m(
@@ -559,22 +545,20 @@
           m('.sub', 'Manually record trace'),
         ),
       ),
-      PERSIST_CONFIG_FLAG.get()
-        ? m(
-            'a[href="#!/record/config"]',
-            {
-              onclick: () => {
-                recordConfigStore.reloadFromLocalStorage();
-              },
-            },
-            m(
-              `li${routePage === 'config' ? '.active' : ''}`,
-              m('i.material-icons', 'save'),
-              m('.title', 'Saved configs'),
-              m('.sub', 'Manage local configs'),
-            ),
-          )
-        : null,
+      m(
+        'a[href="#!/record/config"]',
+        {
+          onclick: () => {
+            recordConfigStore.reloadFromLocalStorage();
+          },
+        },
+        m(
+          `li${routePage === 'config' ? '.active' : ''}`,
+          m('i.material-icons', 'save'),
+          m('.title', 'Saved configs'),
+          m('.sub', 'Manage local configs'),
+        ),
+      ),
     ),
     m('header', 'Probes'),
     m('ul', probes),
@@ -584,10 +568,10 @@
 function getRecordContainer(recMgr: RecordingManager, subpage?: string) {
   const recCfg = recMgr.state.recordConfig;
   const components: m.Children[] = [RecordHeader(recMgr)];
-  if (controller().getState() === RecordingState.NO_TARGET) {
+  if (controller.getState() === RecordingState.NO_TARGET) {
     components.push(m('.full-centered', 'Please connect a valid target.'));
     return m('.record-container', components);
-  } else if (controller().getState() <= RecordingState.ASK_TO_FORCE_P1) {
+  } else if (controller.getState() <= RecordingState.ASK_TO_FORCE_P1) {
     components.push(
       m(
         '.full-centered',
@@ -597,13 +581,13 @@
       ),
     );
     return m('.record-container', components);
-  } else if (controller().getState() === RecordingState.AUTH_P1) {
+  } else if (controller.getState() === RecordingState.AUTH_P1) {
     components.push(
       m('.full-centered', 'Please allow USB debugging on the device.'),
     );
     return m('.record-container', components);
   } else if (
-    controller().getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
+    controller.getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
   ) {
     components.push(
       m('.full-centered', 'Waiting for the trace to be collected.'),
@@ -645,7 +629,7 @@
   for (const [section, component] of settingsSections.entries()) {
     pages.push(
       m(component, {
-        dataSources: controller().getTargetInfo()?.dataSources || [],
+        dataSources: controller.getTargetInfo()?.dataSources || [],
         cssClass: maybeGetActiveCss(routePage, section),
         recState: recMgr.state,
       }),
@@ -656,20 +640,43 @@
   return m('.record-container', components);
 }
 
-export class RecordPageV2 implements m.ClassComponent<PageAttrs> {
-  private readonly recMgr = RecordingManager.instance;
+export interface RecordPageV2Attrs extends PageAttrs {
+  app: App;
+  recCtl: RecordingPageController;
+  recMgr: RecordingManager;
+}
 
-  oninit(): void {
-    controller().initFactories();
+export class RecordPageV2 implements m.ClassComponent<RecordPageV2Attrs> {
+  private lastSubpage: string | undefined = undefined;
+
+  constructor({attrs}: m.CVnode<RecordPageV2Attrs>) {
+    controller ??= attrs.recCtl;
+    recordConfigUtils ??= new RecordingConfigUtils();
   }
 
-  view({attrs}: m.CVnode<PageAttrs>) {
+  oninit({attrs}: m.CVnode<RecordPageV2Attrs>) {
+    this.lastSubpage = attrs.subpage;
+    if (attrs.subpage !== undefined && attrs.subpage.startsWith('/share/')) {
+      const hash = attrs.subpage.substring(7);
+      loadRecordConfig(attrs.recMgr, hash);
+      attrs.app.navigate('#!/record/instructions');
+    }
+  }
+
+  view({attrs}: m.CVnode<RecordPageV2Attrs>) {
+    if (attrs.subpage !== this.lastSubpage) {
+      this.lastSubpage = attrs.subpage;
+      // TODO(primiano): this is a hack necesasry to retrigger the generation of
+      // the record cmdline. Refactor this code once record v1 vs v2 is gone.
+      attrs.recMgr.setRecordConfig(attrs.recMgr.state.recordConfig);
+    }
+
     return m(
       '.record-page',
-      controller().getState() > RecordingState.TARGET_INFO_DISPLAYED
+      controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
         ? m('.hider')
         : [],
-      getRecordContainer(this.recMgr, attrs.subpage),
+      getRecordContainer(attrs.recMgr, attrs.subpage),
     );
   }
 }
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
similarity index 96%
rename from ui/src/frontend/record_widgets.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
index 90f3c3d..325237b 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
@@ -13,11 +13,11 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {copyToClipboard} from '../base/clipboard';
-import {assertExists} from '../base/logging';
-import {RecordConfig} from '../controller/record_config_types';
-import {raf} from '../core/raf_scheduler';
-import {assetSrc} from '../base/assets';
+import {copyToClipboard} from '../../base/clipboard';
+import {assertExists} from '../../base/logging';
+import {RecordConfig} from './record_config_types';
+import {assetSrc} from '../../base/assets';
+import {scheduleFullRedraw} from '../../widgets/raf';
 
 export declare type Setter<T> = (cfg: RecordConfig, val: T) => void;
 export declare type Getter<T> = (cfg: RecordConfig) => T;
@@ -63,7 +63,7 @@
   view({attrs, children}: m.CVnode<ProbeAttrs>) {
     const onToggle = (enabled: boolean) => {
       attrs.setEnabled(attrs.recCfg, enabled);
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
     };
 
     const enabled = attrs.isEnabled(attrs.recCfg);
@@ -130,7 +130,7 @@
   view({attrs}: m.CVnode<ToggleAttrs>) {
     const onToggle = (enabled: boolean) => {
       attrs.setEnabled(attrs.recCfg, enabled);
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
     };
 
     const enabled = attrs.isEnabled(attrs.recCfg);
@@ -175,7 +175,7 @@
 export class Slider implements m.ClassComponent<SliderAttrs> {
   onValueChange(attrs: SliderAttrs, newVal: number) {
     attrs.set(attrs.recCfg, newVal);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   onTimeValueChange(attrs: SliderAttrs, hms: string) {
@@ -276,7 +276,7 @@
       selKeys.push(item.value);
     }
     attrs.set(attrs.recCfg, selKeys);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<DropdownAttrs>) {
@@ -326,7 +326,7 @@
 export class Textarea implements m.ClassComponent<TextareaAttrs> {
   onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
     attrs.set(attrs.recCfg, dom.value);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<TextareaAttrs>) {
@@ -400,7 +400,7 @@
     if (!enabled && index !== -1) {
       values.splice(index, 1);
     }
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
diff --git a/ui/src/common/recordingV2/adb_connection_impl.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
similarity index 94%
rename from ui/src/common/recordingV2/adb_connection_impl.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
index 99ef224..33e0dc1 100644
--- a/ui/src/common/recordingV2/adb_connection_impl.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer} from '../../base/deferred';
-import {ArrayBufferBuilder} from '../../base/array_buffer_builder';
+import {defer} from '../../../base/deferred';
+import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
 import {AdbFileHandler} from './adb_file_handler';
 import {
   AdbConnection,
@@ -21,7 +21,7 @@
   OnDisconnectCallback,
   OnMessageCallback,
 } from './recording_interfaces_v2';
-import {utf8Decode} from '../../base/string_utils';
+import {utf8Decode} from '../../../base/string_utils';
 
 export abstract class AdbConnectionImpl implements AdbConnection {
   // onStatus and onDisconnect are set to callbacks passed from the caller.
diff --git a/ui/src/common/recordingV2/adb_connection_over_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
similarity index 98%
rename from ui/src/common/recordingV2/adb_connection_over_websocket.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
index 160b257..9c9d139 100644
--- a/ui/src/common/recordingV2/adb_connection_over_websocket.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer, Deferred} from '../../base/deferred';
-import {utf8Decode} from '../../base/string_utils';
+import {defer, Deferred} from '../../../base/deferred';
+import {utf8Decode} from '../../../base/string_utils';
 import {AdbConnectionImpl} from './adb_connection_impl';
 import {RecordingError} from './recording_error_handling';
 import {
diff --git a/ui/src/common/recordingV2/adb_connection_over_webusb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
similarity index 98%
rename from ui/src/common/recordingV2/adb_connection_over_webusb.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
index 713d8b3..715d366 100644
--- a/ui/src/common/recordingV2/adb_connection_over_webusb.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../base/logging';
-import {isString} from '../../base/object_utils';
-import {utf8Decode, utf8Encode} from '../../base/string_utils';
-import {CmdType} from '../../controller/adb_interfaces';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
+import {isString} from '../../../base/object_utils';
+import {utf8Decode, utf8Encode} from '../../../base/string_utils';
+import {CmdType} from '../adb_interfaces';
 import {AdbConnectionImpl} from './adb_connection_impl';
 import {AdbKeyManager, maybeStoreKey} from './auth/adb_key_manager';
 import {RecordingError, wrapRecordingError} from './recording_error_handling';
diff --git a/ui/src/common/recordingV2/adb_file_handler.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
similarity index 94%
rename from ui/src/common/recordingV2/adb_file_handler.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
index 1016fe7..078726f 100644
--- a/ui/src/common/recordingV2/adb_file_handler.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer, Deferred} from '../../base/deferred';
-import {assertFalse} from '../../base/logging';
-import {ArrayBufferBuilder} from '../../base/array_buffer_builder';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertFalse} from '../../../base/logging';
+import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
 import {RecordingError} from './recording_error_handling';
 import {ByteStream} from './recording_interfaces_v2';
 import {
   BINARY_PUSH_FAILURE,
   BINARY_PUSH_UNKNOWN_RESPONSE,
 } from './recording_utils';
-import {utf8Decode} from '../../base/string_utils';
+import {utf8Decode} from '../../../base/string_utils';
 
 // https://cs.android.com/android/platform/superproject/+/main:packages/
 // modules/adb/file_sync_protocol.h;l=144
diff --git a/ui/src/common/recordingV2/auth/adb_auth.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
similarity index 97%
rename from ui/src/common/recordingV2/auth/adb_auth.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
index aec8752..7ed275e 100644
--- a/ui/src/common/recordingV2/auth/adb_auth.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
@@ -13,12 +13,12 @@
 // limitations under the License.
 
 import {BigInteger, RSAKey} from 'jsbn-rsa';
-import {assertExists, assertTrue} from '../../../base/logging';
+import {assertExists, assertTrue} from '../../../../base/logging';
 import {
   base64Decode,
   base64Encode,
   hexEncode,
-} from '../../../base/string_utils';
+} from '../../../../base/string_utils';
 import {RecordingError} from '../recording_error_handling';
 
 const WORD_SIZE = 4;
diff --git a/ui/src/common/recordingV2/auth/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
similarity index 98%
rename from ui/src/common/recordingV2/auth/adb_key_manager.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
index 53e233f..0ce297b 100644
--- a/ui/src/common/recordingV2/auth/adb_key_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assetSrc} from '../../../base/assets';
+import {assetSrc} from '../../../../base/assets';
 import {AdbKey} from './adb_auth';
 
 function isPasswordCredential(
diff --git a/ui/src/common/recordingV2/auth/credentials_interfaces.d.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
similarity index 100%
rename from ui/src/common/recordingV2/auth/credentials_interfaces.d.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/credentials_interfaces.d.ts
diff --git a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
similarity index 95%
rename from ui/src/common/recordingV2/chrome_traced_tracing_session.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
index f8ecd03..9461190 100644
--- a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
@@ -12,28 +12,28 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertTrue} from '../../base/logging';
-import {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertTrue} from '../../../base/logging';
+import {binaryDecode, binaryEncode} from '../../../base/string_utils';
 import {
   ChromeExtensionMessage,
   isChromeExtensionError,
   isChromeExtensionStatus,
   isGetCategoriesResponse,
-} from '../../controller/chrome_proxy_record_controller';
+} from '../chrome_proxy_record_controller';
 import {
   isDisableTracingResponse,
   isEnableTracingResponse,
   isFreeBuffersResponse,
   isGetTraceStatsResponse,
   isReadBuffersResponse,
-} from '../../controller/consumer_port_types';
+} from '../consumer_port_types';
 import {
   EnableTracingRequest,
   IBufferStats,
   ISlice,
   TraceConfig,
-} from '../../protos';
+} from '../../../protos';
 import {RecordingError} from './recording_error_handling';
 import {
   TracingSession,
diff --git a/ui/src/common/recordingV2/host_os_byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
similarity index 97%
rename from ui/src/common/recordingV2/host_os_byte_stream.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
index 3c43630..a03b791 100644
--- a/ui/src/common/recordingV2/host_os_byte_stream.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer} from '../../base/deferred';
+import {defer} from '../../../base/deferred';
 import {
   ByteStream,
   OnStreamCloseCallback,
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
similarity index 98%
rename from ui/src/common/recordingV2/recording_config_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
index bc3262e..e4eca50 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {isString} from '../../base/object_utils';
-import {base64Encode} from '../../base/string_utils';
-import {exists} from '../../base/utils';
-import {RecordConfig} from '../../controller/record_config_types';
+import {isString} from '../../../base/object_utils';
+import {base64Encode} from '../../../base/string_utils';
+import {exists} from '../../../base/utils';
+import {RecordConfig} from '../record_config_types';
 import {
   AndroidLogConfig,
   AndroidLogId,
@@ -41,7 +41,7 @@
   TraceConfig,
   TrackEventConfig,
   VmstatCounters,
-} from '../../protos';
+} from '../../../protos';
 import {TargetInfo} from './recording_interfaces_v2';
 import PerfClock = PerfEvents.PerfClock;
 import Timebase = PerfEvents.Timebase;
@@ -463,7 +463,7 @@
 
     if (uiCfg.androidStatsdPushedAtoms.length > 0) {
       ds.config.statsdTracingConfig.pushAtomId =
-        uiCfg.androidStatsdPushedAtoms.map((atom) => atom as any as AtomId);
+        uiCfg.androidStatsdPushedAtoms.map((atom) => atom as unknown as AtomId);
     }
 
     const needPulledAtomConfig =
@@ -471,7 +471,7 @@
       uiCfg.androidStatsdPulledAtoms.length > 0;
 
     if (needPulledAtomConfig) {
-      let pullAtomConfig = new StatsdPullAtomConfig();
+      const pullAtomConfig = new StatsdPullAtomConfig();
       if (uiCfg.androidStatsdRawPulledAtoms.length > 0) {
         for (const line of uiCfg.androidStatsdRawPulledAtoms.split('\n')) {
           if (line.trim().length > 0) {
@@ -479,8 +479,9 @@
           }
         }
       }
-      pullAtomConfig.pullAtomId =
-        uiCfg.androidStatsdPulledAtoms.map((atom) => atom as any as AtomId);
+      pullAtomConfig.pullAtomId = uiCfg.androidStatsdPulledAtoms.map(
+        (atom) => atom as unknown as AtomId,
+      );
       pullAtomConfig.pullFrequencyMs =
         uiCfg.androidStatsdPulledAtomPullFrequencyMs;
       if (uiCfg.androidStatsdPulledAtomPackages.length > 0) {
diff --git a/ui/src/common/recordingV2/recording_config_utils_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
similarity index 96%
rename from ui/src/common/recordingV2/recording_config_utils_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
index 67ac112..dd96a69 100644
--- a/ui/src/common/recordingV2/recording_config_utils_unittest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createEmptyRecordConfig} from '../../controller/record_config_types';
+import {createEmptyRecordConfig} from '../record_config_types';
 import {genTraceConfig} from './recording_config_utils';
 import {AndroidTargetInfo} from './recording_interfaces_v2';
 
diff --git a/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts
new file mode 100644
index 0000000..ba86e65
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_error_handling.ts
@@ -0,0 +1,263 @@
+// Copyright (C) 2022 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 m from 'mithril';
+import {getErrorMessage} from '../../../base/errors';
+import {showModal} from '../../../widgets/modal';
+import {OnMessageCallback} from './recording_interfaces_v2';
+import {
+  ALLOW_USB_DEBUGGING,
+  BINARY_PUSH_FAILURE,
+  BINARY_PUSH_UNKNOWN_RESPONSE,
+  EXTENSION_NOT_INSTALLED,
+  EXTENSION_URL,
+  NO_DEVICE_SELECTED,
+  PARSING_UNABLE_TO_DECODE_METHOD,
+  PARSING_UNKNWON_REQUEST_ID,
+  PARSING_UNRECOGNIZED_MESSAGE,
+  PARSING_UNRECOGNIZED_PORT,
+  WEBSOCKET_UNABLE_TO_CONNECT,
+} from './recording_utils';
+
+// The pattern for handling recording error can have the following nesting in
+// case of errors:
+// A. wrapRecordingError -> wraps a promise
+// B. onFailure -> has user defined logic and calls showRecordingModal
+// C. showRecordingModal -> shows UX for a given error; this is not called
+//    directly by wrapRecordingError, because we want the caller (such as the
+//    UI) to dictate the UX
+
+// This method takes a promise and a callback to be execute in case the promise
+// fails. It then awaits the promise and executes the callback in case of
+// failure. In the recording code it is used to wrap:
+// 1. Acessing the WebUSB API.
+// 2. Methods returning promises which can be rejected. For instance:
+// a) When the user clicks 'Add a new device' but then doesn't select a valid
+//    device.
+// b) When the user starts a tracing session, but cancels it before they
+//    authorize the session on the device.
+export async function wrapRecordingError<T>(
+  promise: Promise<T>,
+  onFailure: OnMessageCallback,
+): Promise<T | undefined> {
+  try {
+    return await promise;
+  } catch (e) {
+    // Sometimes the message is wrapped in an Error object, sometimes not, so
+    // we make sure we transform it into a string.
+    const errorMessage = getErrorMessage(e);
+    onFailure(errorMessage);
+    return undefined;
+  }
+}
+
+// Shows a modal for every known type of error which can arise during recording.
+// In this way, errors occuring at different levels of the recording process
+// can be handled in a central location.
+export function showRecordingModal(message: string): void {
+  if (
+    [
+      'Unable to claim interface.',
+      'The specified endpoint is not part of a claimed and selected ' +
+        'alternate interface.',
+      // thrown when calling the 'reset' method on a WebUSB device.
+      'Unable to reset the device.',
+    ].some((partOfMessage) => message.includes(partOfMessage))
+  ) {
+    showWebUSBErrorV2();
+  } else if (
+    [
+      'A transfer error has occurred.',
+      'The device was disconnected.',
+      'The transfer was cancelled.',
+    ].some((partOfMessage) => message.includes(partOfMessage)) ||
+    isDeviceDisconnectedError(message)
+  ) {
+    showConnectionLostError();
+  } else if (message === ALLOW_USB_DEBUGGING) {
+    showAllowUSBDebugging();
+  } else if (
+    isMessageComposedOf(message, [
+      BINARY_PUSH_FAILURE,
+      BINARY_PUSH_UNKNOWN_RESPONSE,
+    ])
+  ) {
+    showFailedToPushBinary(message.substring(message.indexOf(':') + 1));
+  } else if (message === NO_DEVICE_SELECTED) {
+    showNoDeviceSelected();
+  } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
+    showWebsocketConnectionIssue(message);
+  } else if (message === EXTENSION_NOT_INSTALLED) {
+    showExtensionNotInstalled();
+  } else if (
+    isMessageComposedOf(message, [
+      PARSING_UNKNWON_REQUEST_ID,
+      PARSING_UNABLE_TO_DECODE_METHOD,
+      PARSING_UNRECOGNIZED_PORT,
+      PARSING_UNRECOGNIZED_MESSAGE,
+    ])
+  ) {
+    showIssueParsingTheTracedResponse(message);
+  } else {
+    throw new Error(`${message}`);
+  }
+}
+
+function isDeviceDisconnectedError(message: string) {
+  return (
+    message.includes('Device with serial') &&
+    message.includes('was disconnected.')
+  );
+}
+
+function isMessageComposedOf(message: string, issues: string[]) {
+  for (const issue of issues) {
+    if (message.includes(issue)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+// Exception thrown by the Recording logic.
+export class RecordingError extends Error {}
+
+function showWebUSBErrorV2() {
+  showModal({
+    title: 'A WebUSB error occurred',
+    content: m(
+      'div',
+      m(
+        'span',
+        `Is adb already running on the host? Run this command and
+      try again.`,
+      ),
+      m('br'),
+      m('.modal-bash', '> adb kill-server'),
+      m('br'),
+      // The statement below covers the following edge case:
+      // 1. 'adb server' is running on the device.
+      // 2. The user selects the new Android target, so we try to fetch the
+      // OS version and do QSS.
+      // 3. The error modal is shown.
+      // 4. The user runs 'adb kill-server'.
+      // At this point we don't have a trigger to try fetching the OS version
+      // + QSS again. Therefore, the user will need to refresh the page.
+      m(
+        'span',
+        "If after running 'adb kill-server', you don't see " +
+          "a 'Start Recording' button on the page and you don't see " +
+          "'Allow USB debugging' on the device, " +
+          'you will need to reload this page.',
+      ),
+      m('br'),
+      m('br'),
+      m('span', 'For details see '),
+      m('a', {href: 'http://b/159048331', target: '_blank'}, 'b/159048331'),
+    ),
+  });
+}
+
+function showConnectionLostError(): void {
+  showModal({
+    title: 'Connection with the ADB device lost',
+    content: m(
+      'div',
+      m('span', `Please connect the device again to restart the recording.`),
+      m('br'),
+    ),
+  });
+}
+
+function showAllowUSBDebugging(): void {
+  showModal({
+    title: 'Could not connect to the device',
+    content: m(
+      'div',
+      m('span', 'Please allow USB debugging on the device.'),
+      m('br'),
+    ),
+  });
+}
+
+function showNoDeviceSelected(): void {
+  showModal({
+    title: 'No device was selected for recording',
+    content: m(
+      'div',
+      m(
+        'span',
+        `If you want to connect to an ADB device,
+           please select it from the list.`,
+      ),
+      m('br'),
+    ),
+  });
+}
+
+function showExtensionNotInstalled(): void {
+  showModal({
+    title: 'Perfetto Chrome extension not installed',
+    content: m(
+      'div',
+      m(
+        '.note',
+        `To trace Chrome from the Perfetto UI, you need to install our `,
+        m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
+        ' and then reload this page.',
+      ),
+      m('br'),
+    ),
+  });
+}
+
+function showIssueParsingTheTracedResponse(message: string): void {
+  showModal({
+    title:
+      'A problem was encountered while connecting to' +
+      ' the Perfetto tracing service',
+    content: m('div', m('span', message), m('br')),
+  });
+}
+
+function showFailedToPushBinary(message: string): void {
+  showModal({
+    title: 'Failed to push a binary to the device',
+    content: m(
+      'div',
+      m(
+        'span',
+        'This can happen if your Android device has an OS version lower ' +
+          'than Q. Perfetto tried to push the latest version of its ' +
+          'embedded binary but failed.',
+      ),
+      m('br'),
+      m('br'),
+      m('span', 'Error message:'),
+      m('br'),
+      m('span', message),
+    ),
+  });
+}
+
+function showWebsocketConnectionIssue(message: string): void {
+  showModal({
+    title: 'Unable to connect to the device via websocket',
+    content: m(
+      'div',
+      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+      m('pre', message),
+    ),
+  });
+}
diff --git a/ui/src/common/recordingV2/recording_interfaces_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
similarity index 99%
rename from ui/src/common/recordingV2/recording_interfaces_v2.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
index 954a145..c8a030e 100644
--- a/ui/src/common/recordingV2/recording_interfaces_v2.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TraceConfig} from '../../protos';
+import {TraceConfig} from '../../../protos';
 
 // TargetFactory connects, disconnects and keeps track of targets.
 // There is one factory for AndroidWebusb, AndroidWebsocket, Chrome etc.
diff --git a/ui/src/common/recordingV2/recording_page_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
similarity index 94%
rename from ui/src/common/recordingV2/recording_page_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
index de568aa..76617d5 100644
--- a/ui/src/common/recordingV2/recording_page_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
@@ -12,19 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists, assertTrue} from '../../base/logging';
-import {currentDateHourAndMinute} from '../../base/time';
-import {RecordingManager} from '../../controller/recording_manager';
-import {AppImpl} from '../../core/app_impl';
-import {raf} from '../../core/raf_scheduler';
-import {autosaveConfigStore} from '../../frontend/record_config';
+import {assertExists, assertTrue} from '../../../base/logging';
+import {currentDateHourAndMinute} from '../../../base/time';
+import {RecordingManager} from '../recording_manager';
+import {autosaveConfigStore} from '../record_config';
 import {
   DEFAULT_ADB_WEBSOCKET_URL,
   DEFAULT_TRACED_WEBSOCKET_URL,
-} from '../../frontend/recording/recording_ui_utils';
-import {couldNotClaimInterface} from '../../frontend/recording/reset_interface_modal';
-import {TraceConfig} from '../../protos';
-import {TRACE_SUFFIX} from '../constants';
+} from '../recording_ui_utils';
+import {couldNotClaimInterface} from '../reset_interface_modal';
+import {TraceConfig} from '../../../protos';
+import {TRACE_SUFFIX} from '../../../public/trace';
 import {genTraceConfig} from './recording_config_utils';
 import {RecordingError, showRecordingModal} from './recording_error_handling';
 import {
@@ -47,6 +45,8 @@
   HostOsTargetFactory,
 } from './target_factories/host_os_target_factory';
 import {targetFactoryRegistry} from './target_factory_registry';
+import {scheduleFullRedraw} from '../../../widgets/raf';
+import {App} from '../../../public/app';
 
 // The recording page can be in any of these states. It can transition between
 // states:
@@ -250,6 +250,7 @@
 // Keeps track of the state the Ui is in. Has methods which are executed on
 // user actions such as starting/stopping/cancelling a tracing session.
 export class RecordingPageController {
+  private app: App;
   private recMgr: RecordingManager;
 
   // State of the recording page. This is set by user actions and/or automatic
@@ -267,7 +268,8 @@
   // transitions don't override one another in async functions.
   private stateGeneration = 0;
 
-  constructor(recMgr: RecordingManager) {
+  constructor(app: App, recMgr: RecordingManager) {
+    this.app = app;
     this.recMgr = recMgr;
   }
 
@@ -296,7 +298,7 @@
     }
     this.setState(state);
     this.recMgr.setRecordingStatus(undefined);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
@@ -312,7 +314,7 @@
     if (this.tracingSessionWrapper !== tracingSessionWrapper) {
       return;
     }
-    AppImpl.instance.openTraceFromBuffer({
+    this.app.openTraceFromBuffer({
       title: 'Recorded trace',
       buffer: trace.buffer,
       fileName: `trace_${currentDateHourAndMinute()}${TRACE_SUFFIX}`,
@@ -390,11 +392,11 @@
 
     if (!this.target) {
       this.setState(RecordingState.NO_TARGET);
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
       return;
     }
     this.setState(RecordingState.TARGET_SELECTED);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
 
     this.tracingSessionWrapper = this.createTracingSessionWrapper(this.target);
     this.tracingSessionWrapper.fetchTargetInfo();
@@ -431,7 +433,7 @@
 
     const target = this.getTarget();
     const targetInfo = target.getInfo();
-    AppImpl.instance.analytics.logEvent(
+    this.app.analytics.logEvent(
       'Record Trace',
       `Record trace (${targetInfo.targetType})`,
     );
@@ -484,7 +486,7 @@
     // We redraw if:
     // 1. We received a correct buffer usage value.
     // 2. We receive a RecordingError.
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   initFactories() {
@@ -531,7 +533,7 @@
     // If the change happens for an existing target, the controller keeps the
     // currently selected target in focus.
     if (this.target && allTargets.includes(this.target)) {
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
       return;
     }
     // If the change happens to a new target or the controller does not have a
@@ -552,7 +554,7 @@
     this.recMgr.setRecordingStatus(undefined);
     // Redrawing because this method has changed the RecordingState, which will
     // affect the display of the record_page.
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   private setState(state: RecordingState) {
diff --git a/ui/src/common/recordingV2/recording_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_utils.ts
similarity index 100%
rename from ui/src/common/recordingV2/recording_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_utils.ts
diff --git a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
similarity index 96%
rename from ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
index 21097eb..03cda1f 100644
--- a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {RECORDING_V2_FLAG} from '../../../core/feature_flags';
 import {
   OnTargetChangeCallback,
   RecordingTargetV2,
@@ -22,7 +21,6 @@
   buildAbdWebsocketCommand,
   WEBSOCKET_CLOSED_ABNORMALLY_CODE,
 } from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
 import {AndroidWebsocketTarget} from '../targets/android_websocket_target';
 
 export const ANDROID_WEBSOCKET_TARGET_FACTORY = 'AndroidWebsocketTargetFactory';
@@ -268,8 +266,3 @@
     this.onTargetChange = onTargetChange;
   }
 }
-
-// We only want to instantiate this class if Recording V2 is enabled.
-if (RECORDING_V2_FLAG.get()) {
-  targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
-}
diff --git a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory_unittest.ts
diff --git a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
similarity index 89%
rename from ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
index d27ab07..a969c31 100644
--- a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {getErrorMessage} from '../../../base/errors';
-import {assertExists} from '../../../base/logging';
-import {RECORDING_V2_FLAG} from '../../../core/feature_flags';
+import {getErrorMessage} from '../../../../base/errors';
+import {assertExists} from '../../../../base/logging';
 import {AdbKeyManager} from '../auth/adb_key_manager';
 import {RecordingError} from '../recording_error_handling';
 import {
@@ -23,7 +22,6 @@
   TargetFactory,
 } from '../recording_interfaces_v2';
 import {ADB_DEVICE_FILTER, findInterfaceAndEndpoint} from '../recording_utils';
-import {targetFactoryRegistry} from '../target_factory_registry';
 import {AndroidWebusbTarget} from '../targets/android_webusb_target';
 
 export const ANDROID_WEBUSB_TARGET_FACTORY = 'AndroidWebusbTargetFactory';
@@ -155,11 +153,3 @@
     return deviceValidity;
   }
 }
-
-// We only want to instantiate this class if:
-// 1. The browser implements the USB functionality.
-// 2. Recording V2 is enabled.
-// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-if (navigator.usb && RECORDING_V2_FLAG.get()) {
-  targetFactoryRegistry.register(new AndroidWebusbTargetFactory(navigator.usb));
-}
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/chrome_target_factory_unittest.ts
diff --git a/ui/src/common/recordingV2/target_factories/host_os_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/host_os_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/host_os_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factories/index.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/index.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/index.ts
diff --git a/ui/src/common/recordingV2/target_factories/virtual_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
similarity index 100%
rename from ui/src/common/recordingV2/target_factories/virtual_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/virtual_target_factory.ts
diff --git a/ui/src/common/recordingV2/target_factory_registry.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
similarity index 96%
rename from ui/src/common/recordingV2/target_factory_registry.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
index e8de655..b34070d 100644
--- a/ui/src/common/recordingV2/target_factory_registry.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Registry} from '../../base/registry';
+import {Registry} from '../../../base/registry';
 import {RecordingTargetV2, TargetFactory} from './recording_interfaces_v2';
 
 export class TargetFactoryRegistry extends Registry<TargetFactory> {
diff --git a/ui/src/common/recordingV2/targets/android_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
similarity index 96%
rename from ui/src/common/recordingV2/targets/android_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
index 926846d..0bac1e4 100644
--- a/ui/src/common/recordingV2/targets/android_target.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {fetchWithTimeout} from '../../../base/http_utils';
-import {exists} from '../../../base/utils';
-import {VERSION} from '../../../gen/perfetto_version';
+import {fetchWithTimeout} from '../../../../base/http_utils';
+import {exists} from '../../../../base/utils';
+import {VERSION} from '../../../../gen/perfetto_version';
 import {AdbConnectionImpl} from '../adb_connection_impl';
 import {
   DataSource,
diff --git a/ui/src/common/recordingV2/targets/android_virtual_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/android_virtual_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_virtual_target.ts
diff --git a/ui/src/common/recordingV2/targets/android_websocket_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/android_websocket_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_websocket_target.ts
diff --git a/ui/src/common/recordingV2/targets/android_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
similarity index 96%
rename from ui/src/common/recordingV2/targets/android_webusb_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
index e70a19a..dc6e64d 100644
--- a/ui/src/common/recordingV2/targets/android_webusb_target.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../../../base/logging';
+import {assertExists} from '../../../../base/logging';
 import {AdbConnectionOverWebusb} from '../adb_connection_over_webusb';
 import {AdbKeyManager} from '../auth/adb_key_manager';
 import {OnTargetChangeCallback, TargetInfo} from '../recording_interfaces_v2';
diff --git a/ui/src/common/recordingV2/targets/chrome_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/chrome_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/chrome_target.ts
diff --git a/ui/src/common/recordingV2/targets/host_os_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
similarity index 100%
rename from ui/src/common/recordingV2/targets/host_os_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/host_os_target.ts
diff --git a/ui/src/common/recordingV2/traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
similarity index 98%
rename from ui/src/common/recordingV2/traced_tracing_session.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
index c0ba444..8687432 100644
--- a/ui/src/common/recordingV2/traced_tracing_session.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import protobuf from 'protobufjs/minimal';
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../base/logging';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
 import {
   DisableTracingRequest,
   DisableTracingResponse,
@@ -33,7 +33,7 @@
   ReadBuffersRequest,
   ReadBuffersResponse,
   TraceConfig,
-} from '../../protos';
+} from '../../../protos';
 import {RecordingError} from './recording_error_handling';
 import {
   ByteStream,
@@ -50,7 +50,7 @@
   PARSING_UNRECOGNIZED_PORT,
   RECORDING_IN_PROGRESS,
 } from './recording_utils';
-import {exists} from '../../base/utils';
+import {exists} from '../../../base/utils';
 
 // See wire_protocol.proto for more details.
 const WIRE_PROTOCOL_HEADER_SIZE = 4;
diff --git a/ui/src/common/recordingV2/websocket_menu_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
similarity index 97%
rename from ui/src/common/recordingV2/websocket_menu_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
index 8b800a7..2da8f5b 100644
--- a/ui/src/common/recordingV2/websocket_menu_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
@@ -16,7 +16,7 @@
   ADB_ENDPOINT,
   DEFAULT_WEBSOCKET_URL,
   TRACED_ENDPOINT,
-} from '../../frontend/recording/recording_ui_utils';
+} from '../recording_ui_utils';
 import {TargetFactory} from './recording_interfaces_v2';
 import {
   ANDROID_WEBSOCKET_TARGET_FACTORY,
diff --git a/ui/src/controller/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
similarity index 87%
rename from ui/src/controller/recording_manager.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
index 5d96017..be29691 100644
--- a/ui/src/controller/recording_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createEmptyState} from '../common/empty_state';
+import {createEmptyState} from './empty_state';
 import {
   AdbRecordingTarget,
   LoadedConfig,
@@ -20,39 +20,41 @@
   RecordingTarget,
   getDefaultRecordingTargets,
   isAdbTarget,
-} from '../common/state';
-import {RECORDING_V2_FLAG} from '../core/feature_flags';
-import {raf} from '../core/raf_scheduler';
+} from './state';
 import {AdbOverWebUsb} from './adb';
 import {isGetCategoriesResponse} from './chrome_proxy_record_controller';
 import {RecordConfig, createEmptyRecordConfig} from './record_config_types';
 import {RecordController} from './record_controller';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
+import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
+import {AndroidWebsocketTargetFactory} from './recordingV2/target_factories/android_websocket_target_factory';
+import {AndroidWebusbTargetFactory} from './recordingV2/target_factories/android_webusb_target_factory';
+import {exists} from '../../base/utils';
 
 const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
 
 // TODO(primiano): this class and RecordController should be merged. I'm keeping
 // them separate for now to reduce scope of refactorings.
 export class RecordingManager {
+  readonly app: App;
   private _state: RecordingState = createEmptyState();
   private recCtl: RecordController;
 
-  // TODO(primiano): this singleton is temporary. RecordingManager shoudl be
-  // injected in all the recording pages and the instance should be created and
-  // owned by the recording plugin. But for now we don't have a plugin.
-  private static _instance: RecordingManager | undefined = undefined;
-  static get instance() {
-    if (this._instance === undefined) {
-      this._instance = new RecordingManager();
-    }
-    return this._instance;
-  }
-
-  constructor() {
+  constructor(app: App, useRecordingV2: boolean) {
+    this.app = app;
     const extensionLocalChannel = new MessageChannel();
-    this.recCtl = new RecordController(this, extensionLocalChannel.port1);
+    this.recCtl = new RecordController(app, this, extensionLocalChannel.port1);
     this.setupExtentionPort(extensionLocalChannel);
 
-    if (!RECORDING_V2_FLAG.get()) {
+    if (useRecordingV2) {
+      targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
+      if (exists(navigator.usb)) {
+        targetFactoryRegistry.register(
+          new AndroidWebusbTargetFactory(navigator.usb),
+        );
+      }
+    } else {
       this.updateAvailableAdbDevices();
       try {
         navigator.usb.addEventListener('connect', () =>
@@ -156,7 +158,7 @@
         (message: object, _port: chrome.runtime.Port) => {
           if (isGetCategoriesResponse(message)) {
             this._state.chromeCategories = message.categories;
-            raf.scheduleFullRedraw();
+            scheduleFullRedraw();
             return;
           }
           extensionLocalChannel.port2.postMessage(message);
@@ -191,7 +193,7 @@
 
     this.setAvailableAdbDevices(availableAdbDevices);
     this.selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
     return availableAdbDevices;
   }
 
diff --git a/ui/src/frontend/recording/recording_multiple_choice.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
similarity index 93%
rename from ui/src/frontend/recording/recording_multiple_choice.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
index 27a83fc..0e34f5c 100644
--- a/ui/src/frontend/recording/recording_multiple_choice.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
@@ -16,9 +16,9 @@
 import {
   RecordingTargetV2,
   TargetFactory,
-} from '../../common/recordingV2/recording_interfaces_v2';
-import {RecordingPageController} from '../../common/recordingV2/recording_page_controller';
-import {RECORDING_MODAL_DIALOG_KEY} from '../../common/recordingV2/recording_utils';
+} from './recordingV2/recording_interfaces_v2';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
+import {RECORDING_MODAL_DIALOG_KEY} from './recordingV2/recording_utils';
 import {closeModal} from '../../widgets/modal';
 
 interface RecordingMultipleChoiceAttrs {
diff --git a/ui/src/frontend/recording/recording_sections.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
similarity index 86%
rename from ui/src/frontend/recording/recording_sections.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
index 03ebff8..c83b9e0 100644
--- a/ui/src/frontend/recording/recording_sections.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
-import {RecordingState} from '../../common/state';
+import {DataSource} from './recordingV2/recording_interfaces_v2';
+import {RecordingState} from './state';
 
 export interface RecordingSectionAttrs {
   recState: RecordingState;
diff --git a/ui/src/frontend/recording/recording_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/recording_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
index c61b49d..e3058be 100644
--- a/ui/src/frontend/recording/recording_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {RecordMode} from '../../common/state';
-import {Slider} from '../record_widgets';
+import {RecordMode} from './state';
+import {Slider} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 import {assetSrc} from '../../base/assets';
 
diff --git a/ui/src/frontend/recording/recording_ui_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_ui_utils.ts
similarity index 100%
rename from ui/src/frontend/recording/recording_ui_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_ui_utils.ts
diff --git a/ui/src/frontend/recording/reset_interface_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
similarity index 100%
rename from ui/src/frontend/recording/reset_interface_modal.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
diff --git a/ui/src/frontend/recording/reset_target_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
similarity index 91%
rename from ui/src/frontend/recording/reset_target_modal.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
index 4d3feb3..4d3d048 100644
--- a/ui/src/frontend/recording/reset_target_modal.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
@@ -13,19 +13,19 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {RecordingPageController} from '../../common/recordingV2/recording_page_controller';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
 import {
   EXTENSION_URL,
   RECORDING_MODAL_DIALOG_KEY,
-} from '../../common/recordingV2/recording_utils';
+} from './recordingV2/recording_utils';
 import {
   CHROME_TARGET_FACTORY,
   ChromeTargetFactory,
-} from '../../common/recordingV2/target_factories/chrome_target_factory';
-import {targetFactoryRegistry} from '../../common/recordingV2/target_factory_registry';
-import {WebsocketMenuController} from '../../common/recordingV2/websocket_menu_controller';
+} from './recordingV2/target_factories/chrome_target_factory';
+import {targetFactoryRegistry} from './recordingV2/target_factory_registry';
+import {WebsocketMenuController} from './recordingV2/websocket_menu_controller';
 import {closeModal, showModal} from '../../widgets/modal';
-import {CodeSnippet} from '../record_widgets';
+import {CodeSnippet} from './record_widgets';
 import {RecordingMultipleChoice} from './recording_multiple_choice';
 
 const RUN_WEBSOCKET_CMD =
diff --git a/ui/src/common/state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
similarity index 98%
rename from ui/src/common/state.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/state.ts
index 9c361f6..b94074b 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {RecordConfig} from '../controller/record_config_types';
+import {RecordConfig} from './record_config_types';
 
 export const MAX_TIME = 180;
 
diff --git a/ui/src/core/trace_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
similarity index 96%
rename from ui/src/core/trace_config_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
index e05f711..c7697dd 100644
--- a/ui/src/core/trace_config_utils.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EnableTracingRequest, TraceConfig} from '../protos';
+import {EnableTracingRequest, TraceConfig} from '../../protos';
 
 // In this file are contained a few functions to simplify the proto parsing.
 
diff --git a/ui/src/plugins/dev.perfetto.Sched/index.ts b/ui/src/plugins/dev.perfetto.Sched/index.ts
index a72c0f3..68d3d0b 100644
--- a/ui/src/plugins/dev.perfetto.Sched/index.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/index.ts
@@ -17,7 +17,10 @@
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
 import {ActiveCPUCountTrack, CPUType} from './active_cpu_count';
-import {RunnableThreadCountTrack} from './runnable_thread_count';
+import {
+  RunnableThreadCountTrack,
+  UninterruptibleSleepThreadCountTrack,
+} from './thread_count';
 import {getSchedTable} from './table';
 import {extensions} from '../../public/lib/extensions';
 
@@ -40,6 +43,26 @@
         addPinnedTrack(ctx, runnableThreadCountUri, 'Runnable thread count'),
     });
 
+    const uninterruptibleSleepThreadCountUri = `/uninterruptible_sleep_thread_count`;
+    ctx.tracks.registerTrack({
+      uri: uninterruptibleSleepThreadCountUri,
+      title: 'Uninterruptible Sleep thread count',
+      track: new UninterruptibleSleepThreadCountTrack({
+        trace: ctx,
+        uri: uninterruptibleSleepThreadCountUri,
+      }),
+    });
+    ctx.commands.registerCommand({
+      id: 'dev.perfetto.Sched.AddUninterruptibleSleepThreadCountTrackCommand',
+      name: 'Add track: uninterruptible sleep thread count',
+      callback: () =>
+        addPinnedTrack(
+          ctx,
+          uninterruptibleSleepThreadCountUri,
+          'Uninterruptible Sleep thread count',
+        ),
+    });
+
     const uri = uriForActiveCPUCountTrack();
     const title = 'Active CPU count';
     ctx.tracks.registerTrack({
diff --git a/ui/src/plugins/dev.perfetto.Sched/runnable_thread_count.ts b/ui/src/plugins/dev.perfetto.Sched/thread_count.ts
similarity index 77%
rename from ui/src/plugins/dev.perfetto.Sched/runnable_thread_count.ts
rename to ui/src/plugins/dev.perfetto.Sched/thread_count.ts
index 9b5e9c5..88fe892 100644
--- a/ui/src/plugins/dev.perfetto.Sched/runnable_thread_count.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/thread_count.ts
@@ -18,7 +18,7 @@
 } from '../../frontend/base_counter_track';
 import {NewTrackArgs} from '../../frontend/track';
 
-export class RunnableThreadCountTrack extends BaseCounterTrack {
+abstract class ThreadCountTrack extends BaseCounterTrack {
   constructor(args: NewTrackArgs) {
     super(args);
   }
@@ -35,7 +35,9 @@
       `INCLUDE PERFETTO MODULE sched.thread_level_parallelism`,
     );
   }
+}
 
+export class RunnableThreadCountTrack extends ThreadCountTrack {
   getSqlSource() {
     return `
       select
@@ -45,3 +47,14 @@
     `;
   }
 }
+
+export class UninterruptibleSleepThreadCountTrack extends ThreadCountTrack {
+  getSqlSource() {
+    return `
+      select
+        ts,
+        uninterruptible_sleep_thread_count as value
+      from sched_uninterruptible_sleep_thread_count
+    `;
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/table.ts b/ui/src/plugins/dev.perfetto.ThreadState/table.ts
index 9c86c5c..c93fb40 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/table.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/table.ts
@@ -27,7 +27,7 @@
   return {
     name: 'thread_state',
     columns: [
-      new ThreadStateIdColumn('id', {notNull: true}),
+      new ThreadStateIdColumn('threadStateSqlId', {notNull: true}),
       new TimestampColumn('ts'),
       new DurationColumn('dur'),
       new StandardColumn('state'),
@@ -46,11 +46,11 @@
         },
         {title: 'upid (process)', notNull: true},
       ),
-      new StandardColumn('io_wait', {aggregationType: 'nominal'}),
-      new StandardColumn('blocked_function'),
-      new ThreadColumn('waker_utid', {title: 'Waker thread'}),
-      new ThreadStateIdColumn('waker_id'),
-      new StandardColumn('irq_context', {aggregationType: 'nominal'}),
+      new StandardColumn('ioWait', {aggregationType: 'nominal'}),
+      new StandardColumn('blockedFunction'),
+      new ThreadColumn('wakerUtid', {title: 'Waker thread'}),
+      new ThreadStateIdColumn('wakerId'),
+      new StandardColumn('irqContext', {aggregationType: 'nominal'}),
       new StandardColumn('ucpu', {
         aggregationType: 'nominal',
         startsHidden: true,
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
index b790f1d..0a2287b7 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
@@ -52,7 +52,7 @@
 }
 
 export class ThreadStateDetailsPanel implements TrackEventDetailsPanel {
-  private state?: ThreadState;
+  private threadState?: ThreadState;
   private relatedStates?: RelatedThreadStates;
 
   constructor(
@@ -62,9 +62,9 @@
 
   async load() {
     const id = this.id;
-    this.state = await getThreadState(this.trace.engine, id);
+    this.threadState = await getThreadState(this.trace.engine, id);
 
-    if (!this.state) {
+    if (!this.threadState) {
       return;
     }
 
@@ -72,8 +72,8 @@
     relatedStates.prev = (
       await getThreadStateFromConstraints(this.trace.engine, {
         filters: [
-          `ts + dur = ${this.state.ts}`,
-          `utid = ${this.state.thread?.utid}`,
+          `ts + dur = ${this.threadState.ts}`,
+          `utid = ${this.threadState.thread?.utid}`,
         ],
         limit: 1,
       })
@@ -81,22 +81,30 @@
     relatedStates.next = (
       await getThreadStateFromConstraints(this.trace.engine, {
         filters: [
-          `ts = ${this.state.ts + this.state.dur}`,
-          `utid = ${this.state.thread?.utid}`,
+          `ts = ${this.threadState.ts + this.threadState.dur}`,
+          `utid = ${this.threadState.thread?.utid}`,
         ],
         limit: 1,
       })
     )[0];
-    if (this.state.wakerId !== undefined) {
+    if (this.threadState.wakerId !== undefined) {
       relatedStates.waker = await getThreadState(
         this.trace.engine,
-        this.state.wakerId,
+        this.threadState.wakerId,
+      );
+    } else if (
+      this.threadState.state == 'Running' &&
+      relatedStates.prev.wakerId != undefined
+    ) {
+      relatedStates.waker = await getThreadState(
+        this.trace.engine,
+        relatedStates.prev.wakerId,
       );
     }
     // note: this might be valid even if there is no |waker| slice, in the case
     // of an interrupt wakeup while in the idle process (which is omitted from
     // the thread_state table).
-    relatedStates.wakerInterruptCtx = this.state.wakerInterruptCtx;
+    relatedStates.wakerInterruptCtx = this.threadState.wakerInterruptCtx;
 
     relatedStates.wakee = await getThreadStateFromConstraints(
       this.trace.engine,
@@ -121,7 +129,7 @@
         m(
           Section,
           {title: 'Details'},
-          this.state && this.renderTree(this.state),
+          this.threadState && this.renderTree(this.threadState),
         ),
         m(
           Section,
@@ -133,33 +141,37 @@
   }
 
   private renderLoadingText() {
-    if (!this.state) {
+    if (!this.threadState) {
       return 'Loading';
     }
     return this.id;
   }
 
-  private renderTree(state: ThreadState) {
-    const thread = state.thread;
-    const process = state.thread?.process;
+  private renderTree(threadState: ThreadState) {
+    const thread = threadState.thread;
+    const process = threadState.thread?.process;
     return m(
       Tree,
       m(TreeNode, {
         left: 'Start time',
-        right: m(Timestamp, {ts: state.ts}),
+        right: m(Timestamp, {ts: threadState.ts}),
       }),
       m(TreeNode, {
         left: 'Duration',
-        right: m(DurationWidget, {dur: state.dur}),
+        right: m(DurationWidget, {dur: threadState.dur}),
       }),
       m(TreeNode, {
         left: 'State',
-        right: this.renderState(state.state, state.cpu, state.schedSqlId),
+        right: this.renderState(
+          threadState.state,
+          threadState.cpu,
+          threadState.schedSqlId,
+        ),
       }),
-      state.blockedFunction &&
+      threadState.blockedFunction &&
         m(TreeNode, {
           left: 'Blocked function',
-          right: state.blockedFunction,
+          right: threadState.blockedFunction,
         }),
       process &&
         m(TreeNode, {
@@ -167,9 +179,14 @@
           right: getProcessName(process),
         }),
       thread && m(TreeNode, {left: 'Thread', right: getThreadName(thread)}),
+      threadState.priority !== undefined &&
+        m(TreeNode, {
+          left: 'Priority',
+          right: threadState.priority,
+        }),
       m(TreeNode, {
         left: 'SQL ID',
-        right: m(SqlRef, {table: 'thread_state', id: state.threadStateSqlId}),
+        right: m(SqlRef, {table: 'thread_state', id: threadState.id}),
       }),
     );
   }
@@ -197,18 +214,18 @@
   }
 
   private renderRelatedThreadStates(): m.Children {
-    if (this.state === undefined || this.relatedStates === undefined) {
+    if (this.threadState === undefined || this.relatedStates === undefined) {
       return 'Loading';
     }
-    const startTs = this.state.ts;
+    const startTs = this.threadState.ts;
     const renderRef = (state: ThreadState, name?: string) =>
       m(ThreadStateRef, {
-        id: state.threadStateSqlId,
+        id: state.id,
         name,
       });
 
-    const nameForNextOrPrev = (state: ThreadState) =>
-      `${state.state} for ${renderDuration(state.dur)}`;
+    const nameForNextOrPrev = (threadState: ThreadState) =>
+      `${threadState.state} for ${renderDuration(threadState.dur)}`;
 
     const renderWaker = (related: RelatedThreadStates) => {
       // Could be absent if:
@@ -294,7 +311,7 @@
           onclick: () => {
             this.trace.commands.runCommand(
               CRITICAL_PATH_LITE_CMD,
-              this.state?.thread?.utid,
+              this.threadState?.thread?.utid,
             );
           },
         }),
@@ -305,7 +322,7 @@
           onclick: () => {
             this.trace.commands.runCommand(
               CRITICAL_PATH_CMD,
-              this.state?.thread?.utid,
+              this.threadState?.thread?.utid,
             );
           },
         }),
@@ -313,6 +330,6 @@
   }
 
   isLoading() {
-    return this.state === undefined || this.relatedStates === undefined;
+    return this.threadState === undefined || this.relatedStates === undefined;
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index 0b5a2e3..7bbcace 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -58,6 +58,7 @@
 import {Chip, ChipBar} from '../../widgets/chip';
 import {TrackWidget} from '../../widgets/track_widget';
 import {scheduleFullRedraw} from '../../widgets/raf';
+import {CopyableLink} from '../../widgets/copyable_link';
 
 const DATA_ENGLISH_LETTER_FREQUENCY = {
   table: [
@@ -782,6 +783,17 @@
         },
       }),
       m(WidgetShowcase, {
+        label: 'CopyableLink',
+        renderWidget: ({noicon}) =>
+          m(CopyableLink, {
+            noicon: arg(noicon, true),
+            url: 'https://perfetto.dev/docs/',
+          }),
+        initialOpts: {
+          noicon: false,
+        },
+      }),
+      m(WidgetShowcase, {
         label: 'Table',
         renderWidget: () => m(TableShowcase),
         initialOpts: {},
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index 57bba08..50def57 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -20,6 +20,7 @@
 import {PluginManager} from './plugin';
 import {Trace} from './trace';
 import {PageManager} from './page';
+import {FeatureFlagManager} from './feature_flag';
 
 /**
  * The API endpoint to interact programmaticaly with the UI before a trace has
@@ -37,6 +38,7 @@
   readonly analytics: Analytics;
   readonly plugins: PluginManager;
   readonly pages: PageManager;
+  readonly featureFlags: FeatureFlagManager;
 
   /**
    * The parsed querystring passed when starting the app, before any navigation
@@ -58,4 +60,12 @@
    * Navigate to a new page.
    */
   navigate(newHash: string): void;
+
+  openTraceFromFile(file: File): void;
+  openTraceFromUrl(url: string): void;
+  openTraceFromBuffer(args: {
+    buffer: ArrayBuffer;
+    title: string;
+    fileName: string;
+  }): void;
 }
diff --git a/ui/src/public/feature_flag.ts b/ui/src/public/feature_flag.ts
new file mode 100644
index 0000000..c82d38a
--- /dev/null
+++ b/ui/src/public/feature_flag.ts
@@ -0,0 +1,64 @@
+// 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.
+
+export interface FeatureFlagManager {
+  register(settings: FlagSettings): Flag;
+}
+
+export interface FlagSettings {
+  id: string;
+  defaultValue: boolean;
+  description: string;
+  name?: string;
+  devOnly?: boolean;
+}
+
+export interface Flag {
+  // A unique identifier for this flag ("magicSorting")
+  readonly id: string;
+
+  // The name of the flag the user sees ("New track sorting algorithm")
+  readonly name: string;
+
+  // A longer description which is displayed to the user.
+  // "Sort tracks using an embedded tfLite model based on your expression
+  // while waiting for the trace to load."
+  readonly description: string;
+
+  // Whether the flag defaults to true or false.
+  // If !flag.isOverridden() then flag.get() === flag.defaultValue
+  readonly defaultValue: boolean;
+
+  // Get the current value of the flag.
+  get(): boolean;
+
+  // Override the flag and persist the new value.
+  set(value: boolean): void;
+
+  // If the flag has been overridden.
+  // Note: A flag can be overridden to its default value.
+  isOverridden(): boolean;
+
+  // Reset the flag to its default setting.
+  reset(): void;
+
+  // Get the current state of the flag.
+  overriddenState(): OverrideState;
+}
+
+export enum OverrideState {
+  DEFAULT = 'DEFAULT',
+  TRUE = 'OVERRIDE_TRUE',
+  FALSE = 'OVERRIDE_FALSE',
+}
diff --git a/ui/src/public/trace.ts b/ui/src/public/trace.ts
index 6e60c79..95db546 100644
--- a/ui/src/public/trace.ts
+++ b/ui/src/public/trace.ts
@@ -96,3 +96,5 @@
 export interface TraceAttrs {
   trace: Trace;
 }
+
+export const TRACE_SUFFIX = '.perfetto-trace';
diff --git a/ui/src/trace_processor/sql_utils/thread_state.ts b/ui/src/trace_processor/sql_utils/thread_state.ts
index 92834fe..cc10a5f 100644
--- a/ui/src/trace_processor/sql_utils/thread_state.ts
+++ b/ui/src/trace_processor/sql_utils/thread_state.ts
@@ -85,7 +85,7 @@
 // Single thread state slice, corresponding to a row of |thread_slice| table.
 export interface ThreadState {
   // Id into |thread_state| table.
-  threadStateSqlId: ThreadStateSqlId;
+  id: ThreadStateSqlId;
   // Id of the corresponding entry in the |sched| table.
   schedSqlId?: SchedSqlId;
   // Timestamp of the beginning of this thread state in nanoseconds.
@@ -108,6 +108,8 @@
   // unset even for runnable states, if the trace was recorded without
   // interrupt information.
   wakerInterruptCtx?: boolean;
+  // Kernel priority of this thread state.
+  priority?: number;
 }
 
 // Gets a list of thread state objects from Trace Processor with given
@@ -117,61 +119,63 @@
   constraints: SQLConstraints,
 ): Promise<ThreadState[]> {
   const query = await engine.query(`
-    SELECT
-      thread_state.id as threadStateSqlId,
-      (select sched.id
-        from sched
-        where sched.ts=thread_state.ts and sched.utid=thread_state.utid
-        limit 1
-       ) as schedSqlId,
-      ts,
-      thread_state.dur as dur,
-      thread_state.cpu as cpu,
-      state,
-      thread_state.blocked_function as blockedFunction,
-      io_wait as ioWait,
-      thread_state.utid as utid,
-      waker_utid as wakerUtid,
-      waker_id as wakerId,
-      irq_context as wakerInterruptCtx
-    FROM thread_state
+    WITH raw AS (
+      SELECT
+      ts.id,
+      sched.id AS sched_id,
+      ts.ts,
+      ts.dur,
+      ts.cpu,
+      ts.state,
+      ts.blocked_function,
+      ts.io_wait,
+      ts.utid,
+      ts.waker_utid,
+      ts.waker_id,
+      ts.irq_context,
+      sched.priority
+    FROM thread_state ts
+    LEFT JOIN sched USING (utid, ts)
+    )
+    SELECT * FROM raw
+
     ${constraintsToQuerySuffix(constraints)}`);
   const it = query.iter({
-    threadStateSqlId: NUM,
-    schedSqlId: NUM_NULL,
+    id: NUM,
+    sched_id: NUM_NULL,
     ts: LONG,
     dur: LONG,
     cpu: NUM_NULL,
     state: STR_NULL,
-    blockedFunction: STR_NULL,
-    ioWait: NUM_NULL,
+    blocked_function: STR_NULL,
+    io_wait: NUM_NULL,
     utid: NUM,
-    wakerUtid: NUM_NULL,
-    wakerId: NUM_NULL,
-    wakerInterruptCtx: NUM_NULL,
+    waker_utid: NUM_NULL,
+    waker_id: NUM_NULL,
+    irq_context: NUM_NULL,
+    priority: NUM_NULL,
   });
 
   const result: ThreadState[] = [];
 
   for (; it.valid(); it.next()) {
-    const ioWait = it.ioWait === null ? undefined : it.ioWait > 0;
+    const ioWait = it.io_wait === null ? undefined : it.io_wait > 0;
 
-    // TODO(altimin): Consider fetcing thread / process info using a single
+    // TODO(altimin): Consider fetching thread / process info using a single
     // query instead of one per row.
     result.push({
-      threadStateSqlId: it.threadStateSqlId as ThreadStateSqlId,
-      schedSqlId: fromNumNull(it.schedSqlId) as SchedSqlId | undefined,
+      id: it.id as ThreadStateSqlId,
+      schedSqlId: fromNumNull(it.sched_id) as SchedSqlId | undefined,
       ts: Time.fromRaw(it.ts),
       dur: it.dur,
       cpu: fromNumNull(it.cpu),
       state: translateState(it.state ?? undefined, ioWait),
-      blockedFunction: it.blockedFunction ?? undefined,
+      blockedFunction: it.blocked_function ?? undefined,
       thread: await getThreadInfo(engine, asUtid(it.utid)),
-      wakerUtid: asUtid(it.wakerUtid ?? undefined),
-      wakerId: asThreadStateSqlId(it.wakerId ?? undefined),
-      wakerInterruptCtx: fromNumNull(it.wakerInterruptCtx) as
-        | boolean
-        | undefined,
+      wakerUtid: asUtid(it.waker_id ?? undefined),
+      wakerId: asThreadStateSqlId(it.waker_id ?? undefined),
+      wakerInterruptCtx: fromNumNull(it.irq_context) as boolean | undefined,
+      priority: fromNumNull(it.priority),
     });
   }
   return result;
diff --git a/ui/src/widgets/copyable_link.ts b/ui/src/widgets/copyable_link.ts
new file mode 100644
index 0000000..9244107
--- /dev/null
+++ b/ui/src/widgets/copyable_link.ts
@@ -0,0 +1,46 @@
+// 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 m from 'mithril';
+import {copyToClipboard} from '../base/clipboard';
+import {Anchor} from './anchor';
+
+interface CopyableLinkAttrs {
+  url: string;
+  text?: string; // Will use url if omitted.
+  noicon?: boolean;
+}
+
+export class CopyableLink implements m.ClassComponent<CopyableLinkAttrs> {
+  view({attrs}: m.CVnode<CopyableLinkAttrs>) {
+    const url = attrs.url;
+    return m(
+      'div',
+      m(
+        Anchor,
+        {
+          href: url,
+          title: 'Click to copy the URL into the clipboard',
+          target: '_blank',
+          icon: attrs.noicon ? undefined : 'content_copy',
+          onclick: (e: Event) => {
+            e.preventDefault();
+            copyToClipboard(url);
+          },
+        },
+        attrs.text ?? url,
+      ),
+    );
+  }
+}
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,
   });
 }