Merge "Revert "Add new fields with fixed64 type for flow ids in TrackEvent.""
diff --git a/.gitignore b/.gitignore
index bb3f624..3bd8337 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@
 perf.data*
 TAGS
 /*.pftrace
+examples/sdk/build/
diff --git a/Android.bp b/Android.bp
index 30b7d3a..c45934a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1936,6 +1936,7 @@
         ":perfetto_src_trace_processor_util_interned_message_view",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_views_views",
         ":perfetto_src_traced_probes_android_game_intervention_list_android_game_intervention_list",
@@ -9045,8 +9046,11 @@
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_performance_mark_hashes.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_processes.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_tasks.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_tasks_delaying_input_processing.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_thread_slice.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_unsymbolized_args.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql",
@@ -9349,6 +9353,14 @@
     ],
 }
 
+// GN: //src/trace_processor/util:stack_traces_util
+filegroup {
+    name: "perfetto_src_trace_processor_util_stack_traces_util",
+    srcs: [
+        "src/trace_processor/util/stack_traces_util.cc",
+    ],
+}
+
 // GN: //src/trace_processor/util:unittests
 filegroup {
     name: "perfetto_src_trace_processor_util_unittests",
@@ -10650,6 +10662,7 @@
         ":perfetto_src_trace_processor_util_interned_message_view",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_unittests",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_views_unittests",
@@ -10978,6 +10991,7 @@
         ":perfetto_src_trace_processor_util_interned_message_view",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_views_views",
         "src/trace_processor/trace_processor_shell.cc",
@@ -11149,6 +11163,7 @@
         ":perfetto_src_trace_processor_util_interned_message_view",
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
+        ":perfetto_src_trace_processor_util_stack_traces_util",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_views_views",
         ":perfetto_src_traceconv_lib",
diff --git a/BUILD b/BUILD
index c9bce0b..591153d 100644
--- a/BUILD
+++ b/BUILD
@@ -1261,8 +1261,11 @@
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_performance_mark_hashes.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_processes.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_tasks.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_tasks_delaying_input_processing.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_thread_slice.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_unsymbolized_args.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_user_event_hashes.sql",
@@ -1494,6 +1497,15 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:stack_traces_util
+perfetto_filegroup(
+    name = "src_trace_processor_util_stack_traces_util",
+    srcs = [
+        "src/trace_processor/util/stack_traces_util.cc",
+        "src/trace_processor/util/stack_traces_util.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:util
 perfetto_filegroup(
     name = "src_trace_processor_util_util",
@@ -4042,6 +4054,7 @@
         ":src_trace_processor_util_interned_message_view",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_util",
         ":src_trace_processor_views_views",
     ],
@@ -4157,6 +4170,7 @@
         ":src_trace_processor_util_interned_message_view",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_util",
         ":src_trace_processor_views_views",
         "src/trace_processor/trace_processor_shell.cc",
@@ -4228,6 +4242,7 @@
         ":src_profiling_deobfuscator",
         ":src_profiling_symbolizer_symbolize_database",
         ":src_profiling_symbolizer_symbolizer",
+        ":src_trace_processor_util_stack_traces_util",
         ":src_traceconv_pprofbuilder",
         ":src_traceconv_utils",
     ],
@@ -4327,6 +4342,7 @@
         ":src_trace_processor_util_interned_message_view",
         ":src_trace_processor_util_proto_to_args_parser",
         ":src_trace_processor_util_protozero_to_text",
+        ":src_trace_processor_util_stack_traces_util",
         ":src_trace_processor_util_util",
         ":src_trace_processor_views_views",
         ":src_traceconv_lib",
diff --git a/CHANGELOG b/CHANGELOG
index 052d8be..15cac4e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,8 +1,10 @@
 Unreleased:
   Tracing service and probes:
     * Add android.statsd datasource.
+    * Removed log spam about sys.trace.traced_started in standalone builds.
   Trace Processor:
-    *
+    * Deprecate calling NotifyEndOfFile more than once: Flush should instead be
+      used for all but the final call.
   UI:
     *
   SDK:
diff --git a/examples/sdk/BUILD.gn b/examples/sdk/BUILD.gn
index 8a49c88..219d642 100644
--- a/examples/sdk/BUILD.gn
+++ b/examples/sdk/BUILD.gn
@@ -38,3 +38,14 @@
     "../../src/base",
   ]
 }
+
+executable("example_startup_trace") {
+  sources = [ "example_startup_trace.cc" ]
+  defines = [ "PERFETTO_SDK_EXAMPLE_USE_INTERNAL_HEADERS" ]
+  testonly = true
+  deps = [
+    "../..:libperfetto_client_experimental",
+    "../../gn:default_deps",
+    "../../src/base",
+  ]
+}
diff --git a/examples/sdk/CMakeLists.txt b/examples/sdk/CMakeLists.txt
index a2f74bd..ed15e93 100644
--- a/examples/sdk/CMakeLists.txt
+++ b/examples/sdk/CMakeLists.txt
@@ -31,6 +31,7 @@
                trace_categories.cc)
 add_executable(example_custom_data_source example_custom_data_source.cc)
 add_executable(example_console example_console.cc trace_categories.cc)
+add_executable(example_startup_trace example_startup_trace.cc)
 
 target_link_libraries(example perfetto
                       ${CMAKE_THREAD_LIBS_INIT})
@@ -40,6 +41,8 @@
                       ${CMAKE_THREAD_LIBS_INIT})
 target_link_libraries(example_console perfetto
                       ${CMAKE_THREAD_LIBS_INIT})
+target_link_libraries(example_startup_trace perfetto
+                      ${CMAKE_THREAD_LIBS_INIT})
 
 # On Android we also need the logging library.
 if (ANDROID)
@@ -47,6 +50,7 @@
   target_link_libraries(example_system_wide log)
   target_link_libraries(example_custom_data_source log)
   target_link_libraries(example_console log)
+  target_link_libraries(example_startup_trace log)
 endif (ANDROID)
 
 if (WIN32)
@@ -60,6 +64,7 @@
   target_link_libraries(example_system_wide ws2_32)
   target_link_libraries(example_custom_data_source ws2_32)
   target_link_libraries(example_console ws2_32)
+  target_link_libraries(example_startup_trace ws2_32)
 endif (WIN32)
 
 # Enable standards-compliant mode when using the Visual Studio compiler.
@@ -68,4 +73,5 @@
   target_compile_options(example_system_wide PRIVATE "/permissive-")
   target_compile_options(example_custom_data_source PRIVATE "/permissive-")
   target_compile_options(example_console PRIVATE "/permissive-")
-endif (MSVC)
\ No newline at end of file
+  target_compile_options(example_startup_trace PRIVATE "/permissive-")
+endif (MSVC)
diff --git a/examples/sdk/README.md b/examples/sdk/README.md
index 393217f..d51a171 100644
--- a/examples/sdk/README.md
+++ b/examples/sdk/README.md
@@ -26,6 +26,11 @@
 cmake --build build
 ```
 
+Note: If amalgamated source files are not present, generate them using
+`cd perfetto ; tools/gen_amalgamated --output sdk/perfetto`.
+[Learn more](https://perfetto.dev/docs/contributing/sdk-releasing#building-and-tagging-the-release)
+at the release section.
+
 ## Track event example
 
 The [basic example](example.cc) shows how to instrument an app with track
diff --git a/examples/sdk/example_startup_trace.cc b/examples/sdk/example_startup_trace.cc
new file mode 100644
index 0000000..3a3afe3
--- /dev/null
+++ b/examples/sdk/example_startup_trace.cc
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// This example demonstrates startup tracing with a custom data source.
+// Startup tracing can work only with kSystemBackend. Before running
+// this example, `traced` must already be running in a separate process.
+
+// Run system tracing: ninja -C out/default/ traced && ./out/default/traced
+// And then run this example: ninja -C out/default example_startup_trace &&
+//                            ./out/default/example_startup_trace
+
+#if defined(PERFETTO_SDK_EXAMPLE_USE_INTERNAL_HEADERS)
+#include "perfetto/tracing.h"
+#include "perfetto/tracing/core/data_source_descriptor.h"
+#include "perfetto/tracing/core/trace_config.h"
+#include "perfetto/tracing/data_source.h"
+#include "perfetto/tracing/tracing.h"
+#include "protos/perfetto/trace/test_event.pbzero.h"
+#else
+#include <perfetto.h>
+#endif
+
+#include <unistd.h>
+#include <fstream>
+#include <iostream>
+#include <thread>
+
+namespace {
+
+class EventObserver {
+ public:
+  // Wait until OnEvent is ever called.
+  // Returns immediately if OnEvent was already called.
+  void Wait() {
+    std::unique_lock<std::mutex> lock(mutex);
+    cv.wait(lock, [&]() { return event_occurred_; });
+  }
+  void OnEvent() {
+    {
+      std::unique_lock<std::mutex> lock(mutex);
+      event_occurred_ = true;
+    }
+    cv.notify_all();
+  }
+  EventObserver() = default;
+
+ private:
+  EventObserver(const EventObserver&) = delete;
+  EventObserver& operator=(const EventObserver&) = delete;
+
+  std::mutex mutex;
+  std::condition_variable cv;
+  bool event_occurred_ = false;
+};
+
+// The definition of our custom data source. Instances of this class will be
+// automatically created and destroyed by Perfetto.
+class CustomDataSource : public perfetto::DataSource<CustomDataSource> {
+ public:
+  CustomDataSource(EventObserver* event_observer)
+      : event_observer_(event_observer) {}
+  void OnStart(const StartArgs&) override { event_observer_->OnEvent(); }
+
+ private:
+  EventObserver* event_observer_;
+};
+
+void InitializePerfetto(EventObserver* event_observer) {
+  perfetto::TracingInitArgs args;
+  // The backends determine where trace events are recorded. For this example we
+  // are going to use the system-wide tracing service, because the in-process
+  // backend doesn't support startup tracing.
+  args.backends = perfetto::kSystemBackend;
+  perfetto::Tracing::Initialize(args);
+
+  // Register our custom data source. Only the name is required, but other
+  // properties can be advertised too.
+  perfetto::DataSourceDescriptor dsd;
+  dsd.set_name("com.example.startup_trace");
+  CustomDataSource::Register(dsd, event_observer);
+}
+
+// The trace config defines which types of data sources are enabled for
+// recording.
+perfetto::TraceConfig GetTraceConfig() {
+  perfetto::TraceConfig cfg;
+  cfg.add_buffers()->set_size_kb(1024);
+  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
+  ds_cfg->set_name("com.example.startup_trace");
+  return cfg;
+}
+
+void StartStartupTracing() {
+  perfetto::Tracing::SetupStartupTracingOpts args;
+  args.backend = perfetto::kSystemBackend;
+  perfetto::Tracing::SetupStartupTracing(GetTraceConfig(), args);
+}
+
+std::unique_ptr<perfetto::TracingSession> StartTracing() {
+  auto tracing_session = perfetto::Tracing::NewTrace();
+  tracing_session->Setup(GetTraceConfig());
+  tracing_session->StartBlocking();
+  return tracing_session;
+}
+
+void StopTracing(std::unique_ptr<perfetto::TracingSession> tracing_session) {
+  // Flush to make sure the last written event ends up in the trace.
+  CustomDataSource::Trace(
+      [](CustomDataSource::TraceContext ctx) { ctx.Flush(); });
+
+  // Stop tracing and read the trace data.
+  tracing_session->StopBlocking();
+  std::vector<char> trace_data(tracing_session->ReadTraceBlocking());
+
+  // Write the result into a file.
+  // Note: To save memory with longer traces, you can tell Perfetto to write
+  // directly into a file by passing a file descriptor into Setup() above.
+  std::ofstream output;
+  const char* filename = "example_startup_trace.pftrace";
+  output.open(filename, std::ios::out | std::ios::binary);
+  output.write(&trace_data[0], static_cast<std::streamsize>(trace_data.size()));
+  output.close();
+  PERFETTO_LOG(
+      "Trace written in %s file. To read this trace in "
+      "text form, run `./tools/traceconv text %s`",
+      filename, filename);
+}
+
+}  // namespace
+
+PERFETTO_DECLARE_DATA_SOURCE_STATIC_MEMBERS(CustomDataSource);
+PERFETTO_DEFINE_DATA_SOURCE_STATIC_MEMBERS(CustomDataSource);
+
+int main(int, const char**) {
+  EventObserver event_observer;
+  InitializePerfetto(&event_observer);
+
+  StartStartupTracing();
+  // TODO(mohitms): Once we support `SetupStartupTracingBlocking`,
+  // it won't be required.
+  event_observer.Wait();
+
+  // Write an event using our custom data source before starting tracing
+  // session.
+  CustomDataSource::Trace([](CustomDataSource::TraceContext ctx) {
+    auto packet = ctx.NewTracePacket();
+    packet->set_timestamp(41);
+    packet->set_for_testing()->set_str("Startup Event");
+  });
+
+  auto tracing_session = StartTracing();
+
+  // Write an event using our custom data source.
+  CustomDataSource::Trace([](CustomDataSource::TraceContext ctx) {
+    auto packet = ctx.NewTracePacket();
+    packet->set_timestamp(42);
+    packet->set_for_testing()->set_str("Main Event");
+  });
+  StopTracing(std::move(tracing_session));
+
+  return 0;
+}
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index fbaf9f9..aa15ff1 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -672,6 +672,7 @@
     TASK_TYPE_INTERNAL_INPUT_BLOCKING = 77;
     TASK_TYPE_WEB_GPU = 78;
     TASK_TYPE_INTERNAL_POST_MESSAGE_FORWARDING = 79;
+    TASK_TYPE_INTERNAL_NAVIGATION_CANCELLATION = 80;
   }
 
   enum FrameType {
diff --git a/src/base/utils.cc b/src/base/utils.cc
index 1aa1af5..cb24d25 100644
--- a/src/base/utils.cc
+++ b/src/base/utils.cc
@@ -21,6 +21,7 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/pipe.h"
 #include "perfetto/ext/base/string_utils.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
@@ -190,6 +191,7 @@
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
     PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
     PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
+  Pipe pipe = Pipe::Create(Pipe::kBothBlock);
   pid_t pid;
   switch (pid = fork()) {
     case -1:
@@ -205,12 +207,24 @@
       // Do not accidentally close stdin/stdout/stderr.
       if (*null <= 2)
         null.release();
+      WriteAll(*pipe.wr, "1", 1);
       break;
     }
-    default:
+    default: {
+      // Wait for the child process to have reached the setsid() call. This is
+      // to avoid that 'adb shell perfetto -D' destroys the terminal (hence
+      // sending a SIGHUP to the child) before the child has detached from the
+      // terminal (see b/238644870).
+
+      // This is to unblock the read() below (with EOF, which will fail the
+      // CHECK) in the unlikely case of the child crashing before WriteAll("1").
+      pipe.wr.reset();
+      char one = '\0';
+      PERFETTO_CHECK(Read(*pipe.rd, &one, sizeof(one)) == 1 && one == '1');
       printf("%d\n", pid);
       int err = parent_cb();
       exit(err);
+    }
   }
 #else
   // Avoid -Wunreachable warnings.
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index 85fd6ac..951cb4b 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -1086,6 +1086,10 @@
     desc.set_name(MetatraceWriter::kDataSourceName);
     endpoint_->RegisterDataSource(desc);
   }
+  // Used by tracebox to synchronize with traced_probes being registered.
+  if (all_data_sources_registered_cb_) {
+    endpoint_->Sync(all_data_sources_registered_cb_);
+  }
 }
 
 void PerfProducer::OnDisconnect() {
diff --git a/src/profiling/perf/perf_producer.h b/src/profiling/perf/perf_producer.h
index da87ade..d9c6a18 100644
--- a/src/profiling/perf/perf_producer.h
+++ b/src/profiling/perf/perf_producer.h
@@ -96,6 +96,11 @@
                                      ParsedSample sample) override;
   void PostFinishDataSourceStop(DataSourceInstanceID ds_id) override;
 
+  // Calls `cb` when all data sources have been registered.
+  void SetAllDataSourcesRegisteredCb(std::function<void()> cb) {
+    all_data_sources_registered_cb_ = cb;
+  }
+
  private:
   // State of the producer's connection to tracing service (traced).
   enum State {
@@ -261,6 +266,8 @@
   // best effort - can be null if tracefs isn't accessible.
   std::unique_ptr<FtraceProcfs> tracefs_;
 
+  std::function<void()> all_data_sources_registered_cb_;
+
   base::WeakPtrFactory<PerfProducer> weak_factory_;  // keep last
 };
 
diff --git a/src/profiling/perf/traced_perf.cc b/src/profiling/perf/traced_perf.cc
index 77ce7c4..af2dc1a 100644
--- a/src/profiling/perf/traced_perf.cc
+++ b/src/profiling/perf/traced_perf.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/profiling/perf/traced_perf.h"
+#include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/unix_task_runner.h"
 #include "perfetto/ext/tracing/ipc/default_socket.h"
 #include "src/profiling/perf/perf_producer.h"
@@ -52,6 +53,14 @@
 #endif
 
   profiling::PerfProducer producer(&proc_fd_getter, &task_runner);
+  const char* env_notif = getenv("TRACED_PERF_NOTIFY_FD");
+  if (env_notif) {
+    int notif_fd = atoi(env_notif);
+    producer.SetAllDataSourcesRegisteredCb([notif_fd] {
+      PERFETTO_CHECK(base::WriteAll(notif_fd, "1", 1) == 1);
+      PERFETTO_CHECK(base::CloseFile(notif_fd) == 0);
+    });
+  }
   producer.ConnectWithRetries(GetProducerSocket());
   task_runner.Run();
   return 0;
diff --git a/src/profiling/symbolizer/BUILD.gn b/src/profiling/symbolizer/BUILD.gn
index 70dc091..f5a1af7 100644
--- a/src/profiling/symbolizer/BUILD.gn
+++ b/src/profiling/symbolizer/BUILD.gn
@@ -48,6 +48,7 @@
     "../../../include/perfetto/trace_processor:trace_processor",
     "../../../protos/perfetto/trace:zero",
     "../../../protos/perfetto/trace/profiling:zero",
+    "../../trace_processor/util:stack_traces_util",
   ]
   sources = [
     "symbolize_database.cc",
diff --git a/src/profiling/symbolizer/symbolize_database.cc b/src/profiling/symbolizer/symbolize_database.cc
index c6fca90..b7d7cfb 100644
--- a/src/profiling/symbolizer/symbolize_database.cc
+++ b/src/profiling/symbolizer/symbolize_database.cc
@@ -29,6 +29,8 @@
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
+#include "src/trace_processor/util/stack_traces_util.h"
+
 namespace perfetto {
 namespace profiling {
 
@@ -89,10 +91,14 @@
     int64_t load_bias = it.Get(3).AsLong();
     PERFETTO_CHECK(load_bias >= 0);
     std::string build_id;
-    if (convert_build_id_to_bytes) {
-      build_id = FromHex(it.Get(1).AsString());
+    // TODO(b/148109467): Remove workaround once all active Chrome versions
+    // write raw bytes instead of a string as build_id.
+    std::string raw_build_id = it.Get(1).AsString();
+    if (convert_build_id_to_bytes &&
+        !trace_processor::util::IsHexModuleId(base::StringView(raw_build_id))) {
+      build_id = FromHex(raw_build_id);
     } else {
-      build_id = it.Get(1).AsString();
+      build_id = raw_build_id;
     }
     UnsymbolizedMapping unsymbolized_mapping{it.Get(0).AsString(), build_id,
                                              static_cast<uint64_t>(load_bias)};
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 6d0bfc4..253ea87 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -185,6 +185,7 @@
     "util:gzip",
     "util:interned_message_view",
     "util:proto_to_args_parser",
+    "util:stack_traces_util",
     "views",
   ]
   public_deps = [
diff --git a/src/trace_processor/export_json_unittest.cc b/src/trace_processor/export_json_unittest.cc
index 961ecfc..67c5927 100644
--- a/src/trace_processor/export_json_unittest.cc
+++ b/src/trace_processor/export_json_unittest.cc
@@ -70,12 +70,14 @@
 class ExportJsonTest : public ::testing::Test {
  public:
   ExportJsonTest() {
-    context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
+    context_.storage.reset(new TraceStorage());
+    context_.global_args_tracker.reset(
+        new GlobalArgsTracker(context_.storage.get()));
     context_.args_tracker.reset(new ArgsTracker(&context_));
     context_.event_tracker.reset(new EventTracker(&context_));
-    context_.storage.reset(new TraceStorage());
     context_.track_tracker.reset(new TrackTracker(&context_));
-    context_.metadata_tracker.reset(new MetadataTracker(&context_));
+    context_.metadata_tracker.reset(
+        new MetadataTracker(context_.storage.get()));
     context_.process_tracker.reset(new ProcessTracker(&context_));
   }
 
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 04f720a..829589b 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -176,8 +176,10 @@
     return kSystraceTraceType;
 
   // Systrace with leading HTML.
-  if (base::StartsWith(start, "<!DOCTYPE html>") ||
-      base::StartsWith(start, "<html>"))
+  // Both: <!DOCTYPE html> and <!DOCTYPE HTML> have been observed.
+  std::string lower_start = base::ToLower(start);
+  if (base::StartsWith(lower_start, "<!doctype html>") ||
+      base::StartsWith(lower_start, "<html>"))
     return kSystraceTraceType;
 
   // Traces obtained from atrace -z (compress).
@@ -190,7 +192,7 @@
   if (base::Contains(start, "TRACE:\n"))
     return kSystraceTraceType;
 
-  // Ninja's buils log (.ninja_log).
+  // Ninja's build log (.ninja_log).
   if (base::StartsWith(start, "# ninja log"))
     return kNinjaLogTraceType;
 
diff --git a/src/trace_processor/forwarding_trace_parser_unittest.cc b/src/trace_processor/forwarding_trace_parser_unittest.cc
index 059caa0..74cb631 100644
--- a/src/trace_processor/forwarding_trace_parser_unittest.cc
+++ b/src/trace_processor/forwarding_trace_parser_unittest.cc
@@ -48,6 +48,36 @@
   EXPECT_EQ(kJsonTraceType, GuessTraceType(prefix, sizeof(prefix)));
 }
 
+TEST(TraceProcessorImplTest, GuessTraceType_DoctypeHtmlUppercase) {
+  const uint8_t prefix[] = "<!DOCTYPE HTML>";
+  EXPECT_EQ(kSystraceTraceType, GuessTraceType(prefix, sizeof(prefix)));
+}
+
+TEST(TraceProcessorImplTest, GuessTraceType_DoctypeHtml) {
+  const uint8_t prefix[] = "<!doctype html>";
+  EXPECT_EQ(kSystraceTraceType, GuessTraceType(prefix, sizeof(prefix)));
+}
+
+TEST(TraceProcessorImplTest, GuessTraceType_DoctypeHtmlMixed) {
+  const uint8_t prefix[] = "<!DoCTyPe HtMl>";
+  EXPECT_EQ(kSystraceTraceType, GuessTraceType(prefix, sizeof(prefix)));
+}
+
+TEST(TraceProcessorImplTest, GuessTraceType_Html) {
+  const uint8_t prefix[] = "<html>";
+  EXPECT_EQ(kSystraceTraceType, GuessTraceType(prefix, sizeof(prefix)));
+}
+
+TEST(TraceProcessorImplTest, GuessTraceType_HtmlUpper) {
+  const uint8_t prefix[] = "<HTML>";
+  EXPECT_EQ(kSystraceTraceType, GuessTraceType(prefix, sizeof(prefix)));
+}
+
+TEST(TraceProcessorImplTest, GuessTraceType_HtmlMixed) {
+  const uint8_t prefix[] = "<htmL>";
+  EXPECT_EQ(kSystraceTraceType, GuessTraceType(prefix, sizeof(prefix)));
+}
+
 TEST(TraceProcessorImplTest, GuessTraceType_Proto) {
   const uint8_t prefix[] = {0x0a, 0x00};  // An empty TracePacket.
   EXPECT_EQ(kProtoTraceType, GuessTraceType(prefix, sizeof(prefix)));
diff --git a/src/trace_processor/importers/common/clock_tracker.cc b/src/trace_processor/importers/common/clock_tracker.cc
index 2bb0ff2..bb0158b 100644
--- a/src/trace_processor/importers/common/clock_tracker.cc
+++ b/src/trace_processor/importers/common/clock_tracker.cc
@@ -36,8 +36,8 @@
 
 using Clock = protos::pbzero::ClockSnapshot::Clock;
 
-ClockTracker::ClockTracker(TraceProcessorContext* ctx)
-    : context_(ctx),
+ClockTracker::ClockTracker(TraceStorage* storage)
+    : storage_(storage),
       trace_time_clock_id_(protos::pbzero::BUILTIN_CLOCK_BOOTTIME) {}
 
 ClockTracker::~ClockTracker() = default;
@@ -65,7 +65,7 @@
                       " cannot use incremental encoding; this is only "
                       "supported for sequence-scoped clocks.",
                       clock_id);
-        context_->storage->IncrementStats(stats::invalid_clock_snapshots);
+        storage_->IncrementStats(stats::invalid_clock_snapshots);
         return snapshot_id;
       }
       domain.unit_multiplier_ns = clock.unit_multiplier_ns;
@@ -79,7 +79,7 @@
                     "different properties (unit=%" PRIu64 ", incremental=%d).",
                     clock_id, clock.unit_multiplier_ns, clock.is_incremental,
                     domain.unit_multiplier_ns, domain.is_incremental);
-      context_->storage->IncrementStats(stats::invalid_clock_snapshots);
+      storage_->IncrementStats(stats::invalid_clock_snapshots);
       return snapshot_id;
     }
     const int64_t timestamp_ns =
@@ -92,7 +92,7 @@
       PERFETTO_ELOG("Clock sync error: duplicate clock domain with id=%" PRIu64
                     " at snapshot %" PRIu32 ".",
                     clock_id, snapshot_id);
-      context_->storage->IncrementStats(stats::invalid_clock_snapshots);
+      storage_->IncrementStats(stats::invalid_clock_snapshots);
       return snapshot_id;
     }
 
@@ -116,7 +116,7 @@
                       " not >= %" PRId64 ".",
                       clock_id, snapshot_id, timestamp_ns,
                       vect.timestamps_ns.back());
-        context_->storage->IncrementStats(stats::invalid_clock_snapshots);
+        storage_->IncrementStats(stats::invalid_clock_snapshots);
         return snapshot_id;
       }
 
@@ -216,7 +216,7 @@
   PERFETTO_DCHECK(!IsReservedSeqScopedClockId(src_clock_id));
   PERFETTO_DCHECK(!IsReservedSeqScopedClockId(target_clock_id));
 
-  context_->storage->IncrementStats(stats::clock_sync_cache_miss);
+  storage_->IncrementStats(stats::clock_sync_cache_miss);
 
   ClockPath path = FindPath(src_clock_id, target_clock_id);
   if (!path.valid()) {
@@ -227,7 +227,7 @@
                     " at timestamp %" PRId64,
                     src_clock_id, target_clock_id, src_timestamp);
     }
-    context_->storage->IncrementStats(stats::clock_sync_failure);
+    storage_->IncrementStats(stats::clock_sync_failure);
     return base::nullopt;
   }
 
diff --git a/src/trace_processor/importers/common/clock_tracker.h b/src/trace_processor/importers/common/clock_tracker.h
index 576c51f..210a34d 100644
--- a/src/trace_processor/importers/common/clock_tracker.h
+++ b/src/trace_processor/importers/common/clock_tracker.h
@@ -29,6 +29,7 @@
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/optional.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/storage/trace_storage.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -135,7 +136,7 @@
     return (global_clock_id >> 32) > 0;
   }
 
-  explicit ClockTracker(TraceProcessorContext*);
+  explicit ClockTracker(TraceStorage*);
   virtual ~ClockTracker();
 
   // Clock description and its value in a snapshot.
@@ -312,7 +313,7 @@
     return &it->second;
   }
 
-  TraceProcessorContext* const context_;
+  TraceStorage* const storage_;
   ClockId trace_time_clock_id_ = 0;
   std::map<ClockId, ClockDomain> clocks_;
   std::set<ClockGraphEdge> graph_;
diff --git a/src/trace_processor/importers/common/clock_tracker_unittest.cc b/src/trace_processor/importers/common/clock_tracker_unittest.cc
index fc8cc9d..769bb84 100644
--- a/src/trace_processor/importers/common/clock_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/clock_tracker_unittest.cc
@@ -42,10 +42,8 @@
 
 class ClockTrackerTest : public ::testing::Test {
  public:
-  ClockTrackerTest() { context_.storage.reset(new TraceStorage()); }
-
-  TraceProcessorContext context_;
-  ClockTracker ct_{&context_};
+  TraceStorage storage_;
+  ClockTracker ct_{&storage_};
 };
 
 TEST_F(ClockTrackerTest, ClockDomainConversions) {
diff --git a/src/trace_processor/importers/common/event_tracker_unittest.cc b/src/trace_processor/importers/common/event_tracker_unittest.cc
index 66fc899..c35c7d7 100644
--- a/src/trace_processor/importers/common/event_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/event_tracker_unittest.cc
@@ -34,7 +34,8 @@
  public:
   EventTrackerTest() {
     context.storage.reset(new TraceStorage());
-    context.global_args_tracker.reset(new GlobalArgsTracker(&context));
+    context.global_args_tracker.reset(
+        new GlobalArgsTracker(context.storage.get()));
     context.args_tracker.reset(new ArgsTracker(&context));
     context.process_tracker.reset(new ProcessTracker(&context));
     context.event_tracker.reset(new EventTracker(&context));
diff --git a/src/trace_processor/importers/common/global_args_tracker.cc b/src/trace_processor/importers/common/global_args_tracker.cc
index dc1343f..25ca413 100644
--- a/src/trace_processor/importers/common/global_args_tracker.cc
+++ b/src/trace_processor/importers/common/global_args_tracker.cc
@@ -19,8 +19,8 @@
 namespace perfetto {
 namespace trace_processor {
 
-GlobalArgsTracker::GlobalArgsTracker(TraceProcessorContext* context)
-    : context_(context) {}
+GlobalArgsTracker::GlobalArgsTracker(TraceStorage* storage)
+    : storage_(storage) {}
 
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/global_args_tracker.h b/src/trace_processor/importers/common/global_args_tracker.h
index 3d5fb3d..573a917 100644
--- a/src/trace_processor/importers/common/global_args_tracker.h
+++ b/src/trace_processor/importers/common/global_args_tracker.h
@@ -21,7 +21,6 @@
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/small_vector.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
 namespace perfetto {
@@ -93,7 +92,7 @@
     }
   };
 
-  explicit GlobalArgsTracker(TraceProcessorContext* context);
+  explicit GlobalArgsTracker(TraceStorage* storage);
 
   // Assumes that the interval [begin, end) of |args| is sorted by keys.
   ArgSetId AddArgSet(const Arg* args, uint32_t begin, uint32_t end) {
@@ -123,7 +122,7 @@
       hash.Update(ArgHasher()(args[i]));
     }
 
-    auto* arg_table = context_->storage->mutable_arg_table();
+    auto* arg_table = storage_->mutable_arg_table();
 
     ArgSetHash digest = hash.digest();
     auto it_and_inserted =
@@ -168,7 +167,7 @@
         case Variadic::Type::kNull:
           break;
       }
-      row.value_type = context_->storage->GetIdForVariadicType(arg.value.type);
+      row.value_type = storage_->GetIdForVariadicType(arg.value.type);
       arg_table->Insert(row);
     }
     return id;
@@ -187,7 +186,7 @@
   base::FlatHashMap<ArgSetHash, uint32_t, base::AlreadyHashed<ArgSetHash>>
       arg_row_for_hash_;
 
-  TraceProcessorContext* context_;
+  TraceStorage* storage_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/process_tracker_unittest.cc b/src/trace_processor/importers/common/process_tracker_unittest.cc
index d037136..de4102e 100644
--- a/src/trace_processor/importers/common/process_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/process_tracker_unittest.cc
@@ -34,7 +34,8 @@
  public:
   ProcessTrackerTest() {
     context.storage.reset(new TraceStorage());
-    context.global_args_tracker.reset(new GlobalArgsTracker(&context));
+    context.global_args_tracker.reset(
+        new GlobalArgsTracker(context.storage.get()));
     context.args_tracker.reset(new ArgsTracker(&context));
     context.process_tracker.reset(new ProcessTracker(&context));
     context.event_tracker.reset(new EventTracker(&context));
diff --git a/src/trace_processor/importers/common/slice_tracker_unittest.cc b/src/trace_processor/importers/common/slice_tracker_unittest.cc
index 29e7489..5b8d37d 100644
--- a/src/trace_processor/importers/common/slice_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/slice_tracker_unittest.cc
@@ -130,7 +130,8 @@
 TEST(SliceTrackerTest, OneSliceWithArgs) {
   TraceProcessorContext context;
   context.storage.reset(new TraceStorage());
-  context.global_args_tracker.reset(new GlobalArgsTracker(&context));
+  context.global_args_tracker.reset(
+      new GlobalArgsTracker(context.storage.get()));
   context.slice_translation_table.reset(
       new SliceTranslationTable(context.storage.get()));
   SliceTracker tracker(&context);
@@ -175,7 +176,8 @@
 TEST(SliceTrackerTest, OneSliceWithArgsWithTranslatedName) {
   TraceProcessorContext context;
   context.storage.reset(new TraceStorage());
-  context.global_args_tracker.reset(new GlobalArgsTracker(&context));
+  context.global_args_tracker.reset(
+      new GlobalArgsTracker(context.storage.get()));
   context.slice_translation_table.reset(
       new SliceTranslationTable(context.storage.get()));
   SliceTracker tracker(&context);
diff --git a/src/trace_processor/importers/common/system_info_tracker.cc b/src/trace_processor/importers/common/system_info_tracker.cc
index aac30fd..f8c1007 100644
--- a/src/trace_processor/importers/common/system_info_tracker.cc
+++ b/src/trace_processor/importers/common/system_info_tracker.cc
@@ -20,7 +20,7 @@
 namespace perfetto {
 namespace trace_processor {
 
-SystemInfoTracker::SystemInfoTracker(TraceProcessorContext*) {}
+SystemInfoTracker::SystemInfoTracker() {}
 SystemInfoTracker::~SystemInfoTracker() = default;
 
 void SystemInfoTracker::SetKernelVersion(base::StringView name,
diff --git a/src/trace_processor/importers/common/system_info_tracker.h b/src/trace_processor/importers/common/system_info_tracker.h
index 1295db6..af108c5 100644
--- a/src/trace_processor/importers/common/system_info_tracker.h
+++ b/src/trace_processor/importers/common/system_info_tracker.h
@@ -35,7 +35,7 @@
 
   static SystemInfoTracker* GetOrCreate(TraceProcessorContext* context) {
     if (!context->system_info_tracker) {
-      context->system_info_tracker.reset(new SystemInfoTracker(context));
+      context->system_info_tracker.reset(new SystemInfoTracker());
     }
     return static_cast<SystemInfoTracker*>(context->system_info_tracker.get());
   }
@@ -45,7 +45,7 @@
   base::Optional<VersionNumber> GetKernelVersion() { return version_; }
 
  private:
-  explicit SystemInfoTracker(TraceProcessorContext*);
+  explicit SystemInfoTracker();
 
   base::Optional<VersionNumber> version_;
 };
diff --git a/src/trace_processor/importers/ftrace/binder_tracker_unittest.cc b/src/trace_processor/importers/ftrace/binder_tracker_unittest.cc
index 960bc25..8884704 100644
--- a/src/trace_processor/importers/ftrace/binder_tracker_unittest.cc
+++ b/src/trace_processor/importers/ftrace/binder_tracker_unittest.cc
@@ -37,7 +37,8 @@
  public:
   BinderTrackerTest() {
     context.storage.reset(new TraceStorage());
-    context.global_args_tracker.reset(new GlobalArgsTracker(&context));
+    context.global_args_tracker.reset(
+        new GlobalArgsTracker(context.storage.get()));
     context.args_tracker.reset(new ArgsTracker(&context));
     context.args_translation_table.reset(
         new ArgsTranslationTable(context.storage.get()));
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index a1167e0..203b811 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -262,7 +262,7 @@
       cros_ec_arg_ec_id_(context->storage->InternString("ec_delta")),
       cros_ec_arg_sample_ts_id_(context->storage->InternString("sample_ts")),
       ufs_clkgating_id_(context->storage->InternString(
-          "UFS clkgating (OFF/REQ_OFF/REQ_ON/ON)")),
+          "io.ufs.clkgating (OFF:0/REQ_OFF/REQ_ON/ON:3)")),
       ufs_command_count_id_(
           context->storage->InternString("io.ufs.command.count")) {
   // Build the lookup table for the strings inside ftrace events (e.g. the
diff --git a/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc b/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc
index 802990f..22b1139 100644
--- a/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc
+++ b/src/trace_processor/importers/ftrace/sched_event_tracker_unittest.cc
@@ -34,7 +34,8 @@
  public:
   SchedEventTrackerTest() {
     context.storage.reset(new TraceStorage());
-    context.global_args_tracker.reset(new GlobalArgsTracker(&context));
+    context.global_args_tracker.reset(
+        new GlobalArgsTracker(context.storage.get()));
     context.args_tracker.reset(new ArgsTracker(&context));
     context.event_tracker.reset(new EventTracker(&context));
     context.process_tracker.reset(new ProcessTracker(&context));
diff --git a/src/trace_processor/importers/ftrace/thread_state_tracker.cc b/src/trace_processor/importers/ftrace/thread_state_tracker.cc
index 8289dd1..9ff75e3 100644
--- a/src/trace_processor/importers/ftrace/thread_state_tracker.cc
+++ b/src/trace_processor/importers/ftrace/thread_state_tracker.cc
@@ -15,14 +15,12 @@
  */
 #include "src/trace_processor/importers/ftrace/thread_state_tracker.h"
 
