Merge "[Tracing] Add proto configuration for the ETW proto." into main
diff --git a/Android.bp b/Android.bp
index c2a5340..3b4f823 100644
--- a/Android.bp
+++ b/Android.bp
@@ -14690,7 +14690,7 @@
         host: {
             static_libs: [
                 "libprotobuf-cpp-full",
-                "libsqlite",
+                "libsqlite_static_noicu",
                 "libz",
                 "sqlite_ext_percentile",
             ],
@@ -14843,7 +14843,7 @@
         ":perfetto_src_traceconv_utils",
     ],
     static_libs: [
-        "libsqlite",
+        "libsqlite_static_noicu",
         "libz",
         "perfetto_src_trace_processor_demangle",
         "sqlite_ext_percentile",
diff --git a/BUILD b/BUILD
index 80fc73e..2d28e55 100644
--- a/BUILD
+++ b/BUILD
@@ -860,6 +860,7 @@
         "include/perfetto/tracing/console_interceptor.h",
         "include/perfetto/tracing/data_source.h",
         "include/perfetto/tracing/debug_annotation.h",
+        "include/perfetto/tracing/default_socket.h",
         "include/perfetto/tracing/event_context.h",
         "include/perfetto/tracing/interceptor.h",
         "include/perfetto/tracing/internal/basic_types.h",
diff --git a/include/perfetto/base/build_config.h b/include/perfetto/base/build_config.h
index cd41e86..a0e6bb9 100644
--- a/include/perfetto/base/build_config.h
+++ b/include/perfetto/base/build_config.h
@@ -133,12 +133,10 @@
 //   http://msdn.microsoft.com/en-us/library/b0084kay.aspx
 //   http://www.agner.org/optimize/calling_conventions.pdf
 //   or with gcc, run: "echo | gcc -E -dM -"
-#if defined(_M_X64) || defined(__x86_64__)
-#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ARCH_CPU_ARM64() 0
-#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ARCH_CPU_X86_64() 1
-#elif defined(__aarch64__) || defined(_M_ARM64)
+#if defined(__aarch64__) || defined(_M_ARM64)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ARCH_CPU_ARM64() 1
-#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ARCH_CPU_X86_64() 0
+#else
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_ARCH_CPU_ARM64() 0
 #endif
 
 // perfetto_build_flags.h contains the tweakable build flags defined via GN.
diff --git a/include/perfetto/base/platform_handle.h b/include/perfetto/base/platform_handle.h
index 879fa85..88f6d59 100644
--- a/include/perfetto/base/platform_handle.h
+++ b/include/perfetto/base/platform_handle.h
@@ -17,6 +17,8 @@
 #ifndef INCLUDE_PERFETTO_BASE_PLATFORM_HANDLE_H_
 #define INCLUDE_PERFETTO_BASE_PLATFORM_HANDLE_H_
 
+#include <stdint.h>
+
 #include "perfetto/base/build_config.h"
 
 namespace perfetto {
@@ -30,10 +32,17 @@
 //    in Windows.h take an int, not a HANDLE.
 // 2. Handles returned by old-school WINAPI like CreateFile, CreateEvent etc.
 //    These are proper HANDLE(s). PlatformHandle should be used here.
+//
+// On Windows, sockets have their own type (SOCKET) which is neither a HANDLE
+// nor an int. However Windows SOCKET(s) can have an event HANDLE attached
+// to them (which in Perfetto is a PlatformHandle), and that can be used in
+// WaitForMultipleObjects, hence in base::TaskRunner.AddFileDescriptorWatch().
+// On POSIX OSes, a SocketHandle is really just an int (a file descriptor).
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-// Windows.h typedefs HANDLE to void*. We use void* here to avoid leaking
-// Windows.h through our headers.
+// Windows.h typedefs HANDLE to void*, and SOCKET to uintptr_t. We use their
+// types to avoid leaking Windows.h through our headers.
 using PlatformHandle = void*;
+using SocketHandle = uintptr_t;
 
 // On Windows both nullptr and 0xffff... (INVALID_HANDLE_VALUE) are invalid.
 struct PlatformHandleChecker {
@@ -43,6 +52,7 @@
 };
 #else
 using PlatformHandle = int;
+using SocketHandle = int;
 struct PlatformHandleChecker {
   static inline bool IsValid(PlatformHandle h) { return h >= 0; }
 };
diff --git a/include/perfetto/ext/base/unix_socket.h b/include/perfetto/ext/base/unix_socket.h
index 74ce2d9..e5cc536 100644
--- a/include/perfetto/ext/base/unix_socket.h
+++ b/include/perfetto/ext/base/unix_socket.h
@@ -27,6 +27,7 @@
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/export.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/base/platform_handle.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/base/weak_ptr.h"
@@ -36,23 +37,13 @@
 namespace perfetto {
 namespace base {
 
-// Define the SocketHandle and ScopedSocketHandle types.
-// On POSIX OSes, a SocketHandle is really just an int (a file descriptor).
-// On Windows, sockets are have their own type (SOCKET) which is neither a
-// HANDLE nor an int. However Windows SOCKET(s) can have a event HANDLE attached
-// to them (which in Perfetto is a PlatformHandle), and that can be used in
-// WaitForMultipleObjects, hence in base::TaskRunner.AddFileDescriptorWatch().
+// Define the ScopedSocketHandle type.
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
-// uintptr_t really reads as SOCKET here (Windows headers typedef to that).
-// As usual we don't just use SOCKET here to avoid leaking Windows.h includes
-// in our headers.
-using SocketHandle = uintptr_t;  // SOCKET
 int CloseSocket(SocketHandle);   // A wrapper around ::closesocket().
 using ScopedSocketHandle =
     ScopedResource<SocketHandle, CloseSocket, static_cast<SocketHandle>(-1)>;
 #else
-using SocketHandle = int;
 using ScopedSocketHandle = ScopedFile;
 #endif
 
diff --git a/include/perfetto/ext/tracing/ipc/default_socket.h b/include/perfetto/ext/tracing/ipc/default_socket.h
index 86d7b78..b7d667b 100644
--- a/include/perfetto/ext/tracing/ipc/default_socket.h
+++ b/include/perfetto/ext/tracing/ipc/default_socket.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,25 +17,9 @@
 #ifndef INCLUDE_PERFETTO_EXT_TRACING_IPC_DEFAULT_SOCKET_H_
 #define INCLUDE_PERFETTO_EXT_TRACING_IPC_DEFAULT_SOCKET_H_
 
-#include <string>
-#include <vector>
-
-#include "perfetto/base/export.h"
-
-namespace perfetto {
-
-PERFETTO_EXPORT_COMPONENT const char* GetConsumerSocket();
-// This function is used for tokenize the |producer_socket_names| string into
-// multiple producer socket names.
-PERFETTO_EXPORT_COMPONENT std::vector<std::string> TokenizeProducerSockets(
-    const char* producer_socket_names);
-PERFETTO_EXPORT_COMPONENT const char* GetProducerSocket();
-
-// Optionally returns the relay socket name (nullable). The relay socket is used
-// for forwarding the IPC messages between the local producers and the remote
-// tracing service.
-PERFETTO_EXPORT_COMPONENT const char* GetRelaySocket();
-
-}  // namespace perfetto
+// TODO(khokhlov): Migrate usages of "perfetto/ext/tracing/ipc/default_socket.h"
+// in Chromium to include "perfetto/tracing/internal/default_socket.h" instead,
+// then delete this file.
+#include "perfetto/tracing/default_socket.h"
 
 #endif  // INCLUDE_PERFETTO_EXT_TRACING_IPC_DEFAULT_SOCKET_H_
diff --git a/include/perfetto/ext/tracing/ipc/producer_ipc_client.h b/include/perfetto/ext/tracing/ipc/producer_ipc_client.h
index 1af399b..33e19b4 100644
--- a/include/perfetto/ext/tracing/ipc/producer_ipc_client.h
+++ b/include/perfetto/ext/tracing/ipc/producer_ipc_client.h
@@ -25,6 +25,7 @@
 #include "perfetto/ext/tracing/core/shared_memory.h"
 #include "perfetto/ext/tracing/core/shared_memory_arbiter.h"
 #include "perfetto/ext/tracing/core/tracing_service.h"
+#include "perfetto/tracing/tracing_backend.h"
 
 namespace perfetto {
 
@@ -89,7 +90,8 @@
       size_t shared_memory_size_hint_bytes = 0,
       size_t shared_memory_page_size_hint_bytes = 0,
       std::unique_ptr<SharedMemory> shm = nullptr,
-      std::unique_ptr<SharedMemoryArbiter> shm_arbiter = nullptr);
+      std::unique_ptr<SharedMemoryArbiter> shm_arbiter = nullptr,
+      CreateSocketAsync create_socket_async = {});
 
  protected:
   ProducerIPCClient() = delete;
diff --git a/include/perfetto/tracing/BUILD.gn b/include/perfetto/tracing/BUILD.gn
index 159e155..3e0e9f2 100644
--- a/include/perfetto/tracing/BUILD.gn
+++ b/include/perfetto/tracing/BUILD.gn
@@ -33,6 +33,7 @@
     "console_interceptor.h",
     "data_source.h",
     "debug_annotation.h",
+    "default_socket.h",
     "event_context.h",
     "interceptor.h",
     "internal/basic_types.h",
diff --git a/include/perfetto/tracing/default_socket.h b/include/perfetto/tracing/default_socket.h
new file mode 100644
index 0000000..1989724
--- /dev/null
+++ b/include/perfetto/tracing/default_socket.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef INCLUDE_PERFETTO_TRACING_DEFAULT_SOCKET_H_
+#define INCLUDE_PERFETTO_TRACING_DEFAULT_SOCKET_H_
+
+#include <string>
+#include <vector>
+
+#include "perfetto/base/export.h"
+
+namespace perfetto {
+
+PERFETTO_EXPORT_COMPONENT const char* GetConsumerSocket();
+// This function is used for tokenize the |producer_socket_names| string into
+// multiple producer socket names.
+PERFETTO_EXPORT_COMPONENT std::vector<std::string> TokenizeProducerSockets(
+    const char* producer_socket_names);
+PERFETTO_EXPORT_COMPONENT const char* GetProducerSocket();
+
+// Optionally returns the relay socket name (nullable). The relay socket is used
+// for forwarding the IPC messages between the local producers and the remote
+// tracing service.
+PERFETTO_EXPORT_COMPONENT const char* GetRelaySocket();
+
+}  // namespace perfetto
+
+#endif  // INCLUDE_PERFETTO_TRACING_DEFAULT_SOCKET_H_
diff --git a/include/perfetto/tracing/internal/system_tracing_backend.h b/include/perfetto/tracing/internal/system_tracing_backend.h
index c1ea664..fdcdc99 100644
--- a/include/perfetto/tracing/internal/system_tracing_backend.h
+++ b/include/perfetto/tracing/internal/system_tracing_backend.h
@@ -18,6 +18,7 @@
 #define INCLUDE_PERFETTO_TRACING_INTERNAL_SYSTEM_TRACING_BACKEND_H_
 
 #include "perfetto/base/export.h"
+#include "perfetto/tracing/default_socket.h"
 #include "perfetto/tracing/tracing_backend.h"
 
 namespace perfetto {
diff --git a/include/perfetto/tracing/tracing.h b/include/perfetto/tracing/tracing.h
index 44503ba..824f492 100644
--- a/include/perfetto/tracing/tracing.h
+++ b/include/perfetto/tracing/tracing.h
@@ -152,6 +152,16 @@
   // event tracks for the same thread.
   bool disallow_merging_with_system_tracks = false;
 
+  // If set, this function will be called by the producer client to create a
+  // socket for connection to the system service. The function takes two
+  // arguments: a name of the socket, and a callback that takes an open file
+  // descriptor. It should create a socket with the given name, connect to it,
+  // and return the corresponding descriptor via the callback.
+  // This is intended for the use-case where a process being traced is run
+  // inside a sandbox and can't create sockets directly.
+  // Not yet supported for consumer connections currently.
+  CreateSocketAsync create_socket_async;
+
  protected:
   friend class Tracing;
   friend class internal::TracingMuxerImpl;
diff --git a/include/perfetto/tracing/tracing_backend.h b/include/perfetto/tracing/tracing_backend.h
index 965d726..b915776 100644
--- a/include/perfetto/tracing/tracing_backend.h
+++ b/include/perfetto/tracing/tracing_backend.h
@@ -17,10 +17,12 @@
 #ifndef INCLUDE_PERFETTO_TRACING_TRACING_BACKEND_H_
 #define INCLUDE_PERFETTO_TRACING_TRACING_BACKEND_H_
 
+#include <functional>
 #include <memory>
 #include <string>
 
 #include "perfetto/base/export.h"
+#include "perfetto/base/platform_handle.h"
 
 // The embedder can (but doesn't have to) extend the TracingBackend class and
 // pass as an argument to Tracing::Initialize(kCustomBackend) to override the
@@ -43,6 +45,9 @@
 class Producer;
 class ProducerEndpoint;
 
+using CreateSocketCallback = std::function<void(base::SocketHandle)>;
+using CreateSocketAsync = std::function<void(CreateSocketCallback)>;
+
 // Responsible for connecting to the producer.
 class PERFETTO_EXPORT_COMPONENT TracingProducerBackend {
  public:
@@ -74,6 +79,10 @@
     // it to the service when connecting.
     // It's used in startup tracing.
     bool use_producer_provided_smb = false;
+
+    // If set, the producer will call this function to create and connect to a
+    // socket. See the corresponding field in TracingInitArgs for more info.
+    CreateSocketAsync create_socket_async;
   };
 
   virtual std::unique_ptr<ProducerEndpoint> ConnectProducer(
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index 90cde7d..a68feef 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -1460,7 +1460,7 @@
 
 message StartUp {
   // This enum must be kept up to date with LaunchCauseMetrics.LaunchCause.
-  enum LauchCauseType {
+  enum LaunchCauseType {
     OTHER = 0;
     CUSTOM_TAB = 1;
     TWA = 2;
@@ -1483,7 +1483,8 @@
   }
 
   optional int64 activity_id = 1;
-  optional LauchCauseType launch_cause = 2;
+  // deprecated field 2.
+  optional LaunchCauseType launch_cause = 3;
 }
 
 message WebContentInteraction {
diff --git a/src/tracing/internal/system_tracing_backend.cc b/src/tracing/internal/system_tracing_backend.cc
index 9488495..1b83579 100644
--- a/src/tracing/internal/system_tracing_backend.cc
+++ b/src/tracing/internal/system_tracing_backend.cc
@@ -65,11 +65,12 @@
         shm.get(), shmem_page_size_hint, SharedMemoryABI::ShmemMode::kDefault);
   }
 
+  ipc::Client::ConnArgs conn_args(GetProducerSocket(), true);
   auto endpoint = ProducerIPCClient::Connect(
-      GetProducerSocket(), args.producer, args.producer_name, args.task_runner,
+      std::move(conn_args), args.producer, args.producer_name, args.task_runner,
       TracingService::ProducerSMBScrapingMode::kEnabled, shmem_size_hint,
       shmem_page_size_hint, std::move(shm), std::move(arbiter),
-      ProducerIPCClient::ConnectionFlags::kRetryIfUnreachable);
+      args.create_socket_async);
   PERFETTO_CHECK(endpoint);
   return endpoint;
 }
diff --git a/src/tracing/internal/tracing_muxer_impl.cc b/src/tracing/internal/tracing_muxer_impl.cc
index c0d88ac..80e451f 100644
--- a/src/tracing/internal/tracing_muxer_impl.cc
+++ b/src/tracing/internal/tracing_muxer_impl.cc
@@ -970,6 +970,7 @@
   rb.producer_conn_args.shmem_size_hint_bytes = args.shmem_size_hint_kb * 1024;
   rb.producer_conn_args.shmem_page_size_hint_bytes =
       args.shmem_page_size_hint_kb * 1024;
+  rb.producer_conn_args.create_socket_async = args.create_socket_async;
   rb.producer->Initialize(rb.backend->ConnectProducer(rb.producer_conn_args));
 }
 
diff --git a/src/tracing/ipc/BUILD.gn b/src/tracing/ipc/BUILD.gn
index dfd0eda..1423223 100644
--- a/src/tracing/ipc/BUILD.gn
+++ b/src/tracing/ipc/BUILD.gn
@@ -50,6 +50,7 @@
     "../../../gn:default_deps",
     "../../../include/perfetto/ext/ipc",
     "../../../include/perfetto/ext/tracing/core",
+    "../../../include/perfetto/tracing",
     "../../base",
   ]
 }
