Merge "ui: show slice aggregation view even when pivot table is available" into main
diff --git a/Android.bp b/Android.bp
index d60a652..3dd9d0d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2412,6 +2412,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -2512,6 +2514,7 @@
     shared_libs: [
         "heapprofd_client_api",
         "libbase",
+        "libexpat",
         "libicu",
         "liblog",
         "libprocinfo",
@@ -12244,6 +12247,7 @@
         "src/trace_processor/importers/common/flow_tracker.cc",
         "src/trace_processor/importers/common/global_args_tracker.cc",
         "src/trace_processor/importers/common/jit_cache.cc",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc",
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/mapping_tracker.cc",
         "src/trace_processor/importers/common/metadata_tracker.cc",
@@ -12407,6 +12411,21 @@
     ],
 }
 
+// GN: //src/trace_processor/importers/instruments:instruments
+filegroup {
+    name: "perfetto_src_trace_processor_importers_instruments_instruments",
+    srcs: [
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc",
+        "src/trace_processor/importers/instruments/row_data_tracker.cc",
+        "src/trace_processor/importers/instruments/row_parser.cc",
+    ],
+}
+
+// GN: //src/trace_processor/importers/instruments:row
+filegroup {
+    name: "perfetto_src_trace_processor_importers_instruments_row",
+}
+
 // GN: //src/trace_processor/importers/json:full
 filegroup {
     name: "perfetto_src_trace_processor_importers_json_full",
@@ -12686,7 +12705,6 @@
     srcs: [
         "src/trace_processor/importers/proto/winscope/android_input_event_parser.cc",
         "src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc",
-        "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc",
         "src/trace_processor/importers/proto/winscope/protolog_parser.cc",
         "src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc",
         "src/trace_processor/importers/proto/winscope/shell_transitions_tracker.cc",
@@ -15310,6 +15328,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_unittests",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -15449,6 +15469,7 @@
     ],
     shared_libs: [
         "libbase",
+        "libexpat",
         "libicu",
         "liblog",
         "libprocinfo",
@@ -16284,8 +16305,11 @@
         ":perfetto_include_perfetto_ext_traced_sys_stats_counters",
         ":perfetto_include_perfetto_protozero_protozero",
         ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_abi_public",
         ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protos_protos",
         ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_public_public",
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
@@ -16359,6 +16383,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -16492,6 +16518,7 @@
     target: {
         android: {
             shared_libs: [
+                "libexpat",
                 "libicu",
                 "liblog",
                 "libprotobuf-cpp-full",
@@ -16505,6 +16532,7 @@
         },
         host: {
             static_libs: [
+                "libexpat",
                 "libprotobuf-cpp-full",
                 "libsqlite_static_noicu",
                 "libz",
@@ -16589,6 +16617,7 @@
         ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_perf_record",
@@ -16689,8 +16718,11 @@
         ":perfetto_include_perfetto_profiling_pprof_builder",
         ":perfetto_include_perfetto_protozero_protozero",
         ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_abi_public",
         ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protos_protos",
         ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_public_public",
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
@@ -16762,6 +16794,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -16821,6 +16855,7 @@
         ":perfetto_src_traceconv_utils",
     ],
     static_libs: [
+        "libexpat",
         "libsqlite_static_noicu",
         "libz",
         "perfetto_src_trace_processor_demangle",
diff --git a/BUILD b/BUILD
index ed3355b..5adc8df 100644
--- a/BUILD
+++ b/BUILD
@@ -236,6 +236,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -305,8 +307,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -367,7 +372,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
@@ -1530,6 +1536,8 @@
         "src/trace_processor/importers/common/global_args_tracker.h",
         "src/trace_processor/importers/common/jit_cache.cc",
         "src/trace_processor/importers/common/jit_cache.h",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h",
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/machine_tracker.h",
         "src/trace_processor/importers/common/mapping_tracker.cc",
@@ -1707,6 +1715,28 @@
     ],
 )
 
+# GN target: //src/trace_processor/importers/instruments:instruments
+perfetto_filegroup(
+    name = "src_trace_processor_importers_instruments_instruments",
+    srcs = [
+        "src/trace_processor/importers/instruments/instruments_utils.h",
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc",
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h",
+        "src/trace_processor/importers/instruments/row_data_tracker.cc",
+        "src/trace_processor/importers/instruments/row_data_tracker.h",
+        "src/trace_processor/importers/instruments/row_parser.cc",
+        "src/trace_processor/importers/instruments/row_parser.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/instruments:row
+perfetto_filegroup(
+    name = "src_trace_processor_importers_instruments_row",
+    srcs = [
+        "src/trace_processor/importers/instruments/row.h",
+    ],
+)
+
 # GN target: //src/trace_processor/importers/json:full
 perfetto_filegroup(
     name = "src_trace_processor_importers_json_full",
@@ -1801,8 +1831,6 @@
         "src/trace_processor/importers/proto/winscope/android_input_event_parser.h",
         "src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc",
         "src/trace_processor/importers/proto/winscope/protolog_message_decoder.h",
-        "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc",
-        "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h",
         "src/trace_processor/importers/proto/winscope/protolog_parser.cc",
         "src/trace_processor/importers/proto/winscope/protolog_parser.h",
         "src/trace_processor/importers/proto/winscope/shell_transitions_parser.cc",
@@ -6232,6 +6260,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -6298,8 +6328,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6362,7 +6395,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
@@ -6384,8 +6418,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6414,6 +6451,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -6535,7 +6574,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.linenoise +
            PERFETTO_CONFIG.deps.protobuf_full +
            PERFETTO_CONFIG.deps.sqlite +
@@ -6626,8 +6666,11 @@
         ":include_perfetto_profiling_pprof_builder",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6656,6 +6699,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -6778,7 +6823,8 @@
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
                ":src_traceconv_gen_cc_trace_descriptor",
                ":src_traceconv_gen_cc_winscope_descriptor",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
diff --git a/bazel/deps.bzl b/bazel/deps.bzl
index 69dbcbb..d97179a 100644
--- a/bazel/deps.bzl
+++ b/bazel/deps.bzl
@@ -67,6 +67,14 @@
     )
 
     _add_repo_if_not_existing(
+        new_git_repository,
+        name = "perfetto_dep_expat",
+        remote = "https://github.com/libexpat/libexpat",
+        commit = "fa75b96546c069d17b8f80d91e0f4ef0cde3790d",  # R_2_6_2
+        build_file = "//bazel:expat.BUILD",
+    )
+
+    _add_repo_if_not_existing(
         http_archive,
         name = "perfetto_dep_zlib",
         url = "https://storage.googleapis.com/perfetto/zlib-6d3f6aa0f87c9791ca7724c279ef61384f331dfd.tar.gz",
diff --git a/bazel/expat.BUILD b/bazel/expat.BUILD
new file mode 100644
index 0000000..10f88c7
--- /dev/null
+++ b/bazel/expat.BUILD
@@ -0,0 +1,64 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@perfetto_cfg//:perfetto_cfg.bzl", "PERFETTO_CONFIG")
+
+cc_library(
+    name = "expat",
+    hdrs = glob(["expat/lib/*.h"]),
+    deps = [
+        ":expat_impl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "expat_impl",
+    srcs = [
+      "expat/lib/xmlparse.c",
+      "expat/lib/xmlrole.c",
+      "expat/lib/xmltok.c",
+    ],
+    hdrs = [
+      "expat/lib/ascii.h",
+      "expat/lib/asciitab.h",
+      "expat/lib/expat.h",
+      "expat/lib/expat_external.h",
+      "expat/lib/iasciitab.h",
+      "expat/lib/internal.h",
+      "expat/lib/latin1tab.h",
+      "expat/lib/nametab.h",
+      "expat/lib/siphash.h",
+      "expat/lib/utf8tab.h",
+      "expat/lib/winconfig.h",
+      "expat/lib/xmlrole.h",
+      "expat/lib/xmltok.h",
+      "expat/lib/xmltok_impl.c",
+      "expat/lib/xmltok_impl.h",
+      "expat/lib/xmltok_ns.c",
+    ],
+    deps = [
+        "@perfetto//buildtools/expat/include:expat_config",
+    ],
+    copts = [
+        "-DHAVE_EXPAT_CONFIG_H",
+    ] + PERFETTO_CONFIG.deps_copts.expat,
+    defines = [
+        "XML_STATIC"
+    ],
+    includes = [
+        "expat",
+        "expat/lib",
+    ],
+)
diff --git a/bazel/standalone/perfetto_cfg.bzl b/bazel/standalone/perfetto_cfg.bzl
index cbabb29..254bcb8 100644
--- a/bazel/standalone/perfetto_cfg.bzl
+++ b/bazel/standalone/perfetto_cfg.bzl
@@ -45,6 +45,7 @@
         base_platform = ["//:perfetto_base_default_platform"],
 
         zlib = ["@perfetto_dep_zlib//:zlib"],
+        expat = ["@perfetto_dep_expat//:expat"],
         jsoncpp = ["@perfetto_dep_jsoncpp//:jsoncpp"],
         linenoise = ["@perfetto_dep_linenoise//:linenoise"],
         sqlite = ["@perfetto_dep_sqlite//:sqlite"],
@@ -83,6 +84,7 @@
     # initialized with the Perfetto build files (i.e. via perfetto_deps()).
     deps_copts = struct(
         zlib = [],
+        expat = [],
         jsoncpp = [],
         linenoise = [],
         sqlite = [],
diff --git a/buildtools/.gitignore b/buildtools/.gitignore
index a3e6376..42137c3 100644
--- a/buildtools/.gitignore
+++ b/buildtools/.gitignore
@@ -11,6 +11,7 @@
 /catapult_trace_viewer/
 /clang_format/
 /clang/
+/expat/src/
 /d8/
 /debian_sid_arm-sysroot/
 /debian_sid_arm64-sysroot/
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index b58b678..54196e2 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -1402,6 +1402,43 @@
   deps = [ "//gn:default_deps" ]
 }
 
+config("no_format_warning") {
+  cflags = [ "-Wno-format" ]
+}
+
+if (enable_perfetto_trace_processor_mac_instruments) {
+  config("expat_public_config") {
+    defines = [ "XML_STATIC" ]
+    cflags = [
+      # Using -isystem instead of include_dirs (-I), so we don't need to
+      # suppress warnings coming from third-party headers. Doing so would mask
+      # warnings in our own code.
+      perfetto_isystem_cflag,
+      rebase_path("expat/src/expat/lib", root_build_dir),
+      perfetto_isystem_cflag,
+      rebase_path("expat/include", root_build_dir),
+    ]
+  }
+
+  source_set("expat") {
+    sources = [
+      "expat/src/expat/lib/expat.h",
+      "expat/src/expat/lib/xmlparse.c",
+      "expat/src/expat/lib/xmlrole.c",
+      "expat/src/expat/lib/xmltok.c",
+    ]
+
+    public_configs = [ ":expat_public_config" ]
+    configs -= [ "//gn/standalone:extra_warnings" ]
+    configs += [ ":no_format_warning" ]
+
+    defines = [
+      "_LIB",
+      "HAVE_EXPAT_CONFIG_H",
+    ]
+  }
+}
+
 config("linenoise_config") {
   visibility = _buildtools_visibility
   cflags = [
diff --git a/buildtools/expat/include/BUILD b/buildtools/expat/include/BUILD
new file mode 100644
index 0000000..1c6cb8f
--- /dev/null
+++ b/buildtools/expat/include/BUILD
@@ -0,0 +1,24 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+cc_library(
+    name = "expat_config",
+    hdrs = [
+      "expat_config.h",
+    ],
+    includes = [
+        ".",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/buildtools/expat/include/expat_config.h b/buildtools/expat/include/expat_config.h
new file mode 100644
index 0000000..8ce2388
--- /dev/null
+++ b/buildtools/expat/include/expat_config.h
@@ -0,0 +1,146 @@
+/* expat_config.h.  Generated from expat_config.h.in by configure.  */
+/* expat_config.h.in.  Generated from configure.ac by autoheader.  */
+
+#ifndef EXPAT_CONFIG_H
+#define EXPAT_CONFIG_H 1
+
+/* Define if building universal (internal helper macro) */
+/* #undef AC_APPLE_UNIVERSAL_BUILD */
+
+/* 1234 = LILENDIAN, 4321 = BIGENDIAN */
+#define BYTEORDER 1234
+
+/* Define to 1 if you have the `arc4random' function. */
+/* #undef HAVE_ARC4RANDOM */
+
+/* Define to 1 if you have the `arc4random_buf' function. */
+/* #define HAVE_ARC4RANDOM_BUF 1 */
+
+/* define if the compiler supports basic C++11 syntax */
+#define HAVE_CXX11 1
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#define HAVE_DLFCN_H 1
+
+/* Define to 1 if you have the <fcntl.h> header file. */
+#define HAVE_FCNTL_H 1
+
+/* Define to 1 if you have the `getpagesize' function. */
+#define HAVE_GETPAGESIZE 1
+
+/* Define to 1 if you have the `getrandom' function. */
+/* #define HAVE_GETRANDOM 1 */
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#define HAVE_INTTYPES_H 1
+
+/* Define to 1 if you have the `bsd' library (-lbsd). */
+/* #undef HAVE_LIBBSD */
+
+/* Define to 1 if you have a working `mmap' system call. */
+#define HAVE_MMAP 1
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#define HAVE_STDINT_H 1
+
+/* Define to 1 if you have the <stdio.h> header file. */
+#define HAVE_STDIO_H 1
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#define HAVE_STDLIB_H 1
+
+/* Define to 1 if you have the <strings.h> header file. */
+#define HAVE_STRINGS_H 1
+
+/* Define to 1 if you have the <string.h> header file. */
+#define HAVE_STRING_H 1
+
+/* Define to 1 if you have `syscall' and `SYS_getrandom'. */
+/* #define HAVE_SYSCALL_GETRANDOM 1 */
+
+/* Define to 1 if you have the <sys/param.h> header file. */
+#define HAVE_SYS_PARAM_H 1
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#define HAVE_SYS_STAT_H 1
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#define HAVE_SYS_TYPES_H 1
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#define HAVE_UNISTD_H 1
+
+/* Define to the sub-directory where libtool stores uninstalled libraries. */
+#define LT_OBJDIR ".libs/"
+
+/* Name of package */
+#define PACKAGE "expat"
+
+/* Define to the address where bug reports for this package should be sent. */
+#define PACKAGE_BUGREPORT "https://github.com/libexpat/libexpat/issues"
+
+/* Define to the full name of this package. */
+#define PACKAGE_NAME "expat"
+
+/* Define to the full name and version of this package. */
+#define PACKAGE_STRING "expat 2.6.2"
+
+/* Define to the one symbol short name of this package. */
+#define PACKAGE_TARNAME "expat"
+
+/* Define to the home page for this package. */
+#define PACKAGE_URL ""
+
+/* Define to the version of this package. */
+#define PACKAGE_VERSION "2.6.2"
+
+/* Define to 1 if all of the C90 standard headers exist (not just the ones
+   required in a freestanding environment). This macro is provided for
+   backward compatibility; new code need not use it. */
+#define STDC_HEADERS 1
+
+/* Version number of package */
+#define VERSION "2.6.2"
+
+/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
+   significant byte first (like Motorola and SPARC, unlike Intel). */
+#if defined AC_APPLE_UNIVERSAL_BUILD
+#if defined __BIG_ENDIAN__
+#define WORDS_BIGENDIAN 1
+#endif
+#else
+#ifndef WORDS_BIGENDIAN
+/* #  undef WORDS_BIGENDIAN */
+#endif
+#endif
+
+/* Define to allow retrieving the byte offsets for attribute names and values.
+ */
+/* #undef XML_ATTR_INFO */
+
+/* Define to specify how much context to retain around the current parse
+   point, 0 to disable. */
+#define XML_CONTEXT_BYTES 1024
+
+/* Define to include code reading entropy from `/dev/urandom'. */
+#define XML_DEV_URANDOM 1
+
+/* Define to make parameter entity parsing functionality available. */
+#define XML_DTD 1
+
+/* Define as 1/0 to enable/disable support for general entities. */
+#define XML_GE 1
+
+/* Define to make XML Namespaces functionality available. */
+#define XML_NS 1
+
+/* Define to empty if `const' does not conform to ANSI C. */
+/* #undef const */
+
+/* Define to `long int' if <sys/types.h> does not define. */
+/* #undef off_t */
+
+/* Define to `unsigned int' if <sys/types.h> does not define. */
+/* #undef size_t */
+
+#endif  // ndef EXPAT_CONFIG_H
\ No newline at end of file
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index e61fb9f..1bd21b4 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -87,6 +87,7 @@
     "PERFETTO_TP_LINENOISE=$enable_perfetto_trace_processor_linenoise",
     "PERFETTO_TP_HTTPD=$enable_perfetto_trace_processor_httpd",
     "PERFETTO_TP_JSON=$enable_perfetto_trace_processor_json",
+    "PERFETTO_TP_INSTRUMENTS=$enable_perfetto_trace_processor_mac_instruments",
     "PERFETTO_LOCAL_SYMBOLIZER=$perfetto_local_symbolizer",
     "PERFETTO_ZLIB=$enable_perfetto_zlib",
     "PERFETTO_TRACED_PERF=$enable_perfetto_traced_perf",
@@ -363,6 +364,16 @@
   }
 }
 
+if (enable_perfetto_trace_processor_mac_instruments) {
+  group("expat") {
+    if (perfetto_root_path == "//") {
+      public_deps = [ "//buildtools:expat" ]
+    } else {
+      public_deps = [ "//third_party/expat:expat" ]
+    }
+  }
+}
+
 if (enable_perfetto_trace_processor_json) {
   group("jsoncpp") {
     if (perfetto_root_path == "//") {
diff --git a/gn/perfetto.gni b/gn/perfetto.gni
index ce106c5..6e632ec 100644
--- a/gn/perfetto.gni
+++ b/gn/perfetto.gni
@@ -298,6 +298,14 @@
   enable_perfetto_trace_processor_json =
       enable_perfetto_trace_processor && !perfetto_build_with_android
 
+  # Enables the support for importing profiles from the MacOS Instruments app.
+  # Requires a dependency on libexpat for XML parsing.
+  # Disabled in chromium due to some fuzzer related build failure (b/363347029).
+  enable_perfetto_trace_processor_mac_instruments =
+      enable_perfetto_trace_processor &&
+      (perfetto_build_standalone || perfetto_build_with_android ||
+       is_perfetto_build_generator)
+
   # Enables httpd RPC support in the trace processor.
   # Further per-OS conditionals are applied in gn/BUILD.gn.
   # Chromium+Win: httpd support depends on enable_perfetto_ipc, which is not
diff --git a/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h b/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
index c36d04b..52deaa0 100644
--- a/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/android_tree/perfetto_build_flags.h
@@ -38,6 +38,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_LINENOISE() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_HTTPD() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_JSON() (0)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_INSTRUMENTS() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LOCAL_SYMBOLIZER() (PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_MAC() ||PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_WIN())
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ZLIB() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TRACED_PERF() (1)
diff --git a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
index 54fe273..c982321 100644
--- a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
@@ -38,6 +38,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_LINENOISE() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_HTTPD() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_JSON() (1)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TP_INSTRUMENTS() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LOCAL_SYMBOLIZER() (PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_LINUX() || PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_MAC() ||PERFETTO_BUILDFLAG_DEFINE_PERFETTO_OS_WIN())
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ZLIB() (1)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_TRACED_PERF() (0)
diff --git a/include/perfetto/ext/tracing/core/shared_memory.h b/include/perfetto/ext/tracing/core/shared_memory.h
index f2e5274..4773c34 100644
--- a/include/perfetto/ext/tracing/core/shared_memory.h
+++ b/include/perfetto/ext/tracing/core/shared_memory.h
@@ -20,6 +20,7 @@
 #include <stddef.h>
 
 #include <memory>
+#include <utility>
 
 #include "perfetto/base/export.h"
 #include "perfetto/base/platform_handle.h"
@@ -45,7 +46,20 @@
   // this object region when destroyed.
   virtual ~SharedMemory();
 
-  virtual void* start() const = 0;
+  // Read/write and read-only access to underlying buffer. The non-const method
+  // is implemented in terms of the const one so subclasses need only provide a
+  // single implementation; implementing in the opposite order would be unsafe
+  // since subclasses could effectively mutate state from inside a const method.
+  //
+  // N.B. This signature implements "deep const" that ties the constness of this
+  // object to the constness of the underlying buffer, as opposed to "shallow
+  // const" that would have the signature `void* start() const;`; this is less
+  // flexible for callers but prevents corner cases where it's transitively
+  // possible to change this object's state via the controlled memory.
+  void* start() { return const_cast<void*>(std::as_const(*this).start()); }
+  virtual const void* start() const = 0;
+
+
   virtual size_t size() const = 0;
 };
 
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 702cb98..718da72 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -3173,6 +3173,10 @@
   // Polls /sys/devices/system/cpu/cpu*/cpuidle/state* every X ms, if non-zero.
   // This is required to be > 10ms to avoid excessive CPU usage.
   optional uint32 cpuidle_period_ms = 13;
+
+  // Polls device-specific GPU frequency info every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  optional uint32 gpufreq_period_ms = 14;
 }
 
 // End of protos/perfetto/config/sys_stats/sys_stats_config.proto
diff --git a/protos/perfetto/config/sys_stats/sys_stats_config.proto b/protos/perfetto/config/sys_stats/sys_stats_config.proto
index 3466255..fc16456 100644
--- a/protos/perfetto/config/sys_stats/sys_stats_config.proto
+++ b/protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -87,4 +87,8 @@
   // Polls /sys/devices/system/cpu/cpu*/cpuidle/state* every X ms, if non-zero.
   // This is required to be > 10ms to avoid excessive CPU usage.
   optional uint32 cpuidle_period_ms = 13;
+
+  // Polls device-specific GPU frequency info every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  optional uint32 gpufreq_period_ms = 14;
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index a13fdff..ca676b3 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -3173,6 +3173,10 @@
   // Polls /sys/devices/system/cpu/cpu*/cpuidle/state* every X ms, if non-zero.
   // This is required to be > 10ms to avoid excessive CPU usage.
   optional uint32 cpuidle_period_ms = 13;
+
+  // Polls device-specific GPU frequency info every X ms, if non-zero.
+  // This is required to be > 10ms to avoid excessive CPU usage.
+  optional uint32 gpufreq_period_ms = 14;
 }
 
 // End of protos/perfetto/config/sys_stats/sys_stats_config.proto
@@ -14493,7 +14497,11 @@
     repeated CpuIdleStateEntry cpuidle_state_entry = 2;
   }
   repeated CpuIdleState cpuidle_state = 16;
+
+  // Read GPU frequency info on Intel/AMD devices.
+  repeated uint64 gpufreq_mhz = 17;
 }
+
 // End of protos/perfetto/trace/sys_stats/sys_stats.proto
 
 // Begin of protos/perfetto/trace/system_info.proto
diff --git a/protos/perfetto/trace/sys_stats/sys_stats.proto b/protos/perfetto/trace/sys_stats/sys_stats.proto
index d0ad10f..8532d78 100644
--- a/protos/perfetto/trace/sys_stats/sys_stats.proto
+++ b/protos/perfetto/trace/sys_stats/sys_stats.proto
@@ -179,4 +179,7 @@
     repeated CpuIdleStateEntry cpuidle_state_entry = 2;
   }
   repeated CpuIdleState cpuidle_state = 16;
-}
\ No newline at end of file
+
+  // Read GPU frequency info on Intel/AMD devices.
+  repeated uint64 gpufreq_mhz = 17;
+}
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index a3784cc..50e2b2d 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -1448,7 +1448,11 @@
   }
   optional StepName step = 1;
   optional FrameSinkId frame_sink_id = 2;
+
+  // Id used to link `ChromeGraphicsPipeline`s corresponding to work done
+  // on creating and presenting one frame.
   optional int64 display_trace_id = 3;
+
   optional LocalSurfaceId local_surface_id = 4;
   optional int64 frame_sequence = 5;
   optional FrameSkippedReason frame_skipped_reason = 6;
diff --git a/python/perfetto/bigtrace_clickhouse/grpc_client.py b/python/perfetto/bigtrace_clickhouse/grpc_client.py
new file mode 100755
index 0000000..1f68d70
--- /dev/null
+++ b/python/perfetto/bigtrace_clickhouse/grpc_client.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Executable script used by Clickhouse to make gRPC calls to the Orchestrator
+# from a TVF
+
+import grpc
+import sys
+import os
+
+from protos.perfetto.bigtrace.orchestrator_pb2 import BigtraceQueryArgs
+from protos.perfetto.bigtrace.orchestrator_pb2_grpc import BigtraceOrchestratorStub
+from query_result_iterator import QueryResultIterator
+
+
+def main():
+  orchestrator_address = os.environ.get("BIGTRACE_ORCHESTRATOR_ADDRESS")
+
+  for input in sys.stdin:
+    # Clickhouse input is specified as tab separated
+    traces, sql_query = input.rstrip("\n").split("\t")
+    # Convert the string representation of list of traces given by Clickhouse into
+    # a Python list
+    trace_list = [x[1:-1] for x in traces[1:-1].split(',')]
+
+    channel = grpc.insecure_channel(orchestrator_address)
+    stub = BigtraceOrchestratorStub(channel)
+    args = BigtraceQueryArgs(traces=trace_list, sql_query=sql_query)
+
+    responses = stub.Query(args, wait_for_ready=False)
+    for response in responses:
+      repeated_batches = []
+      results = response.result
+      column_names = results[0].column_names
+      for result in results:
+        repeated_batches.extend(result.batch)
+      qr_it = QueryResultIterator(column_names, repeated_batches)
+
+      for row in qr_it:
+        # Retrieve all values from columns and replace nulls with empty string
+        data = [(x if x else "") for x in row.__repr__().values()]
+        # Convert the list to a tab separated format for Clickhouse to ingest
+        data_str = '\t'.join(data)
+        print(data_str + '\n', end='')
+
+    sys.stdout.flush()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/python/perfetto/bigtrace_clickhouse/query_result_iterator.py b/python/perfetto/bigtrace_clickhouse/query_result_iterator.py
new file mode 100644
index 0000000..8fdb56f
--- /dev/null
+++ b/python/perfetto/bigtrace_clickhouse/query_result_iterator.py
@@ -0,0 +1,165 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class PerfettoException(Exception):
+
+  def __init__(self, message):
+    super().__init__(message)
+
+
+# Provides a Python interface to operate on the contents of QueryResult protos
+class QueryResultIterator:
+  # Values of these constants correspond to the QueryResponse message at
+  # protos/perfetto/trace_processor/trace_processor.proto
+  QUERY_CELL_INVALID_FIELD_ID = 0
+  QUERY_CELL_NULL_FIELD_ID = 1
+  QUERY_CELL_VARINT_FIELD_ID = 2
+  QUERY_CELL_FLOAT64_FIELD_ID = 3
+  QUERY_CELL_STRING_FIELD_ID = 4
+  QUERY_CELL_BLOB_FIELD_ID = 5
+
+  # This is the class returned to the user and contains one row of the
+  # resultant query. Each column name is stored as an attribute of this
+  # class, with the value corresponding to the column name and row in
+  # the query results table.
+  class Row(object):
+    # Required for pytype to correctly infer attributes from Row objects
+    _HAS_DYNAMIC_ATTRIBUTES = True
+
+    def __str__(self):
+      return str(self.__dict__)
+
+    def __repr__(self):
+      return self.__dict__
+
+  def __init__(self, column_names, batches):
+    self.__column_names = list(column_names)
+    self.__column_count = 0
+    self.__count = 0
+    self.__cells = []
+    self.__data_lists = [[], [], [], [], [], []]
+    self.__data_lists_index = [0, 0, 0, 0, 0, 0]
+    self.__current_index = 0
+
+    # Iterate over all the batches and collect their
+    # contents into lists based on the type of the batch
+    batch_index = 0
+    while True:
+      # It's possible on some occasions that there are non UTF-8 characters
+      # in the string_cells field. If this is the case, string_cells is
+      # a bytestring which needs to be decoded (but passing ignore so that
+      # we don't fail in decoding).
+      strings_batch_str = batches[batch_index].string_cells
+      try:
+        strings_batch_str = strings_batch_str.decode('utf-8', 'ignore')
+      except AttributeError:
+        # AttributeError can occur when |strings_batch_str| is an str which
+        # happens when everything in it is UTF-8 (protobuf automatically
+        # does the conversion if it can).
+        pass
+
+      # Null-terminated strings in a batch are concatenated
+      # into a single large byte array, so we split on the
+      # null-terminator to get the individual strings
+      strings_batch = strings_batch_str.split('\0')[:-1]
+      self.__data_lists[QueryResultIterator.QUERY_CELL_STRING_FIELD_ID].extend(
+          strings_batch)
+      self.__data_lists[QueryResultIterator.QUERY_CELL_VARINT_FIELD_ID].extend(
+          batches[batch_index].varint_cells)
+      self.__data_lists[QueryResultIterator.QUERY_CELL_FLOAT64_FIELD_ID].extend(
+          batches[batch_index].float64_cells)
+      self.__data_lists[QueryResultIterator.QUERY_CELL_BLOB_FIELD_ID].extend(
+          batches[batch_index].blob_cells)
+      self.__cells.extend(batches[batch_index].cells)
+
+      if batches[batch_index].is_last_batch:
+        break
+
+      batch_index += 1
+
+    # If there are no rows in the query result, don't bother updating the
+    # counts to avoid dealing with / 0 errors.
+    if len(self.__cells) == 0:
+      return
+
+    # The count we collected so far was a count of all individual columns
+    # in the query result, so we divide by the number of columns in a row
+    # to get the number of rows
+    self.__column_count = len(self.__column_names)
+    self.__count = int(len(self.__cells) / self.__column_count)
+
+    # Data integrity check - see that we have the expected amount of cells
+    # for the number of rows that we need to return
+    if len(self.__cells) % self.__column_count != 0:
+      raise PerfettoException("Cell count " + str(len(self.__cells)) +
+                              " is not a multiple of column count " +
+                              str(len(self.__column_names)))
+
+  # To use the query result as a populated Pandas dataframe, this
+  # function must be called directly after calling query inside
+  # TraceProcessor / Bigtrace.
+  def as_pandas_dataframe(self):
+    try:
+      import pandas as pd
+
+      # Populate the dataframe with the query results
+      rows = []
+      for i in range(0, self.__count):
+        row = []
+        base_cell_index = i * self.__column_count
+        for num in range(len(self.__column_names)):
+          col_type = self.__cells[base_cell_index + num]
+          if col_type == QueryResultIterator.QUERY_CELL_INVALID_FIELD_ID:
+            raise PerfettoException('Invalid cell type')
+
+          if col_type == QueryResultIterator.QUERY_CELL_NULL_FIELD_ID:
+            row.append(None)
+          else:
+            col_index = self.__data_lists_index[col_type]
+            self.__data_lists_index[col_type] += 1
+            row.append(self.__data_lists[col_type][col_index])
+        rows.append(row)
+
+      df = pd.DataFrame(rows, columns=self.__column_names)
+      return df.astype(object).where(df.notnull(), None).reset_index(drop=True)
+
+    except ModuleNotFoundError:
+      raise PerfettoException(
+          'Python dependencies missing. Please pip3 install pandas numpy')
+
+  def __len__(self):
+    return self.__count
+
+  def __iter__(self):
+    return self
+
+  def __next__(self):
+    if self.__current_index == self.__count:
+      raise StopIteration
+    result = QueryResultIterator.Row()
+    base_cell_index = self.__current_index * self.__column_count
+    for num, column_name in enumerate(self.__column_names):
+      col_type = self.__cells[base_cell_index + num]
+      if col_type == QueryResultIterator.QUERY_CELL_INVALID_FIELD_ID:
+        raise PerfettoException('Invalid cell type')
+      if col_type != QueryResultIterator.QUERY_CELL_NULL_FIELD_ID:
+        col_index = self.__data_lists_index[col_type]
+        self.__data_lists_index[col_type] += 1
+        setattr(result, column_name, self.__data_lists[col_type][col_index])
+      else:
+        setattr(result, column_name, None)
+
+    self.__current_index += 1
+    return result
diff --git a/python/setup.py b/python/setup.py
index 9b1e81e..7afeec2 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -8,7 +8,7 @@
     ],
     package_data={'perfetto.trace_processor': ['*.descriptor']},
     include_package_data=True,
-    version='0.8.0',
+    version='0.9.0',
     license='apache-2.0',
     description='Python API for Perfetto\'s Trace Processor',
     author='Perfetto',
diff --git a/python/test/bigtrace_api_integrationtest.py b/python/test/bigtrace_api_integrationtest.py
index 76677a1..65766fc 100644
--- a/python/test/bigtrace_api_integrationtest.py
+++ b/python/test/bigtrace_api_integrationtest.py
@@ -51,20 +51,18 @@
 
   def test_valid_traces(self):
     result = self.client.query([
-        f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
-        f"{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
+        f"/local/{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
+        f"/local/{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
     ], "SELECT count(1) as count FROM slice LIMIT 5")
 
     self.assertEqual(
-        result.loc[
-            result['_trace_address'] ==
-            f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
-            'count'].iloc[0], 9726)
+        result.loc[result['_trace_address'] ==
+                   f"/local/{self.root_dir}/test/data/"
+                   "api24_startup_cold.perfetto-trace", 'count'].iloc[0], 9726)
     self.assertEqual(
-        result.loc[
-            result['_trace_address'] ==
-            f"{self.root_dir}/test/data/api24_startup_hot.perfetto-trace",
-            'count'].iloc[0], 5726)
+        result.loc[result['_trace_address'] ==
+                   f"/local/{self.root_dir}/test/data/"
+                   "api24_startup_hot.perfetto-trace", 'count'].iloc[0], 5726)
 
   def test_empty_traces(self):
     with self.assertRaises(PerfettoException):
@@ -73,6 +71,19 @@
   def test_empty_sql_string(self):
     with self.assertRaises(PerfettoException):
       result = self.client.query([
-          f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
-          f"{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
+          f"/local/{self.root_dir}/test/data/api24_startup_cold.perfetto-trace",
+          f"/local/{self.root_dir}/test/data/api24_startup_hot.perfetto-trace"
       ], "")
+
+  def test_invalid_prefix(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query([
+          f"/badprefix/{self.root_dir}/test/data/"
+          "api24_startup_cold.perfetto-trace"
+      ], "SELECT count(1) FROM slice LIMIT 5"),
+
+  def test_no_prefix(self):
+    with self.assertRaises(PerfettoException):
+      result = self.client.query(
+          [f"{self.root_dir}/test/data/api24_startup_cold.perfetto-trace"],
+          "SELECT count(1) FROM slice LIMIT 5")
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 464683e..c61f3bb 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -242,11 +242,6 @@
         r"/core_plugins/.*",
         "core code should not depend on plugins.",
     ),
-    #NoDirectDep(
-    #    r'/tracks/.*',
-    #    r'/core/.*',
-    #    'instead tracks should depend on the API exposed at ui/src/public.',
-    #),
     NoDep(
         r'/core/.*',
         r'/plugins/.*',
diff --git a/src/bigtrace/orchestrator/orchestrator_impl.h b/src/bigtrace/orchestrator/orchestrator_impl.h
index b989c68..71baa29 100644
--- a/src/bigtrace/orchestrator/orchestrator_impl.h
+++ b/src/bigtrace/orchestrator/orchestrator_impl.h
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
+#ifndef SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
+#define SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
+
 #include "perfetto/ext/base/threading/thread_pool.h"
 #include "protos/perfetto/bigtrace/orchestrator.grpc.pb.h"
 #include "protos/perfetto/bigtrace/worker.grpc.pb.h"
 
-#ifndef SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
-#define SRC_BIGTRACE_ORCHESTRATOR_ORCHESTRATOR_IMPL_H_
-
 namespace perfetto::bigtrace {
 
 class OrchestratorImpl final : public protos::BigtraceOrchestrator::Service {
diff --git a/src/bigtrace/worker/BUILD.gn b/src/bigtrace/worker/BUILD.gn
index 2f91816..0c23c4e 100644
--- a/src/bigtrace/worker/BUILD.gn
+++ b/src/bigtrace/worker/BUILD.gn
@@ -26,14 +26,17 @@
       "worker_main.cc",
     ]
     deps = [
+      "../../../gn:cpp_httplib",
       "../../../gn:default_deps",
       "../../../gn:grpc",
+      "../../../gn:jsoncpp",
       "../../../include/perfetto/ext/trace_processor/rpc:query_result_serializer",
       "../../../protos/perfetto/bigtrace:worker_grpc",
       "../../../protos/perfetto/bigtrace:worker_lite",
       "../../../src/trace_processor",
       "../../../src/trace_processor/rpc:rpc",
       "../../base",
+      "repository_policies:repository_policies",
     ]
   }
 }
diff --git a/src/bigtrace/worker/repository_policies/BUILD.gn b/src/bigtrace/worker/repository_policies/BUILD.gn
new file mode 100644
index 0000000..3b496dc
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/BUILD.gn
@@ -0,0 +1,40 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../gn/perfetto.gni")
+import("../../../../gn/test.gni")
+
+assert(enable_perfetto_trace_processor &&
+       enable_perfetto_trace_processor_sqlite && enable_perfetto_grpc)
+
+source_set("repository_policies") {
+  sources = [
+    "gcs_trace_processor_loader.cc",
+    "gcs_trace_processor_loader.h",
+    "local_trace_processor_loader.cc",
+    "local_trace_processor_loader.h",
+    "trace_processor_loader.cc",
+    "trace_processor_loader.h",
+  ]
+  deps = [
+    "../../../../gn:cpp_httplib",
+    "../../../../gn:default_deps",
+    "../../../../gn:grpc",
+    "../../../../gn:jsoncpp",
+    "../../../../src/trace_processor",
+    "../../../../src/trace_processor/rpc:rpc",
+    "../../../../src/trace_processor/util:util",
+    "../../../base",
+  ]
+}
diff --git a/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.cc b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.cc
new file mode 100644
index 0000000..91649b8
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.cc
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define CPPHTTPLIB_NO_EXCEPTIONS
+#define CPPHTTPLIB_OPENSSL_SUPPORT
+#include <httplib.h>
+#include <json/json.h>
+
+#include "perfetto/base/status.h"
+#include "src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::bigtrace {
+
+namespace {
+
+constexpr char kAuthDomain[] = "http://metadata.google.internal";
+constexpr char kAuthPath[] =
+    "/computeMetadata/v1/instance/service-accounts/default/token";
+constexpr char kGcsDomain[] = "https://storage.googleapis.com";
+constexpr char kGcsBucketPath[] = "/download/storage/v1/b/";
+constexpr char kGcsParams[] = "?alt=media";
+
+}  // namespace
+
+base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+GcsTraceProcessorLoader::LoadTraceProcessor(const std::string& path) {
+  trace_processor::Config config;
+  std::unique_ptr<trace_processor::TraceProcessor> tp =
+      trace_processor::TraceProcessor::CreateInstance(config);
+
+  // Retrieve access token to use in GET request to GCS
+  httplib::Headers auth_headers{{"Metadata-Flavor", "Google"}};
+  httplib::Client auth_client(kAuthDomain);
+
+  httplib::Result auth_response = auth_client.Get(kAuthPath, auth_headers);
+  std::string json_string = auth_response->body;
+
+  if (auth_response->status != httplib::StatusCode::OK_200) {
+    return base::ErrStatus("Failed to get access token: %s",
+                           auth_response->body.c_str());
+  }
+
+  // Parse access token from response
+  Json::Value json_value;
+  Json::Reader json_reader;
+  bool parsed_successfully = json_reader.parse(json_string, json_value);
+  if (!parsed_successfully) {
+    return base::ErrStatus("Failed to parse GCS access token");
+  }
+  std::string access_token = json_value["access_token"].asString();
+
+  // Download trace from GCS
+  std::string gcs_path = kGcsBucketPath + path + kGcsParams;
+  httplib::Headers gcs_headers{{"Authorization", "Bearer " + access_token}};
+
+  httplib::Client gcs_client(kGcsDomain);
+  base::Status response_status;
+
+  httplib::Result trace_response = gcs_client.Get(
+      gcs_path, gcs_headers,
+      [&](const httplib::Response& response) {
+        if (httplib::StatusCode::OK_200 != response.status) {
+          response_status = base::ErrStatus("Failed to download trace: %s",
+                                            response.reason.c_str());
+          return false;
+        }
+        return true;
+      },
+      [&](const char* data, size_t data_length) {
+        std::unique_ptr<uint8_t[]> buf(new uint8_t[data_length]);
+        memcpy(buf.get(), data, data_length);
+        auto status = tp->Parse(std::move(buf), data_length);
+        return true;
+      });
+
+  tp->NotifyEndOfFile();
+
+  RETURN_IF_ERROR(response_status);
+
+  return tp;
+}
+}  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h
new file mode 100644
index 0000000..223edaf
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_GCS_TRACE_PROCESSOR_LOADER_H_
+#define SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_GCS_TRACE_PROCESSOR_LOADER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_processor.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
+namespace perfetto::bigtrace {
+
+class GcsTraceProcessorLoader : public TraceProcessorLoader {
+ public:
+  base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+  LoadTraceProcessor(const std::string& path) override;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_GCS_TRACE_PROCESSOR_LOADER_H_
diff --git a/src/bigtrace/worker/repository_policies/local_trace_processor_loader.cc b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.cc
new file mode 100644
index 0000000..536405a
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.cc
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "src/bigtrace/worker/repository_policies/local_trace_processor_loader.h"
+#include "perfetto/trace_processor/read_trace.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::bigtrace {
+
+base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+LocalTraceProcessorLoader::LoadTraceProcessor(const std::string& path) {
+  trace_processor::Config config;
+  std::unique_ptr<trace_processor::TraceProcessor> tp =
+      trace_processor::TraceProcessor::CreateInstance(config);
+
+  RETURN_IF_ERROR(trace_processor::ReadTrace(tp.get(), path.c_str()));
+
+  return tp;
+}
+
+}  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/repository_policies/local_trace_processor_loader.h b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.h
new file mode 100644
index 0000000..0f60f89
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/local_trace_processor_loader.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_LOCAL_TRACE_PROCESSOR_LOADER_H_
+#define SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_LOCAL_TRACE_PROCESSOR_LOADER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_processor.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
+namespace perfetto::bigtrace {
+
+class LocalTraceProcessorLoader : public TraceProcessorLoader {
+ public:
+  base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+  LoadTraceProcessor(const std::string& path) override;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_LOCAL_TRACE_PROCESSOR_LOADER_H_
diff --git a/src/bigtrace/worker/repository_policies/trace_processor_loader.cc b/src/bigtrace/worker/repository_policies/trace_processor_loader.cc
new file mode 100644
index 0000000..d55bec7
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/trace_processor_loader.cc
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
+namespace perfetto::bigtrace {
+
+TraceProcessorLoader::~TraceProcessorLoader() = default;
+
+}
diff --git a/src/bigtrace/worker/repository_policies/trace_processor_loader.h b/src/bigtrace/worker/repository_policies/trace_processor_loader.h
new file mode 100644
index 0000000..b1a6522
--- /dev/null
+++ b/src/bigtrace/worker/repository_policies/trace_processor_loader.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_TRACE_PROCESSOR_LOADER_H_
+#define SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_TRACE_PROCESSOR_LOADER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_processor.h"
+
+namespace perfetto::bigtrace {
+
+// This interface is designed to facilitate interaction with multiple file
+// systems/object stores e.g. GCS, S3 or the local filesystem by allowing
+// implementation of classes which retrieve a trace using the specific store's
+// interface and returns a TraceProcessor instance containing the loaded trace
+// to the Worker
+class TraceProcessorLoader {
+ public:
+  virtual ~TraceProcessorLoader();
+  // Virtual method to load a trace from a given filesystem/object store and
+  // returns a TraceProcessor instance with the loaded trace
+  virtual base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>>
+  LoadTraceProcessor(const std::string& path) = 0;
+};
+
+}  // namespace perfetto::bigtrace
+
+#endif  // SRC_BIGTRACE_WORKER_REPOSITORY_POLICIES_TRACE_PROCESSOR_LOADER_H_
diff --git a/src/bigtrace/worker/worker_impl.cc b/src/bigtrace/worker/worker_impl.cc
index 53f8606..43729db 100644
--- a/src/bigtrace/worker/worker_impl.cc
+++ b/src/bigtrace/worker/worker_impl.cc
@@ -16,8 +16,9 @@
 
 #include "src/bigtrace/worker/worker_impl.h"
 #include "perfetto/ext/trace_processor/rpc/query_result_serializer.h"
-#include "perfetto/trace_processor/read_trace.h"
 #include "perfetto/trace_processor/trace_processor.h"
+#include "src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h"
+#include "src/bigtrace/worker/repository_policies/local_trace_processor_loader.h"
 
 namespace perfetto::bigtrace {
 
@@ -25,16 +26,31 @@
     grpc::ServerContext*,
     const protos::BigtraceQueryTraceArgs* args,
     protos::BigtraceQueryTraceResponse* response) {
-  trace_processor::Config config;
-  std::unique_ptr<trace_processor::TraceProcessor> tp =
-      trace_processor::TraceProcessor::CreateInstance(config);
+  std::string args_trace = args->trace();
 
-  base::Status status =
-      trace_processor::ReadTrace(tp.get(), args->trace().c_str());
-  if (!status.ok()) {
-    const std::string& error_message = status.c_message();
+  std::string prefix = args_trace.substr(0, args_trace.find("/", 1));
+  if (registry_.find(prefix) == registry_.end()) {
+    return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                        "Invalid path prefix specified");
+  }
+
+  if (prefix.length() == args_trace.length()) {
+    return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
+                        "Empty path is invalid");
+  }
+
+  std::string path = args_trace.substr(prefix.length() + 1);
+
+  base::StatusOr<std::unique_ptr<trace_processor::TraceProcessor>> tp_or =
+      registry_[prefix]->LoadTraceProcessor(path);
+
+  if (!tp_or.ok()) {
+    const std::string& error_message = tp_or.status().message();
     return grpc::Status(grpc::StatusCode::INTERNAL, error_message);
   }
+
+  std::unique_ptr<trace_processor::TraceProcessor> tp = std::move(*tp_or);
+
   auto iter = tp->ExecuteQuery(args->sql_query());
   trace_processor::QueryResultSerializer serializer =
       trace_processor::QueryResultSerializer(std::move(iter));
diff --git a/src/bigtrace/worker/worker_impl.h b/src/bigtrace/worker/worker_impl.h
index b5a9664..946715d 100644
--- a/src/bigtrace/worker/worker_impl.h
+++ b/src/bigtrace/worker/worker_impl.h
@@ -13,20 +13,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#include "protos/perfetto/bigtrace/worker.grpc.pb.h"
-#include "protos/perfetto/bigtrace/worker.pb.h"
-
 #ifndef SRC_BIGTRACE_WORKER_WORKER_IMPL_H_
 #define SRC_BIGTRACE_WORKER_WORKER_IMPL_H_
 
+#include <unordered_map>
+
+#include "protos/perfetto/bigtrace/worker.grpc.pb.h"
+#include "protos/perfetto/bigtrace/worker.pb.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
+
 namespace perfetto::bigtrace {
 
 class WorkerImpl final : public protos::BigtraceWorker::Service {
  public:
+  explicit WorkerImpl(
+      std::unordered_map<std::string, std::unique_ptr<TraceProcessorLoader>>
+          registry)
+      : registry_(std::move(registry)) {}
   grpc::Status QueryTrace(
       grpc::ServerContext*,
       const protos::BigtraceQueryTraceArgs* args,
       protos::BigtraceQueryTraceResponse* response) override;
+
+ private:
+  std::unordered_map<std::string, std::unique_ptr<TraceProcessorLoader>>
+      registry_;
 };
 
 }  // namespace perfetto::bigtrace
diff --git a/src/bigtrace/worker/worker_main.cc b/src/bigtrace/worker/worker_main.cc
index b985f69..c61cfc8 100644
--- a/src/bigtrace/worker/worker_main.cc
+++ b/src/bigtrace/worker/worker_main.cc
@@ -21,6 +21,9 @@
 
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/getopt.h"
+#include "src/bigtrace/worker/repository_policies/gcs_trace_processor_loader.h"
+#include "src/bigtrace/worker/repository_policies/local_trace_processor_loader.h"
+#include "src/bigtrace/worker/repository_policies/trace_processor_loader.h"
 #include "src/bigtrace/worker/worker_impl.h"
 
 namespace perfetto::bigtrace {
@@ -53,7 +56,13 @@
   CommandLineOptions options = ParseCommandLineOptions(argc, argv);
   std::string socket =
       options.socket.empty() ? "127.0.0.1:5052" : options.socket;
-  auto service = std::make_unique<WorkerImpl>();
+
+  std::unordered_map<std::string, std::unique_ptr<TraceProcessorLoader>>
+      registry;
+  registry["/gcs"] = std::make_unique<GcsTraceProcessorLoader>();
+  registry["/local"] = std::make_unique<LocalTraceProcessorLoader>();
+
+  auto service = std::make_unique<WorkerImpl>(std::move(registry));
   grpc::ServerBuilder builder;
   builder.RegisterService(service.get());
   builder.AddListeningPort(socket, grpc::InsecureServerCredentials());
diff --git a/src/profiling/symbolizer/local_symbolizer.cc b/src/profiling/symbolizer/local_symbolizer.cc
index 4d3d6fd..5271e82 100644
--- a/src/profiling/symbolizer/local_symbolizer.cc
+++ b/src/profiling/symbolizer/local_symbolizer.cc
@@ -226,13 +226,27 @@
   uint32_t cmdsize; /* total size of command in bytes */
 };
 
+struct segment_64_command {
+  uint32_t cmd;      /* LC_SEGMENT_64 */
+  uint32_t cmdsize;  /* includes sizeof section_64 structs */
+  char segname[16];  /* segment name */
+  uint64_t vmaddr;   /* memory address of this segment */
+  uint64_t vmsize;   /* memory size of this segment */
+  uint64_t fileoff;  /* file offset of this segment */
+  uint64_t filesize; /* amount to map from the file */
+  uint32_t maxprot;  /* maximum VM protection */
+  uint32_t initprot; /* initial VM protection */
+  uint32_t nsects;   /* number of sections in segment */
+  uint32_t flags;    /* flags */
+};
+
 struct BinaryInfo {
   std::string build_id;
   uint64_t load_bias;
   BinaryType type;
 };
 
-std::optional<std::string> GetMachOUuid(char* mem, size_t size) {
+std::optional<BinaryInfo> GetMachOBinaryInfo(char* mem, size_t size) {
   if (size < sizeof(mach_header_64))
     return {};
 
@@ -242,36 +256,48 @@
   if (size < sizeof(mach_header_64) + header.sizeofcmds)
     return {};
 
-  char* cmd = mem + sizeof(mach_header_64);
-  char* cmds_end = cmd + header.sizeofcmds;
-  while (cmd < cmds_end) {
-    load_command cmd_header;
-    memcpy(&cmd_header, cmd, sizeof(load_command));
+  std::optional<std::string> build_id;
+  uint64_t load_bias = 0;
 
-    if (cmd_header.cmd == 0x1b) {
-      return std::string(cmd + sizeof(load_command),
-                         cmd_header.cmdsize - sizeof(load_command));
+  char* pcmd = mem + sizeof(mach_header_64);
+  char* pcmds_end = pcmd + header.sizeofcmds;
+  while (pcmd < pcmds_end) {
+    load_command cmd_header;
+    memcpy(&cmd_header, pcmd, sizeof(load_command));
+
+    constexpr uint32_t LC_SEGMENT_64 = 0x19;
+    constexpr uint32_t LC_UUID = 0x1b;
+
+    switch (cmd_header.cmd) {
+      case LC_UUID: {
+        build_id = std::string(pcmd + sizeof(load_command),
+                               cmd_header.cmdsize - sizeof(load_command));
+        break;
+      }
+      case LC_SEGMENT_64: {
+        segment_64_command seg_cmd;
+        memcpy(&seg_cmd, pcmd, sizeof(segment_64_command));
+        if (strcmp(seg_cmd.segname, "__TEXT") == 0) {
+          load_bias = seg_cmd.vmaddr;
+        }
+        break;
+      }
+      default:
+        break;
     }
 
-    cmd += cmd_header.cmdsize;
+    pcmd += cmd_header.cmdsize;
   }
 
+  if (build_id) {
+    constexpr uint32_t MH_DSYM = 0xa;
+    BinaryType type = header.filetype == MH_DSYM ? BinaryType::kMachODsym
+                                                 : BinaryType::kMachO;
+    return BinaryInfo{*build_id, load_bias, type};
+  }
   return {};
 }
 
-std::optional<BinaryType> GetMachOBinaryType(char* mem, size_t size) {
-  if (size < sizeof(mach_header_64))
-    return {};
-
-  mach_header_64 header;
-  memcpy(&header, mem, sizeof(mach_header_64));
-
-  constexpr uint32_t MH_DSYM = 0xa;
-
-  return header.filetype == MH_DSYM ? BinaryType::kMachODsym
-                                    : BinaryType::kMachO;
-}
-
 std::optional<BinaryInfo> GetBinaryInfo(const char* fname, size_t size) {
   static_assert(EI_CLASS > EI_MAG3, "mem[EI_MAG?] accesses are in range.");
   if (size <= EI_CLASS)
@@ -285,7 +311,6 @@
 
   std::optional<std::string> build_id;
   std::optional<uint64_t> load_bias;
-  std::optional<BinaryType> type;
   if (IsElf(mem, size)) {
     switch (mem[EI_CLASS]) {
       case ELFCLASS32:
@@ -299,14 +324,11 @@
       default:
         return std::nullopt;
     }
-    type = BinaryType::kElf;
+    if (build_id && load_bias) {
+      return BinaryInfo{*build_id, *load_bias, BinaryType::kElf};
+    }
   } else if (IsMachO64(mem, size)) {
-    build_id = GetMachOUuid(mem, size);
-    load_bias = {0};
-    type = GetMachOBinaryType(mem, size);
-  }
-  if (build_id && load_bias && type) {
-    return BinaryInfo{*build_id, *load_bias, *type};
+    return GetMachOBinaryInfo(mem, size);
   }
   return std::nullopt;
 }
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index ec71357..e063ea6 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -174,6 +174,7 @@
       "importers/ftrace:full",
       "importers/fuchsia:full",
       "importers/gzip:full",
+      "importers/instruments",
       "importers/json:full",
       "importers/json:minimal",
       "importers/ninja",
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 70bffd5..b2250fd 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -57,6 +57,7 @@
       return std::nullopt;
 
     case kPerfDataTraceType:
+    case kInstrumentsXmlTraceType:
       return TraceSorter::SortingMode::kDefault;
 
     case kUnknownTraceType:
diff --git a/src/trace_processor/importers/common/BUILD.gn b/src/trace_processor/importers/common/BUILD.gn
index c2f61da..d128b43 100644
--- a/src/trace_processor/importers/common/BUILD.gn
+++ b/src/trace_processor/importers/common/BUILD.gn
@@ -41,6 +41,8 @@
     "global_args_tracker.h",
     "jit_cache.cc",
     "jit_cache.h",
+    "legacy_v8_cpu_profile_tracker.cc",
+    "legacy_v8_cpu_profile_tracker.h",
     "machine_tracker.cc",
     "machine_tracker.h",
     "mapping_tracker.cc",
diff --git a/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc
new file mode 100644
index 0000000..9206d87
--- /dev/null
+++ b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
+
+#include <cstdint>
+#include <optional>
+#include <utility>
+
+#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_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+LegacyV8CpuProfileTracker::LegacyV8CpuProfileTracker(
+    TraceProcessorContext* context)
+    : context_(context) {}
+
+void LegacyV8CpuProfileTracker::SetStartTsForSessionAndPid(uint64_t session_id,
+                                                           uint32_t pid,
+                                                           int64_t ts) {
+  auto [it, inserted] = state_by_session_and_pid_.Insert(
+      std::make_pair(session_id, pid),
+      State{ts, base::FlatHashMap<uint32_t, CallsiteId>(), nullptr});
+  it->ts = ts;
+  if (inserted) {
+    it->mapping = &context_->mapping_tracker->CreateDummyMapping("");
+  }
+}
+
+base::Status LegacyV8CpuProfileTracker::AddCallsite(
+    uint64_t session_id,
+    uint32_t pid,
+    uint32_t raw_callsite_id,
+    std::optional<uint32_t> parent_raw_callsite_id,
+    base::StringView script_url,
+    base::StringView function_name) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus(
+        "v8 profile id does not exist: cannot insert callsite");
+  }
+  FrameId frame_id =
+      state->mapping->InternDummyFrame(function_name, script_url);
+  CallsiteId callsite_id;
+  if (parent_raw_callsite_id) {
+    auto* parent_id = state->callsites.Find(*parent_raw_callsite_id);
+    if (!parent_id) {
+      return base::ErrStatus(
+          "v8 profile parent id does not exist: cannot insert callsite");
+    }
+    auto row =
+        context_->storage->stack_profile_callsite_table().FindById(*parent_id);
+    callsite_id = context_->stack_profile_tracker->InternCallsite(
+        *parent_id, frame_id, row->depth() + 1);
+  } else {
+    callsite_id = context_->stack_profile_tracker->InternCallsite(std::nullopt,
+                                                                  frame_id, 0);
+  }
+  if (!state->callsites.Insert(raw_callsite_id, callsite_id).second) {
+    return base::ErrStatus("v8 profile: callsite with id already exists");
+  }
+  return base::OkStatus();
+}
+
+base::StatusOr<int64_t> LegacyV8CpuProfileTracker::AddDeltaAndGetTs(
+    uint64_t session_id,
+    uint32_t pid,
+    int64_t delta_ts) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus(
+        "v8 profile id does not exist: cannot compute timestamp from delta");
+  }
+  state->ts += delta_ts;
+  return state->ts;
+}
+
+base::Status LegacyV8CpuProfileTracker::AddSample(int64_t ts,
+                                                  uint64_t session_id,
+                                                  uint32_t pid,
+                                                  uint32_t tid,
+                                                  uint32_t raw_callsite_id) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus("v8 callsite id does not exist: cannot add sample");
+  }
+  auto* id = state->callsites.Find(raw_callsite_id);
+  if (!id) {
+    return base::ErrStatus("v8 callsite id does not exist: cannot add sample");
+  }
+  UniqueTid utid = context_->process_tracker->UpdateThread(tid, pid);
+  auto* samples = context_->storage->mutable_cpu_profile_stack_sample_table();
+  samples->Insert({ts, *id, utid, 0});
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h
new file mode 100644
index 0000000..bdb2e78
--- /dev/null
+++ b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
+
+#include <cstdint>
+#include <optional>
+#include <utility>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+// Stores interned callsites for given pid for legacy v8 samples.
+class LegacyV8CpuProfileTracker {
+ public:
+  explicit LegacyV8CpuProfileTracker(TraceProcessorContext*);
+
+  // Sets the start timestamp for the given pid.
+  void SetStartTsForSessionAndPid(uint64_t session_id,
+                                  uint32_t pid,
+                                  int64_t ts);
+
+  // Adds the callsite with for the given session and pid and given raw callsite
+  // id.
+  base::Status AddCallsite(uint64_t session_id,
+                           uint32_t pid,
+                           uint32_t raw_callsite_id,
+                           std::optional<uint32_t> parent_raw_callsite_id,
+                           base::StringView script_url,
+                           base::StringView function_name);
+
+  // Increments the current timestamp for the given session and pid by
+  // |delta_ts| and returns the resulting full timestamp.
+  base::StatusOr<int64_t> AddDeltaAndGetTs(uint64_t session_id,
+                                           uint32_t pid,
+                                           int64_t delta_ts);
+
+  // Adds the sample with for the given session and pid/tid and given raw
+  // callsite id.
+  base::Status AddSample(int64_t ts,
+                         uint64_t session_id,
+                         uint32_t pid,
+                         uint32_t tid,
+                         uint32_t raw_callsite_id);
+
+ private:
+  struct State {
+    int64_t ts;
+    base::FlatHashMap<uint32_t, CallsiteId> callsites;
+    DummyMemoryMapping* mapping;
+  };
+  struct Hasher {
+    uint64_t operator()(const std::pair<uint64_t, uint32_t>& res) {
+      return base::Hasher::Combine(res.first, res.second);
+    }
+  };
+  base::FlatHashMap<std::pair<uint64_t, uint32_t>, State, Hasher>
+      state_by_session_and_pid_;
+
+  TraceProcessorContext* const context_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
diff --git a/src/trace_processor/importers/common/mapping_tracker.cc b/src/trace_processor/importers/common/mapping_tracker.cc
index 02976f9..6e6fbf3 100644
--- a/src/trace_processor/importers/common/mapping_tracker.cc
+++ b/src/trace_processor/importers/common/mapping_tracker.cc
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/address_range.h"
 #include "src/trace_processor/importers/common/jit_cache.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/build_id.h"
@@ -161,14 +162,15 @@
       });
 }
 
