Merge "traced_probes: Use libstatspull to collect statsd atoms"
diff --git a/Android.bp b/Android.bp
index 336e0e8..549ea74 100644
--- a/Android.bp
+++ b/Android.bp
@@ -9923,6 +9923,7 @@
         "src/trace_processor/prelude/functions/create_function_internal.cc",
         "src/trace_processor/prelude/functions/create_view_function.cc",
         "src/trace_processor/prelude/functions/import.cc",
+        "src/trace_processor/prelude/functions/layout_functions.cc",
         "src/trace_processor/prelude/functions/pprof_functions.cc",
         "src/trace_processor/prelude/functions/register_function.cc",
         "src/trace_processor/prelude/functions/sqlite3_str_split.cc",
diff --git a/BUILD b/BUILD
index 692c44d..c9bdd52 100644
--- a/BUILD
+++ b/BUILD
@@ -1767,6 +1767,8 @@
         "src/trace_processor/prelude/functions/create_view_function.h",
         "src/trace_processor/prelude/functions/import.cc",
         "src/trace_processor/prelude/functions/import.h",
+        "src/trace_processor/prelude/functions/layout_functions.cc",
+        "src/trace_processor/prelude/functions/layout_functions.h",
         "src/trace_processor/prelude/functions/pprof_functions.cc",
         "src/trace_processor/prelude/functions/pprof_functions.h",
         "src/trace_processor/prelude/functions/register_function.cc",
diff --git a/CHANGELOG b/CHANGELOG
index 8194fa8..7662692 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,7 +7,15 @@
   UI:
     *
   SDK:
-    *
+    * Changed the type of the static constexpr metadata on protozero
+      generated bindings from a function returning the metadata to
+      metadata itself. For a field 'foo' the variable kFoo previously
+      defined as:
+      `static constexpr FieldMetadata_Foo kFoo() = { return {}; };`
+      it is now defined as:
+      `static constexpr FieldMetadata_Foo kFoo;`
+      This is a breaking change for users who directly access field
+      metadata.
 
 v33.0 - 2023-03-02:
   All:
diff --git a/docs/visualization/perfetto-ui.md b/docs/visualization/perfetto-ui.md
index 90d5899..5f094b6 100644
--- a/docs/visualization/perfetto-ui.md
+++ b/docs/visualization/perfetto-ui.md
@@ -6,72 +6,6 @@
 
 ## UI Tips and Tricks
 
-### Debug Slices
-
-Sometimes you may want to insert some fake slices into the timeline to help
-with your understanding of the data. You can do so by inserting rows into a
-magic `debug_slices` table.
-
-`debug_slices` table has five columns:
-
-* `id` (integer) [optional] If present, Perfetto UI will use it as slice id to
-  open the details panel when you click on the slices.
-* `name` (string) [optional] The displayed slice title.
-* `ts` (integer) [required] Start of the slice, in nanoseconds.
-* `dur` (integer) [required] Duration of the slice, in nanoseconds. Determines
-  slice width.
-* `depth` (integer) [optional] The row at which the slice is drawn. Depth 0 is
-  the first row.
-
-You can open the debug track by going to the "Sample queries" menu on the
-left, and clicking "Show Debug Track". A debug slice track will become pinned to
-the top and will initially be empty. After you insert slices in the
-`debug_slices` table, you can click the reload button on the track to refresh
-the information shown in that track.
-
-Here is a simple example with random slices to illustrate the use:
-
-```sql
-CREATE VIEW rand_slices AS SELECT * FROM SLICE
-  ORDER BY RANDOM() LIMIT 2000;
-
-INSERT INTO debug_slices(id, name, ts, dur, depth)
-  SELECT id, name, ts, dur, depth FROM RAND_SLICES;
-```
-
-After you click the reload button, you should see the slices in the debug track.
-
-![Example of debug slices](/docs/images/debug-slices-random.png)
-
-Once you're done, you can click the X button to hide the track, and you can
-clear the `debug_slices` table (`DELETE FROM debug_slices`) to clear the track.
-
-A more interesting example is seeing RAIL modes in chrome traces:
-
-```sql
-SELECT RUN_METRIC('chrome/rail_modes.sql');
-
--- Depth 0 is the unified RAIL Mode
-INSERT INTO debug_slices
-  SELECT NULL, rail_mode, ts, dur, 0 FROM combined_overall_rail_slices;
-
--- Depth 2+ are for each Renderer process with depth 1 left blank
-INSERT INTO debug_slices
-  SELECT NULL, short_name, ts, dur, depth + 1 FROM rail_mode_slices,
-    (SELECT track_id, row_number() OVER () AS depth FROM
-      (SELECT DISTINCT track_id FROM rail_mode_slices)) depth_map,
-    rail_modes
-  WHERE depth_map.track_id = rail_mode_slices.track_id
-    AND rail_mode=rail_modes.mode;
-```
-
-This produces a visualization like this:
-
-![RAIL modes in Debug Track](/docs/images/rail-mode-debug-slices.png)
-
-Note: There is no equivalent debug counters feature yet, but the feature request
-is tracked on [b/168886909](http://b/168886909)).
-
 ### Pivot Tables
 
 To use pivot tables in the Perfetto UI, you will need to enable the
diff --git a/include/perfetto/ext/base/subprocess.h b/include/perfetto/ext/base/subprocess.h
index cca6c29..6102832 100644
--- a/include/perfetto/ext/base/subprocess.h
+++ b/include/perfetto/ext/base/subprocess.h
@@ -269,7 +269,6 @@
   void TryPushStdin();
   void TryReadStdoutAndErr();
   void TryReadExitStatus();
-  void KillAtMostOnce();
   bool PollInternal(int poll_timeout_ms);
 #endif
 
diff --git a/include/perfetto/ext/ipc/host.h b/include/perfetto/ext/ipc/host.h
index 0311089..e0f8af4 100644
--- a/include/perfetto/ext/ipc/host.h
+++ b/include/perfetto/ext/ipc/host.h
@@ -72,6 +72,9 @@
   virtual void AdoptConnectedSocket_Fuchsia(
       base::ScopedSocketHandle,
       std::function<bool(int)> send_fd_cb) = 0;
+
+  // Overrides the default send timeout for the per-connection sockets.
+  virtual void SetSocketSendTimeoutMs(uint32_t timeout_ms) = 0;
 };
 
 }  // namespace ipc
diff --git a/include/perfetto/protozero/proto_utils.h b/include/perfetto/protozero/proto_utils.h
index a0cc7bf..cea6ab3 100644
--- a/include/perfetto/protozero/proto_utils.h
+++ b/include/perfetto/protozero/proto_utils.h
@@ -289,21 +289,6 @@
   using message_type = MessageType;
 };
 
-namespace internal {
-
-// Ideally we would create variables of FieldMetadata<...> type directly,
-// but before C++17's support for constexpr inline variables arrive, we have to
-// actually use pointers to inline functions instead to avoid having to define
-// symbols in *.pbzero.cc files.
-//
-// Note: protozero bindings will generate Message::kFieldName variable and which
-// can then be passed to TRACE_EVENT macro for inline writing of typed messages.
-// The fact that the former can be passed to the latter is a part of the stable
-// API, while the particular type is not and users should not rely on it.
-template <typename T>
-using FieldMetadataHelper = T (*)(void);
-
-}  // namespace internal
 }  // namespace proto_utils
 }  // namespace protozero
 
diff --git a/include/perfetto/tracing/internal/track_event_data_source.h b/include/perfetto/tracing/internal/track_event_data_source.h
index 0a636ef..d1e17a6 100644
--- a/include/perfetto/tracing/internal/track_event_data_source.h
+++ b/include/perfetto/tracing/internal/track_event_data_source.h
@@ -661,6 +661,86 @@
             typename TimestampTypeCheck = typename std::enable_if<
                 IsValidTimestamp<TimestampType>()>::type,
             typename TrackTypeCheck =
+                typename std::enable_if<IsValidTrack<TrackType>()>::type>
+  static perfetto::EventContext WriteTrackEvent(
+      typename Base::TraceContext& ctx,
+      const CategoryType& category,
+      const EventNameType& event_name,
+      perfetto::protos::pbzero::TrackEvent::Type type,
+      const TrackType& track,
+      const TimestampType& timestamp) PERFETTO_NO_INLINE {
+    using CatTraits = CategoryTraits<CategoryType>;
+    const Category* static_category =
+        CatTraits::GetStaticCategory(Registry, category);
+
+    const TrackEventTlsState& tls_state = *ctx.GetCustomTlsState();
+    TraceTimestamp trace_timestamp = ::perfetto::TraceTimestampTraits<
+        TimestampType>::ConvertTimestampToTraceTimeNs(timestamp);
+
+    TraceWriterBase* trace_writer = ctx.tls_inst_->trace_writer.get();
+    // Make sure incremental state is valid.
+    TrackEventIncrementalState* incr_state = ctx.GetIncrementalState();
+    TrackEventInternal::ResetIncrementalStateIfRequired(
+        trace_writer, incr_state, tls_state, trace_timestamp);
+
+    // Write the track descriptor before any event on the track.
+    if (track) {
+      TrackEventInternal::WriteTrackDescriptorIfNeeded(
+          track, trace_writer, incr_state, tls_state, trace_timestamp);
+    }
+
+    // Write the event itself.
+    bool on_current_thread_track =
+        (&track == &TrackEventInternal::kDefaultTrack);
+    auto event_ctx = TrackEventInternal::WriteEvent(
+        trace_writer, incr_state, tls_state, static_category, type,
+        trace_timestamp, on_current_thread_track);
+    // event name should be emitted with `TRACE_EVENT_BEGIN` macros
+    // but not with `TRACE_EVENT_END`.
+    if (type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
+      TrackEventInternal::WriteEventName(event_name, event_ctx, tls_state);
+    }
+    // Write dynamic categories (except for events that don't require
+    // categories). For counter events, the counter name (and optional
+    // category) is stored as part of the track descriptor instead being
+    // recorded with individual events.
+    if (CatTraits::kIsDynamic &&
+        type != protos::pbzero::TrackEvent::TYPE_SLICE_END &&
+        type != protos::pbzero::TrackEvent::TYPE_COUNTER) {
+      DynamicCategory dynamic_category =
+          CatTraits::GetDynamicCategory(category);
+      Category cat = Category::FromDynamicCategory(dynamic_category);
+      cat.ForEachGroupMember([&](const char* member_name, size_t name_size) {
+        event_ctx.event()->add_categories(member_name, name_size);
+        return true;
+      });
+    }
+    if (type == protos::pbzero::TrackEvent::TYPE_UNSPECIFIED) {
+      // Explicitly clear the track, so that the event is not associated
+      // with the default track, but instead uses the legacy mechanism
+      // based on the phase and pid/tid override.
+      event_ctx.event()->set_track_uuid(0);
+    } else if (!on_current_thread_track) {
+      // We emit these events using TrackDescriptors, and we cannot emit
+      // events on behalf of other processes using the TrackDescriptor
+      // format. Chrome is the only user of events with explicit process
+      // ids and currently only Chrome emits PHASE_MEMORY_DUMP events
+      // with an explicit process id, so we should be fine here.
+      // TODO(mohitms): Get rid of events with explicit process ids
+      // entirely.
+      event_ctx.event()->set_track_uuid(track.uuid);
+    }
+
+    return event_ctx;
+  }
+
+  template <typename CategoryType,
+            typename EventNameType,
+            typename TrackType = Track,
+            typename TimestampType = uint64_t,
+            typename TimestampTypeCheck = typename std::enable_if<
+                IsValidTimestamp<TimestampType>()>::type,
+            typename TrackTypeCheck =
                 typename std::enable_if<IsValidTrack<TrackType>()>::type,
             typename... Arguments>
   static void TraceForCategoryImpl(
@@ -672,8 +752,6 @@
       const TimestampType& timestamp,
       Arguments&&... args) PERFETTO_ALWAYS_INLINE {
     using CatTraits = CategoryTraits<CategoryType>;
-    const Category* static_category =
-        CatTraits::GetStaticCategory(Registry, category);
     TraceWithInstances(
         instances, category, [&](typename Base::TraceContext ctx) {
           // If this category is dynamic, first check whether it's enabled.
@@ -683,69 +761,10 @@
             return;
           }
 
-          const TrackEventTlsState& tls_state = *ctx.GetCustomTlsState();
-          TraceTimestamp trace_timestamp = ::perfetto::TraceTimestampTraits<
-              TimestampType>::ConvertTimestampToTraceTimeNs(timestamp);
-
-          TraceWriterBase* trace_writer = ctx.tls_inst_->trace_writer.get();
-          // Make sure incremental state is valid.
-          TrackEventIncrementalState* incr_state = ctx.GetIncrementalState();
-          TrackEventInternal::ResetIncrementalStateIfRequired(
-              trace_writer, incr_state, tls_state, trace_timestamp);
-
-          // Write the track descriptor before any event on the track.
-          if (track) {
-            TrackEventInternal::WriteTrackDescriptorIfNeeded(
-                track, trace_writer, incr_state, tls_state, trace_timestamp);
-          }
-
-          // Write the event itself.
-          {
-            bool on_current_thread_track =
-                (&track == &TrackEventInternal::kDefaultTrack);
-            auto event_ctx = TrackEventInternal::WriteEvent(
-                trace_writer, incr_state, tls_state, static_category, type,
-                trace_timestamp, on_current_thread_track);
-            // event name should be emitted with `TRACE_EVENT_BEGIN` macros
-            // but not with `TRACE_EVENT_END`.
-            if (type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
-              TrackEventInternal::WriteEventName(event_name, event_ctx,
-                                                 tls_state);
-            }
-            // Write dynamic categories (except for events that don't require
-            // categories). For counter events, the counter name (and optional
-            // category) is stored as part of the track descriptor instead being
-            // recorded with individual events.
-            if (CatTraits::kIsDynamic &&
-                type != protos::pbzero::TrackEvent::TYPE_SLICE_END &&
-                type != protos::pbzero::TrackEvent::TYPE_COUNTER) {
-              DynamicCategory dynamic_category =
-                  CatTraits::GetDynamicCategory(category);
-              Category cat = Category::FromDynamicCategory(dynamic_category);
-              cat.ForEachGroupMember(
-                  [&](const char* member_name, size_t name_size) {
-                    event_ctx.event()->add_categories(member_name, name_size);
-                    return true;
-                  });
-            }
-            if (type == protos::pbzero::TrackEvent::TYPE_UNSPECIFIED) {
-              // Explicitly clear the track, so that the event is not associated
-              // with the default track, but instead uses the legacy mechanism
-              // based on the phase and pid/tid override.
-              event_ctx.event()->set_track_uuid(0);
-            } else if (!on_current_thread_track) {
-              // We emit these events using TrackDescriptors, and we cannot emit
-              // events on behalf of other processes using the TrackDescriptor
-              // format. Chrome is the only user of events with explicit process
-              // ids and currently only Chrome emits PHASE_MEMORY_DUMP events
-              // with an explicit process id, so we should be fine here.
-              // TODO(mohitms): Get rid of events with explicit process ids
-              // entirely.
-              event_ctx.event()->set_track_uuid(track.uuid);
-            }
-            WriteTrackEventArgs(std::move(event_ctx),
-                                std::forward<Arguments>(args)...);
-          }  // event_ctx
+          auto event_ctx = WriteTrackEvent(ctx, category, event_name, type,
+                                           track, timestamp);
+          WriteTrackEventArgs(std::move(event_ctx),
+                              std::forward<Arguments>(args)...);
         });
   }
 