diff --git a/src/tracing/ipc/default_socket.cc b/src/tracing/ipc/default_socket.cc
index f053756..81ff6df 100644
--- a/src/tracing/ipc/default_socket.cc
+++ b/src/tracing/ipc/default_socket.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "perfetto/ext/tracing/ipc/default_socket.h"
+#include "perfetto/tracing/default_socket.h"
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
diff --git a/src/tracing/ipc/producer/producer_ipc_client_impl.cc b/src/tracing/ipc/producer/producer_ipc_client_impl.cc
index ea87953..3959b0a 100644
--- a/src/tracing/ipc/producer/producer_ipc_client_impl.cc
+++ b/src/tracing/ipc/producer/producer_ipc_client_impl.cc
@@ -66,7 +66,7 @@
                ProducerIPCClient::ConnectionFlags::kRetryIfUnreachable},
           producer, producer_name, task_runner, smb_scraping_mode,
           shared_memory_size_hint_bytes, shared_memory_page_size_hint_bytes,
-          std::move(shm), std::move(shm_arbiter)));
+          std::move(shm), std::move(shm_arbiter), CreateSocketAsync()));
 }
 
 // static. (Declared in include/tracing/ipc/producer_ipc_client.h).
@@ -79,13 +79,14 @@
     size_t shared_memory_size_hint_bytes,
     size_t shared_memory_page_size_hint_bytes,
     std::unique_ptr<SharedMemory> shm,
-    std::unique_ptr<SharedMemoryArbiter> shm_arbiter) {
+    std::unique_ptr<SharedMemoryArbiter> shm_arbiter,
+    CreateSocketAsync create_socket_async) {
   return std::unique_ptr<TracingService::ProducerEndpoint>(
-      new ProducerIPCClientImpl(std::move(conn_args), producer, producer_name,
-                                task_runner, smb_scraping_mode,
-                                shared_memory_size_hint_bytes,
-                                shared_memory_page_size_hint_bytes,
-                                std::move(shm), std::move(shm_arbiter)));
+      new ProducerIPCClientImpl(
+          std::move(conn_args), producer, producer_name, task_runner,
+          smb_scraping_mode, shared_memory_size_hint_bytes,
+          shared_memory_page_size_hint_bytes, std::move(shm),
+          std::move(shm_arbiter), create_socket_async));
 }
 
 ProducerIPCClientImpl::ProducerIPCClientImpl(
@@ -97,13 +98,12 @@
     size_t shared_memory_size_hint_bytes,
     size_t shared_memory_page_size_hint_bytes,
     std::unique_ptr<SharedMemory> shm,
-    std::unique_ptr<SharedMemoryArbiter> shm_arbiter)
+    std::unique_ptr<SharedMemoryArbiter> shm_arbiter,
+    CreateSocketAsync create_socket_async)
     : producer_(producer),
       task_runner_(task_runner),
       receive_shmem_fd_cb_fuchsia_(
           std::move(conn_args.receive_shmem_fd_cb_fuchsia)),
-      ipc_channel_(
-          ipc::Client::CreateInstance(std::move(conn_args), task_runner)),
       producer_port_(
           new protos::gen::ProducerPortProxy(this /* event_listener */)),
       shared_memory_(std::move(shm)),
@@ -124,7 +124,28 @@
     shared_buffer_page_size_kb_ = shared_memory_page_size_hint_bytes_ / 1024;
   }
 
-  ipc_channel_->BindService(producer_port_->GetWeakPtr());
+  if (create_socket_async) {
+    PERFETTO_DCHECK(conn_args.socket_name);
+    auto weak_this = weak_factory_.GetWeakPtr();
+    create_socket_async(
+        [weak_this, task_runner = task_runner_](base::SocketHandle fd) {
+          task_runner->PostTask([weak_this, fd] {
+            base::ScopedSocketHandle handle(fd);
+            if (!weak_this) {
+              return;
+            }
+            ipc::Client::ConnArgs args(std::move(handle));
+            weak_this->ipc_channel_ = ipc::Client::CreateInstance(
+                std::move(args), weak_this->task_runner_);
+            weak_this->ipc_channel_->BindService(
+                weak_this->producer_port_->GetWeakPtr());
+          });
+        });
+  } else {
+    ipc_channel_ =
+        ipc::Client::CreateInstance(std::move(conn_args), task_runner);
+    ipc_channel_->BindService(producer_port_->GetWeakPtr());
+  }
   PERFETTO_DCHECK_THREAD(thread_checker_);
 }
 