-VirtualMemoryMapping* MappingTracker::GetDummyMapping() {
-  if (!dummy_mapping_) {
-    CreateMappingParams params;
-    params.memory_range =
-        AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
-    dummy_mapping_ = &InternMemoryMapping(params);
-  }
-  return dummy_mapping_;
+DummyMemoryMapping& MappingTracker::CreateDummyMapping(std::string name) {
+  CreateMappingParams params;
+  params.name = std::move(name);
+  params.memory_range =
+      AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
+  std::unique_ptr<DummyMemoryMapping> mapping(
+      new DummyMemoryMapping(context_, std::move(params)));
+
+  return AddMapping(std::move(mapping));
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/mapping_tracker.h b/src/trace_processor/importers/common/mapping_tracker.h
index 4791dba..c655d57 100644
--- a/src/trace_processor/importers/common/mapping_tracker.h
+++ b/src/trace_processor/importers/common/mapping_tracker.h
@@ -68,6 +68,10 @@
   UserMemoryMapping& CreateUserMemoryMapping(UniquePid upid,
                                              CreateMappingParams params);
 
+  // Sometimes we just need a mapping and we are lacking trace data to create a
+  // proper one. Use this mapping in those cases.
+  DummyMemoryMapping& CreateDummyMapping(std::string name);
+
   // Create an "other" mapping. Returned reference will be valid for the
   // duration of this instance.
   VirtualMemoryMapping& InternMemoryMapping(CreateMappingParams params);
@@ -91,10 +95,6 @@
   // Jitted ranges will only be applied to UserMemoryMappings
   void AddJitRange(UniquePid upid, AddressRange range, JitCache* jit_cache);
 
-  // Sometimes we just need a mapping and we are lacking trace data to create a
-  // proper one. Use this mapping in those cases.
-  VirtualMemoryMapping* GetDummyMapping();
-
  private:
   template <typename MappingImpl>
   MappingImpl& AddMapping(std::unique_ptr<MappingImpl> mapping);
@@ -140,8 +140,6 @@
   KernelMemoryMapping* kernel_ = nullptr;
 
   base::FlatHashMap<UniquePid, AddressRangeMap<JitCache*>> jit_caches_;
-
-  VirtualMemoryMapping* dummy_mapping_ = nullptr;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/parser_types.h b/src/trace_processor/importers/common/parser_types.h
index 2a23b6c..bd32bde 100644
--- a/src/trace_processor/importers/common/parser_types.h
+++ b/src/trace_processor/importers/common/parser_types.h
@@ -37,6 +37,7 @@
   int32_t next_prio;
   StringPool::Id next_comm;
 };
+static_assert(sizeof(InlineSchedSwitch) == 24);
 
 // We enforce the exact size as it's critical for peak-memory use when sorting
 // data in trace processor that this struct is as small as possible.
@@ -91,6 +92,14 @@
 };
 static_assert(sizeof(TracePacketData) % 8 == 0);
 
+struct alignas(8) LegacyV8CpuProfileEvent {
+  uint64_t session_id;
+  uint32_t pid;
+  uint32_t tid;
+  uint32_t callsite_id;
+};
+static_assert(sizeof(LegacyV8CpuProfileEvent) % 8 == 0);
+
 }  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
diff --git a/src/trace_processor/importers/common/trace_parser.cc b/src/trace_processor/importers/common/trace_parser.cc
index f497278..a667b75 100644
--- a/src/trace_processor/importers/common/trace_parser.cc
+++ b/src/trace_processor/importers/common/trace_parser.cc
@@ -23,6 +23,7 @@
 JsonTraceParser::~JsonTraceParser() = default;
 FuchsiaRecordParser::~FuchsiaRecordParser() = default;
 PerfRecordParser::~PerfRecordParser() = default;
+InstrumentsRowParser::~InstrumentsRowParser() = default;
 AndroidLogEventParser::~AndroidLogEventParser() = default;
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index 94594bc..cf7c84a 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -24,6 +24,9 @@
 namespace perf_importer {
 struct Record;
 }
+namespace instruments_importer {
+struct Row;
+}
 
 struct AndroidLogEvent;
 class PacketSequenceStateGeneration;
@@ -34,6 +37,7 @@
 struct InlineSchedWaking;
 struct TracePacketData;
 struct TrackEventData;
+struct LegacyV8CpuProfileEvent;
 
 class ProtoTraceParser {
  public:
@@ -44,6 +48,7 @@
   virtual void ParseFtraceEvent(uint32_t, int64_t, TracePacketData) = 0;
   virtual void ParseInlineSchedSwitch(uint32_t, int64_t, InlineSchedSwitch) = 0;
   virtual void ParseInlineSchedWaking(uint32_t, int64_t, InlineSchedWaking) = 0;
+  virtual void ParseLegacyV8ProfileEvent(int64_t, LegacyV8CpuProfileEvent) = 0;
 };
 
 class JsonTraceParser {
@@ -65,6 +70,12 @@
   virtual void ParsePerfRecord(int64_t, perf_importer::Record) = 0;
 };
 
+class InstrumentsRowParser {
+ public:
+  virtual ~InstrumentsRowParser();
+  virtual void ParseInstrumentsRow(int64_t, instruments_importer::Row) = 0;
+};
+
 class AndroidLogEventParser {
  public:
   virtual ~AndroidLogEventParser();
diff --git a/src/trace_processor/importers/common/virtual_memory_mapping.cc b/src/trace_processor/importers/common/virtual_memory_mapping.cc
index 0485243..562049e 100644
--- a/src/trace_processor/importers/common/virtual_memory_mapping.cc
+++ b/src/trace_processor/importers/common/virtual_memory_mapping.cc
@@ -23,6 +23,7 @@
 #include <string>
 #include <utility>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/address_range.h"
 #include "src/trace_processor/importers/common/jit_cache.h"
@@ -119,5 +120,40 @@
   return {frame_id, true};
 }
 
+DummyMemoryMapping::~DummyMemoryMapping() = default;
+
+DummyMemoryMapping::DummyMemoryMapping(TraceProcessorContext* context,
+                                       CreateMappingParams params)
+    : VirtualMemoryMapping(context, std::move(params)) {}
+
+FrameId DummyMemoryMapping::InternDummyFrame(base::StringView function_name,
+                                             base::StringView source_file) {
+  DummyFrameKey key{context()->storage->InternString(function_name),
+                    context()->storage->InternString(source_file)};
+
+  if (FrameId* id = interned_dummy_frames_.Find(key); id) {
+    return *id;
+  }
+
+  uint32_t symbol_set_id = context()->storage->symbol_table().row_count();
+
+  tables::SymbolTable::Id symbol_id =
+      context()
+          ->storage->mutable_symbol_table()
+          ->Insert({symbol_set_id, key.function_name_id, key.source_file_id})
+          .id;
+
+  PERFETTO_CHECK(symbol_set_id == symbol_id.value);
+
+  const FrameId frame_id =
+      context()
+          ->storage->mutable_stack_profile_frame_table()
+          ->Insert({key.function_name_id, mapping_id(), 0, symbol_set_id})
+          .id;
+  interned_dummy_frames_.Insert(key, frame_id);
+
+  return frame_id;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/virtual_memory_mapping.h b/src/trace_processor/importers/common/virtual_memory_mapping.h
index 498a9ef..1676437 100644
--- a/src/trace_processor/importers/common/virtual_memory_mapping.h
+++ b/src/trace_processor/importers/common/virtual_memory_mapping.h
@@ -86,6 +86,8 @@
   VirtualMemoryMapping(TraceProcessorContext* context,
                        CreateMappingParams params);
 
+  TraceProcessorContext* context() const { return context_; }
+
  private:
   friend class MappingTracker;
 
@@ -149,6 +151,42 @@
   const UniquePid upid_;
 };
 
+// Dummy mapping to be able to create frames when we have no real pc addresses
+// or real mappings.
+class DummyMemoryMapping : public VirtualMemoryMapping {
+ public:
+  ~DummyMemoryMapping() override;
+
+  // Interns a frame based solely on function name and source file. This is
+  // useful for profilers that do not emit an address nor a mapping.
+  FrameId InternDummyFrame(base::StringView function_name,
+                           base::StringView source_file);
+
+ private:
+  friend class MappingTracker;
+  DummyMemoryMapping(TraceProcessorContext* context,
+                     CreateMappingParams params);
+
+  struct DummyFrameKey {
+    StringId function_name_id;
+    StringId source_file_id;
+
+    bool operator==(const DummyFrameKey& o) const {
+      return function_name_id == o.function_name_id &&
+             source_file_id == o.source_file_id;
+    }
+
+    struct Hasher {
+      size_t operator()(const DummyFrameKey& k) const {
+        return static_cast<size_t>(base::Hasher::Combine(
+            k.function_name_id.raw_id(), k.source_file_id.raw_id()));
+      }
+    };
+  };
+  base::FlatHashMap<DummyFrameKey, FrameId, DummyFrameKey::Hasher>
+      interned_dummy_frames_;
+};
+
 }  // namespace trace_processor
 }  // namespace perfetto
 
diff --git a/src/trace_processor/importers/gzip/gzip_trace_parser.cc b/src/trace_processor/importers/gzip/gzip_trace_parser.cc
index dfa45e0..d133b2c 100644
--- a/src/trace_processor/importers/gzip/gzip_trace_parser.cc
+++ b/src/trace_processor/importers/gzip/gzip_trace_parser.cc
@@ -79,11 +79,9 @@
   // Our default uncompressed buffer size is 32MB as it allows for good
   // throughput.
   constexpr size_t kUncompressedBufferSize = 32ul * 1024 * 1024;
-
-  needs_more_input_ = false;
   decompressor_.Feed(start, len);
 