diff --git a/include/perfetto/tracing/internal/track_event_legacy.h b/include/perfetto/tracing/internal/track_event_legacy.h
index 95b40ca..2ad4813 100644
--- a/include/perfetto/tracing/internal/track_event_legacy.h
+++ b/include/perfetto/tracing/internal/track_event_legacy.h
@@ -17,6 +17,11 @@
 #ifndef INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_LEGACY_H_
 #define INCLUDE_PERFETTO_TRACING_INTERNAL_TRACK_EVENT_LEGACY_H_
 
+#include "perfetto/base/build_config.h"
+#include "perfetto/tracing/event_context.h"
+#include "perfetto/tracing/track.h"
+#include "protos/perfetto/trace/track_event/track_event.pbzero.h"
+
 #ifndef PERFETTO_ENABLE_LEGACY_TRACE_EVENTS
 #define PERFETTO_ENABLE_LEGACY_TRACE_EVENTS 0
 #endif
diff --git a/include/perfetto/tracing/internal/write_track_event_args.h b/include/perfetto/tracing/internal/write_track_event_args.h
index 7d2aca9..fe5d149 100644
--- a/include/perfetto/tracing/internal/write_track_event_args.h
+++ b/include/perfetto/tracing/internal/write_track_event_args.h
@@ -73,6 +73,24 @@
   return IsValidTraceLambdaTakingReferenceImpl<T>(nullptr);
 }
 
+template <typename T>
+static constexpr bool IsFieldMetadataTypeImpl(
+    typename std::enable_if<
+        std::is_base_of<protozero::proto_utils::FieldMetadataBase,
+                        T>::value>::type* = nullptr) {
+  return true;
+}
+
+template <typename T>
+static constexpr bool IsFieldMetadataTypeImpl(...) {
+  return false;
+}
+
+template <typename T>
+static constexpr bool IsFieldMetadataType() {
+  return IsFieldMetadataTypeImpl<T>(nullptr);
+}
+
 }  // namespace
 
 // Write an old-style lambda taking an EventContext (without a reference)
@@ -95,13 +113,15 @@
                                                 ArgValue&& arg_value,
                                                 Args&&... args);
 
-template <typename FieldMetadataType, typename ArgValue, typename... Args>
-PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(
-    EventContext event_ctx,
-    protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>
-        field_name,
-    ArgValue&& arg_value,
-    Args&&... args);
+template <typename FieldMetadataType,
+          typename ArgValue,
+          typename... Args,
+          typename FieldMetadataTypeCheck = typename std::enable_if<
+              IsFieldMetadataType<FieldMetadataType>()>::type>
+PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(EventContext event_ctx,
+                                                FieldMetadataType field_name,
+                                                ArgValue&& arg_value,
+                                                Args&&... args);
 
 template <typename ArgumentFunction,
           typename... Args,
@@ -119,13 +139,14 @@
 }
 
 // Write one typed message and recursively write the rest of the arguments.