diff --git a/src/tracing/ipc/producer/producer_ipc_client_impl.h b/src/tracing/ipc/producer/producer_ipc_client_impl.h
index 664b698..f50dcd7 100644
--- a/src/tracing/ipc/producer/producer_ipc_client_impl.h
+++ b/src/tracing/ipc/producer/producer_ipc_client_impl.h
@@ -46,6 +46,8 @@
 // IPC channel to the remote Service. This class is the glue layer between the
 // generic Service interface exposed to the clients of the library and the
 // actual IPC transport.
+// If create_socket_async is set, it will be called to create and connect to a
+// socket to the service. If unset, the producer will create and connect itself.
 class ProducerIPCClientImpl : public TracingService::ProducerEndpoint,
                               public ipc::ServiceProxy::EventListener {
  public:
@@ -57,7 +59,8 @@
                         size_t shared_memory_size_hint_bytes,
                         size_t shared_memory_page_size_hint_bytes,
                         std::unique_ptr<SharedMemory> shm,
-                        std::unique_ptr<SharedMemoryArbiter> shm_arbiter);
+                        std::unique_ptr<SharedMemoryArbiter> shm_arbiter,
+                        CreateSocketAsync create_socket_async);
   ~ProducerIPCClientImpl() override;
 
   // TracingService::ProducerEndpoint implementation.
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 66b2cff..0040714 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -16,6 +16,9 @@
 
 #include <fcntl.h>
 
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
 #include <chrono>
 #include <condition_variable>
 #include <fstream>
@@ -6303,6 +6306,7 @@
 
   // Create a new trace session.
   auto* tracing_session = NewTraceWithCategories({"test"});
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
 
   // Emit another event after starting.
@@ -6323,6 +6327,7 @@
 
   // Create a new trace session.
   auto* tracing_session = NewTraceWithCategories({"test"});
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
 
   // Emit another event after starting.
@@ -6340,6 +6345,7 @@
   TRACE_EVENT_BEGIN("test", "Event");
 
   auto* tracing_session = NewTraceWithCategories({"test"});
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
 
   TRACE_EVENT_END("test");
@@ -6358,7 +6364,9 @@
   ds_cfg->set_name("CustomDataSource");
   SetupStartupTracing(cfg);
   TRACE_EVENT_BEGIN("test", "TrackEvent.Startup");
+
   auto* tracing_session = NewTraceWithCategories({"test"}, {}, cfg);
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
 
   TRACE_EVENT_BEGIN("test", "TrackEvent.Main");
@@ -6397,7 +6405,9 @@
     auto packet = ctx.NewTracePacket();
     packet->set_for_testing()->set_str("CustomDataSource.Startup");
   });
+
   auto* tracing_session = NewTraceWithCategories({"test"}, {}, cfg);
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
 
   TRACE_EVENT_BEGIN("test", "TrackEvent.Main");
@@ -6470,6 +6480,7 @@
   TRACE_EVENT_BEGIN("test", "StartupEvent2");
 
   auto* tracing_session = NewTraceWithCategories({"test"});
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
 
   TRACE_EVENT_BEGIN("test", "MainEvent");
@@ -6555,7 +6566,9 @@
 // during startup tracing session.
 TEST_P(PerfettoStartupTracingApiTest, NoEventInStartupTracing) {
   SetupStartupTracing();
+
   auto* tracing_session = NewTraceWithCategories({"test"});
+  ASSERT_TRUE(WaitForOneProducerConnected(tracing_session->get()));
   tracing_session->get()->StartBlocking();
   // Emit an event now that the session was fully started. This should go
   // strait to the SMB.
@@ -6797,6 +6810,198 @@
   perfetto::Tracing::ResetForTesting();
 }
 