-  for (auto ret = ResultCode::kOk; ret != ResultCode::kEof;) {
+  for (;;) {
     if (!buffer_) {
       buffer_.reset(new uint8_t[kUncompressedBufferSize]);
       bytes_written_ = 0;
@@ -92,39 +90,44 @@
     auto result =
         decompressor_.ExtractOutput(buffer_.get() + bytes_written_,
                                     kUncompressedBufferSize - bytes_written_);
-    ret = result.ret;
+    util::GzipDecompressor::ResultCode ret = result.ret;
     if (ret == ResultCode::kError)
       return base::ErrStatus("Failed to decompress trace chunk");
 
     if (ret == ResultCode::kNeedsMoreInput) {
       PERFETTO_DCHECK(result.bytes_written == 0);
-      needs_more_input_ = true;
       return base::OkStatus();
     }
     bytes_written_ += result.bytes_written;
+    output_state_ = kMidStream;
 
     if (bytes_written_ == kUncompressedBufferSize || ret == ResultCode::kEof) {
       TraceBlob blob =
           TraceBlob::TakeOwnership(std::move(buffer_), bytes_written_);
       RETURN_IF_ERROR(inner_->Parse(TraceBlobView(std::move(blob))));
     }
+
+    // We support multiple gzip streams in a single gzip file (which is valid
+    // according to RFC1952 section 2.2): in that case, we just need to reset
+    // the decompressor to begin processing the next stream: all other variables
+    // can be preserved.
+    if (ret == ResultCode::kEof) {
+      decompressor_.Reset();
+      output_state_ = kStreamBoundary;
+
+      if (decompressor_.AvailIn() == 0) {
+        return base::OkStatus();
+      }
+    }
   }
-  return base::OkStatus();
 }
 
 base::Status GzipTraceParser::NotifyEndOfFile() {
-  // TODO(lalitm): this should really be an error returned to the caller but
-  // due to historical implementation, NotifyEndOfFile does not return a
-  // base::Status.
-  if (needs_more_input_) {
+  if (output_state_ != kStreamBoundary || decompressor_.AvailIn() > 0) {
     return base::ErrStatus("GZIP stream incomplete, trace is likely corrupt");
   }
-  PERFETTO_DCHECK(!buffer_);
-
-  if (!inner_) {
-    base::OkStatus();
-  }
-  return inner_->NotifyEndOfFile();
+  PERFETTO_CHECK(!buffer_);
+  return inner_ ? inner_->NotifyEndOfFile() : base::OkStatus();
 }
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/gzip/gzip_trace_parser.h b/src/trace_processor/importers/gzip/gzip_trace_parser.h
index 1cd862d..4a565e6 100644
--- a/src/trace_processor/importers/gzip/gzip_trace_parser.h
+++ b/src/trace_processor/importers/gzip/gzip_trace_parser.h
@@ -41,8 +41,6 @@
 
   base::Status ParseUnowned(const uint8_t*, size_t);
 
-  bool needs_more_input() const { return needs_more_input_; }
-
  private:
   TraceProcessorContext* const context_;
   util::GzipDecompressor decompressor_;
@@ -52,7 +50,7 @@
   size_t bytes_written_ = 0;
 
   bool first_chunk_parsed_ = false;
-  bool needs_more_input_ = false;
+  enum { kStreamBoundary, kMidStream } output_state_ = kStreamBoundary;
 };
 
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/instruments/BUILD.gn b/src/trace_processor/importers/instruments/BUILD.gn
new file mode 100644
index 0000000..e5ac896
--- /dev/null
+++ b/src/trace_processor/importers/instruments/BUILD.gn
@@ -0,0 +1,55 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../gn/test.gni")
+
+source_set("row") {
+  sources = [ "row.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+    "../../util:build_id",
+  ]
+}
+
+source_set("instruments") {
+  sources = [
+    "instruments_utils.h",
+    "instruments_xml_tokenizer.h",
+    "row_parser.h",
+  ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../../../include/perfetto/ext/base:base",
+  ]
+  if (enable_perfetto_trace_processor_mac_instruments) {
+    public_deps = [ ":row" ]
+    sources += [
+      "instruments_xml_tokenizer.cc",
+      "row_data_tracker.cc",
+      "row_data_tracker.h",
+      "row_parser.cc",
+    ]
+    deps += [
+      "../../../../gn:expat",
+      "../../../../include/perfetto/public",
+      "../../../../include/perfetto/trace_processor:trace_processor",
+      "../../../../protos/perfetto/trace:zero",
+      "../../sorter",
+      "../../storage",
+      "../../types",
+      "../common:common",
+    ]
+  }
+}
diff --git a/src/trace_processor/importers/instruments/instruments_utils.h b/src/trace_processor/importers/instruments/instruments_utils.h
new file mode 100644
index 0000000..b121468
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_utils.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_UTILS_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_UTILS_H_
+
+#include "perfetto/base/build_config.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+inline bool IsInstrumentsSupported() {
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+  return true;
+#else
+  return false;
+#endif
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_UTILS_H_
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
new file mode 100644
index 0000000..5523c13
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
@@ -0,0 +1,506 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
+
+#include <map>
+
+#include <expat.h>
+#include <stdint.h>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/public/fnv1a.h"
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+#error \
+    "This file should not be built when enable_perfetto_trace_processor_mac_instruments=false"
+#endif
+
+namespace perfetto::trace_processor::instruments_importer {
+
+namespace {
+
+std::string MakeTrimmed(const char* chars, int len) {
+  while (len > 0 && std::isspace(*chars)) {
+    chars++;
+    len--;
+  }
+  while (len > 0 && std::isspace(chars[len - 1])) {
+    len--;
+  }
+  return std::string(chars, static_cast<size_t>(len));
+}
+
+}  // namespace
+
+// The Instruments XML tokenizer reads instruments traces exported with:
+//
+//   xctrace export --input /path/to/profile.trace --xpath
+//     '//trace-toc/run/data/table[@schema="os-signpost and
+//        @category="PointsOfInterest"] |
+//      //trace-toc/run/data/table[@schema="time-sample"]'
+//
+// This exports two tables:
+//   1. Points of interest signposts
+//   2. Time samples
+//
+// The first is used for clock synchronization -- perfetto emits signpost events
+// during tracing which allow synchronization of the xctrace clock (relative to
+// start of profiling) with the perfetto boottime clock. The second contains
+// the samples themselves.
+//
+// The expected format of the rows in the clock sync table is:
+//
+//     <row>
+//       <event-time>1234</event-time>
+//       <subsystem>dev.perfetto.clock_sync</subsystem>
+//       <os-log-metadata>
+//         <uint64>5678</uint64>
+//       </os-log-metadata>
+//     </row>
+//
+// There may be other rows with other data (from other subsystems), and
+// additional data in the row (such as thread data and other metadata) -- this
+// can be safely ignored.
+//
+// The expected format of the rows in the time sample table is:
+//
+//     <row>
+//       <sample-time>1234</sample-time>
+//       <thread fmt="Thread name">
+//         <tid>1</tid>
+//         <process fmt="Process name">
+//           <pid>1<pid>
+//         </process>
+//       </thread>
+//       <core>0</core>
+//       <backtrace>
+//         <frame addr="0x120001234">
+//           <binary
+//             name="MyBinary" UUID="01234567-89ABC-CDEF-0123-456789ABCDEF"
+//             load-addr="0x120000000" path="/path/to/MyBinary.app/MyBinary" />
+//         </frame>
+//         ... more frames ...
+//     </row>
+//
+// Here we do not expect other rows with other data -- every row should have a
+// backtrace, and we use the presence of a backtrace to distinguish time samples
+// and clock sync eventst. However, there can be additional data in the row
+// (such as other metadata) -- this can be safely ignored.
+//
+// In addition, the XML format annotates elements with ids, to later reuse the
+// same data by id without needing to repeat its contents. For example, you
+// might have thread data for a sample:
+//
+//     <thread id="11" fmt="My Thread"><tid id="12">10</tid>...</thread>
+//
+// and subsequent samples on that thread will simply have
+//
+//     <thread ref="11" />
+//
+// This means that most elements have to have their pertinent data cached by id,
+// including any data store in child elements (which themselves also have to
+// be cached by id, like the <tid> in the example above).
+//
+// This importer reads the XML data using a streaming XML parser, which means
+// it has to maintain some parsing state (such as the current stack of tags, or
+// the current element for which we are reading data).
+class InstrumentsXmlTokenizer::Impl {
+ public:
+  explicit Impl(TraceProcessorContext* context)
+      : context_(context), data_(RowDataTracker::GetOrCreate(context_)) {
+    parser_ = XML_ParserCreate(nullptr);
+    XML_SetElementHandler(parser_, ElementStart, ElementEnd);
+    XML_SetCharacterDataHandler(parser_, CharacterData);
+    XML_SetUserData(parser_, this);
+
+    const char* subsystem = "dev.perfetto.instruments_clock";
+    clock_ = static_cast<ClockTracker::ClockId>(
+        PerfettoFnv1a(subsystem, strlen(subsystem)) | 0x80000000);
+
+    // Use the above clock if we can, in case there is no other trace and
+    // no clock sync events.
+    context_->clock_tracker->SetTraceTimeClock(clock_);
+  }
+  ~Impl() { XML_ParserFree(parser_); }
+
+  base::Status Parse(TraceBlobView view) {
+    if (!XML_Parse(parser_, reinterpret_cast<const char*>(view.data()),
+                   static_cast<int>(view.length()), false)) {
+      return base::ErrStatus("XML parse error at line %lu: %s\n",
+                             XML_GetCurrentLineNumber(parser_),
+                             XML_ErrorString(XML_GetErrorCode(parser_)));
+    }
+    return base::OkStatus();
+  }
+
+  base::Status End() {
+    if (!XML_Parse(parser_, nullptr, 0, true)) {
+      return base::ErrStatus("XML parse error at end, line %lu: %s\n",
+                             XML_GetCurrentLineNumber(parser_),
+                             XML_ErrorString(XML_GetErrorCode(parser_)));
+    }
+    return base::OkStatus();
+  }
+
+ private:
+  static void ElementStart(void* data, const char* el, const char** attr) {
+    reinterpret_cast<Impl*>(data)->ElementStart(el, attr);
+  }
+  static void ElementEnd(void* data, const char* el) {
+    reinterpret_cast<Impl*>(data)->ElementEnd(el);
+  }
+  static void CharacterData(void* data, const char* chars, int len) {
+    reinterpret_cast<Impl*>(data)->CharacterData(chars, len);
+  }
+
+  void ElementStart(const char* el, const char** attrs) {
+    tag_stack_.emplace_back(el);
+    std::string_view tag_name = tag_stack_.back();
+
+    if (tag_name == "row") {
+      current_row_ = Row{};
+    } else if (tag_name == "thread") {
+      MaybeCachedRef<ThreadId> thread_lookup =
+          GetOrInsertByRef(attrs, thread_ref_to_thread_);
+      if (thread_lookup.is_new) {
+        auto new_thread = data_.NewThread();
+        thread_lookup.ref = new_thread.id;
+
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "fmt") {
+            new_thread.ptr->fmt = InternString(attrs[i + 1]);
+          }
+        }
+
+        current_new_thread_ = new_thread.id;
+      }
+      current_row_.thread = thread_lookup.ref;
+    } else if (tag_name == "process") {
+      MaybeCachedRef<ProcessId> process_lookup =
+          GetOrInsertByRef(attrs, process_ref_to_process_);
+      if (process_lookup.is_new) {
+        // Can only be processing a new process when processing a new thread.
+        PERFETTO_DCHECK(current_new_thread_ != kNullId);
+        auto new_process = data_.NewProcess();
+        process_lookup.ref = new_process.id;
+
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "fmt") {
+            new_process.ptr->fmt = InternString(attrs[i + 1]);
+          }
+        }
+
+        current_new_process_ = new_process.id;
+      }
+      if (current_new_thread_) {
+        data_.GetThread(current_new_thread_)->process = process_lookup.ref;
+      }
+    } else if (tag_name == "core") {
+      MaybeCachedRef<uint32_t> core_id_lookup =
+          GetOrInsertByRef(attrs, core_ref_to_core_);
+      if (core_id_lookup.is_new) {
+        current_new_core_id_ = &core_id_lookup.ref;
+      } else {
+        current_row_.core_id = core_id_lookup.ref;
+      }
+    } else if (tag_name == "sample-time" || tag_name == "event-time") {
+      // Share time lookup logic between sample times and event times, including
+      // updating the current row's sample time for both.
+      MaybeCachedRef<int64_t> time_lookup =
+          GetOrInsertByRef(attrs, sample_time_ref_to_time_);
+      if (time_lookup.is_new) {
+        current_time_ref_ = &time_lookup.ref;
+      } else {
+        current_row_.timestamp_ = time_lookup.ref;
+      }
+    } else if (tag_name == "subsystem") {
+      MaybeCachedRef<std::string> subsystem_lookup =
+          GetOrInsertByRef(attrs, subsystem_ref_to_subsystem_);
+      current_subsystem_ref_ = &subsystem_lookup.ref;
+    } else if (tag_name == "uint64") {
+      // The only uint64 we care about is the one for the clock sync, which is
+      // expected to contain exactly one uint64 value -- we'll
+      // map all uint64 to a single value and check against the subsystem
+      // when the row is closed.
+      MaybeCachedRef<uint64_t> uint64_lookup =
+          GetOrInsertByRef(attrs, os_log_metadata_or_uint64_ref_to_uint64_);
+      if (uint64_lookup.is_new) {
+        current_uint64_ref_ = &uint64_lookup.ref;
+      } else {
+        if (current_os_log_metadata_uint64_ref_) {
+          // Update the os-log-metadata's uint64 value with this uint64 value.
+          *current_os_log_metadata_uint64_ref_ = uint64_lookup.ref;
+        }
+      }
+    } else if (tag_name == "os-log-metadata") {
+      // The only os-log-metadata we care about is the one with the single
+      // uint64 clock sync value, so also map this to uint64 values with its own
+      // id.
+      MaybeCachedRef<uint64_t> uint64_lookup =
+          GetOrInsertByRef(attrs, os_log_metadata_or_uint64_ref_to_uint64_);
+      current_os_log_metadata_uint64_ref_ = &uint64_lookup.ref;
+    } else if (tag_name == "backtrace") {
+      MaybeCachedRef<BacktraceId> backtrace_lookup =
+          GetOrInsertByRef(attrs, backtrace_ref_to_backtrace_);
+      if (backtrace_lookup.is_new) {
+        backtrace_lookup.ref = data_.NewBacktrace().id;
+      }
+      current_row_.backtrace = backtrace_lookup.ref;
+    } else if (tag_name == "frame") {
+      MaybeCachedRef<BacktraceFrameId> frame_lookup =
+          GetOrInsertByRef(attrs, frame_ref_to_frame_);
+      if (frame_lookup.is_new) {
+        IdPtr<Frame> new_frame = data_.NewFrame();
+        frame_lookup.ref = new_frame.id;
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "addr") {
+            new_frame.ptr->addr = strtoll(attrs[i + 1], nullptr, 16);
+          }
+        }
+        current_new_frame_ = new_frame.id;
+      }
+      data_.GetBacktrace(current_row_.backtrace)
+          ->frames.push_back(frame_lookup.ref);
+    } else if (tag_name == "binary") {
+      // Can only be processing a binary when processing a new frame.
+      PERFETTO_DCHECK(current_new_frame_ != kNullId);
+
+      MaybeCachedRef<BinaryId> binary_lookup =
+          GetOrInsertByRef(attrs, binary_ref_to_binary_);
+      if (binary_lookup.is_new) {
+        auto new_binary = data_.NewBinary();
+        binary_lookup.ref = new_binary.id;
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "path") {
+            new_binary.ptr->path = std::string(attrs[i + 1]);
+          } else if (key == "UUID") {
+            new_binary.ptr->uuid =
+                BuildId::FromHex(base::StringView(attrs[i + 1]));
+          } else if (key == "load-addr") {
+            new_binary.ptr->load_addr = strtoll(attrs[i + 1], nullptr, 16);
+          }
+        }
+        new_binary.ptr->max_addr = new_binary.ptr->load_addr;
+      }
+      PERFETTO_DCHECK(data_.GetFrame(current_new_frame_)->binary == kNullId);
+      data_.GetFrame(current_new_frame_)->binary = binary_lookup.ref;
+    }
+  }
+
+  void ElementEnd(const char* el) {
+    PERFETTO_DCHECK(el == tag_stack_.back());
+    std::string tag_name = std::move(tag_stack_.back());
+    tag_stack_.pop_back();
+
+    if (tag_name == "row") {
+      if (current_row_.backtrace) {
+        // Rows with backtraces are assumed to be time samples.
+        base::StatusOr<int64_t> trace_ts =
+            ToTraceTimestamp(current_row_.timestamp_);
+        if (!trace_ts.ok()) {
+          PERFETTO_DLOG("Skipping timestamp %" PRId64 ", no clock snapshot yet",
+                        current_row_.timestamp_);
+        } else {
+          context_->sorter->PushInstrumentsRow(*trace_ts,
+                                               std::move(current_row_));
+        }
+      } else if (current_subsystem_ref_ != nullptr) {
+        // Rows without backtraces are assumed to be signpost events -- filter
+        // these for `dev.perfetto.clock_sync` events.
+        if (*current_subsystem_ref_ == "dev.perfetto.clock_sync") {
+          PERFETTO_DCHECK(current_os_log_metadata_uint64_ref_ != nullptr);
+          uint64_t clock_sync_timestamp = *current_os_log_metadata_uint64_ref_;
+          if (latest_clock_sync_timestamp_ > clock_sync_timestamp) {
+            PERFETTO_DLOG("Skipping timestamp %" PRId64
+                          ", non-monotonic sync deteced",
+                          current_row_.timestamp_);
+          } else {
+            latest_clock_sync_timestamp_ = clock_sync_timestamp;
+            auto status = context_->clock_tracker->AddSnapshot(
+                {{clock_, current_row_.timestamp_},
+                 {protos::pbzero::ClockSnapshot::Clock::BOOTTIME,
+                  static_cast<int64_t>(latest_clock_sync_timestamp_)}});
+            if (!status.ok()) {
+              PERFETTO_FATAL("Error adding clock snapshot: %s",
+                             status.status().c_message());
+            }
+          }
+        }
+        current_subsystem_ref_ = nullptr;
+        current_os_log_metadata_uint64_ref_ = nullptr;
+        current_uint64_ref_ = nullptr;
+      }
+    } else if (current_new_frame_ != kNullId && tag_name == "frame") {
+      Frame* frame = data_.GetFrame(current_new_frame_);
+      if (frame->binary) {
+        Binary* binary = data_.GetBinary(frame->binary);
+        // We don't know what the binary's mapping end is, but we know that the
+        // current frame is inside of it, so use that.
+        PERFETTO_DCHECK(frame->addr > binary->load_addr);
+        if (frame->addr > binary->max_addr) {
+          binary->max_addr = frame->addr;
+        }
+      }
+      current_new_frame_ = kNullId;
+    } else if (current_new_thread_ != kNullId && tag_name == "thread") {
+      current_new_thread_ = kNullId;
+    } else if (current_new_process_ != kNullId && tag_name == "process") {
+      current_new_process_ = kNullId;
+    } else if (current_new_core_id_ != nullptr && tag_name == "core") {
+      current_new_core_id_ = nullptr;
+    }
+  }
+
+  void CharacterData(const char* chars, int len) {
+    std::string_view tag_name = tag_stack_.back();
+    if (current_time_ref_ != nullptr &&
+        (tag_name == "sample-time" || tag_name == "event-time")) {
+      std::string s = MakeTrimmed(chars, len);
+      current_row_.timestamp_ = *current_time_ref_ = stoll(s);
+      current_time_ref_ = nullptr;
+    } else if (current_new_thread_ != kNullId && tag_name == "tid") {
+      std::string s = MakeTrimmed(chars, len);
+      data_.GetThread(current_new_thread_)->tid = stoi(s);
+    } else if (current_new_process_ != kNullId && tag_name == "pid") {
+      std::string s = MakeTrimmed(chars, len);
+      data_.GetProcess(current_new_process_)->pid = stoi(s);
+    } else if (current_new_core_id_ != nullptr && tag_name == "core") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_new_core_id_ = static_cast<uint32_t>(stoul(s));
+    } else if (current_subsystem_ref_ != nullptr && tag_name == "subsystem") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_subsystem_ref_ = s;
+    } else if (current_uint64_ref_ != nullptr &&
+               current_os_log_metadata_uint64_ref_ != nullptr &&
+               tag_name == "uint64") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_os_log_metadata_uint64_ref_ = *current_uint64_ref_ = stoull(s);
+    }
+  }
+
+  base::StatusOr<int64_t> ToTraceTimestamp(int64_t time) {
+    base::StatusOr<int64_t> trace_ts =
+        context_->clock_tracker->ToTraceTime(clock_, time);
+
+    if (PERFETTO_LIKELY(trace_ts.ok())) {
+      latest_timestamp_ = std::max(latest_timestamp_, *trace_ts);
+    }
+
+    return trace_ts;
+  }
+
+  StringId InternString(base::StringView string_view) {
+    return context_->storage->InternString(string_view);
+  }
+  StringId InternString(const char* string) {
+    return InternString(base::StringView(string));
+  }
+  StringId InternString(const char* data, size_t len) {
+    return InternString(base::StringView(data, len));
+  }
+
+  template <typename Value>
+  struct MaybeCachedRef {
+    Value& ref;
+    bool is_new;
+  };
+  // Implement the element caching mechanism. Either insert an element by its
+  // id attribute into the given map, or look up the element in the cache by its
+  // ref attribute. The returned value is a reference into the map, to allow
+  // in-place modification.
+  template <typename Value>
+  MaybeCachedRef<Value> GetOrInsertByRef(const char** attrs,
+                                         std::map<unsigned long, Value>& map) {
+    PERFETTO_DCHECK(attrs[0] != nullptr);
+    PERFETTO_DCHECK(attrs[1] != nullptr);
+    const char* key = attrs[0];
+    // The id or ref attribute has to be the first attribute on the element.
+    PERFETTO_DCHECK(strcmp(key, "ref") == 0 || strcmp(key, "id") == 0);
+    unsigned long id = strtoul(attrs[1], nullptr, 10);
+    // If the first attribute key is `id`, then this is a new entry in the
+    // cache -- otherwise, for lookup by ref, it should already exist.
+    bool is_new = strcmp(key, "id") == 0;
+    PERFETTO_DCHECK(is_new == (map.find(id) == map.end()));
+    return {map[id], is_new};
+  }
+
+  TraceProcessorContext* context_;
+  RowDataTracker& data_;
+
+  XML_Parser parser_;
+  std::vector<std::string> tag_stack_;
+  int64_t latest_timestamp_;
+
+  // These maps store the cached element data. These currently have to be
+  // std::map, because they require pointer stability under insertion,
+  // as the various `current_foo_` pointers below point directly into the map
+  // data.
+  //
+  // TODO(leszeks): Relax this pointer stability requirement, and use
+  // base::FlatHashMap.
+  // TODO(leszeks): Consider merging these into a single map from ID to
+  // a variant (or similar).
+  std::map<unsigned long, ThreadId> thread_ref_to_thread_;
+  std::map<unsigned long, ProcessId> process_ref_to_process_;
+  std::map<unsigned long, uint32_t> core_ref_to_core_;
+  std::map<unsigned long, int64_t> sample_time_ref_to_time_;
+  std::map<unsigned long, BinaryId> binary_ref_to_binary_;
+  std::map<unsigned long, BacktraceFrameId> frame_ref_to_frame_;
+  std::map<unsigned long, BacktraceId> backtrace_ref_to_backtrace_;
+  std::map<unsigned long, std::string> subsystem_ref_to_subsystem_;
+  std::map<unsigned long, uint64_t> os_log_metadata_or_uint64_ref_to_uint64_;
+
+  Row current_row_;
+  int64_t* current_time_ref_ = nullptr;
+  ThreadId current_new_thread_ = kNullId;
+  ProcessId current_new_process_ = kNullId;
+  uint32_t* current_new_core_id_ = nullptr;
+  BacktraceFrameId current_new_frame_ = kNullId;
+
+  ClockTracker::ClockId clock_;
+  std::string* current_subsystem_ref_ = nullptr;
+  uint64_t* current_os_log_metadata_uint64_ref_ = nullptr;
+  uint64_t* current_uint64_ref_ = nullptr;
+  uint64_t latest_clock_sync_timestamp_ = 0;
+};
+
+InstrumentsXmlTokenizer::InstrumentsXmlTokenizer(TraceProcessorContext* context)
+    : impl_(new Impl(context)) {}
+InstrumentsXmlTokenizer::~InstrumentsXmlTokenizer() {
+  delete impl_;
+}
+
+base::Status InstrumentsXmlTokenizer::Parse(TraceBlobView view) {
+  return impl_->Parse(std::move(view));
+}
+
+[[nodiscard]] base::Status InstrumentsXmlTokenizer::NotifyEndOfFile() {
+  return impl_->End();
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h
new file mode 100644
index 0000000..be044dd
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+class InstrumentsXmlTokenizer : public ChunkedTraceReader {
+ public:
+  explicit InstrumentsXmlTokenizer(TraceProcessorContext*);
+  ~InstrumentsXmlTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+
+  [[nodiscard]] base::Status NotifyEndOfFile() override;
+
+ private:
+  class Impl;
+
+  class Impl* impl_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
diff --git a/src/trace_processor/importers/instruments/row.h b/src/trace_processor/importers/instruments/row.h
new file mode 100644
index 0000000..531a36d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/util/build_id.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+// TODO(leszeks): Would be nice if these were strong type aliases, to be
+// type safe.
+using ThreadId = uint32_t;
+using ProcessId = uint32_t;
+using BacktraceId = uint32_t;
+using BacktraceFrameId = uint32_t;
+using BinaryId = uint32_t;
+
+constexpr uint32_t kNullId = 0u;
+
+struct Binary {
+  std::string path;
+  BuildId uuid = BuildId::FromRaw(std::string(""));
+  long long load_addr = 0;
+  long long max_addr = 0;
+};
+
+struct Frame {
+  long long addr = 0;
+  BinaryId binary = kNullId;
+};
+
+struct Process {
+  int pid = 0;
+  StringPool::Id fmt = StringPool::Id::Null();
+};
+
+struct Thread {
+  int tid = 0;
+  StringPool::Id fmt = StringPool::Id::Null();
+  ProcessId process = kNullId;
+};
+
+struct Backtrace {
+  std::vector<BacktraceFrameId> frames;
+};
+
+struct alignas(8) Row {
+  int64_t timestamp_;
+  uint32_t core_id;
+  ThreadId thread = kNullId;
+  BacktraceId backtrace = kNullId;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
diff --git a/src/trace_processor/importers/instruments/row_data_tracker.cc b/src/trace_processor/importers/instruments/row_data_tracker.cc
new file mode 100644
index 0000000..ff17b7d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_data_tracker.cc
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+
+#include "perfetto/base/status.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+#error \
+    "This file should not be built when enable_perfetto_trace_processor_mac_instruments=false"
+#endif
+
+namespace perfetto::trace_processor::instruments_importer {
+
+RowDataTracker::RowDataTracker() {}
+RowDataTracker::~RowDataTracker() = default;
+
+IdPtr<Thread> RowDataTracker::NewThread() {
+  ThreadId id = static_cast<ThreadId>(threads_.size());
+  Thread* ptr = &threads_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Thread* RowDataTracker::GetThread(ThreadId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &threads_[id - 1];
+}
+
+IdPtr<Process> RowDataTracker::NewProcess() {
+  ProcessId id = static_cast<ProcessId>(processes_.size());
+  Process* ptr = &processes_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Process* RowDataTracker::GetProcess(ProcessId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &processes_[id - 1];
+}
+
+IdPtr<Frame> RowDataTracker::NewFrame() {
+  BacktraceFrameId id = static_cast<BacktraceFrameId>(frames_.size());
+  Frame* ptr = &frames_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Frame* RowDataTracker::GetFrame(BacktraceFrameId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &frames_[id - 1];
+}
+
+IdPtr<Backtrace> RowDataTracker::NewBacktrace() {
+  BacktraceId id = static_cast<BacktraceId>(backtraces_.size());
+  Backtrace* ptr = &backtraces_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Backtrace* RowDataTracker::GetBacktrace(BacktraceId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &backtraces_[id - 1];
+}
+
+IdPtr<Binary> RowDataTracker::NewBinary() {
+  BinaryId id = static_cast<BinaryId>(binaries_.size());
+  Binary* ptr = &binaries_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Binary* RowDataTracker::GetBinary(BinaryId id) {
+  // Frames are allowed to have null binaries.
+  if (id == kNullId)
+    return nullptr;
+  return &binaries_[id - 1];
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/row_data_tracker.h b/src/trace_processor/importers/instruments/row_data_tracker.h
new file mode 100644
index 0000000..247326e
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_data_tracker.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
+
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/types/destructible.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+template <typename T>
+struct IdPtr {
+  uint32_t id;
+  T* ptr;
+};
+
+// Keeps track of row data.
+class RowDataTracker : public Destructible {
+ public:
+  static RowDataTracker& GetOrCreate(TraceProcessorContext* context) {
+    if (!context->instruments_row_data_tracker) {
+      context->instruments_row_data_tracker.reset(new RowDataTracker());
+    }
+    return static_cast<RowDataTracker&>(*context->instruments_row_data_tracker);
+  }
+  ~RowDataTracker() override;
+
+  IdPtr<Thread> NewThread();
+  Thread* GetThread(ThreadId id);
+
+  IdPtr<Process> NewProcess();
+  Process* GetProcess(ProcessId id);
+
+  IdPtr<Frame> NewFrame();
+  Frame* GetFrame(BacktraceFrameId id);
+
+  IdPtr<Backtrace> NewBacktrace();
+  Backtrace* GetBacktrace(BacktraceId id);
+
+  IdPtr<Binary> NewBinary();
+  Binary* GetBinary(BinaryId id);
+
+ private:
+  explicit RowDataTracker();
+
+  std::vector<Thread> threads_;
+  std::vector<Process> processes_;
+  std::vector<Frame> frames_;
+  std::vector<Backtrace> backtraces_;
+  std::vector<Binary> binaries_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
diff --git a/src/trace_processor/importers/instruments/row_parser.cc b/src/trace_processor/importers/instruments/row_parser.cc
new file mode 100644
index 0000000..a5d463e
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_parser.cc
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/row_parser.h"
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+
+#if !PERFETTO_BUILDFLAG(PERFETTO_TP_INSTRUMENTS)
+#error \
+    "This file should not be built when enable_perfetto_trace_processor_mac_instruments=false"
+#endif
+
+namespace perfetto::trace_processor::instruments_importer {
+
+RowParser::RowParser(TraceProcessorContext* context)
+    : context_(context), data_(RowDataTracker::GetOrCreate(context)) {}
+
+void RowParser::ParseInstrumentsRow(int64_t ts, instruments_importer::Row row) {
+  if (!row.backtrace) {
+    return;
+  }
+
+  Thread* thread = data_.GetThread(row.thread);
+  Process* process = data_.GetProcess(thread->process);
+  uint32_t tid = static_cast<uint32_t>(thread->tid);
+  uint32_t pid = static_cast<uint32_t>(process->pid);
+
+  UniqueTid utid = context_->process_tracker->UpdateThread(tid, pid);
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
+
+  // TODO(leszeks): Avoid setting thread/process name if we've already seen this
+  // Thread* / Process*.
+  context_->process_tracker->UpdateThreadNameByUtid(utid, thread->fmt,
+                                                    ThreadNamePriority::kOther);
+  context_->process_tracker->SetProcessNameIfUnset(upid, process->fmt);
+
+  Backtrace* backtrace = data_.GetBacktrace(row.backtrace);
+  std::optional<CallsiteId> parent;
+  uint32_t depth = 0;
+  base::FlatHashMap<FrameId, CallsiteTreeNode>* frame_to_callsite = &top_frames;
+  auto leaf = backtrace->frames.rend() - 1;
+  for (auto it = backtrace->frames.rbegin(); it != backtrace->frames.rend();
+       ++it) {
+    Frame* frame = data_.GetFrame(*it);
+    Binary* binary = data_.GetBinary(frame->binary);
+
+    uint64_t rel_pc = static_cast<uint64_t>(frame->addr);
+    if (frame->binary) {
+      rel_pc -= static_cast<uint64_t>(binary->load_addr);
+    }
+
+    // For non-leaf functions, the pc will be after the end of the call. Adjust
+    // it to be within the call instruction.
+    if (rel_pc != 0 && it != leaf) {
+      --rel_pc;
+    }
+
+    auto frame_inserted = frame_to_frame_id_.Insert(*it, FrameId{0});
+    if (frame_inserted.second) {
+      auto mapping_inserted = binary_to_mapping_.Insert(frame->binary, nullptr);
+      if (mapping_inserted.second) {
+        if (binary == nullptr) {
+          *mapping_inserted.first = GetDummyMapping(upid);
+        } else {
+          BuildId build_id = binary->uuid;
+          *mapping_inserted.first =
+              &context_->mapping_tracker->CreateUserMemoryMapping(
+                  upid, {AddressRange(static_cast<uint64_t>(binary->load_addr),
+                                      static_cast<uint64_t>(binary->max_addr)),
+                         0, 0, 0, binary->path, build_id});
+        }
+      }
+      VirtualMemoryMapping* mapping = *mapping_inserted.first;
+
+      // Intern the frame with no function name -- the symbolizer will annotate
+      // frames later.
+      *frame_inserted.first =
+          mapping->InternFrame(rel_pc, base::StringView(""));
+    }
+    FrameId frame_id = *frame_inserted.first;
+
+    // Lookup the frame id in the current callsite prefix tree node.
+    auto callsite_node_inserted =
+        frame_to_callsite->Insert(frame_id, CallsiteTreeNode{});
+    if (callsite_node_inserted.second) {
+      callsite_node_inserted.first->callsite_id =
+          context_->storage->mutable_stack_profile_callsite_table()
+              ->Insert({depth, parent, frame_id})
+              .id;
+    }
+    parent = callsite_node_inserted.first->callsite_id;
+    frame_to_callsite = &callsite_node_inserted.first->next_frames;
+    depth++;
+  }
+
+  context_->storage->mutable_instruments_sample_table()->Insert(
+      {ts, utid, row.core_id, parent});
+}
+
+DummyMemoryMapping* RowParser::GetDummyMapping(UniquePid upid) {
+  if (auto it = dummy_mappings_.Find(upid); it) {
+    return *it;
+  }
+
+  DummyMemoryMapping* mapping =
+      &context_->mapping_tracker->CreateDummyMapping("");
+  dummy_mappings_.Insert(upid, mapping);
+  return mapping;
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/row_parser.h b/src/trace_processor/importers/instruments/row_parser.h
new file mode 100644
index 0000000..a5e915d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_parser.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+class RowDataTracker;
+
+class RowParser : public InstrumentsRowParser {
+ public:
+  explicit RowParser(TraceProcessorContext*);
+  ~RowParser() override = default;
+
+  void ParseInstrumentsRow(int64_t, instruments_importer::Row) override;
+
+ private:
+  DummyMemoryMapping* GetDummyMapping(UniquePid upid);
+
+  TraceProcessorContext* context_;
+  RowDataTracker& data_;
+
+  // Cache FrameId and binary mappings by instruments frame and binary
+  // pointers, respectively. These are already de-duplicated in the
+  // instruments XML parsing.
+  base::FlatHashMap<BacktraceFrameId, FrameId> frame_to_frame_id_;
+  base::FlatHashMap<BinaryId, VirtualMemoryMapping*> binary_to_mapping_;
+  base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
+
+  // Cache callsites by FrameId in a prefix tree, where children in the
+  // prefix tree are child frames at the callsite. This should be more
+  // efficient than looking up frame+parent pairs in a hashmap.
+  // TODO(leszeks): Verify that this is more efficient and share the code
+  // with other importers.
+  struct CallsiteTreeNode {
+    CallsiteId callsite_id{0};
+    base::FlatHashMap<FrameId, CallsiteTreeNode> next_frames{};
+  };
+  base::FlatHashMap<FrameId, CallsiteTreeNode> top_frames;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
diff --git a/src/trace_processor/importers/perf/record_parser.cc b/src/trace_processor/importers/perf/record_parser.cc
index a63a5bd..0b0d169 100644
--- a/src/trace_processor/importers/perf/record_parser.cc
+++ b/src/trace_processor/importers/perf/record_parser.cc
@@ -225,7 +225,7 @@
       context_->storage->IncrementStats(stats::perf_dummy_mapping_used);
       // Simpleperf will not create mappings for anonymous executable mappings
       // which are used by JITted code (e.g. V8 JavaScript).
-      mapping = mapping_tracker_->GetDummyMapping();
+      mapping = GetDummyMapping(upid);
     }
 
     const FrameId frame_id =
@@ -346,4 +346,14 @@
   return base::OkStatus();
 }
 
+DummyMemoryMapping* RecordParser::GetDummyMapping(UniquePid upid) {
+  if (auto it = dummy_mappings_.Find(upid); it) {
+    return *it;
+  }
+
+  DummyMemoryMapping* mapping = &mapping_tracker_->CreateDummyMapping("");
+  dummy_mappings_.Insert(upid, mapping);
+  return mapping;
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/record_parser.h b/src/trace_processor/importers/perf/record_parser.h
index 76926d1..6230845 100644
--- a/src/trace_processor/importers/perf/record_parser.h
+++ b/src/trace_processor/importers/perf/record_parser.h
@@ -22,6 +22,7 @@
 #include <optional>
 
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/perf/mmap_record.h"
 #include "src/trace_processor/importers/perf/record.h"
@@ -31,6 +32,7 @@
 namespace perfetto {
 namespace trace_processor {
 
+class DummyMemoryMapping;
 class MappingTracker;
 class TraceProcessorContext;
 
@@ -66,8 +68,11 @@
 
   UniquePid GetUpid(const CommonMmapRecordFields& fields) const;
 
-  TraceProcessorContext* const context_ = nullptr;
+  DummyMemoryMapping* GetDummyMapping(UniquePid upid);
+
+  TraceProcessorContext* const context_;
   MappingTracker* const mapping_tracker_;
+  base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
index 68a30bd..393bd79 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
@@ -33,6 +33,7 @@
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -157,6 +158,18 @@
   context_->args_tracker->Flush();
 }
 
+void ProtoTraceParserImpl::ParseLegacyV8ProfileEvent(
+    int64_t ts,
+    LegacyV8CpuProfileEvent event) {
+  base::Status status = context_->legacy_v8_cpu_profile_tracker->AddSample(
+      ts, event.session_id, event.pid, event.tid, event.callsite_id);
+  if (!status.ok()) {
+    context_->storage->IncrementStats(
+        stats::legacy_v8_cpu_profile_invalid_sample);
+  }
+  context_->args_tracker->Flush();
+}
+
 void ProtoTraceParserImpl::ParseChromeEvents(int64_t ts, ConstBytes blob) {
   TraceStorage* storage = context_->storage.get();
   protos::pbzero::ChromeEventBundle::Decoder bundle(blob.data, blob.size);
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.h b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
index 0c1db93..f6b2304 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.h
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
@@ -61,6 +61,8 @@
                               int64_t /*ts*/,
                               InlineSchedWaking data) override;
 
+  void ParseLegacyV8ProfileEvent(int64_t ts, LegacyV8CpuProfileEvent) override;
+
  private:
   StringId GetMetatraceInternedString(uint64_t iid);
 
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 96c367c..360126c 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -39,6 +39,7 @@
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index be85689..268f45f 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -23,6 +23,7 @@
 #include <string>
 #include <utility>
 
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
@@ -34,6 +35,7 @@
 #include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -288,6 +290,15 @@
     return ModuleResult::Handled();
   }
 
+  // Handle legacy sample events which might have timestamps embedded inside.
+  if (PERFETTO_UNLIKELY(event.has_legacy_event())) {
+    protos::pbzero::TrackEvent::LegacyEvent::Decoder leg(event.legacy_event());
+    if (PERFETTO_UNLIKELY(leg.phase() == 'P')) {
+      RETURN_IF_ERROR(TokenizeLegacySampleEvent(
+          event, leg, *data.trace_packet_data.sequence_state));
+    }
+  }
+
   if (event.has_thread_time_delta_us()) {
     // Delta timestamps require a valid ThreadDescriptor packet since the last
     // packet loss.
@@ -437,4 +448,76 @@
   return base::OkStatus();
 }
 
+base::Status TrackEventTokenizer::TokenizeLegacySampleEvent(
+    const protos::pbzero::TrackEvent::Decoder& event,
+    const protos::pbzero::TrackEvent::LegacyEvent::Decoder& legacy,
+    PacketSequenceStateGeneration& state) {
+  // We are just trying to parse out the V8 profiling events into the cpu
+  // sampling tables: if we don't have JSON enabled, just don't do this.
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  for (auto it = event.debug_annotations(); it; ++it) {
+    protos::pbzero::DebugAnnotation::Decoder da(*it);
+    auto* interned_name = state.LookupInternedMessage<
+        protos::pbzero::InternedData::kDebugAnnotationNamesFieldNumber,
+        protos::pbzero::DebugAnnotationName>(da.name_iid());
+    base::StringView name(interned_name->name());
+    if (name != "data" || !da.has_legacy_json_value()) {
+      continue;
+    }
+    auto opt_val = json::ParseJsonString(da.legacy_json_value());
+    if (!opt_val) {
+      continue;
+    }
+    const auto& val = *opt_val;
+    if (val.isMember("startTime")) {
+      ASSIGN_OR_RETURN(int64_t ts, context_->clock_tracker->ToTraceTime(
+                                       protos::pbzero::BUILTIN_CLOCK_MONOTONIC,
+                                       val["startTime"].asInt64() * 1000));
+      context_->legacy_v8_cpu_profile_tracker->SetStartTsForSessionAndPid(
+          legacy.unscoped_id(), static_cast<uint32_t>(state.pid()), ts);
+      continue;
+    }
+    const auto& profile = val["cpuProfile"];
+    for (const auto& n : profile["nodes"]) {
+      uint32_t node_id = n["id"].asUInt();
+      std::optional<uint32_t> parent_node_id =
+          n.isMember("parent") ? std::make_optional(n["parent"].asUInt())
+                               : std::nullopt;
+      const auto& frame = n["callFrame"];
+      base::StringView url =
+          frame.isMember("url") ? frame["url"].asCString() : base::StringView();
+      base::StringView function_name = frame["functionName"].asCString();
+      base::Status status =
+          context_->legacy_v8_cpu_profile_tracker->AddCallsite(
+              legacy.unscoped_id(), static_cast<uint32_t>(state.pid()), node_id,
+              parent_node_id, url, function_name);
+      if (!status.ok()) {
+        context_->storage->IncrementStats(
+            stats::legacy_v8_cpu_profile_invalid_callsite);
+        continue;
+      }
+    }
+    const auto& samples = profile["samples"];
+    const auto& deltas = val["timeDeltas"];
+    if (samples.size() != deltas.size()) {
+      return base::ErrStatus(
+          "v8 legacy profile: samples and timestamps do not have same size");
+    }
+    for (uint32_t i = 0; i < samples.size(); ++i) {
+      ASSIGN_OR_RETURN(
+          int64_t ts,
+          context_->legacy_v8_cpu_profile_tracker->AddDeltaAndGetTs(
+              legacy.unscoped_id(), static_cast<uint32_t>(state.pid()),
+              deltas[i].asInt64() * 1000));
+      context_->sorter->PushLegacyV8CpuProfileEvent(
+          ts, legacy.unscoped_id(), static_cast<uint32_t>(state.pid()),
+          static_cast<uint32_t>(state.tid()), samples[i].asUInt());
+    }
+  }
+#else
+  base::ignore_result(event, legacy, state);
+#endif
+  return base::OkStatus();
+}
+
 }  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.h b/src/trace_processor/importers/proto/track_event_tokenizer.h
index 743258c..0627562 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.h
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.h
@@ -78,6 +78,10 @@
       protozero::RepeatedFieldIterator<T> value_it,
       protozero::RepeatedFieldIterator<uint64_t> packet_track_uuid_it,
       protozero::RepeatedFieldIterator<uint64_t> default_track_uuid_it);
+  base::Status TokenizeLegacySampleEvent(
+      const protos::pbzero::TrackEvent_Decoder&,
+      const protos::pbzero::TrackEvent_LegacyEvent_Decoder&,
+      PacketSequenceStateGeneration& state);
 
   TraceProcessorContext* context_;
   TrackEventTracker* track_event_tracker_;
diff --git a/src/trace_processor/importers/proto/winscope/BUILD.gn b/src/trace_processor/importers/proto/winscope/BUILD.gn
index ab09809..e04c4f0 100644
--- a/src/trace_processor/importers/proto/winscope/BUILD.gn
+++ b/src/trace_processor/importers/proto/winscope/BUILD.gn
@@ -20,8 +20,6 @@
     "android_input_event_parser.h",
     "protolog_message_decoder.cc",
     "protolog_message_decoder.h",
-    "protolog_messages_tracker.cc",
-    "protolog_messages_tracker.h",
     "protolog_parser.cc",
     "protolog_parser.h",
     "shell_transitions_parser.cc",
diff --git a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc
index de82a0e..71bca76 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc
+++ b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.cc
@@ -26,7 +26,8 @@
 
 namespace perfetto::trace_processor {
 
-ProtoLogMessageDecoder::ProtoLogMessageDecoder() = default;
+ProtoLogMessageDecoder::ProtoLogMessageDecoder(TraceProcessorContext* context)
+    : context_(context) {}
 ProtoLogMessageDecoder::~ProtoLogMessageDecoder() = default;
 
 std::optional<DecodedMessage> ProtoLogMessageDecoder::Decode(
@@ -123,6 +124,11 @@
 }
 
 void ProtoLogMessageDecoder::TrackGroup(uint32_t id, const std::string& tag) {
+  auto tracked_group = tracked_groups_.Find(id);
+  if (tracked_group != nullptr && tracked_group->tag != tag) {
+    context_->storage->IncrementStats(
+            stats::winscope_protolog_view_config_collision);
+  }
   tracked_groups_.Insert(id, TrackedGroup{tag});
 }
 
@@ -132,6 +138,11 @@
     uint32_t group_id,
     const std::string& message,
     const std::optional<std::string>& location) {
+  auto tracked_message = tracked_messages_.Find(message_id);
+  if (tracked_message != nullptr && tracked_message->message != message) {
+    context_->storage->IncrementStats(
+            stats::winscope_protolog_view_config_collision);
+  }
   tracked_messages_.Insert(message_id,
                            TrackedMessage{level, group_id, message, location});
 }
diff --git a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h
index 8955c01..c99dd43 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h
+++ b/src/trace_processor/importers/proto/winscope/protolog_message_decoder.h
@@ -59,12 +59,12 @@
 
 class ProtoLogMessageDecoder : public Destructible {
  public:
-  explicit ProtoLogMessageDecoder();
+  explicit ProtoLogMessageDecoder(TraceProcessorContext* context);
   virtual ~ProtoLogMessageDecoder() override;
 
   static ProtoLogMessageDecoder* GetOrCreate(TraceProcessorContext* context) {
     if (!context->protolog_message_decoder) {
-      context->protolog_message_decoder.reset(new ProtoLogMessageDecoder());
+      context->protolog_message_decoder.reset(new ProtoLogMessageDecoder(context));
     }
     return static_cast<ProtoLogMessageDecoder*>(
         context->protolog_message_decoder.get());
@@ -86,6 +86,7 @@
                     const std::optional<std::string>& location);
 
  private:
+  TraceProcessorContext* const context_;
   base::FlatHashMap<uint64_t, TrackedGroup> tracked_groups_;
   base::FlatHashMap<uint64_t, TrackedMessage> tracked_messages_;
 };
diff --git a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
deleted file mode 100644
index 2cf095a..0000000
--- a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.cc
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
-
-#include <cstdint>
-#include <optional>
-#include <vector>
-
-namespace perfetto::trace_processor {
-
-ProtoLogMessagesTracker::ProtoLogMessagesTracker() = default;
-ProtoLogMessagesTracker::~ProtoLogMessagesTracker() = default;
-
-void ProtoLogMessagesTracker::TrackMessage(
-    TrackedProtoLogMessage tracked_protolog_message) {
-  tracked_protolog_messages
-      .Insert(tracked_protolog_message.message_id,
-              std::vector<TrackedProtoLogMessage>())
-      .first->emplace_back(tracked_protolog_message);
-}
-
-std::optional<std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
-ProtoLogMessagesTracker::GetTrackedMessagesByMessageId(uint64_t message_id) {
-  auto* tracked_messages = tracked_protolog_messages.Find(message_id);
-  if (tracked_messages == nullptr) {
-    // No tracked messages found for this id
-    return std::nullopt;
-  }
-  return tracked_messages;
-}
-
-void ProtoLogMessagesTracker::ClearTrackedMessagesForMessageId(
-    uint64_t message_id) {
-  tracked_protolog_messages.Erase(message_id);
-}
-
-}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h b/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
deleted file mode 100644
index 3a34ffb..0000000
--- a/src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
-
-#include <cstdint>
-#include <optional>
-#include <string>
-#include <vector>
-
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/winscope_tables_py.h"
-#include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-
-namespace perfetto::trace_processor {
-
-class ProtoLogMessagesTracker : public Destructible {
- public:
-  explicit ProtoLogMessagesTracker();
-  virtual ~ProtoLogMessagesTracker() override;
-
-  struct TrackedProtoLogMessage {
-    uint64_t message_id;
-    std::vector<int64_t> sint64_params;
-    std::vector<double> double_params;
-    std::vector<bool> boolean_params;
-    std::vector<std::string> string_params;
-    std::optional<StringId> stacktrace;
-    tables::ProtoLogTable::Id table_row_id;
-    int64_t timestamp;
-  };
-
-  static ProtoLogMessagesTracker* GetOrCreate(TraceProcessorContext* context) {
-    if (!context->protolog_messages_tracker) {
-      context->protolog_messages_tracker.reset(new ProtoLogMessagesTracker());
-    }
-    return static_cast<ProtoLogMessagesTracker*>(
-        context->protolog_messages_tracker.get());
-  }
-
-  void TrackMessage(TrackedProtoLogMessage tracked_protolog_message);
-  std::optional<std::vector<ProtoLogMessagesTracker::TrackedProtoLogMessage>*>
-  GetTrackedMessagesByMessageId(uint64_t message_id);
-  void ClearTrackedMessagesForMessageId(uint64_t message_id);
-
- private:
-  base::FlatHashMap<uint64_t, std::vector<TrackedProtoLogMessage>>
-      tracked_protolog_messages;
-};
-
-}  // namespace perfetto::trace_processor
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_WINSCOPE_PROTOLOG_MESSAGES_TRACKER_H_
diff --git a/src/trace_processor/importers/proto/winscope/protolog_parser.cc b/src/trace_processor/importers/proto/winscope/protolog_parser.cc
index 0df2967..9233080 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_parser.cc
+++ b/src/trace_processor/importers/proto/winscope/protolog_parser.cc
@@ -35,7 +35,6 @@
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/winscope/protolog_message_decoder.h"
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
 #include "src/trace_processor/importers/proto/winscope/winscope.descriptor.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -134,33 +133,19 @@
         row_id, decoded_message.log_level, decoded_message.group_tag,
         decoded_message.message, stacktrace, location);
   } else {
-    // Viewer config used to decode messages not yet processed for this message.
-    // Delaying decoding...
-    auto* protolog_message_tracker =
-        ProtoLogMessagesTracker::GetOrCreate(context_);
-
-    protolog_message_tracker->TrackMessage(
-        ProtoLogMessagesTracker::TrackedProtoLogMessage{
-            protolog_message.message_id(), std::move(sint64_params),
-            std::move(double_params), std::move(boolean_params),
-            std::move(string_params), stacktrace, row_id, timestamp});
+    // Failed to fully decode the message.
+    // This shouldn't happen since we should have processed all viewer config
+    // messages in the tokenization state, and process the protolog messages
+    // only in the parsing state.
+    context_->storage->IncrementStats(
+        stats::winscope_protolog_message_decoding_failed);
   }
 }
 
-void ProtoLogParser::ParseProtoLogViewerConfig(protozero::ConstBytes blob) {
+void ProtoLogParser::ParseAndAddViewerConfigToMessageDecoder(
+    protozero::ConstBytes blob) {
   protos::pbzero::ProtoLogViewerConfig::Decoder protolog_viewer_config(blob);
 
-  AddViewerConfigToMessageDecoder(protolog_viewer_config);
-
-  for (auto it = protolog_viewer_config.messages(); it; ++it) {
-    protos::pbzero::ProtoLogViewerConfig::MessageData::Decoder message_data(
-        *it);
-    ProcessPendingMessagesWithId(message_data.message_id());
-  }
-}
-
-void ProtoLogParser::AddViewerConfigToMessageDecoder(
-    protos::pbzero::ProtoLogViewerConfig::Decoder& protolog_viewer_config) {
   auto* protolog_message_decoder =
       ProtoLogMessageDecoder::GetOrCreate(context_);
 
@@ -186,38 +171,6 @@
   }
 }
 
-void ProtoLogParser::ProcessPendingMessagesWithId(uint64_t message_id) {
-  auto* protolog_message_decoder =
-      ProtoLogMessageDecoder::GetOrCreate(context_);
-  auto* protolog_message_tracker =
-      ProtoLogMessagesTracker::GetOrCreate(context_);
-
-  auto tracked_messages_opt =
-      protolog_message_tracker->GetTrackedMessagesByMessageId(message_id);
-
-  if (tracked_messages_opt.has_value()) {
-    // There are undecoded messages that can now be docoded to populate the
-    // table.
-    for (const auto& tracked_message : *tracked_messages_opt.value()) {
-      auto message = protolog_message_decoder
-                         ->Decode(tracked_message.message_id,
-                                  tracked_message.sint64_params,
-                                  tracked_message.double_params,
-                                  tracked_message.boolean_params,
-                                  tracked_message.string_params)
-                         .value();
-
-      std::optional<std::string> location = message.location;
-      PopulateReservedRowWithMessage(
-          tracked_message.table_row_id, message.log_level, message.group_tag,
-          message.message, tracked_message.stacktrace, location);
-    }
-
-    // Clear to avoid decoding again
-    protolog_message_tracker->ClearTrackedMessagesForMessageId(message_id);
-  }
-}
-
 void ProtoLogParser::PopulateReservedRowWithMessage(
     tables::ProtoLogTable::Id table_row_id,
     ProtoLogLevel log_level,
diff --git a/src/trace_processor/importers/proto/winscope/protolog_parser.h b/src/trace_processor/importers/proto/winscope/protolog_parser.h
index b7e0e5f..13348d6 100644
--- a/src/trace_processor/importers/proto/winscope/protolog_parser.h
+++ b/src/trace_processor/importers/proto/winscope/protolog_parser.h
@@ -23,7 +23,6 @@
 
 #include "protos/perfetto/trace/android/protolog.pbzero.h"
 #include "src/trace_processor/importers/proto/winscope/protolog_message_decoder.h"
-#include "src/trace_processor/importers/proto/winscope/protolog_messages_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/util/descriptors.h"
 #include "src/trace_processor/util/proto_to_args_parser.h"
@@ -38,12 +37,9 @@
   void ParseProtoLogMessage(PacketSequenceStateGeneration* sequence_state,
                             protozero::ConstBytes,
                             int64_t timestamp);
-  void ParseProtoLogViewerConfig(protozero::ConstBytes);
+  void ParseAndAddViewerConfigToMessageDecoder(protozero::ConstBytes);
 
  private:
-  void AddViewerConfigToMessageDecoder(
-      protos::pbzero::ProtoLogViewerConfig::Decoder& protolog_viewer_config);
-  void ProcessPendingMessagesWithId(uint64_t message_id);
   void PopulateReservedRowWithMessage(tables::ProtoLogTable::Id table_row_id,
                                       ProtoLogLevel level,
                                       std::string& group_tag,
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.cc b/src/trace_processor/importers/proto/winscope/winscope_module.cc
index 9946a23..ac81193 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.cc
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.cc
@@ -49,6 +49,23 @@
                                  kWinscopeDescriptor.size());
 }
 
+ModuleResult WinscopeModule::TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* /*packet*/,
+    int64_t /*packet_timestamp*/,
+    RefPtr<PacketSequenceStateGeneration> /*state*/,
+    uint32_t field_id) {
+
+  switch (field_id) {
+    case TracePacket::kProtologViewerConfigFieldNumber:
+      protolog_parser_.ParseAndAddViewerConfigToMessageDecoder(
+          decoder.protolog_viewer_config());
+      return ModuleResult::Handled();
+  }
+
+  return ModuleResult::Ignored();
+}
+
 void WinscopeModule::ParseTracePacketData(const TracePacket::Decoder& decoder,
                                           int64_t timestamp,
                                           const TracePacketData& data,
@@ -73,10 +90,6 @@
       protolog_parser_.ParseProtoLogMessage(
           data.sequence_state.get(), decoder.protolog_message(), timestamp);
       return;
-    case TracePacket::kProtologViewerConfigFieldNumber:
-      protolog_parser_.ParseProtoLogViewerConfig(
-          decoder.protolog_viewer_config());
-      return;
     case TracePacket::kWinscopeExtensionsFieldNumber:
       ParseWinscopeExtensionsData(decoder.winscope_extensions(), timestamp,
                                   data);
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.h b/src/trace_processor/importers/proto/winscope/winscope_module.h
index 649e329..e14be59 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.h
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.h
@@ -36,6 +36,13 @@
  public:
   explicit WinscopeModule(TraceProcessorContext* context);
 
+  ModuleResult TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* packet,
+    int64_t packet_timestamp,
+    RefPtr<PacketSequenceStateGeneration> state,
+    uint32_t field_id) override;
+
   void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder&,
                             int64_t ts,
                             const TracePacketData&,
diff --git a/src/trace_processor/read_trace.cc b/src/trace_processor/read_trace.cc
index cd4050a..91e784a 100644
--- a/src/trace_processor/read_trace.cc
+++ b/src/trace_processor/read_trace.cc
@@ -94,10 +94,7 @@
     std::unique_ptr<ChunkedTraceReader> reader(
         new SerializingProtoTraceReader(output));
     GzipTraceParser parser(std::move(reader));
-
     RETURN_IF_ERROR(parser.ParseUnowned(data, size));
-    if (parser.needs_more_input())
-      return base::ErrStatus("Cannot decompress partial trace file");
     return parser.NotifyEndOfFile();
   }
 
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index 913ed83..22400e4 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -33,6 +33,7 @@
     "../importers/common:parser_types",
     "../importers/common:trace_parser_hdr",
     "../importers/fuchsia:fuchsia_record",
+    "../importers/instruments:row",
     "../importers/perf:record",
     "../importers/proto:packet_sequence_state_generation_hdr",
     "../importers/systrace:systrace_line",
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index b684e1a..924660a 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -202,6 +202,10 @@
       context.perf_record_parser->ParsePerfRecord(
           event.ts, token_buffer_.Extract<perf_importer::Record>(id));
       return;
+    case TimestampedEvent::Type::kInstrumentsRow:
+      context.instruments_row_parser->ParseInstrumentsRow(
+          event.ts, token_buffer_.Extract<instruments_importer::Row>(id));
+      return;
     case TimestampedEvent::Type::kTracePacket:
       context.proto_trace_parser->ParseTracePacket(
           event.ts, token_buffer_.Extract<TracePacketData>(id));
@@ -226,6 +230,10 @@
       context.android_log_event_parser->ParseAndroidLogEvent(
           event.ts, token_buffer_.Extract<AndroidLogEvent>(id));
       return;
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+      context.proto_trace_parser->ParseLegacyV8ProfileEvent(
+          event.ts, token_buffer_.Extract<LegacyV8CpuProfileEvent>(id));
+      return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
     case TimestampedEvent::Type::kEtwEvent:
@@ -251,9 +259,11 @@
     case TimestampedEvent::Type::kSystraceLine:
     case TimestampedEvent::Type::kTracePacket:
     case TimestampedEvent::Type::kPerfRecord:
+    case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidLogEvent:
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -281,9 +291,11 @@
     case TimestampedEvent::Type::kSystraceLine:
     case TimestampedEvent::Type::kTracePacket:
     case TimestampedEvent::Type::kPerfRecord:
+    case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidLogEvent:
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -319,9 +331,15 @@
     case TimestampedEvent::Type::kPerfRecord:
       base::ignore_result(token_buffer_.Extract<perf_importer::Record>(id));
       return;
+    case TimestampedEvent::Type::kInstrumentsRow:
+      base::ignore_result(token_buffer_.Extract<instruments_importer::Row>(id));
+      return;
     case TimestampedEvent::Type::kAndroidLogEvent:
       base::ignore_result(token_buffer_.Extract<AndroidLogEvent>(id));
       return;
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+      base::ignore_result(token_buffer_.Extract<LegacyV8CpuProfileEvent>(id));
+      return;
   }
   PERFETTO_FATAL("For GCC");
 }
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 656b283..183f376 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -38,6 +38,7 @@
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/instruments/row.h"
 #include "src/trace_processor/importers/perf/record.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/sorter/trace_token_buffer.h"
@@ -128,6 +129,15 @@
                          machine_id);
   }
 
+  inline void PushInstrumentsRow(
+      int64_t timestamp,
+      instruments_importer::Row row,
+      std::optional<MachineId> machine_id = std::nullopt) {
+    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(row));
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kInstrumentsRow, id,
+                         machine_id);
+  }
+
   inline void PushTracePacket(
       int64_t timestamp,
       TracePacketData data,
@@ -199,6 +209,21 @@
     UpdateAppendMaxTs(queue);
   }
 