-template <typename FieldMetadataType, typename ArgValue, typename... Args>
-PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(
-    EventContext event_ctx,
-    protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>
-        field_name,
-    ArgValue&& arg_value,
-    Args&&... args) {
+template <typename FieldMetadataType,
+          typename ArgValue,
+          typename... Args,
+          typename FieldMetadataTypeCheck>
+PERFETTO_ALWAYS_INLINE void WriteTrackEventArgs(EventContext event_ctx,
+                                                FieldMetadataType field_name,
+                                                ArgValue&& arg_value,
+                                                Args&&... args) {
   static_assert(std::is_base_of<protozero::proto_utils::FieldMetadataBase,
                                 FieldMetadataType>::value,
                 "");
diff --git a/include/perfetto/tracing/traced_proto.h b/include/perfetto/tracing/traced_proto.h
index 042b96e..6e7b27b 100644
--- a/include/perfetto/tracing/traced_proto.h
+++ b/include/perfetto/tracing/traced_proto.h
@@ -102,7 +102,7 @@
   // repeated and non-repeated complex fields are supported.
   template <typename FieldMetadata>
   TracedProto<typename FieldMetadata::cpp_field_type> WriteNestedMessage(
-      protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>) {
+      FieldMetadata) {
     static_assert(std::is_base_of<MessageType,
                                   typename FieldMetadata::message_type>::value,
                   "Field should belong to the current message");
@@ -121,8 +121,7 @@
   // string), but requires the |field| to be non-repeateable (i.e. optional).
   // For repeatable fields, AppendValue or AppendFrom should be used.
   template <typename FieldMetadata, typename ValueType>
-  void Set(protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
-           ValueType&& value) {
+  void Set(FieldMetadata, ValueType&& value) {
     static_assert(std::is_base_of<MessageType,
                                   typename FieldMetadata::message_type>::value,
                   "Field should belong to the current message");
@@ -148,9 +147,7 @@
   // current message. If the field is not repeated, Set() should be used
   // instead.
   template <typename FieldMetadata, typename ValueType>
-  void AppendValue(
-      protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
-      ValueType&& value) {
+  void AppendValue(FieldMetadata, ValueType&& value) {
     static_assert(std::is_base_of<MessageType,
                                   typename FieldMetadata::message_type>::value,
                   "Field should belong to the current message");
@@ -174,9 +171,7 @@
   // current message. If the field is not repeated, Set() should be used
   // instead.
   template <typename FieldMetadata, typename ValueType>
-  void AppendFrom(
-      protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
-      ValueType&& value) {
+  void AppendFrom(FieldMetadata, ValueType&& value) {
     static_assert(std::is_base_of<MessageType,
                                   typename FieldMetadata::message_type>::value,
                   "Field should belong to the current message");
@@ -199,8 +194,7 @@
   // above and make these methods private.
   template <typename FieldMetadata>
   TracedProto<typename FieldMetadata::cpp_field_type> WriteNestedMessage() {
-    return WriteNestedMessage(
-        protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>());
+    return WriteNestedMessage(FieldMetadata());
   }
 
  private:
@@ -401,10 +395,9 @@
 }
 
 template <typename MessageType, typename FieldMetadataType, typename ValueType>
-void WriteTracedProtoField(
-    TracedProto<MessageType>& message,
-    protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadataType>,
-    ValueType&& value) {
+void WriteTracedProtoField(TracedProto<MessageType>& message,
+                           FieldMetadataType,
+                           ValueType&& value) {
   static_assert(
       std::is_base_of<protozero::proto_utils::FieldMetadataBase,
                       FieldMetadataType>::value,
diff --git a/include/perfetto/tracing/traced_value.h b/include/perfetto/tracing/traced_value.h
index 366db4b..5a26753 100644
--- a/include/perfetto/tracing/traced_value.h
+++ b/include/perfetto/tracing/traced_value.h
@@ -286,11 +286,10 @@
   // Create a |TracedDictionary| which will populate the given field of the
   // given |message|.
   template <typename MessageType, typename FieldMetadata>
-  inline TracedDictionary(
-      MessageType* message,
-      protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
-      EventContext* event_context,
-      internal::CheckedScope* parent_scope)
+  inline TracedDictionary(MessageType* message,
+                          FieldMetadata,
+                          EventContext* event_context,
+                          internal::CheckedScope* parent_scope)
       : message_(message),
         field_id_(FieldMetadata::kFieldId),
         event_context_(event_context),
diff --git a/src/ipc/host_impl.cc b/src/ipc/host_impl.cc
index 45030ed..e9a50d1 100644
--- a/src/ipc/host_impl.cc
+++ b/src/ipc/host_impl.cc
@@ -147,6 +147,12 @@
   PERFETTO_DCHECK(client_connection->send_fd_cb_fuchsia);
 }
 
+void HostImpl::SetSocketSendTimeoutMs(uint32_t timeout_ms) {
+  PERFETTO_DCHECK_THREAD(thread_checker_);
+  // Should be less than the watchdog period (30s).
+  socket_tx_timeout_ms_ = timeout_ms;
+}
+
 void HostImpl::OnNewIncomingConnection(
     base::UnixSocket*,
     std::unique_ptr<base::UnixSocket> new_conn) {
@@ -156,8 +162,7 @@
   clients_by_socket_[new_conn.get()] = client.get();
   client->id = client_id;
   client->sock = std::move(new_conn);
-  // Watchdog is 30 seconds, so set the socket timeout to 10 seconds.
-  client->sock->SetTxTimeout(10000);
+  client->sock->SetTxTimeout(socket_tx_timeout_ms_);
   clients_[client_id] = std::move(client);
 }
 
diff --git a/src/ipc/host_impl.h b/src/ipc/host_impl.h
index 2f0a4f8..788b81a 100644
--- a/src/ipc/host_impl.h
+++ b/src/ipc/host_impl.h
@@ -33,6 +33,8 @@
 namespace perfetto {
 namespace ipc {
 
+constexpr uint32_t kDefaultIpcTxTimeoutMs = 10000;
+
 class HostImpl : public Host, public base::UnixSocket::EventListener {
  public:
   HostImpl(const char* socket_name, base::TaskRunner*);
@@ -45,6 +47,7 @@
   void AdoptConnectedSocket_Fuchsia(
       base::ScopedSocketHandle,
       std::function<bool(int)> send_fd_cb) override;
+  void SetSocketSendTimeoutMs(uint32_t timeout_ms) override;
 
   // base::UnixSocket::EventListener implementation.
   void OnNewIncomingConnection(base::UnixSocket*,
@@ -94,6 +97,7 @@
   std::map<base::UnixSocket*, ClientConnection*> clients_by_socket_;
   ServiceID last_service_id_ = 0;
   ClientID last_client_id_ = 0;
+  uint32_t socket_tx_timeout_ms_ = kDefaultIpcTxTimeoutMs;
   PERFETTO_THREAD_CHECKER(thread_checker_)
   base::WeakPtrFactory<HostImpl> weak_ptr_factory_;  // Keep last.
 };
diff --git a/src/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 6a80bf6..bcfbf28 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -913,14 +913,7 @@
     $cpp_type$,
     $message_cpp_type$>;
 
-// Ceci n'est pas une pipe.
-// This is actually a variable of FieldMetadataHelper<FieldMetadata<...>>
-// type (and users are expected to use it as such, hence kCamelCase name).
-// It is declared as a function to keep protozero bindings header-only as
-// inline constexpr variables are not available until C++17 (while inline
-// functions are).
-// TODO(altimin): Use inline variable instead after adopting C++17.
-static constexpr $field_metadata_type$ $field_metadata_var$() { return {}; }
+static constexpr $field_metadata_type$ $field_metadata_var${};
 )";
 
     stub_h_->Print(code_stub, "field_id", std::to_string(field->number()),
diff --git a/src/trace_processor/prelude/functions/BUILD.gn b/src/trace_processor/prelude/functions/BUILD.gn
index 6927947..02cb90f 100644
--- a/src/trace_processor/prelude/functions/BUILD.gn
+++ b/src/trace_processor/prelude/functions/BUILD.gn
@@ -26,6 +26,8 @@
     "create_view_function.h",
     "import.cc",
     "import.h",
+    "layout_functions.cc",
+    "layout_functions.h",
     "pprof_functions.cc",
     "pprof_functions.h",
     "register_function.cc",
diff --git a/src/trace_processor/prelude/functions/layout_functions.cc b/src/trace_processor/prelude/functions/layout_functions.cc
new file mode 100644
index 0000000..2210355
--- /dev/null
+++ b/src/trace_processor/prelude/functions/layout_functions.cc
@@ -0,0 +1,203 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/trace_processor/prelude/functions/layout_functions.h"
+
+#include <queue>
+#include <vector>
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor {
+
+namespace {
+
+constexpr char kFunctionName[] = "INTERNAL_LAYOUT";
+
+// A helper class for tracking which depths are available at a given time
+// and which slices are occupying each depths.
+class SlicePacker {
+ public:
+  SlicePacker() = default;
+
+  // |dur| can be 0 for instant events and -1 for slices which do not end.
+  base::Status AddSlice(int64_t ts, int64_t dur) {
+    if (last_call_ == LastCall::kAddSlice) {
+      return base::ErrStatus(R"(
+Incorrect window clause (observed two consecutive calls to "step" function).
+The window clause should be "rows between unbounded preceding and current row".
+)");
+    }
+    last_call_ = LastCall::kAddSlice;
+    if (ts < last_seen_ts_) {
+      return base::ErrStatus(R"(
+Passed slices are in incorrect order: %s requires timestamps to be sorted.
+Please specify "ORDER BY ts" in the window clause.
+)",
+                             kFunctionName);
+    }
+    last_seen_ts_ = ts;
+    ProcessPrecedingEvents(ts);
+    // If the event is instant, do not mark this depth as occupied as it
+    // becomes immediately available again.
+    bool is_busy = dur != 0;
+    size_t depth = SelectAvailableDepth(is_busy);
+    // If the slice has an end and is not an instant, schedule this depth
+    // to be marked available again when it ends.
+    if (dur > 0) {
+      slice_ends_.push({ts + dur, depth});
+    }
+    last_depth_ = depth;
+    return base::OkStatus();
+  }
+
+  size_t GetLastDepth() {
+    last_call_ = LastCall::kQuery;
+    return last_depth_;
+  }
+
+ private:
+  struct SliceEnd {
+    int64_t ts;
+    size_t depth;
+  };
+
+  struct SliceEndGreater {
+    bool operator()(const SliceEnd& lhs, const SliceEnd& rhs) {
+      return lhs.ts > rhs.ts;
+    }
+  };
+
+  void ProcessPrecedingEvents(int64_t ts) {
+    while (!slice_ends_.empty() && slice_ends_.top().ts <= ts) {
+      is_depth_busy_[slice_ends_.top().depth] = false;
+      slice_ends_.pop();
+    }
+  }
+
+  size_t SelectAvailableDepth(bool new_state) {
+    for (size_t i = 0; i < is_depth_busy_.size(); ++i) {
+      if (!is_depth_busy_[i]) {
+        is_depth_busy_[i] = new_state;
+        return i;
+      }
+    }
+    size_t depth = is_depth_busy_.size();
+    is_depth_busy_.push_back(new_state);
+    return depth;
+  }
+
+  enum class LastCall {
+    kAddSlice,
+    kQuery,
+  };
+  // The first call will be "add slice" and the calls are expected to
+  // interleave, so set initial value to "query".
+  LastCall last_call_ = LastCall::kQuery;
+
+  int64_t last_seen_ts_ = 0;
+  std::vector<bool> is_depth_busy_;
+  // A list of currently open slices, ordered by end timestamp (ascending).
+  std::priority_queue<SliceEnd, std::vector<SliceEnd>, SliceEndGreater>
+      slice_ends_;
+  size_t last_depth_ = 0;
+};
+
+base::StatusOr<SlicePacker*> GetOrCreateAggregationContext(
+    sqlite3_context* ctx) {
+  SlicePacker** packer = static_cast<SlicePacker**>(
+      sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
+  if (!packer) {
+    return base::ErrStatus("Failed to allocate aggregate context");
+  }
+
+  if (!*packer) {
+    *packer = new SlicePacker();
+  }
+  return *packer;
+}
+
+base::Status Step(sqlite3_context* ctx, size_t argc, sqlite3_value** argv) {
+  base::StatusOr<SlicePacker*> slice_packer =
+      GetOrCreateAggregationContext(ctx);
+  RETURN_IF_ERROR(slice_packer.status());
+
+  base::StatusOr<SqlValue> ts =
+      sqlite_utils::ExtractArgument(argc, argv, "ts", 0, SqlValue::kLong);
+  RETURN_IF_ERROR(ts.status());
+
+  base::StatusOr<SqlValue> dur =
+      sqlite_utils::ExtractArgument(argc, argv, "dur", 1, SqlValue::kLong);
+  RETURN_IF_ERROR(dur.status());
+
+  return slice_packer.value()->AddSlice(ts->AsLong(), dur.value().AsLong());
+}
+
+void StepWrapper(sqlite3_context* ctx, int argc, sqlite3_value** argv) {
+  PERFETTO_CHECK(argc >= 0);
+
+  base::Status status = Step(ctx, static_cast<size_t>(argc), argv);
+  if (!status.ok()) {
+    sqlite_utils::SetSqliteError(ctx, kFunctionName, status);
+    return;
+  }
+}
+
+void FinalWrapper(sqlite3_context* ctx) {
+  SlicePacker** slice_packer = static_cast<SlicePacker**>(
+      sqlite3_aggregate_context(ctx, sizeof(SlicePacker*)));
+  if (!slice_packer || !*slice_packer) {
+    return;
+  }
+  sqlite3_result_int64(ctx,
+                       static_cast<int64_t>((*slice_packer)->GetLastDepth()));
+  delete *slice_packer;
+}
+
+void ValueWrapper(sqlite3_context* ctx) {
+  base::StatusOr<SlicePacker*> slice_packer =
+      GetOrCreateAggregationContext(ctx);
+  if (!slice_packer.ok()) {
+    sqlite_utils::SetSqliteError(ctx, kFunctionName, slice_packer.status());
+    return;
+  }
+  sqlite3_result_int64(
+      ctx, static_cast<int64_t>(slice_packer.value()->GetLastDepth()));
+}
+
+void InverseWrapper(sqlite3_context* ctx, int, sqlite3_value**) {
+  sqlite_utils::SetSqliteError(ctx, kFunctionName, base::ErrStatus(R"(
+The inverse step is not supported: the window clause should be
+"BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW".
+)"));
+}
+
+}  // namespace
+
+base::Status LayoutFunctions::Register(sqlite3* db,
+                                       TraceProcessorContext* context) {
+  int flags = SQLITE_UTF8 | SQLITE_DETERMINISTIC;
+  int ret = sqlite3_create_window_function(
+      db, kFunctionName, 2, flags, context, StepWrapper, FinalWrapper,
+      ValueWrapper, InverseWrapper, nullptr);
+  if (ret != SQLITE_OK) {
+    return base::ErrStatus("Unable to register function with name %s",
+                           kFunctionName);
+  }
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/prelude/functions/layout_functions.h b/src/trace_processor/prelude/functions/layout_functions.h
new file mode 100644
index 0000000..a88d8ab
--- /dev/null
+++ b/src/trace_processor/prelude/functions/layout_functions.h
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TRACE_PROCESSOR_PRELUDE_FUNCTIONS_LAYOUT_FUNCTIONS_H_
+#define SRC_TRACE_PROCESSOR_PRELUDE_FUNCTIONS_LAYOUT_FUNCTIONS_H_
+
+#include <sqlite3.h>
+
+#include "perfetto/base/status.h"
+
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+// Implements INTERNAL_LAYOUT(ts, dur) window aggregate function.
+// This function takes a set of slices (ordered by ts) and computes depths
+// allowing them to be displayed on a single track in a non-overlapping manner,
+// while trying to minimising total height.
+//
+// TODO(altimin): this should support grouping sets of sets of slices (aka
+// "tracks") by passing 'track_id' parameter. The complication is that we will
+// need to know the max depth for each "track", so it's punted for now.
+struct LayoutFunctions {
+  static base::Status Register(sqlite3* db, TraceProcessorContext* context);
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_PRELUDE_FUNCTIONS_LAYOUT_FUNCTIONS_H_
diff --git a/src/trace_processor/prelude/functions/pprof_functions.cc b/src/trace_processor/prelude/functions/pprof_functions.cc
index 7d7e56e..23b2f23 100644
--- a/src/trace_processor/prelude/functions/pprof_functions.cc
+++ b/src/trace_processor/prelude/functions/pprof_functions.cc
@@ -54,18 +54,6 @@
   return std::unique_ptr<T>(ptr);
 }
 
-void SetSqliteError(sqlite3_context* ctx, const base::Status& status) {
-  PERFETTO_CHECK(!status.ok());
-  sqlite3_result_error(ctx, status.c_message(), -1);
-}
-
-void SetSqliteError(sqlite3_context* ctx,
-                    const std::string& function_name,
-                    const base::Status& status) {
-  SetSqliteError(ctx, base::ErrStatus("%s: %s", function_name.c_str(),
-                                      status.c_message()));
-}
-
 class AggregateContext {
  public:
   static base::StatusOr<std::unique_ptr<AggregateContext>>
@@ -196,7 +184,7 @@
   base::Status status = Step(ctx, static_cast<size_t>(argc), argv);
 
   if (!status.ok()) {
-    SetSqliteError(ctx, kFunctionName, status);
+    sqlite_utils::SetSqliteError(ctx, kFunctionName, status);
   }
 }
 
diff --git a/src/trace_processor/sqlite/sqlite_utils.h b/src/trace_processor/sqlite/sqlite_utils.h
index cc9cf41..c3b08bb 100644
--- a/src/trace_processor/sqlite/sqlite_utils.h
+++ b/src/trace_processor/sqlite/sqlite_utils.h
@@ -214,6 +214,18 @@
   return base::OkStatus();
 }
 
+inline void SetSqliteError(sqlite3_context* ctx, const base::Status& status) {
+  PERFETTO_CHECK(!status.ok());
+  sqlite3_result_error(ctx, status.c_message(), -1);
+}
+
+inline void SetSqliteError(sqlite3_context* ctx,
+                           const std::string& function_name,
+                           const base::Status& status) {
+  SetSqliteError(ctx, base::ErrStatus("%s: %s", function_name.c_str(),
+                                      status.c_message()));
+}
+
 // Exracts the given type from the SqlValue if |value| can fit
 // in the provided optional. Note that SqlValue::kNull will always
 // succeed and cause base::nullopt to be set.
diff --git a/src/trace_processor/stdlib/android/monitor_contention.sql b/src/trace_processor/stdlib/android/monitor_contention.sql
index a075464..09860e7 100644
--- a/src/trace_processor/stdlib/android/monitor_contention.sql
+++ b/src/trace_processor/stdlib/android/monitor_contention.sql
@@ -152,7 +152,11 @@
 -- @column ts timestamp of lock contention start
 -- @column dur duration of lock contention
 -- @column track_id thread track id of blocked thread
+-- @column is_blocked_main_thread whether the blocked thread is the main thread
+-- @column is_blocking_main_thread whether the blocking thread is the main thread
 -- @column binder_reply_id slice id of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_ts timestamp of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_tid tid of binder reply slice if lock contention was part of a binder txn
 CREATE TABLE android_monitor_contention
 AS
 SELECT
@@ -173,7 +177,11 @@
   slice.ts,
   slice.dur,
   slice.track_id,
-  binder_reply.id AS binder_reply_id
+  thread.is_main_thread AS is_blocked_thread_main,
+  blocking_thread.is_main_thread AS is_blocking_thread_main,
+  binder_reply.id AS binder_reply_id,
+  binder_reply.ts AS binder_reply_ts,
+  binder_reply_thread.tid AS binder_reply_tid
 FROM slice
 JOIN thread_track
   ON thread_track.id = slice.track_id
@@ -183,6 +191,8 @@
   USING (upid)
 LEFT JOIN internal_broken_android_monitor_contention ON internal_broken_android_monitor_contention.id = slice.id
 LEFT JOIN ANCESTOR_SLICE(slice.id) binder_reply ON binder_reply.name = 'binder reply'
+LEFT JOIN thread_track binder_reply_thread_track ON binder_reply.track_id = binder_reply_thread_track.id
+LEFT JOIN thread binder_reply_thread ON binder_reply_thread_track.utid = binder_reply_thread.utid
 JOIN thread blocking_thread ON blocking_thread.name = blocking_thread_name AND blocking_thread.upid = thread.upid
 WHERE slice.name LIKE 'monitor contention%'
   AND slice.dur != -1
@@ -209,7 +219,11 @@
 -- @column ts timestamp of lock contention start
 -- @column dur duration of lock contention
 -- @column track_id thread track id of blocked thread
+-- @column is_blocked_main_thread whether the blocked thread is the main thread
+-- @column is_blocking_main_thread whether the blocking thread is the main thread
 -- @column binder_reply_id slice id of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_ts timestamp of binder reply slice if lock contention was part of a binder txn
+-- @column binder_reply_tid tid of binder reply slice if lock contention was part of a binder txn
 CREATE TABLE android_monitor_contention_chain
 AS
 SELECT parent.id AS parent_id, child.* FROM android_monitor_contention child
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 48a854e..ee0fd9c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -49,6 +49,7 @@
 #include "src/trace_processor/prelude/functions/create_function.h"
 #include "src/trace_processor/prelude/functions/create_view_function.h"
 #include "src/trace_processor/prelude/functions/import.h"
+#include "src/trace_processor/prelude/functions/layout_functions.h"
 #include "src/trace_processor/prelude/functions/pprof_functions.h"
 #include "src/trace_processor/prelude/functions/register_function.h"
 #include "src/trace_processor/prelude/functions/sqlite3_str_split.h"
@@ -768,6 +769,11 @@
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
+  {
+    base::Status status = LayoutFunctions::Register(db, &context_);
+    if (!status.ok())
+      PERFETTO_ELOG("%s", status.c_message());
+  }
 
   auto stdlib_modules = GetStdlibModules();
   for (auto module_it = stdlib_modules.GetIterator(); module_it; ++module_it) {
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index 5e54775..7252a0f 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -99,7 +99,7 @@
 
     template <typename FieldMetadata>
     typename FieldMetadata::cpp_field_type::Decoder* GetInternedMessage(
-        protozero::proto_utils::internal::FieldMetadataHelper<FieldMetadata>,
+        FieldMetadata,
         uint64_t iid) {
       static_assert(std::is_base_of<protozero::proto_utils::FieldMetadataBase,
                                     FieldMetadata>::value,
diff --git a/src/tracing/ipc/service/service_ipc_host_impl.cc b/src/tracing/ipc/service/service_ipc_host_impl.cc
index aab46dd..85029a2 100644
--- a/src/tracing/ipc/service/service_ipc_host_impl.cc
+++ b/src/tracing/ipc/service/service_ipc_host_impl.cc
@@ -31,6 +31,10 @@
 
 namespace perfetto {
 
+namespace {
+constexpr uint32_t kProducerSocketTxTimeoutMs = 10;
+}
+
 // TODO(fmayer): implement per-uid connection limit (b/69093705).
 
 // Implements the publicly exposed factory method declared in
@@ -98,6 +102,16 @@
     return false;
   }
 
+  // Lower the timeout for blocking socket sends to producers as we shouldn't
+  // normally exhaust the kernel send buffer unless the producer is
+  // unresponsive. We'll drop the connection if the timeout is hit (see
+  // UnixSocket::Send). Context in b/236813972, b/193234818.
+  // Consumer port continues using the default timeout (10s) as there are
+  // generally fewer consumer processes, and they're better behaved. Also the
+  // consumer port ipcs might exhaust the send buffer under normal operation
+  // due to large messages such as ReadBuffersResponse.
+  producer_ipc_port_->SetSocketSendTimeoutMs(kProducerSocketTxTimeoutMs);
+
   // TODO(fmayer): add a test that destroyes the ServiceIPCHostImpl soon after
   // Start() and checks that no spurious callbacks are issued.
   bool producer_service_exposed = producer_ipc_port_->ExposeService(
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
index 1f4f279..ee11f6b 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_expand_camera.png.sha256
@@ -1 +1 @@
-5a89fcf84d965f0fb6fa10f376fb6366ab6608974d5bf81cbf58fa784b4c0c83
\ No newline at end of file
+3142c75d3c652460c6f76261b59191afb89be996a234a9e5700bb4147b18537f
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
index eeec9ab..677e059 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_dismiss_1.png.sha256
@@ -1 +1 @@
-0f96be819cae4e8ae4fa80e3b8bfdcd46176a9357172bf5e92b6311d55c74e2e
\ No newline at end of file
+81d485072c6494fa2fb7b23693c3f6ae197534e4fd39d2c4647f735ab2a5fafc
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
index 9427a25..102a59b 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_dismiss_2.png.sha256
@@ -1 +1 @@
-5e6bc6ab0108752c5eb14d6bc1c4b8e3d1283121591a8db45a8a66a2ab2cb00a
\ No newline at end of file
+0690737a791ad7fc869a041574e220eed6cca864f5d93740c44ff2e36b4fe4d5
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
index dab9123..b2a0167 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_1.png.sha256
@@ -1 +1 @@
-53c2f27d6ffb0a10593c331167f92e025daec37b7802fe6f8f1beb056291062e
\ No newline at end of file
+717822b63455dab14a57811379e6e406537d80aa08746903d558aff463ea359a
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
index 684c3da..4b292da 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_show_dialog_2.png.sha256
@@ -1 +1 @@
-3bdbffb6d4ab142b0665d50bc929606edb55f50c056bf17f7f0b337d1cb8b608
\ No newline at end of file
+3efbdefb1d110c11a82d109f4f7c6edf599a4866d5c6c6bdf80011762244b206
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256 b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
index 9094b6c..98dcc39 100644
--- a/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
+++ b/test/data/ui-screenshots/ui-modal_dialog_switch_page_no_dialog.png.sha256
@@ -1 +1 @@
-f76316e3870b30a13b55b7c95f4f7ddea45c1eb33ef49f98ee664c38b35494ab
\ No newline at end of file
+c9769de63b7caa32bda648b08a7e302a80e8bf1a9138c9e508faec5713e0964c
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256 b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
index 42d454e..8e2378f 100644
--- a/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
+++ b/test/data/ui-screenshots/ui-routing_navigate_navigate_back_and_forward.png.sha256
@@ -1 +1 @@
-59a6d1a1b5af0a3b2724e20e4c4290bb8cc3ede400f2bc1fad08ca8f52a3d0d0
\ No newline at end of file
+e4461c82b2051197fab373f50d44ffa6f103d24a10e582a62e67f56f77c0ec3a
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index 7f6390d..573bed9 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -233,8 +233,8 @@
       LIMIT 1;
       """,
         out=Csv("""
-        "blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","binder_reply_id"
-        "float com.android.server.wm.WindowManagerService.getCurrentAnimatorScale()","android.app.ActivityTaskManager$RootTaskInfo com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo()","com.android.server.wm.WindowManagerService.getCurrentAnimatorScale","com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo","WindowManagerService.java:3511","ActivityTaskManagerService.java:1977",2,555,"binder:642_3",527,"android.anim",279,"system_server",69099,146987786843,24888520,1317,69097
+        "blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","is_blocked_thread_main","is_blocking_thread_main","binder_reply_id","binder_reply_ts","binder_reply_tid"
+        "float com.android.server.wm.WindowManagerService.getCurrentAnimatorScale()","android.app.ActivityTaskManager$RootTaskInfo com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo()","com.android.server.wm.WindowManagerService.getCurrentAnimatorScale","com.android.server.wm.ActivityTaskManagerService.getFocusedRootTaskInfo","WindowManagerService.java:3511","ActivityTaskManagerService.java:1977",2,555,"binder:642_3",527,"android.anim",279,"system_server",69099,146987786843,24888520,1317,0,0,69097,146987701011,1559
       """))
 
   def test_monitor_contention_chain_extraction(self):
@@ -261,12 +261,16 @@
         ts,
         dur,
         track_id,
-        IIF(binder_reply_id IS NULL, "", binder_reply_id) AS binder_reply_id
+        is_blocked_thread_main,
+        is_blocking_thread_main,
+        IIF(binder_reply_id IS NULL, "", binder_reply_id) AS binder_reply_id,
+        IIF(binder_reply_ts IS NULL, "", binder_reply_ts) AS binder_reply_ts,
+        IIF(binder_reply_tid IS NULL, "", binder_reply_tid) AS binder_reply_tid
       FROM android_monitor_contention_chain
       ORDER BY dur DESC
       LIMIT 1;
       """,
         out=Csv("""
-        "parent_id","blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","binder_reply_id"
-        "","void java.lang.Object.wait(long, int)","void android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify(java.lang.Runnable)","java.lang.Object.wait","android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify","Object.java:-2","GLSurfaceView.java:1658",0,313,"droid.gallery3d",1769,"GLThread 33",313,"com.android.gallery3d",289064,155411562446,51012448,2036,""
+        "parent_id","blocking_method","blocked_method","short_blocking_method","short_blocked_method","blocking_src","blocked_src","waiter_count","blocked_utid","blocked_thread_name","blocking_utid","blocking_thread_name","upid","process_name","id","ts","dur","track_id","is_blocked_thread_main","is_blocking_thread_main","binder_reply_id","binder_reply_ts","binder_reply_tid"
+        "","void java.lang.Object.wait(long, int)","void android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify(java.lang.Runnable)","java.lang.Object.wait","android.opengl.GLSurfaceView$GLThread.requestRenderAndNotify","Object.java:-2","GLSurfaceView.java:1658",0,313,"droid.gallery3d",1769,"GLThread 33",313,"com.android.gallery3d",289064,155411562446,51012448,2036,1,0,"","",""
       """))
diff --git a/test/trace_processor/diff_tests/functions/tests.py b/test/trace_processor/diff_tests/functions/tests.py
index aa66328..d68159a 100644
--- a/test/trace_processor/diff_tests/functions/tests.py
+++ b/test/trace_processor/diff_tests/functions/tests.py
@@ -37,7 +37,6 @@
 
 
 class Functions(TestSuite):
-
   def test_first_non_null_frame(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
@@ -333,7 +332,7 @@
                   A (0x0)
             """))
 
-def test_annotated_callstack(self):
+  def test_annotated_callstack(self):
     return DiffTestBlueprint(
         trace=DataPath("perf_sample_annotations.pftrace"),
         query="""
@@ -375,3 +374,111 @@
                 main (0x63da9c354c)
                 __libc_init (0x74ff4a0728)
             """))
+
+  def test_layout(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        """),
+        query="""
+        CREATE TABLE TEST(start INTEGER, end INTEGER);
+
+        INSERT INTO TEST
+        VALUES
+        (1, 5),
+        (2, 4),
+        (3, 8),
+        (6, 7),
+        (6, 7),
+        (6, 7);
+
+        WITH custom_slices as (
+          SELECT
+            start as ts,
+            end - start as dur
+          FROM test
+        )
+        SELECT
+          ts,
+          INTERNAL_LAYOUT(ts, dur) over (
+            order by ts
+            rows between unbounded preceding and current row
+          ) as depth
+        FROM custom_slices
+        """,
+        out=Csv("""
+        "ts","depth"
+        1,0
+        2,1
+        3,2
+        6,0
+        6,1
+        6,3
+        """))
+
+  def test_layout_with_instant_events(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        """),
+        query="""
+        CREATE TABLE TEST(start INTEGER, end INTEGER);
+
+        INSERT INTO TEST
+        VALUES
+        (1, 5),
+        (2, 2),
+        (3, 3),
+        (4, 4);
+
+        WITH custom_slices as (
+          SELECT
+            start as ts,
+            end - start as dur
+          FROM test
+        )
+        SELECT
+          ts,
+          INTERNAL_LAYOUT(ts, dur) over (
+            order by ts
+            rows between unbounded preceding and current row
+          ) as depth
+        FROM custom_slices
+        """,
+        out=Csv("""
+        "ts","depth"
+        1,0
+        2,1
+        3,1
+        4,1
+        """))
+
+  def test_layout_with_events_without_end(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        """),
+        query="""
+        CREATE TABLE TEST(ts INTEGER, dur INTEGER);
+
+        INSERT INTO TEST
+        VALUES
+        (1, -1),
+        (2, -1),
+        (3, 5),
+        (4, 1),
+        (5, 1);
+
+        SELECT
+          ts,
+          INTERNAL_LAYOUT(ts, dur) over (
+            order by ts
+            rows between unbounded preceding and current row
+          ) as depth
+        FROM test
+        """,
+        out=Csv("""
+        "ts","depth"
+        1,0
+        2,1
+        3,2
+        4,3
+        5,3
+        """))
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0a30805..1e602fc 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -24,6 +24,7 @@
   tableColumnEquals,
   toggleEnabled,
 } from '../frontend/pivot_table_types';
+import {DebugTrackV2Config} from '../tracks/debug/slice_track';
 
 import {randomColor} from './colorizer';
 import {
@@ -62,7 +63,7 @@
 } from './state';
 import {toNs} from './time';
 
-const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
+export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
 
 type StateDraft = Draft<State>;
 
@@ -275,11 +276,12 @@
     };
   },
 
-  addDebugTrack(state: StateDraft, args: {engineId: string, name: string}):
+  addDebugTrack(
+      state: StateDraft,
+      args: {engineId: string, name: string, config: DebugTrackV2Config}):
       void {
         if (state.debugTrackId !== undefined) return;
         const trackId = generateNextId(state);
-        state.debugTrackId = trackId;
         this.addTrack(state, {
           id: trackId,
           engineId: args.engineId,
@@ -287,18 +289,15 @@
           name: args.name,
           trackSortKey: PrimaryTrackSortKey.DEBUG_SLICE_TRACK,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {
-            maxDepth: 1,
-          },
+          config: args.config,
         });
         this.toggleTrackPinned(state, {trackId});
       },
 
-  removeDebugTrack(state: StateDraft, _: {}): void {
-    const {debugTrackId} = state;
-    if (debugTrackId === undefined) return;
-    removeTrack(state, debugTrackId);
-    state.debugTrackId = undefined;
+  removeDebugTrack(state: StateDraft, args: {trackId: string}): void {
+    const track = state.tracks[args.trackId];
+    assertTrue(track.kind === DEBUG_SLICE_TRACK_KIND);
+    removeTrack(state, args.trackId);
   },
 
   removeVisualisedArgTracks(state: StateDraft, args: {trackIds: string[]}) {
@@ -782,6 +781,23 @@
         state.pendingScrollId = args.scroll ? args.id : undefined;
       },
 
+  selectDebugSlice(state: StateDraft, args: {
+    id: number,
+    sqlTableName: string,
+    startS: number,
+    durationS: number,
+    trackId: string,
+  }): void {
+    state.currentSelection = {
+      kind: 'DEBUG_SLICE',
+      id: args.id,
+      sqlTableName: args.sqlTableName,
+      startS: args.startS,
+      durationS: args.durationS,
+      trackId: args.trackId,
+    };
+  },
+
   clearPendingScrollId(state: StateDraft, _: {}): void {
     state.pendingScrollId = undefined;
   },
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 30c9423..8b6cb46 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -463,4 +463,8 @@
   query(sqlQuery: string, tag?: string): Promise<QueryResult>&QueryResult {
     return this.engine.query(sqlQuery, tag || this.tag);
   }
+
+  get engineId(): string {
+    return this.engine.id;
+  }
 }
diff --git a/ui/src/common/event_set.ts b/ui/src/common/event_set.ts
new file mode 100644
index 0000000..87e62d5
--- /dev/null
+++ b/ui/src/common/event_set.ts
@@ -0,0 +1,153 @@
+// 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.
+
+// A single value. These are often retived from trace_processor so
+// need to map to the related sqlite type:
+// null = NULL, string = TEXT, number = INTEGER/REAL, boolean = INTEGER
+export type Primitive = null|string|boolean|number;
+
+export const NullType = null;
+export const NumType = 0 as const;
+export const StrType = 'str' as const;
+export const IdType = 'id' as const;
+export const BoolType = true as const;
+
+// Values may be of any of the above types:
+type KeyType =
+    typeof NumType|typeof StrType|typeof NullType|typeof IdType|typeof BoolType;
+
+// KeySet is a specification for the key/value pairs on an Event.
+// - Every event must have a string ID.
+// - In addition Events may have 1 or more key/value pairs.
+// The *specification* for the key/value pair has to be *precisely* one
+// of the KeySet constants above. So:
+// const thisTypeChecks: KeySet = { id: IdType, foo: StrType };
+// const thisDoesNot: KeySet = { id: IdType, foo: "bar" };
+// Since although are is a string it's not a KeySet.
+export type KeySet = {
+  readonly id: typeof IdType,
+  readonly [key: string]: KeyType,
+};
+
+export interface EmptyKeySet extends KeySet {
+  readonly id: typeof IdType;
+}
+;
+
+// A particular key/value pair on an Event matches the relevant entry
+// on the KeySet if the KeyType and the value type 'match':
+// IdType => string
+// StrType => string
+// BoolType => boolean
+// NullType => null
+// NumType => number
+type IsExactly<P, Q> = P extends Q ? (Q extends P ? any : never) : never;
+type IsId<T> = T extends IsExactly<T, typeof IdType>? string : never;
+type IsStr<T> = T extends IsExactly<T, typeof StrType>? string : never;
+type IsNum<T> = T extends IsExactly<T, typeof NumType>? number : never;
+type IsBool<T> = T extends IsExactly<T, typeof BoolType>? boolean : never;
+type IsNull<T> = T extends IsExactly<T, typeof NullType>? null : never;
+type MapType<T> = IsId<T>|IsStr<T>|IsNum<T>|IsBool<T>|IsNull<T>;
+type ConformingValue<T> = T extends MapType<T>? MapType<T>: void;
+
+// A single trace Event.
+// Events have:
+// - A globally unique identifier `id`.
+// - Zero or more key/value pairs.
+// Note: Events do *not* have to have all possible keys/value pairs for
+// the given id. It is expected that users will only materialise the
+// key/value pairs relevant to the specific use case at hand.
+export type UntypedEvent = {
+  readonly id: string,
+  readonly [key: string]: Primitive,
+};
+
+export type Event<K extends KeySet> = {
+  [Property in keyof K]: ConformingValue<K[Property]>;
+};
+
+type KeyUnion<P, Q> = P&Q;
+
+// An EventSet is a:
+// - ordered
+// - immutable
+// - subset
+// of events in the trace.
+export interface EventSet<P extends KeySet> {
+  // All possible keys for Events in this EventSet.
+  readonly keys: KeySet;
+
+  // Methods for refining the set.
+  // Note: these are all synchronous - we expect the cost (and hence
+  // any asynchronous queries) to be deferred to analysis time.
+  filter(...filters: Filter[]): EventSet<P>;
+  sort(...sorts: Sort[]): EventSet<P>;
+  union<Q extends KeySet>(other: EventSet<Q>): EventSet<KeyUnion<P, Q>>;
+  intersect<Q extends KeySet>(other: EventSet<Q>): EventSet<KeyUnion<P, Q>>;
+
+  // Methods for analysing the set.
+  // Note: these are all asynchronous - it's expected that these will
+  // often have to do queries.
+  count(): Promise<number>;
+  isEmpty(): Promise<boolean>;
+  materialise<T extends P>(keys: T, offset?: number, limit?: number):
+      Promise<ConcreteEventSet<T>>;
+}
+
+export type UntypedEventSet = EventSet<KeySet>;
+
+// An expression that operates on an Event and produces a Primitive as
+// output. Expressions have to work both in JavaScript and in SQL.
+// In SQL users can use buildQueryFragment to convert the expression
+// into a snippet of SQL. For JavaScript they call execute(). In both
+// cases you need to know which keys the expression uses, for this call
+// `freeVariables`.
+export interface Expr {
+  // Return a fragment of SQL that can be used to evaluate the
+  // expression. `binding` maps key names to column names in the
+  // resulting SQL. The caller must ensure that binding includes at
+  // least all the keys from `freeVariables`.
+  buildQueryFragment(binding: Map<string, string>): string;
+
+  // Run the expression on an Event. The caller must ensure that event
+  // has all the keys from `freeVariables` materialised.
+  execute(event: UntypedEvent): Primitive;
+
+  // Returns the set of keys used in this expression.
+  // For example in an expression representing `(foo + 4) * bar`
+  // freeVariables would return the set {'foo', 'bar'}.
+  freeVariables(): Set<string>;
+}
+
+// A filter is a (normally boolean) expression.
+export type Filter = Expr;
+
+// Sorting direction.
+export enum Direction {
+  ASC,
+  DESC,
+}
+
+// A sort is an expression combined with a direction:
+export interface Sort {
+  direction: Direction;
+  expression: Expr;
+}
+
+// An EventSet where the Event are accesible synchronously.
+interface ConcreteEventSet<T extends KeySet> extends EventSet<T> {
+  readonly events: Event<T>[];
+}
+
+export type UntypedConcreteEventSet = ConcreteEventSet<KeySet>;
diff --git a/ui/src/common/event_set_nocompile_test.ts b/ui/src/common/event_set_nocompile_test.ts
new file mode 100644
index 0000000..e5f74d7
--- /dev/null
+++ b/ui/src/common/event_set_nocompile_test.ts
@@ -0,0 +1,71 @@
+// 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 {
+  BoolType,
+  Event,
+  IdType,
+  KeySet,
+  NullType,
+  NumType,
+  StrType,
+} from './event_set';
+
+export function keySetMustHaveId(): KeySet {
+  // @ts-expect-error
+  const ks: KeySet = {};
+  return ks;
+}
+
+export function keySetMustHaveCorrectIdType(): KeySet {
+  const ks: KeySet = {
+    // @ts-expect-error
+    id: StrType,
+  };
+  return ks;
+}
+
+export function eventMustHaveAllKeys(): Event<KeySet> {
+  const ks = {
+    id: IdType,
+    foo: StrType,
+  };
+
+  // @ts-expect-error
+  const event: Event<typeof ks> = {
+    id: 'myid',
+  };
+
+  return event;
+}
+
+export function eventMayHaveNonKeyTypeValues(): Event<KeySet> {
+  const ks = {
+    id: IdType,
+    foo: StrType,
+    bar: NumType,
+    baz: BoolType,
+    xyzzy: NullType,
+  };
+
+  const event: Event<typeof ks> = {
+    id: 'myid',
+    foo: 'foo',
+    bar: 32,
+    baz: false,
+    xyzzy: null,
+  };
+
+  return event;
+}
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index c72c3c6..3f6f5d2 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -18,6 +18,7 @@
   PivotTree,
   TableColumn,
 } from '../frontend/pivot_table_types';
+import {Direction} from './event_set';
 
 /**
  * A plain js object, holding objects of type |Class| keyed by string id.
@@ -306,6 +307,14 @@
   id: number;
 }
 
+export interface DebugSliceSelection {
+  kind: 'DEBUG_SLICE';
+  id: number;
+  sqlTableName: string;
+  startS: number;
+  durationS: number;
+}
+
 export interface CounterSelection {
   kind: 'COUNTER';
   leftTs: number;
@@ -368,7 +377,8 @@
 export type Selection =
     (NoteSelection|SliceSelection|CounterSelection|HeapProfileSelection|
      CpuProfileSampleSelection|ChromeSliceSelection|ThreadStateSelection|
-     AreaSelection|PerfSamplesSelection|LogSelection)&{trackId?: string};
+     AreaSelection|PerfSamplesSelection|LogSelection|DebugSliceSelection)&
+    {trackId?: string};
 export type SelectionKind = Selection['kind'];  // 'THREAD_STATE' | 'SLICE' ...
 
 export interface Pagination {
@@ -437,7 +447,7 @@
   tracks: string[];
 }
 
-export type SortDirection = 'DESC'|'ASC';
+export type SortDirection = keyof typeof Direction;
 
 export interface PivotTableState {
   // Currently selected area, if null, pivot table is not going to be visible.
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 186620e..421874b 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -498,7 +498,6 @@
       publishFtraceCounters(counters);
     }
 
-    globals.dispatch(Actions.removeDebugTrack({}));
     globals.dispatch(Actions.sortThreadTracks({}));
     globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
 
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 0f205ba..c1a877f 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -317,15 +317,18 @@
         group by parent_id, name
       )
       select
-        parent_id as parentId,
+        t.parent_id as parentId,
+        p.name as parentName,
         t.name as name,
         t.trackIds as trackIds,
         max_layout_depth(t.trackCount, t.trackIds) as maxDepth
       from global_tracks_grouped AS t
-      order by t.name;
+      left join track p on (t.parent_id = p.id)
+      order by p.name, t.name;
     `);
     const it = rawGlobalAsyncTracks.iter({
       name: STR_NULL,
+      parentName: STR_NULL,
       parentId: NUM_NULL,
       trackIds: STR,
       maxDepth: NUM,
@@ -336,6 +339,7 @@
     for (; it.valid(); it.next()) {
       const kind = ASYNC_SLICE_TRACK_KIND;
       const rawName = it.name === null ? undefined : it.name;
+      const rawParentName = it.parentName === null ? undefined : it.name;
       const name = TrackDecider.getTrackName({name: rawName, kind});
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
@@ -349,21 +353,24 @@
           trackGroup = uuidv4();
           parentIdToGroupId.set(parentTrackId, trackGroup);
 
+          const parentName =
+              TrackDecider.getTrackName({name: rawParentName, kind});
+
           const summaryTrackId = uuidv4();
           this.tracksToAdd.push({
             id: summaryTrackId,
             engineId: this.engineId,
             kind: NULL_TRACK_KIND,
             trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
-            name,
             trackGroup: undefined,
+            name: parentName,
             config: {},
           });
 
           this.addTrackGroupActions.push(Actions.addTrackGroup({
             engineId: this.engineId,
             summaryTrackId,
-            name,
+            name: parentName,
             id: trackGroup,
             collapsed: true,
           }));
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index cd6f234..ac3cad0 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -21,7 +21,7 @@
   UNEXPECTED_PINK_COLOR,
 } from '../common/colorizer';
 import {NUM} from '../common/query_result';
-import {SelectionKind} from '../common/state';
+import {Selection, SelectionKind} from '../common/state';
 import {fromNs, toNs} from '../common/time';
 
 import {checkerboardExcept} from './checkerboard';
@@ -204,13 +204,6 @@
   // TODO(hjd): Replace once we have cancellable query sequences.
   private isDestroyed = false;
 
-  // TODO(hjd): Remove when updating selection.
-  // We shouldn't know here about CHROME_SLICE. Maybe should be set by
-  // whatever deals with that. Dunno the namespace of selection is weird. For
-  // most cases in non-ambiguous (because most things are a 'slice'). But some
-  // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
-  protected selectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
-
   // Extension points.
   // Each extension point should take a dedicated argument type (e.g.,
   // OnSliceOverArgs {slice?: T['slice']}) so it makes future extensions
@@ -266,6 +259,16 @@
     this.onUpdatedSlices(this.slices);
   }
 
+  protected isSelectionHandled(selection: Selection): boolean {
+    // TODO(hjd): Remove when updating selection.
+    // We shouldn't know here about CHROME_SLICE. Maybe should be set by
+    // whatever deals with that. Dunno the namespace of selection is weird. For
+    // most cases in non-ambiguous (because most things are a 'slice'). But some
+    // others (e.g. THREAD_SLICE) have their own ID namespace so we need this.
+    const supportedSelectionKinds: SelectionKind[] = ['SLICE', 'CHROME_SLICE'];
+    return supportedSelectionKinds.includes(selection.kind);
+  }
+
   renderCanvas(ctx: CanvasRenderingContext2D): void {
     // TODO(hjd): fonts and colors should come from the CSS and not hardcoded
     // here.
@@ -299,7 +302,7 @@
 
     let selection = globals.state.currentSelection;
 
-    if (!selection || !this.selectionKinds.includes(selection.kind)) {
+    if (!selection || !this.isSelectionHandled(selection)) {
       selection = null;
     }
 
diff --git a/ui/src/frontend/debug.ts b/ui/src/frontend/debug.ts
new file mode 100644
index 0000000..8451e99
--- /dev/null
+++ b/ui/src/frontend/debug.ts
@@ -0,0 +1,36 @@
+// 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 * as m from 'mithril';
+
+import {Actions} from '../common/actions';
+import {getSchema} from '../common/schema';
+
+import {globals} from './globals';
+
+declare global {
+  interface Window {
+    m: typeof m;
+    getSchema: typeof getSchema;
+    globals: typeof globals;
+    Actions: typeof Actions;
+  }
+}
+
+export function registerDebugGlobals() {
+  window.getSchema = getSchema;
+  window.m = m;
+  window.globals = globals;
+  window.Actions = Actions;
+}
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 3dd234c..c9faeaa 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -19,6 +19,7 @@
 import {LogExists, LogExistsKey} from '../common/logs';
 import {addSelectionChangeObserver} from '../common/selection_observer';
 import {Selection} from '../common/state';
+import {DebugSliceDetailsTab} from '../tracks/debug/details_tab';
 
 import {AggregationPanel} from './aggregation_panel';
 import {ChromeSliceDetailsPanel} from './chrome_slice_panel';
@@ -214,6 +215,16 @@
         },
       });
       break;
+    case 'DEBUG_SLICE':
+      bottomTabList.addTab({
+        kind: DebugSliceDetailsTab.kind,
+        tag: currentSelectionTag,
+        config: {
+          sqlTableName: newSelection.sqlTableName,
+          id: newSelection.id,
+        },
+      });
+      break;
     default:
       bottomTabList.closeTabByTag(currentSelectionTag);
   }
diff --git a/ui/src/frontend/help_modal.ts b/ui/src/frontend/help_modal.ts
index 3242b2c..8047377 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -14,51 +14,88 @@
 // limitations under the License.
 
 import * as m from 'mithril';
+import {getErrorMessage} from '../common/errors';
 
 import {globals} from './globals';
+import {
+  KeyboardLayoutMap,
+  nativeKeyboardLayoutMap,
+  NotSupportedError,
+} from './keyboard_layout_map';
 import {showModal} from './modal';
+import {KeyMapping} from './pan_and_zoom_handler';
+import {Spinner} from './widgets/spinner';
 
 export function toggleHelp() {
   globals.logging.logEvent('User Actions', 'Show help');
   showHelp();
 }
 
-function keycap(key: string) {
-  return m('.keycap', key);
+function keycap(glyph: m.Children): m.Children {
+  return m('.keycap', glyph);
 }
 
-function showHelp() {
-  const ctrlOrCmd =
-      window.navigator.platform.indexOf('Mac') !== -1 ? 'Cmd' : 'Ctrl';
-  showModal({
-    title: 'Perfetto Help',
-    content: m(
+// A fallback keyboard map based on the QWERTY keymap. Converts keyboard event
+// codes to their associated glyphs on an English QWERTY keyboard.
+class EnglishQwertyKeyboardLayoutMap implements KeyboardLayoutMap {
+  get(code: string): string {
+    // Converts 'KeyX' -> 'x'
+    return code.replace(/^Key([A-Z])$/, '$1').toLowerCase();
+  }
+}
+
+class KeyMappingsHelp implements m.ClassComponent {
+  private keyMap?: KeyboardLayoutMap;
+
+  oninit() {
+    nativeKeyboardLayoutMap()
+        .then((keyMap: KeyboardLayoutMap) => {
+          this.keyMap = keyMap;
+          globals.rafScheduler.scheduleFullRedraw();
+        })
+        .catch((e) => {
+          if (e instanceof NotSupportedError ||
+              getErrorMessage(e).includes('SecurityError')) {
+            // Keyboard layout is unavailable. Since showing the keyboard
+            // mappings correct for the user's keyboard layout is a nice-to-
+            // have, and users with non-QWERTY layouts are usually aware of the
+            // fact that the are using non-QWERTY layouts, we resort to showing
+            // English QWERTY mappings as a best-effort approach.
+            // The alternative would be to show key mappings for all keyboard
+            // layouts which is not feasible.
+            this.keyMap = new EnglishQwertyKeyboardLayoutMap();
+            globals.rafScheduler.scheduleFullRedraw();
+          } else {
+            // Something unexpected happened. Either the browser doesn't conform
+            // to the keyboard API spec, or the keyboard API spec has changed!
+            throw e;
+          }
+        });
+  }
+
+  view(_: m.Vnode): m.Children {
+    const ctrlOrCmd =
+        window.navigator.platform.indexOf('Mac') !== -1 ? 'Cmd' : 'Ctrl';
+
+    return m(
         '.help',
         m('h2', 'Navigation'),
         m(
             'table',
             m(
                 'tr',
-                m('td', keycap('w'), '/', keycap('s')),
+                m('td',
+                  this.codeToKeycap(KeyMapping.KEY_ZOOM_IN),
+                  '/',
+                  this.codeToKeycap(KeyMapping.KEY_ZOOM_OUT)),
                 m('td', 'Zoom in/out'),
                 ),
             m(
                 'tr',
-                m('td', keycap('a'), '/', keycap('d')),
-                m('td', 'Pan left/right'),
-                ),
-            ),
-        m('h2', 'Navigation (Dvorak)'),
-        m(
-            'table',
-            m(
-                'tr',
-                m('td', keycap(','), '/', keycap('o')),
-                m('td', 'Zoom in/out'),
-                ),
-            m(
-                'tr',
-                m('td', keycap('a'), '/', keycap('e')),
+                m('td',
+                  this.codeToKeycap(KeyMapping.KEY_PAN_LEFT),
+                  '/',
+                  this.codeToKeycap(KeyMapping.KEY_PAN_RIGHT)),
                 m('td', 'Pan left/right'),
                 ),
             ),
@@ -130,7 +167,22 @@
               m('td', keycap(ctrlOrCmd), ' + ', keycap('b')),
               m('td', 'Toggle display of sidebar')),
             m('tr', m('td', keycap('?')), m('td', 'Show help')),
-            )),
+            ));
+  }
+
+  private codeToKeycap(code: string): m.Children {
+    if (this.keyMap) {
+      return keycap(this.keyMap.get(code));
+    } else {
+      return keycap(m(Spinner));
+    }
+  }
+}
+
+function showHelp() {
+  showModal({
+    title: 'Perfetto Help',
+    content: () => m(KeyMappingsHelp),
     buttons: [],
   });
 }
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 94a1ec5..e58a03b 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -32,6 +32,7 @@
 
 import {AnalyzePage} from './analyze_page';
 import {initCssConstants} from './css_constants';
+import {registerDebugGlobals} from './debug';
 import {maybeShowErrorDialog} from './error_dialog';
 import {installFileDropHandler} from './file_drop_handler';
 import {FlagsPage} from './flags_page';
@@ -287,10 +288,8 @@
     if (extensionPort) extensionPort.postMessage(data);
   };
 
-  // Put these variables in the global scope for better debugging.
-  (window as {} as {m: {}}).m = m;
-  (window as {} as {globals: {}}).globals = globals;
-  (window as {} as {Actions: {}}).Actions = Actions;
+  // Put debug variables in the global scope for better debugging.
+  registerDebugGlobals();
 
   // Prevent pinch zoom.
   document.body.addEventListener('wheel', (e: MouseEvent) => {
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index bc1ae88..7da04de 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -245,6 +245,13 @@
     }
   } else if (selection.kind === 'LOG') {
     // TODO(hjd): Make focus selection work for logs.
+  } else if (selection.kind === 'DEBUG_SLICE') {
+    startTs = selection.startS;
+    if (selection.durationS > 0) {
+      endTs = startTs + selection.durationS;
+    } else {
+      endTs = startTs + INSTANT_FOCUS_DURATION_S;
+    }
   }
 
   return {startTs, endTs};
diff --git a/ui/src/frontend/keyboard_layout_map.ts b/ui/src/frontend/keyboard_layout_map.ts
new file mode 100644
index 0000000..d5d2317
--- /dev/null
+++ b/ui/src/frontend/keyboard_layout_map.ts
@@ -0,0 +1,38 @@
+// 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.
+//
+//      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.
+
+// A keyboard layout map that converts key codes to their equivalent glyphs for
+// a given keyboard layout (e.g. 'KeyX' -> 'x').
+export interface KeyboardLayoutMap {
+  get(code: string): string|undefined;
+}
+
+export class NotSupportedError extends Error {}
+
+// Fetch the user's keyboard layout map.
+// This function is merely a wrapper around the keyboard API, which throws a
+// specific error when used in browsers that don't support it.
+export async function nativeKeyboardLayoutMap(): Promise<KeyboardLayoutMap> {
+  // Browser's that don't support the Keyboard API won't have a keyboard
+  // property in their window.navigator object.
+  // Note: it seems this is also what Chrome does when the website is accessed
+  // through an insecure connection.
+  if ('keyboard' in window.navigator) {
+    // Typescript's dom library doesn't know about this feature, so we must
+    // take some liberties when it comes to relaxing types
+    const keyboard = (window.navigator as any).keyboard;
+    return await keyboard.getLayoutMap();
+  } else {
+    throw new NotSupportedError('Keyboard API is not supported');
+  }
+}
diff --git a/ui/src/frontend/modal.ts b/ui/src/frontend/modal.ts
index 8c6ec2e..37f112e 100644
--- a/ui/src/frontend/modal.ts
+++ b/ui/src/frontend/modal.ts
@@ -58,11 +58,9 @@
 import {assertExists, assertTrue} from '../base/logging';
 import {globals} from './globals';
 
-type AnyAttrsVnode = m.Vnode<unknown, {}>;
-
 export interface ModalDefinition {
   title: string;
-  content: AnyAttrsVnode;
+  content: m.Children|(() => m.Children);
   vAlign?: 'MIDDLE' /* default */ | 'TOP';
   buttons?: Button[];
   close?: boolean;
@@ -151,11 +149,19 @@
                     'button[aria-label=Close Modal]',
                     {onclick: () => attrs.parent.close()},
                     m.trust('&#x2715'),
+                    ),
                 ),
-            ),
-            m('main', attrs.content),
+            m('main', this.renderContent(attrs.content)),
             m('footer', buttons),
-        ));
+            ));
+  }
+
+  private renderContent(content: m.Children|(() => m.Children)): m.Children {
+    if (typeof content === 'function') {
+      return content();
+    } else {
+      return content;
+    }
   }
 
   oncreate(vnode: m.VnodeDOM<ModalImplAttrs>) {
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 2a0610e..5f2413c 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -46,15 +46,32 @@
 const DRAG_CURSOR = 'default';
 const PAN_CURSOR = 'move';
 
+// Use key mapping based on the 'KeyboardEvent.code' property vs the
+// 'KeyboardEvent.key', because the the former corresponds to the physical key
+// position rather than the glyph printed on top of it, and is unaffected by
+// the user's keyboard layout.
+// For example, 'KeyW' always corresponds to the key at the physical location of
+// the 'w' key on an English QWERTY keyboard, regardless of the user's keyboard
+// layout, or at least the layout they have configured in their OS.
+// Seeing as most users use the keys in the English QWERTY "WASD" position for
+// controlling kb+mouse applications like games, it's a good bet that these are
+// the keys most poeple are going to find natural for navigating the UI.
+// See https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system
+export enum KeyMapping {
+  KEY_PAN_LEFT = 'KeyA',
+  KEY_PAN_RIGHT = 'KeyD',
+  KEY_ZOOM_IN = 'KeyW',
+  KEY_ZOOM_OUT = 'KeyS',
+}
+
 enum Pan {
   None = 0,
   Left = -1,
   Right = 1
 }
 function keyToPan(e: KeyboardEvent): Pan {
-  const key = e.key.toLowerCase();
-  if (['a'].includes(key)) return Pan.Left;
-  if (['d', 'e'].includes(key)) return Pan.Right;
+  if (e.code === KeyMapping.KEY_PAN_LEFT) return Pan.Left;
+  if (e.code === KeyMapping.KEY_PAN_RIGHT) return Pan.Right;
   return Pan.None;
 }
 
@@ -64,9 +81,8 @@
   Out = -1
 }
 function keyToZoom(e: KeyboardEvent): Zoom {
-  const key = e.key.toLowerCase();
-  if (['w', ','].includes(key)) return Zoom.In;
-  if (['s', 'o'].includes(key)) return Zoom.Out;
+  if (e.code === KeyMapping.KEY_ZOOM_IN) return Zoom.In;
+  if (e.code === KeyMapping.KEY_ZOOM_OUT) return Zoom.Out;
   return Zoom.None;
 }
 
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index e000ba9..8e9e4c8 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -12,7 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import * as m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
+
+import {assertExists} from '../base/logging';
 import {QueryResponse, runQuery} from '../common/queries';
+import {QueryError} from '../common/query_result';
+import {
+  AddDebugTrackMenu,
+  uuidToViewName,
+} from '../tracks/debug/add_debug_track_menu';
+
 import {
   addTab,
   BottomTab,
@@ -22,6 +32,8 @@
 } from './bottom_tab';
 import {globals} from './globals';
 import {QueryTable} from './query_table';
+import {Button} from './widgets/button';
+import {Popup, PopupPosition} from './widgets/popup';
 
 
 export function runQueryInNewTab(query: string, title: string, tag?: string) {
@@ -47,6 +59,7 @@
   static readonly kind = 'org.perfetto.QueryResultTab';
 
   queryResponse?: QueryResponse;
+  sqlViewName?: string;
 
   static create(args: NewBottomTabArgs): QueryResultTab {
     return new QueryResultTab(args);
@@ -58,10 +71,38 @@
     if (this.config.prefetchedResponse !== undefined) {
       this.queryResponse = this.config.prefetchedResponse;
     } else {
-      runQuery(this.config.query, this.engine).then((result: QueryResponse) => {
-        this.queryResponse = result;
-        globals.rafScheduler.scheduleFullRedraw();
-      });
+      runQuery(this.config.query, this.engine)
+          .then(async (result: QueryResponse) => {
+            this.queryResponse = result;
+            globals.rafScheduler.scheduleFullRedraw();
+
+            if (result.error !== undefined) {
+              return;
+            }
+
+            const uuid = uuidv4();
+            const viewId = uuidToViewName(uuid);
+            // Assuming that it was a SELECT query, try creating a view to allow
+            // us to reuse it for further queries.
+            // TODO(altimin): This should get the actual query that was used to
+            // generate the results from the SQL query iterator.
+            try {
+              const createViewResult = await this.engine.query(
+                  `create view ${viewId} as ${this.config.query}`);
+              if (createViewResult.error()) {
+                // If it failed, do nothing.
+                return;
+              }
+            } catch (e) {
+              if (e instanceof QueryError) {
+                // If it failed, do nothing.
+                return;
+              }
+              throw e;
+            }
+            this.sqlViewName = viewId;
+            globals.rafScheduler.scheduleFullRedraw();
+          });
     }
   }
 
@@ -71,11 +112,25 @@
     return `${this.config.title}${suffix}`;
   }
 
-  viewTab(): void {
+  viewTab(): m.Child {
     return m(QueryTable, {
       query: this.config.query,
       resp: this.queryResponse,
       onClose: () => closeTab(this.uuid),
+      contextButtons: [
+        this.sqlViewName === undefined ?
+            null :
+            m(Popup,
+              {
+                trigger: m(Button, {label: 'Show debug track', minimal: true}),
+                position: PopupPosition.Top,
+              },
+              m(AddDebugTrackMenu, {
+                sqlViewName: this.sqlViewName,
+                columns: assertExists(this.queryResponse).columns,
+                engine: this.engine,
+              })),
+      ],
     });
   }
 
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index 0d7eba5..9331749 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -117,19 +117,47 @@
   }
 }
 
-interface QueryTableAttrs {
-  query: string;
-  resp?: QueryResponse;
-  onClose: () => void;
+interface QueryTableContentAttrs {
+  resp: QueryResponse;
 }
 
-export class QueryTable extends Panel<QueryTableAttrs> {
+class QueryTableContent implements m.ClassComponent<QueryTableContentAttrs> {
   private previousResponse?: QueryResponse;
 
-  onbeforeupdate(vnode: m.CVnode<QueryTableAttrs>) {
+  onbeforeupdate(vnode: m.CVnode<QueryTableContentAttrs>) {
     return vnode.attrs.resp !== this.previousResponse;
   }
 
+  view(vnode: m.CVnode<QueryTableContentAttrs>) {
+    const resp = vnode.attrs.resp;
+    this.previousResponse = resp;
+    const cols = [];
+    for (const col of resp.columns) {
+      cols.push(m('td', col));
+    }
+    const tableHeader = m('tr', cols);
+
+    const rows =
+        resp.rows.map((row) => m(QueryTableRow, {row, columns: resp.columns}));
+
+    if (resp.error) {
+      return m('.query-error', `SQL error: ${resp.error}`);
+    } else {
+      return m(
+          '.query-table-container.x-scrollable',
+          m('table.query-table', m('thead', tableHeader), m('tbody', rows)));
+    }
+  }
+}
+
+interface QueryTableAttrs {
+  query: string;
+  onClose: () => void;
+  resp?: QueryResponse;
+  contextButtons?: m.Child[];
+}
+
+export class QueryTable extends Panel<QueryTableAttrs> {
   view(vnode: m.CVnode<QueryTableAttrs>) {
     const resp = vnode.attrs.resp;
 
@@ -139,6 +167,7 @@
                `Query - running`),
       m('span.code.text-select', vnode.attrs.query),
       m('span.spacer'),
+      ...(vnode.attrs.contextButtons ?? []),
       m(Button, {
         label: 'Copy query',
         minimal: true,
@@ -170,18 +199,6 @@
       return m('div', ...headers);
     }
 
-    this.previousResponse = resp;
-    const cols = [];
-    for (const col of resp.columns) {
-      cols.push(m('td', col));
-    }
-    const tableHeader = m('tr', cols);
-
-    const rows = [];
-    for (let i = 0; i < resp.rows.length; i++) {
-      rows.push(m(QueryTableRow, {row: resp.rows[i], columns: resp.columns}));
-    }
-
     if (resp.statementWithOutputCount > 1) {
       headers.push(
           m('header.overview',
@@ -190,14 +207,7 @@
                 `statement are displayed in the table below.`));
     }
 
-    return m(
-        'div',
-        ...headers,
-        resp.error ? m('.query-error', `SQL error: ${resp.error}`) :
-                     m('.query-table-container.x-scrollable',
-                       m('table.query-table',
-                         m('thead', tableHeader),
-                         m('tbody', rows))));
+    return m('div', ...headers, m(QueryTableContent, {resp}));
   }
 
   renderCanvas() {}
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index e4e7f6a..bd63228 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -152,18 +152,6 @@
   };
 }
 
-function showDebugTrack(): (_: Event) => void {
-  return (e: Event) => {
-    e.preventDefault();
-    globals.dispatch(Actions.addDebugTrack({
-      // The debug track will only be shown once we have a currentEngineId which
-      // is not undefined.
-      engineId: assertExists(globals.state.engine).id,
-      name: 'Debug Slices',
-    }));
-  };
-}
-
 const EXAMPLE_ANDROID_TRACE_URL =
     'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
 
@@ -316,12 +304,6 @@
     summary: 'Compute summary statistics',
     items: [
       {
-        t: 'Show Debug Track',
-        a: showDebugTrack(),
-        i: 'view_day',
-        isVisible: () => globals.state.engine !== undefined,
-      },
-      {
         t: 'Record metatrace',
         a: recordMetatrace,
         i: 'fiber_smart_record',
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
new file mode 100644
index 0000000..259c8d8
--- /dev/null
+++ b/ui/src/frontend/widgets/duration.ts
@@ -0,0 +1,27 @@
+// 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 * as m from 'mithril';
+
+import {fromNs, timeToCode} from '../../common/time';
+
+interface DurationAttrs {
+  dur: number;
+}
+
+export class Duration implements m.ClassComponent<DurationAttrs> {
+  view(vnode: m.Vnode<DurationAttrs>) {
+    return timeToCode(fromNs(vnode.attrs.dur));
+  }
+}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
new file mode 100644
index 0000000..8805b70
--- /dev/null
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -0,0 +1,28 @@
+// 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 * as m from 'mithril';
+
+import {timeToCode} from '../../common/time';
+import {toTraceTime, TPTimestamp} from '../sql_types';
+
+interface TimestampAttrs {
+  ts: TPTimestamp;
+}
+
+export class Timestamp implements m.ClassComponent<TimestampAttrs> {
+  view(vnode: m.Vnode<TimestampAttrs>) {
+    return timeToCode(toTraceTime(vnode.attrs.ts));
+  }
+}
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 3bfae60..0953d2f 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -317,7 +317,8 @@
           title = `${threadInfo.threadName} [${threadInfo.tid}]`;
         }
       }
-      const right = Math.min(timeScale.timeToPx(visibleWindowTime.end), rectEnd);
+      const right =
+          Math.min(timeScale.timeToPx(visibleWindowTime.end), rectEnd);
       const left = Math.max(rectStart, 0);
       const visibleWidth = Math.max(right - left, 1);
       title = cropText(title, charWidth, visibleWidth);
diff --git a/ui/src/tracks/debug/add_debug_track_menu.ts b/ui/src/tracks/debug/add_debug_track_menu.ts
new file mode 100644
index 0000000..e4a74bb
--- /dev/null
+++ b/ui/src/tracks/debug/add_debug_track_menu.ts
@@ -0,0 +1,117 @@
+// 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 * as m from 'mithril';
+
+import {EngineProxy} from '../../common/engine';
+import {Button} from '../../frontend/widgets/button';
+import {Select} from '../../frontend/widgets/select';
+import {TextInput} from '../../frontend/widgets/text_input';
+import {Tree, TreeNode} from '../../frontend/widgets/tree';
+import {addDebugTrack, SliceColumns} from './slice_track';
+
+export const ARG_PREFIX = 'arg_';
+
+export function uuidToViewName(uuid: string): string {
+  return `view_${uuid.split('-').join('_')}`;
+}
+
+interface AddDebugTrackMenuAttrs {
+  sqlViewName: string;
+  columns: string[];
+  engine: EngineProxy;
+}
+
+export class AddDebugTrackMenu implements
+    m.ClassComponent<AddDebugTrackMenuAttrs> {
+  name: string = '';
+  sliceColumns: SliceColumns;
+
+  constructor(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+    const chooseDefaultOption = (name: string) => {
+      for (const column of vnode.attrs.columns) {
+        if (column === name) return column;
+      }
+      for (const column of vnode.attrs.columns) {
+        if (column.endsWith(`_${name}`)) return column;
+      }
+      return vnode.attrs.columns[0];
+    };
+
+    this.sliceColumns = {
+      ts: chooseDefaultOption('ts'),
+      dur: chooseDefaultOption('dur'),
+      name: chooseDefaultOption('name'),
+    };
+  }
+
+  view(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
+    const renderSelect = (name: 'ts'|'dur'|'name') => {
+      const options = [];
+      for (const column of vnode.attrs.columns) {
+        options.push(
+            m('option',
+              {
+                selected: this.sliceColumns[name] === column ? true : undefined,
+              },
+              column));
+      }
+      return m(TreeNode, {
+        left: name,
+        right: m(
+            Select,
+            {
+              oninput: (e: Event) => {
+                if (!e.target) return;
+                this.sliceColumns[name] = (e.target as HTMLSelectElement).value;
+              },
+            },
+            options),
+      });
+    };
+    return [
+      m(
+          Tree,
+          m(TreeNode, {
+            left: 'Name',
+            right: m(TextInput, {
+              onkeydown: (e: KeyboardEvent) => {
+                // Allow Esc to close popup.
+                if (e.key === 'Escape') return;
+                e.stopPropagation();
+              },
+              oninput: (e: KeyboardEvent) => {
+                if (!e.target) return;
+                this.name = (e.target as HTMLInputElement).value;
+              },
+            }),
+          }),
+          renderSelect('ts'),
+          renderSelect('dur'),
+          renderSelect('name'),
+          ),
+      m(Button, {
+        label: 'Show',
+        onclick: () => {
+          addDebugTrack(
+              vnode.attrs.engine,
+              vnode.attrs.sqlViewName,
+              this.name,
+              this.sliceColumns,
+              vnode.attrs.columns);
+        },
+      }),
+    ];
+  }
+}
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
new file mode 100644
index 0000000..d30eaa9
--- /dev/null
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -0,0 +1,119 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as m from 'mithril';
+
+import {ColumnType} from '../../common/query_result';
+import {
+  BottomTab,
+  bottomTabRegistry,
+  NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {globals} from '../../frontend/globals';
+import {TPTimestamp} from '../../frontend/sql_types';
+import {Duration} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Tree, TreeNode} from '../../frontend/widgets/tree';
+import {ARG_PREFIX} from './add_debug_track_menu';
+
+interface DebugSliceDetalsTabConfig {
+  sqlTableName: string;
+  id: number;
+}
+
+function SqlValueToString(val: ColumnType) {
+  if (val instanceof Uint8Array) {
+    return `<blob length=${val.length}>`;
+  }
+  if (val === null) {
+    return 'NULL';
+  }
+  return val.toString();
+}
+
+function dictToTree(dict: {[key: string]: m.Child}): m.Children {
+  const children: m.Child[] = [];
+  for (const key of Object.keys(dict)) {
+    children.push(m(TreeNode, {
+      left: key,
+      right: dict[key],
+    }));
+  }
+  return m(Tree, children);
+}
+
+export class DebugSliceDetailsTab extends BottomTab<DebugSliceDetalsTabConfig> {
+  static readonly kind = 'org.perfetto.DebugSliceDetailsTab';
+
+  data: {[key: string]: ColumnType}|undefined;
+
+  static create(args: NewBottomTabArgs): DebugSliceDetailsTab {
+    return new DebugSliceDetailsTab(args);
+  }
+
+  constructor(args: NewBottomTabArgs) {
+    super(args);
+
+    this.engine
+        .query(`select * from ${this.config.sqlTableName} where id = ${
+            this.config.id}`)
+        .then((queryResult) => {
+          this.data = queryResult.firstRow({});
+          globals.rafScheduler.scheduleFullRedraw();
+        });
+  }
+
+  viewTab() {
+    if (this.data === undefined) {
+      return m('h2', 'Loading');
+    }
+    const left = dictToTree({
+      'Name': this.data['name'] as string,
+      'Start time': m(Timestamp, {ts: this.data['ts'] as TPTimestamp}),
+      'Duration': m(Duration, {dur: this.data['dur'] as number}),
+      'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
+    });
+    const args: {[key: string]: m.Child} = {};
+    for (const key of Object.keys(this.data)) {
+      if (key.startsWith(ARG_PREFIX)) {
+        args[key.substr(ARG_PREFIX.length)] = SqlValueToString(this.data[key]);
+      }
+    }
+    return m(
+        'div.details-panel',
+        m('header.overview', m('span', 'Debug Slice')),
+        m('.details-table-multicolumn',
+          {
+            style: {
+              'user-select': 'text',
+            },
+          },
+          m('.half-width-panel', left),
+          m('.half-width-panel', dictToTree(args))));
+  }
+
+  getTitle(): string {
+    return `Current Selection`;
+  }
+
+  isLoading() {
+    return this.data === undefined;
+  }
+
+  renderTabCanvas() {
+    return;
+  }
+}
+
+bottomTabRegistry.register(DebugSliceDetailsTab);
diff --git a/ui/src/tracks/debug/index.ts b/ui/src/tracks/debug/index.ts
new file mode 100644
index 0000000..e68b451
--- /dev/null
+++ b/ui/src/tracks/debug/index.ts
@@ -0,0 +1,25 @@
+// 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 {PluginContext} from '../../common/plugin_api';
+import {DebugTrackV2} from './slice_track';
+
+function activate(ctx: PluginContext) {
+  ctx.registerTrack(DebugTrackV2);
+}
+
+export const plugin = {
+  pluginId: 'perfetto.DebugSlices',
+  activate,
+};
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
new file mode 100644
index 0000000..6622a03
--- /dev/null
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -0,0 +1,141 @@
+// 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 * as m from 'mithril';
+
+import {Actions, DEBUG_SLICE_TRACK_KIND} from '../../common/actions';
+import {EngineProxy} from '../../common/engine';
+import {Selection} from '../../common/state';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+import {
+  NamedSliceTrack,
+  NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
+import {ARG_PREFIX} from './add_debug_track_menu';
+
+// Names of the columns of the underlying view to be used as ts / dur / name.
+export interface SliceColumns {
+  ts: string;
+  dur: string;
+  name: string;
+}
+
+export interface DebugTrackV2Config {
+  sqlTableName: string;
+  columns: SliceColumns;
+}
+
+interface DebugTrackV2Types extends NamedSliceTrackTypes {
+  config: DebugTrackV2Config;
+}
+
+export class DebugTrackV2 extends NamedSliceTrack<DebugTrackV2Types> {
+  static readonly kind = DEBUG_SLICE_TRACK_KIND;
+
+  static create(args: NewTrackArgs) {
+    return new DebugTrackV2(args);
+  }
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  async initSqlTable(tableName: string): Promise<void> {
+    await this.engine.query(`
+      create view ${tableName} as
+      select
+        id,
+        ts,
+        dur,
+        name,
+        depth
+      from ${this.config.sqlTableName}
+    `);
+  }
+
+  isSelectionHandled(selection: Selection) {
+    if (selection.kind !== 'DEBUG_SLICE') {
+      return false;
+    }
+    return selection.sqlTableName === this.config.sqlTableName;
+  }
+
+  onSliceClick(args: OnSliceClickArgs<DebugTrackV2Types['slice']>) {
+    globals.dispatch(Actions.selectDebugSlice({
+      id: args.slice.id,
+      sqlTableName: this.config.sqlTableName,
+      startS: args.slice.startS,
+      durationS: args.slice.durationS,
+      trackId: this.trackId,
+    }));
+  }
+
+  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
+    return [m(TrackButton, {
+      action: () => {
+        globals.dispatch(Actions.removeDebugTrack({trackId: this.trackId}));
+      },
+      i: 'close',
+      tooltip: 'Close',
+      showButton: true,
+    })];
+  }
+}
+
+let debugTrackCount = 0;
+
+export async function addDebugTrack(
+    engine: EngineProxy,
+    sqlViewName: string,
+    trackName: string,
+    sliceColumns: SliceColumns,
+    argColumns: string[]) {
+  // QueryResultTab has successfully created a view corresponding to |uuid|.
+  // To prepare displaying it as a track, we materialize it and compute depths.
+  const debugTrackId = ++debugTrackCount;
+  const sqlTableName = `materialized_${debugTrackId}_${sqlViewName}`;
+  // TODO(altimin): Support removing this table when the track is closed.
+  await engine.query(`
+      create table ${sqlTableName} as
+      with prepared_data as (
+        select
+          row_number() over () as id,
+          ${sliceColumns.ts} as ts,
+          cast(${sliceColumns.dur} as int) as dur,
+          printf('%s', ${sliceColumns.name}) as name
+          ${argColumns.length > 0 ? ',' : ''}
+          ${argColumns.map((c) => `${c} as ${ARG_PREFIX}${c}`).join(',')}
+        from ${sqlViewName}
+      )
+      select
+        *,
+        internal_layout(ts, dur) over (
+          order by ${sliceColumns.ts}
+          rows between unbounded preceding and current row
+        ) as depth
+      from prepared_data
+      order by ts;`);
+
+  globals.dispatch(Actions.addDebugTrack({
+    engineId: engine.engineId,
+    name: trackName.trim() || `Debug Track ${debugTrackId}`,
+    config: {
+      sqlTableName,
+      columns: sliceColumns,
+    },
+  }));
+}
diff --git a/ui/src/tracks/debug_slices/index.ts b/ui/src/tracks/debug_slices/index.ts
deleted file mode 100644
index 46c1fdd..0000000
--- a/ui/src/tracks/debug_slices/index.ts
+++ /dev/null
@@ -1,148 +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 * as m from 'mithril';
-
-import {Actions} from '../../common/actions';
-import {PluginContext} from '../../common/plugin_api';
-import {NUM, NUM_NULL, STR} from '../../common/query_result';
-import {fromNs, toNs} from '../../common/time';
-import {
-  TrackController,
-} from '../../controller/track_controller';
-import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
-import {ChromeSliceTrack} from '../chrome_slices';
-
-export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
-
-export interface Config {
-  maxDepth: number;
-}
-
-import {Data} from '../chrome_slices';
-export {Data} from '../chrome_slices';
-
-class DebugSliceTrackController extends TrackController<Config, Data> {
-  static readonly kind = DEBUG_SLICE_TRACK_KIND;
-
-  async onReload() {
-    const rawResult = await this.query(
-        `select ifnull(max(depth), 1) as maxDepth from debug_slices`);
-    const maxDepth = rawResult.firstRow({maxDepth: NUM}).maxDepth;
-    globals.dispatch(
-        Actions.updateTrackConfig({id: this.trackId, config: {maxDepth}}));
-  }
-
-  async onBoundsChange(start: number, end: number, resolution: number):
-      Promise<Data> {
-    const queryRes = await this.query(`select
-      ifnull(id, -1) as id,
-      CAST(ifnull(name, '[null]') AS text) as name,
-      ts,
-      iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur) as dur,
-      ifnull(depth, 0) as depth
-      from debug_slices
-      where (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
-
-    const numRows = queryRes.numRows();
-
-    const slices: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      strings: [],
-      sliceIds: new Float64Array(numRows),
-      starts: new Float64Array(numRows),
-      ends: new Float64Array(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(
-        {id: NUM, name: STR, ts: NUM_NULL, dur: NUM_NULL, depth: NUM});
-    for (let row = 0; it.valid(); it.next(), row++) {
-      let sliceStart: number;
-      let sliceEnd: number;
-      if (it.ts === null || it.dur === null) {
-        sliceStart = sliceEnd = -1;
-      } else {
-        sliceStart = it.ts;
-        sliceEnd = sliceStart + it.dur;
-      }
-      slices.sliceIds[row] = it.id;
-      slices.starts[row] = fromNs(sliceStart);
-      slices.ends[row] = fromNs(sliceEnd);
-      slices.depths[row] = it.depth;
-      const sliceName = it.name;
-      slices.titles[row] = internString(sliceName);
-      slices.isInstant[row] = 0;
-      slices.isIncomplete[row] = 0;
-    }
-
-    return slices;
-  }
-}
-
-export class DebugSliceTrack extends ChromeSliceTrack {
-  static readonly kind = DEBUG_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): Track {
-    return new DebugSliceTrack(args);
-  }
-
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
-    const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
-    buttons.push(m(TrackButton, {
-      action: () => {
-        globals.dispatch(Actions.requestTrackReload({}));
-      },
-      i: 'refresh',
-      tooltip: 'Refresh tracks',
-      showButton: true,
-    }));
-    buttons.push(m(TrackButton, {
-      action: () => {
-        globals.dispatch(Actions.removeDebugTrack({}));
-      },
-      i: 'close',
-      tooltip: 'Close',
-      showButton: true,
-    }));
-    return buttons;
-  }
-}
-
-function activate(ctx: PluginContext) {
-  ctx.registerTrack(DebugSliceTrack);
-  ctx.registerTrackController(DebugSliceTrackController);
-}
-
-export const plugin = {
-  pluginId: 'perfetto.DebugSlices',
-  activate,
-};