+#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+namespace {
+
+int ConnectUnixSocket() {
+  std::string socket_name = perfetto::GetProducerSocket();
+  int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+  struct sockaddr_un saddr;
+  memset(&saddr, 0, sizeof(saddr));
+  memcpy(saddr.sun_path, socket_name.data(), socket_name.size());
+  saddr.sun_family = AF_UNIX;
+  auto size = static_cast<socklen_t>(__builtin_offsetof(sockaddr_un, sun_path) +
+                                     socket_name.size() + 1);
+  connect(fd, reinterpret_cast<const struct sockaddr*>(&saddr), size);
+  return fd;
+}
+
+}  // namespace
+
+TEST(PerfettoApiInitTest, AsyncSocket) {
+  auto system_service = perfetto::test::SystemService::Start();
+  // If the system backend isn't supported, skip
+  if (!system_service.valid()) {
+    GTEST_SKIP();
+  }
+
+  EXPECT_FALSE(perfetto::Tracing::IsInitialized());
+
+  perfetto::CreateSocketCallback socket_callback;
+  WaitableTestEvent create_socket_called;
+
+  TracingInitArgs args;
+  args.backends = perfetto::kSystemBackend;
+  args.tracing_policy = g_test_tracing_policy;
+  args.create_socket_async = [&socket_callback, &create_socket_called](
+                                 perfetto::CreateSocketCallback cb) {
+    socket_callback = cb;
+    create_socket_called.Notify();
+  };
+
+  perfetto::Tracing::Initialize(args);
+  create_socket_called.Wait();
+
+  int fd = ConnectUnixSocket();
+  socket_callback(fd);
+
+  perfetto::test::SyncProducers();
+  EXPECT_TRUE(perfetto::Tracing::NewTrace(perfetto::kSystemBackend)
+                  ->QueryServiceStateBlocking()
+                  .success);
+
+  perfetto::Tracing::ResetForTesting();
+}
+
+TEST(PerfettoApiInitTest, AsyncSocketDisconnect) {
+  auto system_service = perfetto::test::SystemService::Start();
+  // If the system backend isn't supported, skip
+  if (!system_service.valid()) {
+    GTEST_SKIP();
+  }
+
+  EXPECT_FALSE(perfetto::Tracing::IsInitialized());
+
+  perfetto::CreateSocketCallback socket_callback;
+  testing::MockFunction<perfetto::CreateSocketAsync> mock_create_socket;
+  WaitableTestEvent create_socket_called1, create_socket_called2;
+
+  TracingInitArgs args;
+  args.backends = perfetto::kSystemBackend;
+  args.tracing_policy = g_test_tracing_policy;
+  args.create_socket_async = mock_create_socket.AsStdFunction();
+
+  EXPECT_CALL(mock_create_socket, Call)
+      .WillOnce(Invoke([&socket_callback, &create_socket_called1](
+                           perfetto::CreateSocketCallback cb) {
+        socket_callback = cb;
+        create_socket_called1.Notify();
+      }))
+      .WillOnce(Invoke([&socket_callback, &create_socket_called2](
+                           perfetto::CreateSocketCallback cb) {
+        socket_callback = cb;
+        create_socket_called2.Notify();
+      }));
+
+  perfetto::Tracing::Initialize(args);
+  create_socket_called1.Wait();
+  int fd = ConnectUnixSocket();
+  socket_callback(fd);
+
+  perfetto::test::SyncProducers();
+  EXPECT_TRUE(perfetto::Tracing::NewTrace(perfetto::kSystemBackend)
+                  ->QueryServiceStateBlocking()
+                  .success);
+
+  // Restart the system service. This will cause the producer and consumer to
+  // disconnect and reconnect. The create_socket_async function should be called
+  // for the second time.
+  system_service.Restart();
+  create_socket_called2.Wait();
+  fd = ConnectUnixSocket();
+  socket_callback(fd);
+
+  perfetto::test::SyncProducers();
+  EXPECT_TRUE(perfetto::Tracing::NewTrace(perfetto::kSystemBackend)
+                  ->QueryServiceStateBlocking()
+                  .success);
+
+  perfetto::Tracing::ResetForTesting();
+}
+
+TEST(PerfettoApiInitTest, AsyncSocketStartupTracing) {
+  auto system_service = perfetto::test::SystemService::Start();
+  // If the system backend isn't supported, skip
+  if (!system_service.valid()) {
+    GTEST_SKIP();
+  }
+
+  EXPECT_FALSE(perfetto::Tracing::IsInitialized());
+
+  perfetto::CreateSocketCallback socket_callback;
+  WaitableTestEvent create_socket_called;
+
+  TracingInitArgs args;
+  args.backends = perfetto::kSystemBackend;
+  args.tracing_policy = g_test_tracing_policy;
+  args.create_socket_async = [&socket_callback, &create_socket_called](
+                                 perfetto::CreateSocketCallback cb) {
+    socket_callback = cb;
+    create_socket_called.Notify();
+  };
+
+  perfetto::Tracing::Initialize(args);
+  perfetto::TrackEvent::Register();
+
+  perfetto::TraceConfig cfg;
+  cfg.set_duration_ms(500);
+  cfg.add_buffers()->set_size_kb(1024);
+  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
+  ds_cfg->set_name("track_event");
+
+  perfetto::protos::gen::TrackEventConfig te_cfg;
+  te_cfg.add_disabled_categories("*");
+  te_cfg.add_enabled_categories("test");
+  ds_cfg->set_track_event_config_raw(te_cfg.SerializeAsString());
+
+  perfetto::Tracing::SetupStartupTracingOpts opts;
+  opts.backend = perfetto::kSystemBackend;
+  auto startup_session =
+      perfetto::Tracing::SetupStartupTracingBlocking(cfg, std::move(opts));
+
+  // Emit a significant number of events to write >1 chunk of data.
+  constexpr size_t kNumEvents = 1000;
+  for (size_t i = 0; i < kNumEvents; i++) {
+    TRACE_EVENT_INSTANT("test", "StartupEvent");
+  }
+
+  // Now proceed with the connection to the service and wait until it completes.
+  int fd = ConnectUnixSocket();
+  socket_callback(fd);
+  perfetto::test::SyncProducers();
+
+  auto session = perfetto::Tracing::NewTrace(perfetto::kSystemBackend);
+  session->Setup(cfg);
+  session->StartBlocking();
+
+  // Write even more events, now with connection established.
+  for (size_t i = 0; i < kNumEvents; i++) {
+    TRACE_EVENT_INSTANT("test", "TraceEvent");
+  }
+
+  perfetto::TrackEvent::Flush();
+  session->StopBlocking();
+
+  auto raw_trace = session->ReadTraceBlocking();
+  perfetto::protos::gen::Trace parsed_trace;
+  EXPECT_TRUE(parsed_trace.ParseFromArray(raw_trace.data(), raw_trace.size()));
+
+  size_t n_track_events = 0;
+  for (const auto& packet : parsed_trace.packet()) {
+    if (packet.has_track_event()) {
+      ++n_track_events;
+    }
+  }
+
+  // Events from both startup and service-initiated sessions should be retained.
+  EXPECT_EQ(n_track_events, kNumEvents * 2);
+
+  startup_session.reset();
+  session.reset();
+  perfetto::Tracing::ResetForTesting();
+}
+#endif  // !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+
 struct BackendTypeAsString {
   std::string operator()(
       const ::testing::TestParamInfo<perfetto::BackendType>& info) const {
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 11f6d16..6d00144 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -335,7 +335,7 @@
 
 def enable_sqlite(module):
   if module.type == 'cc_binary_host':
-    module.static_libs.add('libsqlite')
+    module.static_libs.add('libsqlite_static_noicu')
     module.static_libs.add('sqlite_ext_percentile')
   elif module.host_supported:
     # Copy what the sqlite3 command line tool does.
@@ -344,7 +344,7 @@
     module.android.shared_libs.add('liblog')
     module.android.shared_libs.add('libutils')
     module.android.static_libs.add('sqlite_ext_percentile')
-    module.host.static_libs.add('libsqlite')
+    module.host.static_libs.add('libsqlite_static_noicu')
     module.host.static_libs.add('sqlite_ext_percentile')
   else:
     module.shared_libs.add('libsqlite')
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 91a0c0e..8ee9b76 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -6,7 +6,7 @@
     },
     {
       "name": "canary",
-      "rev": "c69b33b9abcc20fad9ad5f39de883216e4b43130"
+      "rev": "9ca89e30931314dec4af1131d516e07e39d8657d"
     },
     {
       "name": "autopush",
diff --git a/ui/src/common/color.ts b/ui/src/common/color.ts
index 30b4dd9..127bfbb 100644
--- a/ui/src/common/color.ts
+++ b/ui/src/common/color.ts
@@ -48,7 +48,10 @@
 // creation time, so they may be used in the hot path (render loop).
 export interface Color {
   readonly cssString: string;
-  readonly isLight: boolean;
+
+  // The perceived brightness of the color using a weighted average of the
+  // r, g and b channels based on human perception.
+  readonly perceivedBrightness: number;
 
   // Bring up the lightness by |percent| percent.
   lighten(percent: number, max?: number): Color;
@@ -78,11 +81,14 @@
   // Saturation: 0-100
   // Lightness:  0-100
   // Alpha:      0-1
-  constructor(hsl: ColorTuple|HSL, alpha?: number) {
-    if (Array.isArray(hsl)) {
-      this.hsl = hsl;
+  constructor(init: ColorTuple|HSL|string, alpha?: number) {
+    if (Array.isArray(init)) {
+      this.hsl = init;
+    } else if (typeof init === 'string') {
+      const rgb = hexToRgb(init);
+      this.hsl = rgbToHsl(rgb);
     } else {
-      this.hsl = [hsl.h, hsl.s, hsl.l];
+      this.hsl = [init.h, init.s, init.l];
     }
     this.alpha = alpha;
   }
@@ -128,19 +134,19 @@
 // Describes a color defined in standard HSL color space.
 export class HSLColor extends HSLColorBase<HSLColor> implements Color {
   readonly cssString: string;
-  readonly isLight: boolean;
+  readonly perceivedBrightness: number;
 
   // Values are in the range:
   // Hue:        0-360
   // Saturation: 0-100
   // Lightness:  0-100
   // Alpha:      0-1
-  constructor(hsl: ColorTuple|HSL, alpha?: number) {
+  constructor(hsl: ColorTuple|HSL|string, alpha?: number) {
     super(hsl, alpha);
 
-    const [r, g, b] = hslToRGB(...this.hsl);
+    const [r, g, b] = hslToRgb(...this.hsl);
 
-    this.isLight = isLight(r, g, b);
+    this.perceivedBrightness = perceivedBrightness(r, g, b);
 
     if (this.alpha === undefined) {
       this.cssString = `rgb(${r} ${g} ${b})`;
@@ -149,7 +155,7 @@
     }
   }
 
-  create(values: ColorTuple|HSL, alpha?: number|undefined): HSLuvColor {
+  create(values: ColorTuple|HSL, alpha?: number|undefined): HSLColor {
     return new HSLColor(values, alpha);
   }
 }
@@ -158,7 +164,7 @@
 // See: https://www.hsluv.org/
 export class HSLuvColor extends HSLColorBase<HSLuvColor> implements Color {
   readonly cssString: string;
-  readonly isLight: boolean;
+  readonly perceivedBrightness: number;
 
   constructor(hsl: ColorTuple|HSL, alpha?: number) {
     super(hsl, alpha);
@@ -168,7 +174,7 @@
     const g = Math.floor(rgb[1] * 255);
     const b = Math.floor(rgb[2] * 255);
 
-    this.isLight = isLight(r, g, b);
+    this.perceivedBrightness = perceivedBrightness(r, g, b);
 
     if (this.alpha === undefined) {
       this.cssString = `rgb(${r} ${g} ${b})`;
@@ -186,7 +192,7 @@
 // Saturation: 0-100
 // Lightness: 0-100
 // RGB: 0-255
-export function hslToRGB(h: number, s: number, l: number): ColorTuple {
+export function hslToRgb(h: number, s: number, l: number): ColorTuple {
   h = h;
   s = s / SATURATION_MAX;
   l = l / LIGHTNESS_MAX;
@@ -219,12 +225,63 @@
   return [r, g, b];
 }
 
-// Get whether a color should be considered "light" based on its perceived
-// brightness.
-function isLight(r: number, g: number, b: number): boolean {
+export function hexToRgb(hex: string): ColorTuple {
+  // Convert hex to RGB first
+  let r: number = 0;
+  let g: number = 0;
+  let b: number = 0;
+
+  if (hex.length === 4) {
+    r = parseInt(hex[1] + hex[1], 16);
+    g = parseInt(hex[2] + hex[2], 16);
+    b = parseInt(hex[3] + hex[3], 16);
+  } else if (hex.length === 7) {
+    r = parseInt(hex.substring(1, 3), 16);
+    g = parseInt(hex.substring(3, 5), 16);
+    b = parseInt(hex.substring(5, 7), 16);
+  }
+
+  return [r, g, b];
+}
+
+export function rgbToHsl(rgb: ColorTuple): ColorTuple {
+  let [r, g, b] = rgb;
+  r /= 255;
+  g /= 255;
+  b /= 255;
+  const max = Math.max(r, g, b);
+  const min = Math.min(r, g, b);
+  let h: number = (max + min) / 2;
+  let s: number = (max + min) / 2;
+  const l: number = (max + min) / 2;
+
+  if (max === min) {
+    h = s = 0;  // achromatic
+  } else {
+    const d = max - min;
+    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+    switch (max) {
+      case r:
+        h = (g - b) / d + (g < b ? 6 : 0);
+        break;
+      case g:
+        h = (b - r) / d + 2;
+        break;
+      case b:
+        h = (r - g) / d + 4;
+        break;
+    }
+    h /= 6;
+  }
+
+  return [h * 360, s * 100, l * 100];
+}
+
+// Return the perceived brightness of a color using a weighted average of the
+// r, g and b channels based on human perception.
+function perceivedBrightness(r: number, g: number, b: number): number {
   // YIQ calculation from https://24ways.org/2010/calculating-color-contrast
-  const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
-  return (yiq >= 128);
+  return ((r * 299) + (g * 587) + (b * 114)) / 1000;
 }
 
 // Comparison function used for sorting colors.
diff --git a/ui/src/common/color_unittest.ts b/ui/src/common/color_unittest.ts
index d2793f4..4973308 100644
--- a/ui/src/common/color_unittest.ts
+++ b/ui/src/common/color_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {HSLColor, hslToRGB, HSLuvColor} from './color';
+import {HSLColor, hslToRgb, HSLuvColor} from './color';
 
 describe('HSLColor', () => {
   const col = new HSLColor({h: 123, s: 66, l: 45});
@@ -45,13 +45,17 @@
     expect(col.setAlpha(undefined).alpha).toEqual(undefined);
   });
 
-  test('isLight', () => {
+  test('perceivedBrightness', () => {
     // Test a few obviously light/dark colours.
-    expect(new HSLColor({h: 0, s: 0, l: 0}).isLight).toBeFalsy();
-    expect(new HSLColor({h: 0, s: 0, l: 100}).isLight).toBeTruthy();
+    expect(new HSLColor({h: 0, s: 0, l: 0}).perceivedBrightness)
+        .toBeLessThan(128);
+    expect(new HSLColor({h: 0, s: 0, l: 100}).perceivedBrightness)
+        .toBeGreaterThan(128);
 
-    expect(new HSLColor({h: 0, s: 0, l: 40}).isLight).toBeFalsy();
-    expect(new HSLColor({h: 0, s: 0, l: 60}).isLight).toBeTruthy();
+    expect(new HSLColor({h: 0, s: 0, l: 40}).perceivedBrightness)
+        .toBeLessThan(128);
+    expect(new HSLColor({h: 0, s: 0, l: 60}).perceivedBrightness)
+        .toBeGreaterThan(128);
   });
 });
 
@@ -86,20 +90,24 @@
     expect(col.setAlpha(undefined).alpha).toEqual(undefined);
   });
 
-  test('isLight', () => {
+  test('perceivedBrightness', () => {
     // Test a few obviously light/dark colours.
-    expect(new HSLuvColor({h: 0, s: 0, l: 0}).isLight).toBeFalsy();
-    expect(new HSLuvColor({h: 0, s: 0, l: 100}).isLight).toBeTruthy();
+    expect(new HSLuvColor({h: 0, s: 0, l: 0}).perceivedBrightness)
+        .toBeLessThan(128);
+    expect(new HSLuvColor({h: 0, s: 0, l: 100}).perceivedBrightness)
+        .toBeGreaterThan(128);
 
-    expect(new HSLuvColor({h: 0, s: 0, l: 40}).isLight).toBeFalsy();
-    expect(new HSLuvColor({h: 0, s: 0, l: 60}).isLight).toBeTruthy();
+    expect(new HSLuvColor({h: 0, s: 0, l: 40}).perceivedBrightness)
+        .toBeLessThan(128);
+    expect(new HSLuvColor({h: 0, s: 0, l: 60}).perceivedBrightness)
+        .toBeGreaterThan(128);
   });
 });
 
 test('hslToRGB', () => {
   // Pick a few well-known conversions to check we're in the right ballpark.
-  expect(hslToRGB(0, 0, 0)).toEqual([0, 0, 0]);
-  expect(hslToRGB(0, 100, 50)).toEqual([255, 0, 0]);
-  expect(hslToRGB(120, 100, 50)).toEqual([0, 255, 0]);
-  expect(hslToRGB(240, 100, 50)).toEqual([0, 0, 255]);
+  expect(hslToRgb(0, 0, 0)).toEqual([0, 0, 0]);
+  expect(hslToRgb(0, 100, 50)).toEqual([255, 0, 0]);
+  expect(hslToRgb(120, 100, 50)).toEqual([0, 255, 0]);
+  expect(hslToRgb(240, 100, 50)).toEqual([0, 0, 255]);
 });
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index b2951a4..584deb5 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -19,6 +19,11 @@
 
 import {Color, HSLColor, HSLuvColor} from './color';
 
+// 128 would provide equal weighting between dark and light text, but we want to
+// slightly prefer light text for stylistic reasons.
+// 140 means we must be brighter on average before switching to dark text.
+const PERCEIVED_BRIGHTNESS_LIMIT = 140;
+
 // This file defines some opinionated colors and provides functions to access
 // random but predictable colors based on a seed, as well as standardized ways
 // to access colors for core objects such as slices and thread states.
@@ -98,15 +103,19 @@
 
 // Create a color scheme based on a single color, which defines the variant
 // color as a slightly darker and more saturated version of the base color.
-export function makeColorScheme(base: Color): ColorScheme {
-  const variant = base.darken(15).saturate(15);
+export function makeColorScheme(base: Color, variant?: Color): ColorScheme {
+  variant = variant ?? base.darken(15).saturate(15);
 
   return {
     base,
     variant,
     disabled: GRAY_COLOR,
-    textBase: base.isLight ? BLACK_COLOR : WHITE_COLOR,
-    textVariant: variant.isLight ? BLACK_COLOR : WHITE_COLOR,
+    textBase: base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT ?
+        BLACK_COLOR :
+        WHITE_COLOR,
+    textVariant: variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT ?
+        BLACK_COLOR :
+        WHITE_COLOR,
     textDisabled: WHITE_COLOR,  // Low contrast is on purpose
   };
 }
@@ -150,14 +159,7 @@
     const base =
         new HSLuvColor({h: hue, s: saturation, l: hash(seed + 'x', 40) + 40});
     const variant = new HSLuvColor({h: hue, s: saturation, l: 30});
-    const colorScheme: ColorScheme = {
-      base,
-      variant,
-      disabled: GRAY_COLOR,
-      textBase: base.isLight ? BLACK_COLOR : WHITE_COLOR,
-      textVariant: variant.isLight ? BLACK_COLOR : WHITE_COLOR,
-      textDisabled: WHITE_COLOR,
-    };
+    const colorScheme = makeColorScheme(base, variant);
 
     proceduralColorCache.set(seed, colorScheme);
 
diff --git a/ui/src/controller/aggregation/slice_aggregation_controller.ts b/ui/src/controller/aggregation/slice_aggregation_controller.ts
index 4de7003..7543110 100644
--- a/ui/src/controller/aggregation/slice_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/slice_aggregation_controller.ts
@@ -17,7 +17,9 @@
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {Engine} from '../../trace_processor/engine';
-import {ASYNC_SLICE_TRACK_KIND} from '../../tracks/async_slices';
+import {
+  ASYNC_SLICE_TRACK_KIND,
+} from '../../tracks/async_slices/async_slice_track';
 import {SLICE_TRACK_KIND} from '../../tracks/chrome_slices';
 
 import {AggregationController} from './aggregation_controller';
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index c123e78..8c6433b 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -39,7 +39,7 @@
   STR_NULL,
 } from '../trace_processor/query_result';
 import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames';
-import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices';
+import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices/async_slice_track';
 import {
   ENABLE_SCROLL_JANK_PLUGIN_V2,
   getScrollJankTracks,
@@ -288,14 +288,25 @@
         }
       }
 
-      const track: AddTrackArgs = {
-        uri: `perfetto.AsyncSlices#${rawName}`,
-        trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-        trackGroup,
-        name,
-      };
+      if (showV1()) {
+        const track: AddTrackArgs = {
+          uri: `perfetto.AsyncSlices#${rawName}`,
+          trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+          trackGroup,
+          name,
+        };
+        this.tracksToAdd.push(track);
+      }
 
-      this.tracksToAdd.push(track);
+      if (showV2()) {
+        const track: AddTrackArgs = {
+          uri: `perfetto.AsyncSlices#${rawName}.v2`,
+          trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+          trackGroup,
+          name,
+        };
+        this.tracksToAdd.push(track);
+      }
     }
   }
 
@@ -1022,12 +1033,24 @@
         processName,
         kind: ASYNC_SLICE_TRACK_KIND,
       });
-      this.tracksToAdd.push({
-        uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}`,
-        name,
-        trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-        trackGroup: uuid,
-      });
+
+      if (showV1()) {
+        this.tracksToAdd.push({
+          uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}`,
+          name,
+          trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+          trackGroup: uuid,
+        });
+      }
+
+      if (showV2()) {
+        this.tracksToAdd.push({
+          uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}.v2`,
+          name,
+          trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+          trackGroup: uuid,
+        });
+      }
     }
   }
 
@@ -1078,12 +1101,24 @@
       const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
       const name =
           getTrackName({name: trackName, upid, pid, processName, kind});
-      this.tracksToAdd.push({
-        uri: `perfetto.ActualFrames#${upid}`,
-        name,
-        trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
-        trackGroup: uuid,
-      });
+
+      if (showV1()) {
+        this.tracksToAdd.push({
+          uri: `perfetto.ActualFrames#${upid}`,
+          name,
+          trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
+          trackGroup: uuid,
+        });
+      }
+
+      if (showV2()) {
+        this.tracksToAdd.push({
+          uri: `perfetto.ActualFrames#${upid}.v2`,
+          name,
+          trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
+          trackGroup: uuid,
+        });
+      }
     }
   }
 
@@ -1135,12 +1170,24 @@
       const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
       const name =
           getTrackName({name: trackName, upid, pid, processName, kind});
-      this.tracksToAdd.push({
-        uri: `perfetto.ExpectedFrames#${upid}`,
-        name,
-        trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
-        trackGroup: uuid,
-      });
+
+      if (showV1()) {
+        this.tracksToAdd.push({
+          uri: `perfetto.ExpectedFrames#${upid}`,
+          name,
+          trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
+          trackGroup: uuid,
+        });
+      }
+
+      if (showV2()) {
+        this.tracksToAdd.push({
+          uri: `perfetto.ExpectedFrames#${upid}.v2`,
+          name,
+          trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
+          trackGroup: uuid,
+        });
+      }
     }
   }
 