+  inline void PushLegacyV8CpuProfileEvent(
+      int64_t timestamp,
+      uint64_t session_id,
+      uint32_t pid,
+      uint32_t tid,
+      uint32_t callsite_id,
+      std::optional<MachineId> machine_id = std::nullopt) {
+    TraceTokenBuffer::Id id = token_buffer_.Append(
+        LegacyV8CpuProfileEvent{session_id, pid, tid, callsite_id});
+    auto* queue = GetQueue(0, machine_id);
+    queue->Append(timestamp, TimestampedEvent::Type::kLegacyV8CpuProfileEvent,
+                  id);
+    UpdateAppendMaxTs(queue);
+  }
+
   inline void PushInlineFtraceEvent(
       uint32_t cpu,
       int64_t timestamp,
@@ -264,6 +289,7 @@
     enum class Type : uint8_t {
       kFtraceEvent,
       kPerfRecord,
+      kInstrumentsRow,
       kTracePacket,
       kInlineSchedSwitch,
       kInlineSchedWaking,
@@ -273,7 +299,8 @@
       kSystraceLine,
       kEtwEvent,
       kAndroidLogEvent,
-      kMax = kAndroidLogEvent,
+      kLegacyV8CpuProfileEvent,
+      kMax = kLegacyV8CpuProfileEvent,
     };
 
     // Number of bits required to store the max element in |Type|.
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 5d340bb..a8c7892 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -353,6 +353,12 @@
   F(winscope_protolog_missing_interned_stacktrace_parse_errors,                \
                                           kSingle,  kInfo,     kAnalysis,      \
       "Failed to find interned ProtoLog stacktrace."),                         \
+  F(winscope_protolog_message_decoding_failed,                                 \
+                                          kSingle,  kInfo,     kAnalysis,      \
+      "Failed to decode ProtoLog message."),                                   \
+  F(winscope_protolog_view_config_collision,                                   \
+                                          kSingle,  kInfo,     kAnalysis,      \
+      "Got a viewer config collision!"),                                       \
   F(winscope_viewcapture_parse_errors,                                         \
                                           kSingle,  kInfo,     kAnalysis,      \
       "ViewCapture packet has unknown fields, which results in some "          \
@@ -379,7 +385,12 @@
   F(mali_unknown_mcu_state_id,            kSingle,  kError,   kAnalysis,       \
       "An invalid Mali GPU MCU state ID was detected."),                       \
   F(pixel_modem_negative_timestamp,       kSingle,  kError,   kAnalysis,       \
-      "A negative timestamp was received from a Pixel modem event.")
+      "A negative timestamp was received from a Pixel modem event."),          \
+  F(legacy_v8_cpu_profile_invalid_callsite, kSingle,  kInfo,  kAnalysis,       \
+      "Indicates a callsite in legacy v8 CPU profiling is invalid."),          \
+  F(legacy_v8_cpu_profile_invalid_sample, kSingle,  kError,  kAnalysis,        \
+      "Indicates a sample in legacy v8 CPU profile is invalid. This will "     \
+      "cause CPU samples to be missing in the UI.")
 // clang-format on
 
 enum Type {
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index 513e251..2a07c33 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -659,6 +659,13 @@
     return &perf_sample_table_;
   }
 
+  const tables::InstrumentsSampleTable& instruments_sample_table() const {
+    return instruments_sample_table_;
+  }
+  tables::InstrumentsSampleTable* mutable_instruments_sample_table() {
+    return &instruments_sample_table_;
+  }
+
   const tables::SymbolTable& symbol_table() const { return symbol_table_; }
 
   tables::SymbolTable* mutable_symbol_table() { return &symbol_table_; }
@@ -1151,6 +1158,7 @@
       &string_pool_};
   tables::PerfSessionTable perf_session_table_{&string_pool_};
   tables::PerfSampleTable perf_sample_table_{&string_pool_};