-#include "src/trace_processor/types/trace_processor_context.h"
-
 namespace perfetto {
 namespace trace_processor {
-ThreadStateTracker::ThreadStateTracker(TraceProcessorContext* context)
-    : context_(context),
-      running_string_id_(context->storage->InternString("Running")),
-      runnable_string_id_(context->storage->InternString("R")) {}
+ThreadStateTracker::ThreadStateTracker(TraceStorage* storage)
+    : storage_(storage),
+      running_string_id_(storage->InternString("Running")),
+      runnable_string_id_(storage->InternString("R")) {}
 ThreadStateTracker::~ThreadStateTracker() = default;
 
 void ThreadStateTracker::PushSchedSwitchEvent(int64_t event_ts,
@@ -113,8 +111,7 @@
   row.dur = -1;
   row.utid = utid;
   row.state = state;
-  auto row_num =
-      context_->storage->mutable_thread_state_table()->Insert(row).row_number;
+  auto row_num = storage_->mutable_thread_state_table()->Insert(row).row_number;
 
   if (utid >= prev_row_numbers_for_thread_.size()) {
     prev_row_numbers_for_thread_.resize(utid + 1);
diff --git a/src/trace_processor/importers/ftrace/thread_state_tracker.h b/src/trace_processor/importers/ftrace/thread_state_tracker.h
index 6e9c414..af3ad4c 100644
--- a/src/trace_processor/importers/ftrace/thread_state_tracker.h
+++ b/src/trace_processor/importers/ftrace/thread_state_tracker.h
@@ -27,13 +27,14 @@
 // waking events and blocking reasons.
 class ThreadStateTracker : public Destructible {
  public:
-  explicit ThreadStateTracker(TraceProcessorContext*);
+  explicit ThreadStateTracker(TraceStorage*);
   ThreadStateTracker(const ThreadStateTracker&) = delete;
   ThreadStateTracker& operator=(const ThreadStateTracker&) = delete;
   ~ThreadStateTracker() override;
   static ThreadStateTracker* GetOrCreate(TraceProcessorContext* context) {
     if (!context->thread_state_tracker) {
-      context->thread_state_tracker.reset(new ThreadStateTracker(context));
+      context->thread_state_tracker.reset(
+          new ThreadStateTracker(context->storage.get()));
     }
     return static_cast<ThreadStateTracker*>(
         context->thread_state_tracker.get());
@@ -74,11 +75,10 @@
 
   tables::ThreadStateTable::RowReference RowNumToRef(
       tables::ThreadStateTable::RowNumber row_number) {
-    return row_number.ToRowReference(
-        context_->storage->mutable_thread_state_table());
+    return row_number.ToRowReference(storage_->mutable_thread_state_table());
   }
 
-  TraceProcessorContext* const context_;
+  TraceStorage* const storage_;
 
   // Strings
   StringId running_string_id_;
diff --git a/src/trace_processor/importers/ftrace/thread_state_tracker_unittest.cc b/src/trace_processor/importers/ftrace/thread_state_tracker_unittest.cc
index 9a19a2e..0c6c370 100644
--- a/src/trace_processor/importers/ftrace/thread_state_tracker_unittest.cc
+++ b/src/trace_processor/importers/ftrace/thread_state_tracker_unittest.cc
@@ -41,9 +41,10 @@
  public:
   ThreadStateTrackerUnittest() {
     context_.storage.reset(new TraceStorage());
-    context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
+    context_.global_args_tracker.reset(
+        new GlobalArgsTracker(context_.storage.get()));
     context_.args_tracker.reset(new ArgsTracker(&context_));
-    tracker_.reset(new ThreadStateTracker(&context_));
+    tracker_.reset(new ThreadStateTracker(context_.storage.get()));
   }
 
   StringId StringIdOf(const char* s) {
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index f2c5d45..732e002 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -28,6 +28,7 @@
     "../../storage",
     "../../tables",
     "../../types",
+    "../../util:stack_traces_util",
     "../common",
   ]
 }
diff --git a/src/trace_processor/importers/proto/android_probes_module.cc b/src/trace_processor/importers/proto/android_probes_module.cc
index 799c8b3..23c25b4 100644
--- a/src/trace_processor/importers/proto/android_probes_module.cc
+++ b/src/trace_processor/importers/proto/android_probes_module.cc
@@ -89,6 +89,7 @@
   RegisterForField(TracePacket::kAndroidGameInterventionListFieldNumber,
                    context);
   RegisterForField(TracePacket::kInitialDisplayStateFieldNumber, context);
+  RegisterForField(TracePacket::kAndroidSystemPropertyFieldNumber, context);
 }
 
 ModuleResult AndroidProbesModule::TokenizePacket(
@@ -198,6 +199,10 @@
       parser_.ParseInitialDisplayState(ttp.timestamp,
                                        decoder.initial_display_state());
       return;
+    case TracePacket::kAndroidSystemPropertyFieldNumber:
+      parser_.ParseAndroidSystemProperty(ttp.timestamp,
+                                         decoder.android_system_property());
+      return;
   }
 }
 
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index 7b5b7e8..fc114ce 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -24,6 +24,7 @@
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/proto/async_track_set_tracker.h"
 #include "src/trace_processor/importers/proto/metadata_tracker.h"
 #include "src/trace_processor/importers/syscalls/syscall_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -33,6 +34,7 @@
 #include "protos/perfetto/config/trace_config.pbzero.h"
 #include "protos/perfetto/trace/android/android_game_intervention_list.pbzero.h"
 #include "protos/perfetto/trace/android/android_log.pbzero.h"
+#include "protos/perfetto/trace/android/android_system_property.pbzero.h"
 #include "protos/perfetto/trace/android/initial_display_state.pbzero.h"
 #include "protos/perfetto/trace/android/packages_list.pbzero.h"
 #include "protos/perfetto/trace/power/battery_counters.pbzero.h"
@@ -54,7 +56,8 @@
       batt_current_id_(context->storage->InternString("batt.current_ua")),
       batt_current_avg_id_(
           context->storage->InternString("batt.current.avg_ua")),
-      screen_state_id_(context->storage->InternString("ScreenState")) {}
+      screen_state_id_(context->storage->InternString("ScreenState")),
+      device_state_id_(context->storage->InternString("DeviceStateChanged")) {}
 
 void AndroidProbesParser::ParseBatteryCounters(int64_t ts, ConstBytes blob) {
   protos::pbzero::BatteryCounters::Decoder evt(blob.data, blob.size);
@@ -309,5 +312,33 @@
   context_->event_tracker->PushCounter(ts, state.display_state(), track);
 }
 
+void AndroidProbesParser::ParseAndroidSystemProperty(int64_t ts,
+                                                     ConstBytes blob) {
+  protos::pbzero::AndroidSystemProperty::Decoder properties(blob.data,
+                                                            blob.size);
+  for (auto it = properties.values(); it; ++it) {
+    protos::pbzero::AndroidSystemProperty::PropertyValue::Decoder kv(*it);
+    if (base::StringView(kv.name()) == "debug.tracing.screen_state") {
+      base::Optional<int32_t> state =
+          base::StringToInt32(kv.value().ToStdString());
+      if (state) {
+        TrackId track =
+            context_->track_tracker->InternGlobalCounterTrack(screen_state_id_);
+        context_->event_tracker->PushCounter(ts, *state, track);
+      }
+    } else if (base::StringView(kv.name()) == "debug.tracing.device_state") {
+      auto state = kv.value();
+
+      StringId state_id = context_->storage->InternString(state);
+      auto track_set_id =
+          context_->async_track_set_tracker->InternGlobalTrackSet(
+              device_state_id_);
+      TrackId track_id =
+          context_->async_track_set_tracker->Scoped(track_set_id, ts, 0);
+      context_->slice_tracker->Scoped(ts, track_id, kNullStringId, state_id, 0);
+    }
+  }
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/android_probes_parser.h b/src/trace_processor/importers/proto/android_probes_parser.h
index fd8fb11..3464866 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.h
+++ b/src/trace_processor/importers/proto/android_probes_parser.h
@@ -41,6 +41,7 @@
   void ParseStatsdMetadata(ConstBytes);
   void ParseAndroidPackagesList(ConstBytes);
   void ParseInitialDisplayState(int64_t ts, ConstBytes);
+  void ParseAndroidSystemProperty(int64_t ts, ConstBytes);
   void ParseAndroidGameIntervention(ConstBytes);
 
  private:
@@ -51,6 +52,7 @@
   const StringId batt_current_id_;
   const StringId batt_current_avg_id_;
   const StringId screen_state_id_;
+  const StringId device_state_id_;
 };
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/android_probes_tracker.cc b/src/trace_processor/importers/proto/android_probes_tracker.cc
index 490ac87..4235cf1 100644
--- a/src/trace_processor/importers/proto/android_probes_tracker.cc
+++ b/src/trace_processor/importers/proto/android_probes_tracker.cc
@@ -19,7 +19,7 @@
 namespace perfetto {
 namespace trace_processor {
 
-AndroidProbesTracker::AndroidProbesTracker(TraceProcessorContext*) {}
+AndroidProbesTracker::AndroidProbesTracker(TraceStorage*) {}
 
 AndroidProbesTracker::~AndroidProbesTracker() = default;
 
diff --git a/src/trace_processor/importers/proto/android_probes_tracker.h b/src/trace_processor/importers/proto/android_probes_tracker.h
index b155cf5..c199e7c 100644
--- a/src/trace_processor/importers/proto/android_probes_tracker.h
+++ b/src/trace_processor/importers/proto/android_probes_tracker.h
@@ -31,12 +31,13 @@
 
 class AndroidProbesTracker : public Destructible {
  public:
-  explicit AndroidProbesTracker(TraceProcessorContext*);
+  explicit AndroidProbesTracker(TraceStorage*);
   ~AndroidProbesTracker() override;
 
   static AndroidProbesTracker* GetOrCreate(TraceProcessorContext* context) {
     if (!context->android_probes_tracker) {
-      context->android_probes_tracker.reset(new AndroidProbesTracker(context));
+      context->android_probes_tracker.reset(
+          new AndroidProbesTracker(context->storage.get()));
     }
     return static_cast<AndroidProbesTracker*>(
         context->android_probes_tracker.get());
diff --git a/src/trace_processor/importers/proto/async_track_set_tracker_unittest.cc b/src/trace_processor/importers/proto/async_track_set_tracker_unittest.cc
index eb2e041..7d26b04 100644
--- a/src/trace_processor/importers/proto/async_track_set_tracker_unittest.cc
+++ b/src/trace_processor/importers/proto/async_track_set_tracker_unittest.cc
@@ -29,7 +29,8 @@
  public:
   AsyncTrackSetTrackerUnittest() {
     context_.storage.reset(new TraceStorage());
-    context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
+    context_.global_args_tracker.reset(
+        new GlobalArgsTracker(context_.storage.get()));
     context_.args_tracker.reset(new ArgsTracker(&context_));
     context_.track_tracker.reset(new TrackTracker(&context_));
     context_.async_track_set_tracker.reset(new AsyncTrackSetTracker(&context_));
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index 696b9e4..39e1418 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -273,18 +273,16 @@
   return result;
 }
 
-HeapGraphTracker::HeapGraphTracker(TraceProcessorContext* context)
-    : context_(context),
-      cleaner_thunk_str_id_(
-          context_->storage->InternString("sun.misc.Cleaner.thunk")),
+HeapGraphTracker::HeapGraphTracker(TraceStorage* storage)
+    : storage_(storage),
+      cleaner_thunk_str_id_(storage_->InternString("sun.misc.Cleaner.thunk")),
       referent_str_id_(
-          context_->storage->InternString("java.lang.ref.Reference.referent")),
-      cleaner_thunk_this0_str_id_(context_->storage->InternString(
+          storage_->InternString("java.lang.ref.Reference.referent")),
+      cleaner_thunk_this0_str_id_(storage_->InternString(
           "libcore.util.NativeAllocationRegistry$CleanerThunk.this$0")),
-      native_size_str_id_(context_->storage->InternString(
-          "libcore.util.NativeAllocationRegistry.size")),
-      cleaner_next_str_id_(
-          context_->storage->InternString("sun.misc.Cleaner.next")) {}
+      native_size_str_id_(
+          storage_->InternString("libcore.util.NativeAllocationRegistry.size")),
+      cleaner_next_str_id_(storage_->InternString("sun.misc.Cleaner.next")) {}
 
 HeapGraphTracker::SequenceState& HeapGraphTracker::GetOrCreateSequence(
     uint32_t seq_id) {
@@ -296,11 +294,11 @@
                                           int64_t ts) {
   if (sequence_state->current_upid != 0 &&
       sequence_state->current_upid != upid) {
-    context_->storage->IncrementStats(stats::heap_graph_non_finalized_graph);
+    storage_->IncrementStats(stats::heap_graph_non_finalized_graph);
     return false;
   }
   if (sequence_state->current_ts != 0 && sequence_state->current_ts != ts) {
-    context_->storage->IncrementStats(stats::heap_graph_non_finalized_graph);
+    storage_->IncrementStats(stats::heap_graph_non_finalized_graph);
     return false;
   }
   sequence_state->current_upid = upid;
@@ -311,7 +309,7 @@
 ObjectTable::RowReference HeapGraphTracker::GetOrInsertObject(
     SequenceState* sequence_state,
     uint64_t object_id) {
-  auto object_table = context_->storage->mutable_heap_graph_object_table();
+  auto object_table = storage_->mutable_heap_graph_object_table();
   auto* ptr = sequence_state->object_id_to_db_row.Find(object_id);
   if (!ptr) {
     auto id_and_row = object_table->Insert({sequence_state->current_upid,
@@ -333,7 +331,7 @@
 ClassTable::RowReference HeapGraphTracker::GetOrInsertType(
     SequenceState* sequence_state,
     uint64_t type_id) {
-  auto class_table = context_->storage->mutable_heap_graph_class_table();
+  auto class_table = storage_->mutable_heap_graph_class_table();
   auto* ptr = sequence_state->type_id_to_db_row.Find(type_id);
   if (!ptr) {
     auto id_and_row =
@@ -372,7 +370,7 @@
   }
 
   uint32_t reference_set_id =
-      context_->storage->heap_graph_reference_table().row_count();
+      storage_->heap_graph_reference_table().row_count();
   bool any_references = false;
 
   ObjectTable::Id owner_id = owner_row_ref.id();
@@ -384,7 +382,7 @@
       owned_row_ref = GetOrInsertObject(&sequence_state, owned_object_id);
 
     auto ref_id_and_row =
-        context_->storage->mutable_heap_graph_reference_table()->Insert(
+        storage_->mutable_heap_graph_reference_table()->Insert(
             {reference_set_id,
              owner_id,
              owned_row_ref ? base::make_optional(owned_row_ref->id())
@@ -462,15 +460,15 @@
     type = str.substr(0, space);
     str = str.substr(space + 1);
   }
-  StringId field_name = context_->storage->InternString(str);
-  StringId type_name = context_->storage->InternString(type);
+  StringId field_name = storage_->InternString(str);
+  StringId type_name = storage_->InternString(type);
 
   sequence_state.interned_fields.Insert(intern_id,
                                         InternedField{field_name, type_name});
 
   auto it = sequence_state.references_for_field_name_id.find(intern_id);
   if (it != sequence_state.references_for_field_name_id.end()) {
-    auto hgr = context_->storage->mutable_heap_graph_reference_table();
+    auto hgr = storage_->mutable_heap_graph_reference_table();
     for (ReferenceTable::RowNumber reference_row_num : it->second) {
       auto row_ref = reference_row_num.ToRowReference(hgr);
       row_ref.set_field_name(field_name);
@@ -501,7 +499,7 @@
       PERFETTO_ELOG("Invalid first packet index %" PRIu64 " (!= 0)", index);
     }
 
-    context_->storage->IncrementIndexedStats(
+    storage_->IncrementIndexedStats(
         stats::heap_graph_missing_packet,
         static_cast<int>(sequence_state.current_upid));
   }
@@ -518,7 +516,7 @@
     if (it != sequence_state->interned_types.end())
       return &it->second;
   }
-  context_->storage->IncrementIndexedStats(
+  storage_->IncrementIndexedStats(
       stats::heap_graph_malformed_packet,
       static_cast<int>(sequence_state->current_upid));
   return nullptr;
@@ -541,7 +539,7 @@
       auto it = sequence_state.interned_location_names.find(
           *interned_type.location_id);
       if (it == sequence_state.interned_location_names.end()) {
-        context_->storage->IncrementIndexedStats(
+        storage_->IncrementIndexedStats(
             stats::heap_graph_invalid_string_id,
             static_cast<int>(sequence_state.current_upid));
       } else {
@@ -555,7 +553,7 @@
     auto sz_obj_it =
         sequence_state.deferred_size_objects_for_type_.find(type_id);
     if (sz_obj_it != sequence_state.deferred_size_objects_for_type_.end()) {
-      auto* hgo = context_->storage->mutable_heap_graph_object_table();
+      auto* hgo = storage_->mutable_heap_graph_object_table();
       for (ObjectTable::RowNumber obj_row_num : sz_obj_it->second) {
         auto obj_row_ref = obj_row_num.ToRowReference(hgo);
         obj_row_ref.set_self_size(
@@ -570,14 +568,14 @@
         sequence_state.deferred_reference_objects_for_type_.end()) {
       for (ObjectTable::RowNumber obj_row_number : ref_obj_it->second) {
         auto obj_row_ref = obj_row_number.ToRowReference(
-            context_->storage->mutable_heap_graph_object_table());
+            storage_->mutable_heap_graph_object_table());
         const InternedType* current_type = &interned_type;
         if (interned_type.no_fields) {
           continue;
         }
         size_t field_offset_in_cls = 0;
         ForReferenceSet(
-            context_->storage.get(), obj_row_ref,
+            storage_, obj_row_ref,
             [this, &current_type, &sequence_state,
              &field_offset_in_cls](ReferenceTable::RowReference ref) {
               while (current_type && field_offset_in_cls >=
@@ -596,7 +594,7 @@
               auto* ptr = sequence_state.interned_fields.Find(field_id);
               if (!ptr) {
                 PERFETTO_DLOG("Invalid field id.");
-                context_->storage->IncrementIndexedStats(
+                storage_->IncrementIndexedStats(
                     stats::heap_graph_malformed_packet,
                     static_cast<int>(sequence_state.current_upid));
                 return true;
@@ -622,40 +620,36 @@
     type_row_ref.set_kind(interned_type.kind);
 
     base::StringView normalized_type =
-        NormalizeTypeName(context_->storage->GetString(interned_type.name));
+        NormalizeTypeName(storage_->GetString(interned_type.name));
 
     base::Optional<StringId> class_package;
     if (location_name) {
       base::Optional<std::string> package_name =
-          PackageFromLocation(context_->storage.get(),
-                              context_->storage->GetString(*location_name));
+          PackageFromLocation(storage_, storage_->GetString(*location_name));
       if (package_name) {
-        class_package =
-            context_->storage->InternString(base::StringView(*package_name));
+        class_package = storage_->InternString(base::StringView(*package_name));
       }
     }
     if (!class_package) {
-      auto app_id = context_->storage->process_table()
+      auto app_id = storage_->process_table()
                         .android_appid()[sequence_state.current_upid];
       if (app_id) {
-        auto pkg_row =
-            context_->storage->package_list_table().uid().IndexOf(*app_id);
+        auto pkg_row = storage_->package_list_table().uid().IndexOf(*app_id);
         if (pkg_row) {
           class_package =
-              context_->storage->package_list_table().package_name()[*pkg_row];
+              storage_->package_list_table().package_name()[*pkg_row];
         }
       }
     }
 
-    class_to_rows_[std::make_pair(
-                       class_package,
-                       context_->storage->InternString(normalized_type))]
+    class_to_rows_[std::make_pair(class_package,
+                                  storage_->InternString(normalized_type))]
         .emplace_back(type_row_ref.ToRowNumber());
   }
 
   if (!sequence_state.deferred_size_objects_for_type_.empty() ||
       !sequence_state.deferred_reference_objects_for_type_.empty()) {
-    context_->storage->IncrementIndexedStats(
+    storage_->IncrementIndexedStats(
         stats::heap_graph_malformed_packet,
         static_cast<int>(sequence_state.current_upid));
   }
@@ -668,13 +662,13 @@
       if (!ptr)
         continue;
 
-      ObjectTable::RowReference row_ref = ptr->ToRowReference(
-          context_->storage->mutable_heap_graph_object_table());
+      ObjectTable::RowReference row_ref =
+          ptr->ToRowReference(storage_->mutable_heap_graph_object_table());
       auto it_and_success = roots_[std::make_pair(sequence_state.current_upid,
                                                   sequence_state.current_ts)]
                                 .emplace(*ptr);
       if (it_and_success.second)
-        MarkRoot(context_->storage.get(), row_ref, root.root_type);
+        MarkRoot(storage_, row_ref, root.root_type);
     }
   }
 
@@ -687,9 +681,8 @@
     ObjectTable::Id obj,
     StringId field) {
   base::Optional<ObjectTable::Id> referred;
-  auto obj_row_ref =
-      *context_->storage->heap_graph_object_table().FindById(obj);
-  ForReferenceSet(context_->storage.get(), obj_row_ref,
+  auto obj_row_ref = *storage_->heap_graph_object_table().FindById(obj);
+  ForReferenceSet(storage_, obj_row_ref,
                   [&](ReferenceTable::RowReference ref) -> bool {
                     if (ref.field_name() == field) {
                       referred = ref.owned_id();
@@ -720,8 +713,8 @@
   //
   // `.size` should be attributed as the native size of Object
 
-  const auto& class_tbl = context_->storage->heap_graph_class_table();
-  auto& objects_tbl = *context_->storage->mutable_heap_graph_object_table();
+  const auto& class_tbl = storage_->heap_graph_class_table();
+  auto& objects_tbl = *storage_->mutable_heap_graph_object_table();
 
   struct Cleaner {
     ObjectTable::Id referent;
@@ -783,10 +776,9 @@
 void HeapGraphTracker::PopulateSuperClasses(const SequenceState& seq) {
   // Maps from normalized class name and location, to superclass.
   std::map<ClassDescriptor, ClassDescriptor> superclass_map =
-      BuildSuperclassMap(seq.current_upid, seq.current_ts,
-                         context_->storage.get());
+      BuildSuperclassMap(seq.current_upid, seq.current_ts, storage_);
 
-  auto* classes_tbl = context_->storage->mutable_heap_graph_class_table();
+  auto* classes_tbl = storage_->mutable_heap_graph_class_table();
   std::map<ClassDescriptor, ClassTable::Id> class_to_id;
   for (uint32_t idx = 0; idx < classes_tbl->row_count(); ++idx) {
     class_to_id[{classes_tbl->name()[idx], classes_tbl->location()[idx]}] =
@@ -798,13 +790,13 @@
   // mapping was generated on the current sequence) - if we cannot identify
   // a superclass we will just skip.
   for (uint32_t idx = 0; idx < classes_tbl->row_count(); ++idx) {
-    auto name = context_->storage->GetString(classes_tbl->name()[idx]);
+    auto name = storage_->GetString(classes_tbl->name()[idx]);
     auto location = classes_tbl->location()[idx];
     auto normalized = GetNormalizedType(name);
     if (normalized.is_static_class || normalized.number_of_arrays > 0)
       continue;
 
-    StringId class_name_id = context_->storage->InternString(normalized.name);
+    StringId class_name_id = storage_->InternString(normalized.name);
     auto map_it = superclass_map.find({class_name_id, location});
     if (map_it == superclass_map.end()) {
       continue;
@@ -941,12 +933,12 @@
 std::unique_ptr<tables::ExperimentalFlamegraphNodesTable>
 HeapGraphTracker::BuildFlamegraph(const int64_t current_ts,
                                   const UniquePid current_upid) {
-  auto profile_type = context_->storage->InternString("graph");
-  auto java_mapping = context_->storage->InternString("JAVA");
+  auto profile_type = storage_->InternString("graph");
+  auto java_mapping = storage_->InternString("JAVA");
 
   std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> tbl(
       new tables::ExperimentalFlamegraphNodesTable(
-          context_->storage->mutable_string_pool(), nullptr));
+          storage_->mutable_string_pool(), nullptr));
 
   auto it = roots_.find(std::make_pair(current_upid, current_ts));
   if (it == roots_.end()) {
@@ -958,7 +950,7 @@
       alloc_row.upid = current_upid;
       alloc_row.profile_type = profile_type;
       alloc_row.depth = 0;
-      alloc_row.name = context_->storage->InternString(
+      alloc_row.name = storage_->InternString(
           "ERROR: INCOMPLETE GRAPH (try increasing buffer size)");
       alloc_row.map_name = java_mapping;
       alloc_row.count = 1;
@@ -974,17 +966,15 @@
   }
 
   const std::set<ObjectTable::RowNumber>& roots = it->second;
-  auto* object_table = context_->storage->mutable_heap_graph_object_table();
+  auto* object_table = storage_->mutable_heap_graph_object_table();
 
   // First pass to calculate shortest paths
   for (ObjectTable::RowNumber root : roots) {
-    UpdateShortestPaths(context_->storage.get(),
-                        root.ToRowReference(object_table));
+    UpdateShortestPaths(storage_, root.ToRowReference(object_table));
   }
   PathFromRoot init_path;
   for (ObjectTable::RowNumber root : roots) {
-    FindPathFromRoot(context_->storage.get(), root.ToRowReference(object_table),
-                     &init_path);
+    FindPathFromRoot(storage_, root.ToRowReference(object_table), &init_path);
   }
 
   std::vector<int64_t> node_to_cumulative_size(init_path.nodes.size());
@@ -1030,7 +1020,7 @@
 
 void HeapGraphTracker::FinalizeAllProfiles() {
   if (!sequence_state_.empty()) {
-    context_->storage->IncrementStats(stats::heap_graph_non_finalized_graph);
+    storage_->IncrementStats(stats::heap_graph_non_finalized_graph);
     // There might still be valuable data even though the trace is truncated.
     while (!sequence_state_.empty()) {
       FinalizeProfile(sequence_state_.begin()->first);
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.h b/src/trace_processor/importers/proto/heap_graph_tracker.h
index c73c902..6fd4fda 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.h
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.h
@@ -93,11 +93,12 @@
     std::vector<uint64_t> object_ids;
   };
 
-  explicit HeapGraphTracker(TraceProcessorContext* context);
+  explicit HeapGraphTracker(TraceStorage* storage);
 
   static HeapGraphTracker* GetOrCreate(TraceProcessorContext* context) {
     if (!context->heap_graph_tracker) {
-      context->heap_graph_tracker.reset(new HeapGraphTracker(context));
+      context->heap_graph_tracker.reset(
+          new HeapGraphTracker(context->storage.get()));
     }
     return static_cast<HeapGraphTracker*>(context->heap_graph_tracker.get());
   }
@@ -230,7 +231,7 @@
   // all the other tables have been fully populated.
   void PopulateNativeSize(const SequenceState& seq);
 
-  TraceProcessorContext* const context_;
+  TraceStorage* const storage_;
   std::map<uint32_t, SequenceState> sequence_state_;
 
   std::map<std::pair<base::Optional<StringId>, StringId>,
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc b/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
index 950f3e9..491082a 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
@@ -65,7 +65,7 @@
   context.process_tracker.reset(new ProcessTracker(&context));
   context.process_tracker->GetOrCreateProcess(kPid);
 
-  HeapGraphTracker tracker(&context);
+  HeapGraphTracker tracker(context.storage.get());
 
   StringPool::Id normal_kind = context.storage->InternString("KIND_NORMAL");
 
@@ -208,7 +208,7 @@
   context.process_tracker.reset(new ProcessTracker(&context));
   context.process_tracker->GetOrCreateProcess(kPid);
 
-  HeapGraphTracker tracker(&context);
+  HeapGraphTracker tracker(context.storage.get());
 
   constexpr uint64_t kField = 1;
   constexpr uint64_t kLocation = 0;
diff --git a/src/trace_processor/importers/proto/metadata_tracker.cc b/src/trace_processor/importers/proto/metadata_tracker.cc
index 7f324b9..a5e6928 100644
--- a/src/trace_processor/importers/proto/metadata_tracker.cc
+++ b/src/trace_processor/importers/proto/metadata_tracker.cc
@@ -30,14 +30,12 @@
 
 }
 
-MetadataTracker::MetadataTracker(TraceProcessorContext* context)
-    : context_(context) {
+MetadataTracker::MetadataTracker(TraceStorage* storage) : storage_(storage) {
   for (uint32_t i = 0; i < kNumKeys; ++i) {
-    key_ids_[i] = context->storage->InternString(metadata::kNames[i]);
+    key_ids_[i] = storage->InternString(metadata::kNames[i]);
   }
   for (uint32_t i = 0; i < kNumKeyTypes; ++i) {
-    key_type_ids_[i] =
-        context->storage->InternString(metadata::kKeyTypeNames[i]);
+    key_type_ids_[i] = storage->InternString(metadata::kKeyTypeNames[i]);
   }
 }
 
@@ -48,11 +46,11 @@
   // When the trace_uuid is set, store a copy in a crash key, so in case of
   // a crash in the pipelines we can tell which trace caused the crash.
   if (key == metadata::trace_uuid && value.type == Variadic::kString) {
-    auto uuid_string_view = context_->storage->GetString(value.string_value);
+    auto uuid_string_view = storage_->GetString(value.string_value);
     g_crash_key_uuid.Set(uuid_string_view);
   }
 
-  auto* metadata_table = context_->storage->mutable_metadata_table();
+  auto* metadata_table = storage_->mutable_metadata_table();
   uint32_t key_idx = static_cast<uint32_t>(key);
   base::Optional<uint32_t> opt_row =
       metadata_table->name().IndexOf(metadata::kNames[key_idx]);
@@ -74,7 +72,7 @@
   // KeyType::kMulti not yet supported by this method:
   PERFETTO_CHECK(metadata::kKeyTypes[key] == metadata::KeyType::kSingle);
 
-  auto* metadata_table = context_->storage->mutable_metadata_table();
+  auto* metadata_table = storage_->mutable_metadata_table();
   uint32_t key_idx = static_cast<uint32_t>(key);
   uint32_t row =
       metadata_table->name().IndexOf(metadata::kNames[key_idx]).value();
@@ -108,7 +106,7 @@
   row.name = key_ids_[key_idx];
   row.key_type = key_type_ids_[static_cast<size_t>(metadata::KeyType::kMulti)];
 
-  auto* metadata_table = context_->storage->mutable_metadata_table();
+  auto* metadata_table = storage_->mutable_metadata_table();
   auto id_and_row = metadata_table->Insert(row);
   WriteValue(id_and_row.row, value);
   return id_and_row.id;
@@ -119,14 +117,14 @@
   row.name = key;
   row.key_type = key_type_ids_[static_cast<size_t>(metadata::KeyType::kSingle)];
 
-  auto* metadata_table = context_->storage->mutable_metadata_table();
+  auto* metadata_table = storage_->mutable_metadata_table();
   auto id_and_row = metadata_table->Insert(row);
   WriteValue(id_and_row.row, value);
   return id_and_row.id;
 }
 
 void MetadataTracker::WriteValue(uint32_t row, Variadic value) {
-  auto* metadata_table = context_->storage->mutable_metadata_table();
+  auto* metadata_table = storage_->mutable_metadata_table();
   switch (value.type) {
     case Variadic::Type::kInt:
       metadata_table->mutable_int_value()->Set(row, value.int_value);
diff --git a/src/trace_processor/importers/proto/metadata_tracker.h b/src/trace_processor/importers/proto/metadata_tracker.h
index 23f48a1..c3e587e 100644
--- a/src/trace_processor/importers/proto/metadata_tracker.h
+++ b/src/trace_processor/importers/proto/metadata_tracker.h
@@ -22,12 +22,10 @@
 namespace perfetto {
 namespace trace_processor {
 
-class TraceProcessorContext;
-
 // Tracks information in the metadata table.
 class MetadataTracker {
  public:
-  MetadataTracker(TraceProcessorContext* context);
+  MetadataTracker(TraceStorage* storage);
 
   // Example usage:
   // SetMetadata(metadata::benchmark_name,
@@ -66,7 +64,7 @@
   std::array<StringId, kNumKeyTypes> key_type_ids_;
   uint32_t chrome_metadata_bundle_count_ = 0;
 
-  TraceProcessorContext* context_;
+  TraceStorage* storage_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/profile_module.cc b/src/trace_processor/importers/proto/profile_module.cc
index a255502..a8cf943 100644
--- a/src/trace_processor/importers/proto/profile_module.cc
+++ b/src/trace_processor/importers/proto/profile_module.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_processor/importers/proto/profile_module.h"
+#include <string>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_utils.h"
@@ -28,11 +29,13 @@
 #include "src/trace_processor/importers/proto/profile_packet_utils.h"
 #include "src/trace_processor/importers/proto/profiler_util.h"
 #include "src/trace_processor/importers/proto/stack_profile_tracker.h"
+#include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/profiler_tables.h"
 #include "src/trace_processor/timestamped_trace_piece.h"
 #include "src/trace_processor/trace_sorter.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/stack_traces_util.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/common/perf_events.pbzero.h"
@@ -446,7 +449,7 @@
   StringId build_id;
   // TODO(b/148109467): Remove workaround once all active Chrome versions
   // write raw bytes instead of a string as build_id.
-  if (module_symbols.build_id().size == 33) {
+  if (util::IsHexModuleId(module_symbols.build_id())) {
     build_id = context_->storage->InternString(module_symbols.build_id());
   } else {
     build_id = context_->storage->InternString(base::StringView(base::ToHex(
@@ -590,5 +593,18 @@
   }
 }
 
+void ProfileModule::NotifyEndOfFile() {
+  for (auto it = context_->storage->stack_profile_mapping_table().IterateRows();
+       it; ++it) {
+    NullTermStringView path = context_->storage->GetString(it.name());
+    NullTermStringView build_id = context_->storage->GetString(it.build_id());
+
+    if (path.StartsWith("/data/local/tmp/") && build_id.empty()) {
+      context_->storage->IncrementStats(
+          stats::symbolization_tmp_build_id_not_found);
+    }
+  }
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/profile_module.h b/src/trace_processor/importers/proto/profile_module.h
index 92f3384..acc2794 100644
--- a/src/trace_processor/importers/proto/profile_module.h
+++ b/src/trace_processor/importers/proto/profile_module.h
@@ -44,6 +44,8 @@
                    const TimestampedTracePiece& ttp,
                    uint32_t field_id) override;
 
+  void NotifyEndOfFile() override;
+
  private:
   // chrome stack sampling:
   ModuleResult TokenizeStreamingProfilePacket(
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
index b6ba662..cbb42cc 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_unittest.cc
@@ -226,12 +226,14 @@
     storage_ = new TraceStorage();
     context_.storage.reset(storage_);
     context_.track_tracker.reset(new TrackTracker(&context_));
-    context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
+    context_.global_args_tracker.reset(
+        new GlobalArgsTracker(context_.storage.get()));
     context_.global_stack_profile_tracker.reset(
         new GlobalStackProfileTracker());
     context_.args_tracker.reset(new ArgsTracker(&context_));
     context_.args_translation_table.reset(new ArgsTranslationTable(storage_));
-    context_.metadata_tracker.reset(new MetadataTracker(&context_));
+    context_.metadata_tracker.reset(
+        new MetadataTracker(context_.storage.get()));
     event_ = new MockEventTracker(&context_);
     context_.event_tracker.reset(event_);
     sched_ = new MockSchedEventTracker(&context_);
@@ -241,7 +243,7 @@
     slice_ = new NiceMock<MockSliceTracker>(&context_);
     context_.slice_tracker.reset(slice_);
     context_.slice_translation_table.reset(new SliceTranslationTable(storage_));
-    clock_ = new ClockTracker(&context_);
+    clock_ = new ClockTracker(context_.storage.get());
     context_.clock_tracker.reset(clock_);
     context_.flow_tracker.reset(new FlowTracker(&context_));
     context_.sorter.reset(new TraceSorter(&context_, CreateParser(),
diff --git a/src/trace_processor/importers/proto/stack_profile_tracker.cc b/src/trace_processor/importers/proto/stack_profile_tracker.cc
index 384adbf..2451142 100644
--- a/src/trace_processor/importers/proto/stack_profile_tracker.cc
+++ b/src/trace_processor/importers/proto/stack_profile_tracker.cc
@@ -20,6 +20,7 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/importers/proto/profiler_util.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/stack_traces_util.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -74,7 +75,7 @@
     // identifier which is already in Hex and doesn't need conversion.
     // TODO(b/148109467): Remove workaround once all active Chrome versions
     // write raw bytes instead of a string as build_id.
-    if (raw_build_id_str.size() == 33) {
+    if (util::IsHexModuleId(raw_build_id_str)) {
       build_id = raw_build_id;
     } else {
       std::string hex_build_id =
diff --git a/src/trace_processor/importers/systrace/systrace_parser.cc b/src/trace_processor/importers/systrace/systrace_parser.cc
index d973c8c..8960a39 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser.cc
@@ -225,9 +225,16 @@
       StringId track_name_id = context_->storage->InternString(point.str_value);
       UniquePid upid =
           context_->process_tracker->GetOrCreateProcess(point.tgid);
-      auto track_set_id =
-          context_->async_track_set_tracker->InternProcessTrackSet(
-              upid, track_name_id);
+
+      // Promote DeviceStateChanged to its own top level track.
+      AsyncTrackSetTracker::TrackSetId track_set_id;
+      if (point.str_value == "DeviceStateChanged") {
+        track_set_id = context_->async_track_set_tracker->InternGlobalTrackSet(
+            track_name_id);
+      } else {
+        track_set_id = context_->async_track_set_tracker->InternProcessTrackSet(
+            upid, track_name_id);
+      }
 
       if (point.phase == 'N') {
         TrackId track_id =
diff --git a/src/trace_processor/metrics/sql/BUILD.gn b/src/trace_processor/metrics/sql/BUILD.gn
index 412e395..2f6be48 100644
--- a/src/trace_processor/metrics/sql/BUILD.gn
+++ b/src/trace_processor/metrics/sql/BUILD.gn
@@ -103,9 +103,12 @@
   "chrome/chrome_input_to_browser_intervals.sql",
   "chrome/chrome_performance_mark_hashes.sql",
   "chrome/chrome_processes.sql",
+  "chrome/chrome_scroll_jank_caused_by_scheduling.sql",
   "chrome/chrome_slice_names.sql",
+  "chrome/chrome_stack_samples_for_task.sql",
   "chrome/chrome_unsymbolized_args.sql",
   "chrome/chrome_tasks.sql",
+  "chrome/chrome_tasks_delaying_input_processing.sql",
   "chrome/chrome_thread_slice.sql",
   "chrome/chrome_user_event_hashes.sql",
   "chrome/cpu_time_by_category.sql",
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql b/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql
index 5b029b3..9f64718 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql
@@ -196,7 +196,7 @@
           -- and is not a fling generated by the viz compositor thread(GPU process).
           ancestor_slices.name = "sendTouchEvent"
       )
-      > 0
+      = 1
       THEN FALSE
     ELSE TRUE
     END AS blocked_gesture
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql b/src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql
new file mode 100644
index 0000000..9fe4516
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_scroll_jank_caused_by_scheduling.sql
@@ -0,0 +1,90 @@
+--
+-- Copyright 2022 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT RUN_METRIC('chrome/chrome_input_to_browser_intervals.sql');
+
+-- Script params:
+-- {{dur_causes_jank_ms}} : The duration of a task barrage on the Chrome
+-- main thread that will delay input causing jank.
+
+-- Filter intervals to only durations longer than {{dur_causes_jank_ms}}.
+DROP VIEW IF EXISTS chrome_input_to_browser_longer_intervals;
+CREATE VIEW chrome_input_to_browser_longer_intervals AS
+SELECT
+  *
+FROM chrome_input_to_browser_intervals
+WHERE
+  (window_end_ts - window_start_ts) >= {{dur_causes_jank_ms}} * 1e6;
+
+-- Assign tasks to each delay interval that we could have started
+-- processing input but didn't on the main thread, and sum those
+-- tasks.
+-- We filter java out here as we're interested in tasks that delayed
+-- yielding to java native work, and we filter tasks that are more
+-- than 8ms here as those are handled separately and are not regarded
+-- as scheduling issues.
+DROP VIEW IF EXISTS chrome_task_barrages_per_interval;
+CREATE VIEW chrome_task_barrages_per_interval AS
+SELECT
+  GROUP_CONCAT(DISTINCT full_name) AS full_name,
+  SUM(dur / 1e6) AS total_duration_ms,
+  SUM(thread_dur / 1e6) AS total_thread_duration_ms,
+  MIN(id) AS first_id_per_task_barrage,
+  MAX(id) AS last_id_per_task_barrage,
+  COUNT(*) as count,
+  window_start_id,
+  window_start_ts,
+  window_end_id,
+  window_end_ts
+FROM
+  (SELECT * FROM (
+    (
+      SELECT
+        chrome_tasks.full_name AS full_name,
+        chrome_tasks.dur  AS dur,
+        chrome_tasks.thread_dur AS thread_dur,
+        chrome_tasks.ts AS ts,
+        chrome_tasks.id,
+        chrome_tasks.upid
+      FROM
+        chrome_tasks
+      WHERE
+         chrome_tasks.thread_name = "CrBrowserMain"
+         AND task_type != "java"
+         AND task_type != "choreographer"
+      ORDER BY chrome_tasks.ts
+    ) tasks
+    JOIN chrome_input_to_browser_longer_intervals
+      ON (tasks.ts + tasks.dur) >
+      chrome_input_to_browser_longer_intervals.window_start_ts
+      AND (tasks.ts + tasks.dur) <
+      chrome_input_to_browser_longer_intervals.window_end_ts
+      AND tasks.ts > chrome_input_to_browser_longer_intervals.window_start_ts
+      AND tasks.ts < chrome_input_to_browser_longer_intervals.window_end_ts
+      -- For cases when there are multiple chrome instances.
+      and tasks.upid = chrome_input_to_browser_longer_intervals.upid)
+    ORDER BY
+    window_start_ts, window_end_ts
+  )
+  GROUP BY window_start_ts, window_end_ts;
+
+-- Filter to task barrages that took more than 8ms, as barrages
+-- that lasted less than that are unlikely to have caused jank.
+DROP VIEW IF EXISTS chrome_scroll_jank_caused_by_scheduling;
+CREATE VIEW chrome_scroll_jank_caused_by_scheduling AS
+  SELECT *
+  FROM chrome_task_barrages_per_interval
+  WHERE total_duration_ms > {{dur_causes_jank_ms}} AND count > 1
+  ORDER BY total_duration_ms DESC;
\ No newline at end of file
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql b/src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql
new file mode 100644
index 0000000..a0523f3
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_stack_samples_for_task.sql
@@ -0,0 +1,165 @@
+--
+-- Copyright 2022 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+-- Params:
+-- @target_duration_ms: find stack samples on tasks that are longer than
+-- this value
+
+-- @thread_name: thread name to look for stack samples on
+
+-- @task_name: a task name following chrome_tasks.sql naming convention to
+-- find stack samples on.
+
+
+SELECT RUN_METRIC('chrome/chrome_tasks.sql');
+
+
+SELECT CREATE_FUNCTION(
+  'DescribeSymbol(symbol STRING, frame_name STRING)',
+  'STRING',
+  'SELECT COALESCE($symbol,
+   CASE WHEN demangle($frame_name) IS NULL
+    THEN $frame_name
+    ELSE demangle($frame_name)
+   END)'
+);
+
+-- Get all Chrome tasks that match a specific name on a specific thread.
+-- The timestamps for those tasks are going to be used later on to gather
+-- information about stack traces that were collected during running them.
+DROP VIEW IF EXISTS chrome_targeted_task;
+CREATE VIEW chrome_targeted_task AS
+SELECT
+  chrome_tasks.full_name AS full_name,
+  chrome_tasks.dur AS dur,
+  chrome_tasks.ts AS ts,
+  chrome_tasks.id AS id,
+  utid AS utid
+FROM
+  chrome_tasks
+WHERE
+  chrome_tasks.dur >= {{target_duration_ms}} * 1e6
+  AND chrome_tasks.thread_name = {{thread_name}}
+  AND chrome_tasks.full_name = {{task_name}};
+
+
+-- Get all frames attached to callsite ids, as frames can be
+-- reused between stack frames, callsite ids are unique per
+-- stack sample.
+DROP VIEW IF EXISTS chrome_non_symbolized_frames;
+CREATE VIEW chrome_non_symbolized_frames AS
+SELECT
+  frames.name as frame_name,
+  callsite.id as callsite_id,
+  *
+FROM
+  stack_profile_frame frames
+  JOIN stack_profile_callsite callsite
+    ON callsite.frame_id = frames.id;
+
+-- Only lowest child frames are join-able with chrome_non_symbolized_frames
+-- which we need for the time at which the callstack was taken.
+DROP VIEW IF EXISTS chrome_symbolized_child_frames;
+CREATE VIEW chrome_symbolized_child_frames AS
+SELECT
+  thread.name as thread_name,
+  sample.utid AS sample_utid,
+  *
+FROM
+  chrome_non_symbolized_frames frames
+  JOIN cpu_profile_stack_sample sample USING(callsite_id)
+  JOIN thread USING(utid)
+  JOIN process USING(upid);
+
+-- Not all frames are symbolized, in cases where those frames
+-- are not symbolized, use the file name as it is usually descriptive
+-- of the function.
+DROP VIEW IF EXISTS chrome_thread_symbolized_child_frames;
+CREATE VIEW chrome_thread_symbolized_child_frames AS
+SELECT
+ DescribeSymbol(symbol.name, frame_name) AS description,
+ depth,
+ ts,
+ callsite_id,
+ sample_utid
+FROM chrome_symbolized_child_frames
+  LEFT JOIN stack_profile_symbol symbol USING(symbol_set_id)
+WHERE thread_name = {{thread_name}} ORDER BY ts DESC;
+
+-- Since only leaf stack frames have a timestamp, let's export this
+-- timestamp to all it's ancestor frames to use it later on for
+-- filtering frames within specific windows
+DROP VIEW IF EXISTS chrome_non_symbolized_frames_timed;
+CREATE VIEW chrome_non_symbolized_frames_timed AS
+  SELECT
+    chrome_non_symbolized_frames.frame_name,
+    chrome_non_symbolized_frames.depth,
+    chrome_thread_symbolized_child_frames.ts,
+    chrome_thread_symbolized_child_frames.sample_utid,
+    chrome_non_symbolized_frames.callsite_id,
+    symbol_set_id,
+    chrome_non_symbolized_frames.frame_id
+FROM chrome_thread_symbolized_child_frames
+  JOIN experimental_ancestor_stack_profile_callsite(
+    chrome_thread_symbolized_child_frames.callsite_id) child
+  JOIN chrome_non_symbolized_frames
+  ON chrome_non_symbolized_frames.callsite_id = child.id;
+
+DROP VIEW IF EXISTS chrome_frames_timed_and_symbolized;
+CREATE VIEW chrome_frames_timed_and_symbolized AS
+  SELECT
+    DescribeSymbol(symbol.name, frame_name) AS description,
+    ts,
+    depth,
+    callsite_id,
+    sample_utid
+FROM chrome_non_symbolized_frames_timed
+  LEFT JOIN stack_profile_symbol symbol
+    USING(symbol_set_id)
+ORDER BY DEPTH ASC;
+
+-- Union leaf stack frames with all stack frames after the timestamp
+-- is attached to get a view of all frames timestamped.
+DROP VIEW IF EXISTS all_frames;
+CREATE VIEW all_frames AS
+SELECT
+  *
+FROM
+  (SELECT
+    * FROM
+      chrome_frames_timed_and_symbolized
+  UNION
+  SELECT
+    description,
+      ts,
+      depth,
+      callsite_id,
+      sample_utid
+  FROM chrome_thread_symbolized_child_frames)
+ORDER BY depth ASC;
+
+-- Filter stack samples that happened only during the specified
+-- task on the specified thread.
+DROP VIEW IF EXISTS chrome_stack_samples_for_task;
+CREATE VIEW chrome_stack_samples_for_task AS
+SELECT
+    all_frames.*
+FROM
+    all_frames JOIN
+    chrome_targeted_task ON
+    all_frames.sample_utid = chrome_targeted_task.utid
+    AND all_frames.ts >= chrome_targeted_task.ts
+    AND all_frames.ts <= chrome_targeted_task.ts + chrome_targeted_task.dur;
\ No newline at end of file
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_tasks_delaying_input_processing.sql b/src/trace_processor/metrics/sql/chrome/chrome_tasks_delaying_input_processing.sql
new file mode 100644
index 0000000..dfab111
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_tasks_delaying_input_processing.sql
@@ -0,0 +1,87 @@
+--
+-- Copyright 2022 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+-- Script params:
+-- {{duration_causing_jank_ms}} : The duration of a single task that would cause
+-- jank, by delaying input from being handled on the main thread.
+
+SELECT RUN_METRIC('chrome/chrome_tasks.sql');
+SELECT RUN_METRIC('chrome/chrome_input_to_browser_intervals.sql');
+
+-- Get the tasks that was running for more than 8ms within windows
+-- that we could have started processing input but did not on the
+-- main thread, because it was blocked by those tasks.
+DROP VIEW IF EXISTS chrome_tasks_delaying_input_processing_unaggregated;
+CREATE VIEW chrome_tasks_delaying_input_processing_unaggregated AS
+SELECT
+  tasks.full_name AS full_name,
+  tasks.dur / 1e6 AS duration_ms,
+  id AS slice_id,
+  thread_dur / 1e6 AS thread_dur_ms,
+  chrome_input_to_browser_intervals.window_start_id,
+  chrome_input_to_browser_intervals.window_end_id
+FROM
+  (
+    (
+      SELECT
+        chrome_tasks.full_name AS full_name,
+        chrome_tasks.dur AS dur,
+        chrome_tasks.ts AS ts,
+        chrome_tasks.id AS id,
+        chrome_tasks.upid AS upid,
+        thread_dur
+      FROM
+        chrome_tasks
+      WHERE
+        chrome_tasks.dur >= {{duration_causing_jank_ms}} * 1e6
+        and chrome_tasks.thread_name = "CrBrowserMain"
+    ) tasks
+    JOIN chrome_input_to_browser_intervals
+      ON tasks.ts + tasks.dur > chrome_input_to_browser_intervals.window_start_ts
+      AND tasks.ts + tasks.dur < chrome_input_to_browser_intervals.window_end_ts
+      AND tasks.upid = chrome_input_to_browser_intervals.upid
+  );
+
+-- Same task can delay multiple GestureUpdates, this step dedups
+-- multiple occrences of the same slice_id
+DROP VIEW IF EXISTS chrome_tasks_delaying_input_processing;
+CREATE VIEW chrome_tasks_delaying_input_processing AS
+SELECT
+  full_name,
+  duration_ms,
+  slice_id,
+  thread_dur_ms
+FROM chrome_tasks_delaying_input_processing_unaggregated
+GROUP BY slice_id;
+
+-- Get the tasks that were running for more than 8ms within windows
+-- that we could have started processing input but did not on the
+-- main thread, because it was blocked by those tasks.
+DROP VIEW IF EXISTS chrome_tasks_delaying_input_processing_summary;
+CREATE VIEW chrome_tasks_delaying_input_processing_summary AS
+SELECT
+  full_name AS full_name,
+  AVG(duration_ms) AS avg_duration_ms,
+  AVG(thread_dur_ms) AS avg_thread_duration_ms,
+  MIN(duration_ms) AS min_task_duration,
+  MAX(duration_ms) as max_task_duration,
+  SUM(duration_ms) AS total_duration_ms,
+  SUM(thread_dur_ms) AS total_thread_duration_ms,
+  GROUP_CONCAT(slice_id, '-') AS slice_ids,
+  COUNT(*) AS count
+FROM
+  chrome_tasks_delaying_input_processing
+GROUP BY
+  full_name;
\ No newline at end of file
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index faa83e6..748172b 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -185,6 +185,10 @@
        "Time (us) the heapprofd client was blocked on the spinlock."),         \
   F(heapprofd_last_profile_timestamp,   kIndexed, kInfo,     kTrace,           \
        "The timestamp (in trace time) for the last dump for a process"),       \
+  F(symbolization_tmp_build_id_not_found,   kSingle,  kError,    kAnalysis,    \
+       "Number of file mappings in /data/local/tmp without a build id. "       \
+       "Symbolization doesn't work for executables in /data/local/tmp "        \
+       "because of SELinux. Please use /data/local/tests"),                    \
   F(metatrace_overruns,                 kSingle,  kError,    kTrace,    ""),   \
   F(packages_list_has_parse_errors,     kSingle,  kError,    kTrace,    ""),   \
   F(packages_list_has_read_errors,      kSingle,  kError,    kTrace,    ""),   \
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index 25087d5..2e019f3 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -59,12 +59,13 @@
   context_.flow_tracker.reset(new FlowTracker(&context_));
   context_.event_tracker.reset(new EventTracker(&context_));
   context_.process_tracker.reset(new ProcessTracker(&context_));
-  context_.clock_tracker.reset(new ClockTracker(&context_));
+  context_.clock_tracker.reset(new ClockTracker(context_.storage.get()));
   context_.heap_profile_tracker.reset(new HeapProfileTracker(&context_));
   context_.perf_sample_tracker.reset(new PerfSampleTracker(&context_));
   context_.global_stack_profile_tracker.reset(new GlobalStackProfileTracker());
-  context_.metadata_tracker.reset(new MetadataTracker(&context_));
-  context_.global_args_tracker.reset(new GlobalArgsTracker(&context_));
+  context_.metadata_tracker.reset(new MetadataTracker(context_.storage.get()));
+  context_.global_args_tracker.reset(
+      new GlobalArgsTracker(context_.storage.get()));
   {
     context_.descriptor_pool_.reset(new DescriptorPool());
     auto status = context_.descriptor_pool_->AddFromFileDescriptorSet(
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index a7e7341..79f04fb 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -45,6 +45,17 @@
   }
 }
 
+source_set("stack_traces_util") {
+  sources = [
+    "stack_traces_util.cc",
+    "stack_traces_util.h",
+  ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../include/perfetto/ext/base:base",
+  ]
+}
+
 source_set("protozero_to_text") {
   sources = [
     "protozero_to_text.cc",
diff --git a/src/trace_processor/util/stack_traces_util.cc b/src/trace_processor/util/stack_traces_util.cc
new file mode 100644
index 0000000..a255560
--- /dev/null
+++ b/src/trace_processor/util/stack_traces_util.cc
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/util/stack_traces_util.h"
+#include "perfetto/ext/base/string_view.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+
+bool IsHexModuleId(base::StringView module) {
+  return module.size() == 33;
+}
+
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/util/stack_traces_util.h b/src/trace_processor/util/stack_traces_util.h
new file mode 100644
index 0000000..1171786
--- /dev/null
+++ b/src/trace_processor/util/stack_traces_util.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_UTIL_STACK_TRACES_UTIL_H_
+#define SRC_TRACE_PROCESSOR_UTIL_STACK_TRACES_UTIL_H_
+
+#include "perfetto/ext/base/string_view.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace util {
+
+// Returns whether this string is of a hex chrome module or not to decide
+// whether to convert the module to/from hex.
+// TODO(b/148109467): Remove workaround once all active Chrome versions
+// write raw bytes instead of a string as build_id.
+bool IsHexModuleId(base::StringView module);
+
+}  // namespace util
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_STACK_TRACES_UTIL_H_
diff --git a/src/tracebox/BUILD.gn b/src/tracebox/BUILD.gn
index 9000732..aeed465 100644
--- a/src/tracebox/BUILD.gn
+++ b/src/tracebox/BUILD.gn
@@ -26,5 +26,8 @@
     "../traced/service",
     "../websocket_bridge:lib",
   ]
+  if(enable_perfetto_traced_perf) {
+    deps += ["../profiling/perf:traced_perf_main"]
+  }
   sources = [ "tracebox.cc" ]
 }
diff --git a/src/tracebox/tracebox.cc b/src/tracebox/tracebox.cc
index a421b97..359b370 100644
--- a/src/tracebox/tracebox.cc
+++ b/src/tracebox/tracebox.cc
@@ -22,6 +22,10 @@
 #include "src/perfetto_cmd/perfetto_cmd.h"
 #include "src/websocket_bridge/websocket_bridge.h"
 
+#if PERFETTO_BUILDFLAG(PERFETTO_TRACED_PERF)
+#include "src/profiling/perf/traced_perf.h"
+#endif
+
 #include <stdio.h>
 
 #include <tuple>
@@ -38,6 +42,9 @@
 const Applet g_applets[]{
     {"traced", ServiceMain},
     {"traced_probes", ProbesMain},
+#if PERFETTO_BUILDFLAG(PERFETTO_TRACED_PERF)
+    {"traced_perf", TracedPerfMain},
+#endif
     {"perfetto", PerfettoCmdMain},
     {"trigger_perfetto", TriggerPerfettoMain},
     {"websocket_bridge", WebsocketBridgeMain},
@@ -182,7 +189,28 @@
                            &traced_probes_notify_msg);
   if (traced_probes_notify_msg != "1")
     PERFETTO_FATAL(
-        "The traced_proces service failed unexpectedly. Check the logs");
+        "The traced_probes service failed unexpectedly. Check the logs");
+#endif
+
+#if PERFETTO_BUILDFLAG(PERFETTO_TRACED_PERF)
+  base::Subprocess traced_perf({self_path, "traced_perf"});
+  // Put traced_perf in the same process group as traced. Same reason (CTRL+C)
+  // but it's not worth creating a new group.
+  traced_perf.args.posix_proc_group_id = traced.pid();
+
+  base::Pipe traced_perf_sync_pipe = base::Pipe::Create();
+  int traced_perf_fd = *traced_perf_sync_pipe.wr;
+  base::SetEnv("TRACED_PERF_NOTIFY_FD", std::to_string(traced_perf_fd));
+  traced_perf.args.preserve_fds.emplace_back(traced_perf_fd);
+  traced_perf.Start();
+  traced_perf_sync_pipe.wr.reset();
+
+  std::string traced_perf_notify_msg;
+  base::ReadPlatformHandle(*traced_perf_sync_pipe.rd,
+                           &traced_perf_notify_msg);
+  if (traced_perf_notify_msg != "1")
+    PERFETTO_FATAL(
+        "The traced_perf service failed unexpectedly. Check the logs");
 #endif
 
   perfetto_cmd.ConnectToServiceRunAndMaybeNotify();
diff --git a/src/traced/service/service.cc b/src/traced/service/service.cc
index 215392a..72f19ac 100644
--- a/src/traced/service/service.cc
+++ b/src/traced/service/service.cc
@@ -225,9 +225,12 @@
     PERFETTO_CHECK(base::CloseFile(notif_fd) == 0);
   }
 
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD) && \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   // Notify init (perfetto.rc) that traced has been started. Used only by
   // the perfetto_trace_on_boot init service.
+  // This property can be set only in in-tree builds. shell.te doesn't have
+  // SELinux permissions to set sys.trace.* properties.
   if (__system_property_set("sys.trace.traced_started", "1") != 0) {
     PERFETTO_PLOG("Failed to set property sys.trace.traced_started");
   }
diff --git a/src/tracing/internal/tracing_muxer_impl.cc b/src/tracing/internal/tracing_muxer_impl.cc
index 41da7e6..1d6e2ca 100644
--- a/src/tracing/internal/tracing_muxer_impl.cc
+++ b/src/tracing/internal/tracing_muxer_impl.cc
@@ -2046,7 +2046,7 @@
           return;
         }
 
-        PERFETTO_DLOG("Reconnecting backend %ld for startup tracing",
+        PERFETTO_DLOG("Reconnecting backend %zu for startup tracing",
                       backend_id);
         backend.producer_conn_args.use_producer_provided_smb = true;
         backend.producer->service_->Disconnect();  // Causes a reconnect.
diff --git a/test/data/chrome_stack_traces_symbolized_trace.pftrace.sha256 b/test/data/chrome_stack_traces_symbolized_trace.pftrace.sha256
new file mode 100644
index 0000000..76156d4
--- /dev/null
+++ b/test/data/chrome_stack_traces_symbolized_trace.pftrace.sha256
@@ -0,0 +1 @@
+a3cb32a2faaef29b0b16c0f8242073beffdc6d0454e7a07a5e5466349f3c3125
\ No newline at end of file
diff --git a/test/data/fling_with_input_delay.pftrace.sha256 b/test/data/fling_with_input_delay.pftrace.sha256
new file mode 100644
index 0000000..c8ea1a2
--- /dev/null
+++ b/test/data/fling_with_input_delay.pftrace.sha256
@@ -0,0 +1 @@
+8c968b20e71481475a429399b4366bd796c527293218fe80789f9ed6ab9db5b4
\ No newline at end of file
diff --git a/test/trace_processor/android/android_system_property.textproto b/test/trace_processor/android/android_system_property.textproto
new file mode 100644
index 0000000..ffcb98d
--- /dev/null
+++ b/test/trace_processor/android/android_system_property.textproto
@@ -0,0 +1,32 @@
+packet {
+  timestamp: 1000
+  android_system_property {
+    values {
+      name: "debug.tracing.screen_state"
+      value: "2"
+    }
+    values {
+      name: "debug.tracing.device_state"
+      value: "some_state_from_sysprops"
+    }
+  }
+}
+packet {
+  ftrace_events {
+    cpu: 1
+    event {
+      timestamp: 2000
+      pid: 1
+      print {
+        buf: "C|1000|ScreenState|1\n"
+      }
+    }
+    event {
+      timestamp: 3000
+      pid: 1
+      print {
+        buf: "N|1000|DeviceStateChanged|some_state_from_atrace\n"
+      }
+    }
+  }
+}
diff --git a/test/trace_processor/android/android_system_property_counter.out b/test/trace_processor/android/android_system_property_counter.out
new file mode 100644
index 0000000..9a52c9f
--- /dev/null
+++ b/test/trace_processor/android/android_system_property_counter.out
@@ -0,0 +1,3 @@
+"id","type","name","id","ts","type","value"
+0,"counter_track","ScreenState",0,1000,"counter",2.000000
+0,"counter_track","ScreenState",1,2000,"counter",1.000000
diff --git a/test/trace_processor/android/android_system_property_counter_test.sql b/test/trace_processor/android/android_system_property_counter_test.sql
new file mode 100644
index 0000000..580e4a9
--- /dev/null
+++ b/test/trace_processor/android/android_system_property_counter_test.sql
@@ -0,0 +1,3 @@
+select t.id, t.type, t.name, c.id, c.ts, c.type, c.value
+from counter_track t join counter c on t.id = c.track_id
+where name = 'ScreenState';
diff --git a/test/trace_processor/android/android_system_property_slice.out b/test/trace_processor/android/android_system_property_slice.out
new file mode 100644
index 0000000..8fe8d07
--- /dev/null
+++ b/test/trace_processor/android/android_system_property_slice.out
@@ -0,0 +1,3 @@
+"id","type","name","id","ts","dur","type","name"
+1,"track","DeviceStateChanged",0,1000,0,"internal_slice","some_state_from_sysprops"
+1,"track","DeviceStateChanged",1,3000,0,"internal_slice","some_state_from_atrace"
diff --git a/test/trace_processor/android/android_system_property_slice_test.sql b/test/trace_processor/android/android_system_property_slice_test.sql
new file mode 100644
index 0000000..96e0fcf
--- /dev/null
+++ b/test/trace_processor/android/android_system_property_slice_test.sql
@@ -0,0 +1,3 @@
+select t.id, t.type, t.name, s.id, s.ts, s.dur, s.type, s.name
+from track t join slice s on s.track_id = t.id
+where t.name = 'DeviceStateChanged';
diff --git a/test/trace_processor/android/index b/test/trace_processor/android/index
index ce248b1..d6be769 100644
--- a/test/trace_processor/android/index
+++ b/test/trace_processor/android/index
@@ -1,2 +1,5 @@
 # Ensure Android game intervntion list are parsed correctly
 game_intervention_list_test.textproto game_intervention_list_test.sql game_intervention_list_test.out
+
+android_system_property.textproto android_system_property_counter_test.sql android_system_property_counter.out
+android_system_property.textproto android_system_property_slice_test.sql android_system_property_slice.out
diff --git a/test/trace_processor/chrome/chrome_scroll_jank_caused_by_scheduling_test.out b/test/trace_processor/chrome/chrome_scroll_jank_caused_by_scheduling_test.out
new file mode 100644
index 0000000..5b78dca
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_scroll_jank_caused_by_scheduling_test.out
@@ -0,0 +1,3 @@
+
+"full_name","total_duration_ms","total_thread_duration_ms","count","window_start_ts","window_end_ts"
+"RunTask(posted_from=cc/scheduler/scheduler.cc:PostPendingBeginFrameTask),RunTask(posted_from=cc/scheduler/scheduler.cc:ScheduleBeginImplFrameDeadline),SingleThreadProxy::BeginMainFrame(java_views=ToolbarLayout),blink.mojom.WidgetInputHandler reply (hash=3392143105),cc.mojom.RenderFrameMetadataObserverClient message (hash=330497194),viz.mojom.CompositorFrameSinkClient message (hash=3114070324),viz.mojom.CompositorFrameSinkClient message (hash=50871626),viz.mojom.FrameSinkManagerClient message (hash=532012934)",7.568000,6.745000,11,666960999011,666972176011
diff --git a/test/trace_processor/chrome/chrome_scroll_jank_caused_by_scheduling_test.sql b/test/trace_processor/chrome/chrome_scroll_jank_caused_by_scheduling_test.sql
new file mode 100644
index 0000000..4caea94
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_scroll_jank_caused_by_scheduling_test.sql
@@ -0,0 +1,27 @@
+--
+-- Copyright 2022 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT RUN_METRIC('chrome/chrome_scroll_jank_caused_by_scheduling.sql',
+'dur_causes_jank_ms',
+/* dur_causes_jank_ms = */ '5') AS suppress_query_output;
+
+SELECT
+  full_name,
+  total_duration_ms,
+  total_thread_duration_ms,
+  count,
+  window_start_ts,
+  window_end_ts
+FROM chrome_scroll_jank_caused_by_scheduling;
\ No newline at end of file
diff --git a/test/trace_processor/chrome/chrome_stack_samples_for_task_test.out b/test/trace_processor/chrome/chrome_stack_samples_for_task_test.out
new file mode 100644
index 0000000..6e63af3
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_stack_samples_for_task_test.out
@@ -0,0 +1,11 @@
+
+"description","ts","depth"
+"Scanned",696373965111470,0
+"tracing::TraceEventDataSource::OnAddLegacyTraceEvent(base::trace_event::TraceEvent*, bool, base::trace_event::TraceEventHandle*)",696373965111470,1
+"base::trace_event::TraceLog::AddTraceEventWithThreadIdAndTimestamps(char, unsigned char const*, char const*, char const*, unsigned long long, unsigned long long, int, base::TimeTicks const&, base::ThreadTicks const&, base::trace_event::TraceArguments*, unsigned int)",696373965111470,2
+"base::subtle::TimeTicksNowIgnoringOverride()",696373965111470,3
+"base::tracing::AutoThreadLocalBoolean::~AutoThreadLocalBoolean()",696373965111470,4
+"tracing::TraceEventDataSource::OnUpdateDuration(unsigned char const*, char const*, base::trace_event::TraceEventHandle, int, bool, base::TimeTicks const&, base::ThreadTicks const&, base::trace_event::ThreadInstructionCount)",696373965111470,5
+"content::ServiceWorkerRegistry::StoreUserData(long long, blink::StorageKey const&, std::__1::vector<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::allocator<std::__1::pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > > const&, base::OnceCallback<void (blink::ServiceWorkerStatusCode)>)",696373965111470,6
+"base::trace_event::TraceLog::UpdateTraceEventDuration(unsigned char const*, char const*, base::trace_event::TraceEventHandle)",696373965111470,7
+"XML_ParseBuffer",696373965111470,8
diff --git a/test/trace_processor/chrome/chrome_stack_samples_for_task_test.sql b/test/trace_processor/chrome/chrome_stack_samples_for_task_test.sql
new file mode 100644
index 0000000..3b33d47
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_stack_samples_for_task_test.sql
@@ -0,0 +1,34 @@
+--
+-- Copyright 2022 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT RUN_METRIC('chrome/chrome_stack_samples_for_task.sql',
+    'target_duration_ms', '0.000001',
+    'thread_name', '"CrBrowserMain"',
+    'task_name', '"sendTouchEvent"') AS suppress_query_output;
+
+SELECT
+    sample.description,
+    sample.ts,
+    sample.depth
+FROM chrome_stack_samples_for_task sample
+JOIN (
+    SELECT
+        ts,
+        dur
+    FROM slice
+    WHERE ts = 696373965001470
+) test_slice
+    ON sample.ts >= test_slice.ts
+    AND sample.ts <= test_slice.ts + test_slice.dur;
\ No newline at end of file
diff --git a/test/trace_processor/chrome/chrome_tasks_delaying_input_processing_test.out b/test/trace_processor/chrome/chrome_tasks_delaying_input_processing_test.out
new file mode 100644
index 0000000..4707368
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_tasks_delaying_input_processing_test.out
@@ -0,0 +1,5 @@
+
+"full_name","duration_ms","thread_dur_ms"
+"content.mojom.FrameHost message (hash=2168461044)",16.111000,10.451000
+"Looper.dispatch: android.net.ConnectivityManager$CallbackHandler(null)",10.507000,0.878000
+"blink.mojom.PresentationService message (hash=3202951471)",22.524000,9.992000
diff --git a/test/trace_processor/chrome/chrome_tasks_delaying_input_processing_test.sql b/test/trace_processor/chrome/chrome_tasks_delaying_input_processing_test.sql
new file mode 100644
index 0000000..d0b2a19
--- /dev/null
+++ b/test/trace_processor/chrome/chrome_tasks_delaying_input_processing_test.sql
@@ -0,0 +1,24 @@
+--
+-- Copyright 2022 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT RUN_METRIC('chrome/chrome_tasks_delaying_input_processing.sql',
+'duration_causing_jank_ms',
+ /* duration_causing_jank_ms = */ '8') AS suppress_query_output;
+
+SELECT
+  full_name,
+  duration_ms,
+  thread_dur_ms
+FROM chrome_tasks_delaying_input_processing;
\ No newline at end of file
diff --git a/test/trace_processor/chrome/index b/test/trace_processor/chrome/index
index e5ef48a..f311dac 100644
--- a/test/trace_processor/chrome/index
+++ b/test/trace_processor/chrome/index
@@ -15,6 +15,8 @@
 ../../data/chrome_scroll_without_vsync.pftrace scroll_jank_cause_queuing_delay_general_validation_test.sql scroll_jank_cause_queuing_delay_general_validation.out
 ../../data/chrome_scroll_without_vsync.pftrace chrome_thread_slice_test.sql chrome_thread_slice.out
 ../../data/scrolling_with_blocked_nonblocked_frames.pftrace chrome_input_to_browser_intervals_test.sql chrome_input_to_browser_intervals.out
+../../data/fling_with_input_delay.pftrace chrome_scroll_jank_caused_by_scheduling_test.sql chrome_scroll_jank_caused_by_scheduling_test.out
+../../data/fling_with_input_delay.pftrace chrome_tasks_delaying_input_processing_test.sql chrome_tasks_delaying_input_processing_test.out
 ../track_event/track_event_counters.textproto chrome_thread_slice_repeated_test.sql chrome_thread_slice_repeated.out
 ../../data/chrome_rendering_desktop.pftrace frame_times frame_times_metric.out
 ../../data/chrome_rendering_desktop.pftrace chrome_dropped_frames chrome_dropped_frames_metric.out
@@ -71,5 +73,8 @@
 # Chrome tasks.
 ../../data/chrome_page_load_all_categories_not_extended.pftrace.gz chrome_tasks_test.sql chrome_tasks.out
 
+# Chrome stack samples.
+../../data/chrome_stack_traces_symbolized_trace.pftrace chrome_stack_samples_for_task_test.sql chrome_stack_samples_for_task_test.out
+
 # Unsymbolized args.
 unsymbolized_args.textproto chrome_unsymbolized_args unsymbolized_args.out
diff --git a/test/trace_processor/profiling/heap_profile_data_local_tmp.textproto b/test/trace_processor/profiling/heap_profile_data_local_tmp.textproto
new file mode 100644
index 0000000..990dcbb
--- /dev/null
+++ b/test/trace_processor/profiling/heap_profile_data_local_tmp.textproto
@@ -0,0 +1,171 @@
+packet {
+  clock_snapshot {
+    primary_trace_clock: BUILTIN_CLOCK_BOOTTIME
+    clocks {
+      clock_id: 6
+      timestamp: 62009635880088
+    }
+    clocks {
+      clock_id: 4
+      timestamp: 16144710445815
+    }
+  }
+  trusted_uid: 9999
+  trusted_packet_sequence_id: 1
+}
+packet {
+  interned_data {
+    build_ids {
+      iid: 0
+      str: ""
+    }
+    mapping_paths {
+      iid: 0
+      str: ""
+    }
+    function_names {
+      iid: 0
+      str: ""
+    }
+  }
+  sequence_flags: 1
+  trusted_uid: 9999
+  trusted_packet_sequence_id: 3
+  trusted_pid: 14809
+}
+packet {
+  profile_packet {
+    index: 0
+    process_dumps {
+      heap_name: "libc.malloc"
+      sampling_interval_bytes: 1
+      orig_sampling_interval_bytes: 1
+      samples {
+        callstack_id: 5
+        self_allocated: 4096
+        self_freed: 0
+        alloc_count: 1
+        free_count: 0
+      }
+      samples {
+        callstack_id: 3
+        self_allocated: 4096
+        self_freed: 4096
+        alloc_count: 1
+        free_count: 1
+      }
+    }
+  }
+  interned_data {
+    mapping_paths {
+      iid: 3
+      str: "apex"
+    }
+    mapping_paths {
+      iid: 4
+      str: "com.android.runtime"
+    }
+    mapping_paths {
+      iid: 5
+      str: "lib64"
+    }
+    mapping_paths {
+      iid: 6
+      str: "bionic"
+    }
+    mapping_paths {
+      iid: 7
+      str: "libc.so"
+    }
+    build_ids {
+      iid: 2
+      str: "\335\234\322\001?\331\324E\033\037\217R\260\320\336\355"
+    }
+    mappings {
+      iid: 2
+      exact_offset: 249856
+      start_offset: 0
+      start: 488590266368
+      end: 488590811136
+      load_bias: 0
+      build_id: 2
+      path_string_ids: 3
+      path_string_ids: 4
+      path_string_ids: 5
+      path_string_ids: 6
+      path_string_ids: 7
+    }
+    function_names {
+      iid: 8
+      str: "malloc"
+    }
+    frames {
+      iid: 2
+      function_name_id: 8
+      mapping_id: 2
+      rel_pc: 254632
+    }
+    mapping_paths {
+      iid: 9
+      str: "data"
+    }
+    mapping_paths {
+      iid: 10
+      str: "local"
+    }
+    mapping_paths {
+      iid: 11
+      str: "tmp"
+    }
+    mapping_paths {
+      iid: 12
+      str: "mallocer"
+    }
+    build_ids {
+      iid: 1
+      str: "" # Build id for /data/local/tmp/mallocer is empty!
+    }
+    mappings {
+      iid: 3
+      exact_offset: 4096
+      start_offset: 0
+      start: 418850668544
+      end: 418850672640
+      load_bias: 0
+      build_id: 1
+      path_string_ids: 9
+      path_string_ids: 10
+      path_string_ids: 11
+      path_string_ids: 12
+    }
+    function_names {
+      iid: 1
+      str: ""
+    }
+    frames {
+      iid: 4
+      function_name_id: 1
+      mapping_id: 3
+      rel_pc: 4476
+    }
+    callstacks {
+      iid: 5
+      frame_ids: 4
+      frame_ids: 2
+    }
+    frames {
+      iid: 3
+      function_name_id: 1
+      mapping_id: 3
+      rel_pc: 4440
+    }
+    callstacks {
+      iid: 3
+      frame_ids: 3
+      frame_ids: 2
+    }
+  }
+  trusted_uid: 9999
+  trusted_packet_sequence_id: 3
+  trusted_pid: 14809
+}
diff --git a/test/trace_processor/profiling/index b/test/trace_processor/profiling/index
index c07214f..733a29c 100644
--- a/test/trace_processor/profiling/index
+++ b/test/trace_processor/profiling/index
@@ -60,3 +60,5 @@
 ../../data/heapprofd_standalone_client_example-trace stack_profile_symbols_test.sql stack_profile_symbols.out
 ../../data/callstack_sampling.pftrace callstack_sampling_flamegraph_test.sql callstack_sampling_flamegraph.out
 ../../data/callstack_sampling.pftrace callstack_sampling_flamegraph_multi_process_test.sql callstack_sampling_flamegraph_multi_process.out
+
+heap_profile_data_local_tmp.textproto no_build_id.sql no_build_id.out
diff --git a/test/trace_processor/profiling/no_build_id.out b/test/trace_processor/profiling/no_build_id.out
new file mode 100644
index 0000000..ba2fd3f
--- /dev/null
+++ b/test/trace_processor/profiling/no_build_id.out
@@ -0,0 +1,2 @@
+"value"
+1
diff --git a/test/trace_processor/profiling/no_build_id.sql b/test/trace_processor/profiling/no_build_id.sql
new file mode 100644
index 0000000..46514db
--- /dev/null
+++ b/test/trace_processor/profiling/no_build_id.sql
@@ -0,0 +1 @@
+SELECT value FROM stats WHERE name = 'symbolization_tmp_build_id_not_found'
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 8f55aa8..1ac2fe3 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -314,6 +314,10 @@
       background: #c7d0db;
     }
   }
+
+  .nested-menu {
+    padding-right: 1em;
+  }
 }
 
 .track {
diff --git a/ui/src/common/protos.ts b/ui/src/common/protos.ts
index 9af9fb9..2245f6e 100644
--- a/ui/src/common/protos.ts
+++ b/ui/src/common/protos.ts
@@ -46,7 +46,7 @@
 import IPCFrame = protos.perfetto.protos.IPCFrame;
 import IMethodInfo =
     protos.perfetto.protos.IPCFrame.BindServiceReply.IMethodInfo;
-import ITraceStats = protos.perfetto.protos.ITraceStats;
+import IBufferStats = protos.perfetto.protos.TraceStats.IBufferStats;
 import ISlice = protos.perfetto.protos.ReadBuffersResponse.ISlice;
 import EnableTracingRequest = protos.perfetto.protos.EnableTracingRequest;
 import DisableTracingRequest = protos.perfetto.protos.DisableTracingRequest;
@@ -87,13 +87,13 @@
   HeapprofdConfig,
   IAndroidPowerConfig,
   IBufferConfig,
+  IBufferStats,
   IMethodInfo,
   IPCFrame,
   IProcessStatsConfig,
   ISlice,
   ISysStatsConfig,
   ITraceConfig,
-  ITraceStats,
   JavaContinuousDumpConfig,
   JavaHprofConfig,
   MeminfoCounters,
diff --git a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts b/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
new file mode 100644
index 0000000..6128658
--- /dev/null
+++ b/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
@@ -0,0 +1,228 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {defer, Deferred} from '../../base/deferred';
+import {assertExists, assertTrue} from '../../base/logging';
+import {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {
+  ChromeExtensionMessage,
+  isChromeExtensionError,
+  isChromeExtensionStatus,
+  isGetCategoriesResponse,
+} from '../../controller/chrome_proxy_record_controller';
+import {
+  isDisableTracingResponse,
+  isEnableTracingResponse,
+  isFreeBuffersResponse,
+  isGetTraceStatsResponse,
+  isReadBuffersResponse,
+} from '../../controller/consumer_port_types';
+import {
+  EnableTracingRequest,
+  IBufferStats,
+  ISlice,
+  TraceConfig,
+} from '../protos';
+
+import {
+  BUFFER_USAGE_INCORRECT_FORMAT,
+  BUFFER_USAGE_NOT_ACCESSIBLE,
+  EXTENSION_ID,
+  MALFORMED_EXTENSION_MESSAGE,
+} from './chrome_utils';
+import {RecordingError} from './recording_error_handling';
+import {
+  TracingSession,
+  TracingSessionListener,
+} from './recording_interfaces_v2';
+
+// This class implements the protocol described in
+// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi
+// However, with the Chrome extension we communicate using JSON messages.
+export class ChromeTracedTracingSession implements TracingSession {
+  // Needed for ReadBufferResponse: all the trace packets are split into
+  // several slices. |partialPacket| is the buffer for them. Once we receive a
+  // slice with the flag |lastSliceForPacket|, a new packet is created.
+  private partialPacket: ISlice[] = [];
+
+  // For concurrent calls to 'GetCategories', we return the same value.
+  private pendingGetCategoriesMessage?: Deferred<string[]>;
+
+  private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
+
+  // Port through which we communicate with the extension.
+  private chromePort: chrome.runtime.Port;
+  // True when Perfetto is connected via the port to the tracing session.
+  private isPortConnected: boolean;
+
+  constructor(private tracingSessionListener: TracingSessionListener) {
+    this.chromePort = chrome.runtime.connect(EXTENSION_ID);
+    this.isPortConnected = true;
+  }
+
+  start(config: TraceConfig): void {
+    if (!this.isPortConnected) return;
+    const duration = config.durationMs;
+    this.tracingSessionListener.onStatus(`Recording in progress${
+        duration ? ' for ' + duration.toString() + ' ms' : ''}...`);
+
+    const enableTracingRequest = new EnableTracingRequest();
+    enableTracingRequest.traceConfig = config;
+    const enableTracingRequestProto = binaryEncode(
+        EnableTracingRequest.encode(enableTracingRequest).finish());
+    this.chromePort.postMessage(
+        {method: 'EnableTracing', requestData: enableTracingRequestProto});
+  }
+
+  // The 'cancel' method will end the tracing session and will NOT return the
+  // trace. Therefore, we do not need to keep the connection open.
+  cancel(): void {
+    if (!this.isPortConnected) return;
+    this.terminateConnection();
+  }
+
+  // The 'stop' method will end the tracing session and cause the trace to be
+  // returned via a callback. We maintain the connection to the target so we can
+  // extract the trace.
+  // See 'DisableTracing' in:
+  // https://perfetto.dev/docs/design-docs/life-of-a-tracing-session
+  stop(): void {
+    if (!this.isPortConnected) return;
+    this.chromePort.postMessage({method: 'DisableTracing'});
+  }
+
+  getCategories(): Promise<string[]> {
+    if (!this.isPortConnected) {
+      throw new RecordingError(
+          'Attempting to get categories from a ' +
+          'disconnected tracing session.');
+    }
+    if (this.pendingGetCategoriesMessage) {
+      return this.pendingGetCategoriesMessage;
+    }
+
+    this.chromePort.postMessage({method: 'GetCategories'});
+    return this.pendingGetCategoriesMessage = defer<string[]>();
+  }
+
+  async getTraceBufferUsage(): Promise<number> {
+    if (!this.isPortConnected) return 0;
+    const bufferStats = await this.getBufferStats();
+    let percentageUsed = -1;
+    for (const buffer of bufferStats) {
+      const used = assertExists(buffer.bytesWritten);
+      const total = assertExists(buffer.bufferSize);
+      if (total >= 0) {
+        percentageUsed = Math.max(percentageUsed, used / total);
+      }
+    }
+
+    if (percentageUsed === -1) {
+      throw new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT);
+    }
+    return percentageUsed;
+  }
+
+  initConnection(): void {
+    this.chromePort.onMessage.addListener((message: ChromeExtensionMessage) => {
+      this.handleExtensionMessage(message);
+    });
+  }
+
+  private getBufferStats(): Promise<IBufferStats[]> {
+    this.chromePort.postMessage({method: 'GetTraceStats'});
+
+    const statsMessage = defer<IBufferStats[]>();
+    this.pendingStatsMessages.push(statsMessage);
+    return statsMessage;
+  }
+
+  private terminateConnection(): void {
+    this.chromePort.postMessage({method: 'FreeBuffers'});
+    this.clearState();
+  }
+
+  private clearState() {
+    this.chromePort.disconnect();
+    this.isPortConnected = false;
+    for (const statsMessage of this.pendingStatsMessages) {
+      statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
+    }
+    this.pendingStatsMessages = [];
+    this.pendingGetCategoriesMessage = undefined;
+  }
+
+  private handleExtensionMessage(message: ChromeExtensionMessage) {
+    if (isChromeExtensionError(message)) {
+      this.terminateConnection();
+      this.tracingSessionListener.onError(message.error);
+    } else if (isChromeExtensionStatus(message)) {
+      this.tracingSessionListener.onStatus(message.status);
+    } else if (isReadBuffersResponse(message)) {
+      if (!message.slices) {
+        return;
+      }
+      for (const messageSlice of message.slices) {
+        // The extension sends the binary data as a string.
+        // see http://shortn/_oPmO2GT6Vb
+        if (typeof messageSlice.data !== 'string') {
+          throw new RecordingError(MALFORMED_EXTENSION_MESSAGE);
+        }
+        const decodedSlice = {
+          data: binaryDecode(messageSlice.data),
+        };
+        this.partialPacket.push(decodedSlice);
+        if (messageSlice.lastSliceForPacket) {
+          let bufferSize = 0;
+          for (const slice of this.partialPacket) {
+            bufferSize += slice.data!.length;
+          }
+
+          const completeTrace = new Uint8Array(bufferSize);
+          let written = 0;
+          for (const slice of this.partialPacket) {
+            const data = slice.data!;
+            completeTrace.set(data, written);
+            written += data.length;
+          }
+          // The trace already comes encoded as a proto.
+          this.tracingSessionListener.onTraceData(completeTrace);
+          this.terminateConnection();
+        }
+      }
+    } else if (isGetCategoriesResponse(message)) {
+      assertExists(this.pendingGetCategoriesMessage)
+          .resolve(message.categories);
+      this.pendingGetCategoriesMessage = undefined;
+    } else if (isEnableTracingResponse(message)) {
+      // Once the service notifies us that a tracing session is enabled,
+      // we can start streaming the response using 'ReadBuffers'.
+      this.chromePort.postMessage({method: 'ReadBuffers'});
+    } else if (isGetTraceStatsResponse(message)) {
+      const maybePendingStatsMessage = this.pendingStatsMessages.shift();
+      if (maybePendingStatsMessage) {
+        maybePendingStatsMessage.resolve(
+            message?.traceStats?.bufferStats || []);
+      }
+    } else if (isFreeBuffersResponse(message)) {
+      // No action required. If we successfully read a whole trace,
+      // we close the connection. Alternatively, if the tracing finishes
+      // with an exception or if the user cancels it, we also close the
+      // connection.
+    } else {
+      assertTrue(isDisableTracingResponse(message));
+      // No action required. Same reasoning as for FreeBuffers.
+    }
+  }
+}
diff --git a/ui/src/common/recordingV2/chrome_utils.ts b/ui/src/common/recordingV2/chrome_utils.ts
new file mode 100644
index 0000000..2cc4b89
--- /dev/null
+++ b/ui/src/common/recordingV2/chrome_utils.ts
@@ -0,0 +1,26 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export const EXTENSION_ID = 'lfmkphfpdbjijhpomgecfikhfohaoine';
+export const EXTENSION_URL =
+    `https://chrome.google.com/webstore/detail/perfetto-ui/${EXTENSION_ID}`;
+export const EXTENSION_NAME = 'Chrome extension';
+export const EXTENSION_NOT_INSTALLED =
+    `To trace Chrome from the Perfetto UI, you need to install our
+    ${EXTENSION_URL} and then reload this page.`;
+
+export const MALFORMED_EXTENSION_MESSAGE = 'Malformed extension message.';
+export const BUFFER_USAGE_NOT_ACCESSIBLE = 'Buffer usage not accessible';
+export const BUFFER_USAGE_INCORRECT_FORMAT =
+    'The buffer usage data has am incorrect format';
diff --git a/ui/src/common/recordingV2/recording_error_handling.ts b/ui/src/common/recordingV2/recording_error_handling.ts
index d3b1b31..e1c00ba 100644
--- a/ui/src/common/recordingV2/recording_error_handling.ts
+++ b/ui/src/common/recordingV2/recording_error_handling.ts
@@ -15,6 +15,7 @@
 import {
   showAllowUSBDebugging,
   showConnectionLostError,
+  showExtensionNotInstalled,
   showIssueParsingTheTracedResponse,
   showNoDeviceSelected,
   showWebsocketConnectionIssue,
@@ -24,6 +25,7 @@
 import {
   WEBSOCKET_UNABLE_TO_CONNECT,
 } from './adb_connection_over_websocket';
+import {EXTENSION_NOT_INSTALLED} from './chrome_utils';
 import {OnMessageCallback} from './recording_interfaces_v2';
 import {
   PARSING_UNABLE_TO_DECODE_METHOD,
@@ -89,6 +91,8 @@
     showNoDeviceSelected();
   } else if (WEBSOCKET_UNABLE_TO_CONNECT === message) {
     showWebsocketConnectionIssue(message);
+  } else if (message === EXTENSION_NOT_INSTALLED) {
+    showExtensionNotInstalled();
   } else if (isParsingError(message)) {
     showIssueParsingTheTracedResponse(message);
   } else {
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts b/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
new file mode 100644
index 0000000..e012bcf
--- /dev/null
+++ b/ui/src/common/recordingV2/target_factories/chrome_target_factory.ts
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {EXTENSION_NOT_INSTALLED} from '../chrome_utils';
+import {RecordingError} from '../recording_error_handling';
+import {
+  OnTargetChangeCallback,
+  RecordingTargetV2,
+  TargetFactory,
+} from '../recording_interfaces_v2';
+import {targetFactoryRegistry} from '../target_factory_registry';
+import {ChromeTarget} from '../targets/chrome_target';
+
+const CHROME_TARGET_FACTORY = 'ChromeTargetFactory';
+
+// Sample user agent for Chrome on Chrome OS:
+// "Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) AppleWebKit/537.36
+// (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36"
+// This condition is wider, in the unlikely possibility of different casing,
+export function isCrOS(userAgent: string) {
+  return userAgent.toLowerCase().includes(' cros ');
+}
+
+export class ChromeTargetFactory implements TargetFactory {
+  readonly kind = CHROME_TARGET_FACTORY;
+  private targets: ChromeTarget[];
+
+  constructor() {
+    this.targets = [new ChromeTarget('Chrome', 'CHROME')];
+    if (isCrOS(navigator.userAgent)) {
+      this.targets.push(new ChromeTarget('Chrome', 'CHROME_OS'));
+    }
+  }
+
+  connectNewTarget(): Promise<RecordingTargetV2> {
+    throw new RecordingError(
+        'Can not create a new Chrome target.' +
+        'All Chrome targets are created at factory initialisation.');
+  }
+
+  getName(): string {
+    return 'Chrome';
+  }
+
+  listRecordingProblems(): string[] {
+    const recordingProblems = [];
+    if (!this.targets[0].getInfo().isExtensionInstalled) {
+      recordingProblems.push(EXTENSION_NOT_INSTALLED);
+    }
+    return recordingProblems;
+  }
+
+  listTargets(): RecordingTargetV2[] {
+    return this.targets;
+  }
+
+  setOnTargetChange(onTargetChange: OnTargetChangeCallback): void {
+    for (const target of this.targets) {
+      target.onTargetChange = onTargetChange;
+    }
+  }
+}
+
+// We only instantiate the factory if Perfetto UI is open in the Chrome browser.
+if (window.chrome && chrome.runtime) {
+  targetFactoryRegistry.register(new ChromeTargetFactory());
+}
diff --git a/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts b/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
new file mode 100644
index 0000000..478a3f4
--- /dev/null
+++ b/ui/src/common/recordingV2/target_factories/chrome_target_factory_unittest.ts
@@ -0,0 +1,28 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {isCrOS} from './chrome_target_factory';
+
+test('parse Chrome on Chrome OS user agent', () => {
+  const userAgent = 'Mozilla/5.0 (X11; CrOS x86_64 14816.99.0) ' +
+      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 ' +
+      'Safari/537.36';
+  expect(isCrOS(userAgent)).toBe(true);
+});
+
+test('parse Chrome on Mac user agent', () => {
+  const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
+      'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
+  expect(isCrOS(userAgent)).toBe(false);
+});
diff --git a/ui/src/common/recordingV2/target_factories/index.ts b/ui/src/common/recordingV2/target_factories/index.ts
index 17afaae..da8b328 100644
--- a/ui/src/common/recordingV2/target_factories/index.ts
+++ b/ui/src/common/recordingV2/target_factories/index.ts
@@ -14,3 +14,4 @@
 
 import './android_webusb_target_factory';
 import './android_websocket_target_factory';
+import './chrome_target_factory';
diff --git a/ui/src/common/recordingV2/targets/chrome_target.ts b/ui/src/common/recordingV2/targets/chrome_target.ts
new file mode 100644
index 0000000..39b13eb
--- /dev/null
+++ b/ui/src/common/recordingV2/targets/chrome_target.ts
@@ -0,0 +1,86 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {ChromeTracedTracingSession} from '../chrome_traced_tracing_session';
+import {EXTENSION_ID} from '../chrome_utils';
+import {
+  ChromeTargetInfo,
+  OnTargetChangeCallback,
+  RecordingTargetV2,
+  TracingSession,
+  TracingSessionListener,
+} from '../recording_interfaces_v2';
+
+export class ChromeTarget implements RecordingTargetV2 {
+  onTargetChange?: OnTargetChangeCallback;
+  private chromeCategories?: string[];
+  // We only check the connection once at the beginning to:
+  // a) Avoid creating a 'Port' object every time 'getInfo' is called.
+  // b) When a new Port is created, the extension starts communicating with it
+  // and leaves aside the old Port objects, so creating a new Port would break
+  // any ongoing tracing session.
+  private isExtensionInstalled: boolean;
+
+  constructor(private name: string, private targetType: 'CHROME'|'CHROME_OS') {
+    const testPort = chrome.runtime.connect(EXTENSION_ID);
+    this.isExtensionInstalled = !!testPort;
+    testPort.disconnect();
+  }
+
+  getInfo(): ChromeTargetInfo {
+    return {
+      targetType: this.targetType,
+      name: this.name,
+      isExtensionInstalled: this.isExtensionInstalled,
+      dataSources:
+          [{name: 'chromeCategories', descriptor: this.chromeCategories}],
+    };
+  }
+
+  async createTracingSession(tracingSessionListener: TracingSessionListener):
+      Promise<TracingSession> {
+    const tracingSession =
+        new ChromeTracedTracingSession(tracingSessionListener);
+    tracingSession.initConnection();
+
+    if (!this.chromeCategories) {
+      // Fetch chrome categories from the extension.
+      this.chromeCategories = await tracingSession.getCategories();
+      if (this.onTargetChange) {
+        this.onTargetChange();
+      }
+    }
+
+    return tracingSession;
+  }
+
+  // Starts a tracing session in order to fetch chrome categories from the
+  // device. Then, it cancels the session.
+  async fetchTargetInfo(tracingSessionListener: TracingSessionListener):
+      Promise<void> {
+    const tracingSession =
+        await this.createTracingSession(tracingSessionListener);
+    tracingSession.cancel();
+  }
+
+  disconnect(_disconnectMessage?: string): Promise<void> {
+    return Promise.resolve(undefined);
+  }
+
+  // We can connect to the Chrome target without taking the connection away
+  // from another process.
+  async canConnectWithoutContention(): Promise<boolean> {
+    return true;
+  }
+}
diff --git a/ui/src/common/recordingV2/traced_tracing_session.ts b/ui/src/common/recordingV2/traced_tracing_session.ts
index e110ddb..2257f85 100644
--- a/ui/src/common/recordingV2/traced_tracing_session.ts
+++ b/ui/src/common/recordingV2/traced_tracing_session.ts
@@ -25,15 +25,19 @@
   FreeBuffersResponse,
   GetTraceStatsRequest,
   GetTraceStatsResponse,
+  IBufferStats,
   IMethodInfo,
   IPCFrame,
   ISlice,
-  ITraceStats,
   ReadBuffersRequest,
   ReadBuffersResponse,
   TraceConfig,
 } from '../protos';
 
+import {
+  BUFFER_USAGE_INCORRECT_FORMAT,
+  BUFFER_USAGE_NOT_ACCESSIBLE,
+} from './chrome_utils';
 import {RecordingError} from './recording_error_handling';
 import {
   ByteStream,
@@ -87,7 +91,7 @@
   // to keep track of the type of request, and parse the response correctly.
   private requestId = 1;
 
-  private pendingStatsMessages = new Array<Deferred<ITraceStats>>();
+  private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>();
 
   // The bytestream is obtained when creating a connection with a target.
   // For instance, the AdbStream is obtained from a connection with an Adb
@@ -125,15 +129,9 @@
     if (!this.byteStream.isOpen()) {
       return 0;
     }
-    const traceStats = await this.getTraceStats();
-    if (!traceStats.bufferStats) {
-      // // If a buffer stats is pending and we finish tracing, it will be
-      // resolved as {} when closing the connection. In that case just ignore it
-      // rather than erroring.
-      return 0;
-    }
-    let percentageUsed = 0;
-    for (const buffer of assertExists(traceStats.bufferStats)) {
+    const bufferStats = await this.getBufferStats();
+    let percentageUsed = -1;
+    for (const buffer of bufferStats) {
       if (!Number.isFinite(buffer.bytesWritten) ||
           !Number.isFinite(buffer.bufferSize)) {
         continue;
@@ -144,6 +142,10 @@
         percentageUsed = Math.max(percentageUsed, used / total);
       }
     }
+
+    if (percentageUsed === -1) {
+      return Promise.reject(new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT));
+    }
     return percentageUsed;
   }
 
@@ -163,7 +165,7 @@
     return this.resolveBindingPromise;
   }
 
-  private getTraceStats(): Promise<ITraceStats> {
+  private getBufferStats(): Promise<IBufferStats[]> {
     const getTraceStatsRequestProto =
         GetTraceStatsRequest.encode(new GetTraceStatsRequest()).finish();
     try {
@@ -173,7 +175,7 @@
       this.raiseError(e);
     }
 
-    const statsMessage = defer<ITraceStats>();
+    const statsMessage = defer<IBufferStats[]>();
     this.pendingStatsMessages.push(statsMessage);
     return statsMessage;
   }
@@ -188,12 +190,7 @@
 
   private clearState() {
     for (const statsMessage of this.pendingStatsMessages) {
-      // Resolving with an empty object instead of rejecting because a rejection
-      // would trigger an 'unhandledRejection' event, causing the application
-      // to show an error modal in the UI, which is not necessary for stats
-      // messages because that would mean the error modal is shown on every
-      // successful recording.
-      statsMessage.resolve({});
+      statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE));
     }
     this.pendingStatsMessages = [];
   }
@@ -345,7 +342,7 @@
       } else if (method === 'GetTraceStats') {
         const maybePendingStatsMessage = this.pendingStatsMessages.shift();
         if (maybePendingStatsMessage) {
-          maybePendingStatsMessage.resolve(data.traceStats || {});
+          maybePendingStatsMessage.resolve(data?.traceStats?.bufferStats || []);
         }
       } else if (method === 'FreeBuffers') {
         // No action required. If we successfully read a whole trace,
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/controller/chrome_proxy_record_controller.ts
index 2c6571e..1444478 100644
--- a/ui/src/controller/chrome_proxy_record_controller.ts
+++ b/ui/src/controller/chrome_proxy_record_controller.ts
@@ -38,11 +38,13 @@
 export type ChromeExtensionMessage = ChromeExtensionError|ChromeExtensionStatus|
     ConsumerPortResponse|GetCategoriesResponse;
 
-function isError(obj: Typed): obj is ChromeExtensionError {
+export function isChromeExtensionError(obj: Typed):
+    obj is ChromeExtensionError {
   return obj.type === 'ChromeExtensionError';
 }
 
-function isStatus(obj: Typed): obj is ChromeExtensionStatus {
+export function isChromeExtensionStatus(obj: Typed):
+    obj is ChromeExtensionStatus {
   return obj.type === 'ChromeExtensionStatus';
 }
 
@@ -80,11 +82,11 @@
   }
 
   onExtensionMessage(message: {data: ChromeExtensionMessage}) {
-    if (isError(message.data)) {
+    if (isChromeExtensionError(message.data)) {
       this.sendErrorMessage(message.data.error);
       return;
     }
-    if (isStatus(message.data)) {
+    if (isChromeExtensionStatus(message.data)) {
       this.sendStatus(message.data.status);
       return;
     }
diff --git a/ui/src/frontend/chrome_slice_panel.ts b/ui/src/frontend/chrome_slice_panel.ts
index 387e68b..5273098 100644
--- a/ui/src/frontend/chrome_slice_panel.ts
+++ b/ui/src/frontend/chrome_slice_panel.ts
@@ -339,12 +339,14 @@
     const fullKey = argument.full_key;
     return [
       {
+        itemType: 'regular',
         text: 'Copy full key',
         callback: () => {
           navigator.clipboard.writeText(fullKey);
         },
       },
       {
+        itemType: 'regular',
         text: 'Find slices with the same arg value',
         callback: () => {
           globals.dispatch(Actions.executeQuery({
@@ -360,6 +362,7 @@
         },
       },
       {
+        itemType: 'regular',
         text: 'Visualise argument values',
         callback: () => {
           globals.dispatch(Actions.addVisualisedArg({argName: fullKey}));
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index b2e20af..b3813df 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -16,6 +16,7 @@
 
 import {assertExists} from '../base/logging';
 import {RECORDING_V2_FLAG} from '../common/feature_flags';
+import {EXTENSION_URL} from '../common/recordingV2/chrome_utils';
 import {TraceUrlSource} from '../common/state';
 import {saveTrace} from '../common/upload_utils';
 
@@ -333,6 +334,20 @@
   });
 }
 
+export function showExtensionNotInstalled(): void {
+  showModal({
+    title: 'Perfetto Chrome extension not installed',
+    content:
+        m('div',
+          m('.note',
+            `To trace Chrome from the Perfetto UI, you need to install our `,
+            m('a', {href: EXTENSION_URL, target: '_blank'}, 'Chrome extension'),
+            ' and then reload this page.'),
+          m('br')),
+    buttons: [],
+  });
+}
+
 export function showWebsocketConnectionIssue(message: string): void {
   showModal({
     title: 'Unable to connect to the device via websocket',
diff --git a/ui/src/frontend/pivot_table_redux.ts b/ui/src/frontend/pivot_table_redux.ts
index b4ba46b..8d71712 100644
--- a/ui/src/frontend/pivot_table_redux.ts
+++ b/ui/src/frontend/pivot_table_redux.ts
@@ -17,7 +17,7 @@
 import * as m from 'mithril';
 
 import {sqliteString} from '../base/string_utils';
-import {Actions, DeferredAction} from '../common/actions';
+import {Actions} from '../common/actions';
 import {COUNT_AGGREGATION} from '../common/empty_state';
 import {ColumnType} from '../common/query_result';
 import {
@@ -44,7 +44,6 @@
   generateQuery,
   QueryGeneratorError,
   sliceAggregationColumns,
-  Table,
   tableColumnEquals,
   tables,
   threadSliceAggregationColumns,
@@ -52,7 +51,6 @@
 import {
   Aggregation,
   AggregationFunction,
-  aggregationKey,
   columnKey,
   PivotTree,
   TableColumn,
@@ -65,49 +63,6 @@
   nextKey: ColumnType;
 }
 
-// Arguments to an action to toggle a table column in a particular part of
-// application's state.
-interface ColumnSetArgs<T> {
-  column: T;
-  selected: boolean;
-}
-
-interface ColumnSetCheckboxAttrs<T> {
-  set: (args: ColumnSetArgs<T>) => DeferredAction<ColumnSetArgs<T>>;
-  get: Map<string, T>;
-  setKey: T;
-}
-
-abstract class GenericCheckbox<T> implements
-    m.ClassComponent<ColumnSetCheckboxAttrs<T>> {
-  abstract keyFn(value: T): string;
-
-  view({attrs}: m.Vnode<ColumnSetCheckboxAttrs<T>>) {
-    return m('input[type=checkbox]', {
-      onclick: (e: InputEvent) => {
-        const target = e.target as HTMLInputElement;
-
-        globals.dispatch(
-            attrs.set({column: attrs.setKey, selected: target.checked}));
-        globals.rafScheduler.scheduleFullRedraw();
-      },
-      checked: attrs.get.has(this.keyFn(attrs.setKey)),
-    });
-  }
-}
-
-class ColumnSetCheckbox extends GenericCheckbox<TableColumn> {
-  keyFn(value: TableColumn): string {
-    return columnKey(value);
-  }
-}
-
-class AggregationCheckbox extends GenericCheckbox<Aggregation> {
-  keyFn(value: Aggregation): string {
-    return aggregationKey(value);
-  }
-}
-
 interface PivotTableReduxAttrs {
   selectionArea: PivotTableReduxAreaState;
 }
@@ -167,22 +122,6 @@
         this.constrainToArea);
   }
 
-  renderTablePivotColumns(t: Table) {
-    return m(
-        'li',
-        t.name,
-        m('ul',
-          t.columns.map(
-              (col) =>
-                  m('li',
-                    m(ColumnSetCheckbox, {
-                      get: this.selectedPivotsMap,
-                      set: Actions.setPivotTablePivotSelected,
-                      setKey: {kind: 'regular', table: t.name, column: col},
-                    }),
-                    col))));
-  }
-
   renderResultsView(attrs: PivotTableReduxAttrs) {
     return m(
         '.pivot-table-redux',
@@ -338,11 +277,9 @@
   }
 
   sortingItem(column: TableColumn, order: SortDirection): PopupMenuItem {
-    // Arrow contains unicode character for up or down arrow, according to the
-    // direction of sorting.
-    const arrow = order === 'DESC' ? '\u25BC' : '\u25B2';
     return {
-      text: `Sort ${arrow}`,
+      itemType: 'regular',
+      text: order === 'DESC' ? 'Highest first' : 'Lowest first',
       callback() {
         globals.dispatch(Actions.setPivotTableSortColumn({column, order}));
         globals.dispatch(
@@ -359,6 +296,51 @@
         readableColumnName(aggregation.column)})`;
   }
 
+  aggregationPopupItem(aggregation: Aggregation, nameOverride?: string):
+      PopupMenuItem {
+    return {
+      itemType: 'regular',
+      text: nameOverride ?? readableColumnName(aggregation.column),
+      callback: () => {
+        globals.dispatch(Actions.setPivotTableAggregationSelected({
+          column: {
+            aggregationFunction: aggregation.aggregationFunction,
+            column: aggregation.column,
+          },
+          selected: true,
+        }));
+        globals.dispatch(
+            Actions.setPivotTableQueryRequested({queryRequested: true}));
+      },
+    };
+  }
+
+  aggregationPopupTableGroup(
+      table: string, columns: string[], used: Set<string>): PopupMenuItem
+      |undefined {
+    const items = [];
+    for (const column of columns) {
+      const tableColumn: TableColumn = {kind: 'regular', table, column};
+      if (used.has(columnKey(tableColumn))) {
+        continue;
+      }
+
+      items.push(this.aggregationPopupItem(
+          {aggregationFunction: 'SUM', column: tableColumn}));
+    }
+
+    if (items.length === 0) {
+      return undefined;
+    }
+
+    return {
+      itemType: 'group',
+      itemId: `aggregations-${table}`,
+      text: `Add ${table} aggregation`,
+      children: items,
+    };
+  }
+
   renderAggregationHeaderCell(aggregation: Aggregation): m.Child {
     const column = aggregation.column;
     const popupItems: PopupMenuItem[] = [];
@@ -384,6 +366,7 @@
         }
 
         popupItems.push({
+          itemType: 'regular',
           text: otherAgg,
           callback() {
             globals.dispatch(Actions.setPivotTableAggregationSelected(
@@ -400,6 +383,35 @@
       }
     }
 
+    const usedAggregations: Set<string> = new Set();
+    let hasCount = false;
+
+    for (const agg of state.selectedAggregations.values()) {
+      if (agg.aggregationFunction === 'COUNT') {
+        hasCount = true;
+        continue;
+      }
+
+      usedAggregations.add(columnKey(agg.column));
+    }
+
+    if (!hasCount) {
+      popupItems.push(this.aggregationPopupItem(
+          COUNT_AGGREGATION, 'Add count aggregation'));
+    }
+
+    const sliceAggregationsItem = this.aggregationPopupTableGroup(
+        'slice', sliceAggregationColumns, usedAggregations);
+    if (sliceAggregationsItem !== undefined) {
+      popupItems.push(sliceAggregationsItem);
+    }
+
+    const threadSliceAggregationsItem = this.aggregationPopupTableGroup(
+        'thread_slice', threadSliceAggregationColumns, usedAggregations);
+    if (threadSliceAggregationsItem !== undefined) {
+      popupItems.push(threadSliceAggregationsItem);
+    }
+
     return m(
         'td', this.readableAggregationName(aggregation), m(PopupMenuButton, {
           icon,
@@ -459,7 +471,8 @@
 
     const pivotTableHeaders = [];
     for (const pivot of state.queryResult.metadata.pivotColumns) {
-      const items = [{
+      const items: PopupMenuItem[] = [{
+        itemType: 'regular',
         text: 'Add argument pivot',
         callback: () => {
           this.showModal = true;
@@ -469,6 +482,7 @@
       }];
       if (state.queryResult.metadata.pivotColumns.length > 1) {
         items.push({
+          itemType: 'regular',
           text: 'Remove',
           callback() {
             globals.dispatch(Actions.setPivotTablePivotSelected(
@@ -478,6 +492,38 @@
           },
         });
       }
+
+      for (const table of tables) {
+        const group: PopupMenuItem[] = [];
+        for (const columnName of table.columns) {
+          const column: TableColumn = {
+            kind: 'regular',
+            table: table.name,
+            column: columnName,
+          };
+          if (this.selectedPivotsMap.has(columnKey(column))) {
+            continue;
+          }
+
+          group.push({
+            itemType: 'regular',
+            text: columnName,
+            callback() {
+              globals.dispatch(
+                  Actions.setPivotTablePivotSelected({column, selected: true}));
+              globals.dispatch(
+                  Actions.setPivotTableQueryRequested({queryRequested: true}));
+            },
+          });
+        }
+        items.push({
+          itemType: 'group',
+          itemId: `pivot-${table.name}`,
+          text: `Add ${table.name} pivot`,
+          children: group,
+        });
+      }
+
       pivotTableHeaders.push(
           m('td',
             readableColumnName(pivot),
@@ -501,6 +547,7 @@
             m('td.menu', m(PopupMenuButton, {
                 icon: 'menu',
                 items: [{
+                  itemType: 'regular',
                   text: 'Edit mode',
                   callback: () => {
                     globals.dispatch(
@@ -568,47 +615,6 @@
   renderEditView(attrs: PivotTableReduxAttrs) {
     return m(
         '.pivot-table-redux.edit',
-        m('div',
-          m('h2', 'Pivots'),
-          m('ul',
-            tables.map(
-                (t) => this.renderTablePivotColumns(t),
-                ))),
-        m('div',
-          m('h2', 'Aggregations'),
-          m('ul',
-            m('li',
-              m(AggregationCheckbox, {
-                get: this.selectedAggregations,
-                set: Actions.setPivotTableAggregationSelected,
-                setKey: COUNT_AGGREGATION,
-              }),
-              'count'),
-            ...sliceAggregationColumns.map(
-                (t) =>
-                    m('li',
-                      m(AggregationCheckbox, {
-                        get: this.selectedAggregations,
-                        set: Actions.setPivotTableAggregationSelected,
-                        setKey: {
-                          aggregationFunction: 'SUM',
-                          column: {kind: 'regular', table: 'slice', column: t},
-                        },
-                      }),
-                      t)),
-            ...threadSliceAggregationColumns.map(
-                (t) => m(
-                    'li',
-                    m(AggregationCheckbox, {
-                      get: this.selectedAggregations,
-                      set: Actions.setPivotTableAggregationSelected,
-                      setKey: {
-                        aggregationFunction: 'SUM',
-                        column:
-                            {kind: 'regular', table: 'thread_slice', column: t},
-                      },
-                    }),
-                    `thread_slice.${t}`)))),
         this.renderQuery(attrs));
   }
 }
diff --git a/ui/src/frontend/popup_menu.ts b/ui/src/frontend/popup_menu.ts
index 2111d9d..863ddd1 100644
--- a/ui/src/frontend/popup_menu.ts
+++ b/ui/src/frontend/popup_menu.ts
@@ -15,13 +15,23 @@
 import * as m from 'mithril';
 import {globals} from './globals';
 
-export interface PopupMenuItem {
+export interface RegularPopupMenuItem {
+  itemType: 'regular';
   // Display text
   text: string;
   // Action on menu item click
   callback: () => void;
 }
 
+export interface GroupPopupMenuItem {
+  itemType: 'group';
+  text: string;
+  itemId: string;
+  children: PopupMenuItem[];
+}
+
+export type PopupMenuItem = RegularPopupMenuItem|GroupPopupMenuItem;
+
 interface PopupMenuButtonAttrs {
   // Icon for button opening a menu
   icon: string;
@@ -83,6 +93,7 @@
 // Component that displays a button that shows a popup menu on click.
 export class PopupMenuButton implements m.ClassComponent<PopupMenuButtonAttrs> {
   popupShown = false;
+  expandedGroups: Set<string> = new Set();
 
   setVisible(visible: boolean) {
     this.popupShown = visible;
@@ -94,6 +105,42 @@
     globals.rafScheduler.scheduleFullRedraw();
   }
 
+  renderItem(item: PopupMenuItem): m.Child {
+    switch (item.itemType) {
+      case 'regular':
+        return m(
+            'button.open-menu',
+            {
+              onclick: () => {
+                item.callback();
+                // Hide the menu item after the action has been invoked
+                this.setVisible(false);
+              },
+            },
+            item.text);
+      case 'group':
+        const isExpanded = this.expandedGroups.has(item.itemId);
+        return m(
+            'div',
+            m('button.open-menu.disallow-selection',
+              {
+                onclick: () => {
+                  if (this.expandedGroups.has(item.itemId)) {
+                    this.expandedGroups.delete(item.itemId);
+                  } else {
+                    this.expandedGroups.add(item.itemId);
+                  }
+                  globals.rafScheduler.scheduleFullRedraw();
+                },
+              },
+              // Show text with up/down arrow, depending on expanded state.
+              item.text + (isExpanded ? ' \u25B2' : ' \u25BC')),
+            isExpanded ? m('div.nested-menu',
+                           item.children.map((item) => this.renderItem(item))) :
+                         null);
+    }
+  }
+
   view(vnode: m.Vnode<PopupMenuButtonAttrs, this>) {
     return m(
         '.dropdown',
@@ -105,16 +152,6 @@
           },
           vnode.attrs.icon),
         m(this.popupShown ? '.popup-menu.opened' : '.popup-menu.closed',
-          vnode.attrs.items.map(
-              (item) =>
-                  m('button.open-menu',
-                    {
-                      onclick: () => {
-                        item.callback();
-                        // Hide the menu item after the action has been invoked
-                        this.setVisible(false);
-                      },
-                    },
-                    item.text))));
+          vnode.attrs.items.map((item) => this.renderItem(item))));
   }
 }
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
index e85b2a5..6474ab8 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/frontend/record_page_v2.ts
@@ -20,6 +20,12 @@
 import {TRACE_SUFFIX} from '../common/constants';
 import {TraceConfig} from '../common/protos';
 import {
+  BUFFER_USAGE_INCORRECT_FORMAT,
+  BUFFER_USAGE_NOT_ACCESSIBLE,
+  EXTENSION_NAME,
+  EXTENSION_URL,
+} from '../common/recordingV2/chrome_utils';
+import {
   genTraceConfig,
   RecordingConfigUtils,
 } from '../common/recordingV2/recording_config_utils';
@@ -28,6 +34,7 @@
   showRecordingModal,
 } from '../common/recordingV2/recording_error_handling';
 import {
+  ChromeTargetInfo,
   OnTargetChangeCallback,
   RecordingTargetV2,
   TargetInfo,
@@ -64,6 +71,7 @@
 import {CodeSnippet} from './record_widgets';
 import {AdvancedSettings} from './recording/advanced_settings';
 import {AndroidSettings} from './recording/android_settings';
+import {ChromeSettings} from './recording/chrome_settings';
 import {CpuSettings} from './recording/cpu_settings';
 import {GpuSettings} from './recording/gpu_settings';
 import {MemorySettings} from './recording/memory_settings';
@@ -122,9 +130,9 @@
     this.tracingSession.stop();
   }
 
-  getTraceBufferUsage(): Promise<number>|undefined {
+  getTraceBufferUsage(): Promise<number> {
     if (!this.tracingSession) {
-      return undefined;
+      throw new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE);
     }
     return this.tracingSession.getTraceBufferUsage();
   }
@@ -180,6 +188,11 @@
   },
 };
 
+function isChromeTargetInfo(targetInfo: TargetInfo):
+    targetInfo is ChromeTargetInfo {
+  return ['CHROME', 'CHROME_OS'].includes(targetInfo.targetType);
+}
+
 function RecordHeader() {
   const platformSelection = RecordingPlatformSelection();
   const statusLabel = RecordingStatusLabel();
@@ -296,15 +309,29 @@
       m('.buttons', StopCancelButtons()));
 }
 
-function BufferUsageProgressBar() {
-  const bufferUsagePromise = tracingSessionWrapper?.getTraceBufferUsage();
-  if (!bufferUsagePromise) {
-    return undefined;
-  }
+async function fetchBufferUsage() {
+  if (!tracingSessionWrapper) return;
 
-  bufferUsagePromise.then((percentage) => {
+  try {
+    const percentage = await tracingSessionWrapper.getTraceBufferUsage();
     publishBufferUsage({percentage});
-  });
+  } catch (e) {
+    if (e instanceof RecordingError) {
+      if (e.message === BUFFER_USAGE_INCORRECT_FORMAT) {
+        // If we have received an incorrectly formatted message, we will
+        // redraw, so we can query the buffer usage again.
+        globals.rafScheduler.scheduleFullRedraw();
+      }
+      // We ignore other possible tracing buffer message errors because they
+      // are not necessary for the trace to be successfully collected.
+    } else {
+      throw e;
+    }
+  }
+}
+
+function BufferUsageProgressBar() {
+  fetchBufferUsage();
 
   const bufferUsage = globals.bufferUsage ? globals.bufferUsage : 0.0;
   // Buffer usage is not available yet on Android.
@@ -322,8 +349,6 @@
   const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
   const cmdlineUrl =
       'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline';
-  const extensionURL = `https://chrome.google.com/webstore/detail/
-      perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`;
 
   const notes: m.Children = [];
 
@@ -342,12 +367,6 @@
           `sideload the latest version of
          Perfetto.`));
 
-  const msgChrome =
-      m('.note',
-        `To trace Chrome from the Perfetto UI, you need to install our `,
-        m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'),
-        ' and then reload this page.');
-
   const msgLinux =
       m('.note',
         `Use this `,
@@ -372,17 +391,21 @@
   }
 
   targetFactoryRegistry.listRecordingProblems().map((recordingProblem) => {
-    notes.push(m('.note', recordingProblem));
+    if (recordingProblem.includes(EXTENSION_URL)) {
+      // Special case for rendering the link to the Chrome extension.
+      const parts = recordingProblem.split(EXTENSION_URL);
+      notes.push(
+          m('.note',
+            parts[0],
+            m('a', {href: EXTENSION_URL, target: '_blank'}, EXTENSION_NAME),
+            parts[1]));
+    }
   });
 
   if (recordingTargetV2) {
     const targetInfo = recordingTargetV2.getInfo();
 
     switch (targetInfo.targetType) {
-      case 'CHROME':
-      case 'CHROME_OS':
-        if (!globals.state.extensionInstalled) notes.push(msgChrome);
-        break;
       case 'LINUX':
         notes.push(msgLinux);
         break;
@@ -409,8 +432,12 @@
 function RecordingSnippet() {
   const targetInfo = assertExists(recordingTargetV2).getInfo();
   // We don't need commands to start tracing on chrome
-  if (targetInfo.targetType === 'CHROME') {
-    if (!globals.state.extensionInstalled) return undefined;
+  if (isChromeTargetInfo(targetInfo)) {
+    if (tracingSessionWrapper) {
+      // If the UI has started tracing, don't display a message guiding the user
+      // to start recording.
+      return undefined;
+    }
     return m(
         'div',
         m('label', `To trace Chrome from the Perfetto UI you just have to press
@@ -482,7 +509,8 @@
   if (targetType === 'ANDROID' &&
       globals.state.recordConfig.mode !== 'LONG_TRACE') {
     buttons.push(start);
-  } else if (targetType === 'CHROME' && globals.state.extensionInstalled) {
+  } else if (
+      isChromeTargetInfo(targetInfo) && targetInfo.isExtensionInstalled) {
     buttons.push(start);
   }
   return m('.button', buttons);
@@ -521,14 +549,10 @@
 
   const target = assertExists(recordingTargetV2);
   const targetInfo = target.getInfo();
-  if (targetInfo.targetType === 'ANDROID' ||
-      targetInfo.targetType === 'CHROME') {
-    globals.logging.logEvent(
-        'Record Trace',
-        `Record trace (${targetInfo.targetType}${targetInfo.targetType})`);
-    const traceConfig = genTraceConfig(globals.state.recordConfig, targetInfo);
-    tracingSessionWrapper = new TracingSessionWrapper(traceConfig, target);
-  }
+  globals.logging.logEvent(
+      'Record Trace', `Record trace (${targetInfo.targetType})`);
+  const traceConfig = genTraceConfig(globals.state.recordConfig, targetInfo);
+  tracingSessionWrapper = new TracingSessionWrapper(traceConfig, target);
   globals.rafScheduler.scheduleFullRedraw();
 }
 
@@ -539,6 +563,12 @@
 }
 
 function recordMenu(routePage: string) {
+  const chromeProbe =
+      m('a[href="#!/record/chrome"]',
+        m(`li${routePage === 'chrome' ? '.active' : ''}`,
+          m('i.material-icons', 'laptop_chromebook'),
+          m('.title', 'Chrome'),
+          m('.sub', 'Chrome traces')));
   const cpuProbe =
       m('a[href="#!/record/cpu"]',
         m(`li${routePage === 'cpu' ? '.active' : ''}`,
@@ -579,7 +609,9 @@
   const targetType = assertExists(recordingTargetV2).getInfo().targetType;
   const probes = [];
   if (targetType === 'CHROME_OS' || targetType === 'LINUX') {
-    probes.push(cpuProbe, powerProbe, memoryProbe, advancedProbe);
+    probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
+  } else if (targetType === 'CHROME') {
+    probes.push(chromeProbe);
   } else {
     probes.push(
         cpuProbe,
@@ -587,6 +619,7 @@
         powerProbe,
         memoryProbe,
         androidProbe,
+        chromeProbe,
         advancedProbe);
   }
 
@@ -694,12 +727,12 @@
     ['power', PowerSettings],
     ['memory', MemorySettings],
     ['android', AndroidSettings],
+    ['chrome', ChromeSettings],
     ['advanced', AdvancedSettings],
-    // TODO(octaviant): Add Chrome settings.
   ]);
   for (const [section, component] of settingsSections.entries()) {
     pages.push(m(component, {
-      dataSources: [],
+      dataSources: targetInfo.dataSources,
       cssClass: maybeGetActiveCss(routePage, section),
     } as RecordingSectionAttrs));
   }
diff --git a/ui/src/frontend/recording/chrome_settings.ts b/ui/src/frontend/recording/chrome_settings.ts
index 3dbd2a2..09b729e 100644
--- a/ui/src/frontend/recording/chrome_settings.ts
+++ b/ui/src/frontend/recording/chrome_settings.ts
@@ -14,18 +14,31 @@
 
 import * as m from 'mithril';
 
+import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
 import {getBuiltinChromeCategoryList, isChromeTarget} from '../../common/state';
 import {globals} from '../globals';
 import {CategoriesCheckboxList, CompactProbe} from '../record_widgets';
 
 import {RecordingSectionAttrs} from './recording_sections';
 
-class ChromeCategoriesSelection implements m.ClassComponent {
-  view() {
+function extractChromeCategories(dataSources: DataSource[]): string[]|
+    undefined {
+  for (const dataSource of dataSources) {
+    if (dataSource.name === 'chromeCategories') {
+      return dataSource.descriptor as string[];
+    }
+  }
+  return undefined;
+}
+
+class ChromeCategoriesSelection implements
+    m.ClassComponent<RecordingSectionAttrs> {
+  view({attrs}: m.CVnode<RecordingSectionAttrs>) {
     // If we are attempting to record via the Chrome extension, we receive the
     // list of actually supported categories via DevTools. Otherwise, we fall
     // back to an integrated list of categories from a recent version of Chrome.
-    let categories = globals.state.chromeCategories;
+    let categories = globals.state.chromeCategories ||
+        extractChromeCategories(attrs.dataSources);
     if (!categories || !isChromeTarget(globals.state.recordingTarget)) {
       categories = getBuiltinChromeCategoryList();
     }
@@ -102,6 +115,6 @@
           setEnabled: (cfg, val) => cfg.chromeLogs = val,
           isEnabled: (cfg) => cfg.chromeLogs,
         }),
-        m(ChromeCategoriesSelection));
+        m(ChromeCategoriesSelection, attrs));
   }
 }