diff --git a/ui/src/tracks/actual_frames/actual_frames_track.ts b/ui/src/tracks/actual_frames/actual_frames_track.ts
new file mode 100644
index 0000000..4b6c198b
--- /dev/null
+++ b/ui/src/tracks/actual_frames/actual_frames_track.ts
@@ -0,0 +1,146 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {duration, time} from '../../base/time';
+import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
+import {
+  EngineProxy,
+} from '../../public';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  STR,
+} from '../../trace_processor/query_result';
+
+export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
+
+const BLUE_COLOR = '#03A9F4';         // Blue 500
+const GREEN_COLOR = '#4CAF50';        // Green 500
+const YELLOW_COLOR = '#FFEB3B';       // Yellow 500
+const RED_COLOR = '#FF5722';          // Red 500
+const LIGHT_GREEN_COLOR = '#C0D588';  // Light Green 500
+const PINK_COLOR = '#F515E0';         // Pink 500
+
+export class ActualFramesTrack extends SliceTrackBase {
+  private maxDur = 0n;
+
+  constructor(
+      private engine: EngineProxy, maxDepth: number, trackKey: string,
+      private trackIds: number[], namespace?: string) {
+    super(maxDepth, trackKey, 'actual_frame_timeline_slice', namespace);
+  }
+
+  async onBoundsChange(start: time, end: time, resolution: duration):
+      Promise<SliceData> {
+    if (this.maxDur === 0n) {
+      const maxDurResult = await this.engine.query(`
+    select
+      max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+        as maxDur
+    from experimental_slice_layout
+    where filter_track_ids = '${this.trackIds.join(',')}'
+  `);
+      this.maxDur = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
+    }
+
+    const rawResult = await this.engine.query(`
+  SELECT
+    (s.ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
+    s.ts as ts,
+    max(iif(s.dur = -1, (SELECT end_ts FROM trace_bounds) - s.ts, s.dur))
+        as dur,
+    s.layout_depth as layoutDepth,
+    s.name as name,
+    s.id as id,
+    s.dur = 0 as isInstant,
+    s.dur = -1 as isIncomplete,
+    CASE afs.jank_tag
+      WHEN 'Self Jank' THEN '${RED_COLOR}'
+      WHEN 'Other Jank' THEN '${YELLOW_COLOR}'
+      WHEN 'Dropped Frame' THEN '${BLUE_COLOR}'
+      WHEN 'Buffer Stuffing' THEN '${LIGHT_GREEN_COLOR}'
+      WHEN 'SurfaceFlinger Stuffing' THEN '${LIGHT_GREEN_COLOR}'
+      WHEN 'No Jank' THEN '${GREEN_COLOR}'
+      ELSE '${PINK_COLOR}'
+    END as color
+  from experimental_slice_layout s
+  join actual_frame_timeline_slice afs using(id)
+  where
+    filter_track_ids = '${this.trackIds.join(',')}' and
+    s.ts >= ${start - this.maxDur} and
+    s.ts <= ${end}
+  group by tsq, s.layout_depth
+  order by tsq, s.layout_depth
+`);
+
+    const numRows = rawResult.numRows();
+    const slices: SliceData = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      strings: [],
+      sliceIds: new Float64Array(numRows),
+      starts: new BigInt64Array(numRows),
+      ends: new BigInt64Array(numRows),
+      depths: new Uint16Array(numRows),
+      titles: new Uint16Array(numRows),
+      colors: new Uint16Array(numRows),
+      isInstant: new Uint16Array(numRows),
+      isIncomplete: new Uint16Array(numRows),
+    };
+
+    const stringIndexes = new Map<string, number>();
+    function internString(str: string) {
+      let idx = stringIndexes.get(str);
+      if (idx !== undefined) return idx;
+      idx = slices.strings.length;
+      slices.strings.push(str);
+      stringIndexes.set(str, idx);
+      return idx;
+    }
+
+    const it = rawResult.iter({
+      'tsq': LONG,
+      'ts': LONG,
+      'dur': LONG,
+      'layoutDepth': NUM,
+      'id': NUM,
+      'name': STR,
+      'isInstant': NUM,
+      'isIncomplete': NUM,
+      'color': STR,
+    });
+    for (let i = 0; it.valid(); i++, it.next()) {
+      const startQ = it.tsq;
+      const start = it.ts;
+      const dur = it.dur;
+      const end = start + dur;
+      const minEnd = startQ + resolution;
+      const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
+
+      slices.starts[i] = startQ;
+      slices.ends[i] = endQ;
+      slices.depths[i] = it.layoutDepth;
+      slices.titles[i] = internString(it.name);
+      slices.colors![i] = internString(it.color);
+      slices.sliceIds[i] = it.id;
+      slices.isInstant[i] = it.isInstant;
+      slices.isIncomplete[i] = it.isIncomplete;
+    }
+    return slices;
+  }
+}
diff --git a/ui/src/tracks/actual_frames/actual_frames_track_v2.ts b/ui/src/tracks/actual_frames/actual_frames_track_v2.ts
new file mode 100644
index 0000000..c30e673
--- /dev/null
+++ b/ui/src/tracks/actual_frames/actual_frames_track_v2.ts
@@ -0,0 +1,96 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {HSLColor} from '../../common/color';
+import {ColorScheme, makeColorScheme} from '../../common/colorizer';
+import {
+  NAMED_ROW,
+  NamedSliceTrack,
+  NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {EngineProxy, Slice, STR_NULL} from '../../public';
+
+const BLUE = makeColorScheme(new HSLColor('#03A9F4'));    // Blue 500
+const GREEN = makeColorScheme(new HSLColor('#4CAF50'));   // Green 500
+const YELLOW = makeColorScheme(new HSLColor('#FFEB3B'));  // Yellow 500
+const RED = makeColorScheme(new HSLColor('#FF5722'));     // Red 500
+const LIGHT_GREEN =
+    makeColorScheme(new HSLColor('#C0D588'));           // Light Green 500
+const PINK = makeColorScheme(new HSLColor('#F515E0'));  // Pink 500
+
+export const ACTUAL_FRAME_ROW = {
+  // Base columns (tsq, ts, dur, id, depth).
+  ...NAMED_ROW,
+
+  // Chrome-specific columns.
+  jankTag: STR_NULL,
+};
+export type ActualFrameRow = typeof ACTUAL_FRAME_ROW;
+
+export interface ActualFrameTrackTypes extends NamedSliceTrackTypes {
+  row: ActualFrameRow;
+}
+
+export class ActualFramesTrack extends NamedSliceTrack<ActualFrameTrackTypes> {
+  constructor(
+      engine: EngineProxy, maxDepth: number, trackKey: string,
+      private trackIds: number[]) {
+    super({engine, trackKey});
+    this.sliceLayout.maxDepth = maxDepth + 1;
+  }
+
+  // This is used by the base class to call iter().
+  getRowSpec() {
+    return ACTUAL_FRAME_ROW;
+  }
+
+  getSqlSource(): string {
+    return `
+      SELECT
+        s.ts as ts,
+        s.dur as dur,
+        s.layout_depth as depth,
+        s.name as name,
+        s.id as id,
+        afs.jank_tag as jankTag
+      from experimental_slice_layout s
+      join actual_frame_timeline_slice afs using(id)
+      where
+        filter_track_ids = '${this.trackIds.join(',')}'
+    `;
+  }
+
+  rowToSlice(row: ActualFrameRow): Slice {
+    const baseSlice = super.rowToSlice(row);
+    return {...baseSlice, colorScheme: getColorSchemeForJank(row.jankTag)};
+  }
+}
+
+function getColorSchemeForJank(jankTag: string|null): ColorScheme {
+  switch (jankTag) {
+    case 'Self Jank':
+      return RED;
+    case 'Other Jank':
+      return YELLOW;
+    case 'Dropped Frame':
+      return BLUE;
+    case 'Buffer Stuffing':
+    case 'SurfaceFlinger Stuffing':
+      return LIGHT_GREEN;
+    case 'No Jank':
+      return GREEN;
+    default:
+      return PINK;
+  }
+}
diff --git a/ui/src/tracks/actual_frames/index.ts b/ui/src/tracks/actual_frames/index.ts
index fbc3cf7..8fcd597 100644
--- a/ui/src/tracks/actual_frames/index.ts
+++ b/ui/src/tracks/actual_frames/index.ts
@@ -12,11 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {duration, time} from '../../base/time';
-import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
 import {
-  EngineProxy,
   Plugin,
   PluginContext,
   PluginContextTrace,
@@ -24,134 +20,19 @@
 } from '../../public';
 import {getTrackName} from '../../public/utils';
 import {
-  LONG,
-  LONG_NULL,
   NUM,
   NUM_NULL,
   STR,
   STR_NULL,
 } from '../../trace_processor/query_result';
 
+import {ActualFramesTrack} from './actual_frames_track';
+import {
+  ActualFramesTrack as ActualFramesTrackV2,
+} from './actual_frames_track_v2';
+
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
 
-const BLUE_COLOR = '#03A9F4';         // Blue 500
-const GREEN_COLOR = '#4CAF50';        // Green 500
-const YELLOW_COLOR = '#FFEB3B';       // Yellow 500
-const RED_COLOR = '#FF5722';          // Red 500
-const LIGHT_GREEN_COLOR = '#C0D588';  // Light Green 500
-const PINK_COLOR = '#F515E0';         // Pink 500
-
-class SliceTrack extends SliceTrackBase {
-  private maxDur = 0n;
-
-  constructor(
-      private engine: EngineProxy, maxDepth: number, trackKey: string,
-      private trackIds: number[], namespace?: string) {
-    super(maxDepth, trackKey, 'actual_frame_timeline_slice', namespace);
-  }
-
-  async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<SliceData> {
-    if (this.maxDur === 0n) {
-      const maxDurResult = await this.engine.query(`
-    select
-      max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-        as maxDur
-    from experimental_slice_layout
-    where filter_track_ids = '${this.trackIds.join(',')}'
-  `);
-      this.maxDur = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
-    }
-
-    const rawResult = await this.engine.query(`
-  SELECT
-    (s.ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
-    s.ts as ts,
-    max(iif(s.dur = -1, (SELECT end_ts FROM trace_bounds) - s.ts, s.dur))
-        as dur,
-    s.layout_depth as layoutDepth,
-    s.name as name,
-    s.id as id,
-    s.dur = 0 as isInstant,
-    s.dur = -1 as isIncomplete,
-    CASE afs.jank_tag
-      WHEN 'Self Jank' THEN '${RED_COLOR}'
-      WHEN 'Other Jank' THEN '${YELLOW_COLOR}'
-      WHEN 'Dropped Frame' THEN '${BLUE_COLOR}'
-      WHEN 'Buffer Stuffing' THEN '${LIGHT_GREEN_COLOR}'
-      WHEN 'SurfaceFlinger Stuffing' THEN '${LIGHT_GREEN_COLOR}'
-      WHEN 'No Jank' THEN '${GREEN_COLOR}'
-      ELSE '${PINK_COLOR}'
-    END as color
-  from experimental_slice_layout s
-  join actual_frame_timeline_slice afs using(id)
-  where
-    filter_track_ids = '${this.trackIds.join(',')}' and
-    s.ts >= ${start - this.maxDur} and
-    s.ts <= ${end}
-  group by tsq, s.layout_depth
-  order by tsq, s.layout_depth
-`);
-
-    const numRows = rawResult.numRows();
-    const slices: SliceData = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      strings: [],
-      sliceIds: new Float64Array(numRows),
-      starts: new BigInt64Array(numRows),
-      ends: new BigInt64Array(numRows),
-      depths: new Uint16Array(numRows),
-      titles: new Uint16Array(numRows),
-      colors: new Uint16Array(numRows),
-      isInstant: new Uint16Array(numRows),
-      isIncomplete: new Uint16Array(numRows),
-    };
-
-    const stringIndexes = new Map<string, number>();
-    function internString(str: string) {
-      let idx = stringIndexes.get(str);
-      if (idx !== undefined) return idx;
-      idx = slices.strings.length;
-      slices.strings.push(str);
-      stringIndexes.set(str, idx);
-      return idx;
-    }
-
-    const it = rawResult.iter({
-      'tsq': LONG,
-      'ts': LONG,
-      'dur': LONG,
-      'layoutDepth': NUM,
-      'id': NUM,
-      'name': STR,
-      'isInstant': NUM,
-      'isIncomplete': NUM,
-      'color': STR,
-    });
-    for (let i = 0; it.valid(); i++, it.next()) {
-      const startQ = it.tsq;
-      const start = it.ts;
-      const dur = it.dur;
-      const end = start + dur;
-      const minEnd = startQ + resolution;
-      const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
-
-      slices.starts[i] = startQ;
-      slices.ends[i] = endQ;
-      slices.depths[i] = it.layoutDepth;
-      slices.titles[i] = internString(it.name);
-      slices.colors![i] = internString(it.color);
-      slices.sliceIds[i] = it.id;
-      slices.isInstant[i] = it.isInstant;
-      slices.isIncomplete[i] = it.isIncomplete;
-    }
-    return slices;
-  }
-}
-
 class ActualFrames implements Plugin {
   onActivate(_ctx: PluginContext): void {}
 
@@ -211,7 +92,22 @@
         trackIds,
         kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
         track: ({trackKey}) => {
-          return new SliceTrack(
+          return new ActualFramesTrack(
+              engine,
+              maxDepth,
+              trackKey,
+              trackIds,
+          );
+        },
+      });
+
+      ctx.registerStaticTrack({
+        uri: `perfetto.ActualFrames#${upid}.v2`,
+        displayName,
+        trackIds,
+        kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
+        track: ({trackKey}) => {
+          return new ActualFramesTrackV2(
               engine,
               maxDepth,
               trackKey,
diff --git a/ui/src/tracks/async_slices/async_slice_track.ts b/ui/src/tracks/async_slices/async_slice_track.ts
new file mode 100644
index 0000000..5d6e25a
--- /dev/null
+++ b/ui/src/tracks/async_slices/async_slice_track.ts
@@ -0,0 +1,119 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {duration, time} from '../../base/time';
+import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
+import {EngineProxy} from '../../public';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  STR,
+} from '../../trace_processor/query_result';
+
+export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
+
+export class AsyncSliceTrack extends SliceTrackBase {
+  private maxDurNs: duration = 0n;
+
+  constructor(
+      private engine: EngineProxy, maxDepth: number, trackKey: string,
+      private trackIds: number[], namespace?: string) {
+    // TODO is 'slice' right here?
+    super(maxDepth, trackKey, 'slice', namespace);
+  }
+
+  async onBoundsChange(start: time, end: time, resolution: duration):
+      Promise<SliceData> {
+    if (this.maxDurNs === 0n) {
+      const maxDurResult = await this.engine.query(`
+        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts,
+        dur)) as maxDur from experimental_slice_layout where filter_track_ids
+        = '${this.trackIds.join(',')}'
+      `);
+      this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
+    }
+
+    const queryRes = await this.engine.query(`
+      SELECT
+      (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
+        ts,
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as
+        dur, layout_depth as depth, ifnull(name, '[null]') as name, id, dur =
+        0 as isInstant, dur = -1 as isIncomplete
+      from experimental_slice_layout
+      where
+        filter_track_ids = '${this.trackIds.join(',')}' and
+        ts >= ${start - this.maxDurNs} and
+        ts <= ${end}
+      group by tsq, layout_depth
+      order by tsq, layout_depth
+    `);
+
+    const numRows = queryRes.numRows();
+    const slices: SliceData = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      strings: [],
+      sliceIds: new Float64Array(numRows),
+      starts: new BigInt64Array(numRows),
+      ends: new BigInt64Array(numRows),
+      depths: new Uint16Array(numRows),
+      titles: new Uint16Array(numRows),
+      isInstant: new Uint16Array(numRows),
+      isIncomplete: new Uint16Array(numRows),
+    };
+
+    const stringIndexes = new Map<string, number>();
+    function internString(str: string) {
+      let idx = stringIndexes.get(str);
+      if (idx !== undefined) return idx;
+      idx = slices.strings.length;
+      slices.strings.push(str);
+      stringIndexes.set(str, idx);
+      return idx;
+    }
+
+    const it = queryRes.iter({
+      tsq: LONG,
+      ts: LONG,
+      dur: LONG,
+      depth: NUM,
+      name: STR,
+      id: NUM,
+      isInstant: NUM,
+      isIncomplete: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startQ = it.tsq;
+      const start = it.ts;
+      const dur = it.dur;
+      const end = start + dur;
+      const minEnd = startQ + resolution;
+      const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
+
+      slices.starts[row] = startQ;
+      slices.ends[row] = endQ;
+      slices.depths[row] = it.depth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
+    }
+    return slices;
+  }
+}
diff --git a/ui/src/tracks/async_slices/async_slice_track_v2.ts b/ui/src/tracks/async_slices/async_slice_track_v2.ts
new file mode 100644
index 0000000..4e87c9b
--- /dev/null
+++ b/ui/src/tracks/async_slices/async_slice_track_v2.ts
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {NamedSliceTrack} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {Slice} from '../../public';
+
+export class AsyncSliceTrackV2 extends NamedSliceTrack {
+  constructor(
+      args: NewTrackArgs, maxDepth: number, private trackIds: number[]) {
+    super(args);
+    this.sliceLayout.maxDepth = maxDepth + 1;
+  }
+
+  getSqlSource(): string {
+    return `
+    select
+      ts,
+      dur,
+      layout_depth as depth,
+      ifnull(name, '[null]') as name,
+      id,
+      thread_dur as threadDur
+    from experimental_slice_layout
+    where filter_track_ids = '${this.trackIds.join(',')}'
+    `;
+  }
+
+  onUpdatedSlices(slices: Slice[]) {
+    for (const slice of slices) {
+      slice.isHighlighted = (slice === this.hoveredSlice);
+    }
+  }
+}
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index 97d43fa..eb3ad88 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -12,11 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {duration, time} from '../../base/time';
-import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
 import {
-  EngineProxy,
   Plugin,
   PluginContext,
   PluginContextTrace,
@@ -24,109 +20,17 @@
 } from '../../public';
 import {getTrackName} from '../../public/utils';
 import {
-  LONG,
-  LONG_NULL,
   NUM,
   NUM_NULL,
   STR,
   STR_NULL,
 } from '../../trace_processor/query_result';
 
+import {AsyncSliceTrack} from './async_slice_track';
+import {AsyncSliceTrackV2} from './async_slice_track_v2';
+
 export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
 
-class AsyncSliceTrack extends SliceTrackBase {
-  private maxDurNs: duration = 0n;
-
-  constructor(
-      private engine: EngineProxy, maxDepth: number, trackKey: string,
-      private trackIds: number[], namespace?: string) {
-    // TODO is 'slice' right here?
-    super(maxDepth, trackKey, 'slice', namespace);
-  }
-
-  async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<SliceData> {
-    if (this.maxDurNs === 0n) {
-      const maxDurResult = await this.engine.query(`
-        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts,
-        dur)) as maxDur from experimental_slice_layout where filter_track_ids
-        = '${this.trackIds.join(',')}'
-      `);
-      this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
-    }
-
-    const queryRes = await this.engine.query(`
-      SELECT
-      (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
-        ts,
-        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as
-        dur, layout_depth as depth, ifnull(name, '[null]') as name, id, dur =
-        0 as isInstant, dur = -1 as isIncomplete
-      from experimental_slice_layout
-      where
-        filter_track_ids = '${this.trackIds.join(',')}' and
-        ts >= ${start - this.maxDurNs} and
-        ts <= ${end}
-      group by tsq, layout_depth
-      order by tsq, layout_depth
-    `);
-
-    const numRows = queryRes.numRows();
-    const slices: SliceData = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      strings: [],
-      sliceIds: new Float64Array(numRows),
-      starts: new BigInt64Array(numRows),
-      ends: new BigInt64Array(numRows),
-      depths: new Uint16Array(numRows),
-      titles: new Uint16Array(numRows),
-      isInstant: new Uint16Array(numRows),
-      isIncomplete: new Uint16Array(numRows),
-    };
-
-    const stringIndexes = new Map<string, number>();
-    function internString(str: string) {
-      let idx = stringIndexes.get(str);
-      if (idx !== undefined) return idx;
-      idx = slices.strings.length;
-      slices.strings.push(str);
-      stringIndexes.set(str, idx);
-      return idx;
-    }
-
-    const it = queryRes.iter({
-      tsq: LONG,
-      ts: LONG,
-      dur: LONG,
-      depth: NUM,
-      name: STR,
-      id: NUM,
-      isInstant: NUM,
-      isIncomplete: NUM,
-    });
-    for (let row = 0; it.valid(); it.next(), row++) {
-      const startQ = it.tsq;
-      const start = it.ts;
-      const dur = it.dur;
-      const end = start + dur;
-      const minEnd = startQ + resolution;
-      const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
-
-      slices.starts[row] = startQ;
-      slices.ends[row] = endQ;
-      slices.depths[row] = it.depth;
-      slices.titles[row] = internString(it.name);
-      slices.sliceIds[row] = it.id;
-      slices.isInstant[row] = it.isInstant;
-      slices.isIncomplete[row] = it.isIncomplete;
-    }
-    return slices;
-  }
-}
-
 class AsyncSlicePlugin implements Plugin {
   onActivate(_ctx: PluginContext) {}
 
@@ -220,6 +124,20 @@
           );
         },
       });