+  tables::InstrumentsSampleTable instruments_sample_table_{&string_pool_};
   tables::PackageListTable package_list_table_{&string_pool_};
   tables::AndroidGameInterventionListTable
       android_game_intervention_list_table_{&string_pool_};
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py
index bbe4f0a..60cb1fc 100644
--- a/src/trace_processor/tables/profiler_tables.py
+++ b/src/trace_processor/tables/profiler_tables.py
@@ -281,6 +281,32 @@
                 streams (i.e. multiple data sources).'''
         }))
 
+INSTRUMENTS_SAMPLE_TABLE = Table(
+    python_module=__file__,
+    class_name='InstrumentsSampleTable',
+    sql_name='instruments_sample',
+    columns=[
+        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+        C('utid', CppUint32()),
+        C('cpu', CppOptional(CppUint32())),
+        C('callsite_id', CppOptional(CppTableId(STACK_PROFILE_CALLSITE_TABLE))),
+    ],
+    tabledoc=TableDoc(
+        doc='''
+          Samples from MacOS Instruments.
+        ''',
+        group='Callstack profilers',
+        columns={
+            'ts':
+                '''Timestamp of the sample.''',
+            'utid':
+                '''Sampled thread.''',
+            'cpu':
+                '''Core the sampled thread was running on.''',
+            'callsite_id':
+                '''If set, unwound callstack of the sampled thread.''',
+        }))
+
 SYMBOL_TABLE = Table(
     python_module=__file__,
     class_name='SymbolTable',
@@ -638,6 +664,7 @@
     HEAP_GRAPH_CLASS_TABLE,
     HEAP_GRAPH_OBJECT_TABLE,
     HEAP_GRAPH_REFERENCE_TABLE,
+    INSTRUMENTS_SAMPLE_TABLE,
     HEAP_PROFILE_ALLOCATION_TABLE,
     PACKAGE_LIST_TABLE,
     PERF_SAMPLE_TABLE,
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index 6d3c6cb..f993a51 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -73,6 +73,7 @@
 CpuProfileStackSampleTable::~CpuProfileStackSampleTable() = default;
 PerfSessionTable::~PerfSessionTable() = default;
 PerfSampleTable::~PerfSampleTable() = default;
+InstrumentsSampleTable::~InstrumentsSampleTable() = default;
 SymbolTable::~SymbolTable() = default;
 HeapProfileAllocationTable::~HeapProfileAllocationTable() = default;
 ExperimentalFlamegraphTable::~ExperimentalFlamegraphTable() = default;
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index d6493a9..53876b0 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -30,6 +30,7 @@
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
@@ -105,6 +106,8 @@
       });
 
   trace_file_tracker = std::make_unique<TraceFileTracker>(this);
+  legacy_v8_cpu_profile_tracker =
+      std::make_unique<LegacyV8CpuProfileTracker>(this);
 }
 
 TraceProcessorContext::TraceProcessorContext() = default;
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 9f101bd..bb47ab5 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -51,6 +51,9 @@
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
 #include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
+#include "src/trace_processor/importers/instruments/instruments_utils.h"
+#include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
+#include "src/trace_processor/importers/instruments/row_parser.h"
 #include "src/trace_processor/importers/json/json_trace_parser_impl.h"
 #include "src/trace_processor/importers/json/json_trace_tokenizer.h"
 #include "src/trace_processor/importers/json/json_utils.h"
@@ -355,6 +358,10 @@
     start_ns = std::min(it.ts(), start_ns);
     end_ns = std::max(it.ts(), end_ns);
   }
+  for (auto it = storage.instruments_sample_table().IterateRows(); it; ++it) {
+    start_ns = std::min(it.ts(), start_ns);
+    end_ns = std::max(it.ts(), end_ns);
+  }
   for (auto it = storage.cpu_profile_stack_sample_table().IterateRows(); it;
        ++it) {
     start_ns = std::min(it.ts(), start_ns);
@@ -394,6 +401,14 @@
   context_.perf_record_parser =
       std::make_unique<perf_importer::RecordParser>(&context_);
 
+  if (instruments_importer::IsInstrumentsSupported()) {
+    context_.reader_registry
+        ->RegisterTraceReader<instruments_importer::InstrumentsXmlTokenizer>(
+            kInstrumentsXmlTraceType);
+    context_.instruments_row_parser =
+        std::make_unique<instruments_importer::RowParser>(&context_);
+  }
+
   if (util::IsGzipSupported()) {
     context_.reader_registry->RegisterTraceReader<GzipTraceParser>(
         kGzipTraceType);
@@ -907,6 +922,7 @@
   RegisterStaticTable(storage->mutable_cpu_profile_stack_sample_table());
   RegisterStaticTable(storage->mutable_perf_session_table());
   RegisterStaticTable(storage->mutable_perf_sample_table());
+  RegisterStaticTable(storage->mutable_instruments_sample_table());
   RegisterStaticTable(storage->mutable_stack_profile_callsite_table());
   RegisterStaticTable(storage->mutable_stack_profile_mapping_table());
   RegisterStaticTable(storage->mutable_stack_profile_frame_table());
diff --git a/src/trace_processor/trace_reader_registry.cc b/src/trace_processor/trace_reader_registry.cc
index b071295..dcecaf9 100644
--- a/src/trace_processor/trace_reader_registry.cc
+++ b/src/trace_processor/trace_reader_registry.cc
@@ -37,6 +37,7 @@
     case kNinjaLogTraceType:
     case kSystraceTraceType:
     case kPerfDataTraceType:
+    case kInstrumentsXmlTraceType:
     case kUnknownTraceType:
     case kJsonTraceType:
     case kFuchsiaTraceType:
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index c448bef..3e7ba55 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -46,7 +46,9 @@
 class FuchsiaRecordParser;
 class GlobalArgsTracker;
 class HeapGraphTracker;
+class InstrumentsRowParser;
 class JsonTraceParser;
+class LegacyV8CpuProfileTracker;
 class MachineTracker;
 class MappingTracker;
 class MetadataTracker;
@@ -129,6 +131,7 @@
   std::unique_ptr<MetadataTracker> metadata_tracker;
   std::unique_ptr<CpuTracker> cpu_tracker;
   std::unique_ptr<TraceFileTracker> trace_file_tracker;
+  std::unique_ptr<LegacyV8CpuProfileTracker> legacy_v8_cpu_profile_tracker;
 
   // These fields are stored as pointers to Destructible objects rather than
   // their actual type (a subclass of Destructible), as the concrete subclass
@@ -136,25 +139,26 @@
   // the GetOrCreate() method on their subclass type, e.g.
   // SyscallTracker::GetOrCreate(context)
   // clang-format off
-  std::unique_ptr<Destructible> android_probes_tracker;    // AndroidProbesTracker
-  std::unique_ptr<Destructible> binder_tracker;            // BinderTracker
-  std::unique_ptr<Destructible> heap_graph_tracker;        // HeapGraphTracker
-  std::unique_ptr<Destructible> syscall_tracker;           // SyscallTracker
-  std::unique_ptr<Destructible> system_info_tracker;       // SystemInfoTracker
-  std::unique_ptr<Destructible> v4l2_tracker;              // V4l2Tracker
-  std::unique_ptr<Destructible> virtio_video_tracker;      // VirtioVideoTracker
-  std::unique_ptr<Destructible> systrace_parser;           // SystraceParser
-  std::unique_ptr<Destructible> thread_state_tracker;      // ThreadStateTracker
-  std::unique_ptr<Destructible> i2c_tracker;               // I2CTracker
-  std::unique_ptr<Destructible> perf_data_tracker;         // PerfDataTracker
-  std::unique_ptr<Destructible> content_analyzer;          // ProtoContentAnalyzer
-  std::unique_ptr<Destructible> shell_transitions_tracker; // ShellTransitionsTracker
-  std::unique_ptr<Destructible> protolog_messages_tracker; // ProtoLogMessagesTracker
-  std::unique_ptr<Destructible> ftrace_sched_tracker;      // FtraceSchedEventTracker
-  std::unique_ptr<Destructible> v8_tracker;                // V8Tracker
-  std::unique_ptr<Destructible> jit_tracker;               // JitTracker
-  std::unique_ptr<Destructible> perf_dso_tracker;          // DsoTracker
-  std::unique_ptr<Destructible> protolog_message_decoder;  // ProtoLogMessageDecoder
+  std::unique_ptr<Destructible> android_probes_tracker;       // AndroidProbesTracker
+  std::unique_ptr<Destructible> binder_tracker;               // BinderTracker
+  std::unique_ptr<Destructible> heap_graph_tracker;           // HeapGraphTracker
+  std::unique_ptr<Destructible> syscall_tracker;              // SyscallTracker
+  std::unique_ptr<Destructible> system_info_tracker;          // SystemInfoTracker
+  std::unique_ptr<Destructible> v4l2_tracker;                 // V4l2Tracker
+  std::unique_ptr<Destructible> virtio_video_tracker;         // VirtioVideoTracker
+  std::unique_ptr<Destructible> systrace_parser;              // SystraceParser
+  std::unique_ptr<Destructible> thread_state_tracker;         // ThreadStateTracker
+  std::unique_ptr<Destructible> i2c_tracker;                  // I2CTracker
+  std::unique_ptr<Destructible> perf_data_tracker;            // PerfDataTracker
+  std::unique_ptr<Destructible> content_analyzer;             // ProtoContentAnalyzer
+  std::unique_ptr<Destructible> shell_transitions_tracker;    // ShellTransitionsTracker
+  std::unique_ptr<Destructible> protolog_messages_tracker;    // ProtoLogMessagesTracker
+  std::unique_ptr<Destructible> ftrace_sched_tracker;         // FtraceSchedEventTracker
+  std::unique_ptr<Destructible> v8_tracker;                   // V8Tracker
+  std::unique_ptr<Destructible> jit_tracker;                  // JitTracker
+  std::unique_ptr<Destructible> perf_dso_tracker;             // DsoTracker
+  std::unique_ptr<Destructible> protolog_message_decoder;     // ProtoLogMessageDecoder
+  std::unique_ptr<Destructible> instruments_row_data_tracker; // RowDataTracker
   // clang-format on
 
   std::unique_ptr<ProtoTraceParser> proto_trace_parser;
@@ -165,6 +169,7 @@
   std::unique_ptr<JsonTraceParser> json_trace_parser;
   std::unique_ptr<FuchsiaRecordParser> fuchsia_record_parser;
   std::unique_ptr<PerfRecordParser> perf_record_parser;
+  std::unique_ptr<InstrumentsRowParser> instruments_row_parser;
   std::unique_ptr<AndroidLogEventParser> android_log_event_parser;
 
   // This field contains the list of proto descriptors that can be used by
diff --git a/src/trace_processor/util/trace_type.cc b/src/trace_processor/util/trace_type.cc
index 4c4b042..3c64c3f 100644
--- a/src/trace_processor/util/trace_type.cc
+++ b/src/trace_processor/util/trace_type.cc
@@ -119,6 +119,8 @@
       return "zip";
     case kPerfDataTraceType:
       return "perf";
+    case kInstrumentsXmlTraceType:
+      return "instruments_xml";
     case kAndroidLogcatTraceType:
       return "android_logcat";
     case kAndroidDumpstateTraceType:
@@ -172,6 +174,10 @@
       base::StartsWith(lower_start, "<html>"))
     return kSystraceTraceType;
 
+  // MacOS Instruments XML export.
+  if (base::StartsWith(start, "<?xml version=\"1.0\"?>\n<trace-query-result>"))
+    return kInstrumentsXmlTraceType;
+
   // Traces obtained from atrace -z (compress).
   // They all have the string "TRACE:" followed by 78 9C which is a zlib header
   // for "deflate, default compression, window size=32K" (see b/208691037)
diff --git a/src/trace_processor/util/trace_type.h b/src/trace_processor/util/trace_type.h
index 7e730fc..dbf72ea 100644
--- a/src/trace_processor/util/trace_type.h
+++ b/src/trace_processor/util/trace_type.h
@@ -37,6 +37,7 @@
   kSystraceTraceType,
   kUnknownTraceType,
   kZipFile,
+  kInstrumentsXmlTraceType,
 };
 
 constexpr size_t kGuessTraceMaxLookahead = 64;
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.cc b/src/traced/probes/sys_stats/sys_stats_data_source.cc
index 393be27..490881b 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.cc
@@ -146,8 +146,8 @@
     stat_enabled_fields_ |= 1ul << static_cast<uint32_t>(*counter);
   }
 
-  std::array<uint32_t, 10> periods_ms{};
-  std::array<uint32_t, 10> ticks{};
+  std::array<uint32_t, 11> periods_ms{};
+  std::array<uint32_t, 11> ticks{};
   static_assert(periods_ms.size() == ticks.size(), "must have same size");
 
   periods_ms[0] = ClampTo10Ms(cfg.meminfo_period_ms(), "meminfo_period_ms");
@@ -160,6 +160,7 @@
   periods_ms[7] = ClampTo10Ms(cfg.psi_period_ms(), "psi_period_ms");
   periods_ms[8] = ClampTo10Ms(cfg.thermal_period_ms(), "thermal_period_ms");
   periods_ms[9] = ClampTo10Ms(cfg.cpuidle_period_ms(), "cpuidle_period_ms");
+  periods_ms[10] = ClampTo10Ms(cfg.gpufreq_period_ms(), "gpufreq_period_ms");
 
   tick_period_ms_ = 0;
   for (uint32_t ms : periods_ms) {
@@ -188,6 +189,7 @@
   psi_ticks_ = ticks[7];
   thermal_ticks_ = ticks[8];
   cpuidle_ticks_ = ticks[9];
+  gpufreq_ticks_ = ticks[10];
 }
 
 void SysStatsDataSource::Start() {
@@ -249,6 +251,9 @@
   if (cpuidle_ticks_ && tick_ % cpuidle_ticks_ == 0)
     ReadCpuIdleStates(sys_stats);
 
+  if (gpufreq_ticks_ && tick_ % gpufreq_ticks_ == 0)
+    ReadGpuFrequency(sys_stats);
+
   sys_stats->set_collection_end_timestamp(
       static_cast<uint64_t>(base::GetBootTimeNs().count()));
 
@@ -376,6 +381,44 @@
   }
 }
 
+std::optional<uint64_t> SysStatsDataSource::ReadAMDGpuFreq() {
+  std::optional<std::string> amd_gpu_freq =
+      ReadFileToString("/sys/class/drm/card0/device/pp_dpm_sclk");
+  if (!amd_gpu_freq) {
+    return std::nullopt;
+  }
+  for (base::StringSplitter lines(*amd_gpu_freq, '\n'); lines.Next();) {
+    base::StringView line(lines.cur_token(), lines.cur_token_size());
+    // Current frequency indicated with asterisk.
+    if (line.EndsWith("*")) {
+      for (base::StringSplitter words(line.ToStdString(), ' '); words.Next();) {
+        if (!base::EndsWith(words.cur_token(), "Mhz"))
+          continue;
+        // Strip suffix "Mhz".
+        std::string maybe_freq = std::string(words.cur_token())
+                                     .substr(0, words.cur_token_size() - 3);
+        auto freq = base::StringToUInt32(maybe_freq);
+        return freq;
+      }
+    }
+  }
+  return std::nullopt;
+}
+
+void SysStatsDataSource::ReadGpuFrequency(protos::pbzero::SysStats* sys_stats) {
+  std::optional<uint64_t> freq;
+  // Intel GPU Frequency.
+  freq = ReadFileToUInt64("/sys/class/drm/card0/gt_act_freq_mhz");
+  if (freq) {
+    sys_stats->add_gpufreq_mhz((*freq));
+    return;
+  }
+  freq = ReadAMDGpuFreq();
+  if (freq) {
+    sys_stats->add_gpufreq_mhz((*freq));
+  }
+}
+
 void SysStatsDataSource::ReadDiskStat(protos::pbzero::SysStats* sys_stats) {
   size_t rsize = ReadFile(&diskstat_fd_, "/proc/diskstats");
   if (!rsize) {
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.h b/src/traced/probes/sys_stats/sys_stats_data_source.h
index 7e4749b..5e212f5 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.h
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.h
@@ -102,6 +102,8 @@
   void ReadPsi(protos::pbzero::SysStats* sys_stats);
   void ReadThermalZones(protos::pbzero::SysStats* sys_stats);
   void ReadCpuIdleStates(protos::pbzero::SysStats* sys_stats);
+  void ReadGpuFrequency(protos::pbzero::SysStats* sys_stats);
+  std::optional<uint64_t> ReadAMDGpuFreq();
 
   size_t ReadFile(base::ScopedFile*, const char* path);
 
@@ -133,6 +135,7 @@
   uint32_t psi_ticks_ = 0;
   uint32_t thermal_ticks_ = 0;
   uint32_t cpuidle_ticks_ = 0;
+  uint32_t gpufreq_ticks_ = 0;
 
   std::unique_ptr<CpuFreqInfo> cpu_freq_info_;
 
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc b/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
index fd5dcb2..7a934c1 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source_unittest.cc
@@ -212,7 +212,13 @@
 const char kMockThermalType[] = "TSR0";
 const uint64_t kMockCpuIdleStateTime = 10000;
 const char kMockCpuIdleStateName[] = "MOCK_STATE_NAME";
-
+const uint64_t kMockIntelGpuFreq = 300;
+// kMockAMDGpuFreq whitespace is intentional.
+const char kMockAMDGpuFreq[] = R"(
+0: 200Mhz 
+1: 400Mhz *
+2: 2000Mhz 
+)";
 class TestSysStatsDataSource : public SysStatsDataSource {
  public:
   TestSysStatsDataSource(base::TaskRunner* task_runner,
@@ -581,6 +587,51 @@
   }
 }
 
+TEST_F(SysStatsDataSourceTest, IntelGpuFrequency) {
+  DataSourceConfig config;
+  protos::gen::SysStatsConfig sys_cfg;
+  sys_cfg.set_gpufreq_period_ms(10);
+  config.set_sys_stats_config_raw(sys_cfg.SerializeAsString());
+  auto data_source = GetSysStatsDataSource(config);
+
+  EXPECT_CALL(*data_source,
+              ReadFileToUInt64("/sys/class/drm/card0/gt_act_freq_mhz"))
+      .WillRepeatedly(Return(std::optional<uint64_t>(kMockIntelGpuFreq)));
+
+  WaitTick(data_source.get());
+
+  protos::gen::TracePacket packet = writer_raw_->GetOnlyTracePacket();
+  ASSERT_TRUE(packet.has_sys_stats());
+  const auto& sys_stats = packet.sys_stats();
+  EXPECT_EQ(sys_stats.gpufreq_mhz_size(), 1);
+  uint32_t intel_gpufreq = 300;
+  EXPECT_EQ(sys_stats.gpufreq_mhz()[0], intel_gpufreq);
+}
+
+TEST_F(SysStatsDataSourceTest, AMDGpuFrequency) {
+  DataSourceConfig config;
+  protos::gen::SysStatsConfig sys_cfg;
+  sys_cfg.set_gpufreq_period_ms(10);
+  config.set_sys_stats_config_raw(sys_cfg.SerializeAsString());
+  auto data_source = GetSysStatsDataSource(config);
+
+  // Ignore other GPU freq calls.
+  EXPECT_CALL(*data_source,
+              ReadFileToUInt64("/sys/class/drm/card0/gt_act_freq_mhz"));
+  EXPECT_CALL(*data_source,
+              ReadFileToString("/sys/class/drm/card0/device/pp_dpm_sclk"))
+      .WillRepeatedly(Return(std::optional<std::string>(kMockAMDGpuFreq)));
+
+  WaitTick(data_source.get());
+
+  protos::gen::TracePacket packet = writer_raw_->GetOnlyTracePacket();
+  ASSERT_TRUE(packet.has_sys_stats());
+  const auto& sys_stats = packet.sys_stats();
+  EXPECT_EQ(sys_stats.gpufreq_mhz_size(), 1);
+  uint32_t amd_gpufreq = 400;
+  EXPECT_EQ(sys_stats.gpufreq_mhz()[0], amd_gpufreq);
+}
+
 TEST_F(SysStatsDataSourceTest, DevfreqAll) {
   DataSourceConfig config;
   protos::gen::SysStatsConfig sys_cfg;
diff --git a/src/tracing/core/in_process_shared_memory.cc b/src/tracing/core/in_process_shared_memory.cc
index 65ae53a..435bcdb 100644
--- a/src/tracing/core/in_process_shared_memory.cc
+++ b/src/tracing/core/in_process_shared_memory.cc
@@ -21,7 +21,7 @@
 InProcessSharedMemory::~InProcessSharedMemory() = default;
 InProcessSharedMemory::Factory::~Factory() = default;
 
-void* InProcessSharedMemory::start() const {
+const void* InProcessSharedMemory::start() const {
   return mem_.Get();
 }
 size_t InProcessSharedMemory::size() const {
diff --git a/src/tracing/core/in_process_shared_memory.h b/src/tracing/core/in_process_shared_memory.h
index 353d44b..9ac692f 100644
--- a/src/tracing/core/in_process_shared_memory.h
+++ b/src/tracing/core/in_process_shared_memory.h
@@ -42,7 +42,8 @@
   }
 
   // SharedMemory implementation.
-  void* start() const override;
+  using SharedMemory::start;  // Equal priority to const and non-const versions
+  const void* start() const override;
   size_t size() const override;
 
   class Factory : public SharedMemory::Factory {
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index 4ad7f44..6658512 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -30,6 +30,9 @@
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
+#include <os/signpost.h>
+#endif
 
 using perfetto::protos::pbzero::ClockSnapshot;
 
@@ -420,6 +423,18 @@
           thread_time_counter_track.uuid);
     }
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
+    // Emit a MacOS point-of-interest signpost to synchonize Mac profiler time
+    // with boot time.
+    // TODO(leszeks): Consider allowing synchronization against other clocks
+    // than boot time.
+    static os_log_t log_handle = os_log_create(
+        "dev.perfetto.clock_sync", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
+    os_signpost_event_emit(
+        log_handle, OS_SIGNPOST_ID_EXCLUSIVE, "boottime", "%" PRId64,
+        static_cast<uint64_t>(perfetto::base::GetBootTimeNs().count()));
+#endif
+
     if (tls_state.default_clock != static_cast<uint32_t>(GetClockId())) {
       ClockSnapshot* clocks = packet->set_clock_snapshot();
       // Trace clock.
diff --git a/src/tracing/ipc/posix_shared_memory.h b/src/tracing/ipc/posix_shared_memory.h
index d620991..d7b9e20 100644
--- a/src/tracing/ipc/posix_shared_memory.h
+++ b/src/tracing/ipc/posix_shared_memory.h
@@ -61,7 +61,8 @@
   int fd() const { return fd_.get(); }
 
   // SharedMemory implementation.
-  void* start() const override { return start_; }
+  using SharedMemory::start;  // Equal priority to const and non-const versions
+  const void* start() const override { return start_; }
   size_t size() const override { return size_; }
 
  private:
diff --git a/src/tracing/test/aligned_buffer_test.h b/src/tracing/test/aligned_buffer_test.h
index 18ee4ba..d604f4e 100644
--- a/src/tracing/test/aligned_buffer_test.h
+++ b/src/tracing/test/aligned_buffer_test.h
@@ -20,6 +20,7 @@
 #include <stdlib.h>
 
 #include <memory>
+#include <utility>
 
 #include "perfetto/ext/base/utils.h"
 #include "src/tracing/test/test_shared_memory.h"
@@ -34,7 +35,10 @@
   void SetUp() override;
   void TearDown() override;
 
-  uint8_t* buf() const { return reinterpret_cast<uint8_t*>(buf_->start()); }
+  uint8_t* buf() { return const_cast<uint8_t*>(std::as_const(*this).buf()); }
+  const uint8_t* buf() const {
+    return reinterpret_cast<const uint8_t*>(buf_->start());
+  }
   size_t buf_size() const { return buf_->size(); }
   size_t page_size() const { return page_size_; }
 
diff --git a/test/data/instruments_trace.xml.sha256 b/test/data/instruments_trace.xml.sha256
new file mode 100644
index 0000000..f524f24
--- /dev/null
+++ b/test/data/instruments_trace.xml.sha256
@@ -0,0 +1 @@
+1f87b2e3f5617f947c3c22fe2282e3341ed4d3b4f37ad2e6752c6dd54836db8a
\ No newline at end of file
diff --git a/test/data/instruments_trace_symbols.pb.sha256 b/test/data/instruments_trace_symbols.pb.sha256
new file mode 100644
index 0000000..8e33c19
--- /dev/null
+++ b/test/data/instruments_trace_symbols.pb.sha256
@@ -0,0 +1 @@
+1f5096a97bd9b7176b1ec52863d02e3e85e19eedbfbbe51cbbecedce8a0248cb
\ No newline at end of file
diff --git a/test/data/instruments_trace_with_symbols.zip.sha256 b/test/data/instruments_trace_with_symbols.zip.sha256
new file mode 100644
index 0000000..adbf7aa
--- /dev/null
+++ b/test/data/instruments_trace_with_symbols.zip.sha256
@@ -0,0 +1 @@
+70733124cf53b8065512204d9a72c7818ebd04afb10cf3c69cc926a2aa5ee07e
\ No newline at end of file
diff --git a/test/data/sfgate-gzip-multi-stream.json.gz.sha256 b/test/data/sfgate-gzip-multi-stream.json.gz.sha256
new file mode 100644
index 0000000..eca4350
--- /dev/null
+++ b/test/data/sfgate-gzip-multi-stream.json.gz.sha256
@@ -0,0 +1 @@
+c46a162875fd826893daec6f1271f8d98710d7ae9a096c108c4a0635b06f14ac
\ No newline at end of file
diff --git a/test/data/v8-samples.pftrace.sha256 b/test/data/v8-samples.pftrace.sha256
new file mode 100644
index 0000000..c9a7d2f
--- /dev/null
+++ b/test/data/v8-samples.pftrace.sha256
@@ -0,0 +1 @@
+564159912db8d8252562f143e6206ae9964759e16193ebd3addd04823d02a6ae
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 5840800..14199e0 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -72,6 +72,8 @@
 from diff_tests.parser.graphics.tests import GraphicsParser
 from diff_tests.parser.graphics.tests_drm_related_ftrace_events import GraphicsDrmRelatedFtraceEvents
 from diff_tests.parser.graphics.tests_gpu_trace import GraphicsGpuTrace
+from diff_tests.parser.gzip.tests import Gzip
+from diff_tests.parser.instruments.tests import Instruments
 from diff_tests.parser.json.tests import JsonParser
 from diff_tests.parser.memory.tests import MemoryParser
 from diff_tests.parser.network.tests import NetworkParser
@@ -238,6 +240,8 @@
       *Zip(index_path, 'parser/zip', 'Zip').fetch(),
       *AndroidInputEvent(index_path, 'parser/android',
                          'AndroidInputEvent').fetch(),
+      *Instruments(index_path, 'parser/instruments', 'Instruments').fetch(),
+      *Gzip(index_path, 'parser/gzip', 'Gzip').fetch(),
   ]
 
   metrics_tests = [
diff --git a/test/trace_processor/diff_tests/parser/chrome/tests_v8.py b/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
index d4a0dc4..f77a47a 100644
--- a/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
+++ b/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
@@ -100,3 +100,41 @@
 0
 """),
     )
+
+  def test_v8_cpu_samples(self):
+    return DiffTestBlueprint(
+        trace=DataPath('v8-samples.pftrace'),
+        query='''
+          include perfetto module callstacks.stack_profile;
+
+          select name, source_file, self_count
+          from _callstacks_for_cpu_profile_stack_samples!(
+            cpu_profile_stack_sample
+          )
+          where self_count > 0
+          order by self_count desc
+          limit 20
+        ''',
+        out=Csv('''
+        "name","source_file","self_count"
+        "(program)","[NULL]",17083
+        "(program)","[NULL]",15399
+        "(program)","[NULL]",9853
+        "(program)","[NULL]",9391
+        "(program)","[NULL]",7299
+        "(program)","[NULL]",5245
+        "(program)","[NULL]",2443
+        "(garbage collector)","[NULL]",107
+        "_.mg","chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=",38
+        "(garbage collector)","[NULL]",34
+        "","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",33
+        "_.m.Ddb","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",18
+        "da","https://www.google.com/",15
+        "","https://www.gstatic.com/_/mss/boq-one-google/_/js/k=boq-one-google.OneGoogleWidgetUi.en.Dv_TT86KXl4.es5.O/ck=boq-one-google.OneGoogleWidgetUi.xexmpZqkioA.L.B1.O/am=QKBgwGw/d=1/exm=_b,_tp/excm=_b,_tp,calloutview/ed=1/wt=2/ujg=1/rs=AM-SdHu61g-i-YBZiLcGm3tURf4VJO5hyA/ee=EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;Erl4fe:FloWmf;JsbNhc:Xd8iUd;LBgRLc:SdcwHb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Oj465e:KG2eXe;Pjplud:EEDORb;QGR0gd:Mlhmy;SNUn3:ZwDk9d;a56pNe:JEfCwb;cEt90b:ws9Tlc;dIoSBb:SpsfSb;eBAeSb:zbML3c;iFQyKf:QIhFr;io8t5d:yDVVkb;kMFpHd:OTA3Ae;nAFL3:s39S4;oGtAuc:sOXFj;pXdRYb:MdUzUe;qddgKe:xQtZb;sP4Vbe:VwDzFe;uY49fb:COQbmf;ul9GGd:VDovNc;wR5FRb:O1Gjze;xqZiqf:wmnU7d;yxTchf:KUM7Z;zxnPse:GkRiKb/m=ws9Tlc,n73qwf,GkRiKb,e5qFLc,IZT63,UUJqVe,O1Gjze,byfTOb,lsjVmc,xUdipf,OTA3Ae,COQbmf,fKUV3e,aurFic,U0aPgd,ZwDk9d,V3dDOb,mI3LFb,yYB61,O6y8ed,PrPYRd,MpJwZc,LEikZe,NwH0H,OmgaI,lazG7b,XVMNvd,L1AAkb,KUM7Z,Mlhmy,s39S4,lwddkf,gychg,w9hDv,EEDORb,RMhBfe,SdcwHb,aW3pY,pw70Gc,EFQ78c,Ulmmrd,ZfAoz,mdR7q,wmnU7d,xQtZb,JNoxi,kWgXee,MI6k7c,kjKdXe,BVgquf,QIhFr,ov",13
+        "updateAttrs","https://ui.perfetto.dev/v46.0-0a53e685b/frontend_bundle.js",12
+        "","https://www.gstatic.com/_/mss/boq-one-google/_/js/k=boq-one-google.OneGoogleWidgetUi.en.Dv_TT86KXl4.es5.O/am=QKBgwGw/d=1/excm=_b,_tp,calloutview/ed=1/dg=0/wt=2/ujg=1/rs=AM-SdHsuxqEW2z6uUf-9MJvUVpOyFk0ecQ/m=_b,_tp",11
+        "a._isVisible","https://ogs.google.com/widget/callout?prid=19037050&pgid=19037049&puid=6a851fbb7ce797ac&eom=1&cce=1&dc=1&origin=https%3A%2F%2Fwww.google.com&cn=callout&pid=1&spid=538&hl=en&dm=",11
+        "","chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js",11
+        "","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/ck=xjs.hd.F00K1IyvS9A.L.B1.O/am=IFEAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAIAAAEAAAAAAAASAEakAAABZ5sAMBiAAAABAAIAAQIAQDEAQAAAwQ4AAAEAQAUABQREAEKEgTgUTYAhIAwAQQoQAgUQAICQBCFCAAAAAMAACEIDDAMQKgAYBQgAAAAAEBABAAAYAA3BhAgAMAPAAAYAKICAAAhoAMQAAABgAJAgIACAtghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=0/dg=0/br=1/ujg=1/rs=ACT90oFYV6TnvY5P3NcVPbMRvVPRlxmm8A/m=sb_wiz,aa,abd,sytt,syts,sytn,syfx,sytr,sytd,sy101,syz7,syti,syz6,syto,sytq,sytm,syu7,sytb,syu8,syu9,syu0,syu4,sytj,syty,syu1,syu2,sytv,sytw,syte,sytf,sys4,syru,syrs,syrr,syth,syz5,syug,syuh,syuf,async,syvk,ifl,pHXghd,sf,sy1c2,sy1c5,sy4e0,sonic,TxCJfd,sy4e4,qzxzOb,IsdWVc,sy4e6,sy1gs,sy1d4,sy1d0,syrq,syro,syrp,syrn,syrm,sy4cl,sy4co,sy2ib,sy18p,sy18r,sy13l,sy13m,syrj,syrh,syfb,sybv,syby,sybt,sybx,sybw,sycp,spch,sys7,sys6,rtH1bd,sy1ea,sy19r,sy18g,syg9,sy1e9,sy13t,sy1e8,sy18h,sygb,sy1eb,SMquOb,sy8f,sygh,sygf,sygg,sygi,syge,sygp,sygn,sygl,sygd,sycm,sych,syck,syak,syac,syb6,syaj,syai,sya",10
+        "maybeUpdateMoreOptions","chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js",10
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/gzip/tests.py b/test/trace_processor/diff_tests/parser/gzip/tests.py
new file mode 100644
index 0000000..a0ac38f
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/gzip/tests.py
@@ -0,0 +1,39 @@
+#!/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 Csv, DataPath
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class Gzip(TestSuite):
+
+  def test_gzip_multi_stream(self):
+    return DiffTestBlueprint(
+        trace=DataPath('sfgate-gzip-multi-stream.json.gz'),
+        query='''select ts, dur, name from slice limit 10''',
+        out=Csv('''
+        "ts","dur","name"
+        2213649212614000,239000,"ThreadTimers::sharedTimerFiredInternal"
+        2213649212678000,142000,"LayoutView::hitTest"
+        2213649214331000,34000,"ThreadTimers::sharedTimerFiredInternal"
+        2213649215569000,16727000,"ThreadTimers::sharedTimerFiredInternal"
+        2213649216760000,50000,"Node::updateDistribution"
+        2213649217290000,1373000,"StyleElement::processStyleSheet"
+        2213649218908000,4862000,"Document::updateRenderTree"
+        2213649218917000,50000,"Node::updateDistribution"
+        2213649218970000,4796000,"Document::updateStyle"
+        2213649218995000,54000,"RuleSet::addRulesFromSheet"
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/instruments/tests.py b/test/trace_processor/diff_tests/parser/instruments/tests.py
new file mode 100644
index 0000000..5558ec9
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/instruments/tests.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License 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 Csv, Path, DataPath
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+# These diff tests use some locally collected trace.
+class Instruments(TestSuite):
+
+  def test_xml_stacks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace.xml'),
+        query='''
+          WITH
+            child AS (
+              SELECT
+                spc.id AS root,
+                spc.id,
+                spc.parent_id,
+                rel_pc AS path
+              FROM
+                instruments_sample s
+                JOIN stack_profile_callsite spc ON (s.callsite_id = spc.id)
+                JOIN stack_profile_frame f ON (f.id = frame_id)
+              UNION ALL
+              SELECT
+                child.root,
+                parent.id,
+                parent.parent_id,
+                COALESCE(f.rel_pc || ',', '') || child.path AS path
+              FROM
+                child
+                JOIN stack_profile_callsite parent ON (child.parent_id = parent.id)
+                LEFT JOIN stack_profile_frame f ON (f.id = frame_id)
+            )
+          SELECT
+            s.id,
+            s.ts,
+            s.utid,
+            c.path
+          FROM
+            instruments_sample s
+            JOIN child c ON s.callsite_id = c.root
+          WHERE
+            c.parent_id IS NULL
+        ''',
+        out=Csv('''
+          "id","ts","utid","path"
+          0,175685291,1,"23999,34891,37935,334037"
+          1,176684208,1,"24307,28687,265407,160467,120123,391295,336787,8955,340991,392555,136711,5707,7603,10507,207839,207495,23655,17383,23211,208391,6225"
+          2,177685166,1,"24915,16095,15891,32211,91151,26907,87887,60651,28343,29471,30159,11087,36269"
+          3,178683916,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16021"
+          4,179687000,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16005"
+          5,180683708,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16005"
+        '''))
+
+  def test_symbolized_frames(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace_with_symbols.zip'),
+        query='''
+          SELECT
+            f.id,
+            m.name,
+            m.build_id,
+            f.rel_pc,
+            s.name,
+            s.source_file,
+            s.line_number
+          FROM
+            stack_profile_frame f
+            JOIN stack_profile_mapping m ON f.mapping = m.id
+            JOIN stack_profile_symbol s ON f.symbol_set_id = s.symbol_set_id
+        ''',
+        out=Csv('''
+          "id","name","build_id","rel_pc","name","source_file","line_number"
+          26,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16095,"main","/tmp/test.cpp",25
+          27,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",15891,"EmitSignpost()","/tmp/test.cpp",8
+          38,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16107,"main","/tmp/test.cpp",27
+          39,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16047,"fib(int)","/tmp/test.cpp",21
+          40,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16021,"fib(int)","/tmp/test.cpp",22
+          41,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16005,"fib(int)","/tmp/test.cpp",15
+        '''))
+
+  def test_symbolized_stacks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace_with_symbols.zip'),
+        query='''
+          WITH
+            frame AS (
+              SELECT
+                f.id AS frame_id,
+                COALESCE(s.name || ':' || s.line_number, f.rel_pc) as name
+              FROM
+                stack_profile_frame f
+                LEFT JOIN stack_profile_symbol s USING (symbol_set_id)
+            ),
+            child AS (
+              SELECT
+                spc.id AS root,
+                spc.id,
+                spc.parent_id,
+                name AS path
+              FROM
+                instruments_sample s
+                JOIN stack_profile_callsite spc ON (s.callsite_id = spc.id)
+                LEFT JOIN frame f USING (frame_id)
+              UNION ALL
+              SELECT
+                child.root,
+                parent.id,
+                parent.parent_id,
+                COALESCE(f.name || ',', '') || child.path AS path
+              FROM
+                child
+                JOIN stack_profile_callsite parent ON (child.parent_id = parent.id)
+                LEFT JOIN frame f USING (frame_id)
+            )
+          SELECT
+            s.id,
+            s.ts,
+            s.utid,
+            c.path
+          FROM
+            instruments_sample s
+            JOIN child c ON s.callsite_id = c.root
+          WHERE
+            c.parent_id IS NULL
+        ''',
+        out=Csv('''
+          "id","ts","utid","path"
+          0,175685291,1,"23999,34891,37935,334037"
+          1,176684208,1,"24307,28687,265407,160467,120123,391295,336787,8955,340991,392555,136711,5707,7603,10507,207839,207495,23655,17383,23211,208391,6225"
+          2,177685166,1,"24915,main:25,EmitSignpost():8,32211,91151,26907,87887,60651,28343,29471,30159,11087,36269"
+          3,178683916,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):22"
+          4,179687000,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):15"
+          5,180683708,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):15"
+        '''))
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 9dbeac7..032b38a 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -382,6 +382,16 @@
     module.shared_libs.add('libz')
 
 
+def enable_expat(module):
+  if module.type == 'cc_binary_host':
+    module.static_libs.add('libexpat')
+  elif module.host_supported:
+    module.android.shared_libs.add('libexpat')
+    module.host.static_libs.add('libexpat')
+  else:
+    module.shared_libs.add('libexpat')
+
+
 def enable_uapi_headers(module):
   module.include_dirs.add('bionic/libc/kernel')
 
@@ -417,6 +427,8 @@
         enable_sqlite,
     '//gn:zlib':
         enable_zlib,
+    '//gn:expat':
+        enable_expat,
     '//gn:bionic_kernel_uapi_headers':
         enable_uapi_headers,
     '//src/profiling/memory:bionic_libc_platform_headers_on_android':
diff --git a/tools/gen_bazel b/tools/gen_bazel
index ec48704..4f9df8f 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -144,6 +144,7 @@
 external_deps = {
     '//gn:default_deps': [],
     '//gn:base_platform': ['PERFETTO_CONFIG.deps.base_platform'],
+    '//gn:expat': ['PERFETTO_CONFIG.deps.expat'],
     '//gn:jsoncpp': ['PERFETTO_CONFIG.deps.jsoncpp'],
     '//gn:linenoise': ['PERFETTO_CONFIG.deps.linenoise'],
     '//gn:protobuf_full': ['PERFETTO_CONFIG.deps.protobuf_full'],
diff --git a/tools/gen_clickhouse_bigtrace_protos.py b/tools/gen_clickhouse_bigtrace_protos.py
new file mode 100755
index 0000000..3b0d3b2
--- /dev/null
+++ b/tools/gen_clickhouse_bigtrace_protos.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+import os
+"""
+Compile the gRPC python code for Clickhouse for Bigtrace
+and modify the include paths to point to the correct file paths
+
+"""
+
+
+def main():
+  subprocess.run([
+      "python",
+      "-m",
+      "grpc_tools.protoc",
+      "-I.",
+      "--python_out=python/perfetto/bigtrace_clickhouse",
+      "--pyi_out=python/perfetto/bigtrace_clickhouse",
+      "protos/perfetto/bigtrace/orchestrator.proto",
+      "protos/perfetto/trace_processor/trace_processor.proto",
+      "protos/perfetto/common/descriptor.proto",
+      "protos/perfetto/trace_processor/metatrace_categories.proto",
+  ])
+  subprocess.run([
+      "python",
+      "-m",
+      "grpc_tools.protoc",
+      "-I.",
+      "--python_out=python/perfetto/bigtrace_clickhouse",
+      "--pyi_out=python/perfetto/bigtrace_clickhouse",
+      "--grpc_python_out=python/perfetto/bigtrace_clickhouse",
+      "protos/perfetto/bigtrace/orchestrator.proto",
+  ])
+  subprocess.run([
+      "sed",
+      "-i",
+      "-e",
+      "s/protos\.perfetto/\./",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/bigtrace/orchestrator_pb2_grpc.py",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/bigtrace/orchestrator_pb2.py",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/bigtrace/orchestrator_pb2.pyi",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/trace_processor/trace_processor_pb2.py",
+      "python/perfetto/bigtrace_clickhouse/protos/perfetto/trace_processor/trace_processor_pb2.pyi",
+  ])
+  return 0
+
+
+if __name__ == "__main__":
+  main()
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 771f43a..7b2b8d7 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -257,6 +257,15 @@
         'all',
         'all'),
 
+    # Libexpat for Instruments XML import.
+    # If updating the version, also update bazel/deps.bzl.
+    Dependency(
+        'buildtools/expat/src',
+        'https://chromium.googlesource.com/external/github.com/libexpat/libexpat.git',
+        'fa75b96546c069d17b8f80d91e0f4ef0cde3790d',  # refs/tags/upstream/R_2_6_2.
+        'all',
+        'all'),
+
     # Archive with only the demangling sources from llvm-project.
     # See tools/repackage_llvm_demangler.sh on how to update this.
     # File suffix is the git reference to the commit at which we rearchived the
diff --git a/ui/src/common/commands.ts b/ui/src/common/commands.ts
index 2375559..6465329 100644
--- a/ui/src/common/commands.ts
+++ b/ui/src/common/commands.ts
@@ -27,6 +27,10 @@
     return this.registry.get(commandId);
   }
 
+  hasCommand(commandId: string): boolean {
+    return this.registry.has(commandId);
+  }
+
   get commands(): Command[] {
     return Array.from(this.registry.values());
   }
diff --git a/ui/src/common/legacy_flamegraph_unittest.ts b/ui/src/common/legacy_flamegraph_unittest.ts
deleted file mode 100644
index 222b2e2..0000000
--- a/ui/src/common/legacy_flamegraph_unittest.ts
+++ /dev/null
@@ -1,1072 +0,0 @@
-// Copyright (C) 2019 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 {CallsiteInfo, mergeCallsites} from './legacy_flamegraph_util';
-
-test('zeroCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 2,
-      name: 'B4',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 5);
-
-  // Small callsites are not next ot each other, nothing should be changed.
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('zeroCallsitesMerged2', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 6,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 2,
-      name: 'B5',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 5);
-
-  // Small callsites are not next ot each other, nothing should be changed.
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('twoCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 6);
-
-  expect(mergedCallsites).toEqual([
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ]);
-});
-
-test('manyCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 3,
-      name: 'A36',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 4,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 5,
-      name: 'A58',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 3,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsites A36, A47
-  // and A58 should also be merged since their parents are merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('manyCallsitesMergedWithoutChildren', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 2,
-      name: 'B6',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 4,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 6,
-      name: 'B68',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 2,
-      name: 'B6',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 3,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 6,
-      name: 'B68',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsite A47
-  // should not be merged with B68 althought they are small because they don't
-  // have sam parent. A47 should now have parent A3 because A4 is merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('smallCallsitesNotNextToEachOtherInArray', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsite A47
-  // should not be merged with B68 althought they are small because they don't
-  // have sam parent. A47 should now have parent A3 because A4 is merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('smallCallsitesNotMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 1);
-
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('mergingRootCallsites', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 20);
-
-  expect(mergedCallsites).toEqual([
-    {
-      id: 1,
-      parentId: -1,
-      name: '[merged]',
-      depth: 0,
-      totalSize: 12,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ]);
-});
-
-test('largerFlamegraph', () => {
-  const data: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 60,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 40,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 15,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 1,
-      name: 'A6',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 2,
-      name: 'B7',
-      depth: 1,
-      totalSize: 30,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 2,
-      name: 'B8',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 9,
-      parentId: 3,
-      name: 'A39',
-      depth: 2,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 10,
-      parentId: 4,
-      name: 'A410',
-      depth: 2,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 11,
-      parentId: 4,
-      name: 'A411',
-      depth: 2,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 12,
-      parentId: 4,
-      name: 'A412',
-      depth: 2,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 13,
-      parentId: 5,
-      name: 'A513',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 14,
-      parentId: 5,
-      name: 'A514',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 15,
-      parentId: 7,
-      name: 'A715',
-      depth: 2,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 16,
-      parentId: 7,
-      name: 'A716',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 17,
-      parentId: 7,
-      name: 'A717',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 18,
-      parentId: 7,
-      name: 'A718',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 19,
-      parentId: 9,
-      name: 'A919',
-      depth: 3,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 20,
-      parentId: 17,
-      name: 'A1720',
-      depth: 3,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedData: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 60,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 40,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 35,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 2,
-      name: 'B7',
-      depth: 1,
-      totalSize: 30,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 2,
-      name: 'B8',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 9,
-      parentId: 3,
-      name: 'A39',
-      depth: 2,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 10,
-      parentId: 4,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 15,
-      parentId: 7,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 19,
-      parentId: 9,
-      name: 'A919',
-      depth: 3,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 20,
-      parentId: 15,
-      name: 'A1720',
-      depth: 3,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  // In this case, on depth 1, callsites A4, A5 and A6 should be merged and
-  // initiate merging of their children A410, A411, A412, A513, A514. On depth2,
-  // callsites A715, A716, A717 and A718 should be merged.
-  const actualData = mergeCallsites(data, 16);
-
-  expect(actualData).toEqual(expectedData);
-});
diff --git a/ui/src/common/legacy_flamegraph_util.ts b/ui/src/common/legacy_flamegraph_util.ts
deleted file mode 100644
index cb540dd..0000000
--- a/ui/src/common/legacy_flamegraph_util.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (C) 2019 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 {featureFlags} from '../core/feature_flags';
-import {ProfileType} from './state';
-
-export enum FlamegraphViewingOption {
-  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE',
-  ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE',
-  OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS',
-  OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS',
-  PERF_SAMPLES_KEY = 'PERF_SAMPLES',
-  DOMINATOR_TREE_OBJ_SIZE_KEY = 'DOMINATED_OBJ_SIZE',
-  DOMINATOR_TREE_OBJ_COUNT_KEY = 'DOMINATED_OBJ_COUNT',
-}
-
-interface ViewingOption {
-  option: FlamegraphViewingOption;
-  name: string;
-}
-
-export interface CallsiteInfo {
-  id: number;
-  parentId: number;
-  depth: number;
-  name?: string;
-  totalSize: number;
-  selfSize: number;
-  mapping: string;
-  merged: boolean;
-  highlighted: boolean;
-  location?: string;
-}
-
-export const SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG = featureFlags.register({
-  id: 'showHeapGraphDominatorTree',
-  name: 'Show heap graph dominator tree',
-  description: 'Show dominated size and objects tabs in Java heap graph view.',
-  defaultValue: true,
-});
-
-export function viewingOptions(profileType: ProfileType): Array<ViewingOption> {
-  switch (profileType) {
-    case ProfileType.PERF_SAMPLE:
-      return [
-        {
-          option: FlamegraphViewingOption.PERF_SAMPLES_KEY,
-          name: 'Samples',
-        },
-      ];
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Objects',
-        },
-      ].concat(
-        SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
-          ? [
-              {
-                option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
-                name: 'Dominated size',
-              },
-              {
-                option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
-                name: 'Dominated objects',
-              },
-            ]
-          : [],
-      );
-    case ProfileType.HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased count',
-        },
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total count',
-        },
-      ];
-    case ProfileType.NATIVE_HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased malloc size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased malloc count',
-        },
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total malloc size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total malloc count',
-        },
-      ];
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return [
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total allocation size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total allocation count',
-        },
-      ];
-    case ProfileType.MIXED_HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total allocation size (malloc + java)',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total allocation count (malloc + java)',
-        },
-      ];
-    default:
-      const exhaustiveCheck: never = profileType;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-export function defaultViewingOption(
-  profileType: ProfileType,
-): FlamegraphViewingOption {
-  return viewingOptions(profileType)[0].option;
-}
-
-export function expandCallsites(
-  data: ReadonlyArray<CallsiteInfo>,
-  clickedCallsiteIndex: number,
-): ReadonlyArray<CallsiteInfo> {
-  if (clickedCallsiteIndex === -1) return data;
-  const expandedCallsites: CallsiteInfo[] = [];
-  if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) {
-    return expandedCallsites;
-  }
-  const clickedCallsite = data[clickedCallsiteIndex];
-  expandedCallsites.unshift(clickedCallsite);
-  // Adding parents
-  let parentId = clickedCallsite.parentId;
-  while (parentId > -1) {
-    expandedCallsites.unshift(data[parentId]);
-    parentId = data[parentId].parentId;
-  }
-  // Adding children
-  const parents: number[] = [];
-  parents.push(clickedCallsiteIndex);
-  for (let i = clickedCallsiteIndex + 1; i < data.length; i++) {
-    const element = data[i];
-    if (parents.includes(element.parentId)) {
-      expandedCallsites.push(element);
-      parents.push(element.id);
-    }
-  }
-  return expandedCallsites;
-}
-
-// Merge callsites that have approximately width less than
-// MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same
-// parent will be merged to one with total size of all merged callsites.
-export function mergeCallsites(
-  data: ReadonlyArray<CallsiteInfo>,
-  minSizeDisplayed: number,
-) {
-  const mergedData: CallsiteInfo[] = [];
-  const mergedCallsites: Map<number, number> = new Map();
-  for (let i = 0; i < data.length; i++) {
-    // When a small callsite is found, it will be merged with other small
-    // callsites of the same depth. So if the current callsite has already been
-    // merged we can skip it.
-    if (mergedCallsites.has(data[i].id)) {
-      continue;
-    }
-    const copiedCallsite = copyCallsite(data[i]);
-    copiedCallsite.parentId = getCallsitesParentHash(
-      copiedCallsite,
-      mergedCallsites,
-    );
-
-    let mergedAny = false;
-    // If current callsite is small, find other small callsites with same depth
-    // and parent and merge them into the current one, marking them as merged.
-    if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) {
-      let j = i + 1;
-      let nextCallsite = data[j];
-      while (j < data.length && copiedCallsite.depth === nextCallsite.depth) {
-        if (
-          copiedCallsite.parentId ===
-            getCallsitesParentHash(nextCallsite, mergedCallsites) &&
-          nextCallsite.totalSize <= minSizeDisplayed
-        ) {
-          copiedCallsite.totalSize += nextCallsite.totalSize;
-          mergedCallsites.set(nextCallsite.id, copiedCallsite.id);
-          mergedAny = true;
-        }
-        j++;
-        nextCallsite = data[j];
-      }
-      if (mergedAny) {
-        copiedCallsite.name = '[merged]';
-        copiedCallsite.merged = true;
-      }
-    }
-    mergedData.push(copiedCallsite);
-  }
-  return mergedData;
-}
-
-function copyCallsite(callsite: CallsiteInfo): CallsiteInfo {
-  return {
-    id: callsite.id,
-    parentId: callsite.parentId,
-    depth: callsite.depth,
-    name: callsite.name,
-    totalSize: callsite.totalSize,
-    mapping: callsite.mapping,
-    selfSize: callsite.selfSize,
-    merged: callsite.merged,
-    highlighted: callsite.highlighted,
-    location: callsite.location,
-  };
-}
-
-function getCallsitesParentHash(
-  callsite: CallsiteInfo,
-  map: Map<number, number>,
-): number {
-  return map.has(callsite.parentId)
-    ? +map.get(callsite.parentId)!
-    : callsite.parentId;
-}
-export function findRootSize(data: ReadonlyArray<CallsiteInfo>) {
-  let totalSize = 0;
-  let i = 0;
-  while (i < data.length && data[i].depth === 0) {
-    totalSize += data[i].totalSize;
-    i++;
-  }
-  return totalSize;
-}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 9766b37..e6196f3 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -40,7 +40,7 @@
 import {horizontalScrollToTs} from '../frontend/scroll_helper';
 import {DisposableStack} from '../base/disposable_stack';
 import {TraceContext} from '../frontend/trace_context';
-import {Workspace} from '../frontend/workspace';
+import {Workspace} from '../public/workspace';
 
 // Every plugin gets its own PluginContext. This is how we keep track
 // what each plugin is doing and how we can blame issues on particular
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 7bba871..3a55bc2 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -87,13 +87,12 @@
   TraceStream,
 } from '../core/trace_stream';
 import {decideTracks} from './track_decider';
-import {profileType} from '../frontend/legacy_flamegraph_panel';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
 import {
   deserializeAppStatePhase1,
   deserializeAppStatePhase2,
 } from '../common/state_serialization';
 import {TraceContext} from '../frontend/trace_context';
+import {profileType} from '../core/selection_manager';
 
 type States = 'init' | 'loading_trace' | 'ready';
 
@@ -374,10 +373,6 @@
   onDestroy() {
     pluginManager.onTraceClose();
     globals.engines.delete(this.engineId);
-
-    // Invalidate the flamegraph cache.
-    // TODO(stevegolton): migrate this to the new system when it's ready.
-    globals.areaFlamegraphCache = new LegacyFlamegraphCache('area');
   }
 
   private async loadTrace(): Promise<EngineMode> {
@@ -653,28 +648,29 @@
   }
 
   private async selectFirstHeapProfile() {
-    const query = `select * from (
-      select
-        min(ts) AS ts,
-        'heap_profile:' || group_concat(distinct heap_name) AS type,
-        upid
-      from heap_profile_allocation
-      group by upid
-      union
-      select distinct graph_sample_ts as ts, 'graph' as type, upid
-      from heap_graph_object)
-      order by ts limit 1`;
+    const query = `
+      select * from (
+        select
+          min(ts) AS ts,
+          'heap_profile:' || group_concat(distinct heap_name) AS type,
+          upid
+        from heap_profile_allocation
+        group by upid
+        union
+        select distinct graph_sample_ts as ts, 'graph' as type, upid
+        from heap_graph_object
+      )
+      order by ts
+      limit 1
+    `;
     const profile = await assertExists(this.engine).query(query);
     if (profile.numRows() !== 1) return;
     const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
     const ts = Time.fromRaw(row.ts);
-    let profType = row.type;
-    if (profType == 'heap_profile:libc.malloc,com.android.art') {
-      profType = 'heap_profile:com.android.art,libc.malloc';
-    }
-    const type = profileType(profType);
     const upid = row.upid;
-    globals.dispatch(Actions.selectHeapProfile({id: 0, upid, ts, type}));
+    globals.dispatch(
+      Actions.selectHeapProfile({id: 0, upid, ts, type: profileType(row.type)}),
+    );
   }
 
   private async selectPendingDeeplink(link: PendingDeeplinkState) {
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 440f907..d7a9e0e 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -35,7 +35,7 @@
   THREAD_STATE_TRACK_KIND,
 } from '../core/track_kinds';
 import {exists, Optional} from '../base/utils';
-import {GroupNode, ContainerNode, TrackNode} from '../frontend/workspace';
+import {GroupNode, ContainerNode, TrackNode} from '../public/workspace';
 
 const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
 const MEM_DMA = 'mem.dma_buffer';
diff --git a/ui/src/core/colorizer.ts b/ui/src/core/colorizer.ts
index 3f0d1b3..ac495d3 100644
--- a/ui/src/core/colorizer.ts
+++ b/ui/src/core/colorizer.ts
@@ -238,13 +238,10 @@
   return materialColorScheme(name);
 }
 
-export function colorForSample(callsiteId: number, isHovered: boolean): string {
-  let colorScheme;
+export function getColorForSample(callsiteId: number): ColorScheme {
   if (USE_CONSISTENT_COLORS.get()) {
-    colorScheme = materialColorScheme(String(callsiteId));
+    return materialColorScheme(String(callsiteId));
   } else {
-    colorScheme = proceduralColorScheme(String(callsiteId));
+    return proceduralColorScheme(String(callsiteId));
   }
-
-  return isHovered ? colorScheme.variant.cssString : colorScheme.base.cssString;
 }
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index d057f66..1cba444 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -47,6 +47,7 @@
   'perfetto.CpuFreq',
   'perfetto.CpuProfile',
   'perfetto.CpuSlices',
+  'perfetto.CriticalPath',
   'perfetto.CriticalUserInteraction',
   'perfetto.DebugTracks',
   'perfetto.ExampleTraces',
diff --git a/ui/src/core/legacy_flamegraph_cache.ts b/ui/src/core/legacy_flamegraph_cache.ts
deleted file mode 100644
index ae37877..0000000
--- a/ui/src/core/legacy_flamegraph_cache.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size 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 {Engine} from '../trace_processor/engine';
-
-export class LegacyFlamegraphCache {
-  private cache: Map<string, string>;
-  private prefix: string;
-  private tableId: number;
-  private cacheSizeLimit: number;
-
-  constructor(prefix: string) {
-    this.cache = new Map<string, string>();
-    this.prefix = prefix;
-    this.tableId = 0;
-    this.cacheSizeLimit = 10;
-  }
-
-  async getTableName(engine: Engine, query: string): Promise<string> {
-    let tableName = this.cache.get(query);
-    if (tableName === undefined) {
-      // TODO(hjd): This should be LRU.
-      if (this.cache.size > this.cacheSizeLimit) {
-        for (const name of this.cache.values()) {
-          await engine.query(`drop table ${name}`);
-        }
-        this.cache.clear();
-      }
-      tableName = `${this.prefix}_${this.tableId++}`;
-      await engine.query(
-        `create temp table if not exists ${tableName} as ${query}`,
-      );
-      this.cache.set(query, tableName);
-    }
-    return tableName;
-  }
-
-  hasQuery(query: string): boolean {
-    return this.cache.get(query) !== undefined;
-  }
-}
diff --git a/ui/src/core/query_flamegraph.ts b/ui/src/core/query_flamegraph.ts
index 802aebc..8fdfc70 100644
--- a/ui/src/core/query_flamegraph.ts
+++ b/ui/src/core/query_flamegraph.ts
@@ -32,8 +32,6 @@
   FlamegraphView,
 } from '../widgets/flamegraph';
 
-import {featureFlags} from './feature_flags';
-
 export interface QueryFlamegraphColumn {
   // The name of the column in SQL.
   readonly name: string;
@@ -452,10 +450,3 @@
   }
   return '0';
 }
-
-export const USE_NEW_FLAMEGRAPH_IMPL = featureFlags.register({
-  id: 'useNewFlamegraphImpl',
-  name: 'Use new flamegraph implementation',
-  description: 'Use new flamgraph implementation in details panels.',
-  defaultValue: true,
-});
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 4ac571f..84271c3 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -26,6 +26,19 @@
   PERF_SAMPLE = 'perf',
 }
 
+export function profileType(s: string): ProfileType {
+  if (s === 'heap_profile:libc.malloc,com.android.art') {
+    s = 'heap_profile:com.android.art,libc.malloc';
+  }
+  if (Object.values(ProfileType).includes(s as ProfileType)) {
+    return s as ProfileType;
+  }
+  if (s.startsWith('heap_profile')) {
+    return ProfileType.HEAP_PROFILE;
+  }
+  throw new Error('Unknown type ${s}');
+}
+
 // LEGACY Selection types:
 export interface SliceSelection {
   kind: 'SCHED_SLICE';
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index 3c50299..1bb7980 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -27,7 +27,7 @@
 import {StartupDetailsPanel} from './startup_details_panel';
 import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
 import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
-import {TrackNode} from '../../frontend/workspace';
+import {TrackNode} from '../../public/workspace';
 import {globals} from '../../frontend/globals';
 
 function addCriticalUserInteractionTrack() {
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index 91c3d71..0430f63 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -33,7 +33,7 @@
   addSqlTableTabImpl,
   SqlTableTabConfig,
 } from '../../frontend/sql_table_tab';
-import {Workspace} from '../../frontend/workspace';
+import {Workspace} from '../../public/workspace';
 
 const SQL_STATS = `
 with first as (select started as ts from sqlstats limit 1)
diff --git a/ui/src/core_plugins/cpu_freq/cpu_freq_track.ts b/ui/src/core_plugins/cpu_freq/cpu_freq_track.ts
new file mode 100644
index 0000000..49e5b20
--- /dev/null
+++ b/ui/src/core_plugins/cpu_freq/cpu_freq_track.ts
@@ -0,0 +1,428 @@
+// 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 {BigintMath as BIMath} from '../../base/bigint_math';
+import {searchSegment} from '../../base/binary_search';
+import {assertTrue} from '../../base/logging';
+import {duration, time, Time} from '../../base/time';
+import {drawTrackHoverTooltip} from '../../common/canvas_utils';
+import {colorForCpu} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
+import {Vector} from '../../base/geom';
+import {createView, createVirtualTable} from '../../trace_processor/sql_utils';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+
+export interface Data extends TrackData {
+  timestamps: BigInt64Array;
+  minFreqKHz: Uint32Array;
+  maxFreqKHz: Uint32Array;
+  lastFreqKHz: Uint32Array;
+  lastIdleValues: Int8Array;
+}
+
+interface Config {
+  cpu: number;
+  freqTrackId: number;
+  idleTrackId?: number;
+  maximumValue: number;
+}
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 20;
+
+export class CpuFreqTrack implements Track {
+  private mousePos: Vector = {x: 0, y: 0};
+  private hoveredValue: number | undefined = undefined;
+  private hoveredTs: time | undefined = undefined;
+  private hoveredTsEnd: time | undefined = undefined;
+  private hoveredIdle: number | undefined = undefined;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  private engine: Engine;
+  private config: Config;
+  private trackUuid = uuidv4Sql();
+
+  private trash!: AsyncDisposableStack;
+
+  constructor(config: Config, engine: Engine) {
+    this.config = config;
+    this.engine = engine;
+  }
+
+  async onCreate() {
+    this.trash = new AsyncDisposableStack();
+    if (this.config.idleTrackId === undefined) {
+      this.trash.use(
+        await createView(
+          this.engine,
+          `raw_freq_idle_${this.trackUuid}`,
+          `
+            select ts, dur, value as freqValue, -1 as idleValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.freqTrackId}
+          `,
+        ),
+      );
+    } else {
+      this.trash.use(
+        await createView(
+          this.engine,
+          `raw_freq_${this.trackUuid}`,
+          `
+            select ts, dur, value as freqValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.freqTrackId}
+          `,
+        ),
+      );
+
+      this.trash.use(
+        await createView(
+          this.engine,
+          `raw_idle_${this.trackUuid}`,
+          `
+            select
+              ts,
+              dur,
+              iif(value = 4294967295, -1, cast(value as int)) as idleValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.idleTrackId}
+          `,
+        ),
+      );
+
+      this.trash.use(
+        await createVirtualTable(
+          this.engine,
+          `raw_freq_idle_${this.trackUuid}`,
+          `span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid})`,
+        ),
+      );
+    }
+
+    this.trash.use(
+      await createVirtualTable(
+        this.engine,
+        `cpu_freq_${this.trackUuid}`,
+        `
+          __intrinsic_counter_mipmap((
+            select ts, freqValue as value
+            from raw_freq_idle_${this.trackUuid}
+          ))
+        `,
+      ),
+    );
+
+    this.trash.use(
+      await createVirtualTable(
+        this.engine,
+        `cpu_idle_${this.trackUuid}`,
+        `
+          __intrinsic_counter_mipmap((
+            select ts, idleValue as value
+            from raw_freq_idle_${this.trackUuid}
+          ))
+        `,
+      ),
+    );
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onDestroy(): Promise<void> {
+    await this.trash.asyncDispose();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    // The resolution should always be a power of two for the logic of this
+    // function to make sense.
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const freqResult = await this.engine.query(`
+      SELECT
+        min_value as minFreq,
+        max_value as maxFreq,
+        last_ts as ts,
+        last_value as lastFreq
+      FROM cpu_freq_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+    const idleResult = await this.engine.query(`
+      SELECT last_value as lastIdle
+      FROM cpu_idle_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+
+    const freqRows = freqResult.numRows();
+    const idleRows = idleResult.numRows();
+    assertTrue(freqRows == idleRows);
+
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: freqRows,
+      timestamps: new BigInt64Array(freqRows),
+      minFreqKHz: new Uint32Array(freqRows),
+      maxFreqKHz: new Uint32Array(freqRows),
+      lastFreqKHz: new Uint32Array(freqRows),
+      lastIdleValues: new Int8Array(freqRows),
+    };
+
+    const freqIt = freqResult.iter({
+      ts: LONG,
+      minFreq: NUM,
+      maxFreq: NUM,
+      lastFreq: NUM,
+    });
+    const idleIt = idleResult.iter({
+      lastIdle: NUM,
+    });
+    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
+      data.timestamps[i] = freqIt.ts;
+      data.minFreqKHz[i] = freqIt.minFreq;
+      data.maxFreqKHz[i] = freqIt.maxFreq;
+      data.lastFreqKHz[i] = freqIt.lastFreq;
+      data.lastIdleValues[i] = idleIt.lastIdle;
+    }
+    return data;
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT;
+  }
+
+  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const data = this.fetcher.data;
+
+    if (data === undefined || data.timestamps.length === 0) {
+      // Can't possibly draw anything.
+      return;
+    }
+
+    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
+    assertTrue(data.timestamps.length === data.minFreqKHz.length);
+    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
+    assertTrue(data.timestamps.length === data.lastIdleValues.length);
+
+    const endPx = size.width;
+    const zeroY = MARGIN_TOP + RECT_HEIGHT;
+
+    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
+    let yMax = this.config.maximumValue;
+    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
+    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
+    const pow10 = Math.pow(10, exp);
+    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
+    const unitGroup = Math.floor(exp / 3);
+    const num = yMax / Math.pow(10, unitGroup * 3);
+    // The values we have for cpufreq are in kHz so +1 to unitGroup.
+    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
+
+    const color = colorForCpu(this.config.cpu);
+    let saturation = 45;
+    if (globals.state.hoveredUtid !== -1) {
+      saturation = 0;
+    }
+
+    ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString;
+    ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString;
+
+    const calculateX = (timestamp: time) => {
+      return Math.floor(timescale.timeToPx(timestamp));
+    };
+    const calculateY = (value: number) => {
+      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
+    };
+
+    const timespan = visibleWindow.toTimeSpan();
+    const start = timespan.start;
+    const end = timespan.end;
+
+    const [rawStartIdx] = searchSegment(data.timestamps, start);
+    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+    const [, rawEndIdx] = searchSegment(data.timestamps, end);
+    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
+
+    // Draw the CPU frequency graph.
+    {
+      ctx.beginPath();
+      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
+      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
+
+      let lastDrawnY = zeroY;
+      for (let i = startIdx; i < endIdx; i++) {
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = Math.max(0, calculateX(timestamp));
+        const minY = calculateY(data.minFreqKHz[i]);
+        const maxY = calculateY(data.maxFreqKHz[i]);
+        const lastY = calculateY(data.lastFreqKHz[i]);
+
+        ctx.lineTo(x, lastDrawnY);
+        if (minY === maxY) {
+          assertTrue(lastY === minY);
+          ctx.lineTo(x, lastY);
+        } else {
+          ctx.lineTo(x, minY);
+          ctx.lineTo(x, maxY);
+          ctx.lineTo(x, lastY);
+        }
+        lastDrawnY = lastY;
+      }
+      ctx.lineTo(endPx, lastDrawnY);
+      ctx.lineTo(endPx, zeroY);
+      ctx.closePath();
+      ctx.fill();
+      ctx.stroke();
+    }
+
+    // Draw CPU idle rectangles that overlay the CPU freq graph.
+    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
+    {
+      for (let i = startIdx; i < endIdx; i++) {
+        if (data.lastIdleValues[i] < 0) {
+          continue;
+        }
+
+        // We intentionally don't use the floor function here when computing x
+        // coordinates. Instead we use floating point which prevents flickering as
+        // we pan and zoom; this relies on the browser anti-aliasing pixels
+        // correctly.
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = timescale.timeToPx(timestamp);
+        const xEnd =
+          i === data.lastIdleValues.length - 1
+            ? endPx
+            : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
+
+        const width = xEnd - x;
+        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
+
+        ctx.fillRect(x, zeroY, width, height);
+      }
+    }
+
+    ctx.font = '10px Roboto Condensed';
+
+    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
+      let text = `${this.hoveredValue.toLocaleString()}kHz`;
+
+      ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString;
+      ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString;
+
+      const xStart = Math.floor(timescale.timeToPx(this.hoveredTs));
+      const xEnd =
+        this.hoveredTsEnd === undefined
+          ? endPx
+          : Math.floor(timescale.timeToPx(this.hoveredTsEnd));
+      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
+
+      // Highlight line.
+      ctx.beginPath();
+      ctx.moveTo(xStart, y);
+      ctx.lineTo(xEnd, y);
+      ctx.lineWidth = 3;
+      ctx.stroke();
+      ctx.lineWidth = 1;
+
+      // Draw change marker.
+      ctx.beginPath();
+      ctx.arc(
+        xStart,
+        y,
+        3 /* r*/,
+        0 /* start angle*/,
+        2 * Math.PI /* end angle*/,
+      );
+      ctx.fill();
+      ctx.stroke();
+
+      // Display idle value if current hover is idle.
+      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
+        // Display the idle value +1 to be consistent with catapult.
+        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
+      }
+
+      // Draw the tooltip.
+      drawTrackHoverTooltip(ctx, this.mousePos, size, text);
+    }
+
+    // Write the Y scale on the top left corner.
+    ctx.textBaseline = 'alphabetic';
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
+    ctx.fillRect(0, 0, 42, 18);
+    ctx.fillStyle = '#666';
+    ctx.textAlign = 'left';
+    ctx.fillText(`${yLabel}`, 4, 14);
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      timescale.timeToPx(data.start),
+      timescale.timeToPx(data.end),
+    );
+  }
+
+  onMouseMove({x, y, timescale}: TrackMouseEvent) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    this.mousePos = {x, y};
+    const time = timescale.pxToHpTime(x);
+
+    const [left, right] = searchSegment(data.timestamps, time.toTime());
+
+    this.hoveredTs =
+      left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
+    this.hoveredTsEnd =
+      right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
+    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
+    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
+  }
+
+  onMouseOut() {
+    this.hoveredValue = undefined;
+    this.hoveredTs = undefined;
+    this.hoveredTsEnd = undefined;
+    this.hoveredIdle = undefined;
+  }
+}
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index c620ebc..0dd4218 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -12,427 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {searchSegment} from '../../base/binary_search';
-import {assertTrue} from '../../base/logging';
-import {duration, time, Time} from '../../base/time';
-import {drawTrackHoverTooltip} from '../../common/canvas_utils';
-import {colorForCpu} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
 import {
   CPU_FREQ_TRACK_KIND,
-  Engine,
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
-  Track,
 } from '../../public';
-import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
-import {Vector} from '../../base/geom';
-import {createView, createVirtualTable} from '../../trace_processor/sql_utils';
-import {AsyncDisposableStack} from '../../base/disposable_stack';
-
-export interface Data extends TrackData {
-  timestamps: BigInt64Array;
-  minFreqKHz: Uint32Array;
-  maxFreqKHz: Uint32Array;
-  lastFreqKHz: Uint32Array;
-  lastIdleValues: Int8Array;
-}
-
-interface Config {
-  cpu: number;
-  freqTrackId: number;
-  idleTrackId?: number;
-  maximumValue: number;
-}
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 20;
-
-class CpuFreqTrack implements Track {
-  private mousePos: Vector = {x: 0, y: 0};
-  private hoveredValue: number | undefined = undefined;
-  private hoveredTs: time | undefined = undefined;
-  private hoveredTsEnd: time | undefined = undefined;
-  private hoveredIdle: number | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  private engine: Engine;
-  private config: Config;
-  private trackUuid = uuidv4Sql();
-
-  private trash!: AsyncDisposableStack;
-
-  constructor(config: Config, engine: Engine) {
-    this.config = config;
-    this.engine = engine;
-  }
-
-  async onCreate() {
-    this.trash = new AsyncDisposableStack();
-    if (this.config.idleTrackId === undefined) {
-      this.trash.use(
-        await createView(
-          this.engine,
-          `raw_freq_idle_${this.trackUuid}`,
-          `
-            select ts, dur, value as freqValue, -1 as idleValue
-            from experimental_counter_dur c
-            where track_id = ${this.config.freqTrackId}
-          `,
-        ),
-      );
-    } else {
-      this.trash.use(
-        await createView(
-          this.engine,
-          `raw_freq_${this.trackUuid}`,
-          `
-            select ts, dur, value as freqValue
-            from experimental_counter_dur c
-            where track_id = ${this.config.freqTrackId}
-          `,
-        ),
-      );
-
-      this.trash.use(
-        await createView(
-          this.engine,
-          `raw_idle_${this.trackUuid}`,
-          `
-            select
-              ts,
-              dur,
-              iif(value = 4294967295, -1, cast(value as int)) as idleValue
-            from experimental_counter_dur c
-            where track_id = ${this.config.idleTrackId}
-          `,
-        ),
-      );
-
-      this.trash.use(
-        await createVirtualTable(
-          this.engine,
-          `raw_freq_idle_${this.trackUuid}`,
-          `span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid})`,
-        ),
-      );
-    }
-
-    this.trash.use(
-      await createVirtualTable(
-        this.engine,
-        `cpu_freq_${this.trackUuid}`,
-        `
-          __intrinsic_counter_mipmap((
-            select ts, freqValue as value
-            from raw_freq_idle_${this.trackUuid}
-          ))
-        `,
-      ),
-    );
-
-    this.trash.use(
-      await createVirtualTable(
-        this.engine,
-        `cpu_idle_${this.trackUuid}`,
-        `
-          __intrinsic_counter_mipmap((
-            select ts, idleValue as value
-            from raw_freq_idle_${this.trackUuid}
-          ))
-        `,
-      ),
-    );
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onDestroy(): Promise<void> {
-    await this.trash.asyncDispose();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    // The resolution should always be a power of two for the logic of this
-    // function to make sense.
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const freqResult = await this.engine.query(`
-      SELECT
-        min_value as minFreq,
-        max_value as maxFreq,
-        last_ts as ts,
-        last_value as lastFreq
-      FROM cpu_freq_${this.trackUuid}(
-        ${start},
-        ${end},
-        ${resolution}
-      );
-    `);
-    const idleResult = await this.engine.query(`
-      SELECT last_value as lastIdle
-      FROM cpu_idle_${this.trackUuid}(
-        ${start},
-        ${end},
-        ${resolution}
-      );
-    `);
-
-    const freqRows = freqResult.numRows();
-    const idleRows = idleResult.numRows();
-    assertTrue(freqRows == idleRows);
-
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: freqRows,
-      timestamps: new BigInt64Array(freqRows),
-      minFreqKHz: new Uint32Array(freqRows),
-      maxFreqKHz: new Uint32Array(freqRows),
-      lastFreqKHz: new Uint32Array(freqRows),
-      lastIdleValues: new Int8Array(freqRows),
-    };
-
-    const freqIt = freqResult.iter({
-      ts: LONG,
-      minFreq: NUM,
-      maxFreq: NUM,
-      lastFreq: NUM,
-    });
-    const idleIt = idleResult.iter({
-      lastIdle: NUM,
-    });
-    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
-      data.timestamps[i] = freqIt.ts;
-      data.minFreqKHz[i] = freqIt.minFreq;
-      data.maxFreqKHz[i] = freqIt.maxFreq;
-      data.lastFreqKHz[i] = freqIt.lastFreq;
-      data.lastIdleValues[i] = idleIt.lastIdle;
-    }
-    return data;
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT;
-  }
-
-  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const data = this.fetcher.data;
-
-    if (data === undefined || data.timestamps.length === 0) {
-      // Can't possibly draw anything.
-      return;
-    }
-
-    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
-    assertTrue(data.timestamps.length === data.minFreqKHz.length);
-    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
-    assertTrue(data.timestamps.length === data.lastIdleValues.length);
-
-    const endPx = size.width;
-    const zeroY = MARGIN_TOP + RECT_HEIGHT;
-
-    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
-    let yMax = this.config.maximumValue;
-    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
-    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
-    const pow10 = Math.pow(10, exp);
-    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
-    const unitGroup = Math.floor(exp / 3);
-    const num = yMax / Math.pow(10, unitGroup * 3);
-    // The values we have for cpufreq are in kHz so +1 to unitGroup.
-    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
-
-    const color = colorForCpu(this.config.cpu);
-    let saturation = 45;
-    if (globals.state.hoveredUtid !== -1) {
-      saturation = 0;
-    }
-
-    ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString;
-    ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString;
-
-    const calculateX = (timestamp: time) => {
-      return Math.floor(timescale.timeToPx(timestamp));
-    };
-    const calculateY = (value: number) => {
-      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
-    };
-
-    const timespan = visibleWindow.toTimeSpan();
-    const start = timespan.start;
-    const end = timespan.end;
-
-    const [rawStartIdx] = searchSegment(data.timestamps, start);
-    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
-    const [, rawEndIdx] = searchSegment(data.timestamps, end);
-    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
-
-    // Draw the CPU frequency graph.
-    {
-      ctx.beginPath();
-      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
-      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
-
-      let lastDrawnY = zeroY;
-      for (let i = startIdx; i < endIdx; i++) {
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const x = Math.max(0, calculateX(timestamp));
-        const minY = calculateY(data.minFreqKHz[i]);
-        const maxY = calculateY(data.maxFreqKHz[i]);
-        const lastY = calculateY(data.lastFreqKHz[i]);
-
-        ctx.lineTo(x, lastDrawnY);
-        if (minY === maxY) {
-          assertTrue(lastY === minY);
-          ctx.lineTo(x, lastY);
-        } else {
-          ctx.lineTo(x, minY);
-          ctx.lineTo(x, maxY);
-          ctx.lineTo(x, lastY);
-        }
-        lastDrawnY = lastY;
-      }
-      ctx.lineTo(endPx, lastDrawnY);
-      ctx.lineTo(endPx, zeroY);
-      ctx.closePath();
-      ctx.fill();
-      ctx.stroke();
-    }
-
-    // Draw CPU idle rectangles that overlay the CPU freq graph.
-    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
-    {
-      for (let i = startIdx; i < endIdx; i++) {
-        if (data.lastIdleValues[i] < 0) {
-          continue;
-        }
-
-        // We intentionally don't use the floor function here when computing x
-        // coordinates. Instead we use floating point which prevents flickering as
-        // we pan and zoom; this relies on the browser anti-aliasing pixels
-        // correctly.
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const x = timescale.timeToPx(timestamp);
-        const xEnd =
-          i === data.lastIdleValues.length - 1
-            ? endPx
-            : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
-
-        const width = xEnd - x;
-        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
-
-        ctx.fillRect(x, zeroY, width, height);
-      }
-    }
-
-    ctx.font = '10px Roboto Condensed';
-
-    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
-      let text = `${this.hoveredValue.toLocaleString()}kHz`;
-
-      ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString;
-      ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString;
-
-      const xStart = Math.floor(timescale.timeToPx(this.hoveredTs));
-      const xEnd =
-        this.hoveredTsEnd === undefined
-          ? endPx
-          : Math.floor(timescale.timeToPx(this.hoveredTsEnd));
-      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
-
-      // Highlight line.
-      ctx.beginPath();
-      ctx.moveTo(xStart, y);
-      ctx.lineTo(xEnd, y);
-      ctx.lineWidth = 3;
-      ctx.stroke();
-      ctx.lineWidth = 1;
-
-      // Draw change marker.
-      ctx.beginPath();
-      ctx.arc(
-        xStart,
-        y,
-        3 /* r*/,
-        0 /* start angle*/,
-        2 * Math.PI /* end angle*/,
-      );
-      ctx.fill();
-      ctx.stroke();
-
-      // Display idle value if current hover is idle.
-      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
-        // Display the idle value +1 to be consistent with catapult.
-        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
-      }
-
-      // Draw the tooltip.
-      drawTrackHoverTooltip(ctx, this.mousePos, size, text);
-    }
-
-    // Write the Y scale on the top left corner.
-    ctx.textBaseline = 'alphabetic';
-    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
-    ctx.fillRect(0, 0, 42, 18);
-    ctx.fillStyle = '#666';
-    ctx.textAlign = 'left';
-    ctx.fillText(`${yLabel}`, 4, 14);
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      timescale.timeToPx(data.start),
-      timescale.timeToPx(data.end),
-    );
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    this.mousePos = {x, y};
-    const time = timescale.pxToHpTime(x);
-
-    const [left, right] = searchSegment(data.timestamps, time.toTime());
-
-    this.hoveredTs =
-      left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
-    this.hoveredTsEnd =
-      right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
-    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
-    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
-  }
-
-  onMouseOut() {
-    this.hoveredValue = undefined;
-    this.hoveredTs = undefined;
-    this.hoveredTsEnd = undefined;
-    this.hoveredIdle = undefined;
-  }
-}
+import {NUM, NUM_NULL} from '../../trace_processor/query_result';
+import {CpuFreqTrack} from './cpu_freq_track';
 
 class CpuFreq implements PerfettoPlugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
index 196f65f..2e70b2f 100644
--- a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
+++ b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assertExists} from '../../base/logging';
 import {Time} from '../../base/time';
 import {Actions} from '../../common/actions';
 import {LegacySelection} from '../../common/state';
-import {getColorForSlice} from '../../core/colorizer';
+import {getColorForSample} from '../../core/colorizer';
 import {
   BaseSliceTrack,
   OnSliceClickArgs,
@@ -23,9 +24,13 @@
 import {globals} from '../../frontend/globals';
 import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {Slice} from '../../public';
+import {NUM, Slice} from '../../public';
 
-export class CpuProfileTrack extends BaseSliceTrack<Slice, NamedRow> {
+interface CpuProfileRow extends NamedRow {
+  callsiteId: number;
+}
+
+export class CpuProfileTrack extends BaseSliceTrack<Slice, CpuProfileRow> {
   constructor(
     args: NewTrackArgs,
     private utid: number,
@@ -33,14 +38,14 @@
     super(args);
   }
 
-  protected getRowSpec(): NamedRow {
-    return NAMED_ROW;
+  protected getRowSpec(): CpuProfileRow {
+    return {...NAMED_ROW, callsiteId: NUM};
   }
 
-  protected rowToSlice(row: NamedRow): Slice {
+  protected rowToSlice(row: CpuProfileRow): Slice {
     const baseSlice = super.rowToSliceBase(row);
-    const name = row.name ?? '';
-    const colorScheme = getColorForSlice(name);
+    const name = assertExists(row.name);
+    const colorScheme = getColorForSample(row.callsiteId);
     return {...baseSlice, title: name, colorScheme};
   }
 
@@ -56,7 +61,13 @@
 
   getSqlSource(): string {
     return `
-      select p.id, ts, 0 as dur, 0 as depth, 'CPU Sample' as name
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'CPU Sample' as name,
+        callsite_id as callsiteId
       from cpu_profile_stack_sample p
       where utid = ${this.utid}
       order by ts
diff --git a/ui/src/core_plugins/critical_path/OWNERS b/ui/src/core_plugins/critical_path/OWNERS
new file mode 100644
index 0000000..cd23fd9
--- /dev/null
+++ b/ui/src/core_plugins/critical_path/OWNERS
@@ -0,0 +1,2 @@
+zezeozue@google.com
+lalitm@google.com
\ No newline at end of file
diff --git a/ui/src/core_plugins/critical_path/index.ts b/ui/src/core_plugins/critical_path/index.ts
new file mode 100644
index 0000000..f16dd65
--- /dev/null
+++ b/ui/src/core_plugins/critical_path/index.ts
@@ -0,0 +1,332 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  getThreadInfo,
+  ThreadInfo,
+} from '../../trace_processor/sql_utils/thread';
+
+import {
+  addDebugSliceTrack,
+  Engine,
+  PerfettoPlugin,
+  PluginContextTrace,
+  PluginDescriptor,
+  THREAD_STATE_TRACK_KIND,
+} from '../../public';
+import {
+  getTimeSpanOfSelectionOrVisibleWindow,
+  globals,
+} from '../../frontend/globals';
+import {asUtid, Utid} from '../../trace_processor/sql_utils/core_types';
+import {addQueryResultsTab} from '../../frontend/query_result_tab';
+import {showModal} from '../../widgets/modal';
+import {Optional} from '../../base/utils';
+import {
+  CRITICAL_PATH_CMD,
+  CRITICAL_PATH_LITE_CMD,
+} from '../../public/exposed_commands';
+
+const criticalPathSliceColumns = {
+  ts: 'ts',
+  dur: 'dur',
+  name: 'name',
+};
+
+const criticalPathsliceColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'name',
+  'table_name',
+];
+
+const criticalPathsliceLiteColumns = {
+  ts: 'ts',
+  dur: 'dur',
+  name: 'thread_name',
+};
+
+const criticalPathsliceLiteColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'thread_name',
+  'process_name',
+  'table_name',
+];
+
+const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
+
+const sliceLiteColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'thread_name',
+  'process_name',
+  'table_name',
+];
+
+const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
+
+const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
+
+function getFirstUtidOfSelectionOrVisibleWindow(): number {
+  const selection = globals.state.selection;
+  if (selection.kind === 'area') {
+    for (const trackUri of selection.trackUris) {
+      const trackDesc = globals.trackManager.getTrack(trackUri);
+      if (
+        trackDesc?.tags?.kind === THREAD_STATE_TRACK_KIND &&
+        trackDesc?.tags?.utid !== undefined
+      ) {
+        return trackDesc.tags.utid;
+      }
+    }
+  }
+
+  return 0;
+}
+
+function showModalErrorAreaSelectionRequired() {
+  showModal({
+    title: 'Error: range selection required',
+    content:
+      'This command requires an area selection over a thread state track.',
+  });
+}
+
+function showModalErrorThreadStateRequired() {
+  showModal({
+    title: 'Error: thread state selection required',
+    content: 'This command requires a thread state slice to be selected.',
+  });
+}
+
+// If utid is undefined, returns the utid for the selected thread state track,
+// if any. If it's defined, looks up the info about that specific utid.
+async function getThreadInfoForUtidOrSelection(
+  engine: Engine,
+  utid?: Utid,
+): Promise<Optional<ThreadInfo>> {
+  if (utid === undefined) {
+    if (
+      globals.state.selection.kind !== 'legacy' ||
+      globals.state.selection.legacySelection.kind !== 'THREAD_STATE'
+    ) {
+      return undefined;
+    }
+    const trackUri = globals.state.selection.legacySelection.trackUri;
+    if (trackUri === undefined) return undefined;
+    const track = globals.trackManager.getTrack(trackUri);
+    utid = asUtid(track?.tags?.utid);
+    if (utid === undefined) return undefined;
+  }
+  return getThreadInfo(engine, utid);
+}
+
+class CriticalPath implements PerfettoPlugin {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    // The 3 commands below are used in two contextes:
+    // 1. By clicking a slice and using the command palette. In this case the
+    //    utid argument is undefined and we need to look at the selection.
+    // 2. Invoked via runCommand(...) by thread_state_tab.ts when the user
+    //    clicks on the buttons in the details panel. In this case the details
+    //    panel passes the utid explicitly.
+    ctx.registerCommand({
+      id: CRITICAL_PATH_LITE_CMD,
+      name: 'Critical path lite (selected thread state slice)',
+      callback: async (utid?: Utid) => {
+        const thdInfo = await getThreadInfoForUtidOrSelection(ctx.engine, utid);
+        if (thdInfo === undefined) {
+          return showModalErrorThreadStateRequired();
+        }
+        ctx.engine
+          .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
+          .then(() =>
+            addDebugSliceTrack(
+              ctx,
+              {
+                sqlSource: `
+                SELECT
+                  cr.id,
+                  cr.utid,
+                  cr.ts,
+                  cr.dur,
+                  thread.name AS thread_name,
+                  process.name AS process_name,
+                  'thread_state' AS table_name
+                FROM
+                  _thread_executing_span_critical_path(
+                    ${thdInfo.utid},
+                    trace_bounds.start_ts,
+                    trace_bounds.end_ts - trace_bounds.start_ts) cr,
+                  trace_bounds
+                JOIN thread USING(utid)
+                JOIN process USING(upid)
+              `,
+                columns: sliceLiteColumnNames,
+              },
+              `${thdInfo.name}`,
+              sliceLiteColumns,
+              sliceLiteColumnNames,
+            ),
+          );
+      },
+    });
+
+    ctx.registerCommand({
+      id: CRITICAL_PATH_CMD,
+      name: 'Critical path (selected thread state slice)',
+      callback: async (utid?: Utid) => {
+        const thdInfo = await getThreadInfoForUtidOrSelection(ctx.engine, utid);
+        if (thdInfo === undefined) {
+          return showModalErrorThreadStateRequired();
+        }
+        ctx.engine
+          .query(
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+          )
+          .then(() =>
+            addDebugSliceTrack(
+              ctx,
+              {
+                sqlSource: `
+                SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
+                  FROM
+                    _thread_executing_span_critical_path_stack(
+                      ${thdInfo.utid},
+                      trace_bounds.start_ts,
+                      trace_bounds.end_ts - trace_bounds.start_ts) cr,
+                    trace_bounds WHERE name IS NOT NULL
+              `,
+                columns: sliceColumnNames,
+              },
+              `${thdInfo.name}`,
+              sliceColumns,
+              sliceColumnNames,
+            ),
+          );
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'perfetto.CriticalPathLite_AreaSelection',
+      name: 'Critical path lite (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow();
+        const window = await getTimeSpanOfSelectionOrVisibleWindow();
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        await ctx.engine.query(
+          `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
+        );
+        await addDebugSliceTrack(
+          ctx,
+          {
+            sqlSource: `
+                SELECT
+                  cr.id,
+                  cr.utid,
+                  cr.ts,
+                  cr.dur,
+                  thread.name AS thread_name,
+                  process.name AS process_name,
+                  'thread_state' AS table_name
+                FROM
+                  _thread_executing_span_critical_path(
+                      ${trackUtid},
+                      ${window.start},
+                      ${window.end} - ${window.start}) cr
+                JOIN thread USING(utid)
+                JOIN process USING(upid)
+                `,
+            columns: criticalPathsliceLiteColumnNames,
+          },
+          (await getThreadInfo(ctx.engine, trackUtid as Utid)).name ??
+            '<thread name>',
+          criticalPathsliceLiteColumns,
+          criticalPathsliceLiteColumnNames,
+        );
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'perfetto.CriticalPath_AreaSelection',
+      name: 'Critical path  (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow();
+        const window = await getTimeSpanOfSelectionOrVisibleWindow();
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        await ctx.engine.query(
+          `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+        );
+        await addDebugSliceTrack(
+          ctx,
+          {
+            sqlSource: `
+                SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
+                FROM
+                _critical_path_stack(
+                  ${trackUtid},
+                  ${window.start},
+                  ${window.end} - ${window.start}, 1, 1, 1, 1) cr
+                WHERE name IS NOT NULL
+                `,
+            columns: criticalPathsliceColumnNames,
+          },
+          (await getThreadInfo(ctx.engine, trackUtid as Utid)).name ??
+            '<thread name>',
+          criticalPathSliceColumns,
+          criticalPathsliceColumnNames,
+        );
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'perfetto.CriticalPathPprof_AreaSelection',
+      name: 'Critical path pprof (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow();
+        const window = await getTimeSpanOfSelectionOrVisibleWindow();
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        addQueryResultsTab({
+          query: `
+              INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
+              SELECT *
+                FROM
+                  _thread_executing_span_critical_path_graph(
+                  "criical_path",
+                    ${trackUtid},
+                    ${window.start},
+                    ${window.end} - ${window.start}) cr`,
+          title: 'Critical path',
+        });
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'perfetto.CriticalPath',
+  plugin: CriticalPath,
+};
diff --git a/ui/src/core_plugins/heap_profile/heap_profile_track.ts b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
index 12311dc..82c1f1f 100644
--- a/ui/src/core_plugins/heap_profile/heap_profile_track.ts
+++ b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
@@ -14,6 +14,7 @@
 
 import {Actions} from '../../common/actions';
 import {LegacySelection, ProfileType} from '../../common/state';
+import {profileType} from '../../core/selection_manager';
 import {
   BASE_ROW,
   BaseSliceTrack,
@@ -21,7 +22,6 @@
   OnSliceOverArgs,
 } from '../../frontend/base_slice_track';
 import {globals} from '../../frontend/globals';
-import {profileType} from '../../frontend/legacy_flamegraph_panel';
 import {NewTrackArgs} from '../../frontend/track';
 import {Slice} from '../../public';
 import {STR} from '../../trace_processor/query_result';
@@ -99,13 +99,9 @@
 
   rowToSlice(row: HeapProfileRow): HeapProfileSlice {
     const slice = this.rowToSliceBase(row);
-    let type = row.type;
-    if (type === 'heap_profile:libc.malloc,com.android.art') {
-      type = 'heap_profile:com.android.art,libc.malloc';
-    }
     return {
       ...slice,
-      type: profileType(type),
+      type: profileType(row.type),
     };
   }
 
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 3039b38..c99e8bf 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -16,16 +16,11 @@
 
 import {assertExists, assertFalse} from '../../base/logging';
 import {Monitor} from '../../base/monitor';
-import {LegacyFlamegraphCache} from '../../core/legacy_flamegraph_cache';
 import {
   HeapProfileSelection,
   LegacySelection,
   ProfileType,
 } from '../../core/selection_manager';
-import {
-  LegacyFlamegraphDetailsPanel,
-  profileType,
-} from '../../frontend/legacy_flamegraph_panel';
 import {Timestamp} from '../../frontend/widgets/timestamp';
 import {
   Engine,
@@ -42,7 +37,6 @@
 import {
   QueryFlamegraph,
   QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
 } from '../../core/query_flamegraph';
 import {time} from '../../base/time';
@@ -57,7 +51,6 @@
 import {Modal} from '../../widgets/modal';
 import {Router} from '../../frontend/router';
 import {Actions} from '../../common/actions';
-import {SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG} from '../../common/legacy_flamegraph_util';
 
 class HeapProfilePlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -104,7 +97,6 @@
     () => this.sel?.type,
   ]);
   private flamegraphAttrs?: QueryFlamegraphAttrs;
-  private cache = new LegacyFlamegraphCache('heap_profile');
 
   constructor(
     private engine: Engine,
@@ -116,18 +108,6 @@
       this.sel = undefined;
       return undefined;
     }
-    if (!USE_NEW_FLAMEGRAPH_IMPL.get()) {
-      this.sel = undefined;
-      return m(LegacyFlamegraphDetailsPanel, {
-        cache: this.cache,
-        selection: {
-          profileType: profileType(sel.type),
-          start: sel.ts,
-          end: sel.ts,
-          upids: [sel.upid],
-        },
-      });
-    }
 
     const {ts, upid, type} = sel;
     this.sel = sel;
@@ -318,37 +298,6 @@
 }
 
 function flamegraphAttrsForHeapGraph(engine: Engine, ts: time, upid: number) {
-  const dominator = SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
-    ? metricsFromTableOrSubquery(
-        `
-          (
-            select
-              id,
-              parent_id as parentId,
-              name,
-              root_type,
-              self_size,
-              self_count
-            from _heap_graph_dominator_class_tree
-            where graph_sample_ts = ${ts} and upid = ${upid}
-          )
-        `,
-        [
-          {
-            name: 'Dominated Object Size',
-            unit: 'B',
-            columnName: 'self_size',
-          },
-          {
-            name: 'Dominated Object Count',
-            unit: '',
-            columnName: 'self_count',
-          },
-        ],
-        'include perfetto module android.memory.heap_graph.dominator_class_tree;',
-        [{name: 'root_type', displayName: 'Root Type'}],
-      )
-    : [];
   return {
     engine,
     metrics: [
@@ -381,7 +330,35 @@
         'include perfetto module android.memory.heap_graph.class_tree;',
         [{name: 'root_type', displayName: 'Root Type'}],
       ),
-      ...dominator,
+      ...metricsFromTableOrSubquery(
+        `
+          (
+            select
+              id,
+              parent_id as parentId,
+              name,
+              root_type,
+              self_size,
+              self_count
+            from _heap_graph_dominator_class_tree
+            where graph_sample_ts = ${ts} and upid = ${upid}
+          )
+        `,
+        [
+          {
+            name: 'Dominated Object Size',
+            unit: 'B',
+            columnName: 'self_size',
+          },
+          {
+            name: 'Dominated Object Count',
+            unit: '',
+            columnName: 'self_count',
+          },
+        ],
+        'include perfetto module android.memory.heap_graph.dominator_class_tree;',
+        [{name: 'root_type', displayName: 'Root Type'}],
+      ),
     ],
   };
 }
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index c2cdf92..3a9092d 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -20,11 +20,6 @@
   LegacyDetailsPanel,
   PERF_SAMPLES_PROFILE_TRACK_KIND,
 } from '../../public';
-import {LegacyFlamegraphCache} from '../../core/legacy_flamegraph_cache';
-import {
-  LegacyFlamegraphDetailsPanel,
-  profileType,
-} from '../../frontend/legacy_flamegraph_panel';
 import {
   PerfettoPlugin,
   PluginContextTrace,
@@ -38,7 +33,6 @@
 import {
   QueryFlamegraph,
   QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
 } from '../../core/query_flamegraph';
 import {Monitor} from '../../base/monitor';
@@ -139,7 +133,6 @@
     () => this.sel?.type,
   ]);
   private flamegraphAttrs?: QueryFlamegraphAttrs;
-  private cache = new LegacyFlamegraphCache('perf_samples');
 
   constructor(private engine: Engine) {}
 
@@ -148,22 +141,6 @@
       this.sel = undefined;
       return undefined;
     }
-    if (
-      !USE_NEW_FLAMEGRAPH_IMPL.get() &&
-      sel.utid === undefined &&
-      sel.upid !== undefined
-    ) {
-      this.sel = undefined;
-      return m(LegacyFlamegraphDetailsPanel, {
-        cache: this.cache,
-        selection: {
-          profileType: profileType(sel.type),
-          start: sel.leftTs,
-          end: sel.rightTs,
-          upids: [sel.upid],
-        },
-      });
-    }
 
     const {leftTs, rightTs, upid, utid} = sel;
     this.sel = sel;
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
index 0e43da4..b8b9d0c 100644
--- a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
+++ b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
@@ -12,35 +12,40 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Slice} from '../../public';
+import {NUM, Slice} from '../../public';
 import {
   BaseSliceTrack,
   OnSliceClickArgs,
 } from '../../frontend/base_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
 import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
-import {getColorForSlice} from '../../core/colorizer';
+import {getColorForSample} from '../../core/colorizer';
 import {Time} from '../../base/time';
 import {globals} from '../../frontend/globals';
 import {Actions} from '../../common/actions';
 import {LegacySelection, ProfileType} from '../../core/selection_manager';
+import {assertExists} from '../../base/logging';
+
+interface PerfSampleRow extends NamedRow {
+  callsiteId: number;
+}
 
 abstract class BasePerfSamplesProfileTrack extends BaseSliceTrack<
   Slice,
-  NamedRow
+  PerfSampleRow
 > {
   constructor(args: NewTrackArgs) {
     super(args);
   }
 
-  protected getRowSpec(): NamedRow {
-    return NAMED_ROW;
+  protected getRowSpec(): PerfSampleRow {
+    return {...NAMED_ROW, callsiteId: NUM};
   }
 
-  protected rowToSlice(row: NamedRow): Slice {
+  protected rowToSlice(row: PerfSampleRow): Slice {
     const baseSlice = super.rowToSliceBase(row);
-    const name = row.name ?? '';
-    const colorScheme = getColorForSlice(name);
+    const name = assertExists(row.name);
+    const colorScheme = getColorForSample(row.callsiteId);
     return {...baseSlice, title: name, colorScheme};
   }
 
@@ -65,11 +70,16 @@
 
   getSqlSource(): string {
     return `
-      select p.id, ts, 0 as dur, 0 as depth, 'Perf Sample' as name
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Perf Sample' as name,
+        callsite_id as callsiteId
       from perf_sample p
       join thread using (utid)
-      where upid = ${this.upid}
-        and callsite_id is not null
+      where upid = ${this.upid} and callsite_id is not null
       order by ts
     `;
   }
@@ -97,10 +107,15 @@
 
   getSqlSource(): string {
     return `
-      select p.id, ts, 0 as dur, 0 as depth, 'Perf Sample' as name
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Perf Sample' as name,
+        callsite_id as callsiteId
       from perf_sample p
-      where utid = ${this.utid}
-        and callsite_id is not null
+      where utid = ${this.utid} and callsite_id is not null
       order by ts
     `;
   }
diff --git a/ui/src/core_plugins/sched/index.ts b/ui/src/core_plugins/sched/index.ts
index 2523a84..b35638b 100644
--- a/ui/src/core_plugins/sched/index.ts
+++ b/ui/src/core_plugins/sched/index.ts
@@ -14,7 +14,7 @@
 
 import {addSqlTableTab} from '../../frontend/sql_table_tab_command';
 import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
-import {TrackNode} from '../../frontend/workspace';
+import {TrackNode} from '../../public/workspace';
 import {
   PerfettoPlugin,
   PluginContextTrace,
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index 8bec08e..aefc40b 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -23,12 +23,7 @@
 import {EmptyState} from '../widgets/empty_state';
 import {FlowEventsAreaSelectedPanel} from './flow_events_panel';
 import {PivotTable} from './pivot_table';
-import {
-  LegacyFlamegraphDetailsPanel,
-  FlamegraphSelectionParams,
-} from './legacy_flamegraph_panel';
-import {AreaSelection, ProfileType} from '../common/state';
-import {assertExists} from '../base/logging';
+import {AreaSelection} from '../common/state';
 import {Monitor} from '../base/monitor';
 import {
   CPU_PROFILE_TRACK_KIND,
@@ -38,10 +33,10 @@
 import {
   QueryFlamegraph,
   QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
 } from '../core/query_flamegraph';
 import {DisposableStack} from '../base/disposable_stack';
+import {assertExists} from '../base/logging';
 
 interface View {
   key: string;
@@ -55,7 +50,6 @@
   private cpuProfileFlamegraphAttrs?: QueryFlamegraphAttrs;
   private perfSampleFlamegraphAttrs?: QueryFlamegraphAttrs;
   private sliceFlamegraphAttrs?: QueryFlamegraphAttrs;
-  private legacyFlamegraphSelection?: FlamegraphSelectionParams;
 
   private getCurrentView(): string | undefined {
     const types = this.getViews().map(({key}) => key);
@@ -103,12 +97,7 @@
       });
     }
 
-    const isChanged = this.monitor.ifStateChanged();
-    if (USE_NEW_FLAMEGRAPH_IMPL.get()) {
-      this.addFlamegraphView(isChanged, views);
-    } else {
-      this.addLegacyFlamegraphView(isChanged, views);
-    }
+    this.addFlamegraphView(this.monitor.ifStateChanged(), views);
 
     // Add this after all aggregation panels, to make it appear after 'Slices'
     if (globals.selectedFlows.length > 0) {
@@ -364,44 +353,6 @@
     };
   }
 
-  private addLegacyFlamegraphView(isChanged: boolean, views: View[]) {
-    this.legacyFlamegraphSelection =
-      this.computeLegacyFlamegraphSelection(isChanged);
-    if (this.legacyFlamegraphSelection === undefined) {
-      return;
-    }
-    views.push({
-      key: 'flamegraph_selection',
-      name: 'Flamegraph Selection',
-      content: m(LegacyFlamegraphDetailsPanel, {
-        cache: globals.areaFlamegraphCache,
-        selection: this.legacyFlamegraphSelection,
-      }),
-    });
-  }
-
-  private computeLegacyFlamegraphSelection(isChanged: boolean) {
-    const currentSelection = globals.state.selection;
-    if (currentSelection.kind !== 'area') {
-      return undefined;
-    }
-    if (!isChanged) {
-      // If the selection has not changed, just return a copy of the last seen
-      // selection.
-      return this.legacyFlamegraphSelection;
-    }
-    const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
-    if (upids.length === 0) {
-      return undefined;
-    }
-    return {
-      profileType: ProfileType.PERF_SAMPLE,
-      start: currentSelection.start,
-      end: currentSelection.end,
-      upids,
-    };
-  }
-
   private getCurrentEngine() {
     const engineId = globals.getCurrentEngine()?.id;
     if (engineId === undefined) return undefined;
diff --git a/ui/src/frontend/cpu_profile_panel.ts b/ui/src/frontend/cpu_profile_panel.ts
deleted file mode 100644
index 0987d50..0000000
--- a/ui/src/frontend/cpu_profile_panel.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {globals} from './globals';
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-
-interface CpuProfileDetailsPanelAttrs {}
-
-export class CpuProfileDetailsPanel
-  implements m.ClassComponent<CpuProfileDetailsPanelAttrs>
-{
-  view() {
-    const sampleDetails = globals.cpuProfileDetails;
-    const header = m(
-      '.details-panel-heading',
-      m('h2', `CPU Profile Sample Details`),
-    );
-    if (sampleDetails.id === undefined) {
-      return m('.details-panel', header);
-    }
-
-    return m(
-      '.details-panel',
-      header,
-      m('table', this.getStackText(sampleDetails.stack)),
-    );
-  }
-
-  getStackText(stack?: CallsiteInfo[]): m.Vnode[] {
-    if (!stack) return [];
-
-    const result = [];
-    for (let i = stack.length - 1; i >= 0; --i) {
-      result.push(m('tr', m('td', stack[i].name), m('td', stack[i].mapping)));
-    }
-
-    return result;
-  }
-}
diff --git a/ui/src/frontend/debug_tracks/debug_tracks.ts b/ui/src/frontend/debug_tracks/debug_tracks.ts
index 4286a2b..ee7ef32 100644
--- a/ui/src/frontend/debug_tracks/debug_tracks.ts
+++ b/ui/src/frontend/debug_tracks/debug_tracks.ts
@@ -24,7 +24,7 @@
 import {Engine} from '../../trace_processor/engine';
 import {DebugCounterTrack} from './counter_track';
 import {ARG_PREFIX} from './details_tab';
-import {TrackNode} from '../workspace';
+import {TrackNode} from '../../public/workspace';
 import {raf} from '../../core/raf_scheduler';
 
 // We need to add debug tracks from the core and from plugins. In order to add a
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 143a53f..d5bef7e 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -75,6 +75,11 @@
     return;
   }
 
+  if (err.message.includes('(ERR:ws)')) {
+    showWebsocketConnectionIssue(err.message);
+    return;
+  }
+
   // This is only for older version of the UI and for ease of tracking across
   // cherry-picks. Newer versions don't have this exception anymore.
   if (err.message.includes('State hash does not match')) {
@@ -443,7 +448,11 @@
 export function showWebsocketConnectionIssue(message: string): void {
   showModal({
     title: 'Unable to connect to the device via websocket',
-    content: m('div', m('span', message), m('br')),
+    content: m(
+      'div',
+      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+      m('pre', message),
+    ),
   });
 }
 
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4838a09..1e8b3d6 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -20,7 +20,7 @@
 import {Flow, globals} from './globals';
 import {RenderedPanelInfo} from './panel_container';
 import {PxSpan, TimeScale} from './time_scale';
-import {TrackNode} from './workspace';
+import {TrackNode} from '../public/workspace';
 
 const TRACK_GROUP_CONNECTION_OFFSET = 5;
 const TRIANGLE_SIZE = 5;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index ee034d9..b889596 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -41,8 +41,6 @@
 import {SelectionManager, LegacySelection} from '../core/selection_manager';
 import {Optional, exists} from '../base/utils';
 import {OmniboxManager} from './omnibox_manager';
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
 import {SerializedAppState} from '../common/state_serialization_schema';
 import {getServingRoot} from '../base/http_utils';
 import {
@@ -53,7 +51,7 @@
 import {TraceContext} from './trace_context';
 import {Registry} from '../base/registry';
 import {SidebarMenuItem} from '../public';
-import {Workspace} from './workspace';
+import {Workspace} from '../public/workspace';
 
 const INSTANT_FOCUS_DURATION = 1n;
 const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -134,13 +132,6 @@
   dur?: duration;
 }
 
-export interface CpuProfileDetails {
-  id?: number;
-  ts?: time;
-  utid?: number;
-  stack?: CallsiteInfo[];
-}
-
 export interface QuantizedLoad {
   start: time;
   end: time;
@@ -224,7 +215,6 @@
   private _connectedFlows?: Flow[] = undefined;
   private _selectedFlows?: Flow[] = undefined;
   private _visibleFlowCategories?: Map<string, boolean> = undefined;
-  private _cpuProfileDetails?: CpuProfileDetails = undefined;
   private _numQueriesQueued = 0;
   private _bufferUsage?: number = undefined;
   private _recordingLog?: string = undefined;
@@ -244,7 +234,6 @@
   private _currentWorkspace: Workspace;
 
   omnibox = new OmniboxManager();
-  areaFlamegraphCache = new LegacyFlamegraphCache('area');
 
   scrollToTrackUri?: string;
   httpRpcState: HttpRpcState = {connected: false};
@@ -371,7 +360,6 @@
     this._selectedFlows = [];
     this._visibleFlowCategories = new Map<string, boolean>();
     this._threadStateDetails = {};
-    this._cpuProfileDetails = {};
     this.engines.clear();
     this._selectionManager.clear();
   }
@@ -502,14 +490,6 @@
     this._metricResult = result;
   }
 
-  get cpuProfileDetails() {
-    return assertExists(this._cpuProfileDetails);
-  }
-
-  set cpuProfileDetails(click: CpuProfileDetails) {
-    this._cpuProfileDetails = assertExists(click);
-  }
-
   set numQueuedQueries(value: number) {
     this._numQueriesQueued = value;
   }
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index e331eee..d61b5a6 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -36,7 +36,7 @@
 import {initWasm} from '../trace_processor/wasm_engine_proxy';
 import {setScheduleFullRedraw} from '../widgets/raf';
 
-import {App} from './app';
+import {UiMain} from './ui_main';
 import {initCssConstants} from './css_constants';
 import {registerDebugGlobals} from './debug';
 import {maybeShowErrorDialog} from './error_dialog';
@@ -289,7 +289,7 @@
   router.onRouteChanged = routeChange;
 
   raf.domRedraw = () => {
-    m.render(document.body, m(App, router.resolve()));
+    m.render(document.body, m(UiMain, router.resolve()));
   };
 
   if (
@@ -356,7 +356,7 @@
   });
 
   // Force one initial render to get everything in place
-  m.render(document.body, m(App, router.resolve()));
+  m.render(document.body, m(UiMain, router.resolve()));
 
   // Initialize plugins, now that we are ready to go
   pluginManager.initialize();
diff --git a/ui/src/frontend/legacy_flamegraph.ts b/ui/src/frontend/legacy_flamegraph.ts
deleted file mode 100644
index caa5e3e..0000000
--- a/ui/src/frontend/legacy_flamegraph.ts
+++ /dev/null
@@ -1,489 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {searchSegment} from '../base/binary_search';
-import {cropText} from '../base/string_utils';
-
-interface Node {
-  width: number;
-  x: number;
-  nextXForChildren: number;
-  size: number;
-}
-
-interface CallsiteInfoWidth {
-  callsite: CallsiteInfo;
-  width: number;
-}
-
-// Height of one 'row' on the flame chart including 1px of whitespace
-// below the box.
-const NODE_HEIGHT = 18;
-
-export const FLAMEGRAPH_HOVERED_COLOR = 'hsl(224, 45%, 55%)';
-
-export function findRootSize(data: ReadonlyArray<CallsiteInfo>) {
-  let totalSize = 0;
-  let i = 0;
-  while (i < data.length && data[i].depth === 0) {
-    totalSize += data[i].totalSize;
-    i++;
-  }
-  return totalSize;
-}
-
-export interface NodeRendering {
-  totalSize?: string;
-  selfSize?: string;
-}
-
-export class Flamegraph {
-  private nodeRendering: NodeRendering = {};
-  private flamegraphData: ReadonlyArray<CallsiteInfo>;
-  private highlightSomeNodes = false;
-  private maxDepth = -1;
-  private totalSize = -1;
-  // Initialised on first draw() call
-  private labelCharWidth = 0;
-  private labelFontStyle = '12px Roboto Mono';
-  private rolloverFontStyle = '12px Roboto Condensed';
-  // Key for the map is depth followed by x coordinate - `depth;x`
-  private graphData: Map<string, CallsiteInfoWidth> = new Map();
-  private xStartsPerDepth: Map<number, number[]> = new Map();
-
-  private hoveredX = -1;
-  private hoveredY = -1;
-  private hoveredCallsite?: CallsiteInfo;
-  private clickedCallsite?: CallsiteInfo;
-
-  private startingY = 0;
-
-  constructor(flamegraphData: CallsiteInfo[]) {
-    this.flamegraphData = flamegraphData;
-    this.findMaxDepth();
-  }
-
-  private findMaxDepth() {
-    this.maxDepth = Math.max(
-      ...this.flamegraphData.map((value) => value.depth),
-    );
-  }
-
-  // Instead of highlighting the interesting nodes, we actually want to
-  // de-emphasize the non-highlighted nodes. Returns true if there
-  // are any highlighted nodes in the flamegraph.
-  private highlightingExists() {
-    this.highlightSomeNodes = this.flamegraphData.some((e) => e.highlighted);
-  }
-
-  generateColor(
-    name: string,
-    isGreyedOut = false,
-    highlighted: boolean,
-  ): string {
-    if (isGreyedOut) {
-      return '#d9d9d9';
-    }
-    if (name === 'unknown' || name === 'root') {
-      return '#c0c0c0';
-    }
-    let x = 0;
-    for (let i = 0; i < name.length; i += 1) {
-      x += name.charCodeAt(i) % 64;
-    }
-    x = x % 360;
-    let l = '76';
-    // Make non-highlighted node lighter.
-    if (this.highlightSomeNodes && !highlighted) {
-      l = '90';
-    }
-    return `hsl(${x}deg, 45%, ${l}%)`;
-  }
-
-  // Caller will have to call draw method after updating data to have updated
-  // graph.
-  updateDataIfChanged(
-    nodeRendering: NodeRendering,
-    flamegraphData: ReadonlyArray<CallsiteInfo>,
-    clickedCallsite?: CallsiteInfo,
-  ) {
-    this.nodeRendering = nodeRendering;
-    this.clickedCallsite = clickedCallsite;
-    if (this.flamegraphData === flamegraphData) {
-      return;
-    }
-    this.flamegraphData = flamegraphData;
-    this.clickedCallsite = clickedCallsite;
-    this.findMaxDepth();
-    this.highlightingExists();
-    // Finding total size of roots.
-    this.totalSize = findRootSize(flamegraphData);
-  }
-
-  draw(
-    ctx: CanvasRenderingContext2D,
-    width: number,
-    height: number,
-    x = 0,
-    y = 0,
-    unit = 'B',
-  ) {
-    if (this.flamegraphData === undefined) {
-      return;
-    }
-
-    ctx.font = this.labelFontStyle;
-    ctx.textBaseline = 'middle';
-    if (this.labelCharWidth === 0) {
-      this.labelCharWidth = ctx.measureText('_').width;
-    }
-
-    this.startingY = y;
-
-    // For each node, we use this map to get information about its parent
-    // (total size of it, width and where it starts in graph) so we can
-    // calculate it's own position in graph.
-    const nodesMap = new Map<number, Node>();
-    let currentY = y;
-    nodesMap.set(-1, {width, nextXForChildren: x, size: this.totalSize, x});
-
-    // Initialize data needed for click/hover behavior.
-    this.graphData = new Map();
-    this.xStartsPerDepth = new Map();
-
-    // Draw root node.
-    ctx.fillStyle = this.generateColor('root', false, false);
-    ctx.fillRect(x, currentY, width, NODE_HEIGHT - 1);
-    const text = cropText(
-      `root: ${this.displaySize(
-        this.totalSize,
-        unit,
-        unit === 'B' ? 1024 : 1000,
-      )}`,
-      this.labelCharWidth,
-      width - 2,
-    );
-    ctx.fillStyle = 'black';
-    ctx.fillText(text, x + 5, currentY + (NODE_HEIGHT - 1) / 2);
-    currentY += NODE_HEIGHT;
-
-    // Set style for borders.
-    ctx.strokeStyle = 'white';
-    ctx.lineWidth = 0.5;
-
-    for (let i = 0; i < this.flamegraphData.length; i++) {
-      if (currentY > height) {
-        break;
-      }
-      const value = this.flamegraphData[i];
-      const parentNode = nodesMap.get(value.parentId);
-      if (parentNode === undefined) {
-        continue;
-      }
-
-      const isClicked = this.clickedCallsite !== undefined;
-      const isFullWidth =
-        isClicked && value.depth <= this.clickedCallsite!.depth;
-      const isGreyedOut =
-        isClicked && value.depth < this.clickedCallsite!.depth;
-
-      const parent = value.parentId;
-      const parentSize = parent === -1 ? this.totalSize : parentNode.size;
-      // Calculate node's width based on its proportion in parent.
-      const width =
-        (isFullWidth ? 1 : value.totalSize / parentSize) * parentNode.width;
-
-      const currentX = parentNode.nextXForChildren;
-      currentY = y + NODE_HEIGHT * (value.depth + 1);
-
-      // Draw node.
-      const name = this.getCallsiteName(value);
-      ctx.fillStyle = this.generateColor(name, isGreyedOut, value.highlighted);
-      ctx.fillRect(currentX, currentY, width, NODE_HEIGHT - 1);
-
-      // Set current node's data in map for children to use.
-      nodesMap.set(value.id, {
-        width,
-        nextXForChildren: currentX,
-        size: value.totalSize,
-        x: currentX,
-      });
-      // Update next x coordinate in parent.
-      nodesMap.set(value.parentId, {
-        width: parentNode.width,
-        nextXForChildren: currentX + width,
-        size: parentNode.size,
-        x: parentNode.x,
-      });
-
-      // Draw name.
-      const labelPaddingPx = 5;
-      const maxLabelWidth = width - labelPaddingPx * 2;
-      let text = cropText(name, this.labelCharWidth, maxLabelWidth);
-      // If cropped text and the original text are within 20% we keep the
-      // original text and just squish it a bit.
-      if (text.length * 1.2 > name.length) {
-        text = name;
-      }
-      ctx.fillStyle = 'black';
-      ctx.fillText(
-        text,
-        currentX + labelPaddingPx,
-        currentY + (NODE_HEIGHT - 1) / 2,
-        maxLabelWidth,
-      );
-
-      // Draw border on the right of node.
-      ctx.beginPath();
-      ctx.moveTo(currentX + width, currentY);
-      ctx.lineTo(currentX + width, currentY + NODE_HEIGHT);
-      ctx.stroke();
-
-      // Add this node for recognizing in click/hover.
-      // Map graphData contains one callsite which is on that depth and X
-      // start. Map xStartsPerDepth for each depth contains all X start
-      // coordinates that callsites on that level have.
-      this.graphData.set(`${value.depth};${currentX}`, {
-        callsite: value,
-        width,
-      });
-      const xStarts = this.xStartsPerDepth.get(value.depth);
-      if (xStarts === undefined) {
-        this.xStartsPerDepth.set(value.depth, [currentX]);
-      } else {
-        xStarts.push(currentX);
-      }
-    }
-
-    // Draw the tooltip.
-    if (this.hoveredX > -1 && this.hoveredY > -1 && this.hoveredCallsite) {
-      // Must set these before measureText below.
-      ctx.font = this.rolloverFontStyle;
-      ctx.textBaseline = 'top';
-
-      // Size in px of the border around the text and the edge of the rollover
-      // background.
-      const paddingPx = 8;
-      // Size in px of the x and y offset between the mouse and the top left
-      // corner of the rollover box.
-      const offsetPx = 4;
-
-      const lines: string[] = [];
-
-      let textWidth = this.addToTooltip(
-        this.getCallsiteName(this.hoveredCallsite),
-        width - paddingPx,
-        ctx,
-        lines,
-      );
-      if (this.hoveredCallsite.location != null) {
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(this.hoveredCallsite.location, width, ctx, lines),
-        );
-      }
-      textWidth = Math.max(
-        textWidth,
-        this.addToTooltip(this.hoveredCallsite.mapping, width, ctx, lines),
-      );
-
-      if (this.nodeRendering.totalSize !== undefined) {
-        const percentage =
-          (this.hoveredCallsite.totalSize / this.totalSize) * 100;
-        const totalSizeText = `${
-          this.nodeRendering.totalSize
-        }: ${this.displaySize(
-          this.hoveredCallsite.totalSize,
-          unit,
-          unit === 'B' ? 1024 : 1000,
-        )} (${percentage.toFixed(2)}%)`;
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(totalSizeText, width, ctx, lines),
-        );
-      }
-
-      if (
-        this.nodeRendering.selfSize !== undefined &&
-        this.hoveredCallsite.selfSize > 0
-      ) {
-        const selfPercentage =
-          (this.hoveredCallsite.selfSize / this.totalSize) * 100;
-        const selfSizeText = `${
-          this.nodeRendering.selfSize
-        }: ${this.displaySize(
-          this.hoveredCallsite.selfSize,
-          unit,
-          unit === 'B' ? 1024 : 1000,
-        )} (${selfPercentage.toFixed(2)}%)`;
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(selfSizeText, width, ctx, lines),
-        );
-      }
-
-      // Compute a line height as the bounding box height + 50%:
-      const heightSample = ctx.measureText(
-        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
-      );
-      const lineHeight = Math.round(
-        heightSample.actualBoundingBoxDescent * 1.5,
-      );
-
-      const rectWidth = textWidth + 2 * paddingPx;
-      const rectHeight = lineHeight * lines.length + 2 * paddingPx;
-
-      let rectXStart = this.hoveredX + offsetPx;
-      let rectYStart = this.hoveredY + offsetPx;
-
-      if (rectXStart + rectWidth > width) {
-        rectXStart = width - rectWidth;
-      }
-
-      if (rectYStart + rectHeight > height) {
-        rectYStart = height - rectHeight;
-      }
-
-      ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
-      ctx.fillRect(rectXStart, rectYStart, rectWidth, rectHeight);
-      ctx.fillStyle = 'hsl(200, 50%, 40%)';
-      ctx.textAlign = 'left';
-      for (let i = 0; i < lines.length; i++) {
-        const line = lines[i];
-        ctx.fillText(
-          line,
-          rectXStart + paddingPx,
-          rectYStart + paddingPx + i * lineHeight,
-        );
-      }
-    }
-  }
-
-  private addToTooltip(
-    text: string,
-    width: number,
-    ctx: CanvasRenderingContext2D,
-    lines: string[],
-  ): number {
-    const lineSplitter: LineSplitter = splitIfTooBig(
-      text,
-      width,
-      ctx.measureText(text).width,
-    );
-    lines.push(...lineSplitter.lines);
-    return lineSplitter.lineWidth;
-  }
-
-  private getCallsiteName(value: CallsiteInfo): string {
-    return value.name === undefined || value.name === ''
-      ? 'unknown'
-      : value.name;
-  }
-
-  private displaySize(totalSize: number, unit: string, step = 1024): string {
-    if (unit === '') return totalSize.toLocaleString();
-    if (totalSize === 0) return `0 ${unit}`;
-    const units = [
-      ['', 1],
-      ['K', step],
-      ['M', Math.pow(step, 2)],
-      ['G', Math.pow(step, 3)],
-    ];
-    let unitsIndex = Math.trunc(Math.log(totalSize) / Math.log(step));
-    unitsIndex = unitsIndex > units.length - 1 ? units.length - 1 : unitsIndex;
-    const result = totalSize / +units[unitsIndex][1];
-    const resultString =
-      totalSize % +units[unitsIndex][1] === 0
-        ? result.toString()
-        : result.toFixed(2);
-    return `${resultString} ${units[unitsIndex][0]}${unit}`;
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    this.hoveredX = x;
-    this.hoveredY = y;
-    this.hoveredCallsite = this.findSelectedCallsite(x, y);
-    const isCallsiteSelected = this.hoveredCallsite !== undefined;
-    if (!isCallsiteSelected) {
-      this.onMouseOut();
-    }
-    return isCallsiteSelected;
-  }
-
-  onMouseOut() {
-    this.hoveredX = -1;
-    this.hoveredY = -1;
-    this.hoveredCallsite = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}): CallsiteInfo | undefined {
-    const clickedCallsite = this.findSelectedCallsite(x, y);
-    // TODO(b/148596659): Allow to expand [merged] callsites. Currently,
-    // this expands to the biggest of the nodes that were merged, which
-    // is confusing, so we disallow clicking on them.
-    if (clickedCallsite === undefined || clickedCallsite.merged) {
-      return undefined;
-    }
-    return clickedCallsite;
-  }
-
-  private findSelectedCallsite(x: number, y: number): CallsiteInfo | undefined {
-    const depth = Math.trunc((y - this.startingY) / NODE_HEIGHT) - 1; // at 0 is root
-    if (depth >= 0 && this.xStartsPerDepth.has(depth)) {
-      const startX = this.searchSmallest(this.xStartsPerDepth.get(depth)!, x);
-      const result = this.graphData.get(`${depth};${startX}`);
-      if (result !== undefined) {
-        const width = result.width;
-        return startX + width >= x ? result.callsite : undefined;
-      }
-    }
-    return undefined;
-  }
-
-  searchSmallest(haystack: number[], needle: number): number {
-    haystack = haystack.sort((n1, n2) => n1 - n2);
-    const [left] = searchSegment(haystack, needle);
-    return left === -1 ? -1 : haystack[left];
-  }
-
-  getHeight(): number {
-    return this.flamegraphData.length === 0
-      ? 0
-      : (this.maxDepth + 2) * NODE_HEIGHT;
-  }
-}
-
-export interface LineSplitter {
-  lineWidth: number;
-  lines: string[];
-}
-
-export function splitIfTooBig(
-  line: string,
-  width: number,
-  lineWidth: number,
-): LineSplitter {
-  if (line === '') return {lineWidth, lines: []};
-  const lines: string[] = [];
-  const charWidth = lineWidth / line.length;
-  const maxWidth = width - 32;
-  const maxLineLen = Math.trunc(maxWidth / charWidth);
-  while (line.length > 0) {
-    lines.push(line.slice(0, maxLineLen));
-    line = line.slice(maxLineLen);
-  }
-  lineWidth = Math.min(maxLineLen * charWidth, lineWidth);
-  return {lineWidth, lines};
-}
diff --git a/ui/src/frontend/legacy_flamegraph_panel.ts b/ui/src/frontend/legacy_flamegraph_panel.ts
deleted file mode 100644
index 9a476b8..0000000
--- a/ui/src/frontend/legacy_flamegraph_panel.ts
+++ /dev/null
@@ -1,902 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m, {Vnode} from 'mithril';
-
-import {findRef} from '../base/dom_utils';
-import {assertExists, assertTrue} from '../base/logging';
-import {time} from '../base/time';
-import {Actions} from '../common/actions';
-import {
-  CallsiteInfo,
-  FlamegraphViewingOption,
-  defaultViewingOption,
-  expandCallsites,
-  findRootSize,
-  mergeCallsites,
-  viewingOptions,
-} from '../common/legacy_flamegraph_util';
-import {ProfileType} from '../common/state';
-import {raf} from '../core/raf_scheduler';
-import {Button} from '../widgets/button';
-import {Icon} from '../widgets/icon';
-import {Modal, ModalAttrs} from '../widgets/modal';
-import {Popup} from '../widgets/popup';
-import {EmptyState} from '../widgets/empty_state';
-import {Spinner} from '../widgets/spinner';
-
-import {Flamegraph, NodeRendering} from './legacy_flamegraph';
-import {globals} from './globals';
-import {debounce} from './rate_limiters';
-import {Router} from './router';
-import {ButtonBar} from '../widgets/button';
-import {DurationWidget} from './widgets/duration';
-import {DetailsShell} from '../widgets/details_shell';
-import {Intent} from '../widgets/common';
-import {Engine, NUM, STR} from '../public';
-import {Monitor} from '../base/monitor';
-import {arrayEquals} from '../base/array_utils';
-import {getCurrentTrace} from './sidebar';
-import {convertTraceToPprofAndDownload} from './trace_converter';
-import {AsyncLimiter} from '../base/async_limiter';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
-
-const HEADER_HEIGHT = 30;
-
-export function profileType(s: string): ProfileType {
-  if (isProfileType(s)) {
-    return s;
-  }
-  if (s.startsWith('heap_profile')) {
-    return ProfileType.HEAP_PROFILE;
-  }
-  throw new Error('Unknown type ${s}');
-}
-
-function isProfileType(s: string): s is ProfileType {
-  return Object.values(ProfileType).includes(s as ProfileType);
-}
-
-function getFlamegraphType(type: ProfileType) {
-  switch (type) {
-    case ProfileType.HEAP_PROFILE:
-    case ProfileType.MIXED_HEAP_PROFILE:
-    case ProfileType.NATIVE_HEAP_PROFILE:
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return 'native';
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return 'graph';
-    case ProfileType.PERF_SAMPLE:
-      return 'perf';
-    default:
-      const exhaustiveCheck: never = type;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-const HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS = [
-  FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
-  FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
-] as const;
-
-export type HeapGraphDominatorTreeViewingOption =
-  (typeof HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS)[number];
-
-export function isHeapGraphDominatorTreeViewingOption(
-  option: FlamegraphViewingOption,
-): option is HeapGraphDominatorTreeViewingOption {
-  return (
-    HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS as readonly FlamegraphViewingOption[]
-  ).includes(option);
-}
-
-const MIN_PIXEL_DISPLAYED = 1;
-
-function toSelectedCallsite(c: CallsiteInfo | undefined): string {
-  if (c !== undefined && c.name !== undefined) {
-    return c.name;
-  }
-  return '(none)';
-}
-
-const RENDER_SELF_AND_TOTAL: NodeRendering = {
-  selfSize: 'Self',
-  totalSize: 'Total',
-};
-const RENDER_OBJ_COUNT: NodeRendering = {
-  selfSize: 'Self objects',
-  totalSize: 'Subtree objects',
-};
-
-export interface FlamegraphSelectionParams {
-  readonly profileType: ProfileType;
-  readonly upids: number[];
-  readonly start: time;
-  readonly end: time;
-}
-
-interface FlamegraphDetailsPanelAttrs {
-  cache: LegacyFlamegraphCache;
-  selection: FlamegraphSelectionParams;
-}
-
-interface FlamegraphResult {
-  queryResults: ReadonlyArray<CallsiteInfo>;
-  incomplete: boolean;
-  renderResults?: ReadonlyArray<CallsiteInfo>;
-}
-
-interface FlamegraphState {
-  selection: FlamegraphSelectionParams;
-  viewingOption: FlamegraphViewingOption;
-  focusRegex: string;
-  result?: FlamegraphResult;
-  selectedCallsites: Readonly<{
-    [key: string]: CallsiteInfo | undefined;
-  }>;
-}
-
-export class LegacyFlamegraphDetailsPanel
-  implements m.ClassComponent<FlamegraphDetailsPanelAttrs>
-{
-  private undebouncedFocusRegex = '';
-  private updateFocusRegexDebounced = debounce(() => {
-    if (this.state === undefined) {
-      return;
-    }
-    this.state.focusRegex = this.undebouncedFocusRegex;
-    raf.scheduleFullRedraw();
-  }, 20);
-
-  private flamegraph: Flamegraph = new Flamegraph([]);
-  private queryLimiter = new AsyncLimiter();
-
-  private state?: FlamegraphState;
-  private queryMonitor = new Monitor([
-    () => this.state?.selection,
-    () => this.state?.focusRegex,
-    () => this.state?.viewingOption,
-  ]);
-  private selectedCallsitesMonitor = new Monitor([
-    () => this.state?.selection,
-    () => this.state?.focusRegex,
-  ]);
-  private renderResultMonitor = new Monitor([
-    () => this.state?.result?.queryResults,
-    () => this.state?.selectedCallsites,
-  ]);
-
-  view({attrs}: Vnode<FlamegraphDetailsPanelAttrs>) {
-    if (attrs.selection === undefined) {
-      this.state = undefined;
-    } else if (
-      attrs.selection.profileType !== this.state?.selection.profileType ||
-      attrs.selection.start !== this.state.selection.start ||
-      attrs.selection.end !== this.state.selection.end ||
-      !arrayEquals(attrs.selection.upids, this.state.selection.upids)
-    ) {
-      this.state = {
-        selection: attrs.selection,
-        focusRegex: '',
-        viewingOption: defaultViewingOption(attrs.selection.profileType),
-        selectedCallsites: {},
-      };
-    }
-    if (this.state === undefined) {
-      return m(
-        '.details-panel',
-        m('.details-panel-heading', m('h2', `Flamegraph Profile`)),
-      );
-    }
-
-    if (this.queryMonitor.ifStateChanged()) {
-      this.state.result = undefined;
-      const state = this.state;
-      this.queryLimiter.schedule(() => {
-        return LegacyFlamegraphDetailsPanel.fetchQueryResults(
-          assertExists(this.getCurrentEngine()),
-          attrs.cache,
-          state,
-        );
-      });
-    }
-
-    if (this.selectedCallsitesMonitor.ifStateChanged()) {
-      this.state.selectedCallsites = {};
-    }
-
-    if (
-      this.renderResultMonitor.ifStateChanged() &&
-      this.state.result !== undefined
-    ) {
-      const selected = this.state.selectedCallsites[this.state.viewingOption];
-      const expanded = expandCallsites(
-        this.state.result.queryResults,
-        selected?.id ?? -1,
-      );
-      this.state.result.renderResults = mergeCallsites(
-        expanded,
-        LegacyFlamegraphDetailsPanel.getMinSizeDisplayed(
-          expanded,
-          selected?.totalSize,
-        ),
-      );
-    }
-
-    let height: number | undefined;
-    if (this.state.result?.renderResults !== undefined) {
-      this.flamegraph.updateDataIfChanged(
-        this.nodeRendering(),
-        this.state.result.renderResults,
-        this.state.selectedCallsites[this.state.viewingOption],
-      );
-      height = this.flamegraph.getHeight() + HEADER_HEIGHT;
-    } else {
-      height = undefined;
-    }
-
-    return m(
-      '.flamegraph-profile',
-      this.maybeShowModal(),
-      m(
-        DetailsShell,
-        {
-          fillParent: true,
-          title: m(
-            'div.title',
-            this.getTitle(),
-            this.state.selection.profileType ===
-              ProfileType.MIXED_HEAP_PROFILE &&
-              m(
-                Popup,
-                {
-                  trigger: m(Icon, {icon: 'warning'}),
-                },
-                m(
-                  '',
-                  {style: {width: '300px'}},
-                  'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
-                ),
-              ),
-            ':',
-          ),
-          description: this.getViewingOptionButtons(),
-          buttons: [
-            m(
-              'div.selected',
-              `Selected function: ${toSelectedCallsite(
-                this.state.selectedCallsites[this.state.viewingOption],
-              )}`,
-            ),
-            m(
-              'div.time',
-              `Snapshot time: `,
-              m(DurationWidget, {
-                dur: this.state.selection.end - this.state.selection.start,
-              }),
-            ),
-            m('input[type=text][placeholder=Focus]', {
-              oninput: (e: Event) => {
-                const target = e.target as HTMLInputElement;
-                this.undebouncedFocusRegex = target.value;
-                this.updateFocusRegexDebounced();
-              },
-              // Required to stop hot-key handling:
-              onkeydown: (e: Event) => e.stopPropagation(),
-            }),
-            (this.state.selection.profileType ===
-              ProfileType.NATIVE_HEAP_PROFILE ||
-              this.state.selection.profileType ===
-                ProfileType.JAVA_HEAP_SAMPLES) &&
-              m(Button, {
-                icon: 'file_download',
-                intent: Intent.Primary,
-                onclick: () => {
-                  this.downloadPprof();
-                  raf.scheduleFullRedraw();
-                },
-              }),
-          ],
-        },
-        m(
-          '.flamegraph-content',
-          this.state.result === undefined
-            ? m(
-                '.loading-container',
-                m(
-                  EmptyState,
-                  {
-                    icon: 'bar_chart',
-                    title: 'Computing graph ...',
-                    className: 'flamegraph-loading',
-                  },
-                  m(Spinner, {easing: true}),
-                ),
-              )
-            : m(`canvas[ref=canvas]`, {
-                style: `height:${height}px; width:100%`,
-                onmousemove: (e: MouseEvent) => {
-                  const {offsetX, offsetY} = e;
-                  this.flamegraph.onMouseMove({x: offsetX, y: offsetY});
-                  raf.scheduleFullRedraw();
-                },
-                onmouseout: () => {
-                  this.flamegraph.onMouseOut();
-                  raf.scheduleFullRedraw();
-                },
-                onclick: (e: MouseEvent) => {
-                  if (
-                    this.state === undefined ||
-                    this.state.result === undefined
-                  ) {
-                    return;
-                  }
-                  const {offsetX, offsetY} = e;
-                  const cs = {...this.state.selectedCallsites};
-                  cs[this.state.viewingOption] = this.flamegraph.onMouseClick({
-                    x: offsetX,
-                    y: offsetY,
-                  });
-                  this.state.selectedCallsites = cs;
-                  raf.scheduleFullRedraw();
-                },
-              }),
-        ),
-      ),
-    );
-  }
-
-  private getTitle(): string {
-    const state = assertExists(this.state);
-    switch (state.selection.profileType) {
-      case ProfileType.MIXED_HEAP_PROFILE:
-        return 'Mixed heap profile';
-      case ProfileType.HEAP_PROFILE:
-        return 'Heap profile';
-      case ProfileType.NATIVE_HEAP_PROFILE:
-        return 'Native heap profile';
-      case ProfileType.JAVA_HEAP_SAMPLES:
-        return 'Java heap samples';
-      case ProfileType.JAVA_HEAP_GRAPH:
-        return 'Java heap graph';
-      case ProfileType.PERF_SAMPLE:
-        return 'Profile';
-      default:
-        throw new Error('unknown type');
-    }
-  }
-
-  private nodeRendering(): NodeRendering {
-    const state = assertExists(this.state);
-    const profileType = state.selection.profileType;
-    switch (profileType) {
-      case ProfileType.JAVA_HEAP_GRAPH:
-        if (
-          state.viewingOption ===
-            FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY ||
-          state.viewingOption ===
-            FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY
-        ) {
-          return RENDER_OBJ_COUNT;
-        } else {
-          return RENDER_SELF_AND_TOTAL;
-        }
-      case ProfileType.MIXED_HEAP_PROFILE:
-      case ProfileType.HEAP_PROFILE:
-      case ProfileType.NATIVE_HEAP_PROFILE:
-      case ProfileType.JAVA_HEAP_SAMPLES:
-      case ProfileType.PERF_SAMPLE:
-        return RENDER_SELF_AND_TOTAL;
-      default:
-        const exhaustiveCheck: never = profileType;
-        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-    }
-  }
-
-  private getViewingOptionButtons(): m.Children {
-    const ret = [];
-    const state = assertExists(this.state);
-    for (const {option, name} of viewingOptions(state.selection.profileType)) {
-      ret.push(
-        m(Button, {
-          label: name,
-          active: option === state.viewingOption,
-          onclick: () => {
-            const state = assertExists(this.state);
-            state.viewingOption = option;
-            raf.scheduleFullRedraw();
-          },
-        }),
-      );
-    }
-    return m(ButtonBar, ret);
-  }
-
-  onupdate({dom}: m.VnodeDOM<FlamegraphDetailsPanelAttrs>) {
-    const canvas = findRef(dom, 'canvas');
-    if (canvas === null || !(canvas instanceof HTMLCanvasElement)) {
-      return;
-    }
-    if (!this.state?.result?.renderResults) {
-      return;
-    }
-    canvas.width = canvas.offsetWidth * devicePixelRatio;
-    canvas.height = canvas.offsetHeight * devicePixelRatio;
-
-    const ctx = canvas.getContext('2d');
-    if (ctx === null) {
-      return;
-    }
-
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-    ctx.save();
-    ctx.scale(devicePixelRatio, devicePixelRatio);
-    const {offsetWidth: width, offsetHeight: height} = canvas;
-    const unit =
-      this.state.viewingOption ===
-        FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
-      this.state.viewingOption ===
-        FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ||
-      this.state.viewingOption ===
-        FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY
-        ? 'B'
-        : '';
-    this.flamegraph.draw(ctx, width, height, 0, 0, unit);
-    ctx.restore();
-  }
-
-  private static async fetchQueryResults(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    state: FlamegraphState,
-  ) {
-    const table = await LegacyFlamegraphDetailsPanel.prepareViewsAndTables(
-      engine,
-      cache,
-      state,
-    );
-    const queryResults =
-      await LegacyFlamegraphDetailsPanel.getFlamegraphDataFromTables(
-        engine,
-        table,
-        state.viewingOption,
-        state.focusRegex,
-      );
-
-    let incomplete = false;
-    if (state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH) {
-      const it = await engine.query(`
-        select value from stats
-        where severity = 'error' and name = 'heap_graph_non_finalized_graph'
-      `);
-      incomplete = it.firstRow({value: NUM}).value > 0;
-    }
-    state.result = {
-      queryResults,
-      incomplete,
-    };
-    raf.scheduleFullRedraw();
-  }
-
-  private static async prepareViewsAndTables(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    state: FlamegraphState,
-  ): Promise<string> {
-    const flamegraphType = getFlamegraphType(state.selection.profileType);
-    if (state.selection.profileType === ProfileType.PERF_SAMPLE) {
-      let upid: string;
-      let upidGroup: string;
-      if (state.selection.upids.length > 1) {
-        upid = `NULL`;
-        upidGroup = `'${this.serializeUpidGroup(state.selection.upids)}'`;
-      } else {
-        upid = `${state.selection.upids[0]}`;
-        upidGroup = `NULL`;
-      }
-      return cache.getTableName(
-        engine,
-        `
-          select
-            id,
-            name,
-            map_name,
-            parent_id,
-            depth,
-            cumulative_size,
-            cumulative_alloc_size,
-            cumulative_count,
-            cumulative_alloc_count,
-            size,
-            alloc_size,
-            count,
-            alloc_count,
-            source_file,
-            line_number
-          from experimental_flamegraph(
-            '${flamegraphType}',
-            NULL,
-            '>=${state.selection.start},<=${state.selection.end}',
-            ${upid},
-            ${upidGroup},
-            '${state.focusRegex}'
-          )
-        `,
-      );
-    }
-    if (
-      state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH &&
-      isHeapGraphDominatorTreeViewingOption(state.viewingOption)
-    ) {
-      assertTrue(state.selection.start == state.selection.end);
-      return cache.getTableName(
-        engine,
-        await this.loadHeapGraphDominatorTreeQuery(
-          engine,
-          cache,
-          state.selection.upids[0],
-          state.selection.start,
-        ),
-      );
-    }
-    assertTrue(state.selection.start == state.selection.end);
-    return cache.getTableName(
-      engine,
-      `
-        select
-          id,
-          name,
-          map_name,
-          parent_id,
-          depth,
-          cumulative_size,
-          cumulative_alloc_size,
-          cumulative_count,
-          cumulative_alloc_count,
-          size,
-          alloc_size,
-          count,
-          alloc_count,
-          source_file,
-          line_number
-        from experimental_flamegraph(
-          '${flamegraphType}',
-          ${state.selection.start},
-          NULL,
-          ${state.selection.upids[0]},
-          NULL,
-          '${state.focusRegex}'
-        )
-      `,
-    );
-  }
-
-  private static async loadHeapGraphDominatorTreeQuery(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    upid: number,
-    timestamp: time,
-  ) {
-    const outputTableName = `heap_graph_type_dominated_${upid}_${timestamp}`;
-    const outputQuery = `SELECT * FROM ${outputTableName}`;
-    if (cache.hasQuery(outputQuery)) {
-      return outputQuery;
-    }
-
-    await engine.query(`
-      INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;
-
-      -- heap graph dominator tree with objects as nodes and all relavant
-      -- object self stats and dominated stats
-      CREATE PERFETTO TABLE _heap_graph_object_dominated AS
-      SELECT
-      node.id,
-      node.idom_id,
-      node.dominated_obj_count,
-      node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size,
-      node.depth,
-      obj.type_id,
-      obj.root_type,
-      obj.self_size + obj.native_size AS self_size
-      FROM heap_graph_dominator_tree node
-      JOIN heap_graph_object obj USING(id)
-      WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp}
-      -- required to accelerate the recursive cte below
-      ORDER BY idom_id;
-
-      -- calculate for each object node in the dominator tree the
-      -- HASH(path of type_id's from the super root to the object)
-      CREATE PERFETTO TABLE _dominator_tree_path_hash AS
-      WITH RECURSIVE _tree_visitor(id, path_hash) AS (
-        SELECT
-          id,
-          HASH(
-            CAST(type_id AS TEXT) || '-' || IFNULL(root_type, '')
-          ) AS path_hash
-        FROM _heap_graph_object_dominated
-        WHERE depth = 1
-        UNION ALL
-        SELECT
-          child.id,
-          HASH(CAST(parent.path_hash AS TEXT) || '/' || CAST(type_id AS TEXT)) AS path_hash
-        FROM _heap_graph_object_dominated child
-        JOIN _tree_visitor parent ON child.idom_id = parent.id
-      )
-      SELECT * from _tree_visitor
-      ORDER BY id;
-
-      -- merge object nodes with the same path into one "class type node", so the
-      -- end result is a tree where nodes are identified by their types and the
-      -- dominator relationships are preserved.
-      CREATE PERFETTO TABLE ${outputTableName} AS
-      SELECT
-        map.path_hash as id,
-        COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
-          node.root_type IS NOT NULL,
-          ' [' || node.root_type || ']', ''
-        ) AS name,
-        IFNULL(parent_map.path_hash, -1) AS parent_id,
-        node.depth - 1 AS depth,
-        sum(dominated_size) AS cumulative_size,
-        -1 AS cumulative_alloc_size,
-        sum(dominated_obj_count) AS cumulative_count,
-        -1 AS cumulative_alloc_count,
-        '' as map_name,
-        '' as source_file,
-        -1 as line_number,
-        sum(self_size) AS size,
-        count(*) AS count
-      FROM _heap_graph_object_dominated node
-      JOIN _dominator_tree_path_hash map USING(id)
-      LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id
-      JOIN heap_graph_class cls ON node.type_id = cls.id
-      GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;
-
-      -- These are intermediates and not needed
-      DROP TABLE _heap_graph_object_dominated;
-      DROP TABLE _dominator_tree_path_hash;
-    `);
-
-    return outputQuery;
-  }
-
-  private static async getFlamegraphDataFromTables(
-    engine: Engine,
-    tableName: string,
-    viewingOption: FlamegraphViewingOption,
-    focusRegex: string,
-  ) {
-    let orderBy = '';
-    let totalColumnName:
-      | 'cumulativeSize'
-      | 'cumulativeAllocSize'
-      | 'cumulativeCount'
-      | 'cumulativeAllocCount' = 'cumulativeSize';
-    let selfColumnName: 'size' | 'count' = 'size';
-    // TODO(fmayer): Improve performance so this is no longer necessary.
-    // Alternatively consider collapsing frames of the same label.
-    const maxDepth = 100;
-    switch (viewingOption) {
-      case FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
-        orderBy = `where cumulative_alloc_size > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_alloc_size desc, name`;
-        totalColumnName = 'cumulativeAllocSize';
-        selfColumnName = 'size';
-        break;
-      case FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_count > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_count desc, name`;
-        totalColumnName = 'cumulativeCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY:
-        orderBy = `where cumulative_alloc_count > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_alloc_count desc, name`;
-        totalColumnName = 'cumulativeAllocCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.PERF_SAMPLES_KEY:
-      case FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_size > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_size desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
-      case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY:
-        orderBy = `where depth < ${maxDepth} order by depth,
-          cumulativeCount desc, name`;
-        totalColumnName = 'cumulativeCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY:
-        orderBy = `where depth < ${maxDepth} order by depth,
-          cumulativeSize desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
-      default:
-        const exhaustiveCheck: never = viewingOption;
-        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-        break;
-    }
-
-    const callsites = await engine.query(`
-      SELECT
-        id as hash,
-        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
-        IFNULL(parent_id, -1) as parentHash,
-        depth,
-        cumulative_size as cumulativeSize,
-        cumulative_alloc_size as cumulativeAllocSize,
-        cumulative_count as cumulativeCount,
-        cumulative_alloc_count as cumulativeAllocCount,
-        map_name as mapping,
-        size,
-        count,
-        IFNULL(source_file, '') as sourceFile,
-        IFNULL(line_number, -1) as lineNumber
-      from ${tableName}
-      ${orderBy}
-    `);
-
-    const flamegraphData: CallsiteInfo[] = [];
-    const hashToindex: Map<number, number> = new Map();
-    const it = callsites.iter({
-      hash: NUM,
-      name: STR,
-      parentHash: NUM,
-      depth: NUM,
-      cumulativeSize: NUM,
-      cumulativeAllocSize: NUM,
-      cumulativeCount: NUM,
-      cumulativeAllocCount: NUM,
-      mapping: STR,
-      sourceFile: STR,
-      lineNumber: NUM,
-      size: NUM,
-      count: NUM,
-    });
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      const hash = it.hash;
-      let name = it.name;
-      const parentHash = it.parentHash;
-      const depth = it.depth;
-      const totalSize = it[totalColumnName];
-      const selfSize = it[selfColumnName];
-      const mapping = it.mapping;
-      const highlighted =
-        focusRegex !== '' &&
-        name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
-      const parentId = hashToindex.has(+parentHash)
-        ? hashToindex.get(+parentHash)!
-        : -1;
-
-      let location: string | undefined;
-      if (/[a-zA-Z]/i.test(it.sourceFile)) {
-        location = it.sourceFile;
-        if (it.lineNumber !== -1) {
-          location += `:${it.lineNumber}`;
-        }
-      }
-
-      if (depth === maxDepth - 1) {
-        name += ' [tree truncated]';
-      }
-      // Instead of hash, we will store index of callsite in this original array
-      // as an id of callsite. That way, we have quicker access to parent and it
-      // will stay unique:
-      hashToindex.set(hash, i);
-
-      flamegraphData.push({
-        id: i,
-        totalSize,
-        depth,
-        parentId,
-        name,
-        selfSize,
-        mapping,
-        merged: false,
-        highlighted,
-        location,
-      });
-    }
-    return flamegraphData;
-  }
-
-  private async downloadPprof() {
-    if (this.state === undefined) {
-      return;
-    }
-    const engine = this.getCurrentEngine();
-    if (engine === undefined) {
-      return;
-    }
-    try {
-      assertTrue(
-        this.state.selection.upids.length === 1,
-        'Native profiles can only contain one pid.',
-      );
-      const pid = await engine.query(
-        `select pid from process where upid = ${this.state.selection.upids[0]}`,
-      );
-      const trace = await getCurrentTrace();
-      convertTraceToPprofAndDownload(
-        trace,
-        pid.firstRow({pid: NUM}).pid,
-        this.state.selection.start,
-      );
-    } catch (error) {
-      throw new Error(`Failed to get current trace ${error}`);
-    }
-  }
-
-  private maybeShowModal() {
-    const state = assertExists(this.state);
-    if (state.result?.incomplete === undefined || !state.result.incomplete) {
-      return undefined;
-    }
-    if (globals.state.flamegraphModalDismissed) {
-      return undefined;
-    }
-    return m(Modal, {
-      title: 'The flamegraph is incomplete',
-      vAlign: 'TOP',
-      content: m(
-        'div',
-        'The current trace does not have a fully formed flamegraph',
-      ),
-      buttons: [
-        {
-          text: 'Show the errors',
-          primary: true,
-          action: () => Router.navigate('#!/info'),
-        },
-        {
-          text: 'Skip',
-          action: () => {
-            globals.dispatch(Actions.dismissFlamegraphModal({}));
-            raf.scheduleFullRedraw();
-          },
-        },
-      ],
-    } as ModalAttrs);
-  }
-
-  private static getMinSizeDisplayed(
-    flamegraphData: ReadonlyArray<CallsiteInfo>,
-    rootSize?: number,
-  ): number {
-    // Note: This is a hack. Really we should obtain the size of the canvas and
-    // use that to determine the number of buckets to display, but this code is
-    // legacy and going away soon, and the calculation before was just plain
-    // wrong anyway so this isn't really any worse.
-    //
-    // 800 buckets is a decent placeholder until the new flamegraph code lands.
-    const bucketCount = 800;
-    if (rootSize === undefined) {
-      rootSize = findRootSize(flamegraphData);
-    }
-    return (MIN_PIXEL_DISPLAYED * rootSize) / bucketCount;
-  }
-
-  private static serializeUpidGroup(upids: number[]) {
-    return new Array(upids).join();
-  }
-
-  private getCurrentEngine() {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) return undefined;
-    return globals.engines.get(engineId);
-  }
-}
diff --git a/ui/src/frontend/legacy_flamegraph_unittest.ts b/ui/src/frontend/legacy_flamegraph_unittest.ts
deleted file mode 100644
index ddb006a..0000000
--- a/ui/src/frontend/legacy_flamegraph_unittest.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2019 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 {splitIfTooBig} from './legacy_flamegraph';
-
-test('textGoingToMultipleLines', () => {
-  const text = 'Dummy text to go to multiple lines.';
-
-  const lineSplitter = splitIfTooBig(text, 7 + 32, text.length);
-
-  expect(lineSplitter).toEqual({
-    lines: ['Dummy t', 'ext to ', 'go to m', 'ultiple', ' lines.'],
-    lineWidth: 7,
-  });
-});
-
-test('emptyText', () => {
-  const text = '';
-
-  const lineSplitter = splitIfTooBig(text, 10, 5);
-
-  expect(lineSplitter).toEqual({lines: [], lineWidth: 5});
-});
-
-test('textEnoughForOneLine', () => {
-  const text = 'Dummy text to go to one lines.';
-
-  const lineSplitter = splitIfTooBig(text, text.length + 32, text.length);
-
-  expect(lineSplitter).toEqual({lines: [text], lineWidth: text.length});
-});
-
-test('textGoingToTwoLines', () => {
-  const text = 'Dummy text to go to two lines.';
-
-  const lineSplitter = splitIfTooBig(text, text.length / 2 + 32, text.length);
-
-  expect(lineSplitter).toEqual({
-    lines: ['Dummy text to g', 'o to two lines.'],
-    lineWidth: text.length / 2,
-  });
-});
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index 784daa1..2b17b93 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -206,6 +206,8 @@
     ) {
       if (typeof value === 'bigint') {
         return m(DurationWidget, {dur: value});
+      } else if (typeof value === 'number') {
+        return m(DurationWidget, {dur: BigInt(Math.round(value))});
       }
     }
     return `${value}`;
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index 5451946..5572814 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -22,7 +22,6 @@
 import {getLegacySelection} from '../common/state';
 
 import {
-  CpuProfileDetails,
   Flow,
   globals,
   QuantizedLoad,
@@ -73,11 +72,6 @@
   raf.scheduleFullRedraw();
 }
 
-export function publishCpuProfileDetails(details: CpuProfileDetails) {
-  globals.cpuProfileDetails = details;
-  globals.publishRedraw();
-}
-
 export function publishHasFtrace(value: boolean): void {
   globals.hasFtrace = value;
   globals.publishRedraw();
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index ed20d99..e137437 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -38,7 +38,6 @@
 } from '../trace_processor/sql_utils/thread_state';
 import {DurationWidget, renderDuration} from './widgets/duration';
 import {Timestamp} from './widgets/timestamp';
-import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
 import {globals} from './globals';
 import {getProcessName} from '../trace_processor/sql_utils/process';
 import {
@@ -47,6 +46,10 @@
   getThreadName,
 } from '../trace_processor/sql_utils/thread';
 import {ThreadStateRef} from './widgets/thread_state';
+import {
+  CRITICAL_PATH_CMD,
+  CRITICAL_PATH_LITE_CMD,
+} from '../public/exposed_commands';
 
 interface ThreadStateTabConfig {
   // Id into |thread_state| sql table.
@@ -257,22 +260,9 @@
         name,
       });
 
-    const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
-    const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
-
-    const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
-    const sliceLiteColumnNames = [
-      'id',
-      'utid',
-      'ts',
-      'dur',
-      'thread_name',
-      'process_name',
-      'table_name',
-    ];
-
     const nameForNextOrPrev = (state: ThreadState) =>
       `${state.state} for ${renderDuration(state.dur)}`;
+
     return [
       m(
         Tree,
@@ -321,83 +311,28 @@
             ),
           ),
       ),
-      m(Button, {
-        label: 'Critical path lite',
-        intent: Intent.Primary,
-        onclick: () =>
-          this.engine
-            .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
-            .then(() =>
-              addDebugSliceTrack(
-                // NOTE(stevegolton): This is a temporary patch, this menu
-                // should become part of a critical path plugin, at which point
-                // we can just use the plugin's context object.
-                {
-                  engine: this.engine,
-                  registerTrack: (x) => globals.trackManager.registerTrack(x),
-                },
-                {
-                  sqlSource: `
-                    SELECT
-                      cr.id,
-                      cr.utid,
-                      cr.ts,
-                      cr.dur,
-                      thread.name AS thread_name,
-                      process.name AS process_name,
-                      'thread_state' AS table_name
-                    FROM
-                      _thread_executing_span_critical_path(
-                        ${this.state?.thread?.utid},
-                        trace_bounds.start_ts,
-                        trace_bounds.end_ts - trace_bounds.start_ts) cr,
-                      trace_bounds
-                    JOIN thread USING(utid)
-                    JOIN process USING(upid)
-                  `,
-                  columns: sliceLiteColumnNames,
-                },
-                `${this.state?.thread?.name}`,
-                sliceLiteColumns,
-                sliceLiteColumnNames,
-              ),
-            ),
-      }),
-      m(Button, {
-        label: 'Critical path',
-        intent: Intent.Primary,
-        onclick: () =>
-          this.engine
-            .query(
-              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            )
-            .then(() =>
-              addDebugSliceTrack(
-                // NOTE(stevegolton): This is a temporary patch, this menu
-                // should become part of a critical path plugin, at which point
-                // we can just use the plugin's context object.
-                {
-                  engine: this.engine,
-                  registerTrack: (x) => globals.trackManager.registerTrack(x),
-                },
-                {
-                  sqlSource: `
-                    SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
-                      FROM
-                        _thread_executing_span_critical_path_stack(
-                          ${this.state?.thread?.utid},
-                          trace_bounds.start_ts,
-                          trace_bounds.end_ts - trace_bounds.start_ts) cr,
-                        trace_bounds WHERE name IS NOT NULL
-                  `,
-                  columns: sliceColumnNames,
-                },
-                `${this.state?.thread?.name}`,
-                sliceColumns,
-                sliceColumnNames,
-              ),
-            ),
-      }),
+      globals.commandManager.hasCommand(CRITICAL_PATH_LITE_CMD) &&
+        m(Button, {
+          label: 'Critical path lite',
+          intent: Intent.Primary,
+          onclick: () => {
+            globals.commandManager.runCommand(
+              CRITICAL_PATH_LITE_CMD,
+              this.state?.thread?.utid,
+            );
+          },
+        }),
+      globals.commandManager.hasCommand(CRITICAL_PATH_CMD) &&
+        m(Button, {
+          label: 'Critical path',
+          intent: Intent.Primary,
+          onclick: () => {
+            globals.commandManager.runCommand(
+              CRITICAL_PATH_CMD,
+              this.state?.thread?.utid,
+            );
+          },
+        }),
     ];
   }
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 8b4d837..5d02c78 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -39,7 +39,7 @@
 import {PxSpan, TimeScale} from './time_scale';
 import {exists} from '../base/utils';
 import {classNames} from '../base/classnames';
-import {GroupNode} from './workspace';
+import {GroupNode} from '../public/workspace';
 import {raf} from '../core/raf_scheduler';
 import {Actions} from '../common/actions';
 
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 65adb48..e11cbbf 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -46,7 +46,7 @@
 import {calculateResolution} from '../common/resolution';
 import {featureFlags} from '../core/feature_flags';
 import {Tree, TreeNode} from '../widgets/tree';
-import {TrackNode} from './workspace';
+import {TrackNode} from '../public/workspace';
 
 export const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
   id: 'showTrackDetailsButton',
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/ui_main.ts
similarity index 77%
rename from ui/src/frontend/app.ts
rename to ui/src/frontend/ui_main.ts
index fbab675..7e62524 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -28,7 +28,7 @@
   TimestampFormat,
 } from '../core/timestamp_format';
 import {raf} from '../core/raf_scheduler';
-import {Command, Engine, addDebugSliceTrack} from '../public';
+import {Command} from '../public';
 import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
 import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
@@ -52,10 +52,7 @@
 } from './keyboard_event_handler';
 import {publishPermalinkHash} from './publish';
 import {OmniboxMode, PromptOption} from './omnibox_manager';
-import {Utid} from '../trace_processor/sql_utils/core_types';
-import {THREAD_STATE_TRACK_KIND} from '../core/track_kinds';
 import {DisposableStack} from '../base/disposable_stack';
-import {getThreadInfo} from '../trace_processor/sql_utils/thread';
 
 function renderPermalink(): m.Children {
   const hash = globals.permalinkHash;
@@ -81,36 +78,7 @@
   }
 }
 
-const criticalPathSliceColumns = {
-  ts: 'ts',
-  dur: 'dur',
-  name: 'name',
-};
-const criticalPathsliceColumnNames = [
-  'id',
-  'utid',
-  'ts',
-  'dur',
-  'name',
-  'table_name',
-];
-
-const criticalPathsliceLiteColumns = {
-  ts: 'ts',
-  dur: 'dur',
-  name: 'thread_name',
-};
-const criticalPathsliceLiteColumnNames = [
-  'id',
-  'utid',
-  'ts',
-  'dur',
-  'thread_name',
-  'process_name',
-  'table_name',
-];
-
-export class App implements m.ClassComponent {
+export class UiMain implements m.ClassComponent {
   private trash = new DisposableStack();
   static readonly OMNIBOX_INPUT_REF = 'omnibox';
   private omniboxInputEl?: HTMLInputElement;
@@ -121,33 +89,6 @@
     this.trash.use(new AggregationsTabs());
   }
 
-  private getEngine(): Engine | undefined {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) {
-      return undefined;
-    }
-    const engine = globals.engines.get(engineId)?.getProxy('QueryPage');
-    return engine;
-  }
-
-  private getFirstUtidOfSelectionOrVisibleWindow(): number {
-    const selection = globals.state.selection;
-    if (selection.kind === 'area') {
-      for (const trackUri of selection.trackUris) {
-        const trackDesc = globals.trackManager.getTrack(trackUri);
-
-        if (
-          trackDesc?.tags?.kind === THREAD_STATE_TRACK_KIND &&
-          trackDesc?.tags?.utid !== undefined
-        ) {
-          return trackDesc.tags.utid;
-        }
-      }
-    }
-
-    return 0;
-  }
-
   private cmds: Command[] = [
     {
       id: 'perfetto.SetTimestampFormat',
@@ -201,116 +142,6 @@
       },
     },
     {
-      id: 'perfetto.CriticalPathLite',
-      name: `Critical path lite`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          await engine.query(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-          );
-          await addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of a critical path plugin, at which point we can just
-            // use the plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
-              sqlSource: `
-                   SELECT
-                      cr.id,
-                      cr.utid,
-                      cr.ts,
-                      cr.dur,
-                      thread.name AS thread_name,
-                      process.name AS process_name,
-                      'thread_state' AS table_name
-                    FROM
-                      _thread_executing_span_critical_path(
-                          ${trackUtid},
-                          ${window.start},
-                          ${window.end} - ${window.start}) cr
-                    JOIN thread USING(utid)
-                    JOIN process USING(upid)
-                  `,
-              columns: criticalPathsliceLiteColumnNames,
-            },
-            (await getThreadInfo(engine, trackUtid as Utid)).name ??
-              '<thread name>',
-            criticalPathsliceLiteColumns,
-            criticalPathsliceLiteColumnNames,
-          );
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPath',
-      name: `Critical path`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          await engine.query(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-          );
-          await addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of a critical path plugin, at which point we can just
-            // use the plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
-              sqlSource: `
-                        SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
-                        FROM
-                        _critical_path_stack(
-                          ${trackUtid},
-                          ${window.start},
-                          ${window.end} - ${window.start}, 1, 1, 1, 1) cr WHERE name IS NOT NULL
-                  `,
-              columns: criticalPathsliceColumnNames,
-            },
-            (await getThreadInfo(engine, trackUtid as Utid)).name ??
-              '<thread name>',
-            criticalPathSliceColumns,
-            criticalPathsliceColumnNames,
-          );
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPathPprof',
-      name: `Critical path pprof`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          addQueryResultsTab({
-            query: `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
-                   SELECT *
-                      FROM
-                        _thread_executing_span_critical_path_graph(
-                        "criical_path",
-                         ${trackUtid},
-                         ${window.start},
-                         ${window.end} - ${window.start}) cr`,
-            title: 'Critical path',
-          });
-        }
-      },
-    },
-    {
       id: 'perfetto.TogglePerformanceMetrics',
       name: 'Toggle performance metrics',
       callback: () => {
@@ -557,7 +388,7 @@
     return m(Omnibox, {
       value: globals.omnibox.text,
       placeholder: prompt.text,
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       extraClasses: 'prompt-mode',
       closeOnOutsideClick: true,
       options,
@@ -617,7 +448,7 @@
     return m(Omnibox, {
       value: globals.omnibox.text,
       placeholder: 'Filter commands...',
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       extraClasses: 'command-mode',
       options,
       closeOnSubmit: true,
@@ -661,7 +492,7 @@
     return m(Omnibox, {
       value: globals.omnibox.text,
       placeholder: ph,
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       extraClasses: 'query-mode',
 
       onInput: (value) => {
@@ -698,7 +529,7 @@
     return m(Omnibox, {
       value: globals.state.omniboxState.omnibox,
       placeholder: "Search or type '>' for commands or ':' for SQL mode",
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       onInput: (value, prev) => {
         if (prev === '') {
           if (value === '>') {
@@ -817,7 +648,7 @@
   }
 
   private updateOmniboxInputRef(dom: Element): void {
-    const el = findRef(dom, App.OMNIBOX_INPUT_REF);
+    const el = findRef(dom, UiMain.OMNIBOX_INPUT_REF);
     if (el && el instanceof HTMLInputElement) {
       this.omniboxInputEl = el;
     }
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index cd842d9..40d851e 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -42,7 +42,7 @@
 import {TrackPanel, getTitleFontSize} from './track_panel';
 import {assertExists} from '../base/logging';
 import {PxSpan, TimeScale} from './time_scale';
-import {GroupNode, Node, TrackNode} from './workspace';
+import {GroupNode, Node, TrackNode} from '../public/workspace';
 import {fuzzyMatch, FuzzySegment} from '../base/fuzzy';
 
 import {exists, Optional} from '../base/utils';
diff --git a/ui/src/frontend/visualized_args_tracks.ts b/ui/src/frontend/visualized_args_tracks.ts
index 6c22f50..eb7c3bc 100644
--- a/ui/src/frontend/visualized_args_tracks.ts
+++ b/ui/src/frontend/visualized_args_tracks.ts
@@ -19,7 +19,7 @@
 import {NUM} from '../trace_processor/query_result';
 import {globals} from './globals';
 import {VisualisedArgsTrack} from './visualized_args_track';
-import {TrackNode} from './workspace';
+import {TrackNode} from '../public/workspace';
 
 const VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX = 'perfetto.VisualisedArgs';
 
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
index d01b349..bdafb8a 100644
--- a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {Optional} from '../../base/utils';
-import {GroupNode, TrackNode} from '../../frontend/workspace';
+import {GroupNode, TrackNode} from '../../public/workspace';
 import {
   PerfettoPlugin,
   PluginContext,
diff --git a/ui/src/public/exposed_commands.ts b/ui/src/public/exposed_commands.ts
new file mode 100644
index 0000000..d75275e
--- /dev/null
+++ b/ui/src/public/exposed_commands.ts
@@ -0,0 +1,26 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file contains constants for some command IDs that are used directly
+// from frontend code (e.g. the details panel that has buttons for critical
+// path). They exist to deal with all cases where some feature cannot be done
+// just with the existing API (e.g. the command palette), and a more direct
+// coupling between frontend and commands is necessary.
+// Adding entries to this file usually is the symptom of a missing API in the
+// plugin surface (e.g. the ability to customize context menus).
+// These constants exist just to make the dependency evident at code
+// search time, rather than copy-pasting the string in two places.
+
+export const CRITICAL_PATH_CMD = 'perfetto.CriticalPath';
+export const CRITICAL_PATH_LITE_CMD = 'perfetto.CriticalPathLite';
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index ea13f72..63eeb30 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -22,7 +22,7 @@
 import {PromptOption} from '../frontend/omnibox_manager';
 import {LegacyDetailsPanel, TrackDescriptor} from './tracks';
 import {TraceContext} from '../frontend/trace_context';
-import {Workspace} from '../frontend/workspace';
+import {Workspace} from './workspace';
 
 export {Engine} from '../trace_processor/engine';
 export {
diff --git a/ui/src/frontend/workspace.ts b/ui/src/public/workspace.ts
similarity index 100%
rename from ui/src/frontend/workspace.ts
rename to ui/src/public/workspace.ts
diff --git a/ui/src/frontend/workspace_unittest.ts b/ui/src/public/workspace_unittest.ts
similarity index 100%
rename from ui/src/frontend/workspace_unittest.ts
rename to ui/src/public/workspace_unittest.ts
diff --git a/ui/src/trace_processor/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index af573d0..27c1c8b 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -49,7 +49,7 @@
       this.websocket.onclose = (e) => this.onWebsocketClosed(e);
       this.websocket.onerror = (e) =>
         this.errorHandler(
-          `WebSocket error (state=${(e.target as WebSocket)?.readyState})`,
+          `WebSocket error rs=${(e.target as WebSocket)?.readyState} (ERR:ws)`,
         );
     }
 
@@ -78,7 +78,7 @@
       this.connected = false;
       this.rpcSendRequestBytes(new Uint8Array()); // Triggers a reconnection.
     } else {
-      this.errorHandler(`Websocket closed (${e.code}: ${e.reason})`);
+      this.errorHandler(`Websocket closed (${e.code}: ${e.reason}) (ERR:ws)`);
     }
   }