Merge changes I40c4d184,I5d9a9e7b into main

* changes:
  shared_lib: autogenerate required proto headers
  Protozero generator for C
diff --git a/Android.bp b/Android.bp
index 081ab9b..d668c2b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -4589,6 +4589,7 @@
     srcs: [
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
@@ -4669,6 +4670,7 @@
     srcs: [
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
@@ -4732,6 +4734,7 @@
     srcs: [
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
@@ -10431,6 +10434,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_camera.sql",
         "src/trace_processor/metrics/sql/android/android_camera_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_cpu.sql",
@@ -10617,6 +10621,7 @@
         "src/trace_processor/perfetto_sql/engine/function_util.cc",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc",
+        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc",
         "src/trace_processor/perfetto_sql/engine/runtime_table_function.cc",
     ],
 }
@@ -10627,6 +10632,7 @@
     srcs: [
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc",
+        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc",
     ],
 }
 
diff --git a/BUILD b/BUILD
index ee2d513..42122bd 100644
--- a/BUILD
+++ b/BUILD
@@ -65,160 +65,6 @@
     linkstatic = True,
 )
 
-# GN target: //src/bigtrace:bigtrace
-perfetto_cc_library(
-    name = "bigtrace",
-    srcs = [
-        ":src_base_threading_threading",
-        ":src_bigtrace_sources",
-        ":src_kernel_utils_syscall_table",
-        ":src_protozero_proto_ring_buffer",
-        ":src_trace_processor_db_db",
-        ":src_trace_processor_db_overlays_overlays",
-        ":src_trace_processor_db_storage_storage",
-        ":src_trace_processor_export_json",
-        ":src_trace_processor_importers_android_bugreport_android_bugreport",
-        ":src_trace_processor_importers_common_common",
-        ":src_trace_processor_importers_common_parser_types",
-        ":src_trace_processor_importers_common_trace_parser_hdr",
-        ":src_trace_processor_importers_ftrace_ftrace_descriptors",
-        ":src_trace_processor_importers_ftrace_full",
-        ":src_trace_processor_importers_ftrace_minimal",
-        ":src_trace_processor_importers_fuchsia_fuchsia_record",
-        ":src_trace_processor_importers_fuchsia_full",
-        ":src_trace_processor_importers_fuchsia_minimal",
-        ":src_trace_processor_importers_gzip_full",
-        ":src_trace_processor_importers_i2c_full",
-        ":src_trace_processor_importers_json_full",
-        ":src_trace_processor_importers_json_minimal",
-        ":src_trace_processor_importers_memory_tracker_graph_processor",
-        ":src_trace_processor_importers_ninja_ninja",
-        ":src_trace_processor_importers_proto_full",
-        ":src_trace_processor_importers_proto_minimal",
-        ":src_trace_processor_importers_proto_packet_sequence_state_generation_hdr",
-        ":src_trace_processor_importers_proto_proto_importer_module",
-        ":src_trace_processor_importers_proto_winscope_full",
-        ":src_trace_processor_importers_syscalls_full",
-        ":src_trace_processor_importers_systrace_full",
-        ":src_trace_processor_importers_systrace_systrace_line",
-        ":src_trace_processor_importers_systrace_systrace_parser",
-        ":src_trace_processor_lib",
-        ":src_trace_processor_metatrace",
-        ":src_trace_processor_metrics_metrics",
-        ":src_trace_processor_perfetto_sql_engine_engine",
-        ":src_trace_processor_perfetto_sql_intrinsics_functions_functions",
-        ":src_trace_processor_perfetto_sql_intrinsics_functions_interface",
-        ":src_trace_processor_perfetto_sql_intrinsics_operators_operators",
-        ":src_trace_processor_perfetto_sql_intrinsics_table_functions_interface",
-        ":src_trace_processor_perfetto_sql_intrinsics_table_functions_table_functions",
-        ":src_trace_processor_perfetto_sql_intrinsics_table_functions_tables",
-        ":src_trace_processor_rpc_rpc",
-        ":src_trace_processor_sorter_sorter",
-        ":src_trace_processor_sqlite_query_constraints",
-        ":src_trace_processor_sqlite_sqlite",
-        ":src_trace_processor_storage_minimal",
-        ":src_trace_processor_storage_storage",
-        ":src_trace_processor_tables_tables",
-        ":src_trace_processor_tables_tables_python",
-        ":src_trace_processor_types_types",
-        ":src_trace_processor_util_bump_allocator",
-        ":src_trace_processor_util_descriptors",
-        ":src_trace_processor_util_glob",
-        ":src_trace_processor_util_gzip",
-        ":src_trace_processor_util_interned_message_view",
-        ":src_trace_processor_util_profile_builder",
-        ":src_trace_processor_util_proto_profiler",
-        ":src_trace_processor_util_proto_to_args_parser",
-        ":src_trace_processor_util_protozero_to_json",
-        ":src_trace_processor_util_protozero_to_text",
-        ":src_trace_processor_util_regex",
-        ":src_trace_processor_util_sql_argument",
-        ":src_trace_processor_util_stack_traces_util",
-        ":src_trace_processor_util_stdlib",
-        ":src_trace_processor_util_util",
-        ":src_trace_processor_util_zip_reader",
-        ":src_trace_processor_views_views",
-    ],
-    hdrs = [
-        ":include_perfetto_base_base",
-        ":include_perfetto_ext_base_base",
-        ":include_perfetto_ext_base_threading_threading",
-        ":include_perfetto_ext_bigtrace_bigtrace",
-        ":include_perfetto_ext_trace_processor_demangle",
-        ":include_perfetto_ext_trace_processor_export_json",
-        ":include_perfetto_ext_trace_processor_importers_memory_tracker_memory_tracker",
-        ":include_perfetto_ext_trace_processor_rpc_query_result_serializer",
-        ":include_perfetto_ext_traced_sys_stats_counters",
-        ":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",
-    ],
-    deps = [
-               ":protos_perfetto_bigtrace_lite",
-               ":protos_perfetto_common_lite",
-               ":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_zero",
-               ":protos_perfetto_trace_chrome_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_processor_lite",
-               ":protos_perfetto_trace_processor_metrics_impl_zero",
-               ":protos_perfetto_trace_processor_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_base_base",
-               ":src_base_version",
-               ":src_trace_processor_containers_containers",
-               ":src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_config_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_statsd_atoms_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_trace_descriptor",
-               ":src_trace_processor_importers_proto_gen_cc_track_event_descriptor",
-               ":src_trace_processor_importers_proto_winscope_gen_cc_winscope_descriptor",
-               ":src_trace_processor_metrics_gen_cc_all_chrome_metrics_descriptor",
-               ":src_trace_processor_metrics_gen_cc_all_webview_metrics_descriptor",
-               ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
-               ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
-               ":src_trace_processor_perfetto_sql_prelude_prelude",
-               ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
-           PERFETTO_CONFIG.deps.sqlite +
-           PERFETTO_CONFIG.deps.sqlite_ext_percentile +
-           PERFETTO_CONFIG.deps.zlib +
-           PERFETTO_CONFIG.deps.demangle_wrapper,
-    linkstatic = True,
-)
-
 # GN target: //src/ipc/protoc_plugin:ipc_plugin
 perfetto_cc_binary(
     name = "ipc_plugin",
@@ -723,22 +569,6 @@
     ],
 )
 
-# GN target: //include/perfetto/ext/base/threading:threading
-perfetto_filegroup(
-    name = "include_perfetto_ext_base_threading_threading",
-    srcs = [
-        "include/perfetto/ext/base/threading/channel.h",
-        "include/perfetto/ext/base/threading/future.h",
-        "include/perfetto/ext/base/threading/future_combinators.h",
-        "include/perfetto/ext/base/threading/poll.h",
-        "include/perfetto/ext/base/threading/spawn.h",
-        "include/perfetto/ext/base/threading/stream.h",
-        "include/perfetto/ext/base/threading/stream_combinators.h",
-        "include/perfetto/ext/base/threading/thread_pool.h",
-        "include/perfetto/ext/base/threading/util.h",
-    ],
-)
-
 # GN target: //include/perfetto/ext/base:base
 perfetto_filegroup(
     name = "include_perfetto_ext_base_base",
@@ -798,16 +628,6 @@
     ],
 )
 
-# GN target: //include/perfetto/ext/bigtrace:bigtrace
-perfetto_filegroup(
-    name = "include_perfetto_ext_bigtrace_bigtrace",
-    srcs = [
-        "include/perfetto/ext/bigtrace/environment.h",
-        "include/perfetto/ext/bigtrace/orchestrator.h",
-        "include/perfetto/ext/bigtrace/worker.h",
-    ],
-)
-
 # GN target: //include/perfetto/ext/ipc:ipc
 perfetto_filegroup(
     name = "include_perfetto_ext_ipc_ipc",
@@ -1129,16 +949,6 @@
     linkstatic = True,
 )
 
-# GN target: //src/base/threading:threading
-perfetto_filegroup(
-    name = "src_base_threading_threading",
-    srcs = [
-        "src/base/threading/spawn.cc",
-        "src/base/threading/stream_combinators.cc",
-        "src/base/threading/thread_pool.cc",
-    ],
-)
-
 # GN target: //src/base:base
 perfetto_cc_library(
     name = "src_base_base",
@@ -1231,19 +1041,6 @@
     ],
 )
 
-# GN target: //src/bigtrace:sources
-perfetto_filegroup(
-    name = "src_bigtrace_sources",
-    srcs = [
-        "src/bigtrace/orchestrator_impl.cc",
-        "src/bigtrace/orchestrator_impl.h",
-        "src/bigtrace/trace_processor_wrapper.cc",
-        "src/bigtrace/trace_processor_wrapper.h",
-        "src/bigtrace/worker_impl.cc",
-        "src/bigtrace/worker_impl.h",
-    ],
-)
-
 # GN target: //src/ipc:client
 perfetto_filegroup(
     name = "src_ipc_client",
@@ -1994,6 +1791,7 @@
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
         "src/trace_processor/metrics/sql/android/android_blocking_calls_cuj_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_boot.sql",
         "src/trace_processor/metrics/sql/android/android_camera.sql",
         "src/trace_processor/metrics/sql/android/android_camera_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_cpu.sql",
@@ -2263,6 +2061,8 @@
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc",
         "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h",
+        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc",
+        "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h",
         "src/trace_processor/perfetto_sql/engine/runtime_table_function.cc",
         "src/trace_processor/perfetto_sql/engine/runtime_table_function.h",
     ],
@@ -3605,41 +3405,50 @@
     ],
 )
 
-# GN target: //protos/perfetto/bigtrace:lite
-perfetto_cc_proto_library(
-    name = "protos_perfetto_bigtrace_lite",
-    deps = [
-        ":protos_perfetto_bigtrace_protos",
-    ],
-)
-
-# GN target: //protos/perfetto/bigtrace:source_set
+# GN target: [//protos/perfetto/trace_processor:source_set]
 perfetto_proto_library(
-    name = "protos_perfetto_bigtrace_protos",
-    srcs = [
-        "protos/perfetto/bigtrace/orchestrator.proto",
-        "protos/perfetto/bigtrace/worker.proto",
-    ],
-    visibility = [
-        PERFETTO_CONFIG.proto_library_visibility,
-    ],
+    name = "trace_processor_proto",
     deps = [
         ":protos_perfetto_common_protos",
         ":protos_perfetto_trace_processor_protos",
     ],
 )
 
-# GN target: //protos/perfetto/common:cpp
-perfetto_cc_protocpp_library(
-    name = "protos_perfetto_common_cpp",
+# GN target: [//protos/perfetto/trace_processor:source_set]
+perfetto_cc_proto_library(
+    name = "trace_processor_cc_proto",
     deps = [
-        ":protos_perfetto_common_protos",
+        ":trace_processor_proto",
     ],
 )
 
-# GN target: //protos/perfetto/common:lite
-perfetto_cc_proto_library(
-    name = "protos_perfetto_common_lite",
+# GN target: [//protos/perfetto/trace_processor:source_set]
+perfetto_java_proto_library(
+    name = "trace_processor_java_proto",
+    deps = [
+        ":trace_processor_proto",
+    ],
+)
+
+# GN target: [//protos/perfetto/trace_processor:source_set]
+perfetto_java_lite_proto_library(
+    name = "trace_processor_java_proto_lite",
+    deps = [
+        ":trace_processor_proto",
+    ],
+)
+
+# GN target: [//protos/perfetto/trace_processor:source_set]
+perfetto_py_proto_library(
+    name = "trace_processor_py_pb2",
+    deps = [
+        ":trace_processor_proto",
+    ],
+)
+
+# GN target: //protos/perfetto/common:cpp
+perfetto_cc_protocpp_library(
+    name = "protos_perfetto_common_cpp",
     deps = [
         ":protos_perfetto_common_protos",
     ],
@@ -4223,6 +4032,7 @@
     srcs = [
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
+        "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
         "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
@@ -4823,14 +4633,6 @@
     ],
 )
 
-# GN target: //protos/perfetto/trace_processor:lite
-perfetto_cc_proto_library(
-    name = "protos_perfetto_trace_processor_lite",
-    deps = [
-        ":protos_perfetto_trace_processor_protos",
-    ],
-)
-
 # GN target: //protos/perfetto/trace_processor:metrics_impl_source_set
 perfetto_proto_library(
     name = "protos_perfetto_trace_processor_metrics_impl_protos",
diff --git a/BUILD.gn b/BUILD.gn
index cff5360..0a93081 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -39,6 +39,9 @@
   if (enable_perfetto_traced_probes) {
     all_targets += [ "src/traced/probes:traced_probes" ]
   }
+  if (enable_perfetto_traced_relay) {
+    all_targets += [ "src/traced_relay:traced_relay" ]
+  }
 }
 
 if (enable_perfetto_trace_processor && enable_perfetto_trace_processor_sqlite) {
diff --git a/CHANGELOG b/CHANGELOG
index 9b27861..bec63e0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -9,6 +9,29 @@
     *
 
 
+v38.0 - 2023-10-10:
+  Tracing service and probes:
+    * Added capability to transfer and clear buffers on CLONE_SNAPSHOT.
+    * Added new service for relaying IPC messages from local producers to a
+      remote tracing instance.
+  Trace Processor:
+    * Added new PerfettoSQL syntax (INCLUDE PERFETTO MODULE) for including
+      tables/views/functions defined in SQL modules.
+    * Added new PerfettoSQL syntax (CREATE PERFETTO TABLE) for defining analytic
+      tables in SQL.
+    * Added new PerfettoSQL syntax (CREATE PERFETTO MACRO) for defining macros
+      in SQL.
+    * Added TO_REALTIME function to convert timestamps to the realtime clock.
+    * Added support for parsing binder_command and binder_return events.
+  UI:
+    * Added support for zooming when using deep-links.
+    * Added track for displaying screenshots in traces.
+    * Added support for displaying UTC timestamps.
+    * Added capability to list, search and debug plugin tracks.
+    * Added plugins with commands for pinning tracks for latency and large
+      screen debugging in Android.
+
+
 v37.0 - 2023-08-10:
   Tracing service and probes:
     * Fixed a bug which would cause sessions cloned with CLONE_SNAPSHOT to not
diff --git a/gn/perfetto.gni b/gn/perfetto.gni
index b23aa06..ecc7252 100644
--- a/gn/perfetto.gni
+++ b/gn/perfetto.gni
@@ -266,6 +266,10 @@
   # extremely low.
   enable_perfetto_traced_probes = enable_perfetto_platform_services && !is_win
 
+  # The relay service is enabled when platform services are enabled.
+  # TODO(chinglinyu) check if we can enable on Windows.
+  enable_perfetto_traced_relay = enable_perfetto_platform_services && !is_win
+
   # Whether info-level logging is enabled.
   perfetto_verbose_logs_enabled =
       !build_with_chromium || perfetto_force_dlog == "on"
diff --git a/include/perfetto/ext/base/file_utils.h b/include/perfetto/ext/base/file_utils.h
index d2412d0..42e9a28e 100644
--- a/include/perfetto/ext/base/file_utils.h
+++ b/include/perfetto/ext/base/file_utils.h
@@ -93,6 +93,11 @@
 base::Status ListFilesRecursive(const std::string& dir_path,
                                 std::vector<std::string>& output);
 
+// Sets |path|'s owner group to |group_name| and permission mode bits to
+// |mode_bits|.
+base::Status SetFilePermissions(const std::string& path,
+                                const std::string& group_name,
+                                const std::string& mode_bits);
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/include/perfetto/ext/tracing/ipc/default_socket.h b/include/perfetto/ext/tracing/ipc/default_socket.h
index c173ebf..86d7b78 100644
--- a/include/perfetto/ext/tracing/ipc/default_socket.h
+++ b/include/perfetto/ext/tracing/ipc/default_socket.h
@@ -31,6 +31,11 @@
     const char* producer_socket_names);
 PERFETTO_EXPORT_COMPONENT const char* GetProducerSocket();
 
+// Optionally returns the relay socket name (nullable). The relay socket is used
+// for forwarding the IPC messages between the local producers and the remote
+// tracing service.
+PERFETTO_EXPORT_COMPONENT const char* GetRelaySocket();
+
 }  // namespace perfetto
 
 #endif  // INCLUDE_PERFETTO_EXT_TRACING_IPC_DEFAULT_SOCKET_H_
diff --git a/include/perfetto/protozero/message.h b/include/perfetto/protozero/message.h
index efcdf9f..760da54 100644
--- a/include/perfetto/protozero/message.h
+++ b/include/perfetto/protozero/message.h
@@ -65,6 +65,9 @@
   // all nested messages) and seals the message. Returns the size of the message
   // (and all nested sub-messages), without taking into account any chunking.
   // Finalize is idempotent and can be called several times w/o side effects.
+  // Short messages may be compacted in memory into the size field, since their
+  // size can be represented with fewer than
+  // proto_utils::kMessageLengthFieldSize bytes.
   uint32_t Finalize();
 
   // Optional. If is_valid() == true, the corresponding memory region (its
@@ -78,11 +81,16 @@
   // This is to deal with case of backfilling the size of a root (non-nested)
   // message which is split into multiple chunks. Upon finalization only the
   // partial size that lies in the last chunk has to be backfilled.
-  void inc_size_already_written(uint32_t sz) { size_already_written_ += sz; }
+  void inc_size_already_written(uint32_t sz) {
+    PERFETTO_DCHECK(!is_finalized());
+    size_already_written_ += sz;
+  }
 
   Message* nested_message() { return nested_message_; }
 
-  bool is_finalized() const { return finalized_; }
+  bool is_finalized() const {
+    return message_state_ != MessageState::kNotFinalized;
+  }
 
 #if PERFETTO_DCHECK_IS_ON()
   void set_handle(MessageHandleBase* handle) { handle_ = handle; }
@@ -197,7 +205,7 @@
   void EndNestedMessage();
 
   void WriteToStream(const uint8_t* src_begin, const uint8_t* src_end) {
-    PERFETTO_DCHECK(!finalized_);
+    PERFETTO_DCHECK(!is_finalized());
     PERFETTO_DCHECK(src_begin <= src_end);
     const uint32_t size = static_cast<uint32_t>(src_end - src_begin);
     stream_writer_->WriteBytes(src_begin, size);
@@ -234,9 +242,19 @@
   // See comment for inc_size_already_written().
   uint32_t size_already_written_;
 
-  // When true, no more changes to the message are allowed. This is to DCHECK
-  // attempts of writing to a message which has been Finalize()-d.
-  bool finalized_;
+  enum class MessageState : uint8_t {
+    // Message is still being written to.
+    kNotFinalized,
+    // Finalized, no more changes to the message are allowed. This is to DCHECK
+    // attempts of writing to a message which has been Finalize()-d.
+    kFinalized,
+    // Finalized, and additionally the message data has been partially or fully
+    // compacted into the last 3 bytes of `size_field_`. See the comment in
+    // Finalize().
+    kFinalizedWithCompaction,
+  };
+
+  MessageState message_state_;
 
 #if PERFETTO_DCHECK_IS_ON()
   // Current generation of message. Incremented on Reset.
diff --git a/include/perfetto/protozero/proto_utils.h b/include/perfetto/protozero/proto_utils.h
index 98c10d3..b961bee 100644
--- a/include/perfetto/protozero/proto_utils.h
+++ b/include/perfetto/protozero/proto_utils.h
@@ -120,6 +120,7 @@
 // Maximum message size supported: 256 MiB (4 x 7-bit due to varint encoding).
 constexpr size_t kMessageLengthFieldSize = 4;
 constexpr size_t kMaxMessageLength = (1u << (kMessageLengthFieldSize * 7)) - 1;
+constexpr size_t kMaxOneByteMessageLength = (1 << 7) - 1;
 
 // Field tag is encoded as 32-bit varint (5 bytes at most).
 // Largest value of simple (not length-delimited) field is 64-bit varint
diff --git a/include/perfetto/protozero/scattered_stream_writer.h b/include/perfetto/protozero/scattered_stream_writer.h
index 40a1c66..96329e9 100644
--- a/include/perfetto/protozero/scattered_stream_writer.h
+++ b/include/perfetto/protozero/scattered_stream_writer.h
@@ -24,6 +24,7 @@
 
 #include "perfetto/base/compiler.h"
 #include "perfetto/base/export.h"
+#include "perfetto/base/logging.h"
 #include "perfetto/protozero/contiguous_memory_range.h"
 
 namespace protozero {
@@ -114,6 +115,20 @@
     return begin;
   }
 
+  // Shifts the previously written `size` bytes backwards in memory by `offset`
+  // bytes, moving the write pointer back accordingly. The shifted result must
+  // still be fully contained by the current range.
+  void Rewind(size_t size, size_t offset) {
+    uint8_t* src = write_ptr_ - size;
+    uint8_t* dst = src - offset;
+    PERFETTO_DCHECK(src >= cur_range_.begin);
+    PERFETTO_DCHECK(src + size <= cur_range_.end);
+    PERFETTO_DCHECK(dst >= cur_range_.begin);
+    PERFETTO_DCHECK(dst + size <= cur_range_.end);
+    memmove(dst, src, size);
+    write_ptr_ -= offset;
+  }
+
   // Resets the buffer boundaries and the write pointer to the given |range|.
   // Subsequent WriteByte(s) will write into |range|.
   void Reset(ContiguousMemoryRange range);
diff --git a/include/perfetto/trace_processor/metatrace_config.h b/include/perfetto/trace_processor/metatrace_config.h
index b89a61e..adcfa41 100644
--- a/include/perfetto/trace_processor/metatrace_config.h
+++ b/include/perfetto/trace_processor/metatrace_config.h
@@ -16,25 +16,46 @@
 #define INCLUDE_PERFETTO_TRACE_PROCESSOR_METATRACE_CONFIG_H_
 
 #include <cstddef>
+#include <cstdint>
 
 namespace perfetto {
 namespace trace_processor {
 namespace metatrace {
 
-enum MetatraceCategories {
-  TOPLEVEL = 1 << 0,
-  QUERY = 1 << 1,
-  FUNCTION = 1 << 2,
+enum MetatraceCategories : uint32_t {
+  // Category for low-frequency events which provide a high-level timeline of
+  // SQL query execution.
+  QUERY_TIMELINE = 1 << 0,
+
+  // Category for high-frequency events which provide details about SQL query
+  // execution.
+  QUERY_DETAILED = 1 << 1,
+
+  // Category for high-frequency events which provide details about SQL function
+  // calls.
+  FUNCTION_CALL = 1 << 2,
+
+  // Category for high-frequency events which provide details about the columnar
+  // database operations.
   DB = 1 << 3,
 
+  // Category for low-frequency events which provide a high-level timeline of
+  // SQL query execution.
+  API_TIMELINE = 1 << 4,
+
+  // Alias for turning off all other categories.
   NONE = 0,
-  ALL = TOPLEVEL | QUERY | FUNCTION | DB,
+
+  // Alias for turning on all other categories.
+  ALL = QUERY_TIMELINE | QUERY_DETAILED | FUNCTION_CALL | DB | API_TIMELINE,
 };
 
 struct MetatraceConfig {
   MetatraceConfig();
 
-  MetatraceCategories categories = MetatraceCategories::TOPLEVEL;
+  MetatraceCategories categories = static_cast<MetatraceCategories>(
+      MetatraceCategories::QUERY_TIMELINE | MetatraceCategories::API_TIMELINE);
+
   // Requested buffer size. The implemenation may choose to allocate a larger
   // buffer size for efficiency.
   size_t override_buffer_size = 0;
diff --git a/include/perfetto/tracing/tracing.h b/include/perfetto/tracing/tracing.h
index f0c515f..44503ba 100644
--- a/include/perfetto/tracing/tracing.h
+++ b/include/perfetto/tracing/tracing.h
@@ -210,30 +210,8 @@
 
   // Start a new tracing session using the given tracing backend. Use
   // |kUnspecifiedBackend| to select an available backend automatically.
-  static inline std::unique_ptr<TracingSession> NewTrace(
-      BackendType backend = kUnspecifiedBackend) PERFETTO_ALWAYS_INLINE {
-    // This code is inlined to allow dead-code elimination for unused consumer
-    // implementation. The logic behind it is the following:
-    // Nothing other than the code below references the GetInstance() method
-    // below. From a linker-graph viewpoint, those GetInstance() pull in many
-    // other pieces of the codebase (ConsumerOnlySystemTracingBackend pulls
-    // ConsumerIPCClient). Due to the inline, the compiler can see through the
-    // code and realize that some branches are always not taken. When that
-    // happens, no reference to the backends' GetInstance() is emitted and that
-    // allows the linker GC to get rid of the entire set of dependencies.
-    TracingConsumerBackend* (*system_backend_factory)();
-    system_backend_factory = nullptr;
-    // In case PERFETTO_IPC is disabled, a fake system backend is used, which
-    // always panics. NewTrace(kSystemBackend) should fail if PERFETTO_IPC is
-    // diabled, not panic.
-#if PERFETTO_BUILDFLAG(PERFETTO_IPC)
-    if (backend & kSystemBackend) {
-      system_backend_factory =
-          &internal::SystemConsumerTracingBackend::GetInstance;
-    }
-#endif
-    return NewTraceInternal(backend, system_backend_factory);
-  }
+  static PERFETTO_ALWAYS_INLINE inline std::unique_ptr<TracingSession> NewTrace(
+      BackendType backend = kUnspecifiedBackend);
 
   // Shut down Perfetto, releasing any allocated OS resources (threads, files,
   // sockets, etc.). Note that Perfetto cannot be reinitialized again in the
@@ -514,6 +492,31 @@
   virtual void AbortBlocking() = 0;
 };
 
+PERFETTO_ALWAYS_INLINE inline std::unique_ptr<TracingSession>
+Tracing::NewTrace(BackendType backend) {
+  // This code is inlined to allow dead-code elimination for unused consumer
+  // implementation. The logic behind it is the following:
+  // Nothing other than the code below references the GetInstance() method
+  // below. From a linker-graph viewpoint, those GetInstance() pull in many
+  // other pieces of the codebase (ConsumerOnlySystemTracingBackend pulls
+  // ConsumerIPCClient). Due to the inline, the compiler can see through the
+  // code and realize that some branches are always not taken. When that
+  // happens, no reference to the backends' GetInstance() is emitted and that
+  // allows the linker GC to get rid of the entire set of dependencies.
+  TracingConsumerBackend* (*system_backend_factory)();
+  system_backend_factory = nullptr;
+  // In case PERFETTO_IPC is disabled, a fake system backend is used, which
+  // always panics. NewTrace(kSystemBackend) should fail if PERFETTO_IPC is
+  // diabled, not panic.
+#if PERFETTO_BUILDFLAG(PERFETTO_IPC)
+  if (backend & kSystemBackend) {
+    system_backend_factory =
+        &internal::SystemConsumerTracingBackend::GetInstance;
+  }
+#endif
+  return NewTraceInternal(backend, system_backend_factory);
+}
+
 }  // namespace perfetto
 
 #endif  // INCLUDE_PERFETTO_TRACING_TRACING_H_
diff --git a/protos/perfetto/ipc/wire_protocol.proto b/protos/perfetto/ipc/wire_protocol.proto
index 038d4bb..8117316 100644
--- a/protos/perfetto/ipc/wire_protocol.proto
+++ b/protos/perfetto/ipc/wire_protocol.proto
@@ -65,6 +65,17 @@
   // Host -> Client.
   message RequestError { optional string error = 1; }
 
+  // Client (relay service) -> Host. This is generated by the relay service to
+  // fill the producer identity in the guest. This message is sent to the host
+  // service *before* any IPCFrame is from a local producer is relayed. This is
+  // accepted only on AF_VSOCK and AF_INET sockets, where we cannot validate the
+  // endpoont of the connection. for AF_UNIX sockets, this is ignored and traced
+  // uses instead the SO_PEERCRED.
+  message SetPeerIdentity {
+    optional int32 pid = 1;
+    optional int32 uid = 2;
+  }
+
   // The client is expected to send requests with monotonically increasing
   // request_id. The host will match the request_id sent from the client.
   // In the case of a Streaming response (has_more = true) the host will send
@@ -77,6 +88,7 @@
     InvokeMethod msg_invoke_method = 5;
     InvokeMethodReply msg_invoke_method_reply = 6;
     RequestError msg_request_error = 7;
+    SetPeerIdentity set_peer_identity = 8;
   }
 
   // Used only in unittests to generate a parsable message of arbitrary size.
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 74e7a72..a2ae264 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -22,6 +22,7 @@
   sources = [
     "android_blocking_call.proto",
     "android_blocking_calls_cuj_metric.proto",
+    "android_boot.proto",
     "android_frame_timeline_metric.proto",
     "android_sysui_notifications_blocking_calls_metric.proto",
     "android_trusty_workqueues.proto",
diff --git a/protos/perfetto/metrics/android/android_boot.proto b/protos/perfetto/metrics/android/android_boot.proto
new file mode 100644
index 0000000..820b4e0
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_boot.proto
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// This metric computes how much time processes spend in UNINTERRUPTIBLE_SLEEP state
+message ProcessStateDurations {
+  optional int64 total_dur = 2;
+  optional int64 uninterruptible_sleep_dur = 3;
+}
+
+message AndroidBootMetric {
+  optional ProcessStateDurations system_server_durations = 1;
+  optional ProcessStateDurations systemui_durations = 2;
+  optional ProcessStateDurations launcher_durations = 3;
+  optional ProcessStateDurations gms_durations = 4;
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 94c7666..95fd3b4 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -18,6 +18,7 @@
 
 package perfetto.protos;
 
+import "protos/perfetto/metrics/android/android_boot.proto";
 import "protos/perfetto/metrics/android/android_frame_timeline_metric.proto";
 import "protos/perfetto/metrics/android/anr_metric.proto";
 import "protos/perfetto/metrics/android/batt_metric.proto";
@@ -108,7 +109,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 57
+// Next id: 58
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -263,6 +264,8 @@
   // Aggregated Android Monitor Contention metrics
   optional AndroidMonitorContentionAggMetric android_monitor_contention_agg = 56;
 
+  optional AndroidBootMetric android_boot = 57;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 70a1f52..e940682 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -105,6 +105,23 @@
 
 // End of protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto
 
+// Begin of protos/perfetto/metrics/android/android_boot.proto
+
+// This metric computes how much time processes spend in UNINTERRUPTIBLE_SLEEP state
+message ProcessStateDurations {
+  optional int64 total_dur = 2;
+  optional int64 uninterruptible_sleep_dur = 3;
+}
+
+message AndroidBootMetric {
+  optional ProcessStateDurations system_server_durations = 1;
+  optional ProcessStateDurations systemui_durations = 2;
+  optional ProcessStateDurations launcher_durations = 3;
+  optional ProcessStateDurations gms_durations = 4;
+}
+
+// End of protos/perfetto/metrics/android/android_boot.proto
+
 // Begin of protos/perfetto/metrics/android/android_frame_timeline_metric.proto
 
 message AndroidFrameTimelineMetric {
@@ -2266,7 +2283,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 57
+// Next id: 58
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -2421,6 +2438,8 @@
   // Aggregated Android Monitor Contention metrics
   optional AndroidMonitorContentionAggMetric android_monitor_contention_agg = 56;
 
+  optional AndroidBootMetric android_boot = 57;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/trace_processor/metatrace_categories.proto b/protos/perfetto/trace_processor/metatrace_categories.proto
index b581ab1..6b9ee23 100644
--- a/protos/perfetto/trace_processor/metatrace_categories.proto
+++ b/protos/perfetto/trace_processor/metatrace_categories.proto
@@ -22,16 +22,17 @@
 // Keep in sync with TraceProcessor::MetatraceCategories.
 enum MetatraceCategories {
   // 1 << 0.
-  TOPLEVEL = 1;
+  QUERY_TIMELINE = 1;
   // 1 << 1.
-  QUERY = 2;
+  QUERY_DETAILED = 2;
   // 1 << 2.
-  FUNCTION = 4;
+  FUNCTION_CALL = 4;
   // 1 << 3.
   DB = 8;
+  // 1 << 4.
+  API_TIMELINE = 16;
 
   // Aliases for common subsets.
   NONE = 0;
-  // TOPLEVEL | QUERY | FUNCTION
-  ALL = 15;
+  ALL = 31;
 }
diff --git a/python/perfetto/__init__.py b/python/perfetto/__init__.py
deleted file mode 100644
index c569069..0000000
--- a/python/perfetto/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (C) 2020 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.
-
-try:
-  __import__('pkg_resources').declare_namespace(__name__)
-except ImportError:
-  __path__ = __import__('pkgutil').extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/python/perfetto/prebuilts/manifests/trace_processor_shell.py b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
index 8d88a66..b31da6e 100755
--- a/python/perfetto/prebuilts/manifests/trace_processor_shell.py
+++ b/python/perfetto/prebuilts/manifests/trace_processor_shell.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9551608,
+        9682976,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/trace_processor_shell',
     'sha256':
-        'f3def324604f551fdf7df088363aebf74de573fdaa02796fd994f1863ad58463',
+        '74b097836f16d788edce11bfda46f52e6499d8ec546d10f8dbab182612407b3b',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8064168,
+        8180008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/trace_processor_shell',
     'sha256':
-        'f34a2cfb4f93579452bbaf4f47c9b0fe475394d55316c9626949760c206ec801',
+        '9a96a2f9ef81f210fcba4a08b21db6f2f57fb1d325e91924043ae066327c29a8',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9415488,
+        9533320,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/trace_processor_shell',
     'sha256':
-        '86d133fc2fe0204693d2b22e9287363cb44bf76794b0091d4d0bb3388b7cd8d5',
+        'ee0ccae766aad09f0135efa83cc3a1cd78bd0e86824a7b1245e013d32e61d820',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6880412,
+        6963584,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/trace_processor_shell',
     'sha256':
-        '21fd4b2ad68b1f9829cee72d3905b1712cbfa5e169e89b89cdd0a3a4b1682e7d',
+        '2c69d2016dd18b6d42bf6c2a5b4398e29e4fd294950882387942272dd25a045a',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8843272,
+        8950112,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/trace_processor_shell',
     'sha256':
-        '97a6fb150fd6655f03394d8ad166fd2041a1b4ca56a9e7ce5b35902ff64e1a35',
+        'a10a7a9a6614461beb1f32ee16da3290e2770bb6f3a506269defbdbf7e8803af',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6497440,
+        6580152,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/trace_processor_shell',
     'sha256':
-        '6018ce36314a75693c5e5c2a69a3aacc9848385bd3ab9121e23de70df5188e1e'
+        '994a038b932accf5796550207a4e7d80c7612fd25e6eee414324e7e4f3ae14f2'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8019768,
+        8115560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/trace_processor_shell',
     'sha256':
-        '4e36255ebcd5e19fe26def6578cc604d636df9c4fa335ec5901e6cd9aba92ac3'
+        '42426b12ad60894aee37e502e4d023cfded53e706d990abf5c70c4556c2b73ff'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8897636,
+        9009020,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/trace_processor_shell',
     'sha256':
-        'f21573cc77603150e461402c6f032b7ff8f103debcd9d9b2093f521a69f581f7'
+        'af982ad7897d8cafb29a9f1303e32df20486a92eb07930db8b1898a75e84a667'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9162616,
+        9270696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/trace_processor_shell',
     'sha256':
-        '2cc5a02270a28db52aa17bfafcf215e3d754a936de595186a7152535ab28531b'
+        '081be392ddccdf37da80dd888277ea9215cfa8d05ddf6f90d9bff13a0a72f672'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        8790016,
+        8898560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        'e3193141bcb8bcc35b47c3ee8126d1f3ebe1b54a9ecde59ade9db4f22c6f77d6',
+        '2e66e5b6ab9c0f7ecad98c3b5b822133c9aa34f137b4007c228c78672fdea5a7',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/prebuilts/manifests/tracebox.py b/python/perfetto/prebuilts/manifests/tracebox.py
index 81c904b..16ce884 100755
--- a/python/perfetto/prebuilts/manifests/tracebox.py
+++ b/python/perfetto/prebuilts/manifests/tracebox.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1481952,
+        1498560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/tracebox',
     'sha256':
-        '81787717e819e1412609a7c67e561f643516cabd897d6b91e1f5cf572a40521a',
+        'b760c7ed682d23f8d268174a939f1b8cb130ffb0d52f42b4cc4499a25423e782',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1359320,
+        1376008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/tracebox',
     'sha256':
-        '5cd27e847e1e88493d136a2a803fd644cffc673d12685442a1df00c928170429',
+        '2835127a5fc42e501e29a685a1cbcd98c26f977ee845bb1bd60a43008fd48161',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2200152,
+        2212088,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/tracebox',
     'sha256':
-        'de4f883b1a3509ad9533c3057787a00cf8e68cbb1b5b6bde385caee3b2433574',
+        '0b4a61a3e45f4e1b6111ca0b440f9e4a0b0726df912542373f69b6821a3113bc',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1317900,
+        1327204,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/tracebox',
     'sha256':
-        'c404c93a16b3808b9b668f2ef071faecbde1a8c012be670a595716d2a730e5e9',
+        '43c20d80e5b40cebe5b26579319be6d57796b04155ffc90f133b6a13c2de25ab',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2128352,
+        2139928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/tracebox',
     'sha256':
-        '851c82a00e84b492627503ca2c46afcbd3ec1770c06b002134602541e6006b0c',
+        '261cd7912dd69e8d82ce61b66a78f098eba46f6f492f6ec6bf73124d40a9a202',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,42 +75,42 @@
     'file_name':
         'tracebox',
     'file_size':
-        1198036,
+        1202132,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/tracebox',
     'sha256':
-        'fac9f7cbe888139c569068d973be79f4d820a8f323ad04cdb9c5c3be6faba067'
+        '43874c4d187e3da5820ae3988d16608a8bf89d04282b8ab3e1486c6e57cd891f'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        1813160,
+        1825448,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/tracebox',
     'sha256':
-        '5c344c218c440c678fa56c8ce4d2f9c26efc300ebf268afd6e68a5775628cc0c'
+        '9264d8f23ea3988f2952696668d3d572669134e4ca5b3e25a3209d3bfdb7d015'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        1812396,
+        1820588,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/tracebox',
     'sha256':
-        'cd44b4ab3a1579dd0082f6f0fa4d7f710b9d056b44606cb247d41524be6d576e'
+        'd591e903b1e2b0e4b270f0510c24992aac68167c80e61450ebf6c36cc780cd21'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2091688,
+        2108072,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/tracebox',
     'sha256':
-        '6d614a3d4e77c7fb0712bf13d7a2c86a59b0945a043fefb70ffc70854551fca0'
+        'd3e4278e17764b605236c568202eb8d94f3f7e7593b0be9eba09f670a987acba'
 }]
diff --git a/python/perfetto/prebuilts/manifests/traceconv.py b/python/perfetto/prebuilts/manifests/traceconv.py
index de17e0d..7065064 100755
--- a/python/perfetto/prebuilts/manifests/traceconv.py
+++ b/python/perfetto/prebuilts/manifests/traceconv.py
@@ -1,15 +1,15 @@
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        8758200,
+        8889568,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/traceconv',
     'sha256':
-        'b6eb3552efc97b15b1cb7a746b90f77a4ac781566faa4ffa84e1785f5d90b38b',
+        'e45d08f7050553f77c87764823c6ca26e877d0b3865f2764b33dcab6dfd0b9ee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -19,11 +19,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7332088,
+        7447928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/traceconv',
     'sha256':
-        '585ee448560fa89f559566cdf41dcc6677a28c74ea3b5bae3cfe31cde813578c',
+        '19cabe8789566e6383632bbe79e360b8bf05465f8bb156788c8c957aba77f224',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -33,11 +33,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8515568,
+        8633416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/traceconv',
     'sha256':
-        'bff2c0c525193fc3555725b5767a30bc2b36d7d84b849b7ce41f7abb349f21be',
+        'a7a309c667a3e6a4c0268252540e3fc8052ebbd0abc30faf8f1783df411cf559',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -47,11 +47,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6421928,
+        6505268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/traceconv',
     'sha256':
-        '566298db777a31f3eabd0e6b17e1b0638875725be767333308f02a7fb7d4e36c',
+        '3ff0f9d4ef43e9e76d27c69827e508f4504d2d1fcfb4dac9d739023cd72106d7',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -61,11 +61,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8001776,
+        8108432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/traceconv',
     'sha256':
-        'ccf81438df9ed30f88e640f62938b6334698b9158364db43252d7183136e137d',
+        '1f4641b341f2c8bf36faf8d4befedc01bd414c8f778a5b314c99c2d42c42c068',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -75,55 +75,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6021600,
+        6096120,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/traceconv',
     'sha256':
-        'edae3606563616fa304e3240a8426db3dfd7d12ad28bb879e7d482af357af7f2'
+        'f15c86b1d83bd1706b81426a6d11c3274044b4838abfb5d313954c65282162a9'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7305880,
+        7401672,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/traceconv',
     'sha256':
-        '1a0ef7601a6d2f77f9a04d5bb8c092f5a536764a8e9a99325fff6b81c2879d1e'
+        '8e98e30a8e137116e74390bcb9d662f0a3f22f9d38fe3fd7ef305d1051bdf19d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8118692,
+        8238268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/traceconv',
     'sha256':
-        '6f9b53427d367e8582b12763d27e83217c9e744f947f58acc0b404b6ce5103e9'
+        '8ce56de90b18cc01d94b26f521292d71326552f8b4fb29b919f4b2b80c3eb2ec'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8288976,
+        8397056,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/traceconv',
     'sha256':
-        '5c843ddb151f5aa32288814cb190a879f15e6da63d642d3ded0f15954f480b5f'
+        'e5f31569261b3f201e2cc13b8eea510627d555767272373d9196e83ddaefa3f0'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7756800,
+        7867904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/windows-amd64/traceconv.exe',
     'sha256':
-        'b07849cd60872279dda68d4b8ec173af01e0cbb1692eb5473ef230ac89198f2d',
+        '74c24b2a74ae2a490ac5f918ab079a7713af07e9a36e70a2cc8536040d7df099',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index ad0d5da..926dee9 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index eca1b1a..33ca764 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/src/android_internal/statsd.cc b/src/android_internal/statsd.cc
index e69de29..3df3986 100644
--- a/src/android_internal/statsd.cc
+++ b/src/android_internal/statsd.cc
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "statsd.h"
+
+#include <binder/ProcessState.h>
+#include <stats_subscription.h>
+
+namespace perfetto {
+namespace android_internal {
+
+int32_t AddAtomSubscription(const uint8_t* subscription_config,
+                            size_t num_bytes,
+                            const AtomCallback callback,
+                            void* cookie) {
+  // Although the binder messages we use are one-way we pass an interface that
+  // statsd uses to talk back to us. For this to work we need the some binder
+  // threads listening to the for these messages. To handle this we start a
+  // thread pool if it hasn't been started already:
+  android::ProcessState::self()->startThreadPool();
+
+  auto c = reinterpret_cast<AStatsManager_SubscriptionCallback>(callback);
+  return AStatsManager_addSubscription(subscription_config, num_bytes, c,
+                                       cookie);
+}
+
+void RemoveAtomSubscription(int32_t subscription_id) {
+  AStatsManager_removeSubscription(subscription_id);
+}
+
+void FlushAtomSubscription(int32_t subscription_id) {
+  AStatsManager_flushSubscription(subscription_id);
+}
+
+}  // namespace android_internal
+}  // namespace perfetto
diff --git a/src/base/file_utils.cc b/src/base/file_utils.cc
index e7d4c94..0969515 100644
--- a/src/base/file_utils.cc
+++ b/src/base/file_utils.cc
@@ -26,11 +26,13 @@
 #include <vector>
 
 #include "perfetto/base/build_config.h"
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/platform_handle.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/platform.h"
 #include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/utils.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
@@ -43,6 +45,17 @@
 #include <unistd.h>
 #endif
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+#define PERFETTO_SET_FILE_PERMISSIONS
+#include <fcntl.h>
+#include <grp.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif
+
 namespace perfetto {
 namespace base {
 namespace {
@@ -346,5 +359,54 @@
   return filename.substr(ext_idx);
 }
 
+base::Status SetFilePermissions(const std::string& file_path,
+                                const std::string& group_name_or_id,
+                                const std::string& mode_bits) {
+#ifdef PERFETTO_SET_FILE_PERMISSIONS
+  PERFETTO_CHECK(!file_path.empty());
+  PERFETTO_CHECK(!group_name_or_id.empty());
+
+  // Default |group_id| to -1 for not changing the group ownership.
+  gid_t group_id = static_cast<gid_t>(-1);
+  auto maybe_group_id = base::StringToUInt32(group_name_or_id);
+  if (maybe_group_id) {  // A numerical group ID.
+    group_id = *maybe_group_id;
+  } else {  // A group name.
+    struct group* file_group = nullptr;
+    // Query the group ID of |group|.
+    do {
+      file_group = getgrnam(group_name_or_id.c_str());
+    } while (file_group == nullptr && errno == EINTR);
+    if (file_group == nullptr) {
+      return base::ErrStatus("Failed to get group information of %s ",
+                             group_name_or_id.c_str());
+    }
+    group_id = file_group->gr_gid;
+  }
+
+  if (PERFETTO_EINTR(chown(file_path.c_str(), geteuid(), group_id))) {
+    return base::ErrStatus("Failed to chown %s ", file_path.c_str());
+  }
+
+  // |mode| accepts values like "0660" as "rw-rw----" mode bits.
+  auto mode_value = base::StringToInt32(mode_bits, 8);
+  if (!(mode_bits.size() == 4 && mode_value.has_value())) {
+    return base::ErrStatus(
+        "The chmod mode bits must be a 4-digit octal number, e.g. 0660");
+  }
+  if (PERFETTO_EINTR(
+          chmod(file_path.c_str(), static_cast<mode_t>(mode_value.value())))) {
+    return base::ErrStatus("Failed to chmod %s", file_path.c_str());
+  }
+  return base::OkStatus();
+#else
+  base::ignore_result(file_path);
+  base::ignore_result(group_name_or_id);
+  base::ignore_result(mode_bits);
+  return base::ErrStatus(
+      "Setting file permissions is not supported on this platform");
+#endif
+}
+
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/ipc/BUILD.gn b/src/ipc/BUILD.gn
index 39843f1..1cd1a8d 100644
--- a/src/ipc/BUILD.gn
+++ b/src/ipc/BUILD.gn
@@ -141,6 +141,7 @@
 perfetto_component("perfetto_ipc") {
   public_deps = [
     ":client",
+    ":common",
     ":host",
     "../../gn:default_deps",
   ]
diff --git a/src/ipc/host_impl.cc b/src/ipc/host_impl.cc
index 0b852c0..029b450 100644
--- a/src/ipc/host_impl.cc
+++ b/src/ipc/host_impl.cc
@@ -21,9 +21,12 @@
 #include <utility>
 
 #include "perfetto/base/build_config.h"
+#include "perfetto/base/logging.h"
 #include "perfetto/base/task_runner.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/crash_keys.h"
+#include "perfetto/ext/base/sys_types.h"
+#include "perfetto/ext/base/unix_socket.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/ipc/service.h"
 #include "perfetto/ext/ipc/service_descriptor.h"
@@ -41,8 +44,9 @@
     kUseTCPSocket ? base::SockFamily::kInet : base::SockFamily::kUnix;
 
 base::CrashKey g_crash_key_uid("ipc_uid");
+}  // namespace
 
-uid_t GetPosixPeerUid(base::UnixSocket* sock) {
+uid_t HostImpl::ClientConnection::GetPosixPeerUid() const {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
     PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
@@ -50,22 +54,23 @@
     return sock->peer_uid_posix();
 #endif
 
-  // Unsupported. Must be != kInvalidUid or the PacketValidator will fail.
-  base::ignore_result(sock);
+  // For non-unix sockets, check if the UID is set in OnSetPeerIdentity().
+  if (uid_override != base::kInvalidUid)
+    return uid_override;
+  // Must be != kInvalidUid or the PacketValidator will fail.
   return 0;
 }
 
-pid_t GetLinuxPeerPid(base::UnixSocket* sock) {
+pid_t HostImpl::ClientConnection::GetLinuxPeerPid() const {
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   if (sock->family() == base::SockFamily::kUnix)
     return sock->peer_pid_linux();
 #endif
-  base::ignore_result(sock);
-  return base::kInvalidPid;  // Unsupported.
-}
 
-}  // namespace
+  // For non-unix sockets, return the PID set in OnSetPeerIdentity().
+  return pid_override;
+}
 
 // static
 std::unique_ptr<Host> Host::CreateInstance(const char* socket_name,
@@ -179,7 +184,7 @@
   ClientConnection* client = it->second;
   BufferedFrameDeserializer& frame_deserializer = client->frame_deserializer;
 
-  auto peer_uid = GetPosixPeerUid(client->sock.get());
+  auto peer_uid = client->GetPosixPeerUid();
   auto scoped_key = g_crash_key_uid.SetScoped(static_cast<int64_t>(peer_uid));
 
   size_t rsize;
@@ -209,6 +214,8 @@
     return OnBindService(client, req_frame);
   if (req_frame.has_msg_invoke_method())
     return OnInvokeMethod(client, req_frame);
+  if (req_frame.has_set_peer_identity())
+    return OnSetPeerIdentity(client, req_frame);
 
   PERFETTO_DLOG("Received invalid RPC frame from client %" PRIu64, client->id);
   Frame reply_frame;
@@ -276,16 +283,35 @@
     });
   }
 
-  auto peer_uid = GetPosixPeerUid(client->sock.get());
+  auto peer_uid = client->GetPosixPeerUid();
   auto scoped_key = g_crash_key_uid.SetScoped(static_cast<int64_t>(peer_uid));
   service->client_info_ =
-      ClientInfo(client->id, peer_uid, GetLinuxPeerPid(client->sock.get()));
+      ClientInfo(client->id, peer_uid, client->GetLinuxPeerPid());
   service->received_fd_ = &client->received_fd;
   method.invoker(service, *decoded_req_args, std::move(deferred_reply));
   service->received_fd_ = nullptr;
   service->client_info_ = ClientInfo();
 }
 
+void HostImpl::OnSetPeerIdentity(ClientConnection* client,
+                                 const Frame& req_frame) {
+  if (client->sock->family() == base::SockFamily::kUnix) {
+    PERFETTO_DLOG("SetPeerIdentity is ignored for unix socket connections.");
+    return;
+  }
+
+  // This is can only be set once by the relay service.
+  if (client->pid_override != base::kInvalidPid ||
+      client->uid_override != base::kInvalidUid) {
+    PERFETTO_DLOG("Already received SetPeerIdentity.");
+    return;
+  }
+
+  client->pid_override = req_frame.set_peer_identity().pid();
+  client->uid_override =
+      static_cast<uid_t>(req_frame.set_peer_identity().uid());
+}
+
 void HostImpl::ReplyToMethodInvocation(ClientID client_id,
                                        RequestID request_id,
                                        AsyncResult<ProtoMessage> reply) {
@@ -312,7 +338,7 @@
 
 // static
 void HostImpl::SendFrame(ClientConnection* client, const Frame& frame, int fd) {
-  auto peer_uid = GetPosixPeerUid(client->sock.get());
+  auto peer_uid = client->GetPosixPeerUid();
   auto scoped_key = g_crash_key_uid.SetScoped(static_cast<int64_t>(peer_uid));
 
   std::string buf = BufferedFrameDeserializer::Serialize(frame);
@@ -345,10 +371,11 @@
   auto it = clients_by_socket_.find(sock);
   if (it == clients_by_socket_.end())
     return;
-  ClientID client_id = it->second->id;
+  auto* client = it->second;
+  ClientID client_id = client->id;
 
-  ClientInfo client_info(client_id, GetPosixPeerUid(sock),
-                         GetLinuxPeerPid(sock));
+  ClientInfo client_info(client_id, client->GetPosixPeerUid(),
+                         client->GetLinuxPeerPid());
   clients_by_socket_.erase(it);
   PERFETTO_DCHECK(clients_.count(client_id));
   clients_.erase(client_id);
diff --git a/src/ipc/host_impl.h b/src/ipc/host_impl.h
index 788b81a..8738459 100644
--- a/src/ipc/host_impl.h
+++ b/src/ipc/host_impl.h
@@ -24,6 +24,7 @@
 
 #include "perfetto/base/task_runner.h"
 #include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/ext/base/sys_types.h"
 #include "perfetto/ext/base/thread_checker.h"
 #include "perfetto/ext/base/unix_socket.h"
 #include "perfetto/ext/ipc/deferred.h"
@@ -66,6 +67,14 @@
     BufferedFrameDeserializer frame_deserializer;
     base::ScopedFile received_fd;
     std::function<bool(int)> send_fd_cb_fuchsia;
+    // Peer identity set using IPCFrame sent by the client. These 2 fields
+    // should be used only for non-AF_UNIX connections AF_UNIX connections
+    // should only rely on the peer identity obtained from the socket.
+    uid_t uid_override = base::kInvalidUid;
+    pid_t pid_override = base::kInvalidPid;
+
+    pid_t GetLinuxPeerPid() const;
+    uid_t GetPosixPeerUid() const;
   };
   struct ExposedService {
     ExposedService(ServiceID, const std::string&, std::unique_ptr<Service>);
@@ -85,6 +94,8 @@
   void OnReceivedFrame(ClientConnection*, const Frame&);
   void OnBindService(ClientConnection*, const Frame&);
   void OnInvokeMethod(ClientConnection*, const Frame&);
+  void OnSetPeerIdentity(ClientConnection*, const Frame&);
+
   void ReplyToMethodInvocation(ClientID, RequestID, AsyncResult<ProtoMessage>);
   const ExposedService* GetServiceByName(const std::string&);
 
diff --git a/src/protozero/filtering/message_tokenizer_unittest.cc b/src/protozero/filtering/message_tokenizer_unittest.cc
index 666c4cf..6808694 100644
--- a/src/protozero/filtering/message_tokenizer_unittest.cc
+++ b/src/protozero/filtering/message_tokenizer_unittest.cc
@@ -131,7 +131,7 @@
   EXPECT_THAT(
       tokens,
       ElementsAre(Token{1, ProtoWireType::kVarInt, 101u},
-                  Token{2, ProtoWireType::kLengthDelimited, 24u},
+                  Token{2, ProtoWireType::kLengthDelimited, 21u},
                   Token{3, ProtoWireType::kVarInt, 103u},
                   Token{4, ProtoWireType::kFixed32, 104u},
                   Token{5, ProtoWireType::kLengthDelimited, 7},
diff --git a/src/protozero/message.cc b/src/protozero/message.cc
index aa5954e..d760e27 100644
--- a/src/protozero/message.cc
+++ b/src/protozero/message.cc
@@ -34,6 +34,8 @@
 
 namespace {
 
+constexpr int kBytesToCompact = proto_utils::kMessageLengthFieldSize - 1u;
+
 #if PERFETTO_DCHECK_IS_ON()
 std::atomic<uint32_t> g_generation;
 #endif
@@ -59,7 +61,7 @@
   size_field_ = nullptr;
   size_already_written_ = 0;
   nested_message_ = nullptr;
-  finalized_ = false;
+  message_state_ = MessageState::kNotFinalized;
 #if PERFETTO_DCHECK_IS_ON()
   handle_ = nullptr;
   generation_ = g_generation.fetch_add(1, std::memory_order_relaxed);
@@ -118,7 +120,7 @@
 }
 
 uint32_t Message::Finalize() {
-  if (finalized_)
+  if (is_finalized())
     return size_;
 
   if (nested_message_)
@@ -127,15 +129,54 @@
   // Write the length of the nested message a posteriori, using a leading-zero
   // redundant varint encoding.
   if (size_field_) {
-    PERFETTO_DCHECK(!finalized_);
+    PERFETTO_DCHECK(!is_finalized());
     PERFETTO_DCHECK(size_ < proto_utils::kMaxMessageLength);
     PERFETTO_DCHECK(size_ >= size_already_written_);
-    proto_utils::WriteRedundantVarInt(size_ - size_already_written_,
-                                      size_field_);
+    //
+    // Normally the size of a protozero message is written with 4 bytes just
+    // before the contents of the message itself:
+    //
+    //    size          message data
+    //   [aa bb cc dd] [01 23 45 67 ...]
+    //
+    // We always reserve 4 bytes for the size, because the real size of the
+    // message isn't known until the call to Finalize(). This is possible
+    // because we can use leading zero redundant varint coding to expand any
+    // size smaller than 256 MiB to 4 bytes.
+    //
+    // However this is wasteful for short, frequently written messages, so the
+    // code below uses a 1 byte size field when possible. This is done by
+    // shifting the already-written data (which should still be in the cache)
+    // back by 3 bytes, resulting in this layout:
+    //
+    //   size  message data
+    //   [aa] [01 23 45 67 ...]
+    //
+    // We can only do this optimization if the message is contained in a single
+    // chunk (since we can't modify previously committed chunks). We can check
+    // this by verifying that the size field is immediately before the message
+    // in memory and is fully contained by the current chunk.
+    //
+    if (PERFETTO_LIKELY(size_already_written_ == 0 &&
+                        size_ <= proto_utils::kMaxOneByteMessageLength &&
+                        size_field_ ==
+                            stream_writer_->write_ptr() - size_ -
+                                proto_utils::kMessageLengthFieldSize &&
+                        size_field_ >= stream_writer_->cur_range().begin)) {
+      stream_writer_->Rewind(size_, kBytesToCompact);
+      PERFETTO_DCHECK(size_field_ == stream_writer_->write_ptr() - size_ - 1u);
+      *size_field_ = static_cast<uint8_t>(size_);
+      message_state_ = MessageState::kFinalizedWithCompaction;
+    } else {
+      proto_utils::WriteRedundantVarInt(size_ - size_already_written_,
+                                        size_field_);
+      message_state_ = MessageState::kFinalized;
+    }
     size_field_ = nullptr;
+  } else {
+    message_state_ = MessageState::kFinalized;
   }
 
-  finalized_ = true;
 #if PERFETTO_DCHECK_IS_ON()
   if (handle_)
     handle_->reset_message();
@@ -170,6 +211,10 @@
 
 void Message::EndNestedMessage() {
   size_ += nested_message_->Finalize();
+  if (nested_message_->message_state_ ==
+      MessageState::kFinalizedWithCompaction) {
+    size_ -= kBytesToCompact;
+  }
   arena_->DeleteLastMessage(nested_message_);
   nested_message_ = nullptr;
 }
diff --git a/src/protozero/message_unittest.cc b/src/protozero/message_unittest.cc
index 985ed5c..26ad5f0 100644
--- a/src/protozero/message_unittest.cc
+++ b/src/protozero/message_unittest.cc
@@ -23,6 +23,7 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/protozero/message_handle.h"
+#include "perfetto/protozero/proto_utils.h"
 #include "perfetto/protozero/root_message.h"
 #include "src/base/test/utils.h"
 #include "src/protozero/test/fake_scattered_buffer.h"
@@ -51,11 +52,7 @@
 
 class MessageTest : public ::testing::Test {
  public:
-  void SetUp() override {
-    buffer_.reset(new FakeScatteredBuffer(kChunkSize));
-    stream_writer_.reset(new ScatteredStreamWriter(buffer_.get()));
-    readback_pos_ = 0;
-  }
+  void SetUp() override { SetChunkSize(kChunkSize); }
 
   void TearDown() override {
     // Check that none of the messages created by the text fixtures below did
@@ -77,6 +74,13 @@
 
   void ResetMessage(FakeRootMessage* msg) { msg->Reset(stream_writer_.get()); }
 
+  void SetChunkSize(size_t size) {
+    buffer_.reset(new FakeScatteredBuffer(size));
+    stream_writer_.reset(new ScatteredStreamWriter(buffer_.get()));
+    chunk_size_ = size;
+    readback_pos_ = 0;
+  }
+
   FakeRootMessage* NewMessage() {
     std::unique_ptr<uint8_t[]> mem(
         new uint8_t[sizeof(kStartWatermark) + sizeof(FakeRootMessage) +
@@ -92,10 +96,19 @@
     return msg;
   }
 
+  FakeRootMessage* NewMessageWithSizeField() {
+    FakeRootMessage* msg = NewMessage();
+    uint8_t* size_field =
+        stream_writer_->ReserveBytes(proto_utils::kMessageLengthFieldSize);
+    memset(size_field, 0u, proto_utils::kMessageLengthFieldSize);
+    msg->set_size_field(size_field);
+    return msg;
+  }
+
   size_t GetNumSerializedBytes() {
     if (buffer_->chunks().empty())
       return 0;
-    return buffer_->chunks().size() * kChunkSize -
+    return buffer_->chunks().size() * chunk_size_ -
            stream_writer_->bytes_available();
   }
 
@@ -107,18 +120,23 @@
 
   static void BuildNestedMessages(Message* msg,
                                   uint32_t max_depth,
+                                  bool empty = false,
                                   uint32_t depth = 0) {
-    for (uint32_t i = 1; i <= 128; ++i)
-      msg->AppendBytes(i, kTestBytes, sizeof(kTestBytes));
+    if (!empty) {
+      for (uint32_t i = 1; i <= 128; ++i)
+        msg->AppendBytes(i, kTestBytes, sizeof(kTestBytes));
+    }
 
     if (depth < max_depth) {
       auto* nested_msg =
           msg->BeginNestedMessage<FakeChildMessage>(1 + depth * 10);
-      BuildNestedMessages(nested_msg, max_depth, depth + 1);
+      BuildNestedMessages(nested_msg, max_depth, empty, depth + 1);
     }
 
-    for (uint32_t i = 129; i <= 256; ++i)
-      msg->AppendVarInt(i, 42);
+    if (!empty) {
+      for (uint32_t i = 129; i <= 256; ++i)
+        msg->AppendVarInt(i, 42);
+    }
 
     if ((depth & 2) == 0)
       msg->Finalize();
@@ -128,7 +146,8 @@
   std::unique_ptr<FakeScatteredBuffer> buffer_;
   std::unique_ptr<ScatteredStreamWriter> stream_writer_;
   std::vector<std::unique_ptr<uint8_t[]>> messages_;
-  size_t readback_pos_;
+  size_t chunk_size_{};
+  size_t readback_pos_{};
 };
 
 TEST_F(MessageTest, ZeroLengthArraysAndStrings) {
@@ -189,26 +208,26 @@
 
   root_msg->AppendVarInt(5 /* field_id */, 3);
 
-  // The expected size of the root message is supposed to be 20 bytes:
+  // The expected size of the root message is supposed to be 14 bytes:
   //   2 bytes for the varint field (id: 1) (1 for preamble and one for payload)
-  //   6 bytes for the preamble of the 1st nested message (2 for id, 4 for size)
+  //   3 bytes for the preamble of the 1st nested message (2 for id, 1 for size)
   //   2 bytes for the varint field (id: 2) of the 1st nested message
-  //   6 bytes for the premable of the 2nd nested message
+  //   3 bytes for the premable of the 2nd nested message
   //   2 bytes for the varint field (id: 4) of the 2nd nested message.
   //   2 bytes for the last varint (id : 5) field of the root message.
   // Test also that finalization is idempontent and Finalize() can be safely
   // called more than once without side effects.
   for (int i = 0; i < 3; ++i) {
-    EXPECT_EQ(20u, root_msg->Finalize());
-    EXPECT_EQ(20u, GetNumSerializedBytes());
+    EXPECT_EQ(14u, root_msg->Finalize());
+    EXPECT_EQ(14u, GetNumSerializedBytes());
   }
 
   ASSERT_EQ("0801", GetNextSerializedBytes(2));
 
-  ASSERT_EQ("820882808000", GetNextSerializedBytes(6));
+  ASSERT_EQ("820802", GetNextSerializedBytes(3));
   ASSERT_EQ("1002", GetNextSerializedBytes(2));
 
-  ASSERT_EQ("8A0882808000", GetNextSerializedBytes(6));
+  ASSERT_EQ("8A0802", GetNextSerializedBytes(3));
   ASSERT_EQ("2002", GetNextSerializedBytes(2));
 
   ASSERT_EQ("2803", GetNextSerializedBytes(2));
@@ -254,7 +273,7 @@
 
   // Nested message should have been finalized as a side effect of appending
   // raw bytes.
-  EXPECT_EQ(0x82u, *nested_msg_size_field);
+  EXPECT_EQ(0x2u, *nested_msg_size_field);
 }
 
 TEST_F(MessageTest, AppendScatteredBytesFinalizesNestedMessage) {
@@ -276,7 +295,7 @@
 
   // Nested message should have been finalized as a side effect of appending
   // scattered bytes.
-  EXPECT_EQ(0x82u, *nested_msg_size_field);
+  EXPECT_EQ(0x2u, *nested_msg_size_field);
 }
 
 // Checks that the size field of root and nested messages is properly written
@@ -305,7 +324,7 @@
 
   // However the size written in the size field should take into account the
   // inc_size_already_written() call and be equal to 118 - 6 = 112, encoded
-  // in a rendundant varint encoding of kMessageLengthFieldSize bytes.
+  // in a redundant varint encoding of kMessageLengthFieldSize bytes.
   EXPECT_STREQ("\xD3\x81\x80\x00", reinterpret_cast<char*>(root_msg_size));
 
   // Skip 2 bytes for the 0x42 varint + 1 byte for the |nested_msg_1| preamble.
@@ -341,8 +360,6 @@
 }
 
 TEST_F(MessageTest, DeeplyNested) {
-  std::vector<Message*> nested_msgs;
-
   Message* root_msg = NewMessage();
   BuildNestedMessages(root_msg, /*max_depth=*/1000);
   root_msg->Finalize();
@@ -352,6 +369,23 @@
   EXPECT_EQ(0xc0fde419, buf_hash);
 }
 
+TEST_F(MessageTest, DeeplyNestedEmptyMessages) {
+  // Stress test writing deeply nested empty messages, many of which will be
+  // packed into the protobuf length field.
+
+  // Use a larger chunk size for this test so there is more opportunity to pack
+  // messages.
+  SetChunkSize(4096u);
+
+  Message* root_msg = NewMessage();
+  BuildNestedMessages(root_msg, /*max_depth=*/1000, /*empty=*/true);
+  root_msg->Finalize();
+
+  std::string full_buf = GetNextSerializedBytes(GetNumSerializedBytes());
+  size_t buf_hash = SimpleHash(full_buf);
+  EXPECT_EQ(0x9371fe8eu, buf_hash);
+}
+
 TEST_F(MessageTest, DestructInvalidMessageHandle) {
   FakeRootMessage* msg = NewMessage();
   EXPECT_DCHECK_DEATH({
@@ -361,16 +395,13 @@
 }
 
 TEST_F(MessageTest, MessageHandle) {
-  FakeRootMessage* msg1 = NewMessage();
-  FakeRootMessage* msg2 = NewMessage();
-  FakeRootMessage* msg3 = NewMessage();
+  FakeRootMessage* msg3 = NewMessageWithSizeField();
+  FakeRootMessage* msg2 = NewMessageWithSizeField();
+  FakeRootMessage* msg1 = NewMessageWithSizeField();
   FakeRootMessage* ignored_msg = NewMessage();
-  uint8_t msg1_size[proto_utils::kMessageLengthFieldSize] = {};
-  uint8_t msg2_size[proto_utils::kMessageLengthFieldSize] = {};
-  uint8_t msg3_size[proto_utils::kMessageLengthFieldSize] = {};
-  msg1->set_size_field(&msg1_size[0]);
-  msg2->set_size_field(&msg2_size[0]);
-  msg3->set_size_field(&msg3_size[0]);
+  uint8_t* msg1_size = msg1->size_field();
+  uint8_t* msg2_size = msg2->size_field();
+  uint8_t* msg3_size = msg3->size_field();
 
   // Test that the handle going out of scope causes the finalization of the
   // target message and triggers the optional callback.
@@ -379,7 +410,7 @@
     handle1->AppendBytes(1 /* field_id */, kTestBytes, 1 /* size */);
     ASSERT_EQ(0u, msg1_size[0]);
   }
-  ASSERT_EQ(0x83u, msg1_size[0]);
+  ASSERT_EQ(0x3u, msg1_size[0]);
 
   // Test that the handle can be late initialized.
   MessageHandle<FakeRootMessage> handle2(ignored_msg);
@@ -445,13 +476,12 @@
     ASSERT_EQ(0x82u, size_msg_1[0]);
     ASSERT_EQ(0u, size_msg_2[0]);
   }
-  ASSERT_EQ(0x83u, size_msg_2[0]);
+  ASSERT_EQ(0x3u, size_msg_2[0]);
 }
 
 TEST_F(MessageTest, MoveMessageHandle) {
-  FakeRootMessage* msg = NewMessage();
-  uint8_t msg_size[proto_utils::kMessageLengthFieldSize] = {};
-  msg->set_size_field(&msg_size[0]);
+  FakeRootMessage* msg = NewMessageWithSizeField();
+  uint8_t* msg_size = msg->size_field();
 
   // Test that the handle going out of scope causes the finalization of the
   // target message.
@@ -462,7 +492,27 @@
     handle2 = std::move(handle1);
     ASSERT_EQ(0u, msg_size[0]);
   }
-  ASSERT_EQ(0x83u, msg_size[0]);
+  ASSERT_EQ(0x3u, msg_size[0]);
+}
+
+TEST_F(MessageTest, FinalizeWithCompaction) {
+  FakeRootMessage* msg = NewMessageWithSizeField();
+
+  msg->AppendBytes(1 /* field_id */, kTestBytes, 5 /* size */);
+  uint32_t size = msg->Finalize();
+  EXPECT_EQ(7u, size);
+  EXPECT_EQ(8u, GetNumSerializedBytes());
+}
+
+TEST_F(MessageTest, FinalizeWithoutCompaction) {
+  FakeRootMessage* msg = NewMessageWithSizeField();
+
+  // This message doesn't fit into a single chunk, so it won't be compacted.
+  msg->AppendBytes(1 /* field_id */, kTestBytes, sizeof(kTestBytes) /* size */);
+  msg->AppendBytes(1 /* field_id */, kTestBytes, sizeof(kTestBytes) /* size */);
+  uint32_t size = msg->Finalize();
+  EXPECT_EQ(24u, size);
+  EXPECT_EQ(28u, GetNumSerializedBytes());
 }
 
 }  // namespace
diff --git a/src/protozero/scattered_stream_writer_unittest.cc b/src/protozero/scattered_stream_writer_unittest.cc
index b3ec99c..af75cbf 100644
--- a/src/protozero/scattered_stream_writer_unittest.cc
+++ b/src/protozero/scattered_stream_writer_unittest.cc
@@ -107,5 +107,27 @@
   EXPECT_EQ(0x52u, other_buffer[3]);
 }
 
+TEST(ScatteredStreamWriterTest, Rewind) {
+  FakeScatteredBuffer delegate(kChunkSize);
+  ScatteredStreamWriter ssw(&delegate);
+  const uint8_t kTestBytes[] = {0x01, 0x02, 0x03, 0x04};
+
+  ssw.WriteBytes(kTestBytes, sizeof(kTestBytes));
+  EXPECT_EQ("0102030400000000", delegate.GetChunkAsString(0));
+  EXPECT_EQ(ssw.write_ptr(), ssw.cur_range().begin + 4u);
+
+  ssw.Rewind(3, 1);
+  EXPECT_EQ(ssw.write_ptr(), ssw.cur_range().begin + 3u);
+  EXPECT_EQ("0203040400000000", delegate.GetChunkAsString(0));
+
+  ssw.Rewind(1, 2);
+  EXPECT_EQ(ssw.write_ptr(), ssw.cur_range().begin + 1u);
+  EXPECT_EQ("0403040400000000", delegate.GetChunkAsString(0));
+
+  ssw.Rewind(1, 0);
+  EXPECT_EQ(ssw.write_ptr(), ssw.cur_range().begin + 1u);
+  EXPECT_EQ("0403040400000000", delegate.GetChunkAsString(0));
+}
+
 }  // namespace
 }  // namespace protozero
diff --git a/src/protozero/test/protozero_conformance_unittest.cc b/src/protozero/test/protozero_conformance_unittest.cc
index 7a73b69..74e2204 100644
--- a/src/protozero/test/protozero_conformance_unittest.cc
+++ b/src/protozero/test/protozero_conformance_unittest.cc
@@ -108,7 +108,7 @@
   msg_c->set_value_c(1000);
 
   std::string serialized = msg_a.SerializeAsString();
-  EXPECT_EQ(serialized.size(), 26u);
+  EXPECT_EQ(serialized.size(), 14u);
 
   pbgold::NestedA gold_msg_a;
   gold_msg_a.ParseFromString(serialized);
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 31a6c7e..dd5c724 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -38,8 +38,8 @@
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/softirq_action.h"
 #include "src/trace_processor/types/tcp_state.h"
-
 #include "protos/perfetto/common/gpu_counter_descriptor.pbzero.h"
+#include "protos/perfetto/trace/ftrace/android_fs.pbzero.h"
 #include "protos/perfetto/trace/ftrace/binder.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cma.pbzero.h"
 #include "protos/perfetto/trace/ftrace/cpuhp.pbzero.h"
@@ -217,8 +217,7 @@
   }
   return buffer;
 }
-}  // namespace
-
+} // namespace
 FtraceParser::FtraceParser(TraceProcessorContext* context)
     : context_(context),
       rss_stat_tracker_(context),
@@ -315,7 +314,15 @@
       cma_nr_test_fail_id_(context_->storage->InternString("cma_nr_test_fail")),
       syscall_ret_id_(context->storage->InternString("ret")),
       syscall_args_id_(context->storage->InternString("args")),
-      replica_slice_id_(context->storage->InternString("replica_slice")) {
+      replica_slice_id_(context->storage->InternString("replica_slice")),
+      file_path_id_(context_->storage->InternString("file_path")),
+      offset_id_start_(context_->storage->InternString("offset_start")),
+      offset_id_end_(context_->storage->InternString("offset_end")),
+      bytes_read_id_start_(context_->storage->InternString("bytes_read_start")),
+      bytes_read_id_end_(context_->storage->InternString("bytes_read_end")),
+      android_fs_category_id_(context_->storage->InternString("android_fs")),
+      android_fs_data_read_id_(
+          context_->storage->InternString("android_fs_data_read")) {
   // Build the lookup table for the strings inside ftrace events (e.g. the
   // name of ftrace event fields and the names of their args).
   for (size_t i = 0; i < GetDescriptorsSize(); i++) {
@@ -1047,7 +1054,14 @@
         ParseMdssTracingMarkWrite(ts, pid, fld_bytes);
         break;
       }
-
+      case FtraceEvent::kAndroidFsDatareadEndFieldNumber: {
+        ParseAndroidFsDatareadEnd(ts, fld_bytes);
+        break;
+      }
+      case FtraceEvent::kAndroidFsDatareadStartFieldNumber: {
+        ParseAndroidFsDatareadStart(ts, pid, fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -3092,6 +3106,62 @@
   context_->slice_tracker->End(timestamp, track, kNullStringId, name_id);
 }
 
+/** Parses android_fs_dataread_start event.*/
+void FtraceParser::ParseAndroidFsDatareadStart(int64_t ts,
+                                               uint32_t pid,
+                                               ConstBytes data) {
+  protos::pbzero::AndroidFsDatareadStartFtraceEvent::Decoder
+      android_fs_read_begin(data);
+  base::StringView file_path(android_fs_read_begin.pathbuf());
+  std::pair<uint64_t, int64_t> key(android_fs_read_begin.ino(),
+                                   android_fs_read_begin.offset());
+  inode_offset_thread_map_.Insert(key, pid);
+  // Create a new Track object for the event.
+  auto async_track = context_->async_track_set_tracker->InternGlobalTrackSet(
+      android_fs_category_id_);
+  TrackId track_id = context_->async_track_set_tracker->Begin(async_track, pid);
+  StringId string_id = context_->storage->InternString(file_path);
+  auto args_inserter = [this, &android_fs_read_begin,
+                        &string_id](ArgsTracker::BoundInserter* inserter) {
+    inserter->AddArg(file_path_id_, Variadic::String(string_id));
+    inserter->AddArg(offset_id_start_,
+                     Variadic::Integer(android_fs_read_begin.offset()));
+    inserter->AddArg(bytes_read_id_start_,
+                     Variadic::Integer(android_fs_read_begin.bytes()));
+  };
+  context_->slice_tracker->Begin(ts, track_id, kNullStringId,
+                                 android_fs_data_read_id_, args_inserter);
+}
+
+/** Parses android_fs_dataread_end event.*/
+void FtraceParser::ParseAndroidFsDatareadEnd(int64_t ts, ConstBytes data) {
+  protos::pbzero::AndroidFsDatareadEndFtraceEvent::Decoder android_fs_read_end(
+      data);
+  std::pair<uint64_t, int64_t> key(android_fs_read_end.ino(),
+                                   android_fs_read_end.offset());
+  // Find the corresponding (inode, offset) pair in the map.
+  auto it = inode_offset_thread_map_.Find(key);
+  if (!it) {
+    return;
+  }
+  uint32_t start_event_tid = *it;
+  auto async_track = context_->async_track_set_tracker->InternGlobalTrackSet(
+      android_fs_category_id_);
+  TrackId track_id =
+      context_->async_track_set_tracker->End(async_track, start_event_tid);
+  auto args_inserter =
+      [this, &android_fs_read_end](ArgsTracker::BoundInserter* inserter) {
+        inserter->AddArg(offset_id_end_,
+                         Variadic::Integer(android_fs_read_end.offset()));
+        inserter->AddArg(bytes_read_id_end_,
+                         Variadic::Integer(android_fs_read_end.bytes()));
+      };
+  context_->slice_tracker->End(ts, track_id, kNullStringId, kNullStringId,
+                               args_inserter);
+  // Erase the entry from the map.
+  inode_offset_thread_map_.Erase(key);
+}
+
 StringId FtraceParser::InternedKernelSymbolOrFallback(
     uint64_t key,
     PacketSequenceStateGeneration* seq_state) {
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index 174861d..8003fb1 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -17,6 +17,8 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_FTRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_FTRACE_PARSER_H_
 
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
 #include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
@@ -285,6 +287,10 @@
   void ParseMaliKcpuFenceSignal(uint32_t pid, int64_t ts);
   void ParseMaliKcpuFenceWaitStart(uint32_t pid, int64_t ts);
   void ParseMaliKcpuFenceWaitEnd(uint32_t pid, int64_t ts);
+  void ParseAndroidFsDatareadEnd(int64_t timestamp, protozero::ConstBytes);
+  void ParseAndroidFsDatareadStart(int64_t ts,
+                                   uint32_t pid,
+                                   protozero::ConstBytes);
 
   TraceProcessorContext* context_;
   RssStatTracker rss_stat_tracker_;
@@ -366,6 +372,13 @@
   const StringId syscall_ret_id_;
   const StringId syscall_args_id_;
   const StringId replica_slice_id_;
+  const StringId file_path_id_;
+  const StringId offset_id_start_;
+  const StringId offset_id_end_;
+  const StringId bytes_read_id_start_;
+  const StringId bytes_read_id_end_;
+  const StringId android_fs_category_id_;
+  const StringId android_fs_data_read_id_;
   std::vector<StringId> syscall_arg_name_ids_;
 
   struct FtraceMessageStrings {
@@ -428,6 +441,18 @@
   // putting them in the metadata multiple times (the ftrace data sources
   // re-emits begin stats on every flush).
   std::unordered_set<uint32_t> seen_errors_for_sequence_id_;
+
+  struct PairHash {
+    std::size_t operator()(const std::pair<uint64_t, int64_t>& p) const {
+      base::Hasher hasher;
+      hasher.Update(p.first);
+      hasher.Update(p.second);
+      return static_cast<std::size_t>(hasher.digest());
+    }
+  };
+
+  base::FlatHashMap<std::pair<uint64_t, int64_t>, uint32_t, PairHash>
+      inode_offset_thread_map_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/heap_graph_module.cc b/src/trace_processor/importers/proto/heap_graph_module.cc
index ad2b806..cc1d7ad 100644
--- a/src/trace_processor/importers/proto/heap_graph_module.cc
+++ b/src/trace_processor/importers/proto/heap_graph_module.cc
@@ -36,72 +36,6 @@
 using ObjectTable = tables::HeapGraphObjectTable;
 using ReferenceTable = tables::HeapGraphReferenceTable;
 
-const char* HeapGraphRootTypeToString(int32_t type) {
-  switch (type) {
-    case protos::pbzero::HeapGraphRoot::ROOT_UNKNOWN:
-      return "ROOT_UNKNOWN";
-    case protos::pbzero::HeapGraphRoot::ROOT_JNI_GLOBAL:
-      return "ROOT_JNI_GLOBAL";
-    case protos::pbzero::HeapGraphRoot::ROOT_JNI_LOCAL:
-      return "ROOT_JNI_LOCAL";
-    case protos::pbzero::HeapGraphRoot::ROOT_JAVA_FRAME:
-      return "ROOT_JAVA_FRAME";
-    case protos::pbzero::HeapGraphRoot::ROOT_NATIVE_STACK:
-      return "ROOT_NATIVE_STACK";
-    case protos::pbzero::HeapGraphRoot::ROOT_STICKY_CLASS:
-      return "ROOT_STICKY_CLASS";
-    case protos::pbzero::HeapGraphRoot::ROOT_THREAD_BLOCK:
-      return "ROOT_THREAD_BLOCK";
-    case protos::pbzero::HeapGraphRoot::ROOT_MONITOR_USED:
-      return "ROOT_MONITOR_USED";
-    case protos::pbzero::HeapGraphRoot::ROOT_THREAD_OBJECT:
-      return "ROOT_THREAD_OBJECT";
-    case protos::pbzero::HeapGraphRoot::ROOT_INTERNED_STRING:
-      return "ROOT_INTERNED_STRING";
-    case protos::pbzero::HeapGraphRoot::ROOT_FINALIZING:
-      return "ROOT_FINALIZING";
-    case protos::pbzero::HeapGraphRoot::ROOT_DEBUGGER:
-      return "ROOT_DEBUGGER";
-    case protos::pbzero::HeapGraphRoot::ROOT_REFERENCE_CLEANUP:
-      return "ROOT_REFERENCE_CLEANUP";
-    case protos::pbzero::HeapGraphRoot::ROOT_VM_INTERNAL:
-      return "ROOT_VM_INTERNAL";
-    case protos::pbzero::HeapGraphRoot::ROOT_JNI_MONITOR:
-      return "ROOT_JNI_MONITOR";
-    default:
-      return "ROOT_UNKNOWN";
-  }
-}
-
-const char* HeapGraphTypeKindToString(int32_t type) {
-  switch (type) {
-    case protos::pbzero::HeapGraphType::KIND_NORMAL:
-      return "KIND_NORMAL";
-    case protos::pbzero::HeapGraphType::KIND_NOREFERENCES:
-      return "KIND_NOREFERENCES";
-    case protos::pbzero::HeapGraphType::KIND_STRING:
-      return "KIND_STRING";
-    case protos::pbzero::HeapGraphType::KIND_ARRAY:
-      return "KIND_ARRAY";
-    case protos::pbzero::HeapGraphType::KIND_CLASS:
-      return "KIND_CLASS";
-    case protos::pbzero::HeapGraphType::KIND_CLASSLOADER:
-      return "KIND_CLASSLOADER";
-    case protos::pbzero::HeapGraphType::KIND_DEXCACHE:
-      return "KIND_DEXCACHE";
-    case protos::pbzero::HeapGraphType::KIND_SOFT_REFERENCE:
-      return "KIND_SOFT_REFERENCE";
-    case protos::pbzero::HeapGraphType::KIND_WEAK_REFERENCE:
-      return "KIND_WEAK_REFERENCE";
-    case protos::pbzero::HeapGraphType::KIND_FINALIZER_REFERENCE:
-      return "KIND_FINALIZER_REFERENCE";
-    case protos::pbzero::HeapGraphType::KIND_PHANTOM_REFERENCE:
-      return "KIND_PHANTOM_REFERENCE";
-    default:
-      return "KIND_UNKNOWN";
-  }
-}
-
 // Iterate over a repeated field of varints, independent of whether it is
 // packed or not.
 template <int32_t field_no, typename T, typename F>
@@ -237,8 +171,13 @@
         entry.kind() == protos::pbzero::HeapGraphType::KIND_ARRAY ||
         entry.kind() == protos::pbzero::HeapGraphType::KIND_STRING;
 
-    StringId kind = context_->storage->InternString(
-        HeapGraphTypeKindToString(entry.kind()));
+    protos::pbzero::HeapGraphType::Kind kind =
+        protos::pbzero::HeapGraphType::KIND_UNKNOWN;
+    if (protos::pbzero::HeapGraphType_Kind_MIN <= entry.kind() &&
+        entry.kind() <= protos::pbzero::HeapGraphType_Kind_MAX) {
+      kind = protos::pbzero::HeapGraphType::Kind(entry.kind());
+    }
+
     std::optional<uint64_t> location_id;
     if (entry.has_location_id())
       location_id = entry.location_id();
@@ -265,11 +204,15 @@
   }
   for (auto it = heap_graph.roots(); it; ++it) {
     protos::pbzero::HeapGraphRoot::Decoder entry(*it);
-    const char* str = HeapGraphRootTypeToString(entry.root_type());
-    auto str_view = base::StringView(str);
 
     HeapGraphTracker::SourceRoot src_root;
-    src_root.root_type = context_->storage->InternString(str_view);
+    if (protos::pbzero::HeapGraphRoot_Type_MIN <= entry.root_type() &&
+        entry.root_type() <= protos::pbzero::HeapGraphRoot_Type_MAX) {
+      src_root.root_type =
+          protos::pbzero::HeapGraphRoot::Type(entry.root_type());
+    } else {
+      src_root.root_type = protos::pbzero::HeapGraphRoot::ROOT_UNKNOWN;
+    }
     // grep-friendly: object_ids
     bool parse_error =
         ForEachVarInt<protos::pbzero::HeapGraphRoot::kObjectIdsFieldNumber>(
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index e337113..f8971c6 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -21,6 +21,7 @@
 #include "perfetto/base/flat_set.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "protos/perfetto/trace/profiling/heap_graph.pbzero.h"
 #include "src/trace_processor/importers/proto/profiler_util.h"
 #include "src/trace_processor/tables/profiler_tables_py.h"
 
@@ -57,42 +58,6 @@
   }
 }
 
-base::FlatSet<ObjectTable::Id> GetChildren(TraceStorage* storage,
-                                           ObjectTable::RowReference object) {
-  auto cls_row_ref =
-      *storage->heap_graph_class_table().FindById(object.type_id());
-
-  StringId kind = cls_row_ref.kind();
-  std::optional<StringId> weakref_kind =
-      storage->string_pool().GetId("KIND_WEAK_REFERENCE");
-  std::optional<StringId> softref_kind =
-      storage->string_pool().GetId("KIND_SOFT_REFERENCE");
-  std::optional<StringId> finalizerref_kind =
-      storage->string_pool().GetId("KIND_FINALIZER_REFERENCE");
-  std::optional<StringId> phantomref_kind =
-      storage->string_pool().GetId("KIND_PHANTOM_REFERENCE");
-
-  if ((weakref_kind && kind == *weakref_kind) ||
-      (softref_kind && kind == *softref_kind) ||
-      (finalizerref_kind && kind == *finalizerref_kind) ||
-      (phantomref_kind && kind == *phantomref_kind)) {
-    // Do not follow weak / soft / finalizer / phantom references.
-    return {};
-  }
-
-  base::FlatSet<ObjectTable::Id> children;
-  ForReferenceSet(storage, object,
-                  [object, &children](ReferenceTable::RowReference ref) {
-                    PERFETTO_CHECK(ref.owner_id() == object.id());
-                    auto opt_owned = ref.owned_id();
-                    if (opt_owned) {
-                      children.insert(*opt_owned);
-                    }
-                    return true;
-                  });
-  return children;
-}
-
 struct ClassDescriptor {
   StringId name;
   std::optional<StringId> location;
@@ -176,56 +141,6 @@
 
 }  // namespace
 
-void MarkRoot(TraceStorage* storage,
-              ObjectTable::RowReference row_ref,
-              StringId type) {
-  row_ref.set_root_type(type);
-
-  // DFS to mark reachability for all children
-  std::vector<ObjectTable::RowReference> stack({row_ref});
-  while (!stack.empty()) {
-    ObjectTable::RowReference cur_node = stack.back();
-    stack.pop_back();
-
-    if (cur_node.reachable())
-      continue;
-    cur_node.set_reachable(true);
-
-    for (ObjectTable::Id child_node : GetChildren(storage, cur_node)) {
-      auto child_ref =
-          *storage->mutable_heap_graph_object_table()->FindById(child_node);
-      stack.push_back(child_ref);
-    }
-  }
-}
-
-void UpdateShortestPaths(TraceStorage* storage,
-                         ObjectTable::RowReference row_ref) {
-  // Calculate shortest distance to a GC root.
-  std::deque<std::pair<int32_t, ObjectTable::RowReference>> reachable_nodes{
-      {0, row_ref}};
-  while (!reachable_nodes.empty()) {
-    auto pair = reachable_nodes.front();
-
-    int32_t distance = pair.first;
-    ObjectTable::RowReference cur_row_ref = pair.second;
-
-    reachable_nodes.pop_front();
-    int32_t cur_distance = cur_row_ref.root_distance();
-    if (cur_distance == -1 || cur_distance > distance) {
-      cur_row_ref.set_root_distance(distance);
-
-      for (ObjectTable::Id child_node : GetChildren(storage, cur_row_ref)) {
-        auto child_row_ref =
-            *storage->mutable_heap_graph_object_table()->FindById(child_node);
-        int32_t child_distance = child_row_ref.root_distance();
-        if (child_distance == -1 || child_distance > distance + 1)
-          reachable_nodes.emplace_back(distance + 1, child_row_ref);
-      }
-    }
-  }
-}
-
 std::optional<base::StringView> GetStaticClassTypeName(base::StringView type) {
   static const base::StringView kJavaClassTemplate("java.lang.Class<");
   if (!type.empty() && type.at(type.size() - 1) == '>' &&
@@ -283,7 +198,21 @@
           "libcore.util.NativeAllocationRegistry$CleanerThunk.this$0")),
       native_size_str_id_(
           storage_->InternString("libcore.util.NativeAllocationRegistry.size")),
-      cleaner_next_str_id_(storage_->InternString("sun.misc.Cleaner.next")) {}
+      cleaner_next_str_id_(storage_->InternString("sun.misc.Cleaner.next")) {
+  for (size_t i = 0; i < root_type_string_ids_.size(); i++) {
+    auto val = static_cast<protos::pbzero::HeapGraphRoot::Type>(i);
+    auto str_view =
+        base::StringView(protos::pbzero::HeapGraphRoot_Type_Name(val));
+    root_type_string_ids_[i] = storage_->InternString(str_view);
+  }
+
+  for (size_t i = 0; i < type_kind_string_ids_.size(); i++) {
+    auto val = static_cast<protos::pbzero::HeapGraphType::Kind>(i);
+    auto str_view =
+        base::StringView(protos::pbzero::HeapGraphType_Kind_Name(val));
+    type_kind_string_ids_[i] = storage_->InternString(str_view);
+  }
+}
 
 HeapGraphTracker::SequenceState& HeapGraphTracker::GetOrCreateSequence(
     uint32_t seq_id) {
@@ -429,26 +358,27 @@
   sequence_state.interned_location_names.emplace(intern_id, strid);
 }
 
-void HeapGraphTracker::AddInternedType(uint32_t seq_id,
-                                       uint64_t intern_id,
-                                       StringId strid,
-                                       std::optional<uint64_t> location_id,
-                                       uint64_t object_size,
-                                       std::vector<uint64_t> field_name_ids,
-                                       uint64_t superclass_id,
-                                       uint64_t classloader_id,
-                                       bool no_fields,
-                                       StringId kind) {
+void HeapGraphTracker::AddInternedType(
+    uint32_t seq_id,
+    uint64_t intern_id,
+    StringId strid,
+    std::optional<uint64_t> location_id,
+    uint64_t object_size,
+    std::vector<uint64_t> field_name_ids,
+    uint64_t superclass_id,
+    uint64_t classloader_id,
+    bool no_fields,
+    protos::pbzero::HeapGraphType::Kind kind) {
   SequenceState& sequence_state = GetOrCreateSequence(seq_id);
-  sequence_state.interned_types[intern_id].name = strid;
-  sequence_state.interned_types[intern_id].location_id = location_id;
-  sequence_state.interned_types[intern_id].object_size = object_size;
-  sequence_state.interned_types[intern_id].field_name_ids =
-      std::move(field_name_ids);
-  sequence_state.interned_types[intern_id].superclass_id = superclass_id;
-  sequence_state.interned_types[intern_id].classloader_id = classloader_id;
-  sequence_state.interned_types[intern_id].no_fields = no_fields;
-  sequence_state.interned_types[intern_id].kind = kind;
+  InternedType& type = sequence_state.interned_types[intern_id];
+  type.name = strid;
+  type.location_id = location_id;
+  type.object_size = object_size;
+  type.field_name_ids = std::move(field_name_ids);
+  type.superclass_id = superclass_id;
+  type.classloader_id = classloader_id;
+  type.no_fields = no_fields;
+  type.kind = kind;
 }
 
 void HeapGraphTracker::AddInternedFieldName(uint32_t seq_id,
@@ -618,7 +548,7 @@
     }
     if (location_name)
       type_row_ref.set_location(*location_name);
-    type_row_ref.set_kind(interned_type.kind);
+    type_row_ref.set_kind(InternTypeKindString(interned_type.kind));
 
     base::StringView normalized_type =
         NormalizeTypeName(storage_->GetString(interned_type.name));
@@ -668,8 +598,9 @@
       auto it_and_success = roots_[std::make_pair(sequence_state.current_upid,
                                                   sequence_state.current_ts)]
                                 .emplace(*ptr);
-      if (it_and_success.second)
-        MarkRoot(storage_, row_ref, root.root_type);
+      if (it_and_success.second) {
+        MarkRoot(row_ref, InternRootTypeString(root.root_type));
+      }
     }
   }
 
@@ -817,9 +748,94 @@
   }
 }
 
-void FindPathFromRoot(TraceStorage* storage,
-                      ObjectTable::RowReference row_ref,
-                      PathFromRoot* path) {
+base::FlatSet<ObjectTable::Id> HeapGraphTracker::GetChildren(
+    ObjectTable::RowReference object) {
+  auto cls_row_ref =
+      *storage_->heap_graph_class_table().FindById(object.type_id());
+
+  StringId kind = cls_row_ref.kind();
+
+  bool is_ignored_reference =
+      kind == InternTypeKindString(
+                  protos::pbzero::HeapGraphType::KIND_WEAK_REFERENCE) ||
+      kind == InternTypeKindString(
+                  protos::pbzero::HeapGraphType::KIND_SOFT_REFERENCE) ||
+      kind == InternTypeKindString(
+                  protos::pbzero::HeapGraphType::KIND_FINALIZER_REFERENCE) ||
+      kind == InternTypeKindString(
+                  protos::pbzero::HeapGraphType::KIND_PHANTOM_REFERENCE);
+
+  base::FlatSet<ObjectTable::Id> children;
+  ForReferenceSet(
+      storage_, object,
+      [object, &children, is_ignored_reference,
+       this](ReferenceTable::RowReference ref) {
+        PERFETTO_CHECK(ref.owner_id() == object.id());
+        auto opt_owned = ref.owned_id();
+        if (!opt_owned) {
+          return true;
+        }
+        if (is_ignored_reference && ref.field_name() == referent_str_id_) {
+          // If `object` is a special reference kind, its
+          // "java.lang.ref.Reference.referent" field should be ignored.
+          return true;
+        }
+        children.insert(*opt_owned);
+        return true;
+      });
+  return children;
+}
+
+void HeapGraphTracker::MarkRoot(ObjectTable::RowReference row_ref,
+                                StringId type) {
+  row_ref.set_root_type(type);
+
+  // DFS to mark reachability for all children
+  std::vector<ObjectTable::RowReference> stack({row_ref});
+  while (!stack.empty()) {
+    ObjectTable::RowReference cur_node = stack.back();
+    stack.pop_back();
+
+    if (cur_node.reachable())
+      continue;
+    cur_node.set_reachable(true);
+
+    for (ObjectTable::Id child_node : GetChildren(cur_node)) {
+      auto child_ref =
+          *storage_->mutable_heap_graph_object_table()->FindById(child_node);
+      stack.push_back(child_ref);
+    }
+  }
+}
+
+void HeapGraphTracker::UpdateShortestPaths(ObjectTable::RowReference row_ref) {
+  // Calculate shortest distance to a GC root.
+  std::deque<std::pair<int32_t, ObjectTable::RowReference>> reachable_nodes{
+      {0, row_ref}};
+  while (!reachable_nodes.empty()) {
+    auto pair = reachable_nodes.front();
+
+    int32_t distance = pair.first;
+    ObjectTable::RowReference cur_row_ref = pair.second;
+
+    reachable_nodes.pop_front();
+    int32_t cur_distance = cur_row_ref.root_distance();
+    if (cur_distance == -1 || cur_distance > distance) {
+      cur_row_ref.set_root_distance(distance);
+
+      for (ObjectTable::Id child_node : GetChildren(cur_row_ref)) {
+        auto child_row_ref =
+            *storage_->mutable_heap_graph_object_table()->FindById(child_node);
+        int32_t child_distance = child_row_ref.root_distance();
+        if (child_distance == -1 || child_distance > distance + 1)
+          reachable_nodes.emplace_back(distance + 1, child_row_ref);
+      }
+    }
+  }
+}
+
+void HeapGraphTracker::FindPathFromRoot(ObjectTable::RowReference row_ref,
+                                        PathFromRoot* path) {
   // We have long retention chains (e.g. from LinkedList). If we use the stack
   // here, we risk running out of stack space. This is why we use a vector to
   // simulate the stack.
@@ -844,7 +860,7 @@
 
     ClassTable::Id type_id = object_row_ref.type_id();
 
-    auto type_row_ref = *storage->heap_graph_class_table().FindById(type_id);
+    auto type_row_ref = *storage_->heap_graph_class_table().FindById(type_id);
     std::optional<StringId> opt_class_name_id =
         type_row_ref.deobfuscated_name();
     if (!opt_class_name_id) {
@@ -854,9 +870,9 @@
     StringId class_name_id = *opt_class_name_id;
     std::optional<StringId> root_type = object_row_ref.root_type();
     if (root_type) {
-      class_name_id = storage->InternString(base::StringView(
-          storage->GetString(class_name_id).ToStdString() + " [" +
-          storage->GetString(*root_type).ToStdString() + "]"));
+      class_name_id = storage_->InternString(base::StringView(
+          storage_->GetString(class_name_id).ToStdString() + " [" +
+          storage_->GetString(*root_type).ToStdString() + "]"));
     }
     auto it = path->nodes[parent_id].children.find(class_name_id);
     if (it == path->nodes[parent_id].children.end()) {
@@ -876,15 +892,14 @@
       // size to the relevant node in the resulting tree.
       output_tree_node->size += object_row_ref.self_size();
       output_tree_node->count++;
-      base::FlatSet<ObjectTable::Id> children_set =
-          GetChildren(storage, object_row_ref);
+      base::FlatSet<ObjectTable::Id> children_set = GetChildren(object_row_ref);
       children.assign(children_set.begin(), children_set.end());
       PERFETTO_CHECK(children.size() == children_set.size());
 
       if (object_row_ref.native_size()) {
-        StringId native_class_name_id = storage->InternString(
+        StringId native_class_name_id = storage_->InternString(
             base::StringView(std::string("[native] ") +
-                             storage->GetString(class_name_id).ToStdString()));
+                             storage_->GetString(class_name_id).ToStdString()));
         std::map<StringId, size_t>::iterator native_it;
         bool inserted_new_node;
         std::tie(native_it, inserted_new_node) =
@@ -910,7 +925,7 @@
       PERFETTO_CHECK(i < children.size());
       ObjectTable::Id child = children[i];
       auto child_row_ref =
-          *storage->mutable_heap_graph_object_table()->FindById(child);
+          *storage_->mutable_heap_graph_object_table()->FindById(child);
       if (++i == children.size())
         stack.pop_back();
 
@@ -971,11 +986,11 @@
 
   // First pass to calculate shortest paths
   for (ObjectTable::RowNumber root : roots) {
-    UpdateShortestPaths(storage_, root.ToRowReference(object_table));
+    UpdateShortestPaths(root.ToRowReference(object_table));
   }
   PathFromRoot init_path;
   for (ObjectTable::RowNumber root : roots) {
-    FindPathFromRoot(storage_, root.ToRowReference(object_table), &init_path);
+    FindPathFromRoot(root.ToRowReference(object_table), &init_path);
   }
 
   std::vector<int64_t> node_to_cumulative_size(init_path.nodes.size());
@@ -1047,6 +1062,26 @@
   return false;
 }
 
+StringId HeapGraphTracker::InternRootTypeString(
+    protos::pbzero::HeapGraphRoot::Type root_type) {
+  size_t idx = static_cast<size_t>(root_type);
+  if (idx >= root_type_string_ids_.size()) {
+    idx = static_cast<size_t>(protos::pbzero::HeapGraphRoot::ROOT_UNKNOWN);
+  }
+
+  return root_type_string_ids_[idx];
+}
+
+StringId HeapGraphTracker::InternTypeKindString(
+    protos::pbzero::HeapGraphType::Kind kind) {
+  size_t idx = static_cast<size_t>(kind);
+  if (idx >= type_kind_string_ids_.size()) {
+    idx = static_cast<size_t>(protos::pbzero::HeapGraphType::KIND_UNKNOWN);
+  }
+
+  return type_kind_string_ids_[idx];
+}
+
 HeapGraphTracker::~HeapGraphTracker() = default;
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.h b/src/trace_processor/importers/proto/heap_graph_tracker.h
index c2f184f..f5d3a1b 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.h
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.h
@@ -23,6 +23,7 @@
 #include <utility>
 #include <vector>
 
+#include "perfetto/base/flat_set.h"
 #include "perfetto/ext/base/string_view.h"
 
 #include "protos/perfetto/trace/profiling/heap_graph.pbzero.h"
@@ -55,15 +56,6 @@
   std::set<tables::HeapGraphObjectTable::Id> visited;
 };
 
-void MarkRoot(TraceStorage*,
-              tables::HeapGraphObjectTable::RowReference,
-              StringId type);
-void UpdateShortestPaths(TraceStorage* s,
-                         tables::HeapGraphObjectTable::RowReference row_ref);
-void FindPathFromRoot(TraceStorage* storage,
-                      tables::HeapGraphObjectTable::RowReference,
-                      PathFromRoot* path);
-
 std::optional<base::StringView> GetStaticClassTypeName(base::StringView type);
 size_t NumberOfArrays(base::StringView type);
 NormalizedType GetNormalizedType(base::StringView type);
@@ -89,7 +81,7 @@
   };
 
   struct SourceRoot {
-    StringId root_type;
+    protos::pbzero::HeapGraphRoot::Type root_type;
     std::vector<uint64_t> object_ids;
   };
 
@@ -114,7 +106,7 @@
                        uint64_t superclass_id,
                        uint64_t classloader_id,
                        bool no_fields,
-                       StringId kind);
+                       protos::pbzero::HeapGraphType::Kind kind);
   void AddInternedFieldName(uint32_t seq_id,
                             uint64_t intern_id,
                             base::StringView str);
@@ -162,7 +154,7 @@
     uint64_t superclass_id;
     bool no_fields;
     uint64_t classloader_id;
-    StringId kind;
+    protos::pbzero::HeapGraphType::Kind kind;
   };
   struct SequenceState {
     UniquePid current_upid = 0;
@@ -218,6 +210,8 @@
   InternedType* GetSuperClass(SequenceState* sequence_state,
                               const InternedType* current_type);
   bool IsTruncated(UniquePid upid, int64_t ts);
+  StringId InternRootTypeString(protos::pbzero::HeapGraphRoot::Type);
+  StringId InternTypeKindString(protos::pbzero::HeapGraphType::Kind);
 
   // Returns the object pointed to by `field` in `obj`.
   std::optional<tables::HeapGraphObjectTable::Id> GetReferenceByFieldName(
@@ -231,6 +225,13 @@
   // all the other tables have been fully populated.
   void PopulateNativeSize(const SequenceState& seq);
 
+  base::FlatSet<tables::HeapGraphObjectTable::Id> GetChildren(
+      tables::HeapGraphObjectTable::RowReference);
+  void MarkRoot(tables::HeapGraphObjectTable::RowReference, StringId type);
+  void UpdateShortestPaths(tables::HeapGraphObjectTable::RowReference row_ref);
+  void FindPathFromRoot(tables::HeapGraphObjectTable::RowReference,
+                        PathFromRoot* path);
+
   TraceStorage* const storage_;
   std::map<uint32_t, SequenceState> sequence_state_;
 
@@ -253,6 +254,16 @@
   StringId cleaner_thunk_this0_str_id_;
   StringId native_size_str_id_;
   StringId cleaner_next_str_id_;
+
+  std::array<StringId, 15> root_type_string_ids_ = {};
+  static_assert(protos::pbzero::HeapGraphRoot_Type_MIN == 0);
+  static_assert(protos::pbzero::HeapGraphRoot_Type_MAX + 1 ==
+                std::tuple_size<decltype(root_type_string_ids_)>{});
+
+  std::array<StringId, 12> type_kind_string_ids_ = {};
+  static_assert(protos::pbzero::HeapGraphType_Kind_MIN == 0);
+  static_assert(protos::pbzero::HeapGraphType_Kind_MAX + 1 ==
+                std::tuple_size<decltype(type_kind_string_ids_)>{});
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc b/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
index 15be4ac..8e9ee32 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
@@ -67,8 +67,6 @@
 
   HeapGraphTracker tracker(context.storage.get());
 
-  StringPool::Id normal_kind = context.storage->InternString("KIND_NORMAL");
-
   constexpr uint64_t kLocation = 0;
   tracker.AddInternedLocationName(kSeqId, kLocation,
                                   context.storage->InternString("location"));
@@ -94,34 +92,34 @@
       kSeqId, kTypeBitmap,
       context.storage->InternString("android.graphics.Bitmap"), kLocation,
       /*object_size=*/0,
-      /*reference_field_name_ids=*/{}, /*superclass_id=*/0,
-      /*classloader_id=*/0, /*no_reference_fields=*/false,
-      /*kind=*/normal_kind);
+      /*field_name_ids=*/{}, /*superclass_id=*/0,
+      /*classloader_id=*/0, /*no_fields=*/false,
+      protos::pbzero::HeapGraphType::KIND_NORMAL);
 
-  tracker.AddInternedType(
-      kSeqId, kTypeCleaner, context.storage->InternString("sun.misc.Cleaner"),
-      kLocation, /*object_size=*/0,
-      /*reference_field_name_ids=*/{kReferent, kThunk, kNext},
-      /*superclass_id=*/0,
-      /*classloader_id=*/0, /*no_reference_fields=*/false,
-      /*kind=*/normal_kind);
+  tracker.AddInternedType(kSeqId, kTypeCleaner,
+                          context.storage->InternString("sun.misc.Cleaner"),
+                          kLocation, /*object_size=*/0,
+                          /*field_name_ids=*/{kReferent, kThunk, kNext},
+                          /*superclass_id=*/0,
+                          /*classloader_id=*/0, /*no_fields=*/false,
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
 
   tracker.AddInternedType(
       kSeqId, kTypeCleanerThunk,
       context.storage->InternString(
           "libcore.util.NativeAllocationRegistry$CleanerThunk"),
       kLocation, /*object_size=*/0,
-      /*reference_field_name_ids=*/{kThis0}, /*superclass_id=*/0,
-      /*classloader_id=*/0, /*no_reference_fields=*/false,
-      /*kind=*/normal_kind);
+      /*field_name_ids=*/{kThis0}, /*superclass_id=*/0,
+      /*classloader_id=*/0, /*no_fields=*/false,
+      protos::pbzero::HeapGraphType::KIND_NORMAL);
 
   tracker.AddInternedType(
       kSeqId, kTypeNativeAllocationRegistry,
       context.storage->InternString("libcore.util.NativeAllocationRegistry"),
       kLocation, /*object_size=*/0,
-      /*reference_field_name_ids=*/{}, /*superclass_id=*/0,
-      /*classloader_id=*/0, /*no_reference_fields=*/false,
-      /*kind=*/normal_kind);
+      /*field_name_ids=*/{}, /*superclass_id=*/0,
+      /*classloader_id=*/0, /*no_fields=*/false,
+      protos::pbzero::HeapGraphType::KIND_NORMAL);
 
   enum Objects : uint64_t {
     kObjBitmap = 1,
@@ -217,18 +215,13 @@
   constexpr uint64_t kY = 2;
   constexpr uint64_t kA = 3;
   constexpr uint64_t kB = 4;
-  constexpr uint64_t kWeakRef = 5;
 
   base::StringView field = base::StringView("foo");
   StringPool::Id x = context.storage->InternString("X");
   StringPool::Id y = context.storage->InternString("Y");
   StringPool::Id a = context.storage->InternString("A");
   StringPool::Id b = context.storage->InternString("B");
-  StringPool::Id weak_ref = context.storage->InternString("WeakReference");
 
-  StringPool::Id normal_kind = context.storage->InternString("KIND_NORMAL");
-  StringPool::Id weak_ref_kind =
-      context.storage->InternString("KIND_WEAK_REFERENCE");
   tracker.AddInternedFieldName(kSeqId, kField, field);
 
   tracker.AddInternedLocationName(kSeqId, kLocation,
@@ -236,35 +229,19 @@
   tracker.AddInternedType(kSeqId, kX, x, kLocation, /*object_size=*/0,
                           /*field_name_ids=*/{}, /*superclass_id=*/0,
                           /*classloader_id=*/0, /*no_fields=*/false,
-                          /*kind=*/normal_kind);
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
   tracker.AddInternedType(kSeqId, kY, y, kLocation, /*object_size=*/0,
                           /*field_name_ids=*/{}, /*superclass_id=*/0,
                           /*classloader_id=*/0, /*no_fields=*/false,
-                          /*kind=*/normal_kind);
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
   tracker.AddInternedType(kSeqId, kA, a, kLocation, /*object_size=*/0,
                           /*field_name_ids=*/{}, /*superclass_id=*/0,
                           /*classloader_id=*/0, /*no_fields=*/false,
-                          /*kind=*/normal_kind);
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
   tracker.AddInternedType(kSeqId, kB, b, kLocation, /*object_size=*/0,
                           /*field_name_ids=*/{}, /*superclass_id=*/0,
                           /*classloader_id=*/0, /*no_fields=*/false,
-                          /*kind=*/normal_kind);
-  tracker.AddInternedType(kSeqId, kWeakRef, weak_ref, kLocation,
-                          /*object_size=*/0,
-                          /*field_name_ids=*/{}, /*superclass_id=*/0,
-                          /*classloader_id=*/0, /*no_fields=*/false,
-                          /*kind=*/weak_ref_kind);
-  {
-    HeapGraphTracker::SourceObject obj;
-    obj.object_id = 999;
-    obj.self_size = 999;
-    obj.type_id = kWeakRef;
-    obj.field_name_ids = {kField};
-    obj.referred_objects = {5};
-
-    tracker.AddObject(kSeqId, kPid, kTimestamp, std::move(obj));
-  }
-
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
   {
     HeapGraphTracker::SourceObject obj;
     obj.object_id = 1;
@@ -312,9 +289,8 @@
   }
 
   HeapGraphTracker::SourceRoot root;
-  root.root_type = context.storage->InternString("ROOT");
+  root.root_type = protos::pbzero::HeapGraphRoot::ROOT_UNKNOWN;
   root.object_ids.emplace_back(1);
-  root.object_ids.emplace_back(999);
   tracker.AddRoot(kSeqId, kPid, kTimestamp, root);
 
   tracker.FinalizeProfile(kSeqId);
@@ -323,16 +299,129 @@
   ASSERT_NE(flame, nullptr);
 
   auto cumulative_sizes = flame->cumulative_size().ToVectorForTesting();
-  EXPECT_THAT(cumulative_sizes, UnorderedElementsAre(15, 4, 14, 5, 999));
+  EXPECT_THAT(cumulative_sizes, UnorderedElementsAre(15, 4, 14, 5));
 
   auto cumulative_counts = flame->cumulative_count().ToVectorForTesting();
-  EXPECT_THAT(cumulative_counts, UnorderedElementsAre(5, 4, 1, 1, 1));
+  EXPECT_THAT(cumulative_counts, UnorderedElementsAre(5, 4, 1, 1));
 
   auto sizes = flame->size().ToVectorForTesting();
-  EXPECT_THAT(sizes, UnorderedElementsAre(1, 5, 4, 5, 999));
+  EXPECT_THAT(sizes, UnorderedElementsAre(1, 5, 4, 5));
 
   auto counts = flame->count().ToVectorForTesting();
-  EXPECT_THAT(counts, UnorderedElementsAre(1, 2, 1, 1, 1));
+  EXPECT_THAT(counts, UnorderedElementsAre(1, 2, 1, 1));
+}
+
+TEST(HeapGraphTrackerTest, BuildFlamegraphWeakReferences) {
+  // Regression test for http://b.corp.google.com/issues/302662734:
+  // For weak (and other) references, we should not follow the
+  // `java.lang.ref.Reference.referent` field, but we should follow other
+  // fields.
+  //
+  //                                   2@A 4@B
+  //  (java.lang.ref.Reference.referent) \ / (X.other)
+  //                                     1@X (extends WeakReference)
+
+  constexpr uint64_t kSeqId = 1;
+  constexpr UniquePid kPid = 1;
+  constexpr int64_t kTimestamp = 1;
+
+  TraceProcessorContext context;
+  context.storage.reset(new TraceStorage());
+  context.process_tracker.reset(new ProcessTracker(&context));
+  context.process_tracker->GetOrCreateProcess(kPid);
+
+  HeapGraphTracker tracker(context.storage.get());
+
+  constexpr uint64_t kLocation = 0;
+
+  base::StringView referent_field =
+      base::StringView("java.lang.ref.Reference.referent");
+  constexpr uint64_t kReferentField = 1;
+  base::StringView other_field = base::StringView("X.other");
+  constexpr uint64_t kOtherField = 2;
+
+  constexpr uint64_t kX = 1;
+  StringPool::Id x = context.storage->InternString("X");
+  constexpr uint64_t kA = 2;
+  StringPool::Id a = context.storage->InternString("A");
+  constexpr uint64_t kB = 4;
+  StringPool::Id b = context.storage->InternString("B");
+  constexpr uint64_t kWeakRef = 5;
+  StringPool::Id weak_ref = context.storage->InternString("WeakReference");
+
+  tracker.AddInternedFieldName(kSeqId, kReferentField, referent_field);
+  tracker.AddInternedFieldName(kSeqId, kOtherField, other_field);
+
+  tracker.AddInternedLocationName(kSeqId, kLocation,
+                                  context.storage->InternString("location"));
+
+  tracker.AddInternedType(kSeqId, kWeakRef, weak_ref, kLocation,
+                          /*object_size=*/0,
+                          /*field_name_ids=*/{kReferentField},
+                          /*superclass_id=*/0,
+                          /*classloader_id=*/0, /*no_fields=*/false,
+                          protos::pbzero::HeapGraphType::KIND_WEAK_REFERENCE);
+  tracker.AddInternedType(kSeqId, kX, x, kLocation,
+                          /*object_size=*/0,
+                          /*field_name_ids=*/{kOtherField},
+                          /*superclass_id=*/kWeakRef,
+                          /*classloader_id=*/0, /*no_fields=*/false,
+                          protos::pbzero::HeapGraphType::KIND_WEAK_REFERENCE);
+  tracker.AddInternedType(kSeqId, kA, a, kLocation, /*object_size=*/0,
+                          /*field_name_ids=*/{}, /*superclass_id=*/0,
+                          /*classloader_id=*/0, /*no_fields=*/false,
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
+  tracker.AddInternedType(kSeqId, kB, b, kLocation, /*object_size=*/0,
+                          /*field_name_ids=*/{}, /*superclass_id=*/0,
+                          /*classloader_id=*/0, /*no_fields=*/false,
+                          protos::pbzero::HeapGraphType::KIND_NORMAL);
+  {
+    HeapGraphTracker::SourceObject obj;
+    obj.object_id = 1;
+    obj.self_size = 1;
+    obj.type_id = kX;
+    obj.referred_objects = {/*X.other*/ 4,
+                            /*java.lang.ref.Reference.referent*/ 2};
+    tracker.AddObject(kSeqId, kPid, kTimestamp, std::move(obj));
+  }
+
+  {
+    HeapGraphTracker::SourceObject obj;
+    obj.object_id = 2;
+    obj.self_size = 2;
+    obj.type_id = kA;
+    tracker.AddObject(kSeqId, kPid, kTimestamp, std::move(obj));
+  }
+
+  {
+    HeapGraphTracker::SourceObject obj;
+    obj.object_id = 4;
+    obj.self_size = 4;
+    obj.type_id = kB;
+    tracker.AddObject(kSeqId, kPid, kTimestamp, std::move(obj));
+  }
+
+  HeapGraphTracker::SourceRoot root;
+  root.root_type = protos::pbzero::HeapGraphRoot::ROOT_UNKNOWN;
+  root.object_ids.emplace_back(1);
+  tracker.AddRoot(kSeqId, kPid, kTimestamp, root);
+
+  tracker.FinalizeProfile(kSeqId);
+  std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> flame =
+      tracker.BuildFlamegraph(kPid, kTimestamp);
+  ASSERT_NE(flame, nullptr);
+
+  auto cumulative_sizes = flame->cumulative_size().ToVectorForTesting();
+  EXPECT_THAT(cumulative_sizes, UnorderedElementsAre(4, 4 + 1));
+
+  auto cumulative_counts = flame->cumulative_count().ToVectorForTesting();
+  EXPECT_THAT(cumulative_counts, UnorderedElementsAre(1, 1 + 1));
+
+  auto sizes = flame->size().ToVectorForTesting();
+  EXPECT_THAT(sizes, UnorderedElementsAre(1, 4));
+
+  auto counts = flame->count().ToVectorForTesting();
+  EXPECT_THAT(counts, UnorderedElementsAre(1, 1));
 }
 
 static const char kArray[] = "X[]";
diff --git a/src/trace_processor/metrics/metrics.cc b/src/trace_processor/metrics/metrics.cc
index d141d68..ddd9e24 100644
--- a/src/trace_processor/metrics/metrics.cc
+++ b/src/trace_processor/metrics/metrics.cc
@@ -729,7 +729,7 @@
     auto output_query =
         "SELECT * FROM " + sql_metric.output_table_name.value() + ";";
     PERFETTO_TP_TRACE(
-        metatrace::Category::QUERY, "COMPUTE_METRIC_QUERY",
+        metatrace::Category::QUERY_TIMELINE, "COMPUTE_METRIC_QUERY",
         [&](metatrace::Record* r) { r->AddArg("SQL", output_query); });
 
     auto it = engine->ExecuteUntilLastStatement(
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index 99223c0..6c31943 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -23,6 +23,7 @@
     "android_batt.sql",
     "android_binder.sql",
     "android_blocking_calls_cuj_metric.sql",
+    "android_boot.sql",
     "android_camera.sql",
     "android_camera_unagg.sql",
     "android_cpu.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_boot.sql b/src/trace_processor/metrics/sql/android/android_boot.sql
new file mode 100644
index 0000000..46caea8
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_boot.sql
@@ -0,0 +1,51 @@
+--
+-- 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 android.process_metadata;
+
+CREATE PERFETTO FUNCTION get_durations(process_name STRING)
+RETURNS TABLE(uint_sleep_dur LONG, total_dur LONG) AS
+SELECT
+    SUM(CASE WHEN thread_state.state="D" then thread_state.dur ELSE 0 END) AS uint_sleep_dur,
+    SUM(thread_state.dur) as total_dur
+FROM android_process_metadata
+INNER JOIN thread ON thread.upid=android_process_metadata.upid
+INNER JOIN thread_state ON thread.utid=thread_state.utid WHERE android_process_metadata.process_name=$process_name;
+
+DROP VIEW IF EXISTS android_boot_output;
+CREATE VIEW android_boot_output AS
+SELECT AndroidBootMetric(
+    'system_server_durations', (
+        SELECT NULL_IF_EMPTY(ProcessStateDurations(
+            'total_dur', total_dur,
+            'uninterruptible_sleep_dur', uint_sleep_dur))
+        FROM get_durations('system_server')),
+    'systemui_durations', (
+        SELECT NULL_IF_EMPTY(ProcessStateDurations(
+            'total_dur', total_dur,
+            'uninterruptible_sleep_dur', uint_sleep_dur))
+        FROM get_durations('com.android.systemui')),
+    'launcher_durations', (
+        SELECT NULL_IF_EMPTY(ProcessStateDurations(
+            'total_dur', total_dur,
+            'uninterruptible_sleep_dur', uint_sleep_dur))
+        FROM get_durations('com.google.android.apps.nexuslauncher')),
+    'gms_durations', (
+        SELECT NULL_IF_EMPTY(ProcessStateDurations(
+            'total_dur', total_dur,
+            'uninterruptible_sleep_dur', uint_sleep_dur))
+        FROM get_durations('com.google.android.gms.persistent'))
+);
diff --git a/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql b/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
index e03dca9..ec17f42 100644
--- a/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
+++ b/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
@@ -32,6 +32,8 @@
        s.name GLOB 'NotificationStackScrollLayout#onMeasure'
     OR s.name GLOB 'NotificationToplineView#onMeasure'
     OR s.name GLOB 'ExpNotRow#*'
+    OR s.name GLOB 'NotificationShadeWindowView#onMeasure'
+    OR s.name GLOB 'ImageFloatingTextView#onMeasure'
 )
 GROUP BY s.name;
 
diff --git a/src/trace_processor/perfetto_sql/engine/BUILD.gn b/src/trace_processor/perfetto_sql/engine/BUILD.gn
index 9374bf6..f3451e0 100644
--- a/src/trace_processor/perfetto_sql/engine/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/engine/BUILD.gn
@@ -26,6 +26,8 @@
     "perfetto_sql_engine.h",
     "perfetto_sql_parser.cc",
     "perfetto_sql_parser.h",
+    "perfetto_sql_preprocessor.cc",
+    "perfetto_sql_preprocessor.h",
     "runtime_table_function.cc",
     "runtime_table_function.h",
   ]
@@ -50,6 +52,8 @@
   sources = [
     "perfetto_sql_engine_unittest.cc",
     "perfetto_sql_parser_unittest.cc",
+    "perfetto_sql_preprocessor_unittest.cc",
+    "perfetto_sql_test_utils.h",
   ]
   deps = [
     ":engine",
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index 66be287..3e88ada 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -320,7 +320,7 @@
   }
 
   base::Status Run(Memoizer::MemoizedArgs initial_args) {
-    PERFETTO_TP_TRACE(metatrace::Category::FUNCTION,
+    PERFETTO_TP_TRACE(metatrace::Category::FUNCTION_CALL,
                       "UNROLL_RECURSIVE_FUNCTION_CALL",
                       [&](metatrace::Record* r) {
                         r->AddArg("Function", prototype_.function_name);
@@ -336,8 +336,8 @@
         state_ = State::kComputingFirstPass;
         Memoizer::MemoizedArgs args = first_pass_.front();
 
-        PERFETTO_TP_TRACE(metatrace::Category::FUNCTION, "SQL_FUNCTION_CALL",
-                          [&](metatrace::Record* r) {
+        PERFETTO_TP_TRACE(metatrace::Category::FUNCTION_CALL,
+                          "SQL_FUNCTION_CALL", [&](metatrace::Record* r) {
                             r->AddArg("Function", prototype_.function_name);
                             r->AddArg("Type", "UnrollRecursiveCall_FirstPass");
                             r->AddArg("Arg 0", std::to_string(args));
@@ -352,7 +352,7 @@
       state_ = State::kComputingSecondPass;
       Memoizer::MemoizedArgs args = second_pass_.top();
 
-      PERFETTO_TP_TRACE(metatrace::Category::FUNCTION, "SQL_FUNCTION_CALL",
+      PERFETTO_TP_TRACE(metatrace::Category::FUNCTION_CALL, "SQL_FUNCTION_CALL",
                         [&](metatrace::Record* r) {
                           r->AddArg("Function", prototype_.function_name);
                           r->AddArg("Type", "UnrollRecursiveCall_SecondPass");
@@ -640,7 +640,7 @@
   }
 
   PERFETTO_TP_TRACE(
-      metatrace::Category::FUNCTION, "SQL_FUNCTION_CALL",
+      metatrace::Category::FUNCTION_CALL, "SQL_FUNCTION_CALL",
       [state, argv](metatrace::Record* r) {
         r->AddArg("Function", state->prototype().function_name.c_str());
         for (uint32_t i = 0; i < state->prototype().arguments.size(); ++i) {
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 b34f556..1efd12d 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.cc
@@ -16,10 +16,12 @@
 
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
 
+#include <cctype>
 #include <memory>
 #include <optional>
 #include <string>
 #include <variant>
+#include <vector>
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
@@ -28,6 +30,7 @@
 #include "src/trace_processor/perfetto_sql/engine/created_function.h"
 #include "src/trace_processor/perfetto_sql/engine/function_util.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 #include "src/trace_processor/sqlite/db_sqlite_table.h"
 #include "src/trace_processor/sqlite/scoped_db.h"
@@ -36,6 +39,31 @@
 #include "src/trace_processor/tp_metatrace.h"
 #include "src/trace_processor/util/status_macros.h"
 
+// Implementation details
+// ----------------------
+//
+// The execution of PerfettoSQL statements is the joint responsibility of
+// several classes which all are linked together in the following way:
+//
+//  PerfettoSqlEngine -> PerfettoSqlParser -> PerfettoSqlPreprocessor
+//
+// The responsibility of each of these classes is as follows:
+//
+// * PerfettoSqlEngine: this class is responsible for the end-to-end processing
+//   of statements. It calls into PerfettoSqlParser to incrementally receive
+//   parsed SQL statements and then executes them. If the statement is a
+//   PerfettoSQL-only statement, the execution happens entirely in this class.
+//   Otherwise, if the statement is a valid SQLite statement, SQLite is called
+//   into to perform the execution.
+// * PerfettoSqlParser: this class is responsible for taking a chunk of SQL and
+//   incrementally converting them into parsed SQL statement. The parser calls
+//   into the PerfettoSqlPreprocessor to split the SQL chunk into a statement
+//   and perform any macro expansion. It then tries to parse any
+//   PerfettoSQL-only statements into their component parts and leaves SQLite
+//   statements as-is for execution by SQLite.
+// * PerfettoSqlPreprocessor: this class is responsible for taking a chunk of
+//   SQL and breaking them into statements, while also expanding any macros
+//   which might be present inside.
 namespace perfetto {
 namespace trace_processor {
 namespace {
@@ -91,6 +119,13 @@
   return status;
 }
 
+// This function is used when the the PerfettoSQL has been fully executed by the
+// PerfettoSqlEngine and a SqlSoruce is needed for SQLite to execute.
+SqlSource RewriteToDummySql(const SqlSource& source) {
+  return source.RewriteAllIgnoreExisting(
+      SqlSource::FromTraceProcessorImplementation("SELECT 0 WHERE 0"));
+}
+
 }  // namespace
 
 PerfettoSqlEngine::PerfettoSqlEngine(StringPool* pool)
@@ -187,7 +222,7 @@
   //    statement for the last valid statement.
   std::optional<SqliteEngine::PreparedStatement> res;
   ExecutionStats stats;
-  PerfettoSqlParser parser(std::move(sql_source));
+  PerfettoSqlParser parser(std::move(sql_source), macros_);
   while (parser.Next()) {
     std::optional<SqlSource> source;
     if (auto* cf = std::get_if<PerfettoSqlParser::CreateFunction>(
@@ -200,17 +235,16 @@
                    &parser.statement())) {
       RETURN_IF_ERROR(AddTracebackIfNeeded(
           RegisterRuntimeTable(cst->name, cst->sql), parser.statement_sql()));
-      // Since the rest of the code requires a statement, just use a no-value
-      // dummy statement.
-      source = parser.statement_sql().FullRewrite(
-          SqlSource::FromTraceProcessorImplementation("SELECT 0 WHERE 0"));
+      source = RewriteToDummySql(parser.statement_sql());
     } else if (auto* include = std::get_if<PerfettoSqlParser::Include>(
                    &parser.statement())) {
       RETURN_IF_ERROR(ExecuteInclude(*include, parser));
-      // Since the rest of the code requires a statement, just use a no-value
-      // dummy statement.
-      source = parser.statement_sql().FullRewrite(
-          SqlSource::FromTraceProcessorImplementation("SELECT 0 WHERE 0"));
+      source = RewriteToDummySql(parser.statement_sql());
+    } else if (auto* macro = std::get_if<PerfettoSqlParser::CreateMacro>(
+                   &parser.statement())) {
+      auto sql = macro->sql;
+      RETURN_IF_ERROR(ExecuteCreateMacro(*macro));
+      source = RewriteToDummySql(sql);
     } else {
       // If none of the above matched, this must just be an SQL statement
       // directly executable by SQLite.
@@ -223,7 +257,7 @@
     // Try to get SQLite to prepare the statement.
     std::optional<SqliteEngine::PreparedStatement> cur_stmt;
     {
-      PERFETTO_TP_TRACE(metatrace::Category::QUERY, "QUERY_PREPARE");
+      PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE, "QUERY_PREPARE");
       auto stmt = engine_->PrepareStatement(std::move(*source));
       RETURN_IF_ERROR(stmt.status());
       cur_stmt = std::move(stmt);
@@ -238,9 +272,11 @@
     // the previous statement so we don't have two clashing statements (e.g.
     // SELECT * FROM v and DROP VIEW v) partially stepped into.
     if (res && !res->IsDone()) {
-      PERFETTO_TP_TRACE(metatrace::Category::QUERY, "STMT_STEP_UNTIL_DONE",
+      PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE,
+                        "STMT_STEP_UNTIL_DONE",
                         [&res](metatrace::Record* record) {
-                          record->AddArg("SQL", res->expanded_sql());
+                          record->AddArg("Original SQL", res->original_sql());
+                          record->AddArg("Executed SQL", res->sql());
                         });
       while (res->Step()) {
       }
@@ -253,11 +289,14 @@
     // Step the newly prepared statement once. This is considered to be
     // "executing" the statement.
     {
-      PERFETTO_TP_TRACE(metatrace::Category::TOPLEVEL, "STMT_FIRST_STEP",
+      PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE, "STMT_FIRST_STEP",
                         [&res](metatrace::Record* record) {
-                          record->AddArg("SQL", res->expanded_sql());
+                          record->AddArg("Original SQL", res->original_sql());
+                          record->AddArg("Executed SQL", res->sql());
                         });
-      PERFETTO_DLOG("Executing statement: %s", res->sql());
+      PERFETTO_DLOG("Executing statement");
+      PERFETTO_DLOG("Original SQL: %s", res->original_sql());
+      PERFETTO_DLOG("Executed SQL: %s", res->sql());
       res->Step();
       RETURN_IF_ERROR(res->status());
     }
@@ -411,8 +450,8 @@
     const PerfettoSqlParser::Include& include,
     const PerfettoSqlParser& parser) {
   std::string key = include.key;
-  PERFETTO_TP_TRACE(metatrace::Category::TOPLEVEL, "Import",
-                    [key](metatrace::Record* r) { r->AddArg("Import", key); });
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_TIMELINE, "Include",
+                    [key](metatrace::Record* r) { r->AddArg("Module", key); });
   std::string module_name = sql_modules::GetModuleName(key);
   auto module = FindModule(module_name);
   if (!module)
@@ -447,11 +486,7 @@
   if (!cf.is_table) {
     RETURN_IF_ERROR(
         RegisterSqlFunction(cf.replace, cf.prototype, cf.returns, cf.sql));
-
-    // Since the rest of the code requires a statement, just use a no-value
-    // dummy statement.
-    return parser.statement_sql().FullRewrite(
-        SqlSource::FromTraceProcessorImplementation("SELECT 0 WHERE 0"));
+    return RewriteToDummySql(parser.statement_sql());
   }
 
   RuntimeTableFunction::State state{cf.prototype, cf.sql, {}, {}, std::nullopt};
@@ -565,10 +600,59 @@
 
   base::StackString<1024> create(
       "CREATE VIRTUAL TABLE %s USING runtime_table_function", fn_name.c_str());
-  return cf.sql.FullRewrite(
+  return cf.sql.RewriteAllIgnoreExisting(
       SqlSource::FromTraceProcessorImplementation(create.ToStdString()));
 }
 
+base::Status PerfettoSqlEngine::ExecuteCreateMacro(
+    const PerfettoSqlParser::CreateMacro& create_macro) {
+  // Check that the argument types is one of the allowed types.
+  for (const auto& [name, type] : create_macro.args) {
+    std::string lower_type = base::ToLower(type.sql());
+    if (lower_type != "tableorsubquery" && lower_type != "expr") {
+      // TODO(lalitm): add a link to create macro documentation.
+      return base::ErrStatus(
+          "%sMacro %s argument %s is unkown type %s. Allowed types: "
+          "TableOrSubquery, Expr",
+          type.AsTraceback(0).c_str(), create_macro.name.sql().c_str(),
+          name.sql().c_str(), type.sql().c_str());
+    }
+  }
+  std::string lower_return = base::ToLower(create_macro.returns.sql());
+  if (lower_return != "tableorsubquery" && lower_return != "expr") {
+    // TODO(lalitm): add a link to create macro documentation.
+    return base::ErrStatus(
+        "%sMacro %s return type %s is unknown. Allowed types: "
+        "TableOrSubquery, Expr",
+        create_macro.returns.AsTraceback(0).c_str(),
+        create_macro.name.sql().c_str(), create_macro.returns.sql().c_str());
+  }
+
+  std::vector<std::string> args;
+  for (const auto& arg : create_macro.args) {
+    args.push_back(arg.first.sql());
+  }
+  PerfettoSqlPreprocessor::Macro macro{
+      create_macro.replace,
+      create_macro.name.sql(),
+      std::move(args),
+      create_macro.sql,
+  };
+  if (auto it = macros_.Find(create_macro.name.sql()); it) {
+    if (!create_macro.replace) {
+      // TODO(lalitm): add a link to create macro documentation.
+      return base::ErrStatus("%sMacro already exists",
+                             create_macro.name.AsTraceback(0).c_str());
+    }
+    *it = std::move(macro);
+    return base::OkStatus();
+  }
+  std::string name = macro.name;
+  auto it_and_inserted = macros_.Insert(std::move(name), std::move(macro));
+  PERFETTO_CHECK(it_and_inserted.second);
+  return base::OkStatus();
+}
+
 RuntimeTableFunction::State* PerfettoSqlEngine::GetRuntimeTableFunctionState(
     const std::string& name) const {
   auto it = runtime_table_fn_states_.Find(base::ToLower(name));
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
index f4a0abd..29c8206 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/status_or.h"
 #include "src/trace_processor/db/runtime_table.h"
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/perfetto_sql/engine/runtime_table_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/functions/sql_function.h"
 #include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
@@ -151,12 +152,15 @@
   // Registers a SQL-defined trace processor C++ table with SQLite.
   base::Status RegisterRuntimeTable(std::string name, SqlSource sql);
 
+  base::Status ExecuteCreateMacro(const PerfettoSqlParser::CreateMacro&);
+
   std::unique_ptr<QueryCache> query_cache_;
   StringPool* pool_ = nullptr;
   base::FlatHashMap<std::string, std::unique_ptr<RuntimeTableFunction::State>>
       runtime_table_fn_states_;
   base::FlatHashMap<std::string, std::unique_ptr<RuntimeTable>> runtime_tables_;
   base::FlatHashMap<std::string, sql_modules::RegisteredModule> modules_;
+  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
   std::unique_ptr<SqliteEngine> engine_;
 };
 
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
index 16a9726..d577ea0 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_engine_unittest.cc
@@ -109,6 +109,24 @@
   ASSERT_TRUE(res.ok());
 }
 
+TEST_F(PerfettoSqlEngineTest, CreateMacro) {
+  auto res_create = engine_.Execute(SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo() RETURNS TableOrSubquery AS select 42 AS x"));
+  ASSERT_TRUE(res_create.ok()) << res_create.status().c_message();
+
+  res_create = engine_.Execute(SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO bar(x TableOrSubquery) RETURNS TableOrSubquery AS "
+      "select * from $x"));
+  ASSERT_TRUE(res_create.ok()) << res_create.status().c_message();
+
+  auto res = engine_.ExecuteUntilLastStatement(
+      SqlSource::FromExecuteQuery("bar!((foo!()))"));
+  ASSERT_TRUE(res.ok()) << res.status().c_message();
+  ASSERT_FALSE(res->stmt.IsDone());
+  ASSERT_EQ(sqlite3_column_int64(res->stmt.sqlite_stmt(), 0), 42);
+  ASSERT_FALSE(res->stmt.Step());
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
index a0b2688..33fc49b 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.cc
@@ -17,13 +17,20 @@
 #include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
 
 #include <algorithm>
+#include <cctype>
 #include <functional>
 #include <optional>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_tokenizer.h"
 #include "src/trace_processor/util/status_macros.h"
@@ -78,13 +85,38 @@
                       std::not_fn(IsValidModuleWord)) == packages.end();
 }
 
+std::string SerializeArgs(std::vector<std::pair<SqlSource, SqlSource>> args) {
+  bool comma = false;
+  std::string serialized;
+  for (const auto& [name, type] : args) {
+    if (comma) {
+      serialized.append(", ");
+    }
+    comma = true;
+    serialized.append(name.sql().c_str());
+    serialized.push_back(' ');
+    serialized.append(type.sql().c_str());
+  }
+  return serialized;
+}
+
 }  // namespace
 
-PerfettoSqlParser::PerfettoSqlParser(SqlSource sql)
-    : tokenizer_(std::move(sql)) {}
+PerfettoSqlParser::PerfettoSqlParser(
+    SqlSource source,
+    const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>&
+        macros)
+    : preprocessor_(std::move(source), macros),
+      tokenizer_(SqlSource::FromTraceProcessorImplementation("")) {}
 
 bool PerfettoSqlParser::Next() {
-  PERFETTO_DCHECK(status_.ok());
+  PERFETTO_CHECK(status_.ok());
+
+  if (!preprocessor_.NextStatement()) {
+    status_ = preprocessor_.status();
+    return false;
+  }
+  tokenizer_.Reset(preprocessor_.statement());
 
   State state = State::kStmtStart;
   std::optional<Token> first_non_space_token;
@@ -120,7 +152,9 @@
 
     switch (state) {
       case State::kPassthrough:
-        break;
+        statement_ = SqliteSql{};
+        statement_sql_ = preprocessor_.statement();
+        return true;
       case State::kStmtStart:
         if (TokenIsSqliteKeyword("create", token)) {
           state = State::kCreate;
@@ -149,9 +183,8 @@
         if (TokenIsSqliteKeyword("trigger", token)) {
           // TODO(lalitm): add this to the "errors" documentation page
           // explaining why this is the case.
-          base::StackString<1024> err(
-              "Creating triggers are not supported by trace processor.");
-          return ErrorAtToken(token, err.c_str());
+          return ErrorAtToken(
+              token, "Creating triggers is not supported in PerfettoSQL.");
         }
         if (TokenIsCustomKeyword("perfetto", token)) {
           state = State::kCreatePerfetto;
@@ -179,9 +212,13 @@
         if (TokenIsSqliteKeyword("table", token)) {
           return ParseCreatePerfettoTable(*first_non_space_token);
         }
+        if (TokenIsCustomKeyword("macro", token)) {
+          return ParseCreatePerfettoMacro(state ==
+                                          State::kCreateOrReplacePerfetto);
+        }
         base::StackString<1024> err(
-            "Expected 'FUNCTION' or 'TABLE' after 'CREATE PERFETTO', received "
-            "'%*s'.",
+            "Expected 'FUNCTION', 'TABLE' or 'MACRO' after 'CREATE PERFETTO', "
+            "received '%*s'.",
             static_cast<int>(token.str.size()), token.str.data());
         return ErrorAtToken(token, err.c_str());
     }
@@ -254,10 +291,13 @@
     return ErrorAtToken(lp, "Malformed function prototype: '(' expected");
   }
 
-  prototype.push_back('(');
-  if (!ParseArgumentDefinitions(&prototype)) {
+  std::vector<Argument> args;
+  if (!ParseArgumentDefinitions(args)) {
     return false;
   }
+
+  prototype.push_back('(');
+  prototype.append(SerializeArgs(args));
   prototype.push_back(')');
 
   if (Token returns = tokenizer_.NextNonWhitespace();
@@ -276,9 +316,11 @@
       return ErrorAtToken(lp, "Malformed table return: '(' expected");
     }
     // Table function return.
-    if (!ParseArgumentDefinitions(&ret)) {
+    std::vector<Argument> ret_args;
+    if (!ParseArgumentDefinitions(ret_args)) {
       return false;
     }
+    ret = SerializeArgs(ret_args);
   } else if (ret_token.token_type != SqliteTokenType::TK_ID) {
     // TODO(lalitm): add a link to create function documentation.
     return ErrorAtToken(ret_token, "Invalid return type");
@@ -301,28 +343,117 @@
   return true;
 }
 
-bool PerfettoSqlParser::ParseArgumentDefinitions(std::string* str) {
-  for (Token tok = tokenizer_.Next();; tok = tokenizer_.Next()) {
-    if (tok.token_type == SqliteTokenType::TK_RP) {
-      return true;
+bool PerfettoSqlParser::ParseCreatePerfettoMacro(bool replace) {
+  Token name = tokenizer_.NextNonWhitespace();
+  if (name.token_type != SqliteTokenType::TK_ID) {
+    // TODO(lalitm): add a link to create macro documentation.
+    base::StackString<1024> err("Invalid macro name %.*s",
+                                static_cast<int>(name.str.size()),
+                                name.str.data());
+    return ErrorAtToken(name, err.c_str());
+  }
+
+  // TK_LP == '(' (i.e. left parenthesis).
+  if (Token lp = tokenizer_.NextNonWhitespace();
+      lp.token_type != SqliteTokenType::TK_LP) {
+    // TODO(lalitm): add a link to create macro documentation.
+    return ErrorAtToken(lp, "Malformed macro prototype: '(' expected");
+  }
+
+  std::vector<Argument> args;
+  if (!ParseArgumentDefinitions(args)) {
+    return false;
+  }
+
+  if (Token returns = tokenizer_.NextNonWhitespace();
+      !TokenIsCustomKeyword("returns", returns)) {
+    // TODO(lalitm): add a link to create macro documentation.
+    return ErrorAtToken(returns, "Expected keyword 'returns'");
+  }
+
+  Token returns_value = tokenizer_.NextNonWhitespace();
+  if (returns_value.token_type != SqliteTokenType::TK_ID) {
+    // TODO(lalitm): add a link to create function documentation.
+    return ErrorAtToken(returns_value, "Expected return type");
+  }
+
+  if (Token as_token = tokenizer_.NextNonWhitespace();
+      !TokenIsSqliteKeyword("as", as_token)) {
+    // TODO(lalitm): add a link to create macro documentation.
+    return ErrorAtToken(as_token, "Expected keyword 'as'");
+  }
+
+  Token first = tokenizer_.NextNonWhitespace();
+  Token tok = tokenizer_.NextTerminal();
+  statement_ = CreateMacro{
+      replace, tokenizer_.SubstrToken(name), std::move(args),
+      tokenizer_.SubstrToken(returns_value), tokenizer_.Substr(first, tok)};
+  return true;
+}
+
+bool PerfettoSqlParser::ParseArgumentDefinitions(std::vector<Argument>& res) {
+  enum TokenType {
+    kIdOrRp,
+    kId,
+    kType,
+    kCommaOrRp,
+  };
+
+  std::optional<Token> id = std::nullopt;
+  TokenType expected = kIdOrRp;
+  for (Token tok = tokenizer_.NextNonWhitespace();;
+       tok = tokenizer_.NextNonWhitespace()) {
+    // Keywords can be used as names accidentally so have an explicit error
+    // message for those.
+    if (tok.token_type == SqliteTokenType::TK_GENERIC_KEYWORD) {
+      base::StackString<1024> err(
+          "Malformed function/macro prototype: %.*s is a SQL keyword so "
+          "cannot appear in a prototype",
+          static_cast<int>(tok.str.size()), tok.str.data());
+      return ErrorAtToken(tok, err.c_str());
     }
-    if (tok.token_type == SqliteTokenType::TK_SPACE) {
-      str->append(" ");
-      continue;
+    if (expected == kCommaOrRp) {
+      PERFETTO_CHECK(expected == kCommaOrRp);
+      if (tok.token_type == SqliteTokenType::TK_RP) {
+        return true;
+      }
+      if (tok.token_type == SqliteTokenType::TK_COMMA) {
+        expected = kId;
+        continue;
+      }
+      return ErrorAtToken(tok, "')' or ',' expected");
     }
-    if (tok.token_type != SqliteTokenType::TK_ID &&
-        tok.token_type != SqliteTokenType::TK_COMMA) {
-      if (tok.token_type == SqliteTokenType::TK_GENERIC_KEYWORD) {
-        base::StackString<1024> err(
-            "Malformed function prototype: %.*s is a SQL keyword so cannot "
-            "appear in a function prototype",
-            static_cast<int>(tok.str.size()), tok.str.data());
+    if (expected == kType) {
+      if (tok.token_type != SqliteTokenType::TK_ID) {
+        // TODO(lalitm): add a link to documentation.
+        base::StackString<1024> err("%.*s is not a valid argument type",
+                                    static_cast<int>(tok.str.size()),
+                                    tok.str.data());
         return ErrorAtToken(tok, err.c_str());
       }
-      // TODO(lalitm): add a link to create function documentation.
-      return ErrorAtToken(tok, "')', ',', name or type expected");
+      PERFETTO_CHECK(id);
+      res.push_back(std::make_pair(tokenizer_.SubstrToken(*id),
+                                   tokenizer_.SubstrToken(tok)));
+      id = std::nullopt;
+      expected = kCommaOrRp;
+      continue;
     }
-    str->append(tok.str);
+
+    // kIdOrRp only happens on the very first token.
+    if (tok.token_type == SqliteTokenType::TK_RP && expected == kIdOrRp) {
+      return true;
+    }
+
+    if (tok.token_type != SqliteTokenType::TK_ID) {
+      // TODO(lalitm): add a link to documentation.
+      base::StackString<1024> err("%.*s is not a valid argument name",
+                                  static_cast<int>(tok.str.size()),
+                                  tok.str.data());
+      return ErrorAtToken(tok, err.c_str());
+    }
+    id = tok;
+    expected = kType;
+    continue;
   }
 }
 
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h
index b7f67d5..1c38d43 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h
@@ -18,10 +18,15 @@
 #define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PARSER_H_
 
 #include <optional>
+#include <string>
 #include <string_view>
+#include <utility>
 #include <variant>
+#include <vector>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "src/trace_processor/sqlite/sqlite_tokenizer.h"
 
@@ -63,12 +68,23 @@
   struct Include {
     std::string key;
   };
-  using Statement =
-      std::variant<SqliteSql, CreateFunction, CreateTable, Include>;
+  // Indicates that the specified SQL was a CREATE PERFETTO MACRO statement
+  // with the following parameter.
+  struct CreateMacro {
+    bool replace;
+    SqlSource name;
+    std::vector<std::pair<SqlSource, SqlSource>> args;
+    SqlSource returns;
+    SqlSource sql;
+  };
+  using Statement = std::
+      variant<SqliteSql, CreateFunction, CreateTable, Include, CreateMacro>;
 
   // Creates a new SQL parser with the a block of PerfettoSQL statements.
   // Concretely, the passed string can contain >1 statement.
-  explicit PerfettoSqlParser(SqlSource);
+  explicit PerfettoSqlParser(
+      SqlSource,
+      const base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro>&);
 
   // Attempts to parse to the next statement in the SQL. Returns true if
   // a statement was successfully parsed and false if EOF was reached or the
@@ -94,11 +110,15 @@
   }
 
   // Returns the error status for the parser. This will be |base::OkStatus()|
-  // until
+  // until an unrecoverable error is encountered.
   const base::Status& status() const { return status_; }
 
  private:
-  // This cannot be moved because we keep pointers into |sql_| in |tokenizer_|.
+  using Argument =
+      std::pair<SqlSource /* name token */, SqlSource /* type token */>;
+
+  // This cannot be moved because we keep pointers into |sql_| in
+  // |preprocessor_|.
   PerfettoSqlParser(PerfettoSqlParser&&) = delete;
   PerfettoSqlParser& operator=(PerfettoSqlParser&&) = delete;
 
@@ -110,11 +130,15 @@
 
   bool ParseIncludePerfettoModule(SqliteTokenizer::Token first_non_space_token);
 
-  bool ParseArgumentDefinitions(std::string*);
+  bool ParseCreatePerfettoMacro(bool replace);
+
+  bool ParseArgumentDefinitions(std::vector<Argument>&);
 
   bool ErrorAtToken(const SqliteTokenizer::Token&, const char* error);
 
+  PerfettoSqlPreprocessor preprocessor_;
   SqliteTokenizer tokenizer_;
+
   base::Status status_;
   std::optional<SqlSource> statement_sql_;
   std::optional<Statement> statement_;
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc
index b436225..d2391c3 100644
--- a/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_parser_unittest.cc
@@ -22,6 +22,7 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h"
 #include "src/trace_processor/sqlite/sql_source.h"
 #include "test/gtest_and_gmock.h"
 
@@ -34,42 +35,15 @@
 using CreateFn = PerfettoSqlParser::CreateFunction;
 using CreateTable = PerfettoSqlParser::CreateTable;
 using Include = PerfettoSqlParser::Include;
-
-inline bool operator==(const SqlSource& a, const SqlSource& b) {
-  return a.sql() == b.sql();
-}
-
-inline bool operator==(const SqliteSql&, const SqliteSql&) {
-  return true;
-}
-
-inline bool operator==(const CreateFn& a, const CreateFn& b) {
-  return std::tie(a.returns, a.is_table, a.prototype, a.replace, a.sql) ==
-         std::tie(b.returns, b.is_table, b.prototype, b.replace, b.sql);
-}
-
-inline bool operator==(const CreateTable& a, const CreateTable& b) {
-  return std::tie(a.name, a.sql) == std::tie(b.name, b.sql);
-}
-
-inline bool operator==(const Include& a, const Include& b) {
-  return std::tie(a.key) == std::tie(b.key);
-}
+using CreateMacro = PerfettoSqlParser::CreateMacro;
 
 namespace {
 
-SqlSource FindSubstr(const SqlSource& source, const std::string& needle) {
-  size_t off = source.sql().find(needle);
-  PERFETTO_CHECK(off != std::string::npos);
-  return source.Substr(static_cast<uint32_t>(off),
-                       static_cast<uint32_t>(needle.size()));
-}
-
 class PerfettoSqlParserTest : public ::testing::Test {
  protected:
   base::StatusOr<std::vector<PerfettoSqlParser::Statement>> Parse(
       SqlSource sql) {
-    PerfettoSqlParser parser(sql);
+    PerfettoSqlParser parser(sql, macros_);
     std::vector<PerfettoSqlParser::Statement> results;
     while (parser.Next()) {
       results.push_back(std::move(parser.statement()));
@@ -79,6 +53,8 @@
     }
     return results;
   }
+
+  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
 };
 
 TEST_F(PerfettoSqlParserTest, Empty) {
@@ -87,7 +63,7 @@
 
 TEST_F(PerfettoSqlParserTest, SemiColonTerminatedStatement) {
   SqlSource res = SqlSource::FromExecuteQuery("SELECT * FROM slice;");
-  PerfettoSqlParser parser(res);
+  PerfettoSqlParser parser(res, macros_);
   ASSERT_TRUE(parser.Next());
   ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
   ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "SELECT * FROM slice"));
@@ -96,7 +72,7 @@
 TEST_F(PerfettoSqlParserTest, MultipleStmts) {
   auto res =
       SqlSource::FromExecuteQuery("SELECT * FROM slice; SELECT * FROM s");
-  PerfettoSqlParser parser(res);
+  PerfettoSqlParser parser(res, macros_);
   ASSERT_TRUE(parser.Next());
   ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
   ASSERT_EQ(parser.statement_sql().sql(),
@@ -109,7 +85,7 @@
 
 TEST_F(PerfettoSqlParserTest, IgnoreOnlySpace) {
   auto res = SqlSource::FromExecuteQuery(" ; SELECT * FROM s; ; ;");
-  PerfettoSqlParser parser(res);
+  PerfettoSqlParser parser(res, macros_);
   ASSERT_TRUE(parser.Next());
   ASSERT_EQ(parser.statement(), Statement{SqliteSql{}});
   ASSERT_EQ(parser.statement_sql().sql(),
@@ -155,7 +131,7 @@
 TEST_F(PerfettoSqlParserTest, CreatePerfettoFunctionAndOther) {
   auto res = SqlSource::FromExecuteQuery(
       "create perfetto function foo() returns INT as select 1; select foo()");
-  PerfettoSqlParser parser(res);
+  PerfettoSqlParser parser(res, macros_);
   ASSERT_TRUE(parser.Next());
   CreateFn fn{false, "foo()", "INT", FindSubstr(res, "select 1"), false};
   ASSERT_EQ(parser.statement(), Statement{fn});
@@ -187,6 +163,46 @@
   ASSERT_FALSE(Parse(res).status().ok());
 }
 
+TEST_F(PerfettoSqlParserTest, CreatePerfettoMacro) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto macro foo(a1 Expr, b1 TableOrSubquery,c3_d "
+      "TableOrSubquery2 ) returns TableOrSubquery3 as random sql snippet");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(
+      parser.statement(),
+      Statement(CreateMacro{
+          false,
+          FindSubstr(res, "foo"),
+          {
+              {FindSubstr(res, "a1"), FindSubstr(res, "Expr")},
+              {FindSubstr(res, "b1"), FindSubstr(res, "TableOrSubquery")},
+              {FindSubstr(res, "c3_d"), FindSubstr(res, "TableOrSubquery2")},
+          },
+          FindSubstr(res, "TableOrSubquery3"),
+          FindSubstr(res, "random sql snippet")}));
+  ASSERT_FALSE(parser.Next());
+}
+
+TEST_F(PerfettoSqlParserTest, CreatePerfettoMacroAndOther) {
+  auto res = SqlSource::FromExecuteQuery(
+      "create perfetto macro foo() returns sql1 as random sql snippet; "
+      "select 1");
+  PerfettoSqlParser parser(res, macros_);
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(CreateMacro{
+                                    false,
+                                    FindSubstr(res, "foo"),
+                                    {},
+                                    FindSubstr(res, "sql1"),
+                                    FindSubstr(res, "random sql snippet"),
+                                }));
+  ASSERT_TRUE(parser.Next());
+  ASSERT_EQ(parser.statement(), Statement(SqliteSql{}));
+  ASSERT_EQ(parser.statement_sql(), FindSubstr(res, "select 1"));
+  ASSERT_FALSE(parser.Next());
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc
new file mode 100644
index 0000000..135f6a8
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.cc
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
+#include <optional>
+#include <unordered_set>
+#include <utility>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+PerfettoSqlPreprocessor::PerfettoSqlPreprocessor(
+    SqlSource source,
+    const base::FlatHashMap<std::string, Macro>& macros)
+    : global_tokenizer_(std::move(source)), macros_(&macros) {}
+
+bool PerfettoSqlPreprocessor::NextStatement() {
+  PERFETTO_CHECK(status_.ok());
+
+  // Skip through any number of semi-colons (representing empty statements).
+  SqliteTokenizer::Token tok = global_tokenizer_.NextNonWhitespace();
+  while (tok.token_type == SqliteTokenType::TK_SEMI) {
+    tok = global_tokenizer_.NextNonWhitespace();
+  }
+
+  // If we still see a terminal token at this point, we must have hit EOF.
+  if (tok.IsTerminal()) {
+    PERFETTO_DCHECK(tok.token_type != SqliteTokenType::TK_SEMI);
+    return false;
+  }
+
+  SqlSource stmt =
+      global_tokenizer_.Substr(tok, global_tokenizer_.NextTerminal());
+  auto stmt_or = RewriteInternal(stmt, {});
+  if (stmt_or.ok()) {
+    statement_ = std::move(*stmt_or);
+    return true;
+  }
+  status_ = stmt_or.status();
+  return false;
+}
+
+base::StatusOr<SqlSource> PerfettoSqlPreprocessor::RewriteInternal(
+    const SqlSource& source,
+    const std::unordered_map<std::string, SqlSource>& arg_bindings) {
+  SqlSource::Rewriter rewriter(source);
+  SqliteTokenizer tokenizer(source);
+  for (SqliteTokenizer::Token tok = tokenizer.NextNonWhitespace(), prev;;
+       prev = tok, tok = tokenizer.NextNonWhitespace()) {
+    if (tok.IsTerminal()) {
+      break;
+    }
+    if (tok.token_type == SqliteTokenType::TK_VARIABLE &&
+        !seen_macros_.empty()) {
+      PERFETTO_CHECK(tok.str.size() >= 2);
+      if (tok.str[0] != '$') {
+        return ErrorAtToken(tokenizer, tok, "Variables must start with $");
+      }
+      auto binding_it = arg_bindings.find(std::string(tok.str.substr(1)));
+      if (binding_it == arg_bindings.end()) {
+        return ErrorAtToken(tokenizer, tok, "Variable not found");
+      }
+      tokenizer.RewriteToken(rewriter, tok, binding_it->second);
+      continue;
+    }
+    if (tok.token_type != SqliteTokenType::TK_ILLEGAL || tok.str != "!") {
+      continue;
+    }
+
+    base::StatusOr<MacroInvocation> invocation_or =
+        ParseMacroInvocation(tokenizer, tok, prev, arg_bindings);
+    RETURN_IF_ERROR(invocation_or.status());
+
+    seen_macros_.emplace(invocation_or->macro->name);
+    auto source_or =
+        RewriteInternal(invocation_or->macro->sql, invocation_or->arg_bindings);
+    RETURN_IF_ERROR(source_or.status());
+    seen_macros_.erase(invocation_or->macro->name);
+
+    tokenizer.Rewrite(rewriter, prev, tok, std::move(*source_or),
+                      SqliteTokenizer::EndToken::kInclusive);
+  }
+  return std::move(rewriter).Build();
+}
+
+base::StatusOr<PerfettoSqlPreprocessor::MacroInvocation>
+PerfettoSqlPreprocessor::ParseMacroInvocation(
+    SqliteTokenizer& tokenizer,
+    SqliteTokenizer::Token& tok,
+    const SqliteTokenizer::Token& name_token,
+    const std::unordered_map<std::string, SqlSource>& arg_bindings) {
+  if (name_token.token_type == SqliteTokenType::TK_VARIABLE) {
+    // TODO(b/290185551): add a link to macro documentation.
+    return ErrorAtToken(tokenizer, name_token,
+                        "Macro name cannot contain a variable");
+  }
+  if (name_token.token_type != SqliteTokenType::TK_ID) {
+    // TODO(b/290185551): add a link to macro documentation.
+    return ErrorAtToken(tokenizer, name_token, "Macro invocation is invalid");
+  }
+
+  // Get the opening left parenthesis.
+  tok = tokenizer.NextNonWhitespace();
+  if (tok.token_type != SqliteTokenType::TK_LP) {
+    // TODO(b/290185551): add a link to macro documentation.
+    return ErrorAtToken(tokenizer, tok, "( expected to open macro invocation");
+  }
+
+  std::string macro_name(name_token.str);
+  Macro* macro = macros_->Find(macro_name);
+  if (!macro) {
+    // TODO(b/290185551): add a link to macro documentation.
+    base::StackString<1024> err("Macro %s does not exist", macro_name.c_str());
+    return ErrorAtToken(tokenizer, name_token, err.c_str());
+  }
+
+  if (seen_macros_.count(macro_name)) {
+    // TODO(b/290185551): add a link to macro documentation.
+    return ErrorAtToken(tokenizer, name_token,
+                        "Macros cannot be recursive or mutually recursive");
+  }
+
+  std::unordered_map<std::string, SqlSource> inner_bindings;
+  for (bool has_more = true; has_more;) {
+    base::StatusOr<InvocationArg> source_or =
+        ParseMacroInvocationArg(tokenizer, tok, !inner_bindings.empty());
+    RETURN_IF_ERROR(source_or.status());
+    if (source_or->arg) {
+      base::StatusOr<SqlSource> res =
+          RewriteInternal(*source_or->arg, arg_bindings);
+      RETURN_IF_ERROR(res.status());
+      if (macro->args.size() <= inner_bindings.size()) {
+        // TODO(lalitm): add a link to macro documentation.
+        return ErrorAtToken(tokenizer, name_token,
+                            "Macro invoked with too many args");
+      }
+      inner_bindings.emplace(macro->args[inner_bindings.size()], *res);
+    }
+    has_more = source_or->has_more;
+  }
+
+  if (inner_bindings.size() < macro->args.size()) {
+    // TODO(lalitm): add a link to macro documentation.
+    return ErrorAtToken(tokenizer, name_token,
+                        "Macro invoked with too few args");
+  }
+  PERFETTO_CHECK(inner_bindings.size() == macro->args.size());
+  return MacroInvocation{macro, inner_bindings};
+}
+
+base::StatusOr<PerfettoSqlPreprocessor::InvocationArg>
+PerfettoSqlPreprocessor::ParseMacroInvocationArg(SqliteTokenizer& tokenizer,
+                                                 SqliteTokenizer::Token& tok,
+                                                 bool has_prev_args) {
+  uint32_t nested_parens = 0;
+  bool seen_token_in_arg = false;
+  auto start = tokenizer.NextNonWhitespace();
+  for (tok = start;; tok = tokenizer.NextNonWhitespace()) {
+    if (tok.IsTerminal()) {
+      if (tok.token_type == SqliteTokenType::TK_SEMI) {
+        // TODO(b/290185551): add a link to macro documentation.
+        return ErrorAtToken(tokenizer, tok,
+                            "Semi-colon is not allowed in macro invocation");
+      }
+      // TODO(b/290185551): add a link to macro documentation.
+      return ErrorAtToken(tokenizer, tok, "Macro invocation not complete");
+    }
+
+    bool is_arg_terminator = tok.token_type == SqliteTokenType::TK_RP ||
+                             tok.token_type == SqliteTokenType::TK_COMMA;
+    if (nested_parens == 0 && is_arg_terminator) {
+      bool token_required =
+          has_prev_args || tok.token_type != SqliteTokenType::TK_RP;
+      if (!seen_token_in_arg && token_required) {
+        // TODO(b/290185551): add a link to macro documentation.
+        return ErrorAtToken(tokenizer, tok, "Macro arg is empty");
+      }
+      return InvocationArg{
+          seen_token_in_arg ? std::make_optional(tokenizer.Substr(start, tok))
+                            : std::optional<SqlSource>(std::nullopt),
+          tok.token_type == SqliteTokenType::TK_COMMA,
+      };
+    }
+    seen_token_in_arg = true;
+
+    if (tok.token_type == SqliteTokenType::TK_LP) {
+      nested_parens++;
+      continue;
+    }
+    if (tok.token_type == SqliteTokenType::TK_RP) {
+      nested_parens--;
+      continue;
+    }
+  }
+}
+
+base::Status PerfettoSqlPreprocessor::ErrorAtToken(
+    const SqliteTokenizer& tokenizer,
+    const SqliteTokenizer::Token& token,
+    const char* error) {
+  std::string traceback = tokenizer.AsTraceback(token);
+  return base::ErrStatus("%s%s", traceback.c_str(), error);
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h
new file mode 100644
index 0000000..de53786
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PREPROCESSOR_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PREPROCESSOR_H_
+
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <unordered_set>
+#include <variant>
+#include <vector>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/sqlite/sqlite_tokenizer.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+// Preprocessor for PerfettoSQL statements. The main responsiblity of this
+// class is to perform similar functions to the C/C++ preprocessor (e.g.
+// expanding macros). It is also responsible for splitting the given SQL into
+// statements.
+class PerfettoSqlPreprocessor {
+ public:
+  struct Macro {
+    bool replace;
+    std::string name;
+    std::vector<std::string> args;
+    SqlSource sql;
+  };
+
+  // Creates a preprocessor acting on the given SqlSource.
+  explicit PerfettoSqlPreprocessor(
+      SqlSource,
+      const base::FlatHashMap<std::string, Macro>&);
+
+  // Preprocesses the next SQL statement. Returns true if a statement was
+  // successfully preprocessed and false if EOF was reached or the statement was
+  // not preprocessed correctly.
+  //
+  // Note: if this function returns false, callers *must* call |status()|: it
+  // is undefined behaviour to not do so.
+  bool NextStatement();
+
+  // Returns the error status for the parser. This will be |base::OkStatus()|
+  // until an unrecoverable error is encountered.
+  const base::Status& status() const { return status_; }
+
+  // Returns the most-recent preprocessed SQL statement.
+  //
+  // Note: this function must not be called unless |NextStatement()| returned
+  // true.
+  SqlSource& statement() { return *statement_; }
+
+ private:
+  struct MacroInvocation {
+    const Macro* macro;
+    std::unordered_map<std::string, SqlSource> arg_bindings;
+  };
+  struct InvocationArg {
+    std::optional<SqlSource> arg;
+    bool has_more;
+  };
+
+  base::Status ErrorAtToken(const SqliteTokenizer& tokenizer,
+                            const SqliteTokenizer::Token& token,
+                            const char* error);
+  base::StatusOr<SqlSource> RewriteInternal(
+      const SqlSource&,
+      const std::unordered_map<std::string, SqlSource>& arg_bindings);
+
+  base::StatusOr<MacroInvocation> ParseMacroInvocation(
+      SqliteTokenizer& tokenizer,
+      SqliteTokenizer::Token& token,
+      const SqliteTokenizer::Token& name_token,
+      const std::unordered_map<std::string, SqlSource>& arg_bindings);
+  base::StatusOr<InvocationArg> ParseMacroInvocationArg(
+      SqliteTokenizer& tokenizer,
+      SqliteTokenizer::Token& token,
+      bool has_prev_args);
+
+  SqliteTokenizer global_tokenizer_;
+  const base::FlatHashMap<std::string, Macro>* macros_ = nullptr;
+  std::unordered_set<std::string> seen_macros_;
+  std::optional<SqlSource> statement_;
+  base::Status status_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_PREPROCESSOR_H_
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc
new file mode 100644
index 0000000..104ee3d
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor_unittest.cc
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_preprocessor.h"
+
+#include <optional>
+#include <string>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace {
+
+using Macro = PerfettoSqlPreprocessor::Macro;
+
+class PerfettoSqlPreprocessorUnittest : public ::testing::Test {
+ protected:
+  base::FlatHashMap<std::string, PerfettoSqlPreprocessor::Macro> macros_;
+};
+
+TEST_F(PerfettoSqlPreprocessorUnittest, Empty) {
+  PerfettoSqlPreprocessor preprocessor(SqlSource::FromExecuteQuery(""),
+                                       macros_);
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, SemiColonTerminatedStatement) {
+  auto source = SqlSource::FromExecuteQuery("SELECT * FROM slice;");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(),
+            FindSubstr(source, "SELECT * FROM slice"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, IgnoreOnlySpace) {
+  auto source = SqlSource::FromExecuteQuery(" ; SELECT * FROM s; ; ;");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT * FROM s"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, MultipleStmts) {
+  auto source =
+      SqlSource::FromExecuteQuery("SELECT * FROM slice; SELECT * FROM s");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(),
+            FindSubstr(source, "SELECT * FROM slice"));
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT * FROM s"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, CreateMacro) {
+  auto source = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo(a, b) AS SELECT $a + $b");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(
+      preprocessor.statement(),
+      FindSubstr(source, "CREATE PERFETTO MACRO foo(a, b) AS SELECT $a + $b"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, SingleMacro) {
+  auto foo = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo(a Expr, b Expr) Returns Expr AS "
+      "SELECT $a + $b");
+  macros_.Insert(
+      "foo",
+      Macro{false, "foo", {"a", "b"}, FindSubstr(foo, "SELECT $a + $b")});
+
+  auto source = SqlSource::FromExecuteQuery(
+      "foo!((select s.ts + r.dur from s, r), 1234); SELECT 1");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement().AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT (select s.ts + r.dur from s, r) + 1234\n"
+            "  ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 1\n"
+            "    foo!((select s.ts + r.dur from s, r), 1234)\n"
+            "    ^\n"
+            "  File \"stdin\" line 1 col 59\n"
+            "    SELECT $a + $b\n"
+            "    ^\n");
+  ASSERT_EQ(preprocessor.statement().AsTraceback(7),
+            "Fully expanded statement\n"
+            "  SELECT (select s.ts + r.dur from s, r) + 1234\n"
+            "         ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 1\n"
+            "    foo!((select s.ts + r.dur from s, r), 1234)\n"
+            "    ^\n"
+            "  File \"stdin\" line 1 col 66\n"
+            "    SELECT $a + $b\n"
+            "           ^\n"
+            "  File \"stdin\" line 1 col 6\n"
+            "    (select s.ts + r.dur from s, r)\n"
+            "    ^\n");
+  ASSERT_EQ(preprocessor.statement().sql(),
+            "SELECT (select s.ts + r.dur from s, r) + 1234");
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement(), FindSubstr(source, "SELECT 1"));
+  ASSERT_FALSE(preprocessor.NextStatement());
+  ASSERT_TRUE(preprocessor.status().ok());
+}
+
+TEST_F(PerfettoSqlPreprocessorUnittest, NestedMacro) {
+  auto foo = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO foo(a Expr, b Expr) Returns Expr AS $a + $b");
+  macros_.Insert("foo", Macro{
+                            false,
+                            "foo",
+                            {"a", "b"},
+                            FindSubstr(foo, "$a + $b"),
+                        });
+
+  auto bar = SqlSource::FromExecuteQuery(
+      "CREATE PERFETTO MACRO bar(a, b) Returns Expr AS "
+      "tfoo!($a, $b) + foo!($b, $a)");
+  macros_.Insert("bar", Macro{
+                            false,
+                            "bar",
+                            {"a", "b"},
+                            FindSubstr(bar, "foo!($a, $b) + foo!($b, $a)"),
+                        });
+
+  auto source = SqlSource::FromExecuteQuery(
+      "SELECT bar!((select s.ts + r.dur from s, r), 1234); SELECT 1");
+  PerfettoSqlPreprocessor preprocessor(source, macros_);
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement().sql(),
+            "SELECT (select s.ts + r.dur from s, r) + 1234 + 1234 + "
+            "(select s.ts + r.dur from s, r)");
+  ASSERT_TRUE(preprocessor.NextStatement());
+  ASSERT_EQ(preprocessor.statement().sql(), "SELECT 1");
+}
+
+}  // namespace
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h b/src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h
new file mode 100644
index 0000000..caca711
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/engine/perfetto_sql_test_utils.h
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_TEST_UTILS_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_TEST_UTILS_H_
+
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_parser.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+inline bool operator==(const SqlSource& a, const SqlSource& b) {
+  return a.sql() == b.sql();
+}
+
+inline bool operator==(const PerfettoSqlParser::SqliteSql&,
+                       const PerfettoSqlParser::SqliteSql&) {
+  return true;
+}
+
+inline bool operator==(const PerfettoSqlParser::CreateFunction& a,
+                       const PerfettoSqlParser::CreateFunction& b) {
+  return std::tie(a.returns, a.is_table, a.prototype, a.replace, a.sql) ==
+         std::tie(b.returns, b.is_table, b.prototype, b.replace, b.sql);
+}
+
+inline bool operator==(const PerfettoSqlParser::CreateTable& a,
+                       const PerfettoSqlParser::CreateTable& b) {
+  return std::tie(a.name, a.sql) == std::tie(b.name, b.sql);
+}
+
+inline bool operator==(const PerfettoSqlParser::Include& a,
+                       const PerfettoSqlParser::Include& b) {
+  return std::tie(a.key) == std::tie(b.key);
+}
+
+constexpr bool operator==(const PerfettoSqlParser::CreateMacro& a,
+                          const PerfettoSqlParser::CreateMacro& b) {
+  return std::tie(a.replace, a.name, a.sql, a.args) ==
+         std::tie(b.replace, b.name, b.sql, b.args);
+}
+
+inline std::ostream& operator<<(std::ostream& stream, const SqlSource& sql) {
+  return stream << "SqlSource(sql=" << testing::PrintToString(sql.sql()) << ")";
+}
+
+inline std::ostream& operator<<(std::ostream& stream,
+                                const PerfettoSqlParser::Statement& line) {
+  if (std::get_if<PerfettoSqlParser::SqliteSql>(&line)) {
+    return stream << "SqliteSql()";
+  }
+  if (auto* fn = std::get_if<PerfettoSqlParser::CreateFunction>(&line)) {
+    return stream << "CreateFn(sql=" << testing::PrintToString(fn->sql)
+                  << ", prototype=" << testing::PrintToString(fn->prototype)
+                  << ", returns=" << testing::PrintToString(fn->returns)
+                  << ", is_table=" << testing::PrintToString(fn->is_table)
+                  << ", replace=" << testing::PrintToString(fn->replace) << ")";
+  }
+  if (auto* tab = std::get_if<PerfettoSqlParser::CreateTable>(&line)) {
+    return stream << "CreateTable(name=" << testing::PrintToString(tab->name)
+                  << ", sql=" << testing::PrintToString(tab->sql) << ")";
+  }
+  if (auto* macro = std::get_if<PerfettoSqlParser::CreateMacro>(&line)) {
+    return stream << "CreateTable(name=" << testing::PrintToString(macro->name)
+                  << ", args=" << testing::PrintToString(macro->args)
+                  << ", replace=" << testing::PrintToString(macro->replace)
+                  << ", sql=" << testing::PrintToString(macro->sql) << ")";
+  }
+  PERFETTO_FATAL("Unknown type");
+}
+
+template <typename T>
+inline bool operator==(const base::StatusOr<T>& a, const base::StatusOr<T>& b) {
+  return a.status().ok() == b.ok() &&
+         a.status().message() == b.status().message() &&
+         (!a.ok() || a.value() == b.value());
+}
+
+inline std::ostream& operator<<(std::ostream& stream, const base::Status& a) {
+  return stream << "base::Status(ok=" << a.ok()
+                << ", message=" << testing::PrintToString(a.message()) << ")";
+}
+
+template <typename T>
+inline std::ostream& operator<<(std::ostream& stream,
+                                const base::StatusOr<T>& a) {
+  std::string val = a.ok() ? testing::PrintToString(a.value()) : "";
+  return stream << "base::StatusOr(status="
+                << testing::PrintToString(a.status()) << ", value=" << val
+                << ")";
+}
+
+inline SqlSource FindSubstr(const SqlSource& source,
+                            const std::string& needle) {
+  size_t off = source.sql().find(needle);
+  PERFETTO_CHECK(off != std::string::npos);
+  return source.Substr(static_cast<uint32_t>(off),
+                       static_cast<uint32_t>(needle.size()));
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_PERFETTO_SQL_ENGINE_PERFETTO_SQL_TEST_UTILS_H_
diff --git a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
index 7804e7a..247d79b 100644
--- a/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/runtime_table_function.cc
@@ -126,7 +126,7 @@
 base::Status RuntimeTableFunction::Cursor::Filter(const QueryConstraints& qc,
                                                   sqlite3_value** argv,
                                                   FilterHistory) {
-  PERFETTO_TP_TRACE(metatrace::Category::FUNCTION, "TABLE_FUNCTION_CALL",
+  PERFETTO_TP_TRACE(metatrace::Category::FUNCTION_CALL, "TABLE_FUNCTION_CALL",
                     [this](metatrace::Record* r) {
                       r->AddArg("Function",
                                 state_->prototype.function_name.c_str());
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
index 2462d59..15b1f45 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc
@@ -404,7 +404,7 @@
 base::Status SpanJoinOperatorTable::Cursor::Filter(const QueryConstraints& qc,
                                                    sqlite3_value** argv,
                                                    FilterHistory) {
-  PERFETTO_TP_TRACE(metatrace::Category::QUERY, "SPAN_JOIN_XFILTER");
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_DETAILED, "SPAN_JOIN_XFILTER");
 
   bool t1_partitioned_mixed =
       t1_.definition()->IsPartitioned() &&
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc
index 9a87d07..73f4748 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/experimental_flamegraph.cc
@@ -18,6 +18,7 @@
 
 #include <unordered_set>
 
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 
@@ -342,6 +343,10 @@
         context_->storage.get(), values.upid, values.upid_group,
         values.time_constraints);
   }
+  if (!table) {
+    return base::ErrStatus("Failed to build flamegraph");
+  }
+
   if (!values.focus_str.empty()) {
     table =
         FocusTable(context_->storage.get(), std::move(table), values.focus_str);
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql b/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql
index 68c8dd1..bf280ea 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql
@@ -215,15 +215,16 @@
 -- Monitor contention slices that are neither blocking nor blocked by another monitor contention
 -- slice. They neither have |parent_id| nor |child_id| fields.
 CREATE TABLE internal_isolated AS
-WITH
-  x AS (
+WITH parents_and_children AS (
+ SELECT id FROM internal_children
+ UNION ALL
+ SELECT id FROM internal_parents
+), isolated AS (
     SELECT id FROM android_monitor_contention
     EXCEPT
-    SELECT id FROM internal_children
-    UNION ALL
-    SELECT id FROM internal_parents
+    SELECT id FROM parents_and_children
   )
-SELECT * FROM android_monitor_contention JOIN x USING (id);
+SELECT * FROM android_monitor_contention JOIN isolated USING (id);
 
 -- Contains parsed monitor contention slices with the parent-child relationships.
 --
diff --git a/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql b/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql
index ef4921a..b0f9310 100644
--- a/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/experimental/thread_executing_span.sql
@@ -440,19 +440,53 @@
 SELECT ts, dur, id, slice_id, slice_depth, slice_name
 FROM internal_span_graph_slice_sp;
 
--- |experimental_thread_executing_span_graph| + thread_state view span joined with critical_path information.
-CREATE VIRTUAL TABLE internal_critical_path_thread_state_sp
-USING
-  SPAN_JOIN(
-    internal_span_graph_thread_state PARTITIONED id,
-     internal_critical_path PARTITIONED id);
+-- |experimental_thread_executing_span_graph| + thread_state view joined with critical_path information.
+CREATE PERFETTO TABLE internal_critical_path_thread_state AS
+WITH span AS MATERIALIZED (
+    SELECT * FROM internal_critical_path
+  ),
+  span_starts AS (
+    SELECT
+      span.id,
+      span.utid,
+      span.critical_path_id,
+      span.critical_path_blocked_dur,
+      span.critical_path_blocked_state,
+      span.critical_path_blocked_function,
+      span.critical_path_utid,
+      thread_state_id,
+      MAX(thread_state.ts, span.ts) AS ts,
+      span.ts + span.dur AS span_end_ts,
+      thread_state.ts + thread_state.dur AS thread_state_end_ts,
+      thread_state.state,
+      thread_state.function,
+      thread_state.cpu
+    FROM span
+    JOIN internal_span_graph_thread_state_sp thread_state USING(id)
+  )
+SELECT
+  id,
+  thread_state_id,
+  ts,
+  MIN(span_end_ts, thread_state_end_ts) - ts AS dur,
+  utid,
+  state,
+  function,
+  cpu,
+  critical_path_id,
+  critical_path_blocked_dur,
+  critical_path_blocked_state,
+  critical_path_blocked_function,
+  critical_path_utid
+FROM span_starts
+WHERE MIN(span_end_ts, thread_state_end_ts) - ts > 0;
 
 -- |experimental_thread_executing_span_graph| + thread_state + critical_path span joined with
 -- |experimental_thread_executing_span_graph| + slice view.
 CREATE VIRTUAL TABLE internal_critical_path_sp
 USING
   SPAN_LEFT_JOIN(
-    internal_critical_path_thread_state_sp PARTITIONED id,
+    internal_critical_path_thread_state PARTITIONED id,
      internal_span_graph_slice PARTITIONED id);
 
 -- Flattened slices span joined with their thread_states. This contains the 'self' information
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 366ebfc..031e3c8 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -132,14 +132,16 @@
 TraceProcessor::MetatraceCategories MetatraceCategoriesToPublicEnum(
     ProtoEnum categories) {
   switch (categories) {
-    case ProtoEnum::TOPLEVEL:
-      return TraceProcessor::MetatraceCategories::TOPLEVEL;
-    case ProtoEnum::QUERY:
-      return TraceProcessor::MetatraceCategories::QUERY;
-    case ProtoEnum::FUNCTION:
-      return TraceProcessor::MetatraceCategories::FUNCTION;
+    case ProtoEnum::QUERY_TIMELINE:
+      return TraceProcessor::MetatraceCategories::QUERY_TIMELINE;
+    case ProtoEnum::QUERY_DETAILED:
+      return TraceProcessor::MetatraceCategories::QUERY_DETAILED;
+    case ProtoEnum::FUNCTION_CALL:
+      return TraceProcessor::MetatraceCategories::FUNCTION_CALL;
     case ProtoEnum::DB:
       return TraceProcessor::MetatraceCategories::DB;
+    case ProtoEnum::API_TIMELINE:
+      return TraceProcessor::MetatraceCategories::API_TIMELINE;
     case ProtoEnum::ALL:
       return TraceProcessor::MetatraceCategories::ALL;
     case ProtoEnum::NONE:
@@ -288,7 +290,7 @@
 
 util::Status Rpc::Parse(const uint8_t* data, size_t len) {
   PERFETTO_TP_TRACE(
-      metatrace::Category::TOPLEVEL, "RPC_PARSE",
+      metatrace::Category::API_TIMELINE, "RPC_PARSE",
       [&](metatrace::Record* r) { r->AddArg("length", std::to_string(len)); });
   if (eof_) {
     // Reset the trace processor state if another trace has been previously
@@ -310,7 +312,8 @@
 }
 
 void Rpc::NotifyEndOfFile() {
-  PERFETTO_TP_TRACE(metatrace::Category::TOPLEVEL, "RPC_NOTIFY_END_OF_FILE");
+  PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE,
+                    "RPC_NOTIFY_END_OF_FILE");
 
   trace_processor_->NotifyEndOfFile();
   eof_ = true;
@@ -372,7 +375,7 @@
   protos::pbzero::QueryArgs::Decoder query(args, len);
   std::string sql = query.sql_query().ToStdString();
   PERFETTO_DLOG("[RPC] Query < %s", sql.c_str());
-  PERFETTO_TP_TRACE(metatrace::Category::TOPLEVEL, "RPC_QUERY",
+  PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "RPC_QUERY",
                     [&](metatrace::Record* r) {
                       r->AddArg("SQL", sql);
                       if (query.has_tag()) {
@@ -402,7 +405,7 @@
     metric_names.emplace_back(it->as_std_string());
   }
 
-  PERFETTO_TP_TRACE(metatrace::Category::TOPLEVEL, "RPC_COMPUTE_METRIC",
+  PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "RPC_COMPUTE_METRIC",
                     [&](metatrace::Record* r) {
                       for (const auto& metric : metric_names) {
                         r->AddArg("Metric", metric);
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index 027f8ce..04967f9 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -506,9 +506,9 @@
       TryCacheCreateSortedTable(qc, history);
       break;
     case TableComputation::kTableFunction: {
-      PERFETTO_TP_TRACE(metatrace::Category::QUERY, "DYNAMIC_TABLE_GENERATE",
-                        [this](metatrace::Record* r) {
-                          r->AddArg("Table", db_sqlite_table_->name());
+      PERFETTO_TP_TRACE(metatrace::Category::QUERY_DETAILED,
+                        "TABLE_FUNCTION_CALL", [this](metatrace::Record* r) {
+                          r->AddArg("Name", db_sqlite_table_->name());
                         });
       // If we have a dynamically created table, regenerate the table based on
       // the new constraints.
@@ -530,7 +530,7 @@
   }
 
   PERFETTO_TP_TRACE(
-      metatrace::Category::QUERY, "DB_TABLE_FILTER_AND_SORT",
+      metatrace::Category::QUERY_DETAILED, "DB_TABLE_FILTER_AND_SORT",
       [this](metatrace::Record* r) {
         const Table* source = SourceTable();
         r->AddArg("Table", db_sqlite_table_->name());
diff --git a/src/trace_processor/sqlite/sql_source.cc b/src/trace_processor/sqlite/sql_source.cc
index 5e12cd1..570e87e 100644
--- a/src/trace_processor/sqlite/sql_source.cc
+++ b/src/trace_processor/sqlite/sql_source.cc
@@ -20,12 +20,15 @@
 #include <algorithm>
 #include <cstdint>
 #include <iterator>
+#include <limits>
 #include <optional>
 #include <string>
 #include <string_view>
 #include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/sys_types.h"
 
@@ -84,6 +87,9 @@
 
 }  // namespace
 
+SqlSource::SqlSource() = default;
+SqlSource::SqlSource(Node node) : root_(std::move(node)) {}
+
 SqlSource::SqlSource(std::string sql,
                      std::string name,
                      bool include_traceback_header) {
@@ -140,9 +146,15 @@
   return source;
 }
 
-SqlSource SqlSource::FullRewrite(SqlSource source) const {
-  SqlSource::Rewriter rewriter(*this);
-  rewriter.Rewrite(0, static_cast<uint32_t>(sql().size()), source);
+SqlSource SqlSource::RewriteAllIgnoreExisting(SqlSource source) const {
+  // Reset any rewrites.
+  SqlSource copy = *this;
+  copy.root_.rewritten_sql = copy.root_.original_sql;
+  copy.root_.rewrites.clear();
+
+  SqlSource::Rewriter rewriter(std::move(copy));
+  rewriter.Rewrite(0, static_cast<uint32_t>(root_.original_sql.size()),
+                   std::move(source));
   return std::move(rewriter).Build();
 }
 
@@ -162,8 +174,9 @@
 }
 
 std::string SqlSource::Node::AsTraceback(uint32_t rewritten_offset) const {
+  PERFETTO_CHECK(rewritten_offset <= rewritten_sql.size());
   uint32_t original_offset = RewrittenOffsetToOriginalOffset(rewritten_offset);
-  std::string res = SelfTraceback(original_offset);
+  std::string res = SelfTraceback(rewritten_offset, original_offset);
   if (auto opt_idx = RewriteForOriginalOffset(original_offset); opt_idx) {
     const Rewrite& rewrite = rewrites[*opt_idx];
     PERFETTO_CHECK(rewritten_offset >= rewrite.rewritten_sql_start);
@@ -174,12 +187,22 @@
   return res;
 }
 
-std::string SqlSource::Node::SelfTraceback(uint32_t original_offset) const {
+std::string SqlSource::Node::SelfTraceback(uint32_t rewritten_offset,
+                                           uint32_t original_offset) const {
+  PERFETTO_DCHECK(original_offset <= original_sql.size());
   auto [o_context, o_caret_pos] =
       SqlContextAndCaretPos(original_sql, original_offset);
   std::string header;
   if (include_traceback_header) {
-    header = "Traceback (most recent call last):\n";
+    if (!rewrites.empty()) {
+      auto [r_context, r_caret_pos] =
+          SqlContextAndCaretPos(rewritten_sql, rewritten_offset);
+      std::string caret = std::string(r_caret_pos, ' ') + "^";
+      base::StackString<1024> str("Fully expanded statement\n  %s\n  %s\n",
+                                  r_context.c_str(), caret.c_str());
+      header.append(str.c_str());
+    }
+    header += "Traceback (most recent call last):\n";
   }
 
   auto line_and_col =
@@ -193,17 +216,54 @@
 }
 
 SqlSource::Node SqlSource::Node::Substr(uint32_t offset, uint32_t len) const {
-  PERFETTO_CHECK(rewrites.empty());
-  auto line_and_col =
-      GetLineAndColumnForOffset(rewritten_sql, line, col, offset);
+  uint32_t offset_end = offset + len;
+  PERFETTO_CHECK(offset_end <= rewritten_sql.size());
+
+  uint32_t original_offset_start = RewrittenOffsetToOriginalOffset(offset);
+  uint32_t original_offset_end = RewrittenOffsetToOriginalOffset(offset_end);
+  std::vector<Rewrite> new_rewrites;
+  for (const Rewrite& rewrite : rewrites) {
+    if (offset >= rewrite.rewritten_sql_end) {
+      continue;
+    }
+    if (offset_end < rewrite.rewritten_sql_start) {
+      break;
+    }
+    // Special case: when the end of the substr is in the middle of a rewrite,
+    // we actually want to capture the original SQL up to the end of the
+    // rewrite, not just to the start as |ChildRewrittenOffset| returns.
+    if (offset_end < rewrite.rewritten_sql_end) {
+      original_offset_end = rewrite.original_sql_end;
+    }
+    uint32_t bounded_start = std::max(offset, rewrite.rewritten_sql_start);
+    uint32_t bounded_end = std::min(offset_end, rewrite.rewritten_sql_end);
+
+    uint32_t nested_start = bounded_start - rewrite.rewritten_sql_start;
+    uint32_t nested_len = bounded_end - bounded_start;
+
+    new_rewrites.push_back(Rewrite{
+        rewrite.original_sql_start - original_offset_start,
+        rewrite.original_sql_end - original_offset_start,
+        bounded_start - offset,
+        bounded_end - offset,
+        rewrite.rewrite_node.Substr(nested_start, nested_len),
+    });
+  }
+  std::string new_original = original_sql.substr(
+      original_offset_start, original_offset_end - original_offset_start);
+  std::string new_rewritten = rewritten_sql.substr(offset, len);
+  PERFETTO_DCHECK(ApplyRewrites(new_original, new_rewrites) == new_rewritten);
+
+  auto line_and_col = GetLineAndColumnForOffset(rewritten_sql, line, col,
+                                                original_offset_start);
   return Node{
       name,
       include_traceback_header,
       line_and_col.first,
       line_and_col.second,
-      original_sql.substr(offset, len),
-      {},
-      rewritten_sql.substr(offset, len),
+      new_original,
+      std::move(new_rewrites),
+      new_rewritten,
   };
 }
 
@@ -239,32 +299,94 @@
   return std::nullopt;
 }
 
-SqlSource::Rewriter::Rewriter(SqlSource source) : orig_(std::move(source)) {
-  PERFETTO_CHECK(!orig_.IsRewritten());
+SqlSource::Rewriter::Rewriter(SqlSource source)
+    : Rewriter(std::move(source.root_)) {}
+SqlSource::Rewriter::Rewriter(Node source) : orig_(std::move(source)) {
+  // Note: it's important that we *don't* move out of |orig_| here as we want to
+  // be able to access the untouched offsets through
+  // calls to |RewrittenOffsetToOriginalOffset| etc.
+  for (const SqlSource::Rewrite& rewrite : orig_.rewrites) {
+    nested_.push_back(SqlSource::Rewriter(rewrite.rewrite_node));
+  }
 }
 
-void SqlSource::Rewriter::Rewrite(uint32_t start,
-                                  uint32_t end,
+void SqlSource::Rewriter::Rewrite(uint32_t rewritten_start,
+                                  uint32_t rewritten_end,
                                   SqlSource source) {
-  PERFETTO_CHECK(start < end);
-  PERFETTO_CHECK(end <= orig_.sql().size());
+  PERFETTO_CHECK(rewritten_start < rewritten_end);
+  PERFETTO_CHECK(rewritten_end <= orig_.rewritten_sql.size());
 
-  uint32_t source_size = static_cast<uint32_t>(source.sql().size());
-
-  uint32_t rewritten_start =
-      start + rewritten_bytes_in_rewrites - original_bytes_in_rewrites;
-  orig_.root_.rewrites.push_back(SqlSource::Rewrite{
-      start, end, rewritten_start, rewritten_start + source_size,
-      std::move(source.root_)});
-
-  original_bytes_in_rewrites += end - start;
-  rewritten_bytes_in_rewrites += source_size;
+  uint32_t original_start =
+      orig_.RewrittenOffsetToOriginalOffset(rewritten_start);
+  std::optional<uint32_t> maybe_rewrite =
+      orig_.RewriteForOriginalOffset(original_start);
+  if (maybe_rewrite) {
+    const SqlSource::Rewrite& rewrite = orig_.rewrites[*maybe_rewrite];
+    nested_[*maybe_rewrite].Rewrite(
+        rewritten_start - rewrite.rewritten_sql_start,
+        rewritten_end - rewrite.rewritten_sql_start, std::move(source));
+  } else {
+    uint32_t original_end =
+        orig_.RewrittenOffsetToOriginalOffset(rewritten_end);
+    non_nested_.push_back(SqlSource::Rewrite{
+        original_start,
+        original_end,
+        std::numeric_limits<uint32_t>::max(),  // Dummy, corrected in |Build|.
+        std::numeric_limits<uint32_t>::max(),  // Dummy, corrected in |Build|.
+        std::move(source.root_),
+    });
+  }
 }
 
 SqlSource SqlSource::Rewriter::Build() && {
-  orig_.root_.rewritten_sql =
-      ApplyRewrites(orig_.root_.original_sql, orig_.root_.rewrites);
-  return orig_;
+  // Phase 1: finalize all the nested rewrites and merge both nested and
+  // non-nested into a single vector.
+  std::vector<SqlSource::Rewrite> all_rewrites = std::move(non_nested_);
+  for (uint32_t i = 0; i < nested_.size(); ++i) {
+    const SqlSource::Rewrite orig_rewrite = orig_.rewrites[i];
+    all_rewrites.push_back(SqlSource::Rewrite{
+        orig_rewrite.original_sql_start,
+        orig_rewrite.original_sql_end,
+        std::numeric_limits<uint32_t>::max(),  // Dummy, corrected in phase 3.
+        std::numeric_limits<uint32_t>::max(),  // Dummy, corrected in phase 3.
+        std::move(nested_[i]).Build().root_,
+    });
+  }
+
+  // Phase 2: sort the new rewrite vector by original offset and verify that the
+  // original offsets are monotonic and non-overlapping.
+  std::sort(all_rewrites.begin(), all_rewrites.end(),
+            [](const SqlSource::Rewrite& a, const SqlSource::Rewrite& b) {
+              return a.original_sql_start < b.original_sql_start;
+            });
+  for (uint32_t i = 1; i < all_rewrites.size(); ++i) {
+    PERFETTO_CHECK(all_rewrites[i - 1].original_sql_end <=
+                   all_rewrites[i].original_sql_start);
+  }
+
+  // Phase 3: compute the new rewritten offsets and assign them to the rewrites.
+  // Also unset the traceback flag for all rewrites.
+  uint32_t original_bytes_in_rewrites = 0;
+  uint32_t rewritten_bytes_in_rewrites = 0;
+  for (SqlSource::Rewrite& rewrite : all_rewrites) {
+    uint32_t source_size =
+        static_cast<uint32_t>(rewrite.rewrite_node.rewritten_sql.size());
+
+    rewrite.rewritten_sql_start = rewrite.original_sql_start +
+                                  rewritten_bytes_in_rewrites -
+                                  original_bytes_in_rewrites;
+    rewrite.rewritten_sql_end = rewrite.rewritten_sql_start + source_size;
+    rewrite.rewrite_node.include_traceback_header = false;
+
+    original_bytes_in_rewrites +=
+        rewrite.original_sql_end - rewrite.original_sql_start;
+    rewritten_bytes_in_rewrites += source_size;
+  }
+
+  // Phase 4: update the node to reflect the new rewrites.
+  orig_.rewrites = std::move(all_rewrites);
+  orig_.rewritten_sql = ApplyRewrites(orig_.original_sql, orig_.rewrites);
+  return SqlSource(std::move(orig_));
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/sqlite/sql_source.h b/src/trace_processor/sqlite/sql_source.h
index b4b9870..66e4f31 100644
--- a/src/trace_processor/sqlite/sql_source.h
+++ b/src/trace_processor/sqlite/sql_source.h
@@ -78,19 +78,19 @@
   // at |offset| with |len| characters.
   SqlSource Substr(uint32_t offset, uint32_t len) const;
 
-  // Creates a SqlSource instance with the execution SQL rewritten to
-  // |rewrite_sql| but preserving the context from |this|.
+  // Rewrites the SQL backing |this| to SQL from |source| ignoring any existing
+  // rewrites in |this|.
   //
   // This is useful when PerfettoSQL statements are transpiled into SQLite
   // statements but we want to preserve the context of the original statement.
-  //
-  // Note: this function should only be called if |this| has not already been
-  // rewritten (i.e. it is undefined behaviour if |IsRewritten()| returns true).
-  SqlSource FullRewrite(SqlSource) const;
+  SqlSource RewriteAllIgnoreExisting(SqlSource source) const;
 
   // Returns the SQL string backing this SqlSource instance;
   const std::string& sql() const { return root_.rewritten_sql; }
 
+  // Returns the original SQL string backing this SqlSource instance;
+  const std::string& original_sql() const { return root_.original_sql; }
+
   // Returns whether this SqlSource has been rewritten.
   bool IsRewritten() const { return root_.IsRewritten(); }
 
@@ -102,7 +102,7 @@
   // Suppose that we have the the following situation:
   // User: `SELECT foo!(a) FROM bar!(slice) a`
   // foo : `$1.x, $1.y`
-  // bar : `(SELECT baz!($1) FROM $1 LIMIT 1)`
+  // bar : `(SELECT baz!($1) FROM $1)`
   // baz : `$1.x, $1.y, $1.z`
   //
   // We want to expand this to
@@ -127,8 +127,8 @@
   //   rewritten_sql: "SELECT a.x, a.y FROM (SELECT slice.x, slice.y, slice.z
   //                   FROM slice) a"
   //   rewrites: [
-  //     {start: 7, end: 12, node: foo},
-  //     {start: 17, end: 26, node: bar}]
+  //     {original_sql_start: 7, original_sql_end: 14, node: foo},
+  //     {original_sql_start: 20, original_sql_end: 31, node: bar}]
   //   ]
   // }
   // foo {
@@ -139,7 +139,7 @@
   // bar {
   //   original_sql: "(SELECT baz!($1) FROM $1 LIMIT 1)"
   //   rewritten_sql: "(SELECT slice.x, slice.y, slice.z FROM slice)"
-  //   rewrites: [{start: 8, end: 16, node: baz}]
+  //   rewrites: [{original_sql_start: 8, original_sql_end: 16, node: baz}]
   // }
   // baz {
   //   original_sql = "$1.x, $1.y, $1.z"
@@ -170,7 +170,8 @@
 
     // Returns the "traceback" for this node only. See |SqlSource::AsTraceback|
     // for details.
-    std::string SelfTraceback(uint32_t original_offset) const;
+    std::string SelfTraceback(uint32_t rewritten_offset,
+                              uint32_t original_offset) const;
 
     Node Substr(uint32_t rewritten_offset, uint32_t rewritten_len) const;
 
@@ -185,14 +186,22 @@
     // IMPORTANT: if |rewritten_offset| is *inside* a rewrite, the original
     // offset will point to the *start of the rewrite*. For example, if
     // we have:
-    //   original_sql: "SELECT foo!(a) FROM slice"
-    //   rewritten_sql: "SELECT a.x, a.y FROM slice"
-    //   rewrites: [{start: 7, end: 12, node: foo}]
+    //   original_sql: "SELECT foo!(a) FROM slice a"
+    //   rewritten_sql: "SELECT a.x, a.y FROM slice a"
+    //   rewrites: [
+    //     {
+    //       original_sql_start: 7,
+    //       original_sql_end: 14,
+    //       rewritten_sql_start: 7,
+    //       rewritten_sql_end: 15,
+    //       node: foo
+    //     }
+    //   ]
     // then:
     //   RewrittenOffsetToOriginalOffset(7) == 7     // 7 = start of foo
     //   RewrittenOffsetToOriginalOffset(14) == 7    // 7 = start of foo
-    //   RewrittenOffsetToOriginalOffset(15) == 12   // 12 = end of foo
-    //   RewrittenOffsetToOriginalOffset(16) == 13   // 12 = end of foo
+    //   RewrittenOffsetToOriginalOffset(15) == 14   // 14 = end of foo
+    //   RewrittenOffsetToOriginalOffset(16) == 15
     uint32_t RewrittenOffsetToOriginalOffset(uint32_t rewritten_offset) const;
 
     // Given an |original_offset| for this node, returns the index of a
@@ -218,7 +227,8 @@
     Node rewrite_node;
   };
 
-  SqlSource() = default;
+  SqlSource();
+  explicit SqlSource(Node);
   SqlSource(std::string sql, std::string name, bool include_traceback_header);
 
   static std::string ApplyRewrites(const std::string&,
@@ -233,21 +243,56 @@
   // Creates a Rewriter object which can be used to rewrite the SQL backing
   // |source|.
   //
-  // Note: this function should only be called if |source| has not already been
-  // rewritten (i.e. it is undefined behaviour if |source.IsRewritten()| returns
-  // true).
+  // Note that rewrites of portions of the SQL which have already been rewritten
+  // is supported but *only in limited cases*. Specifically, the new rewrite
+  // must not cross the boundary of any existing rewrite.
+  //
+  // For example, if we have:
+  //   SqlSource {
+  //     original_sql: "SELECT foo!(a) FROM bar!(slice) a"
+  //     rewritten_sql: "SELECT a.x, a.y FROM (SELECT slice.x FROM slice) a"
+  //   }
+  // then the following are valid:
+  //   # Replaces "SELECT " with "INSERT ". Valid because it does not touch
+  //   # any rewrite.
+  //   Rewrite(0, 7, "INSERT ")
+  //
+  //   # Replaces "a.x, a." with "a.z, ". Valid because it only touches the
+  //   # contents of the existing "foo" rewrite.
+  //   Rewrite(7, 14, "a.z, ")
+  // while the following are invalid:
+  //   # Fails to replace "SELECT a" with "I". Invalid because it affects both
+  //   # non-rewritten source and the "foo" rewrite.
+  //   Rewrite(0, 8, "I")
+  //
+  //   # Fails to replace "a.x, a.y FROM (" with "(". Invalid because it affects
+  //   # the "foo" rewrite, non-rewritten source and the "bar" rewrite.
+  //   Rewrite(7, 23, "(")
   explicit Rewriter(SqlSource source);
 
-  // Replaces the SQL between |start| and |end| with the contents of |rewrite|.
-  void Rewrite(uint32_t start, uint32_t end, SqlSource rewrite);
+  // Replaces the SQL in |source.rewritten_sql| between |rewritten_start| and
+  // |rewritten_end| with the contents of |rewrite|.
+  //
+  // Note that calls to Rewrite must be monontonic and non-overlapping. i.e.
+  // if Rewrite(0, 10) is called, the next |rewritten_end| must be greater than
+  // or equal to 10.
+  //
+  // Note also that all offsets passed to this function correspond to offsets
+  // into |source.rewritten_sql|: past calls to rewrite do not affect future
+  // offsets.
+  void Rewrite(uint32_t rewritten_start,
+               uint32_t rewritten_end,
+               SqlSource rewrite);
 
   // Returns the rewritten SqlSource instance.
   SqlSource Build() &&;
 
  private:
-  SqlSource orig_;
-  uint32_t original_bytes_in_rewrites = 0;
-  uint32_t rewritten_bytes_in_rewrites = 0;
+  explicit Rewriter(Node);
+
+  Node orig_;
+  std::vector<SqlSource::Rewriter> nested_;
+  std::vector<SqlSource::Rewrite> non_nested_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/sqlite/sql_source_unittest.cc b/src/trace_processor/sqlite/sql_source_unittest.cc
index 945144e..adca9db 100644
--- a/src/trace_processor/sqlite/sql_source_unittest.cc
+++ b/src/trace_processor/sqlite/sql_source_unittest.cc
@@ -53,14 +53,17 @@
             "          ^\n");
 }
 
-TEST(SqlSourceTest, FullRewrite) {
+TEST(SqlSourceTest, RewriteAllIgnoreExisting) {
   SqlSource source =
       SqlSource::FromExecuteQuery("macro!()")
-          .FullRewrite(SqlSource::FromTraceProcessorImplementation(
+          .RewriteAllIgnoreExisting(SqlSource::FromTraceProcessorImplementation(
               "SELECT * FROM slice"));
   ASSERT_EQ(source.sql(), "SELECT * FROM slice");
 
   ASSERT_EQ(source.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT * FROM slice\n"
+            "  ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 1\n"
             "    macro!()\n"
@@ -69,6 +72,9 @@
             "    SELECT * FROM slice\n"
             "    ^\n");
   ASSERT_EQ(source.AsTraceback(7),
+            "Fully expanded statement\n"
+            "  SELECT * FROM slice\n"
+            "         ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 1\n"
             "    macro!()\n"
@@ -81,15 +87,18 @@
 TEST(SqlSourceTest, NestedFullRewrite) {
   SqlSource nested =
       SqlSource::FromTraceProcessorImplementation("nested!()")
-          .FullRewrite(SqlSource::FromTraceProcessorImplementation(
+          .RewriteAllIgnoreExisting(SqlSource::FromTraceProcessorImplementation(
               "SELECT * FROM slice"));
   ASSERT_EQ(nested.sql(), "SELECT * FROM slice");
 
-  SqlSource source =
-      SqlSource::FromExecuteQuery("macro!()").FullRewrite(std::move(nested));
+  SqlSource source = SqlSource::FromExecuteQuery("macro!()")
+                         .RewriteAllIgnoreExisting(std::move(nested));
   ASSERT_EQ(source.sql(), "SELECT * FROM slice");
 
   ASSERT_EQ(source.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT * FROM slice\n"
+            "  ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 1\n"
             "    macro!()\n"
@@ -101,6 +110,9 @@
             "    SELECT * FROM slice\n"
             "    ^\n");
   ASSERT_EQ(source.AsTraceback(7),
+            "Fully expanded statement\n"
+            "  SELECT * FROM slice\n"
+            "         ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 1\n"
             "    macro!()\n"
@@ -113,6 +125,38 @@
             "           ^\n");
 }
 
+TEST(SqlSourceTest, RewriteAllIgnoresExistingCorrectly) {
+  SqlSource foo =
+      SqlSource::FromExecuteQuery("foo!()").RewriteAllIgnoreExisting(
+          SqlSource::FromTraceProcessorImplementation("SELECT * FROM slice"));
+  SqlSource source = foo.RewriteAllIgnoreExisting(
+      SqlSource::FromTraceProcessorImplementation("SELECT 0 WHERE 0"));
+  ASSERT_EQ(source.sql(), "SELECT 0 WHERE 0");
+
+  ASSERT_EQ(source.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT 0 WHERE 0\n"
+            "  ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 1\n"
+            "    foo!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 1\n"
+            "    SELECT 0 WHERE 0\n"
+            "    ^\n");
+  ASSERT_EQ(source.AsTraceback(4),
+            "Fully expanded statement\n"
+            "  SELECT 0 WHERE 0\n"
+            "      ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 1\n"
+            "    foo!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 5\n"
+            "    SELECT 0 WHERE 0\n"
+            "        ^\n");
+}
+
 TEST(SqlSourceTest, Rewriter) {
   SqlSource::Rewriter rewriter(
       SqlSource::FromExecuteQuery("SELECT cols!() FROM slice"));
@@ -125,11 +169,17 @@
 
   // Offset points at the top level source.
   ASSERT_EQ(rewritten.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT ts, dur, ts + dur AS ts_end FROM slice\n"
+            "  ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 1\n"
             "    SELECT cols!() FROM slice\n"
             "    ^\n");
   ASSERT_EQ(rewritten.AsTraceback(40),
+            "Fully expanded statement\n"
+            "  SELECT ts, dur, ts + dur AS ts_end FROM slice\n"
+            "                                          ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 21\n"
             "    SELECT cols!() FROM slice\n"
@@ -137,6 +187,9 @@
 
   // Offset points at the nested source.
   ASSERT_EQ(rewritten.AsTraceback(16),
+            "Fully expanded statement\n"
+            "  SELECT ts, dur, ts + dur AS ts_end FROM slice\n"
+            "                  ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 8\n"
             "    SELECT cols!() FROM slice\n"
@@ -164,11 +217,17 @@
 
   // Offset points at the top level source.
   ASSERT_EQ(rewritten.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  SELECT id, ts, dur, depth, name FROM slice\n"
+            "  ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 1\n"
             "    SELECT cols!() FROM slice\n"
             "    ^\n");
   ASSERT_EQ(rewritten.AsTraceback(37),
+            "Fully expanded statement\n"
+            "  SELECT id, ts, dur, depth, name FROM slice\n"
+            "                                       ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 21\n"
             "    SELECT cols!() FROM slice\n"
@@ -176,6 +235,9 @@
 
   // Offset points at the first nested source.
   ASSERT_EQ(rewritten.AsTraceback(15),
+            "Fully expanded statement\n"
+            "  SELECT id, ts, dur, depth, name FROM slice\n"
+            "                 ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 8\n"
             "    SELECT cols!() FROM slice\n"
@@ -189,6 +251,9 @@
 
   // Offset points at the second nested source.
   ASSERT_EQ(rewritten.AsTraceback(20),
+            "Fully expanded statement\n"
+            "  SELECT id, ts, dur, depth, name FROM slice\n"
+            "                      ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 8\n"
             "    SELECT cols!() FROM slice\n"
@@ -200,6 +265,9 @@
             "    depth\n"
             "    ^\n");
   ASSERT_EQ(rewritten.AsTraceback(22),
+            "Fully expanded statement\n"
+            "  SELECT id, ts, dur, depth, name FROM slice\n"
+            "                        ^\n"
             "Traceback (most recent call last):\n"
             "  File \"stdin\" line 1 col 8\n"
             "    SELECT cols!() FROM slice\n"
@@ -212,6 +280,162 @@
             "      ^\n");
 }
 
+TEST(SqlSourceTest, NestedRewriteSubstr) {
+  SqlSource::Rewriter nested_rewrite(
+      SqlSource::FromTraceProcessorImplementation(
+          "id, common_cols!(), other_cols!(), name"));
+  nested_rewrite.Rewrite(
+      4, 18, SqlSource::FromTraceProcessorImplementation("ts, dur"));
+  nested_rewrite.Rewrite(20, 33,
+                         SqlSource::FromTraceProcessorImplementation("depth"));
+
+  SqlSource::Rewriter rewriter(
+      SqlSource::FromExecuteQuery("SELECT cols!() FROM slice"));
+  rewriter.Rewrite(7, 14, std::move(nested_rewrite).Build());
+
+  SqlSource rewritten = std::move(rewriter).Build();
+  ASSERT_EQ(rewritten.sql(), "SELECT id, ts, dur, depth, name FROM slice");
+
+  // Full macro cover.
+  SqlSource cols = rewritten.Substr(7, 24);
+  ASSERT_EQ(cols.sql(), "id, ts, dur, depth, name");
+  ASSERT_EQ(cols.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  id, ts, dur, depth, name\n"
+            "  ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 8\n"
+            "    cols!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 1\n"
+            "    id, common_cols!(), other_cols!(), name\n"
+            "    ^\n");
+  ASSERT_EQ(cols.AsTraceback(5),
+            "Fully expanded statement\n"
+            "  id, ts, dur, depth, name\n"
+            "       ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 8\n"
+            "    cols!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 5\n"
+            "    id, common_cols!(), other_cols!(), name\n"
+            "        ^\n"
+            "  Trace Processor Internal line 1 col 2\n"
+            "    ts, dur\n"
+            "     ^\n");
+  ASSERT_EQ(cols.AsTraceback(14),
+            "Fully expanded statement\n"
+            "  id, ts, dur, depth, name\n"
+            "                ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 8\n"
+            "    cols!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 21\n"
+            "    id, common_cols!(), other_cols!(), name\n"
+            "                        ^\n"
+            "  Trace Processor Internal line 1 col 2\n"
+            "    depth\n"
+            "     ^\n");
+
+  // Intersect with nested.
+  SqlSource intersect = rewritten.Substr(8, 13);
+  ASSERT_EQ(intersect.sql(), "d, ts, dur, d");
+  ASSERT_EQ(intersect.AsTraceback(0),
+            "Fully expanded statement\n"
+            "  d, ts, dur, d\n"
+            "  ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 8\n"
+            "    cols!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 2\n"
+            "    d, common_cols!(), other_cols!()\n"
+            "    ^\n");
+  ASSERT_EQ(intersect.AsTraceback(4),
+            "Fully expanded statement\n"
+            "  d, ts, dur, d\n"
+            "      ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 8\n"
+            "    cols!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 5\n"
+            "    d, common_cols!(), other_cols!()\n"
+            "       ^\n"
+            "  Trace Processor Internal line 1 col 2\n"
+            "    ts, dur\n"
+            "     ^\n");
+  ASSERT_EQ(intersect.AsTraceback(12),
+            "Fully expanded statement\n"
+            "  d, ts, dur, d\n"
+            "              ^\n"
+            "Traceback (most recent call last):\n"
+            "  File \"stdin\" line 1 col 8\n"
+            "    cols!()\n"
+            "    ^\n"
+            "  Trace Processor Internal line 1 col 21\n"
+            "    d, common_cols!(), other_cols!()\n"
+            "                       ^\n"
+            "  Trace Processor Internal line 1 col 1\n"
+            "    d\n"
+            "    ^\n");
+}
+
+TEST(SqlSourceTest, Rerewrites) {
+  SqlSource::Rewriter rewriter(
+      SqlSource::FromExecuteQuery("SELECT foo!(a) FROM bar!(slice) a"));
+  rewriter.Rewrite(7, 14,
+                   SqlSource::FromTraceProcessorImplementation("a.x, a.y"));
+  rewriter.Rewrite(20, 31,
+                   SqlSource::FromTraceProcessorImplementation(
+                       "(SELECT slice.x, slice.y, slice.z FROM slice)"));
+
+  SqlSource rewritten = std::move(rewriter).Build();
+  ASSERT_EQ(
+      rewritten.sql(),
+      "SELECT a.x, a.y FROM (SELECT slice.x, slice.y, slice.z FROM slice) a");
+
+  SqlSource::Rewriter rerewriter(std::move(rewritten));
+  rerewriter.Rewrite(0, 7,
+                     SqlSource::FromTraceProcessorImplementation("INSERT "));
+  rerewriter.Rewrite(7, 14,
+                     SqlSource::FromTraceProcessorImplementation("a.z, "));
+
+  SqlSource rerewritten = std::move(rerewriter).Build();
+  ASSERT_EQ(
+      rerewritten.sql(),
+      "INSERT a.z, y FROM (SELECT slice.x, slice.y, slice.z FROM slice) a");
+  ASSERT_EQ(
+      rerewritten.AsTraceback(0),
+      "Fully expanded statement\n"
+      "  INSERT a.z, y FROM (SELECT slice.x, slice.y, slice.z FROM slice) a\n"
+      "  ^\n"
+      "Traceback (most recent call last):\n"
+      "  File \"stdin\" line 1 col 1\n"
+      "    SELECT foo!(a) FROM bar!(slice) a\n"
+      "    ^\n"
+      "  Trace Processor Internal line 1 col 1\n"
+      "    INSERT \n"
+      "    ^\n");
+  ASSERT_EQ(
+      rerewritten.AsTraceback(8),
+      "Fully expanded statement\n"
+      "  INSERT a.z, y FROM (SELECT slice.x, slice.y, slice.z FROM slice) a\n"
+      "          ^\n"
+      "Traceback (most recent call last):\n"
+      "  File \"stdin\" line 1 col 8\n"
+      "    SELECT foo!(a) FROM bar!(slice) a\n"
+      "           ^\n"
+      "  Trace Processor Internal line 1 col 1\n"
+      "    a.x, a.y\n"
+      "    ^\n"
+      "  Trace Processor Internal line 1 col 2\n"
+      "    a.z, \n"
+      "     ^\n");
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/sqlite/sqlite_engine.cc b/src/trace_processor/sqlite/sqlite_engine.cc
index 77f4003..23b064a 100644
--- a/src/trace_processor/sqlite/sqlite_engine.cc
+++ b/src/trace_processor/sqlite/sqlite_engine.cc
@@ -131,7 +131,7 @@
 }
 
 SqliteEngine::PreparedStatement SqliteEngine::PrepareStatement(SqlSource sql) {
-  PERFETTO_TP_TRACE(metatrace::Category::QUERY, "QUERY_PREPARE");
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_DETAILED, "QUERY_PREPARE");
   sqlite3_stmt* raw_stmt = nullptr;
   int err =
       sqlite3_prepare_v2(db_.get(), sql.sql().c_str(), -1, &raw_stmt, nullptr);
@@ -222,12 +222,15 @@
 
 SqliteEngine::PreparedStatement::PreparedStatement(ScopedStmt stmt,
                                                    SqlSource source)
-    : stmt_(std::move(stmt)), sql_source_(std::move(source)) {}
+    : stmt_(std::move(stmt)),
+      expanded_sql_(sqlite3_expanded_sql(stmt_.get())),
+      sql_source_(std::move(source)) {}
 
 bool SqliteEngine::PreparedStatement::Step() {
-  PERFETTO_TP_TRACE(metatrace::Category::QUERY, "STMT_STEP",
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_DETAILED, "STMT_STEP",
                     [this](metatrace::Record* record) {
-                      record->AddArg("SQL", expanded_sql());
+                      record->AddArg("Original SQL", original_sql());
+                      record->AddArg("Executed SQL", sql());
                     });
 
   // Now step once into |cur_stmt| so that when we prepare the next statment
@@ -251,14 +254,11 @@
   return !sqlite3_stmt_busy(stmt_.get());
 }
 
-const char* SqliteEngine::PreparedStatement::sql() const {
-  return sqlite3_sql(stmt_.get());
+const char* SqliteEngine::PreparedStatement::original_sql() const {
+  return sql_source_.original_sql().c_str();
 }
 
-const char* SqliteEngine::PreparedStatement::expanded_sql() {
-  if (!expanded_sql_) {
-    expanded_sql_.reset(sqlite3_expanded_sql(stmt_.get()));
-  }
+const char* SqliteEngine::PreparedStatement::sql() const {
   return expanded_sql_.get();
 }
 
diff --git a/src/trace_processor/sqlite/sqlite_engine.h b/src/trace_processor/sqlite/sqlite_engine.h
index b93248e..1a1b089 100644
--- a/src/trace_processor/sqlite/sqlite_engine.h
+++ b/src/trace_processor/sqlite/sqlite_engine.h
@@ -58,8 +58,8 @@
     bool Step();
     bool IsDone() const;
 
+    const char* original_sql() const;
     const char* sql() const;
-    const char* expanded_sql();
 
     const base::Status& status() const { return status_; }
     sqlite3_stmt* sqlite_stmt() const { return stmt_.get(); }
@@ -70,8 +70,8 @@
     explicit PreparedStatement(ScopedStmt, SqlSource);
 
     ScopedStmt stmt_;
-    SqlSource sql_source_;
     ScopedSqliteString expanded_sql_;
+    SqlSource sql_source_;
     base::Status status_ = base::OkStatus();
   };
 
diff --git a/src/trace_processor/sqlite/sqlite_table.cc b/src/trace_processor/sqlite/sqlite_table.cc
index c32a318..c599b2f 100644
--- a/src/trace_processor/sqlite/sqlite_table.cc
+++ b/src/trace_processor/sqlite/sqlite_table.cc
@@ -184,8 +184,8 @@
     cache_hit = false;
   }
 
-  PERFETTO_TP_TRACE(metatrace::Category::QUERY, "SQLITE_TABLE_READ_CONSTRAINTS",
-                    [&](metatrace::Record* r) {
+  PERFETTO_TP_TRACE(metatrace::Category::QUERY_DETAILED,
+                    "SQLITE_TABLE_READ_CONSTRAINTS", [&](metatrace::Record* r) {
                       r->AddArg("cache_hit", std::to_string(cache_hit));
                       r->AddArg("name", name_);
                       WriteQueryConstraintsToMetatrace(r, qc_cache_, schema_);
@@ -412,7 +412,7 @@
   }
 
   PERFETTO_TP_TRACE(
-      metatrace::Category::QUERY, "SQLITE_TABLE_BEST_INDEX",
+      metatrace::Category::QUERY_TIMELINE, "SQLITE_TABLE_BEST_INDEX",
       [&](metatrace::Record* r) {
         r->AddArg("name", table->name());
         WriteQueryConstraintsToMetatrace(r, qc, table->schema());
diff --git a/src/trace_processor/sqlite/sqlite_tokenizer.cc b/src/trace_processor/sqlite/sqlite_tokenizer.cc
index 6ad1bb6..5a17f30 100644
--- a/src/trace_processor/sqlite/sqlite_tokenizer.cc
+++ b/src/trace_processor/sqlite/sqlite_tokenizer.cc
@@ -18,6 +18,7 @@
 
 #include <ctype.h>
 #include <sqlite3.h>
+#include <cstdint>
 #include <optional>
 #include <string_view>
 
@@ -454,14 +455,21 @@
   return tok;
 }
 
-SqlSource SqliteTokenizer::Substr(Token start, Token end) const {
+SqlSource SqliteTokenizer::Substr(const Token& start, const Token& end) const {
   uint32_t offset =
       static_cast<uint32_t>(start.str.data() - source_.sql().c_str());
   uint32_t len = static_cast<uint32_t>(end.str.data() - start.str.data());
   return source_.Substr(offset, len);
 }
 
-std::string SqliteTokenizer::AsTraceback(Token token) const {
+SqlSource SqliteTokenizer::SubstrToken(const Token& token) const {
+  uint32_t offset =
+      static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
+  uint32_t len = static_cast<uint32_t>(token.str.size());
+  return source_.Substr(offset, len);
+}
+
+std::string SqliteTokenizer::AsTraceback(const Token& token) const {
   PERFETTO_CHECK(source_.sql().c_str() <= token.str.data());
   PERFETTO_CHECK(token.str.data() <=
                  source_.sql().c_str() + source_.sql().size());
@@ -470,5 +478,30 @@
   return source_.AsTraceback(offset);
 }
 
+void SqliteTokenizer::Rewrite(SqlSource::Rewriter& rewriter,
+                              const Token& start,
+                              const Token& end,
+                              SqlSource rewrite,
+                              EndToken end_token) const {
+  uint32_t s_off =
+      static_cast<uint32_t>(start.str.data() - source_.sql().c_str());
+  uint32_t e_off =
+      static_cast<uint32_t>(end.str.data() - source_.sql().c_str());
+  uint32_t e_diff = end_token == EndToken::kInclusive
+                        ? static_cast<uint32_t>(end.str.size())
+                        : 0;
+  rewriter.Rewrite(s_off, e_off + e_diff, std::move(rewrite));
+}
+
+void SqliteTokenizer::RewriteToken(SqlSource::Rewriter& rewriter,
+                                   const Token& token,
+                                   SqlSource rewrite) const {
+  uint32_t s_off =
+      static_cast<uint32_t>(token.str.data() - source_.sql().c_str());
+  uint32_t e_off = static_cast<uint32_t>(token.str.data() + token.str.size() -
+                                         source_.sql().c_str());
+  rewriter.Rewrite(s_off, e_off, std::move(rewrite));
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/sqlite/sqlite_tokenizer.h b/src/trace_processor/sqlite/sqlite_tokenizer.h
index d0ef24c..f4e9f79 100644
--- a/src/trace_processor/sqlite/sqlite_tokenizer.h
+++ b/src/trace_processor/sqlite/sqlite_tokenizer.h
@@ -80,7 +80,7 @@
     std::string_view str;
 
     // The type of the token.
-    SqliteTokenType token_type;
+    SqliteTokenType token_type = SqliteTokenType::TK_ILLEGAL;
 
     bool operator==(const Token& o) const {
       return str == o.str && token_type == o.token_type;
@@ -92,6 +92,12 @@
     }
   };
 
+  enum class EndToken {
+    kExclusive,
+    kInclusive,
+  };
+
+  // Creates a tokenizer which tokenizes |sql|.
   explicit SqliteTokenizer(SqlSource sql);
 
   // Returns the next SQL token.
@@ -107,14 +113,34 @@
   //
   // Note: |start| and |end| must both have been previously returned by this
   // tokenizer.
-  SqlSource Substr(Token start, Token end) const;
+  SqlSource Substr(const Token& start, const Token& end) const;
+
+  // Returns an SqlSource containing only the SQL backing |token|.
+  //
+  // Note: |token| must have been previously returned by this tokenizer.
+  SqlSource SubstrToken(const Token& token) const;
 
   // Returns a traceback error message for the SqlSource backing this tokenizer
   // pointing to |token|. See SqlSource::AsTraceback for more information about
   // this method.
   //
   // Note: |token| must have been previously returned by this tokenizer.
-  std::string AsTraceback(Token) const;
+  std::string AsTraceback(const Token&) const;
+
+  // Replaces the SQL in |rewriter| between |start| and |end| with the contents
+  // of |rewrite|. If |end_token| == kInclusive, the end token is also included
+  // in the rewrite.
+  void Rewrite(SqlSource::Rewriter& rewriter,
+               const Token& start,
+               const Token& end,
+               SqlSource rewrite,
+               EndToken end_token = EndToken::kExclusive) const;
+
+  // Replaces the SQL in |rewriter| backing |token| with the contents of
+  // |rewrite|.
+  void RewriteToken(SqlSource::Rewriter&,
+                    const Token&,
+                    SqlSource rewrite) const;
 
   // Resets this tokenizer to tokenize |source|. Any previous returned tokens
   // are invalidated.
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py
index c712adf..8361d8c 100644
--- a/src/trace_processor/tables/profiler_tables.py
+++ b/src/trace_processor/tables/profiler_tables.py
@@ -144,10 +144,10 @@
         ''',
         group='Callstack profilers',
         columns={
-            'build_id': '''hex-encoded Build ID of the binary / library.''',
-            'start': '''start of the mapping in the process' address space.''',
-            'end': '''end of the mapping in the process' address space.''',
-            'name': '''filename of the binary / library.''',
+            'build_id': '''Hex-encoded Build ID of the binary / library.''',
+            'start': '''Start of the mapping in the process' address space.''',
+            'end': '''End of the mapping in the process' address space.''',
+            'name': '''Filename of the binary / library.''',
             'exact_offset': '''''',
             'start_offset': '''''',
             'load_bias': ''''''
@@ -172,16 +172,16 @@
         group='Callstack profilers',
         columns={
             'name':
-                '''name of the function this location is in.''',
+                '''Name of the function this location is in.''',
             'mapping':
-                '''the mapping (library / binary) this location is in.''',
+                '''The mapping (library / binary) this location is in.''',
             'rel_pc':
-                '''the program counter relative to the start of the mapping.''',
+                '''The program counter relative to the start of the mapping.''',
             'symbol_set_id':
-                '''if the profile was offline symbolized, the offline
+                '''If the profile was offline symbolized, the offline
 symbol information of this frame.''',
             'deobfuscated_name':
-                ''''''
+                '''Deobfuscated name of the function this location is in.'''
         }))
 
 STACK_PROFILE_CALLSITE_TABLE = Table(
@@ -201,11 +201,11 @@
         group='Callstack profilers',
         columns={
             'depth':
-                '''distance from the bottom-most frame of the callstack.''',
+                '''Distance from the bottom-most frame of the callstack.''',
             'parent_id':
-                '''parent frame on the callstack. NULL for the bottom-most.''',
+                '''Parent frame on the callstack. NULL for the bottom-most.''',
             'frame_id':
-                '''frame at this position in the callstack.'''
+                '''Frame at this position in the callstack.'''
         }))
 
 STACK_SAMPLE_TABLE = Table(
@@ -265,23 +265,23 @@
         group='Callstack profilers',
         columns={
             'ts':
-                '''timestamp of the sample.''',
+                '''Timestamp of the sample.''',
             'utid':
-                '''sampled thread..''',
+                '''Sampled thread.''',
             'cpu':
-                '''the core the sampled thread was running on.''',
+                '''Core the sampled thread was running on.''',
             'cpu_mode':
-                '''execution state (userspace/kernelspace) of the sampled
+                '''Execution state (userspace/kernelspace) of the sampled
 thread.''',
             'callsite_id':
-                '''if set, unwound callstack of the sampled thread.''',
+                '''If set, unwound callstack of the sampled thread.''',
             'unwind_error':
-                '''if set, indicates that the unwinding for this sample
+                '''If set, indicates that the unwinding for this sample
 encountered an error. Such samples still reference the best-effort
-result via the callsite_id (with a synthetic error frame at the point
-where unwinding stopped).''',
+result via the callsite_id, with a synthetic error frame at the point
+where unwinding stopped.''',
             'perf_session_id':
-                '''distinguishes samples from different profiling
+                '''Distinguishes samples from different profiling
 streams (i.e. multiple data sources).'''
         }))
 
diff --git a/src/trace_processor/tp_metatrace.cc b/src/trace_processor/tp_metatrace.cc
index 65f0d07..d9bbfe2 100644
--- a/src/trace_processor/tp_metatrace.cc
+++ b/src/trace_processor/tp_metatrace.cc
@@ -24,15 +24,19 @@
 
 using ProtoEnum = protos::pbzero::MetatraceCategories;
 ProtoEnum MetatraceCategoriesToProtoEnum(MetatraceCategories categories) {
+  // Note: these are intentionally chained ifs and not else-ifs as it's possible
+  // for multiple of these if statements to be true.
   ProtoEnum result = ProtoEnum::NONE;
-  if (categories & MetatraceCategories::TOPLEVEL)
-    result = static_cast<ProtoEnum>(result | ProtoEnum::TOPLEVEL);
-  if (categories & MetatraceCategories::FUNCTION)
-    result = static_cast<ProtoEnum>(result | ProtoEnum::FUNCTION);
-  if (categories & MetatraceCategories::QUERY)
-    result = static_cast<ProtoEnum>(result | ProtoEnum::QUERY);
+  if (categories & MetatraceCategories::QUERY_TIMELINE)
+    result = static_cast<ProtoEnum>(result | ProtoEnum::QUERY_TIMELINE);
+  if (categories & MetatraceCategories::FUNCTION_CALL)
+    result = static_cast<ProtoEnum>(result | ProtoEnum::FUNCTION_CALL);
+  if (categories & MetatraceCategories::QUERY_DETAILED)
+    result = static_cast<ProtoEnum>(result | ProtoEnum::QUERY_DETAILED);
   if (categories & MetatraceCategories::DB)
     result = static_cast<ProtoEnum>(result | ProtoEnum::DB);
+  if (categories & MetatraceCategories::API_TIMELINE)
+    result = static_cast<ProtoEnum>(result | ProtoEnum::API_TIMELINE);
   return result;
 }
 
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 21f4709..da31fc4 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -726,7 +726,7 @@
 }
 
 Iterator TraceProcessorImpl::ExecuteQuery(const std::string& sql) {
-  PERFETTO_TP_TRACE(metatrace::Category::TOPLEVEL, "QUERY_EXECUTE");
+  PERFETTO_TP_TRACE(metatrace::Category::API_TIMELINE, "EXECUTE_QUERY");
 
   uint32_t sql_stats_row =
       context_.storage->mutable_sql_stats()->RecordQueryBegin(
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 0f0543f..7873af5 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -46,6 +46,7 @@
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/version.h"
 
+#include "perfetto/trace_processor/metatrace_config.h"
 #include "perfetto/trace_processor/read_trace.h"
 #include "perfetto/trace_processor/trace_processor.h"
 #include "src/trace_processor/metrics/all_chrome_metrics.descriptor.h"
@@ -645,14 +646,16 @@
     std::string cur = splitter.cur_token();
     if (cur == "all" || cur == "*") {
       result = Cat::ALL;
-    } else if (cur == "toplevel") {
-      result = static_cast<Cat>(result | Cat::TOPLEVEL);
-    } else if (cur == "function") {
-      result = static_cast<Cat>(result | Cat::FUNCTION);
-    } else if (cur == "query") {
-      result = static_cast<Cat>(result | Cat::QUERY);
+    } else if (cur == "query_toplevel") {
+      result = static_cast<Cat>(result | Cat::QUERY_TIMELINE);
+    } else if (cur == "query_detailed") {
+      result = static_cast<Cat>(result | Cat::QUERY_DETAILED);
+    } else if (cur == "function_call") {
+      result = static_cast<Cat>(result | Cat::FUNCTION_CALL);
     } else if (cur == "db") {
       result = static_cast<Cat>(result | Cat::DB);
+    } else if (cur == "api") {
+      result = static_cast<Cat>(result | Cat::API_TIMELINE);
     } else {
       PERFETTO_ELOG("Unknown metatrace category %s", cur.data());
       exit(1);
@@ -681,7 +684,9 @@
   std::string metatrace_path;
   size_t metatrace_buffer_capacity = 0;
   metatrace::MetatraceCategories metatrace_categories =
-      metatrace::MetatraceCategories::TOPLEVEL;
+      static_cast<metatrace::MetatraceCategories>(
+          metatrace::MetatraceCategories::QUERY_TIMELINE |
+          metatrace::MetatraceCategories::API_TIMELINE);
   bool dev = false;
   bool no_ftrace_raw = false;
   bool analyze_trace_proto_content = false;
diff --git a/src/trace_processor/util/proto_profiler_unittest.cc b/src/trace_processor/util/proto_profiler_unittest.cc
index 181bc97..a2c969a 100644
--- a/src/trace_processor/util/proto_profiler_unittest.cc
+++ b/src/trace_processor/util/proto_profiler_unittest.cc
@@ -57,9 +57,9 @@
     got.emplace_back(path, *sample);
   }
   std::vector<Item> expected{
-      {{"NestedA"}, 15},
-      {{"NestedA", "#repeated_a", "NestedB"}, 5},
-      {{"NestedA", "#repeated_a", "NestedB"}, 5},
+      {{"NestedA"}, 6},
+      {{"NestedA", "#repeated_a", "NestedB"}, 2},
+      {{"NestedA", "#repeated_a", "NestedB"}, 2},
       {{"NestedA", "#repeated_a", "NestedB", "#value_b", "NestedC"}, 1},
       {{"NestedA", "#repeated_a", "NestedB", "#value_b", "NestedC"}, 1},
       {{"NestedA", "#repeated_a", "NestedB", "#value_b", "NestedC", "#value_c",
diff --git a/src/traced/service/service.cc b/src/traced/service/service.cc
index edbc981..a9b6bb7 100644
--- a/src/traced/service/service.cc
+++ b/src/traced/service/service.cc
@@ -17,6 +17,7 @@
 #include <stdio.h>
 #include <algorithm>
 
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/getopt.h"
 #include "perfetto/ext/base/string_utils.h"
@@ -29,16 +30,6 @@
 #include "perfetto/ext/tracing/ipc/service_ipc_host.h"
 #include "src/traced/service/builtin_producer.h"
 
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
-    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define PERFETTO_SET_SOCKET_PERMISSIONS
-#include <fcntl.h>
-#include <grp.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-#endif
-
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
 #include <sys/system_properties.h>
 #endif
@@ -49,40 +40,6 @@
 
 namespace perfetto {
 namespace {
-#if defined(PERFETTO_SET_SOCKET_PERMISSIONS)
-void SetSocketPermissions(const std::string& socket_name,
-                          const std::string& group_name,
-                          const std::string& mode_bits) {
-  PERFETTO_CHECK(!socket_name.empty());
-  PERFETTO_CHECK(!group_name.empty());
-  struct group* socket_group = nullptr;
-  // Query the group ID of |group|.
-  do {
-    socket_group = getgrnam(group_name.c_str());
-  } while (socket_group == nullptr && errno == EINTR);
-  if (socket_group == nullptr) {
-    PERFETTO_FATAL("Failed to get group information of %s ",
-                   group_name.c_str());
-  }
-
-  if (PERFETTO_EINTR(
-          chown(socket_name.c_str(), geteuid(), socket_group->gr_gid))) {
-    PERFETTO_FATAL("Failed to chown %s ", socket_name.c_str());
-  }
-
-  // |mode| accepts values like "0660" as "rw-rw----" mode bits.
-  auto mode_value = base::StringToInt32(mode_bits, 8);
-  if (!(mode_bits.size() == 4 && mode_value.has_value())) {
-    PERFETTO_FATAL(
-        "The chmod option must be a 4-digit octal number, e.g. 0660");
-  }
-  if (PERFETTO_EINTR(chmod(socket_name.c_str(),
-                           static_cast<mode_t>(mode_value.value())))) {
-    PERFETTO_FATAL("Failed to chmod %s", socket_name.c_str());
-  }
-}
-#endif  // defined(PERFETTO_SET_SOCKET_PERMISSIONS)
-
 void PrintUsage(const char* prog_name) {
   fprintf(stderr, R"(
 Usage: %s [option] ...
@@ -192,18 +149,21 @@
     started = svc->Start(producer_sockets, GetConsumerSocket());
 
     if (!producer_socket_group.empty()) {
-#if defined(PERFETTO_SET_SOCKET_PERMISSIONS)
+      auto status = base::OkStatus();
       for (const auto& producer_socket : producer_sockets) {
-        SetSocketPermissions(producer_socket, producer_socket_group,
-                             producer_socket_mode);
+        status = base::SetFilePermissions(
+            producer_socket, producer_socket_group, producer_socket_mode);
+        if (!status.ok()) {
+          PERFETTO_ELOG("%s", status.c_message());
+          return 1;
+        }
       }
-      SetSocketPermissions(GetConsumerSocket(), consumer_socket_group,
-                           consumer_socket_mode);
-#else
-      PERFETTO_ELOG(
-          "Setting socket permissions is not supported on this platform");
-      return 1;
-#endif
+      status = base::SetFilePermissions(
+          GetConsumerSocket(), consumer_socket_group, consumer_socket_mode);
+      if (!status.ok()) {
+        PERFETTO_ELOG("%s", status.c_message());
+        return 1;
+      }
     }
   }
 
diff --git a/src/traced_relay/BUILD.gn b/src/traced_relay/BUILD.gn
new file mode 100644
index 0000000..d4e27cc
--- /dev/null
+++ b/src/traced_relay/BUILD.gn
@@ -0,0 +1,47 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../gn/perfetto.gni")
+import("../../gn/perfetto_component.gni")
+
+executable("traced_relay") {
+  deps = [
+    ":lib",
+    "../../gn:default_deps",
+    "../../include/perfetto/ext/traced",
+    "../base",
+    "../base:unix_socket",
+    "../base:version",
+    "../ipc:perfetto_ipc",
+    "../tracing/ipc:default_socket",
+  ]
+  sources = [ "relay_service_main.cc" ]
+}
+
+source_set("lib") {
+  public_deps = [ "../../include/perfetto/ext/tracing/ipc" ]
+  sources = [
+    "relay_service.cc",
+    "relay_service.h",
+    "socket_relay_handler.cc",
+    "socket_relay_handler.h",
+  ]
+  deps = [
+    "../../gn:default_deps",
+    "../../protos/perfetto/ipc",
+    "../../protos/perfetto/ipc:wire_protocol_cpp",
+    "../base",
+    "//src/ipc:perfetto_ipc",
+  ]
+}
diff --git a/src/traced_relay/relay_service.cc b/src/traced_relay/relay_service.cc
new file mode 100644
index 0000000..4b1dc6b
--- /dev/null
+++ b/src/traced_relay/relay_service.cc
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/traced_relay/relay_service.h"
+#include <memory>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "perfetto/ext/base/utils.h"
+#include "protos/perfetto/ipc/wire_protocol.gen.h"
+#include "src/ipc/buffered_frame_deserializer.h"
+#include "src/traced_relay/socket_relay_handler.h"
+
+using ::perfetto::protos::gen::IPCFrame;
+
+namespace perfetto {
+
+RelayService::RelayService(base::TaskRunner* task_runner)
+    : task_runner_(task_runner) {}
+
+void RelayService::Start(const char* listening_socket_name,
+                         const char* client_socket_name) {
+  auto sock_family = base::GetSockFamily(listening_socket_name);
+  listening_socket_ =
+      base::UnixSocket::Listen(listening_socket_name, this, task_runner_,
+                               sock_family, base::SockType::kStream);
+  bool producer_socket_listening =
+      listening_socket_ && listening_socket_->is_listening();
+  if (!producer_socket_listening) {
+    PERFETTO_FATAL("Failed to listen to socket %s", listening_socket_name);
+  }
+
+  // Save |client_socket_name| for opening new client connection to remote
+  // service when a local producer connects.
+  client_socket_name_ = client_socket_name;
+}
+
+void RelayService::OnNewIncomingConnection(
+    base::UnixSocket* listen_socket,
+    std::unique_ptr<base::UnixSocket> server_conn) {
+  PERFETTO_DCHECK(listen_socket == listening_socket_.get());
+
+  // Create a connection to the host to pair with |listen_conn|.
+  auto sock_family = base::GetSockFamily(client_socket_name_.c_str());
+  auto client_conn =
+      base::UnixSocket::Connect(client_socket_name_, this, task_runner_,
+                                sock_family, base::SockType::kStream);
+
+  // Pre-queue the SetPeerIdentity request. By enqueueing it into the buffer,
+  // this will be sent out as first frame as soon as we connect to the real
+  // traced.
+  //
+  // This code pretends that we received a SetPeerIdentity frame from the
+  // connecting producer (while instead we are just forging it). The host traced
+  // will only accept only one SetPeerIdentity request pre-queued here.
+  IPCFrame ipc_frame;
+  ipc_frame.set_request_id(0);
+  auto* set_peer_identity = ipc_frame.mutable_set_peer_identity();
+  set_peer_identity->set_pid(server_conn->peer_pid_linux());
+  set_peer_identity->set_uid(
+      static_cast<int32_t>(server_conn->peer_uid_posix()));
+
+  // Buffer the SetPeerIdentity request.
+  auto req = ipc::BufferedFrameDeserializer::Serialize(ipc_frame);
+  SocketWithBuffer server, client;
+  PERFETTO_CHECK(server.available_bytes() >= req.size());
+  memcpy(server.buffer(), req.data(), req.size());
+  server.EnqueueData(req.size());
+
+  // Shut down all callbacks associated with the socket in preparation for the
+  // transfer to |socket_relay_handler_|.
+  server.sock = server_conn->ReleaseSocket();
+  auto new_socket_pair =
+      std::make_unique<SocketPair>(std::move(server), std::move(client));
+  pending_connections_.push_back(
+      {std::move(new_socket_pair), std::move(client_conn)});
+}
+
+void RelayService::OnConnect(base::UnixSocket* self, bool connected) {
+  // This only happens when the client connection is connected or has failed.
+  auto it =
+      std::find_if(pending_connections_.begin(), pending_connections_.end(),
+                   [&](const PendingConnection& pending_conn) {
+                     return pending_conn.connecting_client_conn.get() == self;
+                   });
+  PERFETTO_CHECK(it != pending_connections_.end());
+  // Need to remove the element in |pending_connections_| regardless of
+  // |connected|.
+  auto remover = base::OnScopeExit([&]() { pending_connections_.erase(it); });
+
+  if (!connected)
+    return;  // This closes both sockets in PendingConnection.
+
+  // Shut down event handlers and pair with a server connection.
+  it->socket_pair->second.sock = self->ReleaseSocket();
+
+  // Transfer the socket pair to SocketRelayHandler.
+  socket_relay_handler_.AddSocketPair(std::move(it->socket_pair));
+}
+
+void RelayService::OnDisconnect(base::UnixSocket*) {
+  PERFETTO_DFATAL("Should be unreachable.");
+}
+
+void RelayService::OnDataAvailable(base::UnixSocket*) {
+  PERFETTO_DFATAL("Should be unreachable.");
+}
+
+}  // namespace perfetto
diff --git a/src/traced_relay/relay_service.h b/src/traced_relay/relay_service.h
new file mode 100644
index 0000000..8e5bf6d
--- /dev/null
+++ b/src/traced_relay/relay_service.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACED_RELAY_RELAY_SERVICE_H_
+#define SRC_TRACED_RELAY_RELAY_SERVICE_H_
+
+#include <memory>
+#include <vector>
+
+#include "perfetto/ext/base/unix_socket.h"
+#include "src/traced_relay/socket_relay_handler.h"
+
+namespace perfetto {
+
+namespace base {
+class TaskRunner;
+}  // namespace base.
+
+// A class for relaying the producer data between the local producers and the
+// remote tracing service.
+class RelayService : public base::UnixSocket::EventListener {
+ public:
+  explicit RelayService(base::TaskRunner* task_runner);
+  ~RelayService() override = default;
+
+  // Starts the service relay that forwards messages between the
+  // |server_socket_name| and |client_socket_name| ports.
+  void Start(const char* server_socket_name, const char* client_socket_name);
+
+ private:
+  struct PendingConnection {
+    // This keeps a connected UnixSocketRaw server socket in its first element.
+    std::unique_ptr<SocketPair> socket_pair;
+    // This keeps the connecting client connection.
+    std::unique_ptr<base::UnixSocket> connecting_client_conn;
+  };
+
+  RelayService(const RelayService&) = delete;
+  RelayService& operator=(const RelayService&) = delete;
+
+  // UnixSocket::EventListener implementation.
+  void OnNewIncomingConnection(base::UnixSocket*,
+                               std::unique_ptr<base::UnixSocket>) override;
+  void OnConnect(base::UnixSocket* self, bool connected) override;
+  void OnDisconnect(base::UnixSocket* self) override;
+  void OnDataAvailable(base::UnixSocket* self) override;
+
+  base::TaskRunner* const task_runner_ = nullptr;
+
+  std::unique_ptr<base::UnixSocket> listening_socket_;
+  std::string client_socket_name_;
+
+  // Keeps the socket pairs while waiting for relay connections to be
+  // established.
+  std::vector<PendingConnection> pending_connections_;
+
+  SocketRelayHandler socket_relay_handler_;
+};
+
+}  // namespace perfetto
+
+#endif  // SRC_TRACED_RELAY_RELAY_SERVICE_H_
diff --git a/src/traced_relay/relay_service_main.cc b/src/traced_relay/relay_service_main.cc
new file mode 100644
index 0000000..9562b2d
--- /dev/null
+++ b/src/traced_relay/relay_service_main.cc
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/getopt.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/unix_task_runner.h"
+#include "perfetto/ext/base/version.h"
+#include "perfetto/ext/base/watchdog.h"
+#include "perfetto/ext/traced/traced.h"
+#include "perfetto/ext/tracing/ipc/default_socket.h"
+#include "src/traced_relay/relay_service.h"
+
+namespace perfetto {
+namespace {
+void PrintUsage(const char* prog_name) {
+  fprintf(stderr, R"(
+Usage: %s [option] ...
+Options and arguments
+    --background : Exits immediately and continues running in the background
+    --version : print the version number and exit.
+    --set-socket-permissions <permissions> : sets group ownership and permission
+        mode bits of the listening socket.
+        <permissions> format: <prod_group>:<prod_mode>,
+        where <prod_group> is the group name for chgrp the listening socket,
+        <prod_mode> is the mode bits (e.g. 0660) for chmod the producer socket,
+
+Example:
+    %s --set-socket-permissions traced-producer:0660 starts the service and sets
+    the group ownership of the listening socket to "traced-producer". The
+    listening socket is chmod with 0660 (rw-rw----) mode bits. )",
+          prog_name, prog_name);
+}
+
+}  // namespace
+
+static int RelayServiceMain(int argc, char** argv) {
+  enum LongOption {
+    OPT_VERSION = 1000,
+    OPT_SET_SOCKET_PERMISSIONS = 1001,
+    OPT_BACKGROUND,
+  };
+
+  bool background = false;
+
+  static const option long_options[] = {
+      {"background", no_argument, nullptr, OPT_BACKGROUND},
+      {"version", no_argument, nullptr, OPT_VERSION},
+      {"set-socket-permissions", required_argument, nullptr,
+       OPT_SET_SOCKET_PERMISSIONS},
+      {nullptr, 0, nullptr, 0}};
+
+  std::string listen_socket_group, consumer_socket_group,
+      listen_socket_mode_bits, consumer_socket_mode;
+
+  for (;;) {
+    int option = getopt_long(argc, argv, "", long_options, nullptr);
+    if (option == -1)
+      break;
+    switch (option) {
+      case OPT_BACKGROUND:
+        background = true;
+        break;
+      case OPT_VERSION:
+        printf("%s\n", base::GetVersionString());
+        return 0;
+      case OPT_SET_SOCKET_PERMISSIONS: {
+        // Check that the socket permission argument is well formed.
+        auto parts = perfetto::base::SplitString(std::string(optarg), ":");
+        PERFETTO_CHECK(parts.size() == 2);
+        PERFETTO_CHECK(
+            std::all_of(parts.cbegin(), parts.cend(),
+                        [](const std::string& part) { return !part.empty(); }));
+        listen_socket_group = parts[0];
+        listen_socket_mode_bits = parts[1];
+        break;
+      }
+      default:
+        PrintUsage(argv[0]);
+        return 1;
+    }
+  }
+
+  if (background) {
+    base::Daemonize([] { return 0; });
+  }
+
+  auto listen_socket = GetProducerSocket();
+  remove(listen_socket);
+  if (!listen_socket_group.empty()) {
+    auto status = base::SetFilePermissions(listen_socket, listen_socket_group,
+                                           listen_socket_mode_bits);
+    if (!status.ok()) {
+      PERFETTO_ELOG("Failed to set socket permissions: %s", status.c_message());
+      return 1;
+    }
+  }
+
+  base::UnixTaskRunner task_runner;
+  auto svc = std::make_unique<RelayService>(&task_runner);
+  svc->Start(listen_socket, GetRelaySocket());
+
+  // Set the CPU limit and start the watchdog running. The memory limit will
+  // be set inside the service code as it relies on the size of buffers.
+  // The CPU limit is the generic one defined in watchdog.h.
+  base::Watchdog* watchdog = base::Watchdog::GetInstance();
+  watchdog->SetCpuLimit(base::kWatchdogDefaultCpuLimit,
+                        base::kWatchdogDefaultCpuWindow);
+  watchdog->Start();
+
+  PERFETTO_ILOG("Started traced_relay, listening on %s, forwarding to %s",
+                GetProducerSocket(), GetRelaySocket());
+
+  task_runner.Run();
+  return 0;
+}
+}  // namespace perfetto
+
+int main(int argc, char** argv) {
+  return perfetto::RelayServiceMain(argc, argv);
+}
diff --git a/src/traced_relay/socket_relay_handler.cc b/src/traced_relay/socket_relay_handler.cc
new file mode 100644
index 0000000..2930c86
--- /dev/null
+++ b/src/traced_relay/socket_relay_handler.cc
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/traced_relay/socket_relay_handler.h"
+
+#include <fcntl.h>
+#include <sys/poll.h>
+#include <algorithm>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/platform_handle.h"
+#include "perfetto/ext/base/thread_checker.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/ext/base/watchdog.h"
+#include "perfetto/ext/base/watchdog_posix.h"
+
+namespace perfetto {
+namespace {
+// Use the the default watchdog timeout for task runners.
+static constexpr int kWatchdogTimeoutMs = 30000;
+// Timeout of the epoll_wait() call.
+static constexpr int kPollTimeoutMs = 30000;
+}  // namespace
+
+FdPoller::Watcher::~Watcher() = default;
+
+FdPoller::FdPoller(Watcher* watcher) : watcher_(watcher) {
+  WatchForRead(notify_fd_.fd());
+
+  // This is done last in the ctor because WatchForRead() asserts using
+  // |thread_checker_|.
+  PERFETTO_DETACH_FROM_THREAD(thread_checker_);
+}
+
+void FdPoller::Poll() {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+
+  int num_fds =
+      PERFETTO_EINTR(poll(&poll_fds_[0], poll_fds_.size(), kPollTimeoutMs));
+  if (num_fds == -1 && base::IsAgain(errno))
+    return;  // Poll again.
+  PERFETTO_DCHECK(num_fds <= static_cast<int>(poll_fds_.size()));
+
+  // Make a copy of |poll_fds_| so it's safe to watch and unwatch while
+  // notifying the watcher.
+  const auto poll_fds(poll_fds_);
+
+  for (const auto& event : poll_fds) {
+    if (!event.revents)  // This event isn't active.
+      continue;
+
+    // Check whether the poller needs to break the polling loop for updates.
+    if (event.fd == notify_fd_.fd()) {
+      notify_fd_.Clear();
+      continue;
+    }
+
+    // Notify the callers on fd events.
+    if (event.revents & POLLOUT) {
+      watcher_->OnFdWritable(event.fd);
+    } else if (event.revents & POLLIN) {
+      watcher_->OnFdReadable(event.fd);
+    } else {
+      PERFETTO_DLOG("poll() returns events %d on fd %d", event.events,
+                    event.fd);
+    }  // Other events like POLLHUP or POLLERR are ignored.
+  }
+}
+
+void FdPoller::Notify() {
+  // Can be called from any thread.
+  notify_fd_.Notify();
+}
+
+std::vector<pollfd>::iterator FdPoller::FindPollEvent(base::PlatformHandle fd) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+
+  return std::find_if(poll_fds_.begin(), poll_fds_.end(),
+                      [fd](const pollfd& item) { return fd == item.fd; });
+}
+
+void FdPoller::WatchFd(base::PlatformHandle fd, WatchEvents events) {
+  auto it = FindPollEvent(fd);
+  if (it == poll_fds_.end()) {
+    poll_fds_.push_back({fd, events, 0});
+  } else {
+    it->events |= events;
+  }
+}
+
+void FdPoller::UnwatchFd(base::PlatformHandle fd, WatchEvents events) {
+  auto it = FindPollEvent(fd);
+  PERFETTO_CHECK(it != poll_fds_.end());
+  it->events &= ~events;
+}
+
+void FdPoller::RemoveWatch(base::PlatformHandle fd) {
+  auto it = FindPollEvent(fd);
+  PERFETTO_CHECK(it != poll_fds_.end());
+  poll_fds_.erase(it);
+}
+
+SocketRelayHandler::SocketRelayHandler() : fd_poller_(this) {
+  PERFETTO_DETACH_FROM_THREAD(io_thread_checker_);
+
+  io_thread_ = std::thread([this]() { this->Run(); });
+}
+
+SocketRelayHandler::~SocketRelayHandler() {
+  RunOnIOThread([this]() { this->exited_ = true; });
+  io_thread_.join();
+}
+
+void SocketRelayHandler::AddSocketPair(
+    std::unique_ptr<SocketPair> socket_pair) {
+  RunOnIOThread([this, socket_pair = std::move(socket_pair)]() mutable {
+    PERFETTO_DCHECK_THREAD(io_thread_checker_);
+
+    base::PlatformHandle fd1 = socket_pair->first.sock.fd();
+    base::PlatformHandle fd2 = socket_pair->second.sock.fd();
+    auto* ptr = socket_pair.get();
+    socket_pairs_.emplace_back(std::move(socket_pair));
+
+    fd_poller_.WatchForRead(fd1);
+    fd_poller_.WatchForRead(fd2);
+
+    socket_pairs_by_fd_[fd1] = ptr;
+    socket_pairs_by_fd_[fd2] = ptr;
+  });
+}
+
+void SocketRelayHandler::Run() {
+  PERFETTO_DCHECK_THREAD(io_thread_checker_);
+
+  while (!exited_) {
+    fd_poller_.Poll();
+
+    auto handle = base::Watchdog::GetInstance()->CreateFatalTimer(
+        kWatchdogTimeoutMs, base::WatchdogCrashReason::kTaskRunnerHung);
+
+    std::deque<std::packaged_task<void()>> pending_tasks;
+    {
+      std::lock_guard<std::mutex> lock(mutex_);
+      pending_tasks = std::move(pending_tasks_);
+    }
+    while (!pending_tasks.empty()) {
+      auto task = std::move(pending_tasks.front());
+      pending_tasks.pop_front();
+      task();
+    }
+  }
+}
+
+void SocketRelayHandler::OnFdReadable(base::PlatformHandle fd) {
+  PERFETTO_DCHECK_THREAD(io_thread_checker_);
+
+  auto socket_pair = GetSocketPair(fd);
+  if (!socket_pair)
+    return;  // Already removed.
+
+  auto [fd_sock, peer_sock] = *socket_pair;
+  // Buffer some bytes.
+  auto peer_fd = peer_sock.sock.fd();
+  while (fd_sock.available_bytes() > 0) {
+    auto rsize =
+        fd_sock.sock.Receive(fd_sock.buffer(), fd_sock.available_bytes());
+    if (rsize > 0) {
+      fd_sock.EnqueueData(static_cast<size_t>(rsize));
+      continue;
+    }
+
+    if (rsize == 0 || (rsize == -1 && !base::IsAgain(errno))) {
+      // TODO(chinglinyu): flush the remaining data to |peer_sock|.
+      RemoveSocketPair(fd_sock, peer_sock);
+      return;
+    }
+
+    // If there is any buffered data that needs to be sent to |peer_sock|, arm
+    // the write watcher.
+    if (fd_sock.data_size() > 0) {
+      fd_poller_.WatchForWrite(peer_fd);
+    }
+    return;
+  }
+  // We are not bufferable: need to turn off POLLIN to avoid spinning.
+  fd_poller_.UnwatchForRead(fd);
+  PERFETTO_DCHECK(fd_sock.data_size() > 0);
+  // Watching for POLLOUT will cause an OnFdWritable() event of
+  // |peer_sock|.
+  fd_poller_.WatchForWrite(peer_fd);
+}
+
+void SocketRelayHandler::OnFdWritable(base::PlatformHandle fd) {
+  PERFETTO_DCHECK_THREAD(io_thread_checker_);
+
+  auto socket_pair = GetSocketPair(fd);
+  if (!socket_pair)
+    return;  // Already removed.
+
+  auto [fd_sock, peer_sock] = *socket_pair;
+  // |fd_sock| can be written to without blocking. Now we can transfer from the
+  // buffer in |peer_sock|.
+  while (peer_sock.data_size() > 0) {
+    auto wsize = fd_sock.sock.Send(peer_sock.data(), peer_sock.data_size());
+    if (wsize > 0) {
+      peer_sock.DequeueData(static_cast<size_t>(wsize));
+      continue;
+    }
+
+    if (wsize == -1 && !base::IsAgain(errno)) {
+      RemoveSocketPair(fd_sock, peer_sock);
+    }
+    // errno == EAGAIN and we still have data to send: continue watching for
+    // read.
+    return;
+  }
+
+  // We don't have buffered data to send. Disable watching for write.
+  fd_poller_.UnwatchForWrite(fd);
+  auto peer_fd = peer_sock.sock.fd();
+  if (peer_sock.available_bytes())
+    fd_poller_.WatchForRead(peer_fd);
+}
+
+std::optional<std::tuple<SocketWithBuffer&, SocketWithBuffer&>>
+SocketRelayHandler::GetSocketPair(base::PlatformHandle fd) {
+  PERFETTO_DCHECK_THREAD(io_thread_checker_);
+
+  auto* socket_pair = socket_pairs_by_fd_.Find(fd);
+  if (!socket_pair)
+    return std::nullopt;
+
+  PERFETTO_DCHECK(fd == (*socket_pair)->first.sock.fd() ||
+                  fd == (*socket_pair)->second.sock.fd());
+
+  if (fd == (*socket_pair)->first.sock.fd())
+    return std::tie((*socket_pair)->first, (*socket_pair)->second);
+
+  return std::tie((*socket_pair)->second, (*socket_pair)->first);
+}
+
+void SocketRelayHandler::RemoveSocketPair(SocketWithBuffer& sock1,
+                                          SocketWithBuffer& sock2) {
+  PERFETTO_DCHECK_THREAD(io_thread_checker_);
+
+  auto fd1 = sock1.sock.fd();
+  auto fd2 = sock2.sock.fd();
+  fd_poller_.RemoveWatch(fd1);
+  fd_poller_.RemoveWatch(fd2);
+
+  auto* ptr1 = socket_pairs_by_fd_.Find(fd1);
+  auto* ptr2 = socket_pairs_by_fd_.Find(fd2);
+  PERFETTO_DCHECK(ptr1 && ptr2);
+  PERFETTO_DCHECK(*ptr1 == *ptr2);
+
+  auto* socket_pair_ptr = *ptr1;
+
+  socket_pairs_by_fd_.Erase(fd1);
+  socket_pairs_by_fd_.Erase(fd2);
+
+  socket_pairs_.erase(
+      std::remove_if(
+          socket_pairs_.begin(), socket_pairs_.end(),
+          [socket_pair_ptr](const std::unique_ptr<SocketPair>& item) {
+            return item.get() == socket_pair_ptr;
+          }),
+      socket_pairs_.end());
+}
+
+}  // namespace perfetto
diff --git a/src/traced_relay/socket_relay_handler.h b/src/traced_relay/socket_relay_handler.h
new file mode 100644
index 0000000..39579a9
--- /dev/null
+++ b/src/traced_relay/socket_relay_handler.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACED_RELAY_SOCKET_RELAY_HANDLER_H_
+#define SRC_TRACED_RELAY_SOCKET_RELAY_HANDLER_H_
+
+#include <poll.h>
+
+#include <cstring>
+#include <deque>
+#include <future>
+#include <mutex>
+#include <optional>
+#include <thread>
+#include <tuple>
+
+#include "perfetto/base/platform_handle.h"
+#include "perfetto/ext/base/event_fd.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/thread_checker.h"
+#include "perfetto/ext/base/unix_socket.h"
+#include "perfetto/ext/ipc/basic_types.h"
+
+namespace perfetto {
+
+// FdPoller is a utility for waiting for IO events of a set of watched file
+// descriptors. It's used for multiplexing non-blocking IO operations.
+class FdPoller {
+ public:
+  // The interface class for observing IO events from the FdPoller class.
+  class Watcher {
+   public:
+    virtual ~Watcher();
+    // Called when |fd| can be read from without blocking. For a socket
+    // connection, this indicates the socket read buffer has some data.
+    virtual void OnFdReadable(base::PlatformHandle fd) = 0;
+    // Called when |fd| can be written to without blocking. For a socket
+    // connection, this indicates that the socket write buffer has some capacity
+    // for writting data into.
+    virtual void OnFdWritable(base::PlatformHandle fd) = 0;
+  };
+
+  using WatchEvents = decltype(pollfd::events);
+
+  explicit FdPoller(Watcher* watcher);
+
+  // Watch and unwatch IO event for a given file descriptor.
+  inline void WatchForRead(base::PlatformHandle fd) { WatchFd(fd, POLLIN); }
+  inline void WatchForWrite(base::PlatformHandle fd) { WatchFd(fd, POLLOUT); }
+  inline void UnwatchForRead(base::PlatformHandle fd) { UnwatchFd(fd, POLLIN); }
+  inline void UnwatchForWrite(base::PlatformHandle fd) {
+    UnwatchFd(fd, POLLOUT);
+  }
+
+  // Called when |fd| is no longer of interest (e.g. when |fd| is to be closed).
+  void RemoveWatch(base::PlatformHandle fd);
+
+  // Poll for all watched events previously added with WatchForRead() and
+  // WatchForWrite().
+  //
+  // Must be called on poller thread.
+  void Poll();
+
+  // Notifies the poller for pending updates. Calling Notify() will unblock the
+  // poller and make it return from Poll(). It is caller's responsibility to
+  // call Poll() again once the updates are complete.
+  //
+  // This can be (and typically is) called from any thread.
+  void Notify();
+
+ private:
+  std::vector<pollfd>::iterator FindPollEvent(base::PlatformHandle fd);
+  void WatchFd(base::PlatformHandle fd, WatchEvents events);
+  void UnwatchFd(base::PlatformHandle fd, WatchEvents events);
+
+  base::ThreadChecker thread_checker_;
+  Watcher* const watcher_;
+  base::EventFd notify_fd_;
+  std::vector<pollfd> poll_fds_;
+};
+
+// This class groups a UnixSocketRaw with an associated ring buffer. The ring
+// buffer is used as a temporary storage for data *read* from the socket.
+class SocketWithBuffer {
+ public:
+  constexpr static size_t kBuffSize = ipc::kIPCBufferSize;
+
+  base::UnixSocketRaw sock;
+
+  // Points to the beginning of buffered data.
+  inline uint8_t* data() { return &buf_[0]; }
+  // Size of the buffered data.
+  inline size_t data_size() { return data_size_; }
+
+  // Points to the beginning of the free space for buffering new data.
+  inline uint8_t* buffer() { return &buf_[data_size_]; }
+  // Size of the free space.
+  inline size_t available_bytes() { return buf_.size() - data_size_; }
+
+  // Called when |bytes| of data is enqueued to the buffer.
+  void EnqueueData(size_t bytes) {
+    PERFETTO_CHECK(bytes <= available_bytes());
+    data_size_ += bytes;
+  }
+  // Called when |bytes| of data is dequeued from the buffer.
+  void DequeueData(size_t bytes) {
+    PERFETTO_CHECK(bytes <= data_size());
+    memmove(data(), data() + bytes, data_size() - bytes);
+    data_size_ -= bytes;
+  }
+
+  SocketWithBuffer() : buf_(kBuffSize) {}
+
+  // Movable only.
+  SocketWithBuffer(SocketWithBuffer&& other) = default;
+  SocketWithBuffer& operator=(SocketWithBuffer&& other) = default;
+  SocketWithBuffer(const SocketWithBuffer& other) = delete;
+  SocketWithBuffer& operator=(const SocketWithBuffer& other) = delete;
+
+ private:
+  std::vector<uint8_t> buf_;
+  size_t data_size_ = 0;
+};
+
+using SocketPair = std::pair<SocketWithBuffer, SocketWithBuffer>;
+
+// SocketRelayHandler bidirectionally forwards data between paired sockets.
+// Internally it multiplexes IO operations of the sockets using a FdPoller on a
+// dedicated thread.
+class SocketRelayHandler : public FdPoller::Watcher {
+ public:
+  SocketRelayHandler();
+  SocketRelayHandler(const SocketRelayHandler&) = delete;
+  SocketRelayHandler& operator=(const SocketRelayHandler&) = delete;
+  ~SocketRelayHandler() override;
+
+  // Transfer a pair of sockets to be relayed. Can be called from any thread.
+  void AddSocketPair(std::unique_ptr<SocketPair> socket_pair);
+
+  // The FdPoller::Watcher callbacks.
+  void OnFdReadable(base::PlatformHandle fd) override;
+  void OnFdWritable(base::PlatformHandle fd) override;
+
+ private:
+  void Run();
+  void RemoveSocketPair(SocketWithBuffer&, SocketWithBuffer&);
+
+  // A helper for running a callable object on |io_thread_|.
+  template <typename Callable>
+  void RunOnIOThread(Callable&& c) {
+    std::lock_guard<std::mutex> lock(mutex_);
+    pending_tasks_.emplace_back(std::forward<Callable>(c));
+    fd_poller_.Notify();
+  }
+
+  std::optional<std::tuple<SocketWithBuffer&, SocketWithBuffer&>> GetSocketPair(
+      base::PlatformHandle fd);
+
+  base::FlatHashMap<base::PlatformHandle, SocketPair*> socket_pairs_by_fd_;
+  std::vector<std::unique_ptr<SocketPair>> socket_pairs_;
+
+  FdPoller fd_poller_;
+
+  // The thread that fd_poller_ polls for IO events. Most methods of this class
+  // asserts to be running on this thread.
+  std::thread io_thread_;
+  base::ThreadChecker io_thread_checker_;
+
+  bool exited_ = false;
+
+  //--------------- Member data with multi-thread access ------------------
+  std::mutex mutex_;
+  std::deque<std::packaged_task<void()>> pending_tasks_;
+};
+
+}  // namespace perfetto
+#endif  // SRC_TRACED_RELAY_SOCKET_RELAY_HANDLER_H_
diff --git a/src/tracing/core/trace_writer_impl_unittest.cc b/src/tracing/core/trace_writer_impl_unittest.cc
index a5b8606..c59d9c5 100644
--- a/src/tracing/core/trace_writer_impl_unittest.cc
+++ b/src/tracing/core/trace_writer_impl_unittest.cc
@@ -299,7 +299,14 @@
   const BufferID kBufId = 42;
   std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId);
 
-  ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter();
+  TraceWriterImpl::TracePacketHandle handle = writer->NewTracePacket();
+
+  // Avoid a secondary DCHECK failure from ~TraceWriterImpl() =>
+  // Message::Finalize() due to the stream writer being modified behind the
+  // Message's back. This turns the Finalize() call into a no-op.
+  handle->set_size_field(nullptr);
+
+  ScatteredStreamWriter* sw = handle.TakeStreamWriter();
   std::string raw_proto_bytes = std::string("RAW_PROTO_BYTES");
   sw->WriteBytes(reinterpret_cast<const uint8_t*>(raw_proto_bytes.data()),
                  raw_proto_bytes.size());
diff --git a/src/tracing/ipc/default_socket.cc b/src/tracing/ipc/default_socket.cc
index a2c11d8..c445c6e 100644
--- a/src/tracing/ipc/default_socket.cc
+++ b/src/tracing/ipc/default_socket.cc
@@ -88,6 +88,11 @@
   return name;
 }
 
+const char* GetRelaySocket() {
+  // The relay socket is optional and is connected only when the env var is set.
+  return getenv("PERFETTO_RELAY_SOCK_NAME");
+}
+
 std::vector<std::string> TokenizeProducerSockets(
     const char* producer_socket_names) {
   return base::SplitString(producer_socket_names, ",");
diff --git a/test/data/android_boot.pftrace.sha256 b/test/data/android_boot.pftrace.sha256
new file mode 100644
index 0000000..1d9798f
--- /dev/null
+++ b/test/data/android_boot.pftrace.sha256
@@ -0,0 +1 @@
+660231895e7c1816bcbd02771fdf825917ba0540124c3fdd174c1a91470b9448
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index 05dac05..6cee1b2 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-b301d6515efaffb58f8e8227ae5bcf6b78ea592b92f9037c2abe1954c6102b8e
\ No newline at end of file
+ec0a00856b147b2e13d0fe18666a307eb085ac437d67f78787131d4ea4190581
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/android/android_monitor_contention.out b/test/trace_processor/diff_tests/android/android_monitor_contention.out
index b0a9894..43cf561 100644
--- a/test/trace_processor/diff_tests/android/android_monitor_contention.out
+++ b/test/trace_processor/diff_tests/android/android_monitor_contention.out
@@ -9455,41 +9455,6 @@
     }
   }
   node {
-    node_id: 1303
-    ts: 1737162534263
-    dur: 420259
-    blocking_method: "void com.android.server.am.AppProfiler.collectPssInBackground()"
-    blocked_method: "void com.android.server.am.ProcessProfileRecord.onProcessInactive(com.android.server.am.ProcessStatsService)"
-    short_blocking_method: "com.android.server.am.AppProfiler.collectPssInBackground"
-    short_blocked_method: "com.android.server.am.ProcessProfileRecord.onProcessInactive"
-    blocking_src: "AppProfiler.java:514"
-    blocked_src: "ProcessProfileRecord.java:349"
-    waiter_count: 0
-    blocking_thread_name: "android.bg"
-    blocked_thread_name: "binder:642_14"
-    blocked_thread_tid: 3485
-    blocking_thread_tid: 670
-    process_name: "system_server"
-    pid: 642
-    is_blocked_thread_main: false
-    is_blocking_thread_main: false
-    thread_states {
-      thread_state: "R"
-      thread_state_dur: 389378
-      thread_state_count: 2
-    }
-    thread_states {
-      thread_state: "R+"
-      thread_state_dur: 8868
-      thread_state_count: 1
-    }
-    thread_states {
-      thread_state: "Running"
-      thread_state_dur: 22013
-      thread_state_count: 2
-    }
-  }
-  node {
     node_child_id: 1307
     node_id: 1303
     ts: 1737162534263
@@ -9526,36 +9491,6 @@
     }
   }
   node {
-    node_id: 949
-    ts: 1737122781871
-    dur: 7301144
-    blocking_method: "void com.android.server.am.AppProfiler.collectPssInBackground()"
-    blocked_method: "void com.android.server.am.ProcessRecord.setPid(int)"
-    short_blocking_method: "com.android.server.am.AppProfiler.collectPssInBackground"
-    short_blocked_method: "com.android.server.am.ProcessRecord.setPid"
-    blocking_src: "AppProfiler.java:514"
-    blocked_src: "ProcessRecord.java:596"
-    waiter_count: 0
-    blocking_thread_name: "android.bg"
-    blocked_thread_name: "binder:642_12"
-    blocked_thread_tid: 2720
-    blocking_thread_tid: 670
-    process_name: "system_server"
-    pid: 642
-    is_blocked_thread_main: false
-    is_blocking_thread_main: false
-    thread_states {
-      thread_state: "R+"
-      thread_state_dur: 7127675
-      thread_state_count: 3
-    }
-    thread_states {
-      thread_state: "Running"
-      thread_state_dur: 173469
-      thread_state_count: 2
-    }
-  }
-  node {
     node_child_id: 956
     node_id: 949
     ts: 1737122781871
diff --git a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
index 5b69129..a30c810 100644
--- a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
+++ b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
@@ -14,6 +14,20 @@
         min_dur_ns: 10000000
     }
     blocking_calls {
+        name: "ImageFloatingTextView#onMeasure"
+        cnt: 1
+        total_dur_ns: 10000000
+        max_dur_ns: 10000000
+        min_dur_ns: 10000000
+    }
+    blocking_calls {
+        name: "NotificationShadeWindowView#onMeasure"
+        cnt: 1
+        total_dur_ns: 10000000
+        max_dur_ns: 10000000
+        min_dur_ns: 10000000
+    }
+    blocking_calls {
         name: "NotificationStackScrollLayout#onMeasure"
         cnt: 1
         total_dur_ns: 10000000
diff --git a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
index c5df8b4..cab8c8f 100644
--- a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
+++ b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
@@ -24,7 +24,8 @@
 # List of blocking calls
 blocking_call_names = [
     'NotificationStackScrollLayout#onMeasure', 'ExpNotRow#onMeasure(MessagingStyle)',
-    'ExpNotRow#onMeasure(BigTextStyle)',
+    'ExpNotRow#onMeasure(BigTextStyle)', 'NotificationShadeWindowView#onMeasure',
+    'ImageFloatingTextView#onMeasure',
     'Should not be in the metric'
 ]
 
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index fcf165c..8bbbeb7 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -1173,3 +1173,16 @@
         /system/bin/servicemanager (0x0)
         /system/bin/storaged (0x0)
         """))
+
+  def test_android_boot(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_boot.pftrace'),
+        query=Metric('android_boot'),
+        out=TextProto(r"""
+        android_boot {
+          system_server_durations {
+            total_dur: 267193980530
+            uninterruptible_sleep_dur: 3843119529
+          }
+        }
+        """))
diff --git a/test/trace_processor/diff_tests/android_fs/tests.py b/test/trace_processor/diff_tests/android_fs/tests.py
new file mode 100644
index 0000000..358384d
--- /dev/null
+++ b/test/trace_processor/diff_tests/android_fs/tests.py
@@ -0,0 +1,67 @@
+#!/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 AndroidFs(TestSuite):
+
+  # android_fs_dataread
+  def test_android_fs_dataread(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 46448185788840
+              pid: 5892
+             android_fs_dataread_start {
+                bytes: 4096
+                pid: 5892
+                ino: 836
+                offset: 0
+                cmdline: "am"
+                i_size: 31772
+                pathbuf: "/system/bin/cmd"
+              }
+            }
+          }
+        }
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 46448185789530
+              pid: 156
+              android_fs_dataread_end {
+                bytes: 4096
+                ino: 836
+                offset: 0
+              }
+            }
+          }
+        }
+        """),
+        query="""
+        SELECT ts, dur, name FROM slice WHERE name = 'android_fs_data_read';
+        """,
+        out=Csv("""
+        "ts","dur","name"
+        46448185788840,690,"android_fs_data_read"
+        """))
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index b960520..b131e43 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -94,6 +94,7 @@
 from diff_tests.translation.tests import Translation
 from diff_tests.ufs.tests import Ufs
 from diff_tests.webview.tests import WebView
+from diff_tests.android_fs.tests import AndroidFs
 
 sys.path.pop()
 
@@ -102,6 +103,7 @@
   return [
       *Android(index_path, 'android', 'Android').fetch(),
       *AndroidBugreport(index_path, 'android', 'AndroidBugreport').fetch(),
+      *AndroidFs(index_path, 'android_fs', 'AndroidFs').fetch(),
       *AndroidGames(index_path, 'android', 'AndroidGames').fetch(),
       *Atrace(index_path, 'atrace', 'Atrace').fetch(),
       *AtraceErrorHandling(index_path, 'atrace', 'AtraceErrorHandling').fetch(),
diff --git a/test/trace_processor/diff_tests/perfetto_sql/tests.py b/test/trace_processor/diff_tests/perfetto_sql/tests.py
index f011d3e..d5e37b3 100644
--- a/test/trace_processor/diff_tests/perfetto_sql/tests.py
+++ b/test/trace_processor/diff_tests/perfetto_sql/tests.py
@@ -157,3 +157,31 @@
         "TRACE_START()"
         1000
         """))
+
+  def test_macro(self):
+    return DiffTestBlueprint(
+        trace=TextProto(''),
+        query='''
+        CREATE PERFETTO MACRO foo(a Expr,b Expr) RETURNS TableOrSubquery AS
+        SELECT $a - $b;
+        SELECT (foo!(123, 100)) as res;
+        ''',
+        out=Csv("""
+        "res"
+        23
+        """))
+
+  def test_nested_macro(self):
+    return DiffTestBlueprint(
+        trace=TextProto(''),
+        query='''
+        CREATE PERFETTO MACRO foo(a Expr) returns Expr AS $a;
+        CREATE PERFETTO MACRO bar(a Expr) returns Expr AS (SELECT $a);
+        CREATE PERFETTO MACRO baz(a Expr,b Expr) returns TableOrSubquery AS
+        SELECT bar!(foo!(123)) - $b as res;
+        baz!(123, 100);
+        ''',
+        out=Csv("""
+        "res"
+        23
+        """))
diff --git a/tools/cpu_profile b/tools/cpu_profile
index 8ba1f0a..a5e852e 100755
--- a/tools/cpu_profile
+++ b/tools/cpu_profile
@@ -37,18 +37,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        8758200,
+        8889568,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/traceconv',
     'sha256':
-        'b6eb3552efc97b15b1cb7a746b90f77a4ac781566faa4ffa84e1785f5d90b38b',
+        'e45d08f7050553f77c87764823c6ca26e877d0b3865f2764b33dcab6dfd0b9ee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -58,11 +58,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7332088,
+        7447928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/traceconv',
     'sha256':
-        '585ee448560fa89f559566cdf41dcc6677a28c74ea3b5bae3cfe31cde813578c',
+        '19cabe8789566e6383632bbe79e360b8bf05465f8bb156788c8c957aba77f224',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -72,11 +72,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8515568,
+        8633416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/traceconv',
     'sha256':
-        'bff2c0c525193fc3555725b5767a30bc2b36d7d84b849b7ce41f7abb349f21be',
+        'a7a309c667a3e6a4c0268252540e3fc8052ebbd0abc30faf8f1783df411cf559',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -86,11 +86,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6421928,
+        6505268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/traceconv',
     'sha256':
-        '566298db777a31f3eabd0e6b17e1b0638875725be767333308f02a7fb7d4e36c',
+        '3ff0f9d4ef43e9e76d27c69827e508f4504d2d1fcfb4dac9d739023cd72106d7',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -100,11 +100,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8001776,
+        8108432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/traceconv',
     'sha256':
-        'ccf81438df9ed30f88e640f62938b6334698b9158364db43252d7183136e137d',
+        '1f4641b341f2c8bf36faf8d4befedc01bd414c8f778a5b314c99c2d42c42c068',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -114,55 +114,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6021600,
+        6096120,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/traceconv',
     'sha256':
-        'edae3606563616fa304e3240a8426db3dfd7d12ad28bb879e7d482af357af7f2'
+        'f15c86b1d83bd1706b81426a6d11c3274044b4838abfb5d313954c65282162a9'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7305880,
+        7401672,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/traceconv',
     'sha256':
-        '1a0ef7601a6d2f77f9a04d5bb8c092f5a536764a8e9a99325fff6b81c2879d1e'
+        '8e98e30a8e137116e74390bcb9d662f0a3f22f9d38fe3fd7ef305d1051bdf19d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8118692,
+        8238268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/traceconv',
     'sha256':
-        '6f9b53427d367e8582b12763d27e83217c9e744f947f58acc0b404b6ce5103e9'
+        '8ce56de90b18cc01d94b26f521292d71326552f8b4fb29b919f4b2b80c3eb2ec'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8288976,
+        8397056,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/traceconv',
     'sha256':
-        '5c843ddb151f5aa32288814cb190a879f15e6da63d642d3ded0f15954f480b5f'
+        'e5f31569261b3f201e2cc13b8eea510627d555767272373d9196e83ddaefa3f0'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7756800,
+        7867904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/windows-amd64/traceconv.exe',
     'sha256':
-        'b07849cd60872279dda68d4b8ec173af01e0cbb1692eb5473ef230ac89198f2d',
+        '74c24b2a74ae2a490ac5f918ab079a7713af07e9a36e70a2cc8536040d7df099',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/gen_bazel b/tools/gen_bazel
index d07cc13..b693c6f 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -76,7 +76,6 @@
 # exported publicly.
 default_targets = [
     '//src/base:perfetto_base_default_platform',
-    '//src/bigtrace:bigtrace',
     '//src/ipc:perfetto_ipc',
     '//src/ipc/protoc_plugin:ipc_plugin',
     '//src/protozero:protozero',
@@ -88,12 +87,6 @@
     '//test:client_api_example',
 ] + public_targets
 
-# Proto targets are required by internal build rules but don't need to be
-# exported publicly.
-proto_default_targets = [
-  '//protos/perfetto/bigtrace:lite'
-]
-
 # Proto target groups which will be made public.
 proto_groups = {
     'config': {
@@ -119,6 +112,10 @@
         'sources': ['//protos/perfetto/metrics/chrome:source_set',],
         'visibility': ALLOWLIST_PUBLIC_VISIBILITY,
     },
+    'trace_processor': {
+        'sources': ['//protos/perfetto/trace_processor:source_set',],
+        'visibility': [],
+    },
 }
 
 # Path for the protobuf sources in the standalone build.
@@ -517,30 +514,33 @@
       ':' + get_bazel_proto_sources_label(name)
       for name in sorted(list(deps_set))
   ]
-  sources_label.visibility = desc['visibility']
   sources_label.comment = f'''[{', '.join(desc['sources'])}]'''
 
   cc_label = BazelLabel(name + '_cc_proto', 'perfetto_cc_proto_library')
   cc_label.deps = [':' + sources_label.name]
-  cc_label.visibility = desc['visibility']
   cc_label.comment = sources_label.comment
 
   java_label = BazelLabel(name + '_java_proto', 'perfetto_java_proto_library')
   java_label.deps = [':' + sources_label.name]
-  java_label.visibility = desc['visibility']
   java_label.comment = sources_label.comment
 
   lite_name = name + '_java_proto_lite'
   java_lite_label = BazelLabel(lite_name, 'perfetto_java_lite_proto_library')
   java_lite_label.deps = [':' + sources_label.name]
-  java_lite_label.visibility = desc['visibility']
   java_lite_label.comment = sources_label.comment
 
   py_label = BazelLabel(name + '_py_pb2', 'perfetto_py_proto_library')
   py_label.deps = [':' + sources_label.name]
-  py_label.visibility = desc['visibility']
   py_label.comment = sources_label.comment
 
+  visibility = desc['visibility']
+  if visibility:
+    sources_label.visibility = visibility
+    cc_label.visibility = visibility
+    java_label.visibility = visibility
+    java_lite_label.visibility = visibility
+    py_label.visibility = visibility
+
   return [sources_label, cc_label, java_label, java_lite_label, py_label]
 
 
@@ -796,10 +796,6 @@
       continue
     gn.get_target(re.sub('(lite|zero|cpp|ipc)$', 'source_set', target.name))
 
-  # Discover all the default proto targets so it will be generated next.
-  for target in sorted(proto_default_targets):
-    gn.get_target(target)
-
   # Generate targets for the transitive set of proto targets.
   labels = [
       l for target in sorted(itervalues(gn.proto_libs))
diff --git a/tools/heap_profile b/tools/heap_profile
index 71fb54a..6c2a399 100755
--- a/tools/heap_profile
+++ b/tools/heap_profile
@@ -34,18 +34,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        8758200,
+        8889568,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/traceconv',
     'sha256':
-        'b6eb3552efc97b15b1cb7a746b90f77a4ac781566faa4ffa84e1785f5d90b38b',
+        'e45d08f7050553f77c87764823c6ca26e877d0b3865f2764b33dcab6dfd0b9ee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -55,11 +55,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7332088,
+        7447928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/traceconv',
     'sha256':
-        '585ee448560fa89f559566cdf41dcc6677a28c74ea3b5bae3cfe31cde813578c',
+        '19cabe8789566e6383632bbe79e360b8bf05465f8bb156788c8c957aba77f224',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -69,11 +69,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8515568,
+        8633416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/traceconv',
     'sha256':
-        'bff2c0c525193fc3555725b5767a30bc2b36d7d84b849b7ce41f7abb349f21be',
+        'a7a309c667a3e6a4c0268252540e3fc8052ebbd0abc30faf8f1783df411cf559',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -83,11 +83,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6421928,
+        6505268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/traceconv',
     'sha256':
-        '566298db777a31f3eabd0e6b17e1b0638875725be767333308f02a7fb7d4e36c',
+        '3ff0f9d4ef43e9e76d27c69827e508f4504d2d1fcfb4dac9d739023cd72106d7',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -97,11 +97,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8001776,
+        8108432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/traceconv',
     'sha256':
-        'ccf81438df9ed30f88e640f62938b6334698b9158364db43252d7183136e137d',
+        '1f4641b341f2c8bf36faf8d4befedc01bd414c8f778a5b314c99c2d42c42c068',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -111,55 +111,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6021600,
+        6096120,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/traceconv',
     'sha256':
-        'edae3606563616fa304e3240a8426db3dfd7d12ad28bb879e7d482af357af7f2'
+        'f15c86b1d83bd1706b81426a6d11c3274044b4838abfb5d313954c65282162a9'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7305880,
+        7401672,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/traceconv',
     'sha256':
-        '1a0ef7601a6d2f77f9a04d5bb8c092f5a536764a8e9a99325fff6b81c2879d1e'
+        '8e98e30a8e137116e74390bcb9d662f0a3f22f9d38fe3fd7ef305d1051bdf19d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8118692,
+        8238268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/traceconv',
     'sha256':
-        '6f9b53427d367e8582b12763d27e83217c9e744f947f58acc0b404b6ce5103e9'
+        '8ce56de90b18cc01d94b26f521292d71326552f8b4fb29b919f4b2b80c3eb2ec'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8288976,
+        8397056,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/traceconv',
     'sha256':
-        '5c843ddb151f5aa32288814cb190a879f15e6da63d642d3ded0f15954f480b5f'
+        'e5f31569261b3f201e2cc13b8eea510627d555767272373d9196e83ddaefa3f0'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7756800,
+        7867904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/windows-amd64/traceconv.exe',
     'sha256':
-        'b07849cd60872279dda68d4b8ec173af01e0cbb1692eb5473ef230ac89198f2d',
+        '74c24b2a74ae2a490ac5f918ab079a7713af07e9a36e70a2cc8536040d7df099',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/record_android_trace b/tools/record_android_trace
index 70f2906..f91496c 100755
--- a/tools/record_android_trace
+++ b/tools/record_android_trace
@@ -33,18 +33,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1481952,
+        1498560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/tracebox',
     'sha256':
-        '81787717e819e1412609a7c67e561f643516cabd897d6b91e1f5cf572a40521a',
+        'b760c7ed682d23f8d268174a939f1b8cb130ffb0d52f42b4cc4499a25423e782',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -54,11 +54,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1359320,
+        1376008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/tracebox',
     'sha256':
-        '5cd27e847e1e88493d136a2a803fd644cffc673d12685442a1df00c928170429',
+        '2835127a5fc42e501e29a685a1cbcd98c26f977ee845bb1bd60a43008fd48161',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -68,11 +68,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2200152,
+        2212088,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/tracebox',
     'sha256':
-        'de4f883b1a3509ad9533c3057787a00cf8e68cbb1b5b6bde385caee3b2433574',
+        '0b4a61a3e45f4e1b6111ca0b440f9e4a0b0726df912542373f69b6821a3113bc',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -82,11 +82,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1317900,
+        1327204,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/tracebox',
     'sha256':
-        'c404c93a16b3808b9b668f2ef071faecbde1a8c012be670a595716d2a730e5e9',
+        '43c20d80e5b40cebe5b26579319be6d57796b04155ffc90f133b6a13c2de25ab',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -96,11 +96,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2128352,
+        2139928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/tracebox',
     'sha256':
-        '851c82a00e84b492627503ca2c46afcbd3ec1770c06b002134602541e6006b0c',
+        '261cd7912dd69e8d82ce61b66a78f098eba46f6f492f6ec6bf73124d40a9a202',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -110,44 +110,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1198036,
+        1202132,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/tracebox',
     'sha256':
-        'fac9f7cbe888139c569068d973be79f4d820a8f323ad04cdb9c5c3be6faba067'
+        '43874c4d187e3da5820ae3988d16608a8bf89d04282b8ab3e1486c6e57cd891f'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        1813160,
+        1825448,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/tracebox',
     'sha256':
-        '5c344c218c440c678fa56c8ce4d2f9c26efc300ebf268afd6e68a5775628cc0c'
+        '9264d8f23ea3988f2952696668d3d572669134e4ca5b3e25a3209d3bfdb7d015'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        1812396,
+        1820588,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/tracebox',
     'sha256':
-        'cd44b4ab3a1579dd0082f6f0fa4d7f710b9d056b44606cb247d41524be6d576e'
+        'd591e903b1e2b0e4b270f0510c24992aac68167c80e61450ebf6c36cc780cd21'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2091688,
+        2108072,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/tracebox',
     'sha256':
-        '6d614a3d4e77c7fb0712bf13d7a2c86a59b0945a043fefb70ffc70854551fca0'
+        'd3e4278e17764b605236c568202eb8d94f3f7e7593b0be9eba09f670a987acba'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/trace_processor b/tools/trace_processor
index 50e7fe0..f88419c 100755
--- a/tools/trace_processor
+++ b/tools/trace_processor
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/trace_processor_shell.py
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACE_PROCESSOR_SHELL_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9551608,
+        9682976,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/trace_processor_shell',
     'sha256':
-        'f3def324604f551fdf7df088363aebf74de573fdaa02796fd994f1863ad58463',
+        '74b097836f16d788edce11bfda46f52e6499d8ec546d10f8dbab182612407b3b',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8064168,
+        8180008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/trace_processor_shell',
     'sha256':
-        'f34a2cfb4f93579452bbaf4f47c9b0fe475394d55316c9626949760c206ec801',
+        '9a96a2f9ef81f210fcba4a08b21db6f2f57fb1d325e91924043ae066327c29a8',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9415488,
+        9533320,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/trace_processor_shell',
     'sha256':
-        '86d133fc2fe0204693d2b22e9287363cb44bf76794b0091d4d0bb3388b7cd8d5',
+        'ee0ccae766aad09f0135efa83cc3a1cd78bd0e86824a7b1245e013d32e61d820',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6880412,
+        6963584,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/trace_processor_shell',
     'sha256':
-        '21fd4b2ad68b1f9829cee72d3905b1712cbfa5e169e89b89cdd0a3a4b1682e7d',
+        '2c69d2016dd18b6d42bf6c2a5b4398e29e4fd294950882387942272dd25a045a',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8843272,
+        8950112,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/trace_processor_shell',
     'sha256':
-        '97a6fb150fd6655f03394d8ad166fd2041a1b4ca56a9e7ce5b35902ff64e1a35',
+        'a10a7a9a6614461beb1f32ee16da3290e2770bb6f3a506269defbdbf7e8803af',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'trace_processor_shell',
     'file_size':
-        6497440,
+        6580152,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/trace_processor_shell',
     'sha256':
-        '6018ce36314a75693c5e5c2a69a3aacc9848385bd3ab9121e23de70df5188e1e'
+        '994a038b932accf5796550207a4e7d80c7612fd25e6eee414324e7e4f3ae14f2'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8019768,
+        8115560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/trace_processor_shell',
     'sha256':
-        '4e36255ebcd5e19fe26def6578cc604d636df9c4fa335ec5901e6cd9aba92ac3'
+        '42426b12ad60894aee37e502e4d023cfded53e706d990abf5c70c4556c2b73ff'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        8897636,
+        9009020,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/trace_processor_shell',
     'sha256':
-        'f21573cc77603150e461402c6f032b7ff8f103debcd9d9b2093f521a69f581f7'
+        'af982ad7897d8cafb29a9f1303e32df20486a92eb07930db8b1898a75e84a667'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'trace_processor_shell',
     'file_size':
-        9162616,
+        9270696,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/trace_processor_shell',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/trace_processor_shell',
     'sha256':
-        '2cc5a02270a28db52aa17bfafcf215e3d754a936de595186a7152535ab28531b'
+        '081be392ddccdf37da80dd888277ea9215cfa8d05ddf6f90d9bff13a0a72f672'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'trace_processor_shell.exe',
     'file_size':
-        8790016,
+        8898560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/trace_processor_shell.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/windows-amd64/trace_processor_shell.exe',
     'sha256':
-        'e3193141bcb8bcc35b47c3ee8126d1f3ebe1b54a9ecde59ade9db4f22c6f77d6',
+        '2e66e5b6ab9c0f7ecad98c3b5b822133c9aa34f137b4007c228c78672fdea5a7',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/tools/tracebox b/tools/tracebox
index f5bbf47..4425edd 100755
--- a/tools/tracebox
+++ b/tools/tracebox
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACEBOX_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'tracebox',
     'file_size':
-        1481952,
+        1498560,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/tracebox',
     'sha256':
-        '81787717e819e1412609a7c67e561f643516cabd897d6b91e1f5cf572a40521a',
+        'b760c7ed682d23f8d268174a939f1b8cb130ffb0d52f42b4cc4499a25423e782',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1359320,
+        1376008,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/tracebox',
     'sha256':
-        '5cd27e847e1e88493d136a2a803fd644cffc673d12685442a1df00c928170429',
+        '2835127a5fc42e501e29a685a1cbcd98c26f977ee845bb1bd60a43008fd48161',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2200152,
+        2212088,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/tracebox',
     'sha256':
-        'de4f883b1a3509ad9533c3057787a00cf8e68cbb1b5b6bde385caee3b2433574',
+        '0b4a61a3e45f4e1b6111ca0b440f9e4a0b0726df912542373f69b6821a3113bc',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        1317900,
+        1327204,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/tracebox',
     'sha256':
-        'c404c93a16b3808b9b668f2ef071faecbde1a8c012be670a595716d2a730e5e9',
+        '43c20d80e5b40cebe5b26579319be6d57796b04155ffc90f133b6a13c2de25ab',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'tracebox',
     'file_size':
-        2128352,
+        2139928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/tracebox',
     'sha256':
-        '851c82a00e84b492627503ca2c46afcbd3ec1770c06b002134602541e6006b0c',
+        '261cd7912dd69e8d82ce61b66a78f098eba46f6f492f6ec6bf73124d40a9a202',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,44 +107,44 @@
     'file_name':
         'tracebox',
     'file_size':
-        1198036,
+        1202132,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/tracebox',
     'sha256':
-        'fac9f7cbe888139c569068d973be79f4d820a8f323ad04cdb9c5c3be6faba067'
+        '43874c4d187e3da5820ae3988d16608a8bf89d04282b8ab3e1486c6e57cd891f'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'tracebox',
     'file_size':
-        1813160,
+        1825448,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/tracebox',
     'sha256':
-        '5c344c218c440c678fa56c8ce4d2f9c26efc300ebf268afd6e68a5775628cc0c'
+        '9264d8f23ea3988f2952696668d3d572669134e4ca5b3e25a3209d3bfdb7d015'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'tracebox',
     'file_size':
-        1812396,
+        1820588,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/tracebox',
     'sha256':
-        'cd44b4ab3a1579dd0082f6f0fa4d7f710b9d056b44606cb247d41524be6d576e'
+        'd591e903b1e2b0e4b270f0510c24992aac68167c80e61450ebf6c36cc780cd21'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'tracebox',
     'file_size':
-        2091688,
+        2108072,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/tracebox',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/tracebox',
     'sha256':
-        '6d614a3d4e77c7fb0712bf13d7a2c86a59b0945a043fefb70ffc70854551fca0'
+        'd3e4278e17764b605236c568202eb8d94f3f7e7593b0be9eba09f670a987acba'
 }]
 
 # ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py
diff --git a/tools/traceconv b/tools/traceconv
index ef63547..7ba7d3e 100755
--- a/tools/traceconv
+++ b/tools/traceconv
@@ -30,18 +30,18 @@
 
 
 # ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/traceconv.py
-# This file has been generated by: tools/roll-prebuilts v37.0
+# This file has been generated by: tools/roll-prebuilts v38.0
 TRACECONV_MANIFEST = [{
     'arch':
         'mac-amd64',
     'file_name':
         'traceconv',
     'file_size':
-        8758200,
+        8889568,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-amd64/traceconv',
     'sha256':
-        'b6eb3552efc97b15b1cb7a746b90f77a4ac781566faa4ffa84e1785f5d90b38b',
+        'e45d08f7050553f77c87764823c6ca26e877d0b3865f2764b33dcab6dfd0b9ee',
     'platform':
         'darwin',
     'machine': ['x86_64']
@@ -51,11 +51,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        7332088,
+        7447928,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/mac-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/mac-arm64/traceconv',
     'sha256':
-        '585ee448560fa89f559566cdf41dcc6677a28c74ea3b5bae3cfe31cde813578c',
+        '19cabe8789566e6383632bbe79e360b8bf05465f8bb156788c8c957aba77f224',
     'platform':
         'darwin',
     'machine': ['arm64']
@@ -65,11 +65,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8515568,
+        8633416,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-amd64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-amd64/traceconv',
     'sha256':
-        'bff2c0c525193fc3555725b5767a30bc2b36d7d84b849b7ce41f7abb349f21be',
+        'a7a309c667a3e6a4c0268252540e3fc8052ebbd0abc30faf8f1783df411cf559',
     'platform':
         'linux',
     'machine': ['x86_64']
@@ -79,11 +79,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        6421928,
+        6505268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm/traceconv',
     'sha256':
-        '566298db777a31f3eabd0e6b17e1b0638875725be767333308f02a7fb7d4e36c',
+        '3ff0f9d4ef43e9e76d27c69827e508f4504d2d1fcfb4dac9d739023cd72106d7',
     'platform':
         'linux',
     'machine': ['armv6l', 'armv7l', 'armv8l']
@@ -93,11 +93,11 @@
     'file_name':
         'traceconv',
     'file_size':
-        8001776,
+        8108432,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/linux-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/linux-arm64/traceconv',
     'sha256':
-        'ccf81438df9ed30f88e640f62938b6334698b9158364db43252d7183136e137d',
+        '1f4641b341f2c8bf36faf8d4befedc01bd414c8f778a5b314c99c2d42c42c068',
     'platform':
         'linux',
     'machine': ['aarch64']
@@ -107,55 +107,55 @@
     'file_name':
         'traceconv',
     'file_size':
-        6021600,
+        6096120,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm/traceconv',
     'sha256':
-        'edae3606563616fa304e3240a8426db3dfd7d12ad28bb879e7d482af357af7f2'
+        'f15c86b1d83bd1706b81426a6d11c3274044b4838abfb5d313954c65282162a9'
 }, {
     'arch':
         'android-arm64',
     'file_name':
         'traceconv',
     'file_size':
-        7305880,
+        7401672,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-arm64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-arm64/traceconv',
     'sha256':
-        '1a0ef7601a6d2f77f9a04d5bb8c092f5a536764a8e9a99325fff6b81c2879d1e'
+        '8e98e30a8e137116e74390bcb9d662f0a3f22f9d38fe3fd7ef305d1051bdf19d'
 }, {
     'arch':
         'android-x86',
     'file_name':
         'traceconv',
     'file_size':
-        8118692,
+        8238268,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x86/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x86/traceconv',
     'sha256':
-        '6f9b53427d367e8582b12763d27e83217c9e744f947f58acc0b404b6ce5103e9'
+        '8ce56de90b18cc01d94b26f521292d71326552f8b4fb29b919f4b2b80c3eb2ec'
 }, {
     'arch':
         'android-x64',
     'file_name':
         'traceconv',
     'file_size':
-        8288976,
+        8397056,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/android-x64/traceconv',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/android-x64/traceconv',
     'sha256':
-        '5c843ddb151f5aa32288814cb190a879f15e6da63d642d3ded0f15954f480b5f'
+        'e5f31569261b3f201e2cc13b8eea510627d555767272373d9196e83ddaefa3f0'
 }, {
     'arch':
         'windows-amd64',
     'file_name':
         'traceconv.exe',
     'file_size':
-        7756800,
+        7867904,
     'url':
-        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v37.0/windows-amd64/traceconv.exe',
+        'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v38.0/windows-amd64/traceconv.exe',
     'sha256':
-        'b07849cd60872279dda68d4b8ec173af01e0cbb1692eb5473ef230ac89198f2d',
+        '74c24b2a74ae2a490ac5f918ab079a7713af07e9a36e70a2cc8536040d7df099',
     'platform':
         'win32',
     'machine': ['amd64']
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index acb6a47..0999b31 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -16,6 +16,7 @@
 
 import {assertExists, assertTrue, assertUnreachable} from '../base/logging';
 import {duration, time} from '../base/time';
+import {exists} from '../base/utils';
 import {RecordConfig} from '../controller/record_config_types';
 import {
   GenericSliceDetailsTabConfig,
@@ -29,7 +30,7 @@
   tableColumnEquals,
   toggleEnabled,
 } from '../frontend/pivot_table_types';
-import {TrackTags} from '../public/index';
+import {PrimaryTrackSortKey, TrackTags} from '../public/index';
 import {DebugTrackV2Config} from '../tracks/debug/slice_track';
 
 import {randomColor} from './colorizer';
@@ -47,6 +48,7 @@
   traceEventEnd,
   TraceEventScope,
 } from './metatracing';
+import {pluginManager} from './plugins';
 import {
   AdbRecordingTarget,
   Area,
@@ -61,7 +63,6 @@
   Pagination,
   PendingDeeplinkState,
   PivotTableResult,
-  PrimaryTrackSortKey,
   ProfileType,
   RecordingTarget,
   SCROLLING_TRACK_GROUP,
@@ -225,16 +226,25 @@
       state.uiTrackIdByTraceTrackId[trackId] = uiTrackId;
     };
 
-    const config = trackState.config as {trackId: number};
-    if (config.trackId !== undefined) {
-      setUiTrackId(config.trackId, uiTrackId);
-      return;
-    }
-
-    const multiple = trackState.config as {trackIds: number[]};
-    if (multiple.trackIds !== undefined) {
-      for (const trackId of multiple.trackIds) {
+    const {uri, config} = trackState;
+    if (exists(uri)) {
+      // If track is a new "plugin" type track (i.e. it has a uri), resolve the
+      // track ids from through the pluginManager.
+      const trackInfo = pluginManager.resolveTrackInfo(uri);
+      if (trackInfo?.trackIds) {
+        for (const trackId of trackInfo.trackIds) {
+          setUiTrackId(trackId, uiTrackId);
+        }
+      }
+    } else {
+      // Traditional track - resolve track ids through the config.
+      const {trackId, trackIds} = config;
+      if (exists(trackId)) {
         setUiTrackId(trackId, uiTrackId);
+      } else if (exists(trackIds)) {
+        for (const trackId of trackIds) {
+          setUiTrackId(trackId, uiTrackId);
+        }
       }
     }
   },
@@ -413,11 +423,6 @@
     state.visibleTracks = args.tracks;
   },
 
-  updateTrackConfig(state: StateDraft, args: {id: string, config: {}}) {
-    if (state.tracks[args.id] === undefined) return;
-    state.tracks[args.id].config = args.config;
-  },
-
   moveTrack(
       state: StateDraft,
       args: {srcId: string; op: 'before' | 'after', dstId: string}): void {
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 261dd2e..8713c17 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -16,18 +16,18 @@
 
 import {assertExists} from '../base/logging';
 import {Time} from '../base/time';
+import {PrimaryTrackSortKey} from '../public';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
 import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile';
 import {
   PROCESS_SCHEDULING_TRACK_KIND,
-} from '../tracks/process_scheduling';
+} from '../tracks/process_summary/process_scheduling_track';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
 
 import {StateActions} from './actions';
 import {createEmptyState} from './empty_state';
 import {
   InThreadTrackSortKey,
-  PrimaryTrackSortKey,
   ProfileType,
   SCROLLING_TRACK_GROUP,
   State,
diff --git a/ui/src/common/basic_async_track.ts b/ui/src/common/basic_async_track.ts
index 24e3f28..2faf829 100644
--- a/ui/src/common/basic_async_track.ts
+++ b/ui/src/common/basic_async_track.ts
@@ -20,7 +20,7 @@
 import {PxSpan, TimeScale} from '../frontend/time_scale';
 import {SliceRect} from '../frontend/track';
 import {TrackButtonAttrs} from '../frontend/track_panel';
-import {TrackLike} from '../public';
+import {Track} from '../public';
 
 import {TrackData} from './track_data';
 
@@ -40,7 +40,7 @@
 // This provides the logic to perform data reloads at appropriate times as the
 // window is panned and zoomed about.
 // The extending class need only define renderCanvas() and onBoundsChange().
-export abstract class BasicAsyncTrack<Data> implements TrackLike {
+export abstract class BasicAsyncTrack<Data> implements Track {
   private requestingData = false;
   private queuedRequest = false;
   private currentState?: TrackData;
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 43c5f16..c442e73 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -499,6 +499,13 @@
     return this.engine.getCpus();
   }
 
+  async getNumberOfGpus(): Promise<number> {
+    if (!this.isAlive) {
+      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
+    }
+    return this.engine.getNumberOfGpus();
+  }
+
   get engineId(): string {
     return this.engine.id;
   }
diff --git a/ui/src/common/metatracing.ts b/ui/src/common/metatracing.ts
index 09d080f..6d070a1 100644
--- a/ui/src/common/metatracing.ts
+++ b/ui/src/common/metatracing.ts
@@ -49,7 +49,7 @@
 function getInitialCategories(): MetatraceCategories|undefined {
   if (!AOMT_FLAG.get()) return undefined;
   if (AOMT_DETAILED_FLAG.get()) return MetatraceCategories.ALL;
-  return MetatraceCategories.TOPLEVEL;
+  return MetatraceCategories.QUERY_TIMELINE | MetatraceCategories.API_TIMELINE;
 }
 
 let enabledCategories: MetatraceCategories|undefined = getInitialCategories();
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 9961921..13a4e10 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -30,14 +30,14 @@
   Plugin,
   PluginClass,
   PluginContext,
-  PluginInfo,
-  PluginTrackInfo,
+  PluginContextTrace,
+  PluginDescriptor,
   StatefulPlugin,
   Store,
-  TracePluginContext,
+  Track,
   TrackContext,
-  TrackInfo,
-  TrackLike,
+  TrackDescriptor,
+  TrackInstanceDescriptor,
   Viewer,
 } from '../public';
 
@@ -73,13 +73,13 @@
     });
   }
 
-  registerTrackController(track: TrackControllerFactory): void {
+  LEGACY_registerTrackController(track: TrackControllerFactory): void {
     if (!this.alive) return;
     const unregister = trackControllerRegistry.register(track);
     this.trash.add(unregister);
   }
 
-  registerTrack(track: TrackCreator): void {
+  LEGACY_registerTrack(track: TrackCreator): void {
     if (!this.alive) return;
     const unregister = trackRegistry.register(track);
     this.trash.add(unregister);
@@ -95,29 +95,30 @@
 // related resources, such as the engine and the store.
 // The TracePluginContext exists for the whole duration a plugin is active AND a
 // trace is loaded.
-class TracePluginContextImpl<T> implements TracePluginContext<T>, Disposable {
+class TracePluginContextImpl<T> implements PluginContextTrace<T>, Disposable {
   private trash = new Trash();
   private alive = true;
 
   constructor(
       private ctx: PluginContext, readonly store: Store<T>,
       readonly engine: EngineProxy,
-      private trackRegistry: Map<string, PluginTrackInfo>,
+      readonly trackRegistry: Map<string, TrackDescriptor>,
+      private suggestedTracks: Set<TrackInstanceDescriptor>,
       private commandRegistry: Map<string, Command>) {
     this.trash.add(engine);
     this.trash.add(store);
   }
 
-  registerTrackController(track: TrackControllerFactory): void {
+  LEGACY_registerTrackController(track: TrackControllerFactory): void {
     // Silently ignore if context is dead.
     if (!this.alive) return;
-    this.ctx.registerTrackController(track);
+    this.ctx.LEGACY_registerTrackController(track);
   }
 
-  registerTrack(track: TrackCreator): void {
+  LEGACY_registerTrack(track: TrackCreator): void {
     // Silently ignore if context is dead.
     if (!this.alive) return;
-    this.ctx.registerTrack(track);
+    this.ctx.LEGACY_registerTrack(track);
   }
 
   addCommand(cmd: Command): void {
@@ -142,16 +143,22 @@
   // Register a new track in this context.
   // All tracks registered through this method are removed when this context is
   // destroyed, i.e. when the trace is unloaded.
-  addTrack(trackDetails: PluginTrackInfo): void {
+  addTrack(trackDetails: TrackDescriptor): void {
     // Silently ignore if context is dead.
     if (!this.alive) return;
     const {uri} = trackDetails;
     this.trackRegistry.set(uri, trackDetails);
-    this.trash.add({
-      dispose: () => {
-        this.trackRegistry.delete(uri);
-      },
-    });
+    this.trash.addCallback(() => this.trackRegistry.delete(uri));
+  }
+
+  // Ask Perfetto to add a track to the track list when a fresh trace is loaded.
+  // Ignored when a trace is loaded from a permalink.
+  // This is a direct replacement for findPotentialTracks().
+  // Note: This interface is likely to be deprecated soon, but is required while
+  // both plugin and original type tracks coexist.
+  suggestTrack(trackInfo: TrackInstanceDescriptor): void {
+    this.suggestedTracks.add(trackInfo);
+    this.trash.addCallback(() => this.suggestedTracks.delete(trackInfo));
   }
 
   dispose(): void {
@@ -161,7 +168,7 @@
 }
 
 // 'Static' registry of all known plugins.
-export class PluginRegistry extends Registry<PluginInfo<unknown>> {
+export class PluginRegistry extends Registry<PluginDescriptor<unknown>> {
   constructor() {
     super((info) => info.pluginId);
   }
@@ -170,26 +177,24 @@
 interface PluginDetails<T> {
   plugin: Plugin<T>;
   context: PluginContext&Disposable;
-  traceContext?: TracePluginContext<T>&Disposable;
+  traceContext?: TracePluginContextImpl<unknown>;
 }
 
 function isPluginClass<T>(v: unknown): v is PluginClass<T> {
   return typeof v === 'function' && !!(v.prototype.onActivate);
 }
 
-function makePlugin<T>(info: PluginInfo<T>): Plugin<T> {
-  const {plugin: pluginFactory} = info;
+function makePlugin<T>(info: PluginDescriptor<T>): Plugin<T> {
+  const {plugin} = info;
 
-  if (typeof pluginFactory === 'function') {
-    if (isPluginClass(pluginFactory)) {
-      const PluginClass = pluginFactory;
+  if (typeof plugin === 'function') {
+    if (isPluginClass(plugin)) {
+      const PluginClass = plugin;
       return new PluginClass();
     } else {
-      return pluginFactory();
+      return plugin();
     }
   } else {
-    // pluginFactory is the plugin!
-    const plugin = pluginFactory;
     return plugin;
   }
 }
@@ -198,8 +203,9 @@
   private registry: PluginRegistry;
   private plugins: Map<string, PluginDetails<unknown>>;
   private engine?: Engine;
-  readonly trackRegistry = new Map<string, PluginTrackInfo>();
+  readonly trackRegistry = new Map<string, TrackDescriptor>();
   readonly commandRegistry = new Map<string, Command>();
+  readonly suggestedTracks = new Set<TrackInstanceDescriptor>();
 
   constructor(registry: PluginRegistry) {
     this.registry = registry;
@@ -257,15 +263,8 @@
     return this.plugins.get(pluginId);
   }
 
-  findPotentialTracks(): Promise<TrackInfo[]>[] {
-    const promises: Promise<TrackInfo[]>[] = [];
-    for (const {plugin, traceContext} of this.plugins.values()) {
-      if (plugin.findPotentialTracks && traceContext) {
-        const promise = plugin.findPotentialTracks(traceContext);
-        promises.push(promise);
-      }
-    }
-    return promises;
+  findPotentialTracks(): TrackInstanceDescriptor[] {
+    return Array.from(this.suggestedTracks);
   }
 
   onTraceLoad(engine: Engine): void {
@@ -299,16 +298,15 @@
 
   // Look up track into for a given track's URI.
   // Returns |undefined| if no track can be found.
-  resolveTrackInfo(uri: string): PluginTrackInfo|undefined {
+  resolveTrackInfo(uri: string): TrackDescriptor|undefined {
     return this.trackRegistry.get(uri);
   }
 
   // Create a new plugin track object from its URI.
   // Returns undefined if no such track is registered.
-  createTrack(uri: string, trackInstanceId: string): TrackLike|undefined {
+  createTrack(uri: string, trackCtx: TrackContext): Track|undefined {
     const trackInfo = pluginManager.trackRegistry.get(uri);
-    const trackContext: TrackContext = {trackInstanceId};
-    return trackInfo && trackInfo.trackFactory(trackContext);
+    return trackInfo && trackInfo.track(trackCtx);
   }
 
   private doPluginTraceLoad<T>(
@@ -331,6 +329,7 @@
           proxyStore,
           engineProxy,
           this.trackRegistry,
+          this.suggestedTracks,
           this.commandRegistry);
       pluginDetails.traceContext = traceCtx;
 
@@ -347,6 +346,7 @@
           proxyStore,
           engineProxy,
           this.trackRegistry,
+          this.suggestedTracks,
           this.commandRegistry);
       pluginDetails.traceContext = traceCtx;
 
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 0d31e01..1a8c68e 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -23,7 +23,7 @@
   PivotTree,
   TableColumn,
 } from '../frontend/pivot_table_types';
-import {TrackTags} from '../public/index';
+import {PrimaryTrackSortKey, TrackTags} from '../public/index';
 
 import {Direction} from './event_set';
 
@@ -122,7 +122,8 @@
 // 39. Ported cpu_slice, ftrace, and android_log tracks to plugin tracks. Track
 //     state entries now require a URI and old track implementations are no
 //     longer registered.
-export const STATE_VERSION = 39;
+// 40. Ported counter, process summary/sched, & cpu_freq to plugin tracks.
+export const STATE_VERSION = 40;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -130,36 +131,6 @@
 
 export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM';
 
-// Tracks within track groups (usually corresponding to processes) are sorted.
-// As we want to group all tracks related to a given thread together, we use
-// two keys:
-// - Primary key corresponds to a priority of a track block (all tracks related
-//   to a given thread or a single track if it's not thread-associated).
-// - Secondary key corresponds to a priority of a given thread-associated track
-//   within its thread track block.
-// Each track will have a sort key, which either a primary sort key
-// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
-// primary sort key is done independently).
-export enum PrimaryTrackSortKey {
-  DEBUG_SLICE_TRACK,
-  NULL_TRACK,
-  PROCESS_SCHEDULING_TRACK,
-  PROCESS_SUMMARY_TRACK,
-  EXPECTED_FRAMES_SLICE_TRACK,
-  ACTUAL_FRAMES_SLICE_TRACK,
-  PERF_SAMPLES_PROFILE_TRACK,
-  HEAP_PROFILE_TRACK,
-  MAIN_THREAD,
-  RENDER_THREAD,
-  GPU_COMPLETION_THREAD,
-  CHROME_IO_THREAD,
-  CHROME_COMPOSITOR_THREAD,
-  ORDINARY_THREAD,
-  COUNTER_TRACK,
-  ASYNC_SLICE_TRACK,
-  ORDINARY_TRACK,
-}
-
 // Key that is used to sort tracks within a block of tracks associated with a
 // given thread.
 export enum InThreadTrackSortKey {
@@ -260,6 +231,7 @@
     trackIds?: number[];
   };
   uri?: string;
+  state?: unknown;
 }
 
 export interface TrackGroupState {
@@ -268,6 +240,7 @@
   name: string;
   collapsed: boolean;
   tracks: string[];  // Child track ids.
+  state?: unknown;
 }
 
 export interface EngineConfig {
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 3a34afa..34646ba 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {PrimaryTrackSortKey} from '../public';
+
 import {createEmptyState} from './empty_state';
-import {getContainingTrackId, PrimaryTrackSortKey, State} from './state';
+import {getContainingTrackId, State} from './state';
 import {deserializeStateObject, serializeStateObject} from './upload_utils';
 
 test('createEmptyState', () => {
diff --git a/ui/src/common/track_adapter.ts b/ui/src/common/track_adapter.ts
index 9bb57a4..1c145d3 100644
--- a/ui/src/common/track_adapter.ts
+++ b/ui/src/common/track_adapter.ts
@@ -41,6 +41,7 @@
     BasicAsyncTrack<Data> {
   private track: TrackAdapter<Config, Data>;
   private controller: TrackControllerAdapter<Config, Data>;
+  private isSetup = false;
 
   constructor(
       engine: EngineProxy, trackInstanceId: string, config: Config,
@@ -57,11 +58,6 @@
     this.controller = new Controller(config, engine);
   }
 
-  onCreate(): void {
-    this.controller.onSetup();
-    super.onCreate();
-  }
-
   onDestroy(): void {
     this.track.onDestroy();
     this.controller.onDestroy();
@@ -104,8 +100,13 @@
     this.track.onFullRedraw();
   }
 
-  onBoundsChange(start: time, end: time, resolution: duration): Promise<Data> {
-    return this.controller.onBoundsChange(start, end, resolution);
+  async onBoundsChange(start: time, end: time, resolution: duration):
+      Promise<Data> {
+    if (!this.isSetup) {
+      await this.controller.onSetup();
+      this.isSetup = true;
+    }
+    return await this.controller.onBoundsChange(start, end, resolution);
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D): void {
@@ -181,6 +182,12 @@
   new (args: NewTrackArgs): TrackAdapter<Config, Data>
 }
 
+function hasNamespace(config: unknown): config is {
+  namespace: string
+} {
+  return !!config && typeof config === 'object' && 'namespace' in config;
+}
+
 // Extend from this class instead of `TrackController` to use existing track
 // controller implementations with `TrackWithControllerAdapter`.
 export abstract class TrackControllerAdapter<Config, Data> {
@@ -189,7 +196,7 @@
   // don't have access to it.
   private uuid = uuidv4();
 
-  constructor(protected config: Config, private engine: EngineProxy) {}
+  constructor(protected config: Config, protected engine: EngineProxy) {}
 
   protected async query(query: string) {
     const result = await this.engine.query(query);
@@ -199,8 +206,8 @@
   abstract onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data>;
 
-  onSetup(): void {}
-  onDestroy(): void {}
+  async onSetup(): Promise<void> {}
+  async onDestroy(): Promise<void> {}
 
   // Returns a valid SQL table name with the given prefix that should be unique
   // for each track.
@@ -210,6 +217,14 @@
     const idSuffix = this.uuid.split('-').join('_');
     return `${prefix}_${idSuffix}`;
   }
+
+  namespaceTable(tableName: string): string {
+    if (hasNamespace(this.config)) {
+      return this.config.namespace + '_' + tableName;
+    } else {
+      return tableName;
+    }
+  }
 }
 
 type TrackControllerAdapterClass<Config, Data> = {
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
index 3c3cf15..ae3e1d7 100644
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts
@@ -15,9 +15,10 @@
 import {Duration} from '../../base/time';
 import {ColumnDef} from '../../common/aggregation_data';
 import {Engine} from '../../common/engine';
+import {pluginManager} from '../../common/plugins';
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
-import {Config, COUNTER_TRACK_KIND} from '../../tracks/counter';
+import {COUNTER_TRACK_KIND} from '../../tracks/counter';
 
 import {AggregationController} from './aggregation_controller';
 
@@ -25,19 +26,17 @@
   async createAggregateView(engine: Engine, area: Area) {
     await engine.query(`drop view if exists ${this.kind};`);
 
-    const ids = [];
+    const trackIds: (string|number)[] = [];
     for (const trackId of area.tracks) {
       const track = globals.state.tracks[trackId];
-      // Track will be undefined for track groups.
-      if (track !== undefined && track.kind === COUNTER_TRACK_KIND) {
-        const config = track.config as Config;
-        // TODO(hjd): Also aggregate annotation (with namespace) counters.
-        if (config.namespace === undefined) {
-          ids.push(config.trackId);
+      if (track?.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        if (trackInfo?.kind === COUNTER_TRACK_KIND) {
+          trackInfo.trackIds && trackIds.push(...trackInfo.trackIds);
         }
       }
     }
-    if (ids.length === 0) return false;
+    if (trackIds.length === 0) return false;
     const duration = area.end - area.start;
     const durationSec = Duration.toSeconds(duration);
 
@@ -61,7 +60,7 @@
         (partition by track_id order by ts
             range between unbounded preceding and unbounded following) as last
         from experimental_counter_dur
-        where track_id in (${ids})
+        where track_id in (${trackIds})
         and ts + dur >= ${area.start} and
         ts <= ${area.end})
     join counter_track
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 46f0f47..db176d9 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -31,9 +31,8 @@
       const track = globals.state.tracks[trackId];
       if (track?.uri) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          const cpu = trackInfo?.tags?.cpu;
-          cpu && selectedCpus.push(cpu);
+        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+          trackInfo.cpu && selectedCpus.push(trackInfo.cpu);
         }
       }
     }
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
index aebaf94..dbdf07c 100644
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
@@ -30,9 +30,8 @@
       const track = globals.state.tracks[trackId];
       if (track?.uri) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          const cpu = trackInfo?.tags?.cpu;
-          cpu && selectedCpus.push(cpu);
+        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+          trackInfo.cpu && selectedCpus.push(trackInfo.cpu);
         }
       }
     }
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index 06f9da9..a1c4170 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -15,6 +15,7 @@
 import {Time} from '../base/time';
 import {Engine} from '../common/engine';
 import {featureFlags} from '../common/feature_flags';
+import {pluginManager} from '../common/plugins';
 import {LONG, NUM, STR_NULL} from '../common/query_result';
 import {Area} from '../common/state';
 import {Flow, globals} from '../frontend/globals';
@@ -243,6 +244,17 @@
         return null;
       }
 
+      // Perform the same check for "plugin" style tracks.
+      if (track.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        const trackIds = trackInfo?.trackIds;
+        if (trackIds === undefined || trackIds.length <= 1) {
+          uiTrackIdToInfo.set(uiTrackId, null);
+          trackIdToInfo.set(trackId, null);
+          return null;
+        }
+      }
+
       const newInfo = {
         uiTrackId,
         siblingTrackIds: trackIds,
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index b8ee0d3..6055221 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -204,8 +204,8 @@
     for (const track of Object.values(globals.state.tracks)) {
       if (exists(track?.uri)) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          const cpu = trackInfo?.tags?.cpu;
+        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+          const cpu = trackInfo?.cpu;
           cpu && cpuToTrackId.set(cpu, track.id);
         }
       }
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 027398a..31000d6 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -470,8 +470,6 @@
       }
     }
 
-    pluginManager.onTraceLoad(engine);
-
     const emptyOmniboxState = {
       omnibox: '',
       mode: globals.state.omniboxState.mode || 'SEARCH',
@@ -501,6 +499,8 @@
     // Make sure the helper views are available before we start adding tracks.
     await this.initialiseHelperViews();
 
+    pluginManager.onTraceLoad(engine);
+
     {
       // When we reload from a permalink don't create extra tracks:
       const {pinnedTracks, tracks} = globals.state;
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 04bac66..b4a8228 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -25,7 +25,6 @@
 import {featureFlags, PERF_SAMPLE_FLAG} from '../common/feature_flags';
 import {pluginManager} from '../common/plugins';
 import {
-  LONG_NULL,
   NUM,
   NUM_NULL,
   STR,
@@ -33,11 +32,12 @@
 } from '../common/query_result';
 import {
   InThreadTrackSortKey,
-  PrimaryTrackSortKey,
   SCROLLING_TRACK_GROUP,
   TrackSortKey,
   UtidToTrackSortKey,
 } from '../common/state';
+import {PrimaryTrackSortKey} from '../public';
+import {getTrackName} from '../public/utils';
 import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames';
 import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices';
 import {
@@ -49,8 +49,7 @@
   decideTracks as scrollJankDecideTracks,
 } from '../tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
-import {COUNTER_TRACK_KIND, CounterScaleOptions} from '../tracks/counter';
-import {CPU_FREQ_TRACK_KIND} from '../tracks/cpu_freq';
+import {COUNTER_TRACK_KIND} from '../tracks/counter';
 import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile';
 import {
   EXPECTED_FRAMES_SLICE_TRACK_KIND,
@@ -61,10 +60,6 @@
   PERF_SAMPLES_PROFILE_TRACK_KIND,
 } from '../tracks/perf_samples_profile';
 import {
-  PROCESS_SCHEDULING_TRACK_KIND,
-} from '../tracks/process_scheduling';
-import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary';
-import {
   decideTracks as screenshotDecideTracks,
 } from '../tracks/screenshots';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
@@ -130,29 +125,6 @@
 const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
 const MISC_GROUP = 'Misc Global Tracks';
 
-// Sets the default 'scale' for counter tracks. If the regex matches
-// then the paired mode is used. Entries are in priority order so the
-// first match wins.
-const COUNTER_REGEX: [RegExp, CounterScaleOptions][] = [
-  // Power counters make more sense in rate mode since you're typically
-  // interested in the slope of the graph rather than the absolute
-  // value.
-  [new RegExp('^power\..*$'), 'RATE'],
-  // Same for network counters.
-  [NETWORK_TRACK_REGEX, 'RATE'],
-  // Entity residency
-  [ENTITY_RESIDENCY_REGEX, 'RATE'],
-];
-
-function getCounterScale(name: string): CounterScaleOptions|undefined {
-  for (const [re, scale] of COUNTER_REGEX) {
-    if (name.match(re)) {
-      return scale;
-    }
-  }
-  return undefined;
-}
-
 export async function decideTracks(
     engineId: string, engine: Engine): Promise<DeferredAction[]> {
   return (new TrackDecider(engineId, engine)).decideTracks();
@@ -171,66 +143,6 @@
     this.engine = engine;
   }
 
-  static getTrackName(args: Partial<{
-    name: string | null,
-    utid: number,
-    processName: string|null,
-    pid: number|null,
-    threadName: string|null,
-    tid: number|null,
-    upid: number|null,
-    kind: string,
-    threadTrack: boolean
-  }>) {
-    const {
-      name,
-      upid,
-      utid,
-      processName,
-      threadName,
-      pid,
-      tid,
-      kind,
-      threadTrack,
-    } = args;
-
-    const hasName = name !== undefined && name !== null && name !== '[NULL]';
-    const hasUpid = upid !== undefined && upid !== null;
-    const hasUtid = utid !== undefined && utid !== null;
-    const hasProcessName = processName !== undefined && processName !== null;
-    const hasThreadName = threadName !== undefined && threadName !== null;
-    const hasTid = tid !== undefined && tid !== null;
-    const hasPid = pid !== undefined && pid !== null;
-    const hasKind = kind !== undefined;
-    const isThreadTrack = threadTrack !== undefined && threadTrack;
-
-    // If we don't have any useful information (better than
-    // upid/utid) we show the track kind to help with tracking
-    // down where this is coming from.
-    const kindSuffix = hasKind ? ` (${kind})` : '';
-
-    if (isThreadTrack && hasName && hasTid) {
-      return `${name} (${tid})`;
-    } else if (hasName) {
-      return `${name}`;
-    } else if (hasUpid && hasPid && hasProcessName) {
-      return `${processName} ${pid}`;
-    } else if (hasUpid && hasPid) {
-      return `Process ${pid}`;
-    } else if (hasThreadName && hasTid) {
-      return `${threadName} ${tid}`;
-    } else if (hasTid) {
-      return `Thread ${tid}`;
-    } else if (hasUpid) {
-      return `upid: ${upid}${kindSuffix}`;
-    } else if (hasUtid) {
-      return `utid: ${utid}${kindSuffix}`;
-    } else if (hasKind) {
-      return `Unnamed ${kind}`;
-    }
-    return 'Unknown';
-  }
-
   async guessCpuSizes(): Promise<Map<number, string>> {
     const cpuToSize = new Map<number, string>();
     await this.engine.query(`
@@ -289,54 +201,35 @@
   async addCpuFreqTracks(engine: EngineProxy): Promise<void> {
     const cpus = await this.engine.getCpus();
 
-    const maxCpuFreqResult = await engine.query(`
-    select ifnull(max(value), 0) as freq
-    from counter c
-    inner join cpu_counter_track t on c.track_id = t.id
-    where name = 'cpufreq';
-  `);
-    const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
-
     for (const cpu of cpus) {
       // Only add a cpu freq track if we have
       // cpu freq data.
       // TODO(hjd): Find a way to display cpu idle
       // events even if there are no cpu freq events.
       const cpuFreqIdleResult = await engine.query(`
-      select
-        id as cpuFreqId,
-        (
-          select id
-          from cpu_counter_track
-          where name = 'cpuidle'
-          and cpu = ${cpu}
-          limit 1
-        ) as cpuIdleId
-      from cpu_counter_track
-      where name = 'cpufreq' and cpu = ${cpu}
-      limit 1;
-    `);
+        select
+          id as cpuFreqId,
+          (
+            select id
+            from cpu_counter_track
+            where name = 'cpuidle'
+            and cpu = ${cpu}
+            limit 1
+          ) as cpuIdleId
+        from cpu_counter_track
+        where name = 'cpufreq' and cpu = ${cpu}
+        limit 1;
+      `);
 
       if (cpuFreqIdleResult.numRows() > 0) {
-        const row = cpuFreqIdleResult.firstRow({
-          cpuFreqId: NUM,
-          cpuIdleId: NUM_NULL,
-        });
-        const freqTrackId = row.cpuFreqId;
-        const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
-
         this.tracksToAdd.push({
           engineId: this.engineId,
-          kind: CPU_FREQ_TRACK_KIND,
+          kind: PLUGIN_TRACK_KIND,
           trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
           name: `Cpu ${cpu} Frequency`,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {
-            cpu,
-            maximumValue: maxCpuFreq,
-            freqTrackId,
-            idleTrackId,
-          },
+          config: {},
+          uri: `perfetto.CpuFreq#${cpu}`,
         });
       }
     }
@@ -394,7 +287,7 @@
       const kind = ASYNC_SLICE_TRACK_KIND;
       const rawName = it.name === null ? undefined : it.name;
       const rawParentName = it.parentName === null ? undefined : it.parentName;
-      const name = TrackDecider.getTrackName({name: rawName, kind});
+      const name = getTrackName({name: rawName, kind});
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const parentTrackId = it.parentId;
@@ -412,8 +305,7 @@
           trackGroup = uuidv4();
           parentIdToGroupId.set(parentTrackId, trackGroup);
 
-          const parentName =
-              TrackDecider.getTrackName({name: rawParentName, kind});
+          const parentName = getTrackName({name: rawParentName, kind});
 
           const summaryTrackId = uuidv4();
           this.tracksToAdd.push({
@@ -463,36 +355,24 @@
 
   async addGpuFreqTracks(engine: EngineProxy): Promise<void> {
     const numGpus = await this.engine.getNumberOfGpus();
-    const maxGpuFreqResult = await engine.query(`
-    select ifnull(max(value), 0) as maximumValue
-    from counter c
-    inner join gpu_counter_track t on c.track_id = t.id
-    where name = 'gpufreq';
-  `);
-    const maximumValue =
-        maxGpuFreqResult.firstRow({maximumValue: NUM}).maximumValue;
-
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
       // gpu freq data.
       const freqExistsResult = await engine.query(`
-      select id
+      select *
       from gpu_counter_track
       where name = 'gpufreq' and gpu_id = ${gpu}
       limit 1;
     `);
       if (freqExistsResult.numRows() > 0) {
-        const trackId = freqExistsResult.firstRow({id: NUM}).id;
         this.tracksToAdd.push({
           engineId: this.engineId,
-          kind: COUNTER_TRACK_KIND,
+          kind: PLUGIN_TRACK_KIND,
           name: `Gpu ${gpu} Frequency`,
           trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {
-            trackId,
-            maximumValue,
-          },
+          config: {},
+          uri: `perfetto.Counter#gpu_freq${gpu}`,
         });
       }
     }
@@ -537,15 +417,12 @@
       const trackId = it.id;
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind: COUNTER_TRACK_KIND,
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
         trackGroup: SCROLLING_TRACK_GROUP,
-        config: {
-          name,
-          trackId,
-          scale: getCounterScale(name),
-        },
+        config: {},
+        uri: `perfetto.Counter#cpu${trackId}`,
       });
     }
   }
@@ -866,17 +743,6 @@
     }
   }
 
-  applyDefaultCounterScale(): void {
-    for (const track of this.tracksToAdd) {
-      if (track.kind === COUNTER_TRACK_KIND) {
-        const scaleConfig = {
-          scale: getCounterScale(track.name),
-        };
-        track.config = Object.assign({}, track.config, scaleConfig);
-      }
-    }
-  }
-
   async addLogsTrack(engine: EngineProxy): Promise<void> {
     const result =
         await engine.query(`select count(1) as cnt from android_logs`);
@@ -1017,44 +883,30 @@
     }
 
     const counterResult = await engine.query(`
-    SELECT
-      id,
-      name,
-      upid,
-      min_value as minValue,
-      max_value as maxValue
-    FROM annotation_counter_track`);
+      SELECT id, name, upid FROM annotation_counter_track
+    `);
 
     const counterIt = counterResult.iter({
       id: NUM,
       name: STR,
       upid: NUM,
-      minValue: NUM_NULL,
-      maxValue: NUM_NULL,
     });
 
     for (; counterIt.valid(); counterIt.next()) {
       const id = counterIt.id;
       const name = counterIt.name;
       const upid = counterIt.upid;
-      const minimumValue =
-          counterIt.minValue === null ? undefined : counterIt.minValue;
-      const maximumValue =
-          counterIt.maxValue === null ? undefined : counterIt.maxValue;
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind: 'CounterTrack',
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
         trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
                                  this.upidToUuid.get(upid),
         config: {
-          name,
           namespace: 'annotation',
-          trackId: id,
-          minimumValue,
-          maximumValue,
         },
+        uri: `perfetto.Annotation#counter${id}`,
       });
     }
   }
@@ -1101,7 +953,7 @@
         this.tracksToAdd.push({
           engineId: this.engineId,
           kind: THREAD_STATE_TRACK_KIND,
-          name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
+          name: getTrackName({utid, tid, threadName, kind}),
           trackGroup: uuid,
           trackSortKey: {
             utid,
@@ -1116,7 +968,7 @@
         this.tracksToAdd.push({
           engineId: this.engineId,
           kind,
-          name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
+          name: getTrackName({utid, tid, threadName, kind}),
           trackGroup: uuid,
           trackSortKey: {
             utid,
@@ -1177,9 +1029,7 @@
       upid,
       tid,
       thread.name as threadName,
-      thread_counter_track.id as trackId,
-      thread.start_ts as startTs,
-      thread.end_ts as endTs
+      thread_counter_track.id as trackId
     from thread_counter_track
     join thread using(utid)
     left join process using(upid)
@@ -1192,9 +1042,7 @@
       upid: NUM_NULL,
       tid: NUM_NULL,
       threadName: STR_NULL,
-      startTs: LONG_NULL,
       trackId: NUM,
-      endTs: LONG_NULL,
     });
     for (; it.valid(); it.next()) {
       const utid = it.utid;
@@ -1204,27 +1052,25 @@
       const trackName = it.trackName;
       const threadName = it.threadName;
       const uuid = this.getUuid(utid, upid);
-      const startTs = it.startTs === null ? undefined : it.startTs;
-      const endTs = it.endTs === null ? undefined : it.endTs;
-      const kind = COUNTER_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, utid, tid, kind, threadName, threadTrack: true});
+      const name = getTrackName({
+        name: trackName,
+        utid,
+        tid,
+        kind: COUNTER_TRACK_KIND,
+        threadName,
+        threadTrack: true,
+      });
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind,
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: {
           utid,
           priority: InThreadTrackSortKey.ORDINARY,
         },
         trackGroup: uuid,
-        config: {
-          name,
-          trackId,
-          startTs,
-          endTs,
-          tid,
-        },
+        config: {},
+        uri: `perfetto.Counter#thread${trackId}`,
       });
     }
   }
@@ -1279,8 +1125,8 @@
       const uuid = this.getUuid(0, upid);
 
       const kind = ASYNC_SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, processName, kind});
+      const name =
+          getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind,
@@ -1343,8 +1189,8 @@
       const uuid = this.getUuid(0, upid);
 
       const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, processName, kind});
+      const name =
+          getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind,
@@ -1408,8 +1254,8 @@
       const uuid = this.getUuid(0, upid);
 
       const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, processName, kind});
+      const name =
+          getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind,
@@ -1467,8 +1313,7 @@
       const uuid = this.getUuid(utid, upid);
 
       const kind = SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, utid, tid, threadName, kind});
+      const name = getTrackName({name: trackName, utid, tid, threadName, kind});
       if (showV1()) {
         this.tracksToAdd.push({
           engineId: this.engineId,
@@ -1514,9 +1359,7 @@
       process_counter_track.name as trackName,
       upid,
       process.pid,
-      process.name as processName,
-      process.start_ts as startTs,
-      process.end_ts as endTs
+      process.name as processName
     from process_counter_track
     join process using(upid);
   `);
@@ -1526,8 +1369,6 @@
       upid: NUM,
       pid: NUM_NULL,
       processName: STR_NULL,
-      startTs: LONG_NULL,
-      endTs: LONG_NULL,
     });
     for (let i = 0; it.valid(); ++i, it.next()) {
       const pid = it.pid;
@@ -1536,24 +1377,17 @@
       const trackName = it.trackName;
       const processName = it.processName;
       const uuid = this.getUuid(0, upid);
-      const startTs = it.startTs === null ? undefined : it.startTs;
-      const endTs = it.endTs === null ? undefined : it.endTs;
-      const kind = COUNTER_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, kind, processName});
+      const name = getTrackName(
+          {name: trackName, upid, pid, kind: COUNTER_TRACK_KIND, processName});
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind,
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack(
             upid, trackName || undefined),
         trackGroup: uuid,
-        config: {
-          name,
-          trackId,
-          startTs,
-          endTs,
-        },
+        config: {},
+        uri: `perfetto.Counter#process${trackId}`,
       });
     }
   }
@@ -1671,10 +1505,11 @@
     this.tracksToAdd.push({
       id: summaryTrackId,
       engineId: this.engineId,
-      kind: PROCESS_SUMMARY_TRACK,
+      kind: PLUGIN_TRACK_KIND,
       trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
       name: `Kernel thread summary`,
-      config: {pidForColor: 2, upid: it.upid, utid: it.utid},
+      config: {},
+      uri: 'perfetto.ProcessSummary#kernel',
     });
     const addTrackGroup = Actions.addTrackGroup({
       engineId: this.engineId,
@@ -1822,7 +1657,6 @@
       processName: STR_NULL,
       hasSched: NUM_NULL,
       hasHeapProfiles: NUM_NULL,
-      isDebuggable: NUM_NULL,
       chromeProcessLabels: STR,
     });
     for (; it.valid(); it.next()) {
@@ -1834,7 +1668,6 @@
       const processName = it.processName;
       const hasSched = !!it.hasSched;
       const hasHeapProfiles = !!it.hasHeapProfiles;
-      const isDebuggable = !!it.isDebuggable;
 
       // Group by upid if present else by utid.
       let pUuid =
@@ -1843,31 +1676,24 @@
       if (pUuid === undefined) {
         pUuid = this.getOrCreateUuid(utid, upid);
         const summaryTrackId = uuidv4();
-
-        const pidForColor = pid || tid || upid || utid || 0;
-        const kind =
-            hasSched ? PROCESS_SCHEDULING_TRACK_KIND : PROCESS_SUMMARY_TRACK;
+        const type = hasSched ? 'schedule' : 'summary';
+        const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
 
         this.tracksToAdd.push({
           id: summaryTrackId,
           engineId: this.engineId,
-          kind,
+          kind: PLUGIN_TRACK_KIND,
           trackSortKey: hasSched ?
               PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK :
               PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
           name: `${upid === null ? tid : pid} summary`,
-          config: {
-            pidForColor,
-            upid,
-            utid,
-            tid,
-            isDebuggable: isDebuggable ?? undefined,
-          },
+          config: {},
           labels: it.chromeProcessLabels.split(','),
+          uri,
         });
 
-        const name = TrackDecider.getTrackName(
-            {utid, processName, pid, threadName, tid, upid});
+        const name =
+            getTrackName({utid, processName, pid, threadName, tid, upid});
         const addTrackGroup = Actions.addTrackGroup({
           engineId: this.engineId,
           summaryTrackId,
@@ -1934,22 +1760,20 @@
     `);
   }
 
-  async addPluginTracks(): Promise<void> {
-    const promises = pluginManager.findPotentialTracks();
-    const groups = await Promise.all(promises);
-    for (const infos of groups) {
-      for (const info of infos) {
-        this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: info.trackKind,
-          name: info.name,
-          // TODO(hjd): Fix how sorting works. Plugins should expose
-          // 'sort keys' which the user can use to choose a sort order.
-          trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-          trackGroup: SCROLLING_TRACK_GROUP,
-          config: info.config,
-        });
-      }
+  addPluginTracks(): void {
+    const tracks = pluginManager.findPotentialTracks();
+    for (const info of tracks) {
+      this.tracksToAdd.push({
+        engineId: this.engineId,
+        kind: PLUGIN_TRACK_KIND,
+        name: info.name,
+        uri: info.uri,
+        // TODO(hjd): Fix how sorting works. Plugins should expose
+        // 'sort keys' which the user can use to choose a sort order.
+        trackSortKey: info.sortKey,
+        trackGroup: SCROLLING_TRACK_GROUP,
+        config: {},
+      });
     }
   }
 
@@ -1978,7 +1802,7 @@
           this.engine.getProxy('TrackDecider::addCpuFreqLimitCounterTracks'));
     await this.addCpuPerfCounterTracks(
         this.engine.getProxy('TrackDecider::addCpuPerfCounterTracks'));
-    await this.addPluginTracks();
+    this.addPluginTracks();
     await this.addAnnotationTracks(
         this.engine.getProxy('TrackDecider::addAnnotationTracks'));
     await this.groupGlobalIonTracks();
@@ -2058,8 +1882,6 @@
     this.addTrackGroupActions.push(
         Actions.setUtidToTrackSortKey({threadOrderingMetadata}));
 
-    this.applyDefaultCounterScale();
-
     return this.addTrackGroupActions;
   }
 
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 23401a0..955df02 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -43,6 +43,7 @@
 import {fullscreenModalContainer} from './modal';
 import {Omnibox, OmniboxOption} from './omnibox';
 import {runQueryInNewTab} from './query_result_tab';
+import {verticalScrollToTrack} from './scroll_helper';
 import {executeSearch} from './search_handler';
 import {Sidebar} from './sidebar';
 import {SqlTableTab} from './sql_table/tab';
@@ -307,8 +308,9 @@
           },
     },
     {
-      id: 'perfetto.PrintTrackInfoToConsole',
-      name: 'Print track info to console',
+      // Selects & reveals the first track on the timeline with a given URI.
+      id: 'perfetto.FindTrack',
+      name: 'Find track by URI',
       callback:
           async () => {
             const tracks = Array.from(pluginManager.trackRegistry.values());
@@ -326,9 +328,28 @@
             });
 
             try {
-              const uri = await this.prompt('Choose a track...', sortedOptions);
-              const trackDetails = pluginManager.resolveTrackInfo(uri);
-              console.log(trackDetails);
+              const selectedUri =
+                  await this.prompt('Choose a track...', sortedOptions);
+
+              // Find the first track with this URI
+              const firstTrack = Object.values(globals.state.tracks)
+                                     .find(({uri}) => uri === selectedUri);
+              if (firstTrack) {
+                console.log(firstTrack);
+                verticalScrollToTrack(firstTrack.id, true);
+                const traceTime = globals.stateTraceTimeTP();
+                globals.makeSelection(
+                    Actions.selectArea({
+                      area: {
+                        start: traceTime.start,
+                        end: traceTime.end,
+                        tracks: [firstTrack.id],
+                      },
+                    }),
+                );
+              } else {
+                alert(`No tracks with uri ${selectedUri} on the timeline`);
+              }
             } catch {
               // Prompt was probably cancelled - do nothing.
             }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index f16057b..e95fb20 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -40,7 +40,7 @@
 import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
 import {constraintsToQuerySuffix} from './sql_utils';
 import {PxSpan, TimeScale} from './time_scale';
-import {NewTrackArgs, SliceRect, Track} from './track';
+import {NewTrackArgs, SliceRect, TrackBase} from './track';
 import {BUCKETS_PER_PIXEL, CacheKey, TrackCache} from './track_cache';
 
 // The common class that underpins all tracks drawing slices.
@@ -179,7 +179,7 @@
 
 export abstract class BaseSliceTrack<T extends BaseSliceTrackTypes =
                                                    BaseSliceTrackTypes> extends
-    Track<T['config']> {
+    TrackBase<T['config']> {
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
 
   // This is the over-skirted cached bounds:
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4d49c1a..c414454 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {TrackState} from 'src/common/state';
+
 import {time} from '../base/time';
+import {pluginManager} from '../common/plugins';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
@@ -67,6 +70,22 @@
   return (obj as {trackGroupId?: string}).trackGroupId !== undefined;
 }
 
+function getTrackIds(track: TrackState): number[] {
+  if (track.uri) {
+    const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+    if (trackInfo?.trackIds) return trackInfo?.trackIds;
+  } else {
+    const config = track.config;
+    if (hasTrackId(config)) {
+      return [config.trackId];
+    }
+    if (hasManyTrackIds(config)) {
+      return config.trackIds;
+    }
+  }
+  return [];
+}
+
 export class FlowEventsRendererArgs {
   trackIdToTrackPanel: Map<number, TrackPanelInfo>;
   groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>;
@@ -78,15 +97,9 @@
 
   registerPanel(panel: PanelVNode, yStart: number, height: number) {
     if (panel.state instanceof TrackPanel && hasId(panel.attrs)) {
-      const config = globals.state.tracks[panel.attrs.id].config;
-      if (hasTrackId(config)) {
-        this.trackIdToTrackPanel.set(
-            config.trackId, {panel: panel.state, yStart});
-      }
-      if (hasManyTrackIds(config)) {
-        for (const trackId of config.trackIds) {
-          this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
-        }
+      const track = globals.state.tracks[panel.attrs.id];
+      for (const trackId of getTrackIds(track)) {
+        this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
       }
     } else if (
         panel.state instanceof TrackGroupPanel &&
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index bb1b2c7..80f690b 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -146,8 +146,8 @@
   for (const track of Object.values(globals.state.tracks)) {
     if (exists(track?.uri)) {
       const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        if (trackInfo?.tags?.cpu === cpu) {
+      if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+        if (trackInfo?.cpu === cpu) {
           trackId = track.id;
           break;
         }
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 31e9c79..6ddbc51 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -19,7 +19,7 @@
 import {EngineProxy} from '../common/engine';
 import {TrackState} from '../common/state';
 import {TrackData} from '../common/track_data';
-import {TrackLike} from '../public';
+import {Track} from '../public';
 
 import {checkerboard} from './checkerboard';
 import {globals} from './globals';
@@ -42,7 +42,7 @@
 
   // We need the |create| method because the stored value in the registry can be
   // an abstract class, and we cannot call 'new' on an abstract class.
-  create(args: NewTrackArgs): Track;
+  create(args: NewTrackArgs): TrackBase;
 }
 
 export interface SliceRect {
@@ -54,8 +54,8 @@
 }
 
 // The abstract class that needs to be implemented by all tracks.
-export abstract class Track<Config = {}, Data extends TrackData = TrackData>
-    implements TrackLike {
+export abstract class TrackBase<Config = {}, Data extends TrackData = TrackData>
+    implements Track {
   // The UI-generated track ID (not to be confused with the SQL track.id).
   protected readonly trackId: string;
   protected readonly engine: EngineProxy;
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index e1f2c86..b378189 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -18,17 +18,19 @@
 import {assertExists} from '../base/logging';
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
+import {pluginManager} from '../common/plugins';
+import {RegistryError} from '../common/registry';
 import {
   getContainingTrackId,
   TrackGroupState,
   TrackState,
 } from '../common/state';
+import {Migrate, Track, TrackContext} from '../public';
 
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
-import {Track} from './track';
-import {TrackChips, TrackContent} from './track_panel';
+import {renderChips, TrackContent} from './track_panel';
 import {trackRegistry} from './track_registry';
 import {
   drawVerticalLineAtTime,
@@ -43,21 +45,33 @@
   private readonly trackGroupId: string;
   private shellWidth = 0;
   private backgroundColor = '#ffffff';  // Updated from CSS later.
-  private summaryTrack: Track|undefined;
+  private summaryTrack?: Track;
 
   constructor({attrs}: m.CVnode<Attrs>) {
     super();
     this.trackGroupId = attrs.trackGroupId;
-    const trackCreator = trackRegistry.get(this.summaryTrackState.kind);
-    const engineId = this.summaryTrackState.engineId;
-    const engine = globals.engines.get(engineId);
-    if (engine !== undefined) {
-      this.summaryTrack = trackCreator.create({
-        trackId: this.summaryTrackState.id,
-        engine: engine.getProxy(`Track; kind: ${
-            this.summaryTrackState.kind}; id: ${this.summaryTrackState.id}`),
-      });
-    }
+  }
+
+  private tryLoadTrack() {
+    const trackId = this.trackGroupId;
+    const trackState = this.summaryTrackState;
+
+    const {id, uri} = trackState;
+
+    const ctx: TrackContext = {
+      trackInstanceId: id,
+      mountStore: <T>(migrate: Migrate<T>) => {
+        const {store, state} = globals;
+        const migratedState = migrate(state.trackGroups[trackId].state);
+        store.edit((draft) => {
+          draft.trackGroups[trackId].state = migratedState;
+        });
+        return store.createProxy<T>(['trackGroups', trackId, 'state']);
+      },
+    };
+
+    this.summaryTrack =
+        uri ? pluginManager.createTrack(uri, ctx) : loadTrack(trackState, id);
   }
 
   get trackGroupState(): TrackGroupState {
@@ -69,6 +83,10 @@
   }
 
   view({attrs}: m.CVnode<Attrs>) {
+    if (!this.summaryTrack) {
+      this.tryLoadTrack();
+    }
+
     const collapsed = this.trackGroupState.collapsed;
     let name = this.trackGroupState.name;
     if (name[0] === '/') {
@@ -132,7 +150,7 @@
                 'h1.track-title',
                 {title: name},
                 name,
-                m(TrackChips, {config: this.summaryTrackState.config}),
+                renderChips(this.summaryTrackState),
                 ),
             (this.trackGroupState.collapsed && child !== null) ?
                 m('h2.track-subtitle', child) :
@@ -286,3 +304,25 @@
 function StripPathFromExecutable(path: string) {
   return path.split('/').slice(-1)[0];
 }
+
+function loadTrack(trackState: TrackState, trackId: string): Track|undefined {
+  const engine = globals.engines.get(trackState.engineId);
+  if (engine === undefined) {
+    return undefined;
+  }
+
+  try {
+    const trackCreator = trackRegistry.get(trackState.kind);
+    return trackCreator.create({
+      trackId,
+      engine:
+          engine.getProxy(`Track; kind: ${trackState.kind}; id: ${trackId}`),
+    });
+  } catch (e) {
+    if (e instanceof RegistryError) {
+      return undefined;
+    } else {
+      throw e;
+    }
+  }
+}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 8dffff1..c280ced 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -18,12 +18,13 @@
 import {currentTargetOffset} from '../base/dom_utils';
 import {Icons} from '../base/semantic_icons';
 import {duration, Span, time} from '../base/time';
+import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {pluginManager} from '../common/plugins';
 import {RegistryError} from '../common/registry';
 import {TrackState} from '../common/state';
 import {raf} from '../core/raf_scheduler';
-import {TrackLike} from '../public';
+import {Migrate, Track, TrackContext} from '../public';
 
 import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
@@ -68,26 +69,39 @@
   return selectedArea.tracks.includes(id);
 }
 
-interface TrackChipsAttrs {
-  config: {[k: string]: any};
+interface TrackChipAttrs {
+  text: string;
 }
 
-export class TrackChips implements m.ClassComponent<TrackChipsAttrs> {
-  view({attrs}: m.CVnode<TrackChipsAttrs>) {
-    const {config} = attrs;
-
-    const isMetric = 'namespace' in config;
-    const isDebuggable = ('isDebuggable' in config) && config.isDebuggable;
-
-    return [
-      isMetric && m('span.chip', 'metric'),
-      isDebuggable && m('span.chip', 'debuggable'),
-    ];
+class TrackChip implements m.ClassComponent<TrackChipAttrs> {
+  view({attrs}: m.CVnode<TrackChipAttrs>) {
+    return m('span.chip', attrs.text);
   }
 }
 
+export function renderChips({uri, config}: TrackState) {
+  const tagElements: m.Children = [];
+  if (exists(uri)) {
+    const trackInfo = pluginManager.resolveTrackInfo(uri);
+    const tags = trackInfo?.tags;
+    tags?.metric && tagElements.push(m(TrackChip, {text: 'metric'}));
+    tags?.debuggable && tagElements.push(m(TrackChip, {text: 'debuggable'}));
+  } else {
+    if (config && typeof config === 'object') {
+      if ('namespace' in config) {
+        tagElements.push(m(TrackChip, {text: 'metric'}));
+      }
+      if ('isDebuggable' in config && config.isDebuggable) {
+        tagElements.push(m(TrackChip, {text: 'debuggable'}));
+      }
+    }
+  }
+
+  return tagElements;
+}
+
 interface TrackShellAttrs {
-  track: TrackLike;
+  track: Track;
   trackState: TrackState;
 }
 
@@ -134,7 +148,7 @@
               },
             },
             attrs.trackState.name,
-            m(TrackChips, {config: attrs.trackState.config}),
+            renderChips(attrs.trackState),
             ),
         m('.track-buttons',
           attrs.track.getTrackShellButtons(),
@@ -219,7 +233,7 @@
 }
 
 export interface TrackContentAttrs {
-  track: TrackLike;
+  track: Track;
 }
 export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
   private mouseDownX?: number;
@@ -278,7 +292,7 @@
 
 interface TrackComponentAttrs {
   trackState: TrackState;
-  track: TrackLike;
+  track: Track;
 }
 class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
   view({attrs}: m.CVnode<TrackComponentAttrs>) {
@@ -342,7 +356,7 @@
   // TODO(hjd): It would be nicer if these could not be undefined here.
   // We should implement a NullTrack which can be used if the trackState
   // has disappeared.
-  private track: TrackLike|undefined;
+  private track: Track|undefined;
   private trackState: TrackState|undefined;
 
   private tryLoadTrack(vnode: m.CVnode<TrackPanelAttrs>) {
@@ -352,8 +366,22 @@
     if (!trackState) return;
 
     const {id, uri} = trackState;
-    this.track =
-        uri ? pluginManager.createTrack(uri, id) : loadTrack(trackState, id);
+
+    const trackCtx: TrackContext = {
+      trackInstanceId: id,
+      mountStore: <T>(migrate: Migrate<T>) => {
+        const {store, state} = globals;
+        const migratedState = migrate(state.tracks[trackId].state);
+        globals.store.edit((draft) => {
+          draft.tracks[trackId].state = migratedState;
+        });
+        return store.createProxy<T>(['tracks', trackId, 'state']);
+      },
+    };
+
+    this.track = uri ? pluginManager.createTrack(uri, trackCtx) :
+                       loadTrack(trackState, id);
+
     this.track?.onCreate();
     this.trackState = trackState;
   }
@@ -492,8 +520,7 @@
   }
 }
 
-function loadTrack(trackState: TrackState, trackId: string): TrackLike|
-    undefined {
+function loadTrack(trackState: TrackState, trackId: string): Track|undefined {
   const engine = globals.engines.get(trackState.engineId);
   if (engine === undefined) {
     return undefined;
diff --git a/ui/src/plugins/com.example.Skeleton/index.ts b/ui/src/plugins/com.example.Skeleton/index.ts
index 0881b5f..354cb7f 100644
--- a/ui/src/plugins/com.example.Skeleton/index.ts
+++ b/ui/src/plugins/com.example.Skeleton/index.ts
@@ -16,9 +16,9 @@
   MetricVisualisation,
   Plugin,
   PluginContext,
-  PluginInfo,
-  TracePluginContext,
-  TrackInfo,
+  PluginContextTrace,
+  PluginDescriptor,
+  TrackInstanceDescriptor,
 } from '../../public';
 
 interface State {
@@ -35,11 +35,11 @@
     return {foo: 'bar'};
   }
 
-  async onTraceLoad(_: TracePluginContext<State>): Promise<void> {
+  async onTraceLoad(_: PluginContextTrace<State>): Promise<void> {
     //
   }
 
-  async onTraceUnload(_: TracePluginContext<State>): Promise<void> {
+  async onTraceUnload(_: PluginContextTrace<State>): Promise<void> {
     //
   }
 
@@ -47,17 +47,17 @@
     //
   }
 
-  async findPotentialTracks(_: TracePluginContext<State>):
-      Promise<TrackInfo[]> {
+  async findPotentialTracks(_: PluginContextTrace<State>):
+      Promise<TrackInstanceDescriptor[]> {
     return [];
   }
 
-  metricVisualisations(_: TracePluginContext<State>): MetricVisualisation[] {
+  metricVisualisations(_: PluginContextTrace<State>): MetricVisualisation[] {
     return [];
   }
 }
 
-export const plugin: PluginInfo<State> = {
+export const plugin: PluginDescriptor<State> = {
   // SKELETON: Update pluginId to match the directory of the plugin.
   pluginId: 'com.example.Skeleton',
   plugin: Skeleton,
diff --git a/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts b/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
index c7e0c8c..a77b14e 100644
--- a/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidBinderViz/index.ts
@@ -16,7 +16,7 @@
   MetricVisualisation,
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginDescriptor,
 } from '../../public';
 
 const SPEC = `
@@ -50,7 +50,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.AndroidBinderVizPlugin',
   plugin: AndroidBinderVizPlugin,
 };
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 3870cfe..8947364 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -15,7 +15,7 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginDescriptor,
 } from '../../public';
 
 class AndroidCujs implements Plugin {
@@ -130,7 +130,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.AndroidCujs',
   plugin: AndroidCujs,
 };
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index 17aa796..feb7080 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -15,7 +15,7 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginDescriptor,
 } from '../../public';
 
 class AndroidPerf implements Plugin {
@@ -60,7 +60,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.AndroidPerf',
   plugin: AndroidPerf,
 };
diff --git a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
index fedc300..5b94d08 100644
--- a/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
+++ b/ui/src/plugins/dev.perfetto.CoreCommands/index.ts
@@ -15,7 +15,7 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginDescriptor,
 } from '../../public';
 
 const SQL_STATS = `
@@ -174,7 +174,7 @@
   },
 };
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.CoreCommands',
   plugin: coreCommands,
 };
diff --git a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
index 1051784..4bb6e12 100644
--- a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
@@ -15,7 +15,7 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginDescriptor,
 } from '../../public';
 
 // This is just an example plugin, used to prove that the plugin system works.
@@ -29,7 +29,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.ExampleSimpleCommand',
   plugin: ExampleSimpleCommand,
 };
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
index a124f71..a18cfe2 100644
--- a/ui/src/plugins/dev.perfetto.ExampleState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
@@ -15,8 +15,8 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
-  TracePluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
 } from '../../public';
 
 interface State {
@@ -39,7 +39,7 @@
     //
   }
 
-  async onTraceLoad(ctx: TracePluginContext<State>): Promise<void> {
+  async onTraceLoad(ctx: PluginContextTrace<State>): Promise<void> {
     const {viewer, store} = ctx;
     ctx.addCommand({
       id: 'dev.perfetto.ExampleState#ShowCounter',
@@ -54,7 +54,7 @@
   }
 }
 
-export const plugin: PluginInfo<State> = {
+export const plugin: PluginDescriptor<State> = {
   pluginId: 'dev.perfetto.ExampleState',
   plugin: ExampleState,
 };
diff --git a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
index 07881da..07872ed 100644
--- a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
@@ -15,7 +15,7 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginDescriptor,
 } from '../../public';
 
 class LargeScreensPerf implements Plugin {
@@ -36,7 +36,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'dev.perfetto.LargeScreensPerf',
   plugin: LargeScreensPerf,
 };
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index b939428..60ab2b8 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -154,7 +154,7 @@
   // 'TrackController' and a 'Track'. In more recent versions of the UI
   // the functionality of |TrackController| has been merged into Track so
   // |TrackController|s are not necessary in new code.
-  registerTrackController(track: TrackControllerFactory): void;
+  LEGACY_registerTrackController(track: TrackControllerFactory): void;
 
   // Register a track factory. The core UI invokes |TrackCreator| to
   // construct tracks discovered by invoking |TrackProvider|s.
@@ -164,25 +164,30 @@
   // which returns GPU counter tracks. The counter track factory itself
   // could be registered in dev.perfetto.CounterTrack - a whole
   // different plugin.
-  registerTrack(track: TrackCreator): void;
+  LEGACY_registerTrack(track: TrackCreator): void;
 
   // Add a command.
   addCommand(command: Command): void;
 }
 
-export interface TrackContext {
-  // A unique ID for the instance of this track.
-  trackInstanceId: string;
-}
+export type Migrate<State> = (init: unknown) => State;
 
 export interface TrackContext {
-  // A unique ID for the instance of this track.
+  // The ID of this track instance.
   trackInstanceId: string;
+
+  // Creates a new store overlaying the track instance's state object.
+  // A migrate function must be passed to convert any existing state to a
+  // compatible format.
+  // When opening a fresh trace, the value of |init| will be undefined, and
+  // state should be updated to an appropriate default value.
+  // When loading a permalink, the value of |init| will be whatever was saved
+  // when the permalink was shared, which might be from an old version of this
+  // track.
+  mountStore<State>(migrate: Migrate<State>): Store<State>;
 }
 
-// TODO(stevegolton): Rename `Track` to `BaseTrack` (or similar) and rename this
-// interface to `Track`.
-export interface TrackLike {
+export interface Track {
   onCreate(): void;
   render(ctx: CanvasRenderingContext2D): void;
   onFullRedraw(): void;
@@ -199,7 +204,7 @@
   onDestroy(): void;
 }
 
-export interface PluginTrackInfo {
+export interface TrackDescriptor {
   // A unique identifier for the track. This must be unique within all tracks.
   uri: string;
 
@@ -208,34 +213,83 @@
   displayName: string;
 
   // A factory function returning the track object.
-  trackFactory: (ctx: TrackContext) => TrackLike;
+  track: (ctx: TrackContext) => Track;
 
-  // A list of tags used for sorting and grouping.
+  // The track "kind" Uued by various subsystems e.g. aggregation controllers.
+  // This is where "XXX_TRACK_KIND" values should be placed.
+  // TODO(stevegolton): This will be deprecated once we handle group selections
+  // in a more generic way - i.e. EventSet.
+  kind: string;
+
+  // An optional list of track IDs represented by this trace.
+  // This list is used for participation in track indexing by track ID.
+  // This index is used by various subsystems to find links between tracks based
+  // on the track IDs used by trace processor.
+  trackIds?: number[];
+
+  // Optional: The CPU number associated with this track.
+  cpu?: number;
+
+  // Optional: A list of tags used for sorting, grouping and "chips".
   tags?: TrackTags;
 }
 
+// Tracks within track groups (usually corresponding to processes) are sorted.
+// As we want to group all tracks related to a given thread together, we use
+// two keys:
+// - Primary key corresponds to a priority of a track block (all tracks related
+//   to a given thread or a single track if it's not thread-associated).
+// - Secondary key corresponds to a priority of a given thread-associated track
+//   within its thread track block.
+// Each track will have a sort key, which either a primary sort key
+// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
+// primary sort key is done independently).
+export enum PrimaryTrackSortKey {
+  DEBUG_SLICE_TRACK,
+  NULL_TRACK,
+  PROCESS_SCHEDULING_TRACK,
+  PROCESS_SUMMARY_TRACK,
+  EXPECTED_FRAMES_SLICE_TRACK,
+  ACTUAL_FRAMES_SLICE_TRACK,
+  PERF_SAMPLES_PROFILE_TRACK,
+  HEAP_PROFILE_TRACK,
+  MAIN_THREAD,
+  RENDER_THREAD,
+  GPU_COMPLETION_THREAD,
+  CHROME_IO_THREAD,
+  CHROME_COMPOSITOR_THREAD,
+  ORDINARY_THREAD,
+  COUNTER_TRACK,
+  ASYNC_SLICE_TRACK,
+  ORDINARY_TRACK,
+}
+
 // Similar to PluginContext but with additional properties to operate on the
 // currently loaded trace. Passed to trace-relevant hooks instead of
 // PluginContext.
-export interface TracePluginContext<T = undefined> extends PluginContext {
+export interface PluginContextTrace<T = undefined> extends PluginContext {
   readonly engine: EngineProxy;
   readonly store: Store<T>;
 
   // Add a new track from this plugin. The track is just made available here,
   // it's not automatically shown until it's added to a workspace.
-  addTrack(trackDetails: PluginTrackInfo): void;
+  addTrack(trackDetails: TrackDescriptor): void;
+
+  // Suggest a track be added to the workspace on a fresh trace load.
+  // Supersedes `findPotentialTracks()` which has been removed.
+  // Note: this API will be deprecated soon.
+  suggestTrack(trackInfo: TrackInstanceDescriptor): void;
 }
 
 export interface BasePlugin<State> {
   // Lifecycle methods.
   onActivate(ctx: PluginContext): void;
-  onTraceLoad?(ctx: TracePluginContext<State>): Promise<void>;
-  onTraceUnload?(ctx: TracePluginContext<State>): Promise<void>;
+  onTraceLoad?(ctx: PluginContextTrace<State>): Promise<void>;
+  onTraceUnload?(ctx: PluginContextTrace<State>): Promise<void>;
   onDeactivate?(ctx: PluginContext): void;
 
   // Extension points.
   metricVisualisations?(ctx: PluginContext): MetricVisualisation[];
-  findPotentialTracks?(ctx: TracePluginContext<State>): Promise<TrackInfo[]>;
 }
 
 export interface StatefulPlugin<State> extends BasePlugin<State> {
@@ -264,17 +318,17 @@
   new(): Plugin<T>;
 }
 
-export interface TrackInfo {
-  // The id of this 'type' of track. This id is used to select the
-  // correct |TrackCreator| to construct the track.
-  trackKind: string;
-
+export interface TrackInstanceDescriptor {
   // A human readable name for this specific track. It will normally be
   // displayed on the left-hand-side of the track.
   name: string;
 
-  // An opaque config for the track.
-  config: {};
+  // Used to define default sort order for new traces.
+  // Note: sortKey will be deprecated soon in favour of tags.
+  sortKey: PrimaryTrackSortKey;
+
+  // URI of the suggested track.
+  uri: string;
 }
 
 // A predicate for selecting a groups of tracks.
@@ -284,30 +338,28 @@
   // A human readable name for this specific track.
   name: string;
 
-  // This is where "XXX_TRACK_KIND" values should be placed.
-  kind: string;
+  // Controls whether to show the "metric" chip.
+  metric: boolean;
 
-  // The CPU number associated with this track.
-  cpu: number;
+  // Controls whether to show the "debuggable" chip.
+  debuggable: boolean;
 }
 
 // An set of key/value pairs describing a given track. These are used for
-// selecting tracks to pin/unpin and (in future) the sorting and grouping of
-// tracks.
-// These are also (ab)used for communicating information about tracks for the
-// purposes of locating tracks by their properties e.g. aggregation & search.
+// selecting tracks to pin/unpin, diplsaying "chips" in the track shell, and
+// (in future) the sorting and grouping of tracks.
 // We define a handful of well known fields, and the rest are arbitrary key-
 // value pairs.
 export type TrackTags = Partial<WellKnownTrackTags>&{
   // There may be arbitrary other key/value pairs.
-  [key: string]: string|number|undefined;
+  [key: string]: string|number|boolean|undefined;
 }
 
 // Plugins can be passed as class refs, factory functions, or concrete plugin
 // implementations.
 export type PluginFactory<T> = PluginClass<T>|Plugin<T>|(() => Plugin<T>);
 
-export interface PluginInfo<T = undefined> {
+export interface PluginDescriptor<T = undefined> {
   // A unique string for your plugin. To ensure the name is unique you
   // may wish to use a URL with reversed components in the manner of
   // Java package names.
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
new file mode 100644
index 0000000..d8db8f0
--- /dev/null
+++ b/ui/src/public/utils.ts
@@ -0,0 +1,73 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export function getTrackName(args: Partial<{
+  name: string | null,
+  utid: number,
+  processName: string | null,
+  pid: number | null,
+  threadName: string | null,
+  tid: number | null,
+  upid: number | null,
+  kind: string,
+  threadTrack: boolean
+}>) {
+  const {
+    name,
+    upid,
+    utid,
+    processName,
+    threadName,
+    pid,
+    tid,
+    kind,
+    threadTrack,
+  } = args;
+
+  const hasName = name !== undefined && name !== null && name !== '[NULL]';
+  const hasUpid = upid !== undefined && upid !== null;
+  const hasUtid = utid !== undefined && utid !== null;
+  const hasProcessName = processName !== undefined && processName !== null;
+  const hasThreadName = threadName !== undefined && threadName !== null;
+  const hasTid = tid !== undefined && tid !== null;
+  const hasPid = pid !== undefined && pid !== null;
+  const hasKind = kind !== undefined;
+  const isThreadTrack = threadTrack !== undefined && threadTrack;
+
+  // If we don't have any useful information (better than
+  // upid/utid) we show the track kind to help with tracking
+  // down where this is coming from.
+  const kindSuffix = hasKind ? ` (${kind})` : '';
+
+  if (isThreadTrack && hasName && hasTid) {
+    return `${name} (${tid})`;
+  } else if (hasName) {
+    return `${name}`;
+  } else if (hasUpid && hasPid && hasProcessName) {
+    return `${processName} ${pid}`;
+  } else if (hasUpid && hasPid) {
+    return `Process ${pid}`;
+  } else if (hasThreadName && hasTid) {
+    return `${threadName} ${tid}`;
+  } else if (hasTid) {
+    return `Thread ${tid}`;
+  } else if (hasUpid) {
+    return `upid: ${upid}${kindSuffix}`;
+  } else if (hasUtid) {
+    return `utid: ${utid}${kindSuffix}`;
+  } else if (hasKind) {
+    return `Unnamed ${kind}`;
+  }
+  return 'Unknown';
+}
diff --git a/ui/src/tracks/actual_frames/index.ts b/ui/src/tracks/actual_frames/index.ts
index 5e1d631..90de29a 100644
--- a/ui/src/tracks/actual_frames/index.ts
+++ b/ui/src/tracks/actual_frames/index.ts
@@ -17,8 +17,8 @@
 import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
 import {TrackData} from '../../common/track_data';
 import {TrackController} from '../../controller/track_controller';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 import {ChromeSliceTrack} from '../chrome_slices';
 
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
@@ -156,19 +156,19 @@
 
 export class ActualFramesSliceTrack extends ChromeSliceTrack {
   static readonly kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new ActualFramesSliceTrack(args);
   }
 }
 
 class ActualFrames implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(ActualFramesSliceTrackController);
-    ctx.registerTrack(ActualFramesSliceTrack);
+    ctx.LEGACY_registerTrackController(ActualFramesSliceTrackController);
+    ctx.LEGACY_registerTrack(ActualFramesSliceTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ActualFrames',
   plugin: ActualFrames,
 };
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index 948f82d..743d665 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -26,10 +26,12 @@
 import {
   Plugin,
   PluginContext,
-  PluginInfo,
-  TracePluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
 } from '../../public';
 
+export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
+
 export interface Data extends TrackData {
   // Total number of log events within [start, end], before any quantization.
   numEvents: number;
@@ -147,7 +149,7 @@
 class AndroidLog implements Plugin {
   onActivate(_ctx: PluginContext): void {}
 
-  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result =
         await ctx.engine.query(`select count(1) as cnt from android_logs`);
     const count = result.firstRow({cnt: NUM}).cnt;
@@ -155,7 +157,8 @@
       ctx.addTrack({
         uri: 'perfetto.AndroidLog',
         displayName: 'Android logs',
-        trackFactory: ({trackInstanceId}) => {
+        kind: ANDROID_LOGS_TRACK_KIND,
+        track: ({trackInstanceId}) => {
           return new TrackWithControllerAdapter<Config, Data>(
               ctx.engine,
               trackInstanceId,
@@ -168,7 +171,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.AndroidLog',
   plugin: AndroidLog,
 };
diff --git a/ui/src/tracks/annotation/index.ts b/ui/src/tracks/annotation/index.ts
new file mode 100644
index 0000000..2c8649a
--- /dev/null
+++ b/ui/src/tracks/annotation/index.ts
@@ -0,0 +1,90 @@
+// 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.
+
+import {
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../../common/query_result';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {
+  Config as CounterTrackConfig,
+  COUNTER_TRACK_KIND,
+  CounterTrack,
+} from '../counter';
+
+class AnnotationPlugin implements Plugin {
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    await this.addAnnotationCounterTracks(ctx);
+  }
+
+  private async addAnnotationCounterTracks(ctx: PluginContextTrace) {
+    const {engine} = ctx;
+    const counterResult = await engine.query(`
+      SELECT
+        id,
+        name,
+        min_value as minValue,
+        max_value as maxValue
+      FROM annotation_counter_track`);
+
+    const counterIt = counterResult.iter({
+      id: NUM,
+      name: STR,
+      minValue: NUM_NULL,
+      maxValue: NUM_NULL,
+    });
+
+    for (; counterIt.valid(); counterIt.next()) {
+      const id = counterIt.id;
+      const name = counterIt.name;
+      const minimumValue =
+          counterIt.minValue === null ? undefined : counterIt.minValue;
+      const maximumValue =
+          counterIt.maxValue === null ? undefined : counterIt.maxValue;
+
+      const config: CounterTrackConfig = {
+        name,
+        trackId: id,
+        namespace: 'annotation',
+        minimumValue,
+        maximumValue,
+      };
+
+      ctx.addTrack({
+        uri: `perfetto.Annotation#counter${id}`,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        tags: {
+          metric: true,
+        },
+        track: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'perfetto.Annotation',
+  plugin: AnnotationPlugin,
+};
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index d25717f..09d0dda 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -19,8 +19,8 @@
 import {
   TrackController,
 } from '../../controller/track_controller';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 import {ChromeSliceTrack} from '../chrome_slices';
 
 export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
@@ -134,19 +134,19 @@
 
 export class AsyncSliceTrack extends ChromeSliceTrack {
   static readonly kind = ASYNC_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new AsyncSliceTrack(args);
   }
 }
 
 class AsyncSlicePlugin implements Plugin {
   onActivate(ctx: PluginContext) {
-    ctx.registerTrackController(AsyncSliceTrackController);
-    ctx.registerTrack(AsyncSliceTrack);
+    ctx.LEGACY_registerTrackController(AsyncSliceTrackController);
+    ctx.LEGACY_registerTrack(AsyncSliceTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.AsyncSlices',
   plugin: AsyncSlicePlugin,
 };
diff --git a/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts b/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
index 60db8a0..1026328 100644
--- a/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
@@ -22,10 +22,12 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {runQueryInNewTab} from '../../frontend/query_result_tab';
-import {NewTrackArgs, Track} from '../../frontend/track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
 
-import {ScrollJankTracks as DecideTracksResult} from './index';
-import {ENABLE_CHROME_SCROLL_JANK_PLUGIN} from './index';
+import {
+  ENABLE_CHROME_SCROLL_JANK_PLUGIN,
+  ScrollJankTracks as DecideTracksResult,
+} from './index';
 
 interface ChromeTasksScrollJankTrackConfig {}
 
@@ -36,7 +38,7 @@
 export class ChromeTasksScrollJankTrack extends
     NamedSliceTrack<ChromeTasksScrollJankTrackTypes> {
   static readonly kind = 'org.chromium.ScrollJank.BrowserUIThreadLongTasks';
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new ChromeTasksScrollJankTrack(args);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index c69e9a2..cfcaada 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -21,12 +21,13 @@
 import {
   generateSqlWithInternalLayout,
 } from '../../common/internal_layout_utils';
-import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
-import {NewTrackArgs, Track} from '../../frontend/track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -50,7 +51,7 @@
     CustomSqlTableSliceTrack<EventLatencyTrackTypes> {
   static readonly kind = 'org.chromium.ScrollJank.event_latencies';
 
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new EventLatencyTrack(args);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 963a2b1..592d9a5 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -16,7 +16,7 @@
 import {Engine} from '../../common/engine';
 import {featureFlags} from '../../common/feature_flags';
 import {ObjectById} from '../../common/state';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
 
 import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
@@ -129,14 +129,14 @@
 
 class ChromeScrollJankPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(ChromeTasksScrollJankTrack);
-    ctx.registerTrack(EventLatencyTrack);
-    ctx.registerTrack(ScrollJankV3Track);
-    ctx.registerTrack(TopLevelScrollTrack);
+    ctx.LEGACY_registerTrack(ChromeTasksScrollJankTrack);
+    ctx.LEGACY_registerTrack(EventLatencyTrack);
+    ctx.LEGACY_registerTrack(ScrollJankV3Track);
+    ctx.LEGACY_registerTrack(TopLevelScrollTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ChromeScrollJank',
   plugin: ChromeScrollJankPlugin,
 };
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index 6e554d4..f1db56e 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -19,12 +19,12 @@
 } from '../../common/colorizer';
 import {Engine} from '../../common/engine';
 import {
-  PrimaryTrackSortKey,
   SCROLLING_TRACK_GROUP,
 } from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
-import {NewTrackArgs, Track} from '../../frontend/track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -48,7 +48,7 @@
     CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
   static readonly kind = 'org.chromium.ScrollJank.scroll_jank_v3_track';
 
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new ScrollJankV3Track(args);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index d82944b..97ccc4a 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -15,20 +15,20 @@
 import {v4 as uuidv4} from 'uuid';
 
 import {Engine} from '../../common/engine';
-import {
-  PrimaryTrackSortKey,
-  SCROLLING_TRACK_GROUP,
-} from '../../common/state';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
-import {NewTrackArgs, Track} from '../../frontend/track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
-import {ScrollJankPluginState} from './index';
 
-import {ScrollJankTracks as DecideTracksResult} from './index';
+import {
+  ScrollJankPluginState,
+  ScrollJankTracks as DecideTracksResult,
+} from './index';
 import {ScrollDetailsPanel} from './scroll_details_panel';
 
 export {Data} from '../chrome_slices';
@@ -37,7 +37,7 @@
     CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
   static readonly kind = 'org.chromium.TopLevelScrolls.scrolls';
 
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new TopLevelScrollTrack(args);
   }
 
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 875a7bf..28efb24 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -28,8 +28,8 @@
 import {globals} from '../../frontend/globals';
 import {cachedHsluvToHex} from '../../frontend/hsluv_cache';
 import {PxSpan, TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, SliceRect, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, SliceRect, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
 const SLICE_HEIGHT = 18;
@@ -159,9 +159,9 @@
   }
 }
 
-export class ChromeSliceTrack extends Track<Config, Data> {
+export class ChromeSliceTrack extends TrackBase<Config, Data> {
   static readonly kind: string = SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new ChromeSliceTrack(args);
   }
 
@@ -439,12 +439,12 @@
 
 class ChromeSlicesPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(ChromeSliceTrackController);
-    ctx.registerTrack(ChromeSliceTrack);
+    ctx.LEGACY_registerTrackController(ChromeSliceTrackController);
+    ctx.LEGACY_registerTrack(ChromeSliceTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ChromeSlices',
   plugin: ChromeSlicesPlugin,
 };
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index cb96f6f..1fbc53f 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -13,28 +13,36 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
 
 import {searchSegment} from '../../base/binary_search';
 import {assertTrue} from '../../base/logging';
 import {duration, time, Time} from '../../base/time';
 import {Actions} from '../../common/actions';
+import {
+  BasicAsyncTrack,
+  NUM_NULL,
+  STR_NULL,
+} from '../../common/basic_async_track';
 import {drawTrackHoverTooltip} from '../../common/canvas_utils';
 import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
 import {
+  EngineProxy,
   LONG,
   LONG_NULL,
   NUM,
   Plugin,
   PluginContext,
-  PluginInfo,
+  PluginContextTrace,
+  PluginDescriptor,
+  PrimaryTrackSortKey,
+  Store,
   STR,
-  TracePluginContext,
-  TrackInfo,
+  TrackContext,
 } from '../../public';
+import {getTrackName} from '../../public/utils';
 import {Button} from '../../widgets/button';
 import {MenuItem, PopupMenu2} from '../../widgets/menu';
 
@@ -66,76 +74,157 @@
   minimumValue?: number;
   startTs?: time;
   endTs?: time;
-  namespace: string;
+  namespace?: string;
   trackId: number;
-  scale?: CounterScaleOptions;
+  defaultScale?: CounterScaleOptions;
 }
 
-class CounterTrackController extends TrackController<Config, Data> {
-  static readonly kind = COUNTER_TRACK_KIND;
-  private setup = false;
+const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
+const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
+
+// Sets the default 'scale' for counter tracks. If the regex matches
+// then the paired mode is used. Entries are in priority order so the
+// first match wins.
+const COUNTER_REGEX: [RegExp, CounterScaleOptions][] = [
+  // Power counters make more sense in rate mode since you're typically
+  // interested in the slope of the graph rather than the absolute
+  // value.
+  [new RegExp('^power\..*$'), 'RATE'],
+  // Same for network counters.
+  [NETWORK_TRACK_REGEX, 'RATE'],
+  // Entity residency
+  [ENTITY_RESIDENCY_REGEX, 'RATE'],
+];
+
+function getCounterScale(name: string): CounterScaleOptions|undefined {
+  for (const [re, scale] of COUNTER_REGEX) {
+    if (name.match(re)) {
+      return scale;
+    }
+  }
+  return undefined;
+}
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 3.5;
+const RECT_HEIGHT = 24.5;
+
+interface CounterTrackState {
+  scale: CounterScaleOptions;
+}
+
+function isCounterState(x: unknown): x is CounterTrackState {
+  if (x && typeof x === 'object' && 'scale' in x) {
+    if (typeof x.scale === 'string') {
+      return true;
+    } else {
+      return false;
+    }
+  } else {
+    return false;
+  }
+}
+
+export class CounterTrack extends BasicAsyncTrack<Data> {
   private maximumValueSeen = 0;
   private minimumValueSeen = 0;
   private maximumDeltaSeen = 0;
   private minimumDeltaSeen = 0;
   private maxDurNs: duration = 0n;
+  private store: Store<CounterTrackState>;
+  private id: string;
+  private uuid = uuidv4();
+  private isSetup = false;
+
+  constructor(
+      ctx: TrackContext, private config: Config, private engine: EngineProxy) {
+    super();
+    this.id = ctx.trackInstanceId;
+    this.store = ctx.mountStore<CounterTrackState>((init: unknown) => {
+      if (isCounterState(init)) {
+        return init;
+      } else {
+        return {scale: this.config.defaultScale ?? 'ZERO_BASED'};
+      }
+    });
+  }
+
+  // Returns a valid SQL table name with the given prefix that should be unique
+  // for each track.
+  tableName(prefix: string) {
+    // Derive table name from, since that is unique for each track.
+    // Track ID can be UUID but '-' is not valid for sql table name.
+    const idSuffix = this.uuid.split('-').join('_');
+    return `${prefix}_${idSuffix}`;
+  }
+
+  private namespaceTable(tableName: string): string {
+    if (this.config.namespace) {
+      return this.config.namespace + '_' + tableName;
+    } else {
+      return tableName;
+    }
+  }
+
+  private async setup() {
+    if (this.config.namespace === undefined) {
+      await this.engine.query(`
+        create view ${this.tableName('counter_view')} as
+        select
+          id,
+          ts,
+          dur,
+          value,
+          delta
+        from experimental_counter_dur
+        where track_id = ${this.config.trackId};
+      `);
+    } else {
+      await this.engine.query(`
+        create view ${this.tableName('counter_view')} as
+        select
+          id,
+          ts,
+          lead(ts, 1, ts) over (order by ts) - ts as dur,
+          lead(value, 1, value) over (order by ts) - value as delta,
+          value
+        from ${this.namespaceTable('counter')}
+        where track_id = ${this.config.trackId};
+      `);
+    }
+
+    const maxDurResult = await this.engine.query(`
+        select
+          max(
+            iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
+          ) as maxDur
+        from ${this.tableName('counter_view')}
+    `);
+    this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
+
+    const queryRes = await this.engine.query(`
+      select
+        ifnull(max(value), 0) as maxValue,
+        ifnull(min(value), 0) as minValue,
+        ifnull(max(delta), 0) as maxDelta,
+        ifnull(min(delta), 0) as minDelta
+      from ${this.tableName('counter_view')}`);
+    const row = queryRes.firstRow(
+        {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
+    this.maximumValueSeen = row.maxValue;
+    this.minimumValueSeen = row.minValue;
+    this.maximumDeltaSeen = row.maxDelta;
+    this.minimumDeltaSeen = row.minDelta;
+  }
 
   async onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data> {
-    if (!this.setup) {
-      if (this.config.namespace === undefined) {
-        await this.query(`
-          create view ${this.tableName('counter_view')} as
-          select
-            id,
-            ts,
-            dur,
-            value,
-            delta
-          from experimental_counter_dur
-          where track_id = ${this.config.trackId};
-        `);
-      } else {
-        await this.query(`
-          create view ${this.tableName('counter_view')} as
-          select
-            id,
-            ts,
-            lead(ts, 1, ts) over (order by ts) - ts as dur,
-            lead(value, 1, value) over (order by ts) - value as delta,
-            value
-          from ${this.namespaceTable('counter')}
-          where track_id = ${this.config.trackId};
-        `);
-      }
-
-      const maxDurResult = await this.query(`
-          select
-            max(
-              iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
-            ) as maxDur
-          from ${this.tableName('counter_view')}
-      `);
-      this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
-
-      const queryRes = await this.query(`
-        select
-          ifnull(max(value), 0) as maxValue,
-          ifnull(min(value), 0) as minValue,
-          ifnull(max(delta), 0) as maxDelta,
-          ifnull(min(delta), 0) as minDelta
-        from ${this.tableName('counter_view')}`);
-      const row = queryRes.firstRow(
-          {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
-      this.maximumValueSeen = row.maxValue;
-      this.minimumValueSeen = row.minValue;
-      this.maximumDeltaSeen = row.maxDelta;
-      this.minimumDeltaSeen = row.minDelta;
-
-      this.setup = true;
+    if (!this.isSetup) {
+      await this.setup();
+      this.isSetup = true;
     }
 
-    const queryRes = await this.query(`
+    const queryRes = await this.engine.query(`
       select
         (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
         min(value) as minValue,
@@ -219,34 +308,18 @@
       return this.config.minimumValue;
     }
   }
-}
-
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 3.5;
-const RECT_HEIGHT = 24.5;
-
-class CounterTrack extends Track<Config, Data> {
-  static readonly kind = COUNTER_TRACK_KIND;
-  static create(args: NewTrackArgs): CounterTrack {
-    return new CounterTrack(args);
-  }
 
   private mousePos = {x: 0, y: 0};
   private hoveredValue: number|undefined = undefined;
   private hoveredTs: time|undefined = undefined;
   private hoveredTsEnd: time|undefined = undefined;
 
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
   getHeight() {
     return MARGIN_TOP + RECT_HEIGHT;
   }
 
   getContextMenu(): m.Vnode<any> {
-    const currentScale = this.config.scale;
+    const currentScale = this.store.state.scale;
     const scales: {name: CounterScaleOptions, humanName: string}[] = [
       {name: 'ZERO_BASED', humanName: 'Zero based'},
       {name: 'MIN_MAX', humanName: 'Min/Max'},
@@ -258,10 +331,8 @@
         label: scale.humanName,
         active: currentScale === scale.name,
         onclick: () => {
-          this.config.scale = scale.name;
-          Actions.updateTrackConfig({
-            id: this.trackState.id,
-            config: this.config,
+          this.store.edit((draft) => {
+            draft.scale = scale.name;
           });
         },
       });
@@ -282,7 +353,7 @@
       visibleTimeScale: timeScale,
       windowSpan,
     } = globals.frontendLocalState;
-    const data = this.data();
+    const data = this.data;
 
     // Can't possibly draw anything.
     if (data === undefined || data.timestamps.length === 0) {
@@ -295,7 +366,7 @@
     assertTrue(data.timestamps.length === data.totalDeltas.length);
     assertTrue(data.timestamps.length === data.rate.length);
 
-    const scale: CounterScaleOptions = this.config.scale || 'ZERO_BASED';
+    const scale: CounterScaleOptions = this.store.state.scale;
 
     let minValues = data.minValues;
     let maxValues = data.maxValues;
@@ -485,17 +556,17 @@
   }
 
   onMouseMove(pos: {x: number, y: number}) {
-    const data = this.data();
+    const data = this.data;
     if (data === undefined) return;
     this.mousePos = pos;
     const {visibleTimeScale} = globals.frontendLocalState;
     const time = visibleTimeScale.pxToHpTime(pos.x);
 
     let values = data.lastValues;
-    if (this.config.scale === 'DELTA_FROM_PREVIOUS') {
+    if (this.store.state.scale === 'DELTA_FROM_PREVIOUS') {
       values = data.totalDeltas;
     }
-    if (this.config.scale === 'RATE') {
+    if (this.store.state.scale === 'RATE') {
       values = data.rate;
     }
 
@@ -513,7 +584,7 @@
   }
 
   onMouseClick({x}: {x: number}): boolean {
-    const data = this.data();
+    const data = this.data;
     if (data === undefined) return false;
     const {visibleTimeScale} = globals.frontendLocalState;
     const time = visibleTimeScale.pxToHpTime(x);
@@ -527,21 +598,59 @@
         leftTs: Time.fromRaw(data.timestamps[left]),
         rightTs: Time.fromRaw(right !== -1 ? data.timestamps[right] : -1n),
         id: counterId,
-        trackId: this.trackState.id,
+        trackId: this.id,
       }));
       return true;
     }
   }
+
+  async onDestroy(): Promise<void> {
+    await this.engine.query(
+        `DROP VIEW IF EXISTS ${this.tableName('counter_view')}`);
+  }
+}
+
+interface CounterInfo {
+  name: string;
+  trackId: number;
 }
 
 class CounterPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(CounterTrackController);
-    ctx.registerTrack(CounterTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    await this.addCounterTracks(ctx);
+    await this.addGpuFrequencyTracks(ctx);
+    await this.addCpuFreqLimitCounterTracks(ctx);
+    await this.addCpuPerfCounterTracks(ctx);
+    await this.addThreadCounterTracks(ctx);
+    await this.addProcessCounterTracks(ctx);
   }
 
-  async findPotentialTracks({engine}: TracePluginContext):
-      Promise<TrackInfo[]> {
+  private async addCounterTracks(ctx: PluginContextTrace) {
+    const counters = await this.getCounterNames(ctx.engine);
+    for (const {trackId, name} of counters) {
+      const config:
+          Config = {name, trackId, defaultScale: getCounterScale(name)};
+      const uri = `perfetto.Counter#${trackId}`;
+      ctx.addTrack({
+        uri,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        trackIds: [trackId],
+        track: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+      ctx.suggestTrack({
+        uri,
+        name,
+        sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
+      });
+    }
+  }
+
+  private async getCounterNames(engine: EngineProxy): Promise<CounterInfo[]> {
     const result = await engine.query(`
     select name, id
     from (
@@ -562,24 +671,237 @@
       id: NUM,
     });
 
-    const tracks: TrackInfo[] = [];
+    const tracks: CounterInfo[] = [];
     for (; it.valid(); it.next()) {
-      const name = it.name;
-      const trackId = it.id;
       tracks.push({
-        trackKind: COUNTER_TRACK_KIND,
-        name,
-        config: {
-          name,
-          trackId,
-        },
+        trackId: it.id,
+        name: it.name,
       });
     }
     return tracks;
   }
+
+  private async addGpuFrequencyTracks(ctx: PluginContextTrace) {
+    const engine = ctx.engine;
+    const numGpus = await engine.getNumberOfGpus();
+    const maxGpuFreqResult = await engine.query(`
+      select ifnull(max(value), 0) as maximumValue
+      from counter c
+      inner join gpu_counter_track t on c.track_id = t.id
+      where name = 'gpufreq';
+    `);
+    const maximumValue =
+        maxGpuFreqResult.firstRow({maximumValue: NUM}).maximumValue;
+
+    for (let gpu = 0; gpu < numGpus; gpu++) {
+      // Only add a gpu freq track if we have
+      // gpu freq data.
+      const freqExistsResult = await engine.query(`
+      select id
+      from gpu_counter_track
+      where name = 'gpufreq' and gpu_id = ${gpu}
+      limit 1;
+    `);
+      if (freqExistsResult.numRows() > 0) {
+        const trackId = freqExistsResult.firstRow({id: NUM}).id;
+        const uri = `perfetto.Counter#gpu_freq${gpu}`;
+        const name = `Gpu ${gpu} Frequency`;
+        const config: Config = {
+          name,
+          trackId,
+          maximumValue,
+          defaultScale: getCounterScale(name),
+        };
+        ctx.addTrack({
+          uri,
+          displayName: name,
+          kind: COUNTER_TRACK_KIND,
+          trackIds: [trackId],
+          track: (trackCtx) => {
+            return new CounterTrack(trackCtx, config, ctx.engine);
+          },
+        });
+      }
+    }
+  }
+
+  async addCpuFreqLimitCounterTracks(ctx: PluginContextTrace): Promise<void> {
+    const cpuFreqLimitCounterTracksSql = `
+      select name, id
+      from cpu_counter_track
+      where name glob "Cpu * Freq Limit"
+      order by name asc
+    `;
+
+    this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql);
+  }
+
+  async addCpuPerfCounterTracks(ctx: PluginContextTrace): Promise<void> {
+    // Perf counter tracks are bound to CPUs, follow the scheduling and
+    // frequency track naming convention ("Cpu N ...").
+    // Note: we might not have a track for a given cpu if no data was seen from
+    // it. This might look surprising in the UI, but placeholder tracks are
+    // wasteful as there's no way of collapsing global counter tracks at the
+    // moment.
+    const addCpuPerfCounterTracksSql = `
+      select printf("Cpu %u %s", cpu, name) as name, id
+      from perf_counter_track as pct
+      order by perf_session_id asc, pct.name asc, cpu asc
+    `;
+    this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql);
+  }
+
+  async addCpuCounterTracks(ctx: PluginContextTrace, sql: string):
+      Promise<void> {
+    const result = await ctx.engine.query(sql);
+
+    const it = result.iter({
+      name: STR,
+      id: NUM,
+    });
+
+    for (; it.valid(); it.next()) {
+      const name = it.name;
+      const trackId = it.id;
+      const config: Config = {
+        name,
+        trackId,
+        defaultScale: getCounterScale(name),
+      };
+      ctx.addTrack({
+        uri: `perfetto.Counter#cpu${trackId}`,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        trackIds: [trackId],
+        track: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
+
+  async addThreadCounterTracks(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+      select
+        thread_counter_track.name as trackName,
+        utid,
+        upid,
+        tid,
+        thread.name as threadName,
+        thread_counter_track.id as trackId,
+        thread.start_ts as startTs,
+        thread.end_ts as endTs
+      from thread_counter_track
+      join thread using(utid)
+      left join process using(upid)
+      where thread_counter_track.name != 'thread_time'
+    `);
+
+    const it = result.iter({
+      startTs: LONG_NULL,
+      trackId: NUM,
+      endTs: LONG_NULL,
+      trackName: STR_NULL,
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      threadName: STR_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const tid = it.tid;
+      const startTs = it.startTs === null ? undefined : it.startTs;
+      const endTs = it.endTs === null ? undefined : it.endTs;
+      const trackId = it.trackId;
+      const trackName = it.trackName;
+      const threadName = it.threadName;
+      const kind = COUNTER_TRACK_KIND;
+      const name = getTrackName({
+        name: trackName,
+        utid,
+        tid,
+        kind,
+        threadName,
+        threadTrack: true,
+      });
+      const config: Config = {
+        name,
+        trackId,
+        startTs: Time.fromRaw(startTs),
+        endTs: Time.fromRaw(endTs),
+        defaultScale: getCounterScale(name),
+      };
+      ctx.addTrack({
+        uri: `perfetto.Counter#thread${trackId}`,
+        displayName: name,
+        kind,
+        trackIds: [trackId],
+        track: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
+
+  async addProcessCounterTracks(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+    select
+      process_counter_track.id as trackId,
+      process_counter_track.name as trackName,
+      upid,
+      process.pid,
+      process.name as processName,
+      process.start_ts as startTs,
+      process.end_ts as endTs
+    from process_counter_track
+    join process using(upid);
+  `);
+    const it = result.iter({
+      trackId: NUM,
+      trackName: STR_NULL,
+      upid: NUM,
+      startTs: LONG_NULL,
+      endTs: LONG_NULL,
+      pid: NUM_NULL,
+      processName: STR_NULL,
+    });
+    for (let i = 0; it.valid(); ++i, it.next()) {
+      const trackId = it.trackId;
+      const startTs = it.startTs === null ? undefined : it.startTs;
+      const endTs = it.endTs === null ? undefined : it.endTs;
+      const pid = it.pid;
+      const trackName = it.trackName;
+      const upid = it.upid;
+      const processName = it.processName;
+      const kind = COUNTER_TRACK_KIND;
+      const name = getTrackName({
+        name: trackName,
+        upid,
+        pid,
+        kind,
+        processName,
+      });
+      const config: Config = {
+        name,
+        trackId,
+        startTs: Time.fromRaw(startTs),
+        endTs: Time.fromRaw(endTs),
+        defaultScale: getCounterScale(name),
+      };
+      ctx.addTrack({
+        uri: `perfetto.Counter#process${trackId}`,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        trackIds: [trackId],
+        track: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.Counter',
   plugin: CounterPlugin,
 };
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index baa24f9..35faefc 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -26,12 +26,21 @@
   NUM_NULL,
   QueryResult,
 } from '../../common/query_result';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+  TrackWithControllerAdapter,
+} from '../../common/track_adapter';
 import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
 
 
 export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
@@ -55,9 +64,7 @@
   minimumValue?: number;
 }
 
-class CpuFreqTrackController extends TrackController<Config, Data> {
-  static readonly kind = CPU_FREQ_TRACK_KIND;
-
+class CpuFreqTrackController extends TrackControllerAdapter<Config, Data> {
   private maxDur: duration = 0n;
   private maxTsEnd: time = Time.ZERO;
   private maximumValueSeen = 0;
@@ -266,8 +273,7 @@
 const MARGIN_TOP = 4.5;
 const RECT_HEIGHT = 20;
 
-class CpuFreqTrack extends Track<Config, Data> {
-  static readonly kind = CPU_FREQ_TRACK_KIND;
+class CpuFreqTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): CpuFreqTrack {
     return new CpuFreqTrack(args);
   }
@@ -484,13 +490,74 @@
 }
 
 class CpuFreq implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(CpuFreqTrackController);
-    ctx.registerTrack(CpuFreqTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const {engine} = ctx;
+
+    const cpus = await engine.getCpus();
+
+    const maxCpuFreqResult = await engine.query(`
+      select ifnull(max(value), 0) as freq
+      from counter c
+      inner join cpu_counter_track t on c.track_id = t.id
+      where name = 'cpufreq';
+    `);
+    const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
+
+    for (const cpu of cpus) {
+      // Only add a cpu freq track if we have
+      // cpu freq data.
+      // TODO(hjd): Find a way to display cpu idle
+      // events even if there are no cpu freq events.
+      const cpuFreqIdleResult = await engine.query(`
+        select
+          id as cpuFreqId,
+          (
+            select id
+            from cpu_counter_track
+            where name = 'cpuidle'
+            and cpu = ${cpu}
+            limit 1
+          ) as cpuIdleId
+        from cpu_counter_track
+        where name = 'cpufreq' and cpu = ${cpu}
+        limit 1;
+      `);
+
+      if (cpuFreqIdleResult.numRows() > 0) {
+        const row = cpuFreqIdleResult.firstRow({
+          cpuFreqId: NUM,
+          cpuIdleId: NUM_NULL,
+        });
+        const freqTrackId = row.cpuFreqId;
+        const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
+
+        ctx.addTrack({
+          uri: `perfetto.CpuFreq#${cpu}`,
+          displayName: `Cpu ${cpu} Frequency`,
+          kind: CPU_FREQ_TRACK_KIND,
+          cpu,
+          track: ({trackInstanceId}) => {
+            return new TrackWithControllerAdapter<Config, Data>(
+                engine,
+                trackInstanceId,
+                {
+                  cpu,
+                  maximumValue: maxCpuFreq,
+                  freqTrackId,
+                  idleTrackId,
+                },
+                CpuFreqTrack,
+                CpuFreqTrackController);
+          },
+        });
+      }
+    }
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CpuFreq',
   plugin: CpuFreq,
 };
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index 7045ac0..616e9a2 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -25,8 +25,8 @@
 import {globals} from '../../frontend/globals';
 import {cachedHsluvToHex} from '../../frontend/hsluv_cache';
 import {TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 const BAR_HEIGHT = 3;
 const MARGIN_TOP = 4.5;
@@ -85,7 +85,7 @@
   return cachedHsluvToHex(hue, saturation, lightness);
 }
 
-class CpuProfileTrack extends Track<Config, Data> {
+class CpuProfileTrack extends TrackBase<Config, Data> {
   static readonly kind = CPU_PROFILE_TRACK_KIND;
   static create(args: NewTrackArgs): CpuProfileTrack {
     return new CpuProfileTrack(args);
@@ -246,12 +246,12 @@
 
 class CpuProfile implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(CpuProfileTrackController);
-    ctx.registerTrack(CpuProfileTrack);
+    ctx.LEGACY_registerTrackController(CpuProfileTrackController);
+    ctx.LEGACY_registerTrack(CpuProfileTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CpuProfile',
   plugin: CpuProfile,
 };
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 6694ccd..c2de3f6 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -39,8 +39,8 @@
   EngineProxy,
   Plugin,
   PluginContext,
-  PluginInfo,
-  TracePluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
 } from '../../public';
 
 export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
@@ -472,7 +472,7 @@
     // No-op
   }
 
-  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const cpus = await ctx.engine.getCpus();
     const cpuToSize = await this.guessCpuSizes(ctx.engine);
 
@@ -484,11 +484,9 @@
       ctx.addTrack({
         uri,
         displayName: name,
-        tags: {
-          cpu,
-          kind: CPU_SLICE_TRACK_KIND,
-        },
-        trackFactory: ({trackInstanceId}) => {
+        kind: CPU_SLICE_TRACK_KIND,
+        cpu,
+        track: ({trackInstanceId}) => {
           return new TrackWithControllerAdapter<Config, Data>(
               ctx.engine,
               trackInstanceId,
@@ -525,7 +523,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CpuSlices',
   plugin: CpuSlices,
 };
diff --git a/ui/src/tracks/custom_sql_table_slices/index.ts b/ui/src/tracks/custom_sql_table_slices/index.ts
index e371bff..8ffe388 100644
--- a/ui/src/tracks/custom_sql_table_slices/index.ts
+++ b/ui/src/tracks/custom_sql_table_slices/index.ts
@@ -27,7 +27,7 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export interface CustomSqlTableDefConfig {
   // Table name
@@ -109,7 +109,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CustomSqlTrack',
   plugin: CustomSqlTrackPlugin,
 };
diff --git a/ui/src/tracks/debug/index.ts b/ui/src/tracks/debug/index.ts
index b3195e1..78c12e7 100644
--- a/ui/src/tracks/debug/index.ts
+++ b/ui/src/tracks/debug/index.ts
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 import {DebugTrackV2} from './slice_track';
 
 class DebugTrackPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(DebugTrackV2);
+    ctx.LEGACY_registerTrack(DebugTrackV2);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.DebugSlices',
   plugin: DebugTrackPlugin,
 };
diff --git a/ui/src/tracks/expected_frames/index.ts b/ui/src/tracks/expected_frames/index.ts
index dce354c..b30d31e 100644
--- a/ui/src/tracks/expected_frames/index.ts
+++ b/ui/src/tracks/expected_frames/index.ts
@@ -16,7 +16,7 @@
 
 export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
 
-import {NewTrackArgs, Track} from '../../frontend/track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
 import {ChromeSliceTrack} from '../chrome_slices';
 
 import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
@@ -24,7 +24,7 @@
 import {
   TrackController,
 } from '../../controller/track_controller';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 import {BigintMath as BIMath} from '../../base/bigint_math';
 
 export interface Config {
@@ -142,19 +142,19 @@
 
 export class ExpectedFramesSliceTrack extends ChromeSliceTrack {
   static readonly kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new ExpectedFramesSliceTrack(args);
   }
 }
 
 class ExpectedFramesPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(ExpectedFramesSliceTrackController);
-    ctx.registerTrack(ExpectedFramesSliceTrack);
+    ctx.LEGACY_registerTrackController(ExpectedFramesSliceTrackController);
+    ctx.LEGACY_registerTrack(ExpectedFramesSliceTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ExpectedFrames',
   plugin: ExpectedFramesPlugin,
 };
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index 124696e..f2ad696 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -23,10 +23,11 @@
   EngineProxy,
   Plugin,
   PluginContext,
-  PluginInfo,
-  TracePluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
 } from '../../public';
 
+export const FTRACE_RAW_TRACK_KIND = 'FtraceRawTrack';
 
 export interface Data extends TrackData {
   timestamps: BigInt64Array;
@@ -138,7 +139,7 @@
 class FtraceRawPlugin implements Plugin {
   onActivate(_: PluginContext) {}
 
-  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const cpus = await this.lookupCpuCores(ctx.engine);
     for (const cpuNum of cpus) {
       const uri = `perfetto.FtraceRaw#cpu${cpuNum}`;
@@ -146,7 +147,9 @@
       ctx.addTrack({
         uri,
         displayName: `Ftrace Track for CPU ${cpuNum}`,
-        trackFactory: () => {
+        kind: FTRACE_RAW_TRACK_KIND,
+        cpu: cpuNum,
+        track: () => {
           return new FtraceRawTrack(ctx.engine, cpuNum);
         },
       });
@@ -169,7 +172,7 @@
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.FtraceRaw',
   plugin: FtraceRawPlugin,
 };
diff --git a/ui/src/tracks/generic_slice_track/index.ts b/ui/src/tracks/generic_slice_track/index.ts
index c467254..6053621 100644
--- a/ui/src/tracks/generic_slice_track/index.ts
+++ b/ui/src/tracks/generic_slice_track/index.ts
@@ -17,7 +17,7 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export interface GenericSliceTrackConfig {
   sqlTrackId: number;
@@ -47,11 +47,11 @@
 
 class GenericSliceTrackPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(GenericSliceTrack);
+    ctx.LEGACY_registerTrack(GenericSliceTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.GenericSliceTrack',
   plugin: GenericSliceTrackPlugin,
 };
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index 08018ce..38b19e7 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -23,8 +23,8 @@
 import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
 import {globals} from '../../frontend/globals';
 import {TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
 
@@ -88,7 +88,7 @@
 const MARGIN_TOP = 4.5;
 const RECT_HEIGHT = 30.5;
 
-class HeapProfileTrack extends Track<Config, Data> {
+class HeapProfileTrack extends TrackBase<Config, Data> {
   static readonly kind = HEAP_PROFILE_TRACK_KIND;
   static create(args: NewTrackArgs): HeapProfileTrack {
     return new HeapProfileTrack(args);
@@ -217,12 +217,12 @@
 
 class HeapProfilePlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(HeapProfileTrackController);
-    ctx.registerTrack(HeapProfileTrack);
+    ctx.LEGACY_registerTrackController(HeapProfileTrackController);
+    ctx.LEGACY_registerTrack(HeapProfileTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.HeapProfile',
   plugin: HeapProfilePlugin,
 };
diff --git a/ui/src/tracks/null_track/index.ts b/ui/src/tracks/null_track/index.ts
index 1ff53a4..a007185 100644
--- a/ui/src/tracks/null_track/index.ts
+++ b/ui/src/tracks/null_track/index.ts
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const NULL_TRACK_KIND = 'NullTrack';
 
-export class NullTrack extends Track {
+export class NullTrack extends TrackBase {
   static readonly kind = NULL_TRACK_KIND;
   constructor(args: NewTrackArgs) {
     super(args);
@@ -37,11 +37,11 @@
 
 class NullTrackPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(NullTrack);
+    ctx.LEGACY_registerTrack(NullTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.NullTrack',
   plugin: NullTrackPlugin,
 };
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index ff836ad..b6e624a 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -22,8 +22,8 @@
 import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
 import {globals} from '../../frontend/globals';
 import {TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
 
@@ -77,7 +77,7 @@
 const MARGIN_TOP = 4.5;
 const RECT_HEIGHT = 30.5;
 
-class PerfSamplesProfileTrack extends Track<Config, Data> {
+class PerfSamplesProfileTrack extends TrackBase<Config, Data> {
   static readonly kind = PERF_SAMPLES_PROFILE_TRACK_KIND;
   static create(args: NewTrackArgs): PerfSamplesProfileTrack {
     return new PerfSamplesProfileTrack(args);
@@ -210,12 +210,12 @@
 
 class PerfSamplesProfilePlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(PerfSamplesProfileTrackController);
-    ctx.registerTrack(PerfSamplesProfileTrack);
+    ctx.LEGACY_registerTrackController(PerfSamplesProfileTrackController);
+    ctx.LEGACY_registerTrack(PerfSamplesProfileTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.PerfSamplesProfile',
   plugin: PerfSamplesProfilePlugin,
 };
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index ea795a4..24cae70 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -12,216 +12,341 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath} from '../../base/bigint_math';
-import {assertFalse} from '../../base/logging';
-import {duration, Time, time} from '../../base/time';
-import {colorForTid} from '../../common/colorizer';
-import {NUM} from '../../common/query_result';
-import {LIMIT, TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {v4 as uuidv4} from 'uuid';
 
-export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
+import {
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+} from '../../common/query_result';
+import {TrackWithControllerAdapter} from '../../common/track_adapter';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
 
-// TODO(dproy): Consider deduping with CPU summary data.
-export interface Data extends TrackData {
-  bucketSize: duration;
-  utilizations: Float64Array;
-}
+import {
+  Config as ProcessSchedulingTrackConfig,
+  Data as ProcessSchedulingTrackData,
+  PROCESS_SCHEDULING_TRACK_KIND,
+  ProcessSchedulingTrack,
+  ProcessSchedulingTrackController,
+} from './process_scheduling_track';
+import {
+  Config as ProcessSummaryTrackConfig,
+  Data as ProcessSummaryTrackData,
+  PROCESS_SUMMARY_TRACK,
+  ProcessSummaryTrack,
+  ProcessSummaryTrackController,
+} from './process_summary_track';
 
-export interface Config {
-  pidForColor: number;
-  upid: number|null;
-  utid: number;
-}
+// This plugin now manages both process "scheduling" and "summary" tracks.
+class ProcessSummaryPlugin implements Plugin {
+  private upidToUuid = new Map<number, string>();
+  private utidToUuid = new Map<number, string>();
 
-// This is the summary displayed when a process only contains chrome slices
-// and no cpu scheduling.
-class ProcessSummaryTrackController extends TrackController<Config, Data> {
-  static readonly kind = PROCESS_SUMMARY_TRACK;
-  private setup = false;
+  onActivate(_ctx: PluginContext): void {}
 
-  async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
-    assertFalse(resolution === 0n, 'Resolution cannot be 0');
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    await this.addProcessTrackGroups(ctx);
+    await this.addKernelThreadSummary(ctx);
+  }
 
-    if (this.setup === false) {
-      await this.query(
-          `create virtual table ${this.tableName('window')} using window;`);
+  private async addProcessTrackGroups(ctx: PluginContextTrace): Promise<void> {
+    this.upidToUuid.clear();
+    this.utidToUuid.clear();
 
-      let utids = [this.config.utid];
-      if (this.config.upid) {
-        const threadQuery = await this.query(
-            `select utid from thread where upid=${this.config.upid}`);
-        utids = [];
-        for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
-          utids.push(it.utid);
+    // We want to create groups of tracks in a specific order.
+    // The tracks should be grouped:
+    //    by upid
+    //    or (if upid is null) by utid
+    // the groups should be sorted by:
+    //  Chrome-based process rank based on process names (e.g. Browser)
+    //  has a heap profile or not
+    //  total cpu time *for the whole parent process*
+    //  process name
+    //  upid
+    //  thread name
+    //  utid
+    const result = await ctx.engine.query(`
+    select
+      the_tracks.upid,
+      the_tracks.utid,
+      total_dur as hasSched,
+      hasHeapProfiles,
+      process.pid as pid,
+      thread.tid as tid,
+      process.name as processName,
+      thread.name as threadName,
+      package_list.debuggable as isDebuggable,
+      ifnull((
+        select group_concat(string_value)
+        from args
+        where
+          process.arg_set_id is not null and
+          arg_set_id = process.arg_set_id and
+          flat_key = 'chrome.process_label'
+      ), '') AS chromeProcessLabels,
+      (case process.name
+         when 'Browser' then 3
+         when 'Gpu' then 2
+         when 'Renderer' then 1
+         else 0
+      end) as chromeProcessRank
+    from (
+      select upid, 0 as utid from process_track
+      union
+      select upid, 0 as utid from process_counter_track
+      union
+      select upid, utid from thread_counter_track join thread using(utid)
+      union
+      select upid, utid from thread_track join thread using(utid)
+      union
+      select upid, utid from sched join thread using(utid) group by utid
+      union
+      select upid, 0 as utid from (
+        select distinct upid
+        from perf_sample join thread using (utid) join process using (upid)
+        where callsite_id is not null)
+      union
+      select upid, utid from (
+        select distinct(utid) from cpu_profile_stack_sample
+      ) join thread using(utid)
+      union
+      select distinct(upid) as upid, 0 as utid from heap_profile_allocation
+      union
+      select distinct(upid) as upid, 0 as utid from heap_graph_object
+    ) the_tracks
+    left join (
+      select upid, sum(thread_total_dur) as total_dur
+      from (
+        select utid, sum(dur) as thread_total_dur
+        from sched where dur != -1 and utid != 0
+        group by utid
+      )
+      join thread using (utid)
+      group by upid
+    ) using(upid)
+    left join (
+      select
+        distinct(upid) as upid,
+        true as hasHeapProfiles
+      from heap_profile_allocation
+      union
+      select
+        distinct(upid) as upid,
+        true as hasHeapProfiles
+      from heap_graph_object
+    ) using (upid)
+    left join (
+      select
+        thread.upid as upid,
+        sum(cnt) as perfSampleCount
+      from (
+          select utid, count(*) as cnt
+          from perf_sample where callsite_id is not null
+          group by utid
+      ) join thread using (utid)
+      group by thread.upid
+    ) using (upid)
+    left join (
+      select
+        process.upid as upid,
+        sum(cnt) as sliceCount
+      from (select track_id, count(*) as cnt from slice group by track_id)
+        left join thread_track on track_id = thread_track.id
+        left join thread on thread_track.utid = thread.utid
+        left join process_track on track_id = process_track.id
+        join process on process.upid = thread.upid
+          or process_track.upid = process.upid
+      where process.upid is not null
+      group by process.upid
+    ) using (upid)
+    left join thread using(utid)
+    left join process using(upid)
+    left join package_list using(uid)
+    order by
+      chromeProcessRank desc,
+      hasHeapProfiles desc,
+      perfSampleCount desc,
+      total_dur desc,
+      sliceCount desc,
+      processName asc nulls last,
+      the_tracks.upid asc nulls last,
+      threadName asc nulls last,
+      the_tracks.utid asc nulls last;
+  `);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      pid: NUM_NULL,
+      threadName: STR_NULL,
+      processName: STR_NULL,
+      hasSched: NUM_NULL,
+      hasHeapProfiles: NUM_NULL,
+      isDebuggable: NUM_NULL,
+      chromeProcessLabels: STR,
+    });
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const tid = it.tid;
+      const upid = it.upid;
+      const pid = it.pid;
+      const hasSched = !!it.hasSched;
+      const isDebuggable = !!it.isDebuggable;
+
+      // Group by upid if present else by utid.
+      let pUuid =
+          upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
+      // These should only happen once for each track group.
+      if (pUuid === undefined) {
+        pUuid = this.getOrCreateUuid(utid, upid);
+        const pidForColor = pid || tid || upid || utid || 0;
+        const type = hasSched ? 'schedule' : 'summary';
+        const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
+
+        if (hasSched) {
+          const config: ProcessSchedulingTrackConfig = {
+            pidForColor,
+            upid,
+            utid,
+          };
+
+          ctx.addTrack({
+            uri,
+            displayName: `${upid === null ? tid : pid} schedule`,
+            kind: PROCESS_SCHEDULING_TRACK_KIND,
+            tags: {
+              isDebuggable,
+            },
+            track: ({trackInstanceId}) => {
+              return new TrackWithControllerAdapter<
+                  ProcessSchedulingTrackConfig,
+                  ProcessSchedulingTrackData>(
+                  ctx.engine,
+                  trackInstanceId,
+                  config,
+                  ProcessSchedulingTrack,
+                  ProcessSchedulingTrackController);
+            },
+          });
+        } else {
+          const config: ProcessSummaryTrackConfig = {
+            pidForColor,
+            upid,
+            utid,
+          };
+
+          ctx.addTrack({
+            uri,
+            displayName: `${upid === null ? tid : pid} summary`,
+            kind: PROCESS_SUMMARY_TRACK,
+            tags: {
+              isDebuggable,
+            },
+            track: ({trackInstanceId}) => {
+              return new TrackWithControllerAdapter<
+                  ProcessSummaryTrackConfig,
+                  ProcessSummaryTrackData>(
+                  ctx.engine,
+                  trackInstanceId,
+                  config,
+                  ProcessSummaryTrack,
+                  ProcessSummaryTrackController);
+            },
+          });
         }
       }
-
-      const trackQuery = await this.query(
-          `select id from thread_track where utid in (${utids.join(',')})`);
-      const tracks = [];
-      for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
-        tracks.push(it.id);
-      }
-
-      const processSliceView = this.tableName('process_slice_view');
-      await this.query(
-          `create view ${processSliceView} as ` +
-          // 0 as cpu is a dummy column to perform span join on.
-          `select ts, dur/${utids.length} as dur ` +
-          `from slice s ` +
-          `where depth = 0 and track_id in ` +
-          `(${tracks.join(',')})`);
-      await this.query(`create virtual table ${this.tableName('span')}
-          using span_join(${processSliceView},
-                          ${this.tableName('window')});`);
-      this.setup = true;
     }
-
-    // |resolution| is in ns/px we want # ns for 10px window:
-    // Max value with 1 so we don't end up with resolution 0.
-    const bucketSize = resolution * 10n;
-    const windowStart = Time.quant(start, bucketSize);
-    const windowDur = BigintMath.max(1n, end - windowStart);
-
-    await this.query(`update ${this.tableName('window')} set
-      window_start=${windowStart},
-      window_dur=${windowDur},
-      quantum=${bucketSize}
-      where rowid = 0;`);
-
-    return this.computeSummary(windowStart, end, resolution, bucketSize);
   }
 
-  private async computeSummary(
-      start: time, end: time, resolution: duration,
-      bucketSize: duration): Promise<Data> {
-    const duration = end - start;
-    const numBuckets = Math.min(Number(duration / bucketSize), LIMIT);
+  private async addKernelThreadSummary(ctx: PluginContextTrace): Promise<void> {
+    const {engine} = ctx;
 
-    const query = `select
-      quantum_ts as bucket,
-      sum(dur)/cast(${bucketSize} as float) as utilization
-      from ${this.tableName('span')}
-      group by quantum_ts
-      limit ${LIMIT}`;
+    // Identify kernel threads if this is a linux system trace, and sufficient
+    // process information is available. Kernel threads are identified by being
+    // children of kthreadd (always pid 2).
+    // The query will return the kthreadd process row first, which must exist
+    // for any other kthreads to be returned by the query.
+    // TODO(rsavitski): figure out how to handle the idle process (swapper),
+    // which has pid 0 but appears as a distinct process (with its own comm) on
+    // each cpu. It'd make sense to exclude its thread state track, but still
+    // put process-scoped tracks in this group.
+    const result = await engine.query(`
+      select
+        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
+      from
+        thread t
+        join process p using (upid)
+        left join process parent on (p.parent_upid = parent.upid)
+        join
+          (select true from metadata m
+             where (m.name = 'system_name' and m.str_value = 'Linux')
+           union
+           select 1 from (select true from sched limit 1))
+      where
+        p.pid = 2 or parent.pid = 2
+      order by isKthreadd desc
+    `);
 
-    const summary: Data = {
-      start,
-      end,
-      resolution,
-      length: numBuckets,
-      bucketSize,
-      utilizations: new Float64Array(numBuckets),
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM,
+    });
+
+    // Not applying kernel thread grouping.
+    if (!it.valid()) {
+      return;
+    }
+
+    const config: ProcessSummaryTrackConfig = {
+      pidForColor: 2,
+      upid: it.upid,
+      utid: it.utid,
     };
 
-    const queryRes = await this.query(query);
-    const it = queryRes.iter({bucket: NUM, utilization: NUM});
-    for (; it.valid(); it.next()) {
-      const bucket = it.bucket;
-      if (bucket > numBuckets) {
-        continue;
+    ctx.addTrack({
+      uri: 'perfetto.ProcessSummary#kernel',
+      displayName: `Kernel thread summary`,
+      kind: PROCESS_SUMMARY_TRACK,
+      track: ({trackInstanceId}) => {
+        return new TrackWithControllerAdapter<
+            ProcessSummaryTrackConfig,
+            ProcessSummaryTrackData>(
+            ctx.engine,
+            trackInstanceId,
+            config,
+            ProcessSummaryTrack,
+            ProcessSummaryTrackController);
+      },
+    });
+  }
+
+  private getOrCreateUuid(utid: number, upid: number|null) {
+    let uuid = this.getUuidUnchecked(utid, upid);
+    if (uuid === undefined) {
+      uuid = uuidv4();
+      if (upid === null) {
+        this.utidToUuid.set(utid, uuid);
+      } else {
+        this.upidToUuid.set(upid, uuid);
       }
-      summary.utilizations[bucket] = it.utilization;
     }
-
-    return summary;
+    return uuid;
   }
 
-  onDestroy(): void {
-    if (this.setup) {
-      this.query(`drop table ${this.tableName('window')}`);
-      this.query(`drop table ${this.tableName('span')}`);
-      this.setup = false;
-    }
+  getUuidUnchecked(utid: number, upid: number|null) {
+    return upid === null ? this.utidToUuid.get(utid) :
+                           this.upidToUuid.get(upid);
   }
 }
 
-const MARGIN_TOP = 5;
-const RECT_HEIGHT = 30;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
-
-class ProcessSummaryTrack extends Track<Config, Data> {
-  static readonly kind = PROCESS_SUMMARY_TRACK;
-  static create(args: NewTrackArgs): ProcessSummaryTrack {
-    return new ProcessSummaryTrack(args);
-  }
-
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D): void {
-    const {
-      visibleTimeScale,
-      windowSpan,
-    } = globals.frontendLocalState;
-    const data = this.data();
-    if (data === undefined) return;  // Can't possibly draw anything.
-
-    checkerboardExcept(
-        ctx,
-        this.getHeight(),
-        windowSpan.start,
-        windowSpan.end,
-        visibleTimeScale.timeToPx(data.start),
-        visibleTimeScale.timeToPx(data.end));
-
-    this.renderSummary(ctx, data);
-  }
-
-  // TODO(dproy): Dedup with CPU slices.
-  renderSummary(ctx: CanvasRenderingContext2D, data: Data): void {
-    const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
-    const startPx = windowSpan.start;
-    const bottomY = TRACK_HEIGHT;
-
-    let lastX = startPx;
-    let lastY = bottomY;
-
-    // TODO(hjd): Dedupe this math.
-    const color = colorForTid(this.config.pidForColor);
-    color.l = Math.min(color.l + 10, 60);
-    color.s -= 20;
-
-    ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
-    ctx.beginPath();
-    ctx.moveTo(lastX, lastY);
-    for (let i = 0; i < data.utilizations.length; i++) {
-      // TODO(dproy): Investigate why utilization is > 1 sometimes.
-      const utilization = Math.min(data.utilizations[i], 1);
-      const startTime = Time.fromRaw(BigInt(i) * data.bucketSize + data.start);
-
-      lastX = Math.floor(visibleTimeScale.timeToPx(startTime));
-
-      ctx.lineTo(lastX, lastY);
-      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
-      ctx.lineTo(lastX, lastY);
-    }
-    ctx.lineTo(lastX, bottomY);
-    ctx.closePath();
-    ctx.fill();
-  }
-}
-
-class ProcessSummaryPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(ProcessSummaryTrack);
-    ctx.registerTrackController(ProcessSummaryTrackController);
-  }
-}
-
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ProcessSummary',
   plugin: ProcessSummaryPlugin,
 };
diff --git a/ui/src/tracks/process_scheduling/index.ts b/ui/src/tracks/process_summary/process_scheduling_track.ts
similarity index 91%
rename from ui/src/tracks/process_scheduling/index.ts
rename to ui/src/tracks/process_summary/process_scheduling_track.ts
index 631998c..8759260 100644
--- a/ui/src/tracks/process_scheduling/index.ts
+++ b/ui/src/tracks/process_summary/process_scheduling_track.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 The Android Open Source Project
+// 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.
@@ -20,16 +20,26 @@
 import {calcCachedBucketSize} from '../../common/cache_utils';
 import {drawTrackHoverTooltip} from '../../common/canvas_utils';
 import {colorForThread} from '../../common/colorizer';
-import {LONG, NUM, QueryResult} from '../../common/query_result';
+import {
+  LONG,
+  NUM,
+  QueryResult,
+} from '../../common/query_result';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+} from '../../common/track_adapter';
 import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
 
 export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
 
+const MARGIN_TOP = 5;
+const RECT_HEIGHT = 30;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
 export interface Data extends TrackData {
   kind: 'slice';
   maxCpu: number;
@@ -49,9 +59,8 @@
 
 // This summary is displayed for any processes that have CPU scheduling activity
 // associated with them.
-class ProcessSchedulingTrackController extends TrackController<Config, Data> {
-  static readonly kind = PROCESS_SCHEDULING_TRACK_KIND;
-
+export class ProcessSchedulingTrackController extends
+    TrackControllerAdapter<Config, Data> {
   private maxCpu = 0;
   private maxDur = 0n;
   private cachedBucketSize = BIMath.INT64_MAX;
@@ -178,12 +187,7 @@
   }
 }
 
-const MARGIN_TOP = 5;
-const RECT_HEIGHT = 30;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-class ProcessSchedulingTrack extends Track<Config, Data> {
-  static readonly kind = PROCESS_SCHEDULING_TRACK_KIND;
+export class ProcessSchedulingTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): ProcessSchedulingTrack {
     return new ProcessSchedulingTrack(args);
   }
@@ -312,15 +316,3 @@
     this.mousePos = undefined;
   }
 }
-
-class ProcessSchedulingPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(ProcessSchedulingTrackController);
-    ctx.registerTrack(ProcessSchedulingTrack);
-  }
-}
-
-export const plugin: PluginInfo = {
-  pluginId: 'perfetto.ProcessScheduling',
-  plugin: ProcessSchedulingPlugin,
-};
diff --git a/ui/src/tracks/process_summary/process_summary_track.ts b/ui/src/tracks/process_summary/process_summary_track.ts
new file mode 100644
index 0000000..4641eb0
--- /dev/null
+++ b/ui/src/tracks/process_summary/process_summary_track.ts
@@ -0,0 +1,208 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath} from '../../base/bigint_math';
+import {assertFalse} from '../../base/logging';
+import {duration, Time, time} from '../../base/time';
+import {colorForTid} from '../../common/colorizer';
+import {NUM} from '../../common/query_result';
+import {TrackAdapter, TrackControllerAdapter} from '../../common/track_adapter';
+import {LIMIT, TrackData} from '../../common/track_data';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {NewTrackArgs} from '../../frontend/track';
+
+export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
+
+// TODO(dproy): Consider deduping with CPU summary data.
+export interface Data extends TrackData {
+  bucketSize: duration;
+  utilizations: Float64Array;
+}
+
+export interface Config {
+  pidForColor: number;
+  upid: number|null;
+  utid: number;
+}
+
+// This is the summary displayed when a process only contains chrome slices
+// and no cpu scheduling.
+export class ProcessSummaryTrackController extends
+    TrackControllerAdapter<Config, Data> {
+  async onSetup(): Promise<void> {
+    await this.query(
+        `create virtual table ${this.tableName('window')} using window;`);
+
+    let utids = [this.config.utid];
+    if (this.config.upid) {
+      const threadQuery = await this.query(
+          `select utid from thread where upid=${this.config.upid}`);
+      utids = [];
+      for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
+        utids.push(it.utid);
+      }
+    }
+
+    const trackQuery = await this.query(
+        `select id from thread_track where utid in (${utids.join(',')})`);
+    const tracks = [];
+    for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
+      tracks.push(it.id);
+    }
+
+    const processSliceView = this.tableName('process_slice_view');
+    await this.query(
+        `create view ${processSliceView} as ` +
+        // 0 as cpu is a dummy column to perform span join on.
+        `select ts, dur/${utids.length} as dur ` +
+        `from slice s ` +
+        `where depth = 0 and track_id in ` +
+        `(${tracks.join(',')})`);
+    await this.query(`create virtual table ${this.tableName('span')}
+        using span_join(${processSliceView},
+                        ${this.tableName('window')});`);
+  }
+
+  async onBoundsChange(start: time, end: time, resolution: duration):
+      Promise<Data> {
+    assertFalse(resolution === 0n, 'Resolution cannot be 0');
+
+    // |resolution| is in ns/px we want # ns for 10px window:
+    // Max value with 1 so we don't end up with resolution 0.
+    const bucketSize = resolution * 10n;
+    const windowStart = Time.quant(start, bucketSize);
+    const windowDur = BigintMath.max(1n, end - windowStart);
+
+    await this.query(`update ${this.tableName('window')} set
+      window_start=${windowStart},
+      window_dur=${windowDur},
+      quantum=${bucketSize}
+      where rowid = 0;`);
+
+    return this.computeSummary(windowStart, end, resolution, bucketSize);
+  }
+
+  private async computeSummary(
+      start: time, end: time, resolution: duration,
+      bucketSize: duration): Promise<Data> {
+    const duration = end - start;
+    const numBuckets = Math.min(Number(duration / bucketSize), LIMIT);
+
+    const query = `select
+      quantum_ts as bucket,
+      sum(dur)/cast(${bucketSize} as float) as utilization
+      from ${this.tableName('span')}
+      group by quantum_ts
+      limit ${LIMIT}`;
+
+    const summary: Data = {
+      start,
+      end,
+      resolution,
+      length: numBuckets,
+      bucketSize,
+      utilizations: new Float64Array(numBuckets),
+    };
+
+    const queryRes = await this.query(query);
+    const it = queryRes.iter({bucket: NUM, utilization: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
+      if (bucket > numBuckets) {
+        continue;
+      }
+      summary.utilizations[bucket] = it.utilization;
+    }
+
+    return summary;
+  }
+
+  async onDestroy(): Promise<void> {
+    await this.query(`drop table if exists ${
+        this.tableName(
+            'window')}; drop table if exists ${this.tableName('span')}`);
+  }
+}
+
+const MARGIN_TOP = 5;
+const RECT_HEIGHT = 30;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
+
+export class ProcessSummaryTrack extends TrackAdapter<Config, Data> {
+  static create(args: NewTrackArgs): ProcessSummaryTrack {
+    return new ProcessSummaryTrack(args);
+  }
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D): void {
+    const {
+      visibleTimeScale,
+      windowSpan,
+    } = globals.frontendLocalState;
+    const data = this.data();
+    if (data === undefined) return;  // Can't possibly draw anything.
+
+    checkerboardExcept(
+        ctx,
+        this.getHeight(),
+        windowSpan.start,
+        windowSpan.end,
+        visibleTimeScale.timeToPx(data.start),
+        visibleTimeScale.timeToPx(data.end));
+
+    this.renderSummary(ctx, data);
+  }
+
+  // TODO(dproy): Dedup with CPU slices.
+  renderSummary(ctx: CanvasRenderingContext2D, data: Data): void {
+    const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
+    const startPx = windowSpan.start;
+    const bottomY = TRACK_HEIGHT;
+
+    let lastX = startPx;
+    let lastY = bottomY;
+
+    // TODO(hjd): Dedupe this math.
+    const color = colorForTid(this.config.pidForColor);
+    color.l = Math.min(color.l + 10, 60);
+    color.s -= 20;
+
+    ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
+    ctx.beginPath();
+    ctx.moveTo(lastX, lastY);
+    for (let i = 0; i < data.utilizations.length; i++) {
+      // TODO(dproy): Investigate why utilization is > 1 sometimes.
+      const utilization = Math.min(data.utilizations[i], 1);
+      const startTime = Time.fromRaw(BigInt(i) * data.bucketSize + data.start);
+
+      lastX = Math.floor(visibleTimeScale.timeToPx(startTime));
+
+      ctx.lineTo(lastX, lastY);
+      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
+      ctx.lineTo(lastX, lastY);
+    }
+    ctx.lineTo(lastX, bottomY);
+    ctx.closePath();
+    ctx.fill();
+  }
+}
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index c1eb5ed..cc85c16 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.ts
@@ -16,12 +16,16 @@
 
 import {AddTrackArgs} from '../../common/actions';
 import {Engine} from '../../common/engine';
-import {PrimaryTrackSortKey} from '../../common/state';
 import {
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginDescriptor,
+  PrimaryTrackSortKey,
+} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -36,7 +40,7 @@
 
 class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
   static readonly kind = 'dev.perfetto.ScreenshotsTrack';
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new ScreenshotsTrack(args);
   }
 
@@ -84,11 +88,11 @@
 
 class ScreenshotsPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(ScreenshotsTrack);
+    ctx.LEGACY_registerTrack(ScreenshotsTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.Screenshots',
   plugin: ScreenshotsPlugin,
 };
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index b684e6c..807f8f9 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -25,8 +25,8 @@
 import {TrackController} from '../../controller/track_controller';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 
 export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
@@ -162,7 +162,7 @@
 const RECT_HEIGHT = 12;
 const EXCESS_WIDTH = 10;
 
-class ThreadStateTrack extends Track<Config, Data> {
+class ThreadStateTrack extends TrackBase<Config, Data> {
   static readonly kind = THREAD_STATE_TRACK_KIND;
   static create(args: NewTrackArgs): ThreadStateTrack {
     return new ThreadStateTrack(args);
@@ -285,12 +285,12 @@
 
 class ThreadState implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(ThreadStateTrack);
-    ctx.registerTrackController(ThreadStateTrackController);
+    ctx.LEGACY_registerTrack(ThreadStateTrack);
+    ctx.LEGACY_registerTrackController(ThreadStateTrackController);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ThreadState',
   plugin: ThreadState,
 };
diff --git a/ui/src/tracks/thread_state_v2/index.ts b/ui/src/tracks/thread_state_v2/index.ts
index ce78b78..9c465b6 100644
--- a/ui/src/tracks/thread_state_v2/index.ts
+++ b/ui/src/tracks/thread_state_v2/index.ts
@@ -29,7 +29,7 @@
   SliceLayout,
 } from '../../frontend/slice_layout';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const THREAD_STATE_ROW = {
   ...BASE_SLICE_ROW,
@@ -126,11 +126,11 @@
 
 class ThreadStateTrackV2 implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(ThreadStateTrack);
+    ctx.LEGACY_registerTrack(ThreadStateTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ThreadStateTrackV2',
   plugin: ThreadStateTrackV2,
 };
diff --git a/ui/src/tracks/visualised_args/index.ts b/ui/src/tracks/visualised_args/index.ts
index abee625..3385bf9 100644
--- a/ui/src/tracks/visualised_args/index.ts
+++ b/ui/src/tracks/visualised_args/index.ts
@@ -16,9 +16,9 @@
 
 import {Actions} from '../../common/actions';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
 import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 import {
   ChromeSliceTrack,
   ChromeSliceTrackController,
@@ -41,7 +41,7 @@
 
 export class VisualisedArgsTrack extends ChromeSliceTrack {
   static readonly kind = VISUALISED_ARGS_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): Track {
+  static create(args: NewTrackArgs): TrackBase {
     return new VisualisedArgsTrack(args);
   }
 
@@ -67,12 +67,12 @@
 
 class VisualisedArgsPlugin implements Plugin {
   onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(VisualisedArgsTrackController);
-    ctx.registerTrack(VisualisedArgsTrack);
+    ctx.LEGACY_registerTrackController(VisualisedArgsTrackController);
+    ctx.LEGACY_registerTrack(VisualisedArgsTrack);
   }
 }
 
-export const plugin: PluginInfo = {
+export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.VisualisedArgs',
   plugin: VisualisedArgsPlugin,
 };