+
+      ctx.registerStaticTrack({
+        uri: `perfetto.AsyncSlices#${rawName}.v2`,
+        displayName,
+        trackIds,
+        kind: ASYNC_SLICE_TRACK_KIND,
+        track: ({trackKey}) => {
+          return new AsyncSliceTrackV2(
+              {engine, trackKey},
+              maxDepth,
+              trackIds,
+          );
+        },
+      });
     }
   }
 
@@ -288,6 +206,20 @@
           );
         },
       });
+
+      ctx.registerStaticTrack({
+        uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}.v2`,
+        displayName,
+        trackIds,
+        kind: ASYNC_SLICE_TRACK_KIND,
+        track: ({trackKey}) => {
+          return new AsyncSliceTrackV2(
+              {engine: ctx.engine, trackKey},
+              maxDepth,
+              trackIds,
+          );
+        },
+      });
     }
   }
 }
diff --git a/ui/src/tracks/expected_frames/expected_frames_track.ts b/ui/src/tracks/expected_frames/expected_frames_track.ts
new file mode 100644
index 0000000..665cfc5
--- /dev/null
+++ b/ui/src/tracks/expected_frames/expected_frames_track.ts
@@ -0,0 +1,123 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {Duration, duration, time} from '../../base/time';
+import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
+import {EngineProxy} from '../../public';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  STR,
+} from '../../trace_processor/query_result';
+
+export class ExpectedFramesTrack extends SliceTrackBase {
+  private maxDur = Duration.ZERO;
+
+  constructor(
+      private engine: EngineProxy, maxDepth: number, trackKey: string,
+      private trackIds: number[], namespace?: string) {
+    super(maxDepth, trackKey, '', namespace);
+  }
+
+  async onBoundsChange(start: time, end: time, resolution: duration):
+      Promise<SliceData> {
+    if (this.maxDur === Duration.ZERO) {
+      const maxDurResult = await this.engine.query(`
+        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+          as maxDur
+        from experimental_slice_layout
+        where filter_track_ids = '${this.trackIds.join(',')}'
+      `);
+      this.maxDur = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
+    }
+
+    const queryRes = await this.engine.query(`
+      SELECT
+        (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
+        ts,
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
+        layout_depth as layoutDepth,
+        name,
+        id,
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
+      from experimental_slice_layout
+      where
+        filter_track_ids = '${this.trackIds.join(',')}' and
+        ts >= ${start - this.maxDur} and
+        ts <= ${end}
+      group by tsq, layout_depth
+      order by tsq, layout_depth
+    `);
+
+    const numRows = queryRes.numRows();
+    const slices: SliceData = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      strings: [],
+      sliceIds: new Float64Array(numRows),
+      starts: new BigInt64Array(numRows),
+      ends: new BigInt64Array(numRows),
+      depths: new Uint16Array(numRows),
+      titles: new Uint16Array(numRows),
+      colors: new Uint16Array(numRows),
+      isInstant: new Uint16Array(numRows),
+      isIncomplete: new Uint16Array(numRows),
+    };
+
+    const stringIndexes = new Map<string, number>();
+    function internString(str: string) {
+      let idx = stringIndexes.get(str);
+      if (idx !== undefined) return idx;
+      idx = slices.strings.length;
+      slices.strings.push(str);
+      stringIndexes.set(str, idx);
+      return idx;
+    }
+    const greenIndex = internString('#4CAF50');
+
+    const it = queryRes.iter({
+      tsq: LONG,
+      ts: LONG,
+      dur: LONG,
+      layoutDepth: NUM,
+      id: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      const startQ = it.tsq;
+      const start = it.ts;
+      const dur = it.dur;
+      const end = start + dur;
+      const minEnd = startQ + resolution;
+      const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
+
+      slices.starts[row] = startQ;
+      slices.ends[row] = endQ;
+      slices.depths[row] = it.layoutDepth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
+      slices.colors![row] = greenIndex;
+    }
+    return slices;
+  }
+}
diff --git a/ui/src/tracks/expected_frames/expected_frames_track_v2.ts b/ui/src/tracks/expected_frames/expected_frames_track_v2.ts
new file mode 100644
index 0000000..268b4b6
--- /dev/null
+++ b/ui/src/tracks/expected_frames/expected_frames_track_v2.ts
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {HSLColor} from '../../common/color';
+import {makeColorScheme} from '../../common/colorizer';
+import {NamedRow, NamedSliceTrack} from '../../frontend/named_slice_track';
+import {EngineProxy, Slice} from '../../public';
+
+const GREEN = makeColorScheme(new HSLColor('#4CAF50'));  // Green 500
+
+export class ExpectedFramesTrack extends NamedSliceTrack {
+  constructor(
+      engine: EngineProxy, maxDepth: number, trackKey: string,
+      private trackIds: number[]) {
+    super({engine, trackKey});
+    this.sliceLayout.maxDepth = maxDepth + 1;
+  }
+
+  getSqlSource(): string {
+    return `
+      SELECT
+        ts,
+        dur,
+        layout_depth as depth,
+        name,
+        id
+      from experimental_slice_layout
+      where
+        filter_track_ids = '${this.trackIds.join(',')}'
+    `;
+  }
+
+  rowToSlice(row: NamedRow): Slice {
+    const baseSlice = super.rowToSlice(row);
+    return {...baseSlice, colorScheme: GREEN};
+  }
+}
diff --git a/ui/src/tracks/expected_frames/index.ts b/ui/src/tracks/expected_frames/index.ts
index 8cc1833..10b3f44 100644
--- a/ui/src/tracks/expected_frames/index.ts
+++ b/ui/src/tracks/expected_frames/index.ts
@@ -12,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {Duration, duration, time} from '../../base/time';
+// import { NamedSliceTrack } from 'src/frontend/named_slice_track';
 import {
-  SliceData,
-  SliceTrackBase,
-} from '../../frontend/slice_track_base';
-import {
-  EngineProxy,
   Plugin,
   PluginContext,
   PluginContextTrace,
@@ -27,115 +21,19 @@
 } from '../../public';
 import {getTrackName} from '../../public/utils';
 import {
-  LONG,
-  LONG_NULL,
   NUM,
   NUM_NULL,
   STR,
   STR_NULL,
 } from '../../trace_processor/query_result';
 
+import {ExpectedFramesTrack} from './expected_frames_track';
+import {
+  ExpectedFramesTrack as ExpectedFramesTrackV2,
+} from './expected_frames_track_v2';
+
 export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
 
-class SliceTrack extends SliceTrackBase {
-  private maxDur = Duration.ZERO;
-
-  constructor(
-      private engine: EngineProxy, maxDepth: number, trackKey: string,
-      private trackIds: number[], namespace?: string) {
-    super(maxDepth, trackKey, '', namespace);
-  }
-
-  async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<SliceData> {
-    if (this.maxDur === Duration.ZERO) {
-      const maxDurResult = await this.engine.query(`
-        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-          as maxDur
-        from experimental_slice_layout
-        where filter_track_ids = '${this.trackIds.join(',')}'
-      `);
-      this.maxDur = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
-    }
-
-    const queryRes = await this.engine.query(`
-      SELECT
-        (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
-        ts,
-        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
-        layout_depth as layoutDepth,
-        name,
-        id,
-        dur = 0 as isInstant,
-        dur = -1 as isIncomplete
-      from experimental_slice_layout
-      where
-        filter_track_ids = '${this.trackIds.join(',')}' and
-        ts >= ${start - this.maxDur} and
-        ts <= ${end}
-      group by tsq, layout_depth
-      order by tsq, layout_depth
-    `);
-
-    const numRows = queryRes.numRows();
-    const slices: SliceData = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      strings: [],
-      sliceIds: new Float64Array(numRows),
-      starts: new BigInt64Array(numRows),
-      ends: new BigInt64Array(numRows),
-      depths: new Uint16Array(numRows),
-      titles: new Uint16Array(numRows),
-      colors: new Uint16Array(numRows),
-      isInstant: new Uint16Array(numRows),
-      isIncomplete: new Uint16Array(numRows),
-    };
-
-    const stringIndexes = new Map<string, number>();
-    function internString(str: string) {
-      let idx = stringIndexes.get(str);
-      if (idx !== undefined) return idx;
-      idx = slices.strings.length;
-      slices.strings.push(str);
-      stringIndexes.set(str, idx);
-      return idx;
-    }
-    const greenIndex = internString('#4CAF50');
-
-    const it = queryRes.iter({
-      tsq: LONG,
-      ts: LONG,
-      dur: LONG,
-      layoutDepth: NUM,
-      id: NUM,
-      name: STR,
-      isInstant: NUM,
-      isIncomplete: NUM,
-    });
-    for (let row = 0; it.valid(); it.next(), ++row) {
-      const startQ = it.tsq;
-      const start = it.ts;
-      const dur = it.dur;
-      const end = start + dur;
-      const minEnd = startQ + resolution;
-      const endQ = BIMath.max(BIMath.quant(end, resolution), minEnd);
-
-      slices.starts[row] = startQ;
-      slices.ends[row] = endQ;
-      slices.depths[row] = it.layoutDepth;
-      slices.titles[row] = internString(it.name);
-      slices.sliceIds[row] = it.id;
-      slices.isInstant[row] = it.isInstant;
-      slices.isIncomplete[row] = it.isIncomplete;
-      slices.colors![row] = greenIndex;
-    }
-    return slices;
-  }
-}
-
 class ExpectedFramesPlugin implements Plugin {
   onActivate(_ctx: PluginContext): void {}
 
@@ -195,7 +93,22 @@
         trackIds,
         kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
         track: ({trackKey}) => {
-          return new SliceTrack(
+          return new ExpectedFramesTrack(
+              engine,
+              maxDepth,
+              trackKey,
+              trackIds,
+          );
+        },
+      });
+
+      ctx.registerStaticTrack({
+        uri: `perfetto.ExpectedFrames#${upid}.v2`,
+        displayName,
+        trackIds,
+        kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
+        track: ({trackKey}) => {
+          return new ExpectedFramesTrackV2(
               engine,
               maxDepth,
               trackKey,
diff --git a/ui/src/tracks/thread_state/thread_state_track_v2.ts b/ui/src/tracks/thread_state/thread_state_track_v2.ts
deleted file mode 100644
index c61ca5b..0000000
--- a/ui/src/tracks/thread_state/thread_state_track_v2.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Actions} from '../../common/actions';
-import {colorForState} from '../../common/colorizer';
-import {Selection} from '../../common/state';
-import {translateState} from '../../common/thread_state';
-import {
-  BASE_ROW,
-  BaseSliceTrack,
-  BaseSliceTrackTypes,
-  OnSliceClickArgs,
-} from '../../frontend/base_slice_track';
-import {globals} from '../../frontend/globals';
-import {
-  SLICE_LAYOUT_FLAT_DEFAULTS,
-  SliceLayout,
-} from '../../frontend/slice_layout';
-import {NewTrackArgs} from '../../frontend/track';
-import {NUM_NULL, STR} from '../../trace_processor/query_result';
-
-export const THREAD_STATE_ROW = {
-  ...BASE_ROW,
-  state: STR,
-  ioWait: NUM_NULL,
-};
-
-export type ThreadStateRow = typeof THREAD_STATE_ROW;
-
-export interface ThreadStateTrackTypes extends BaseSliceTrackTypes {
-  row: ThreadStateRow;
-}
-
-export class ThreadStateTrack extends BaseSliceTrack<ThreadStateTrackTypes> {
-  protected sliceLayout: SliceLayout = {...SLICE_LAYOUT_FLAT_DEFAULTS};
-
-  constructor(args: NewTrackArgs, private utid: number) {
-    super(args);
-  }
-
-  // This is used by the base class to call iter().
-  getRowSpec(): ThreadStateTrackTypes['row'] {
-    return THREAD_STATE_ROW;
-  }
-
-  getSqlSource(): string {
-    // Do not display states 'x' and 'S' (dead & sleeping).
-    const sql = `
-      select
-        id,
-        ts,
-        dur,
-        cpu,
-        state,
-        io_wait as ioWait,
-        0 as depth
-      from thread_state
-      where
-        utid = ${this.utid} and
-        state != 'x' and
-        state != 'S'
-    `;
-    return sql;
-  }
-
-  rowToSlice(row: ThreadStateTrackTypes['row']):
-      ThreadStateTrackTypes['slice'] {
-    const baseSlice = super.rowToSlice(row);
-    const ioWait = row.ioWait === null ? undefined : !!row.ioWait;
-    const title = translateState(row.state, ioWait);
-    const color = colorForState(title);
-    return {...baseSlice, title, colorScheme: color};
-  }
-
-  onUpdatedSlices(slices: ThreadStateTrackTypes['slice'][]) {
-    for (const slice of slices) {
-      slice.isHighlighted = (slice === this.hoveredSlice);
-    }
-  }
-
-  onSliceClick(args: OnSliceClickArgs<ThreadStateTrackTypes['slice']>) {
-    globals.makeSelection(Actions.selectThreadState({
-      id: args.slice.id,
-      trackKey: this.trackKey,
-    }));
-  }
-
-  protected isSelectionHandled(selection: Selection): boolean {
-    return selection.kind === 'THREAD_STATE';
-  }
-}
diff --git a/ui/src/tracks/thread_state/thread_state_v2.ts b/ui/src/tracks/thread_state/thread_state_v2.ts
index 54dec7d..c35ed5c 100644
--- a/ui/src/tracks/thread_state/thread_state_v2.ts
+++ b/ui/src/tracks/thread_state/thread_state_v2.ts
@@ -35,19 +35,13 @@
   state: STR,
   ioWait: NUM_NULL,
 };
+
 export type ThreadStateRow = typeof THREAD_STATE_ROW;
 
-
-export interface ThreadStateTrackConfig {
-  utid: number;
-}
-
 export interface ThreadStateTrackTypes extends BaseSliceTrackTypes {
   row: ThreadStateRow;
 }
 
-export const THREAD_STATE_TRACK_V2_KIND = 'ThreadStateTrackV2';
-
 export class ThreadStateTrack extends BaseSliceTrack<ThreadStateTrackTypes> {
   protected sliceLayout: SliceLayout = {...SLICE_LAYOUT_FLAT_DEFAULTS};
 
@@ -62,11 +56,15 @@
 
   getSqlSource(): string {
     // Do not display states 'x' and 'S' (dead & sleeping).
+    // Note: Thread state tracks V1 basically ignores incomplete slices, faking
+    // their duration as 1 instead. Let's just do this here as well for now to
+    // achieve feature parity with tracks V1 and tackle the issue of overlapping
+    // incomplete slices later.
     return `
       select
         id,
         ts,
-        dur,
+        max(dur, 1) as dur,
         cpu,
         state,
         io_wait as ioWait,