Merge "Use DetailsShell in ftrace panel, logs panel, and query panel."
diff --git a/Android.bp b/Android.bp
index 2586c75..722dca6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -4364,6 +4364,7 @@
     srcs: [
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/batt_metric.proto",
         "protos/perfetto/metrics/android/binder_metric.proto",
@@ -4437,6 +4438,7 @@
     srcs: [
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/batt_metric.proto",
         "protos/perfetto/metrics/android/binder_metric.proto",
@@ -4493,6 +4495,7 @@
     srcs: [
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/batt_metric.proto",
         "protos/perfetto/metrics/android/binder_metric.proto",
@@ -10055,6 +10058,7 @@
         "src/trace_processor/metrics/sql/android/android_simpleperf.sql",
         "src/trace_processor/metrics/sql/android/android_startup.sql",
         "src/trace_processor/metrics/sql/android/android_surfaceflinger.sql",
+        "src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql",
         "src/trace_processor/metrics/sql/android/android_task_names.sql",
         "src/trace_processor/metrics/sql/android/android_trace_quality.sql",
         "src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql",
diff --git a/BUILD b/BUILD
index e590dd7..d076160 100644
--- a/BUILD
+++ b/BUILD
@@ -1790,6 +1790,7 @@
         "src/trace_processor/metrics/sql/android/android_simpleperf.sql",
         "src/trace_processor/metrics/sql/android/android_startup.sql",
         "src/trace_processor/metrics/sql/android/android_surfaceflinger.sql",
+        "src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql",
         "src/trace_processor/metrics/sql/android/android_task_names.sql",
         "src/trace_processor/metrics/sql/android/android_trace_quality.sql",
         "src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql",
@@ -3933,6 +3934,7 @@
     srcs = [
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
         "protos/perfetto/metrics/android/android_trusty_workqueues.proto",
         "protos/perfetto/metrics/android/batt_metric.proto",
         "protos/perfetto/metrics/android/binder_metric.proto",
diff --git a/OWNERS b/OWNERS
index 5c57ecb..3c29a20 100644
--- a/OWNERS
+++ b/OWNERS
@@ -25,6 +25,9 @@
 # UI, Chromium-related metrics and simpler trace processor changes.
 altimin@google.com
 
+# UI
+stevegolton@google.com
+
 # Most Android-related metrics.
 ilkos@google.com
 
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 2fed17a..5a48545 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -20,6 +20,7 @@
     "source_set",
   ]
   sources = [
+    "android_sysui_notifications_blocking_calls_metric.proto",
     "android_blocking_calls_cuj_metric.proto",
     "android_frame_timeline_metric.proto",
     "android_trusty_workqueues.proto",
diff --git a/protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto b/protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
new file mode 100644
index 0000000..909c48c
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+import "protos/perfetto/metrics/android/process_metadata.proto";
+
+// Blocking calls inside System UI Notifications. Shows count and total duration for each.
+message AndroidSysUINotificationsBlockingCallsMetric {
+  repeated BlockingCall blocking_calls = 1;
+
+  // Blocking call on the main thread.
+  message BlockingCall {
+    // Name of the blocking call
+    optional string name = 1;
+    // Number of times it happened within the CUJ
+    optional int64 cnt = 2;
+    // Total duration within the CUJ
+    optional int64 total_dur_ms = 3;
+    // Maximal duration within the CUJ
+    optional int64 max_dur_ms = 4;
+    // Minimal duration within the CUJ
+    optional int64 min_dur_ms = 5;
+    // Total duration within the CUJ in nanoseconds
+    optional int64 total_dur_ns = 6;
+    // Maximal duration within the CUJ in nanoseconds
+    optional int64 max_dur_ns = 7;
+    // Minimal duration within the CUJ in nanoseconds
+    optional int64 min_dur_ns = 8;
+  }
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 8b5e5e4..1beec18 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -20,6 +20,7 @@
 
 import "protos/perfetto/metrics/android/android_frame_timeline_metric.proto";
 import "protos/perfetto/metrics/android/batt_metric.proto";
+import "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto";
 import "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto";
 import "protos/perfetto/metrics/android/cpu_metric.proto";
 import "protos/perfetto/metrics/android/camera_metric.proto";
@@ -102,7 +103,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 50
+// Next id: 52
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -239,6 +240,8 @@
 
   optional AndroidMonitorContentionMetric android_monitor_contention = 50;
 
+  optional AndroidSysUINotificationsBlockingCallsMetric android_sysui_notifications_blocking_calls_metric = 51;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 393095c..0758d23 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -133,6 +133,35 @@
 
 // End of protos/perfetto/metrics/android/android_frame_timeline_metric.proto
 
+// Begin of protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
+
+// Blocking calls inside System UI Notifications. Shows count and total duration for each.
+message AndroidSysUINotificationsBlockingCallsMetric {
+  repeated BlockingCall blocking_calls = 1;
+
+  // Blocking call on the main thread.
+  message BlockingCall {
+    // Name of the blocking call
+    optional string name = 1;
+    // Number of times it happened within the CUJ
+    optional int64 cnt = 2;
+    // Total duration within the CUJ
+    optional int64 total_dur_ms = 3;
+    // Maximal duration within the CUJ
+    optional int64 max_dur_ms = 4;
+    // Minimal duration within the CUJ
+    optional int64 min_dur_ms = 5;
+    // Total duration within the CUJ in nanoseconds
+    optional int64 total_dur_ns = 6;
+    // Maximal duration within the CUJ in nanoseconds
+    optional int64 max_dur_ns = 7;
+    // Minimal duration within the CUJ in nanoseconds
+    optional int64 min_dur_ns = 8;
+  }
+}
+
+// End of protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
+
 // Begin of protos/perfetto/metrics/android/android_trusty_workqueues.proto
 
 // Metric used to generate a simplified view of the Trusty kworker events.
@@ -2042,7 +2071,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 50
+// Next id: 52
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -2179,6 +2208,8 @@
 
   optional AndroidMonitorContentionMetric android_monitor_contention = 50;
 
+  optional AndroidSysUINotificationsBlockingCallsMetric android_sysui_notifications_blocking_calls_metric = 51;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/python/BUILD.gn b/python/BUILD.gn
index 17ad6f3..dbd1e9d 100644
--- a/python/BUILD.gn
+++ b/python/BUILD.gn
@@ -26,7 +26,6 @@
   sources = [
     "generators/stdlib_docs/extractor.py",
     "generators/stdlib_docs/parse.py",
-    "generators/stdlib_docs/types.py",
     "generators/stdlib_docs/utils.py",
   ]
 }
diff --git a/python/generators/stdlib_docs/extractor.py b/python/generators/stdlib_docs/extractor.py
index 94db4d3..fce88f2 100644
--- a/python/generators/stdlib_docs/extractor.py
+++ b/python/generators/stdlib_docs/extractor.py
@@ -17,7 +17,7 @@
 from re import Match
 from typing import List, Optional, Tuple
 
-from python.generators.stdlib_docs.types import ObjKind
+from python.generators.stdlib_docs.utils import ObjKind
 from python.generators.stdlib_docs.utils import extract_comment
 from python.generators.stdlib_docs.utils import match_pattern
 from python.generators.stdlib_docs.utils import PATTERN_BY_KIND
diff --git a/python/generators/stdlib_docs/parse.py b/python/generators/stdlib_docs/parse.py
index a78bcca..0bb4c28 100644
--- a/python/generators/stdlib_docs/parse.py
+++ b/python/generators/stdlib_docs/parse.py
@@ -20,7 +20,7 @@
 from typing import Any, Dict, List, Optional, Set, Tuple, Union
 
 from python.generators.stdlib_docs.extractor import DocsExtractor
-from python.generators.stdlib_docs.types import ObjKind
+from python.generators.stdlib_docs.utils import ObjKind
 from python.generators.stdlib_docs.utils import ARG_ANNOTATION_PATTERN
 from python.generators.stdlib_docs.utils import NAME_AND_TYPE_PATTERN
 from python.generators.stdlib_docs.utils import FUNCTION_RETURN_PATTERN
diff --git a/python/generators/stdlib_docs/types.py b/python/generators/stdlib_docs/types.py
deleted file mode 100644
index a9baad3..0000000
--- a/python/generators/stdlib_docs/types.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the 'License');
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an 'AS IS' BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from enum import Enum
-
-
-class ObjKind(str, Enum):
-  table_view = 'table_view'
-  function = 'function'
-  view_function = 'view_function'
diff --git a/python/generators/stdlib_docs/utils.py b/python/generators/stdlib_docs/utils.py
index eeccc01..d635b08 100644
--- a/python/generators/stdlib_docs/utils.py
+++ b/python/generators/stdlib_docs/utils.py
@@ -12,14 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from enum import Enum
 import re
 from typing import Dict, List
 
-from python.generators.stdlib_docs.types import ObjKind
-
 LOWER_NAME = r'[a-z_\d]+'
 UPPER_NAME = r'[A-Z_\d]+'
 ANY_WORDS = r'[^\s].*'
+ANY_NON_QUOTE = r'[^\']*.*'
 TYPE = r'[A-Z]+'
 SQL = r'[\s\S]*?'
 
@@ -47,10 +47,17 @@
     # Args: anything before closing bracket with '.
     fr"({ANY_WORDS})\)',\s*"
     # Return columns: anything between two '.
-    fr"'\s*({ANY_WORDS})',\s*"
+    fr"'\s*({ANY_NON_QUOTE})\s*',\s*"
     # Sql: Anything between ' and ');. We are catching \'.
     fr"'({SQL})'\s*\);")
 
+
+class ObjKind(str, Enum):
+  table_view = 'table_view'
+  function = 'function'
+  view_function = 'view_function'
+
+
 PATTERN_BY_KIND = {
     ObjKind.table_view: CREATE_TABLE_VIEW_PATTERN,
     ObjKind.function: CREATE_FUNCTION_PATTERN,
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 536dff9..fcdc866 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_processor/metrics.descriptor.sha1 b/python/perfetto/trace_processor/metrics.descriptor.sha1
new file mode 100644
index 0000000..d0befe1
--- /dev/null
+++ b/python/perfetto/trace_processor/metrics.descriptor.sha1
@@ -0,0 +1,6 @@
+
+// SHA1(tools/gen_binary_descriptors)
+// 6886b319e65925c037179e71a803b8473d06dc7d
+// SHA1(protos/perfetto/metrics/metrics.proto)
+// fe539c6187d701d117ab2f757e1a6b20a035af9f
+  
\ No newline at end of file
diff --git a/src/trace_processor/db/overlays/null_overlay.cc b/src/trace_processor/db/overlays/null_overlay.cc
index 204f113..7bc1b43 100644
--- a/src/trace_processor/db/overlays/null_overlay.cc
+++ b/src/trace_processor/db/overlays/null_overlay.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/db/overlays/null_overlay.h"
 #include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/containers/bit_vector.h"
 #include "src/trace_processor/db/overlays/types.h"
 
 namespace perfetto {
@@ -31,6 +32,18 @@
   return StorageRange(start, end);
 }
 
+TableRangeOrBitVector NullOverlay::MapToTableRangeOrBitVector(
+    StorageRange s_range,
+    OverlayOp op) const {
+  PERFETTO_DCHECK(s_range.range.end <= non_null_->CountSetBits());
+
+  BitVector range_to_bv(s_range.range.start, false);
+  range_to_bv.Resize(s_range.range.end, true);
+
+  return TableRangeOrBitVector(
+      MapToTableBitVector(StorageBitVector{std::move(range_to_bv)}, op).bv);
+}
+
 TableBitVector NullOverlay::MapToTableBitVector(StorageBitVector s_bv,
                                                 OverlayOp op) const {
   BitVector res = non_null_->Copy();
diff --git a/src/trace_processor/db/overlays/null_overlay.h b/src/trace_processor/db/overlays/null_overlay.h
index 328dc9b..5753fa3 100644
--- a/src/trace_processor/db/overlays/null_overlay.h
+++ b/src/trace_processor/db/overlays/null_overlay.h
@@ -18,6 +18,7 @@
 #define SRC_TRACE_PROCESSOR_DB_OVERLAYS_NULL_OVERLAY_H_
 
 #include "src/trace_processor/db/overlays/storage_overlay.h"
+#include "src/trace_processor/db/overlays/types.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -31,6 +32,9 @@
 
   StorageRange MapToStorageRange(TableRange) const override;
 
+  TableRangeOrBitVector MapToTableRangeOrBitVector(StorageRange,
+                                                   OverlayOp) const override;
+
   TableBitVector MapToTableBitVector(StorageBitVector,
                                      OverlayOp) const override;
 
diff --git a/src/trace_processor/db/overlays/null_overlay_unittest.cc b/src/trace_processor/db/overlays/null_overlay_unittest.cc
index 5782c3e..d69780c 100644
--- a/src/trace_processor/db/overlays/null_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/null_overlay_unittest.cc
@@ -40,6 +40,25 @@
   ASSERT_EQ(r.range.end, 4u);
 }
 
+TEST(NullOverlay, MapToTableRangeOutsideBoundary) {
+  BitVector bv{0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0};
+  NullOverlay overlay(&bv);
+  auto r =
+      overlay.MapToTableRangeOrBitVector(StorageRange(1, 3), OverlayOp::kOther);
+
+  // All set bits between |bv| index 3 and 6.
+  ASSERT_EQ(r.TakeIfBitVector().CountSetBits(), 2u);
+}
+
+TEST(NullOverlay, MapToTableRangeOnBoundary) {
+  BitVector bv{0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0};
+  NullOverlay overlay(&bv);
+  auto r =
+      overlay.MapToTableRangeOrBitVector(StorageRange(0, 5), OverlayOp::kOther);
+
+  ASSERT_EQ(r.TakeIfBitVector().CountSetBits(), 5u);
+}
+
 TEST(NullOverlay, MapToTableBitVector) {
   BitVector bv{0, 1, 1, 0, 0, 1, 1, 0};
   NullOverlay overlay(&bv);
diff --git a/src/trace_processor/db/overlays/selector_overlay.cc b/src/trace_processor/db/overlays/selector_overlay.cc
index bc6fcfb..fc40bc6 100644
--- a/src/trace_processor/db/overlays/selector_overlay.cc
+++ b/src/trace_processor/db/overlays/selector_overlay.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/db/overlays/selector_overlay.h"
 #include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/overlays/types.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -30,6 +31,18 @@
             selected_->IndexOfNthSet(t_range.range.end - 1) + 1)};
 }
 
+TableRangeOrBitVector SelectorOverlay::MapToTableRangeOrBitVector(
+    StorageRange s_range,
+    OverlayOp) const {
+  if (s_range.range.size() == 0)
+    return TableRangeOrBitVector(Range());
+
+  uint32_t start = selected_->CountSetBits(s_range.range.start);
+  uint32_t end = selected_->CountSetBits(s_range.range.end);
+
+  return TableRangeOrBitVector(Range(start, end));
+}
+
 TableBitVector SelectorOverlay::MapToTableBitVector(StorageBitVector s_bv,
                                                     OverlayOp) const {
   PERFETTO_DCHECK(selected_->size() >= s_bv.bv.size());
diff --git a/src/trace_processor/db/overlays/selector_overlay.h b/src/trace_processor/db/overlays/selector_overlay.h
index 73499b2..ac591d3 100644
--- a/src/trace_processor/db/overlays/selector_overlay.h
+++ b/src/trace_processor/db/overlays/selector_overlay.h
@@ -31,6 +31,9 @@
 
   StorageRange MapToStorageRange(TableRange) const override;
 
+  TableRangeOrBitVector MapToTableRangeOrBitVector(StorageRange,
+                                                   OverlayOp) const override;
+
   TableBitVector MapToTableBitVector(StorageBitVector,
                                      OverlayOp) const override;
 
diff --git a/src/trace_processor/db/overlays/selector_overlay_unittest.cc b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
index ccc6980..1e4a130 100644
--- a/src/trace_processor/db/overlays/selector_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
@@ -15,6 +15,7 @@
  */
 
 #include "src/trace_processor/db/overlays/selector_overlay.h"
+#include "src/trace_processor/db/overlays/types.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
@@ -40,6 +41,26 @@
   ASSERT_EQ(r.range.end, 7u);
 }
 
+TEST(SelectorOverlay, MapToTableRangeFirst) {
+  BitVector selector{0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1};
+  SelectorOverlay overlay(&selector);
+  auto r =
+      overlay.MapToTableRangeOrBitVector(StorageRange(2, 5), OverlayOp::kOther);
+
+  ASSERT_EQ(r.TakeIfRange().start, 1u);
+  ASSERT_EQ(r.TakeIfRange().end, 3u);
+}
+
+TEST(SelectorOverlay, MapToTableRangeSecond) {
+  BitVector selector{0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0};
+  SelectorOverlay overlay(&selector);
+  auto r = overlay.MapToTableRangeOrBitVector(StorageRange(0, 10),
+                                              OverlayOp::kOther);
+
+  ASSERT_EQ(r.TakeIfRange().start, 0u);
+  ASSERT_EQ(r.TakeIfRange().end, 6u);
+}
+
 TEST(SelectorOverlay, MapToTableBitVector) {
   BitVector selector{0, 1, 1, 0, 0, 1, 1, 0};
   SelectorOverlay overlay(&selector);
diff --git a/src/trace_processor/db/overlays/storage_overlay.h b/src/trace_processor/db/overlays/storage_overlay.h
index c31610b..d6ad8df 100644
--- a/src/trace_processor/db/overlays/storage_overlay.h
+++ b/src/trace_processor/db/overlays/storage_overlay.h
@@ -50,6 +50,11 @@
   // indices in the storage space.
   virtual StorageRange MapToStorageRange(TableRange) const = 0;
 
+  // Returns the smallest Range or BitVector containing all of the elements
+  // matching the OverlayOp.
+  virtual TableRangeOrBitVector MapToTableRangeOrBitVector(StorageRange,
+                                                           OverlayOp) const = 0;
+
   // Maps a BitVector of indices in storage space to an equivalent range of
   // indices in the table space.
   virtual TableBitVector MapToTableBitVector(StorageBitVector,
diff --git a/src/trace_processor/db/overlays/types.h b/src/trace_processor/db/overlays/types.h
index 0ea50c5..83af6bc 100644
--- a/src/trace_processor/db/overlays/types.h
+++ b/src/trace_processor/db/overlays/types.h
@@ -16,6 +16,8 @@
 #ifndef SRC_TRACE_PROCESSOR_DB_OVERLAYS_TYPES_H_
 #define SRC_TRACE_PROCESSOR_DB_OVERLAYS_TYPES_H_
 
+#include <variant>
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/containers/bit_vector.h"
 #include "src/trace_processor/containers/row_map.h"
 #include "src/trace_processor/db/storage/types.h"
@@ -24,20 +26,22 @@
 namespace trace_processor {
 namespace overlays {
 
+using Range = RowMap::Range;
+
 // A range of indices in the table space.
 struct TableRange {
   TableRange(uint32_t start, uint32_t end) : range(start, end) {}
-  explicit TableRange(RowMap::Range r) : range(r) {}
+  explicit TableRange(Range r) : range(r) {}
 
-  RowMap::Range range;
+  Range range;
 };
 
 // A range of indices in the storage space.
 struct StorageRange {
   StorageRange(uint32_t start, uint32_t end) : range(start, end) {}
-  explicit StorageRange(RowMap::Range r) : range(r) {}
+  explicit StorageRange(Range r) : range(r) {}
 
-  RowMap::Range range;
+  Range range;
 };
 
 // A BitVector with set bits corresponding to indices in the table space.
@@ -50,6 +54,27 @@
   BitVector bv;
 };
 
+using RangeOrBitVector = std::variant<Range, BitVector>;
+
+struct TableRangeOrBitVector {
+  explicit TableRangeOrBitVector(Range range) : val(range) {}
+  explicit TableRangeOrBitVector(BitVector bv) : val(std::move(bv)) {}
+
+  bool IsRange() const { return std::holds_alternative<Range>(val); }
+  bool IsBitVector() const { return std::holds_alternative<BitVector>(val); }
+
+  BitVector TakeIfBitVector() {
+    PERFETTO_DCHECK(IsBitVector());
+    return std::move(*std::get_if<BitVector>(&val));
+  }
+  Range TakeIfRange() {
+    PERFETTO_DCHECK(IsRange());
+    return std::move(*std::get_if<Range>(&val));
+  }
+
+  RangeOrBitVector val = Range();
+};
+
 // Represents a vector of indices in the table space.
 struct TableIndexVector {
   std::vector<uint32_t> indices;
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 923e321..8fceb62 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -22,11 +22,14 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/containers/row_map.h"
 #include "src/trace_processor/db/overlays/null_overlay.h"
 #include "src/trace_processor/db/overlays/selector_overlay.h"
 #include "src/trace_processor/db/overlays/storage_overlay.h"
+#include "src/trace_processor/db/overlays/types.h"
 #include "src/trace_processor/db/query_executor.h"
 #include "src/trace_processor/db/storage/numeric_storage.h"
+#include "src/trace_processor/db/storage/types.h"
 #include "src/trace_processor/db/table.h"
 
 namespace perfetto {
@@ -125,10 +128,16 @@
   if (rm->empty())
     return;
 
+  if (col.sorted_intrinsically && c.op != FilterOp::kNe) {
+    BinarySearch(c, col, rm);
+    return;
+  }
+
   uint32_t rm_size = rm->size();
   uint32_t rm_first = rm->Get(0);
   uint32_t rm_last = rm->Get(rm_size - 1);
   uint32_t range_size = rm_last - rm_first;
+
   // If the number of elements in the rowmap is small or the number of elements
   // is less than 1/10th of the range, use indexed filtering.
   // TODO(b/283763282): use Overlay estimations.
@@ -180,6 +189,45 @@
   return std::move(filtered_storage.bv);
 }
 
+void QueryExecutor::BinarySearch(const Constraint& c,
+                                 const SimpleColumn& col,
+                                 RowMap* rm) {
+  // TODO(b/283763282): We should align these to word boundaries.
+  TableRange table_range{Range(rm->Get(0), rm->Get(rm->size() - 1) + 1)};
+  base::SmallVector<Range, kMaxOverlayCount> overlay_bounds;
+
+  for (const auto& overlay : col.overlays) {
+    StorageRange storage_range = overlay->MapToStorageRange(table_range);
+    overlay_bounds.emplace_back(storage_range.range);
+    table_range = TableRange({storage_range.range});
+  }
+
+  // Use binary search algorithm on storage.
+  overlays::TableRangeOrBitVector res(
+      col.storage->BinarySearchIntrinsic(c.op, c.value, table_range.range));
+
+  OverlayOp op = overlays::FilterOpToOverlayOp(c.op);
+  for (uint32_t i = 0; i < col.overlays.size(); ++i) {
+    uint32_t rev_i = static_cast<uint32_t>(col.overlays.size()) - 1 - i;
+
+    if (res.IsBitVector()) {
+      TableBitVector t_bv = col.overlays[rev_i]->MapToTableBitVector(
+          StorageBitVector{res.TakeIfBitVector()}, op);
+      res.val = std::move(t_bv.bv);
+    } else {
+      res = col.overlays[rev_i]->MapToTableRangeOrBitVector(
+          StorageRange(res.TakeIfRange()), op);
+    }
+  }
+
+  if (res.IsBitVector()) {
+    rm->Intersect(RowMap(res.TakeIfBitVector()));
+    return;
+  }
+
+  rm->Intersect(RowMap(res.TakeIfRange().start, res.TakeIfRange().end));
+}
+
 RowMap QueryExecutor::IndexSearch(const Constraint& c,
                                   const SimpleColumn& col,
                                   RowMap* rm) {
@@ -253,10 +301,12 @@
     use_legacy = use_legacy || (overlays::FilterOpToOverlayOp(c.op) ==
                                     overlays::OverlayOp::kOther &&
                                 col.type() != c.value.type);
-    use_legacy = use_legacy || col.IsSorted() || col.IsDense() || col.IsSetId();
+    use_legacy = use_legacy || col.IsDense() || col.IsSetId();
     use_legacy =
         use_legacy || (col.overlay().size() != col.storage_base().size() &&
                        !col.overlay().row_map().IsBitVector());
+    use_legacy = use_legacy ||
+                 (col.IsSorted() && col.overlay().row_map().IsIndexVector());
     if (use_legacy) {
       col.FilterInto(c.op, c.value, &rm);
       continue;
@@ -266,7 +316,7 @@
     uint32_t s_size = col.storage_base().non_null_size();
 
     storage::NumericStorage storage(s_data, s_size, col.col_type());
-    SimpleColumn s_col{OverlaysVec(), &storage};
+    SimpleColumn s_col{OverlaysVec(), &storage, col.IsSorted()};
 
     overlays::SelectorOverlay selector_overlay(
         col.overlay().row_map().GetIfBitVector());
diff --git a/src/trace_processor/db/query_executor.h b/src/trace_processor/db/query_executor.h
index 2e04bb9..d875453 100644
--- a/src/trace_processor/db/query_executor.h
+++ b/src/trace_processor/db/query_executor.h
@@ -42,6 +42,8 @@
     base::SmallVector<const overlays::StorageOverlay*, kMaxOverlayCount>
         overlays;
     const storage::Storage* storage;
+    // TODO(b/283763282): Move knowledge about sorted state to Storage
+    bool sorted_intrinsically = false;
   };
 
   // |row_count| is the size of the last overlay.
@@ -85,6 +87,12 @@
     return IndexSearch(c, col, rm);
   }
 
+  static void BinarySearchForTesting(const Constraint& c,
+                                     const SimpleColumn& col,
+                                     RowMap* rm) {
+    BinarySearch(c, col, rm);
+  }
+
  private:
   // Updates RowMap with result of filtering single column using the Constraint.
   static void FilterColumn(const Constraint&, const SimpleColumn&, RowMap*);
@@ -95,6 +103,8 @@
                                 const SimpleColumn&,
                                 RowMap*);
 
+  static void BinarySearch(const Constraint&, const SimpleColumn&, RowMap*);
+
   // Filters the column using Index algorithm - finds the indices to filter the
   // storage with.
   static RowMap IndexSearch(const Constraint&, const SimpleColumn&, RowMap*);
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 9024a50..e6eb97d 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -15,6 +15,7 @@
  */
 
 #include <benchmark/benchmark.h>
+#include <string>
 
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/string_utils.h"
@@ -23,6 +24,7 @@
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
+#include "src/trace_processor/tables/track_tables_py.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -247,6 +249,13 @@
 
 BENCHMARK(BM_QESliceTableParentIdEq)->ArgsProduct({{DB::V1, DB::V2}});
 
+static void BM_QESliceTableSorted(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTable(state, table, table.table_.ts().gt(1000));
+}
+
+BENCHMARK(BM_QESliceTableSorted)->ArgsProduct({{DB::V1, DB::V2}});
+
 static void BM_QEFilterWithSparseSelector(benchmark::State& state) {
   ExpectedFrameTimelineTableForBenchmark table(state);
   BenchmarkExpectedFrameTable(state, table, table.table_.track_id().eq(88));
diff --git a/src/trace_processor/db/query_executor_unittest.cc b/src/trace_processor/db/query_executor_unittest.cc
index fa914c0..3e02d79 100644
--- a/src/trace_processor/db/query_executor_unittest.cc
+++ b/src/trace_processor/db/query_executor_unittest.cc
@@ -283,6 +283,63 @@
   ASSERT_EQ(res.Get(1), 3u);
 }
 
+TEST(QueryExecutor, BinarySearch) {
+  std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 5, 6};
+  NumericStorage storage(storage_data.data(), 7, ColumnType::kInt64);
+
+  // Add nulls - {0, 1, NULL, NULL, 2, 3, NULL, NULL, 4, 5, 6, NULL}
+  BitVector null_bv{1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0};
+  NullOverlay null_overlay(&null_bv);
+
+  // Final vector {1, NULL, 3, NULL, 5, NULL}.
+  BitVector selector_bv{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1};
+  SelectorOverlay selector_overlay(&selector_bv);
+
+  // Create the column.
+  OverlaysVec overlays_vec;
+  overlays_vec.emplace_back(&selector_overlay);
+  overlays_vec.emplace_back(&null_overlay);
+  SimpleColumn col{overlays_vec, &storage, true};
+
+  // Filter.
+  Constraint c{0, FilterOp::kGe, SqlValue::Long(3)};
+  QueryExecutor exec({col}, 6);
+  RowMap res = exec.Filter({c});
+
+  ASSERT_EQ(res.size(), 2u);
+  ASSERT_EQ(res.Get(0), 2u);
+  ASSERT_EQ(res.Get(1), 4u);
+}
+
+TEST(QueryExecutor, BinarySearchIsNull) {
+  std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+  NumericStorage storage(storage_data.data(), 10, ColumnType::kInt64);
+
+  // Select 6 elements from storage, resulting in a vector {0, 1, 3, 4, 6, 7}.
+  BitVector selector_bv{1, 1, 0, 1, 1, 0, 1, 1, 0, 0};
+  SelectorOverlay selector_overlay(&selector_bv);
+
+  // Add nulls, final vector {NULL, NULL, NULL 0, 1, 3, 4, 6, 7}.
+  BitVector null_bv{0, 0, 0, 1, 1, 1, 1, 1, 1};
+  NullOverlay null_overlay(&null_bv);
+
+  // Create the column.
+  OverlaysVec overlays_vec;
+  overlays_vec.emplace_back(&null_overlay);
+  overlays_vec.emplace_back(&selector_overlay);
+  SimpleColumn col{overlays_vec, &storage, true};
+
+  // Filter.
+  Constraint c{0, FilterOp::kIsNull, SqlValue::Long(0)};
+  QueryExecutor exec({col}, 9);
+  RowMap res = exec.Filter({c});
+
+  ASSERT_EQ(res.size(), 3u);
+  ASSERT_EQ(res.Get(0), 0u);
+  ASSERT_EQ(res.Get(1), 1u);
+  ASSERT_EQ(res.Get(2), 2u);
+}
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/db/storage/numeric_storage.cc b/src/trace_processor/db/storage/numeric_storage.cc
index 0c3f345..71eebec 100644
--- a/src/trace_processor/db/storage/numeric_storage.cc
+++ b/src/trace_processor/db/storage/numeric_storage.cc
@@ -321,7 +321,7 @@
     RowMap::Range search_range) const {
   std::optional<NumericValue> val = GetNumericTypeVariant(type_, sql_val);
   if (op == FilterOp::kIsNotNull)
-    return RowMap::Range(0, size());
+    return search_range;
 
   if (!val.has_value() || op == FilterOp::kIsNull || op == FilterOp::kGlob)
     return RowMap::Range();
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index a566661..d61d0e3 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -22,6 +22,7 @@
     "android_batt.sql",
     "android_binder.sql",
     "android_blocking_calls_cuj_metric.sql",
+    "android_sysui_notifications_blocking_calls_metric.sql",
     "android_camera.sql",
     "android_camera_unagg.sql",
     "android_cpu.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql b/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
new file mode 100644
index 0000000..470b245
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
@@ -0,0 +1,53 @@
+--
+-- Copyright 2023 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT IMPORT('android.slices');
+
+DROP TABLE IF EXISTS android_sysui_notifications_blocking_calls;
+CREATE TABLE android_sysui_notifications_blocking_calls AS
+SELECT
+    s.name name,
+    COUNT(s.name) count,
+    MAX(dur) AS max_dur_ns,
+    MIN(dur) AS min_dur_ns,
+    SUM(dur) AS total_dur_ns
+FROM slice s
+    JOIN thread_track ON s.track_id = thread_track.id
+    JOIN thread USING (utid)
+WHERE
+    thread.is_main_thread AND
+    s.dur > 0 AND (
+       s.name GLOB 'NotificationStackScrollLayout#onMeasure'
+    OR s.name GLOB 'NotificationToplineView#onMeasure'
+    OR s.name GLOB 'ExpNotRow#*'
+)
+GROUP BY s.name;
+
+DROP VIEW IF EXISTS android_sysui_notifications_blocking_calls_metric_output;
+CREATE VIEW android_sysui_notifications_blocking_calls_metric_output AS
+SELECT AndroidSysUINotificationsBlockingCallsMetric('blocking_calls', (
+        SELECT RepeatedField(
+                AndroidSysUINotificationsBlockingCallsMetric_BlockingCall(
+                'name', a.name,
+                'cnt', a.count,
+                'total_dur_ns', a.total_dur_ns,
+                'max_dur_ns', a.max_dur_ns,
+                'min_dur_ns', a.min_dur_ns
+            )
+        )
+        FROM android_sysui_notifications_blocking_calls a
+        ORDER BY total_dur_ns DESC
+    )
+);
diff --git a/src/trace_processor/stdlib/experimental/thread_executing_span.sql b/src/trace_processor/stdlib/experimental/thread_executing_span.sql
index 338ec66..44bac68 100644
--- a/src/trace_processor/stdlib/experimental/thread_executing_span.sql
+++ b/src/trace_processor/stdlib/experimental/thread_executing_span.sql
@@ -339,6 +339,8 @@
 -- @column blocked_dur        Duration of blocking thread state before waking up.
 -- @column blocked_state      Thread state ('D' or 'S') of blocked thread_state before waking up.
 -- @column blocked_function   Kernel blocking function of thread state before waking up.
+-- @column is_root            Whether this span is the root in the slice tree.
+-- @column is_leaf            Whether this span is the leaf in the slice tree.
 -- @column depth              Tree depth from |root_id|
 -- @column root_id            Thread state id used to start the recursion. Helpful for SQL JOINs
 SELECT CREATE_VIEW_FUNCTION(
@@ -410,6 +412,8 @@
 -- @column blocked_dur        Duration of blocking thread state before waking up.
 -- @column blocked_state      Thread state ('D' or 'S') of blocked thread_state before waking up.
 -- @column blocked_function   Kernel blocking function of thread state before waking up.
+-- @column is_root            Whether this span is the root in the slice tree.
+-- @column is_leaf            Whether this span is the leaf in the slice tree.
 -- @column height             Tree height from |leaf_id|
 -- @column leaf_id            Thread state id used to start the recursion. Helpful for SQL JOINs
 SELECT CREATE_VIEW_FUNCTION(
diff --git a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
new file mode 100644
index 0000000..5b69129
--- /dev/null
+++ b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
@@ -0,0 +1,23 @@
+android_sysui_notifications_blocking_calls_metric {
+    blocking_calls {
+        name: "ExpNotRow#onMeasure(BigTextStyle)"
+        cnt: 1
+        total_dur_ns: 10000000
+        max_dur_ns: 10000000
+        min_dur_ns: 10000000
+    }
+    blocking_calls {
+        name: "ExpNotRow#onMeasure(MessagingStyle)"
+        cnt: 1
+        total_dur_ns: 10000000
+        max_dur_ns: 10000000
+        min_dur_ns: 10000000
+    }
+    blocking_calls {
+        name: "NotificationStackScrollLayout#onMeasure"
+        cnt: 1
+        total_dur_ns: 10000000
+        max_dur_ns: 10000000
+        min_dur_ns: 10000000
+    }
+}
diff --git a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
new file mode 100644
index 0000000..c5df8b4
--- /dev/null
+++ b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License 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.
+
+from os import sys, path
+import synth_common
+
+# com.android.systemui
+SYSUI_PID = 1000
+
+THIRD_PROCESS_PID = 3000
+
+# List of blocking calls
+blocking_call_names = [
+    'NotificationStackScrollLayout#onMeasure', 'ExpNotRow#onMeasure(MessagingStyle)',
+    'ExpNotRow#onMeasure(BigTextStyle)',
+    'Should not be in the metric'
+]
+
+
+def add_main_thread_atrace(trace, ts, ts_end, buf, pid):
+  trace.add_atrace_begin(ts=ts, tid=pid, pid=pid, buf=buf)
+  trace.add_atrace_end(ts=ts_end, tid=pid, pid=pid)
+
+
+def add_async_trace(trace, ts, ts_end, buf, pid):
+  trace.add_atrace_async_begin(ts=ts, tid=pid, pid=pid, buf=buf)
+  trace.add_atrace_async_end(ts=ts_end, tid=pid, pid=pid, buf=buf)
+
+# Creates a trace that contains one of each blocking call.
+def add_all_sysui_notifications_blocking_calls(trace, pid):
+  blocking_call_dur = 10_000_000
+  blocking_call_ts = 2_000_000
+
+  cuj_dur = len(blocking_call_names) * blocking_call_dur
+  add_async_trace(
+      trace,
+      ts=blocking_call_ts,
+      ts_end=blocking_call_ts + cuj_dur,
+      buf="L<TEST_WITH_MANY_BLOCKING_CALLS>",
+      pid=pid)
+
+  for blocking_call in blocking_call_names:
+    add_main_thread_atrace(
+        trace,
+        ts=blocking_call_ts,
+        ts_end=blocking_call_ts + blocking_call_dur,
+        buf=blocking_call,
+        pid=pid)
+    blocking_call_ts += blocking_call_dur
+
+
+def add_process(trace, package_name, uid, pid):
+  trace.add_package_list(ts=0, name=package_name, uid=uid, version_code=1)
+  trace.add_process(
+      pid=pid, ppid=0, cmdline=package_name, uid=uid)
+  trace.add_thread(tid=pid, tgid=pid, cmdline="MainThread", name="MainThread")
+
+
+def setup_trace():
+  trace = synth_common.create_trace()
+  trace.add_packet()
+  add_process(trace, package_name="com.android.systemui", uid=10001,
+              pid=SYSUI_PID)
+  trace.add_ftrace_packet(cpu=0)
+  return trace
+
+
+trace = setup_trace()
+
+
+add_all_sysui_notifications_blocking_calls(trace, pid=SYSUI_PID)
+
+# See test_android_sysui_notifications_blocking_calls.
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index 7d683cf..ba2d198 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -396,6 +396,12 @@
         """,
         out=Path('android_slice_standardization.out'))
 
+  def test_android_sysui_notifications_blocking_calls(self):
+    return DiffTestBlueprint(
+        trace=Path('android_sysui_notifications_blocking_calls_metric.py'),
+        query=Metric('android_sysui_notifications_blocking_calls_metric'),
+        out=Path('android_sysui_notifications_blocking_calls_metric.out'))
+
   def test_monitor_contention_extraction(self):
     return DiffTestBlueprint(
         trace=DataPath('android_monitor_contention_trace.atr'),
diff --git a/tools/gen_stdlib_docs_json.py b/tools/gen_stdlib_docs_json.py
index 1f2e5be..dcb4023 100755
--- a/tools/gen_stdlib_docs_json.py
+++ b/tools/gen_stdlib_docs_json.py
@@ -71,6 +71,11 @@
     import_key = path.split(".sql")[0].replace("/", ".")
 
     docs = parse_file_to_dict(path, sql)
+    if isinstance(docs, list):
+      for d in docs:
+        print(d)
+      return 1
+
     assert isinstance(docs, dict)
     if not any(docs.values()):
       continue
diff --git a/ui/OWNERS b/ui/OWNERS
index 7b9c12b..aaf587f 100644
--- a/ui/OWNERS
+++ b/ui/OWNERS
@@ -1,5 +1,6 @@
 hjd@google.com
 primiano@google.com
+stevegolton@google.com
 
 # Chrome-related bits (but also good escalations for many other UI changes).
 ddrone@google.com
diff --git a/ui/src/assets/widgets/timestamp.scss b/ui/src/assets/widgets/timestamp.scss
index f361ff4..c71bb8d 100644
--- a/ui/src/assets/widgets/timestamp.scss
+++ b/ui/src/assets/widgets/timestamp.scss
@@ -14,23 +14,17 @@
 
 @import "theme";
 
-// Make millis, micros, & nanos slightly smaller than hh:mm:ss for readability.
-$subsec-font-size: 0.9em;
-
 .pf-timecode {
   // Spacing the sub sections using CSS rather than spaces makes the spaces
   // disappear when copying.
   .pf-timecode-millis {
     margin-left: 1px;
-    font-size: $subsec-font-size;
   }
   .pf-timecode-micros {
     margin-left: 2px;
-    font-size: $subsec-font-size;
   }
   .pf-timecode-nanos {
     margin-left: 2px;
-    font-size: $subsec-font-size;
   }
   .pf-button {
     margin-left: 2px;
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 3b95c53..42f2a48 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -178,7 +178,7 @@
     };
     this.sendMessage(response);
     if (res.eof) return;
-    this.readBuffers(offset + res.data.length);
+    this.readBuffers(offset + chunk.length);
   }
 
   async disableTracing() {
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 54bfb5d..1d34836 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -334,7 +334,7 @@
 
 export class ChromeSliceDetailsTab extends
     BottomTab<ChromeSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.ChromeSliceDetailsTab';
+  static readonly kind = 'dev.perfetto.ChromeSliceDetailsTab';
 
   private sliceDetails?: SliceDetails;
 
diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts
index 0cf3043..5d206f7 100644
--- a/ui/src/frontend/generic_slice_details_tab.ts
+++ b/ui/src/frontend/generic_slice_details_tab.ts
@@ -46,7 +46,7 @@
 // need to be rendered and how.
 export class GenericSliceDetailsTab extends
     BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.GenericSliceDetailsTab';
+  static readonly kind = 'dev.perfetto.GenericSliceDetailsTab';
 
   data: {[key: string]: ColumnType}|undefined;
 
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index 7f370f6..dfcaf8c 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -56,7 +56,7 @@
 }
 
 export class QueryResultTab extends BottomTab<QueryResultTabConfig> {
-  static readonly kind = 'org.perfetto.QueryResultTab';
+  static readonly kind = 'dev.perfetto.QueryResultTab';
 
   queryResponse?: QueryResponse;
   sqlViewName?: string;
diff --git a/ui/src/frontend/sql/args.ts b/ui/src/frontend/sql/args.ts
index a03a8b5..6d76ad4 100644
--- a/ui/src/frontend/sql/args.ts
+++ b/ui/src/frontend/sql/args.ts
@@ -27,7 +27,7 @@
 } from '../sql_types';
 
 export type ArgValue = bigint|string|number|boolean|null;
-type ArgValueType = 'int'|'uint'|'string'|'bool'|'real'|'null';
+type ArgValueType = 'int'|'uint'|'pointer'|'string'|'bool'|'real'|'null';
 
 export interface Arg {
   id: ArgsId;
@@ -90,6 +90,9 @@
     case 'int':
     case 'uint':
       return value.intValue;
+    case 'pointer':
+      return value.intValue === null ? null :
+                                       `0x${value.intValue.toString(16)}`;
     case 'string':
       return value.stringValue;
     case 'bool':
diff --git a/ui/src/frontend/sql_table/column.ts b/ui/src/frontend/sql_table/column.ts
index a57a85b..e94da6f 100644
--- a/ui/src/frontend/sql_table/column.ts
+++ b/ui/src/frontend/sql_table/column.ts
@@ -49,7 +49,7 @@
 export function argColumn(c: ArgSetIdColumn, argName: string): Column {
   const escape = (name: string) => name.replace(/\.|\[|\]/g, '_');
   return {
-    expression: `extract_arg(${c.name}, ${sqliteString(argName)}`,
+    expression: `extract_arg(${c.name}, ${sqliteString(argName)})`,
     alias: `_arg_${c.name}_${escape(argName)}`,
     title: `${c.title ?? c.name} ${argName}`,
   };
diff --git a/ui/src/frontend/sql_table/render_cell.ts b/ui/src/frontend/sql_table/render_cell.ts
index 4fdc99b..b3d0a4b 100644
--- a/ui/src/frontend/sql_table/render_cell.ts
+++ b/ui/src/frontend/sql_table/render_cell.ts
@@ -25,7 +25,7 @@
 import {sqlValueToString} from '../sql_utils';
 import {Err} from '../widgets/error';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
-import {renderTimecode} from '../widgets/timestamp';
+import {Timestamp} from '../widgets/timestamp';
 
 import {Column} from './column';
 import {SqlTableState} from './state';
@@ -83,11 +83,6 @@
   return sqlValueToString(value);
 }
 
-function displayTimestamp(value: SqlValue): m.Children {
-  if (typeof value !== 'bigint') return displayValue(value);
-  return renderTimecode(asTPTimestamp(value));
-}
-
 function displayDuration(value: TPTime): string;
 function displayDuration(value: SqlValue): m.Children;
 function displayDuration(value: SqlValue): m.Children {
@@ -100,8 +95,6 @@
 
   // Handle all cases when we have non-trivial formatting.
   switch (column.display?.type) {
-    case 'timestamp':
-      return displayTimestamp(value);
     case 'duration':
     case 'thread_duration':
       return displayDuration(value);
@@ -125,11 +118,6 @@
   const result: m.Child[] = [];
   const value = row[column.alias];
 
-  if (column.display?.type === 'timestamp' && typeof value === 'bigint') {
-    result.push(copyMenuItem('Copy raw timestamp', `${value}`));
-    // result.push(
-    //    copyMenuItem('Copy formatted timestamp', displayTimestamp(value)));
-  }
   if ((column.display?.type === 'duration' ||
        column.display?.type === 'thread_duration') &&
       typeof value === 'bigint') {
@@ -150,6 +138,32 @@
   return result;
 }
 
+function renderStandardColumn(
+    column: Column, row: Row, state: SqlTableState): m.Children {
+  const displayValue = display(column, row);
+  const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
+  return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, displayValue),
+      },
+      ...contextMenuItems,
+  );
+}
+
+function renderTimestampColumn(
+    column: Column, row: Row, state: SqlTableState): m.Children {
+  const value = row[column.alias];
+  if (typeof value !== 'bigint') {
+    return renderStandardColumn(column, row, state);
+  }
+
+  return m(Timestamp, {
+    ts: asTPTimestamp(value),
+    extraMenuItems: getContextMenuItems(column, row, state),
+  });
+}
+
 function renderSliceIdColumn(
     column: {alias: string, display: SliceIdDisplayConfig},
     row: Row): m.Children {
@@ -193,14 +207,8 @@
   if (column.display && column.display.type === 'slice_id') {
     return renderSliceIdColumn(
         {alias: column.alias, display: column.display}, row);
+  } else if (column.display && column.display.type === 'timestamp') {
+    return renderTimestampColumn(column, row, state);
   }
-  const displayValue = display(column, row);
-  const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
-  return m(
-      PopupMenu2,
-      {
-        trigger: m(Anchor, displayValue),
-      },
-      ...contextMenuItems,
-  );
+  return renderStandardColumn(column, row, state);
 }
diff --git a/ui/src/frontend/sql_table/state.ts b/ui/src/frontend/sql_table/state.ts
index 81fd2e7..409d409 100644
--- a/ui/src/frontend/sql_table/state.ts
+++ b/ui/src/frontend/sql_table/state.ts
@@ -18,12 +18,13 @@
 import {NUM, Row} from '../../common/query_result';
 import {globals} from '../globals';
 import {constraintsToQueryFragment} from '../sql_utils';
+
 import {
   Column,
   columnFromSqlTableColumn,
   sqlProjectionsForColumn,
 } from './column';
-import {SqlTableDescription} from './table_description';
+import {SqlTableDescription, startsHidden} from './table_description';
 
 interface ColumnOrderClause {
   // We only allow the table to be sorted by the columns which are displayed to
@@ -78,7 +79,7 @@
     this.filters = filters || [];
     this.columns = [];
     for (const column of this.table.columns) {
-      if (column.startsHidden) continue;
+      if (startsHidden(column)) continue;
       this.columns.push(columnFromSqlTableColumn(column));
     }
     this.orderBy = [];
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
index 177ce43..bc3318f 100644
--- a/ui/src/frontend/sql_table/tab.ts
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -32,7 +32,7 @@
 }
 
 export class SqlTableTab extends BottomTab<SqlTableTabConfig> {
-  static readonly kind = 'org.perfetto.SqlTableTab';
+  static readonly kind = 'dev.perfetto.SqlTableTab';
 
   private state: SqlTableState;
 
diff --git a/ui/src/frontend/sql_table/table_description.ts b/ui/src/frontend/sql_table/table_description.ts
index b3fa312..1aef50b 100644
--- a/ui/src/frontend/sql_table/table_description.ts
+++ b/ui/src/frontend/sql_table/table_description.ts
@@ -21,7 +21,7 @@
 // additional filtering.
 
 export type DisplayConfig =
-    SliceIdDisplayConfig|Timestamp|Duration|ThreadDuration|ArgSetId;
+    SliceIdDisplayConfig|Timestamp|Duration|ThreadDuration;
 
 // Common properties for all columns.
 interface SqlTableColumnBase {
@@ -29,8 +29,6 @@
   name: string;
   // Display name of the column in the UI.
   title?: string;
-  // Whether the column should be hidden by default.
-  startsHidden?: boolean;
 }
 
 export interface ArgSetIdColumn extends SqlTableColumnBase {
@@ -41,10 +39,17 @@
   // Special rendering instructions for this column, including the list
   // of additional columns required for the rendering.
   display?: DisplayConfig;
+  // Whether the column should be hidden by default.
+  startsHidden?: boolean;
 }
 
 export type SqlTableColumn = RegularSqlTableColumn|ArgSetIdColumn;
 
+export function startsHidden(c: SqlTableColumn): boolean {
+  if (isArgSetIdColumn(c)) return true;
+  return c.startsHidden ?? false;
+}
+
 export function isArgSetIdColumn(c: SqlTableColumn): c is ArgSetIdColumn {
   return (c as {type?: string}).type === 'arg_set_id';
 }
@@ -89,9 +94,3 @@
 export interface ThreadDuration {
   type: 'thread_duration';
 }
-
-// Column corresponding to an arg_set_id. Will never be directly displayed,
-// but will allow the user select an argument to display from the arg_set.
-export interface ArgSetId {
-  type: 'arg_set_id';
-}
diff --git a/ui/src/frontend/sql_table/well_known_tables.ts b/ui/src/frontend/sql_table/well_known_tables.ts
index 4b503a4..0af9690 100644
--- a/ui/src/frontend/sql_table/well_known_tables.ts
+++ b/ui/src/frontend/sql_table/well_known_tables.ts
@@ -102,9 +102,7 @@
     {
       name: 'arg_set_id',
       title: 'Arg',
-      display: {
-        type: 'arg_set_id',
-      },
+      type: 'arg_set_id',
     },
   ],
 };
diff --git a/ui/src/frontend/thread_and_process_info.ts b/ui/src/frontend/thread_and_process_info.ts
index 46ab39e..a255902 100644
--- a/ui/src/frontend/thread_and_process_info.ts
+++ b/ui/src/frontend/thread_and_process_info.ts
@@ -119,3 +119,11 @@
 export function getThreadName(info?: ThreadInfo): string|undefined {
   return getDisplayName(info?.name, info?.tid);
 }
+
+// Return the full thread name, including the process name.
+export function getFullThreadName(info?: ThreadInfo): string|undefined {
+  if (info?.process === undefined) {
+    return getThreadName(info);
+  }
+  return `${getThreadName(info)} ${getProcessName(info.process)}`;
+}
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index a4a71a1..e2355d6 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -22,14 +22,17 @@
   TPDuration,
   TPTime,
 } from '../common/time';
-import {Anchor} from './anchor';
 
+import {Anchor} from './anchor';
 import {globals} from './globals';
 import {scrollToTrackAndTs} from './scroll_helper';
+import {Icons} from './semantic_icons';
 import {
+  asTPTimestamp,
   asUtid,
   SchedSqlId,
   ThreadStateSqlId,
+  TPTimestamp,
   Utid,
 } from './sql_types';
 import {
@@ -50,7 +53,7 @@
   // Id of the corresponding entry in the |sched| table.
   schedSqlId?: SchedSqlId;
   // Timestamp of the beginning of this thread state in nanoseconds.
-  ts: TPTime;
+  ts: TPTimestamp;
   // Duration of this thread state in nanoseconds.
   dur: TPDuration;
   // CPU id if this thread state corresponds to a thread running on the CPU.
@@ -109,7 +112,7 @@
     result.push({
       threadStateSqlId: it.threadStateSqlId as ThreadStateSqlId,
       schedSqlId: fromNumNull(it.schedSqlId) as (SchedSqlId | undefined),
-      ts: it.ts,
+      ts: asTPTimestamp(it.ts),
       dur: it.dur,
       cpu: fromNumNull(it.cpu),
       state: translateState(it.state || undefined, ioWait),
@@ -153,7 +156,7 @@
 
 interface ThreadStateRefAttrs {
   id: ThreadStateSqlId;
-  ts: TPTime;
+  ts: TPTimestamp;
   dur: TPDuration;
   utid: Utid;
   // If not present, a placeholder name will be used.
@@ -165,7 +168,7 @@
     return m(
         Anchor,
         {
-          icon: 'open_in_new',
+          icon: Icons.UpdateSelection,
           onclick: () => {
             let trackId: string|number|undefined;
             for (const track of Object.values(globals.state.tracks)) {
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index c4a7295..3d38cc5 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -14,18 +14,25 @@
 
 import m from 'mithril';
 
-import {TPTime} from '../common/time';
+import {formatDurationShort, TPTime} from '../common/time';
 
 import {Anchor} from './anchor';
 import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
 import {globals} from './globals';
 import {asTPTimestamp, SchedSqlId, ThreadStateSqlId} from './sql_types';
 import {
+  getFullThreadName,
   getProcessName,
   getThreadName,
   ThreadInfo,
 } from './thread_and_process_info';
-import {getThreadState, goToSchedSlice, ThreadState} from './thread_state';
+import {
+  getThreadState,
+  getThreadStateFromConstraints,
+  goToSchedSlice,
+  ThreadState,
+  ThreadStateRef,
+} from './thread_state';
 import {DetailsShell} from './widgets/details_shell';
 import {Duration} from './widgets/duration';
 import {GridLayout} from './widgets/grid_layout';
@@ -39,10 +46,18 @@
   readonly id: ThreadStateSqlId;
 }
 
+interface RelatedThreadStates {
+  prev?: ThreadState;
+  next?: ThreadState;
+  waker?: ThreadState;
+  wakee?: ThreadState[];
+}
+
 export class ThreadStateTab extends BottomTab<ThreadStateTabConfig> {
-  static readonly kind = 'org.perfetto.ThreadStateTab';
+  static readonly kind = 'dev.perfetto.ThreadStateTab';
 
   state?: ThreadState;
+  relatedStates?: RelatedThreadStates;
   loaded: boolean = false;
 
   static create(args: NewBottomTabArgs): ThreadStateTab {
@@ -52,13 +67,55 @@
   constructor(args: NewBottomTabArgs) {
     super(args);
 
-    getThreadState(this.engine, this.config.id).then((state?: ThreadState) => {
+    this.load().then(() => {
       this.loaded = true;
-      this.state = state;
       globals.rafScheduler.scheduleFullRedraw();
     });
   }
 
+  async load() {
+    this.state = await getThreadState(this.engine, this.config.id);
+
+    if (!this.state) {
+      return;
+    }
+
+    const relatedStates: RelatedThreadStates = {};
+    relatedStates.prev = (await getThreadStateFromConstraints(this.engine, {
+      filters: [
+        `ts + dur = ${this.state.ts}`,
+        `utid = ${this.state.thread?.utid}`,
+      ],
+      limit: 1,
+    }))[0];
+    relatedStates.next = (await getThreadStateFromConstraints(this.engine, {
+      filters: [
+        `ts = ${this.state.ts + this.state.dur}`,
+        `utid = ${this.state.thread?.utid}`,
+      ],
+      limit: 1,
+    }))[0];
+    if (this.state.wakerThread?.utid !== undefined) {
+      relatedStates.waker = (await getThreadStateFromConstraints(this.engine, {
+        filters: [
+          `utid = ${this.state.wakerThread?.utid}`,
+          `ts <= ${this.state.ts}`,
+          `ts + dur >= ${this.state.ts}`,
+        ],
+      }))[0];
+    }
+    relatedStates.wakee = await getThreadStateFromConstraints(this.engine, {
+      filters: [
+        `waker_utid = ${this.state.thread?.utid}`,
+        `state = 'R'`,
+        `ts >= ${this.state.ts}`,
+        `ts <= ${this.state.ts + this.state.dur}`,
+      ],
+    });
+
+    this.relatedStates = relatedStates;
+  }
+
   getTitle() {
     // TODO(altimin): Support dynamic titles here.
     return 'Current Selection';
@@ -75,7 +132,10 @@
               Section,
               {title: 'Details'},
               this.state && this.renderTree(this.state),
-              )),
+              ),
+          m(Section,
+            {title: 'Related thread states'},
+            this.renderRelatedThreadStates())),
     );
   }
 
@@ -154,8 +214,63 @@
     );
   }
 
+  private renderRelatedThreadStates(): m.Children {
+    if (this.state === undefined || this.relatedStates === undefined) {
+      return 'Loading';
+    }
+    const startTs = this.state.ts;
+    const renderRef = (state: ThreadState, name?: string) => m(ThreadStateRef, {
+      id: state.threadStateSqlId,
+      ts: state.ts,
+      dur: state.dur,
+      utid: state.thread!.utid,
+      name,
+    });
+
+    const nameForNextOrPrev = (state: ThreadState) =>
+        `${state.state} for ${formatDurationShort(state.dur)}`;
+    return m(
+        Tree,
+        this.relatedStates.waker && m(TreeNode, {
+          left: 'Waker',
+          right: renderRef(
+              this.relatedStates.waker,
+              getFullThreadName(this.relatedStates.waker.thread)),
+        }),
+        this.relatedStates.prev && m(TreeNode, {
+          left: 'Previous state',
+          right: renderRef(
+              this.relatedStates.prev,
+              nameForNextOrPrev(this.relatedStates.prev)),
+        }),
+        this.relatedStates.next && m(TreeNode, {
+          left: 'Next state',
+          right: renderRef(
+              this.relatedStates.next,
+              nameForNextOrPrev(this.relatedStates.next)),
+        }),
+        this.relatedStates.wakee && this.relatedStates.wakee.length > 0 &&
+            m(TreeNode,
+              {
+                left: 'Woken threads',
+              },
+              this.relatedStates.wakee.map(
+                  (state) =>
+                      m(TreeNode, ({
+                          left: m(Timestamp, {
+                            ts: state.ts,
+                            display: `Start+${
+                                formatDurationShort(state.ts - startTs)}`,
+                          }),
+                          right:
+                              renderRef(state, getFullThreadName(state.thread)),
+                        })))),
+    );
+  }
+
+
   isLoading() {
-    return this.state === undefined;
+    return this.state === undefined || this.relatedStates === undefined;
   }
 
   renderTabCanvas(): void {}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index a138f17..91b9b82 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -14,12 +14,14 @@
 
 import m from 'mithril';
 
+import {Actions} from '../../common/actions';
 import {Timecode, toDomainTime} from '../../common/time';
+import {Anchor} from '../anchor';
 import {copyToClipboard} from '../clipboard';
+import {globals} from '../globals';
 import {Icons} from '../semantic_icons';
 import {TPTimestamp} from '../sql_types';
 
-import {Button} from './button';
 import {MenuItem, PopupMenu2} from './menu';
 
 // import {MenuItem, PopupMenu2} from './menu';
@@ -28,31 +30,38 @@
   // The timestamp to print, this should be the absolute, raw timestamp as
   // found in trace processor.
   ts: TPTimestamp;
+  // Custom text value to show instead of the default HH:MM:SS.mmm uuu nnn
+  // formatting.
+  display?: m.Children;
+  extraMenuItems?: m.Child[];
 }
 
 export class Timestamp implements m.ClassComponent<TimestampAttrs> {
   view({attrs}: m.Vnode<TimestampAttrs>) {
     const {ts} = attrs;
     return m(
-        'span.pf-timecode',
-        renderTimecode(ts),
-        m(
-            PopupMenu2,
-            {
-              trigger: m(Button, {
-                icon: Icons.ContextMenu,
-                compact: true,
-                minimal: true,
-              }),
-            },
-            m(MenuItem, {
-              icon: Icons.Copy,
-              label: `Copy raw value`,
-              onclick: () => {
-                copyToClipboard(ts.toString());
+        PopupMenu2,
+        {
+          trigger: m(
+              Anchor,
+              {
+                onmouseover: () => {
+                  globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
+                },
+                onmouseout: () => {
+                  globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1n}));
+                },
               },
-            }),
-            ),
+              attrs.display ?? renderTimecode(ts)),
+        },
+        m(MenuItem, {
+          icon: Icons.Copy,
+          label: `Copy raw value`,
+          onclick: () => {
+            copyToClipboard(ts.toString());
+          },
+        }),
+        ...(attrs.extraMenuItems ?? []),
     );
   }
 }
@@ -60,11 +69,12 @@
 export function renderTimecode(ts: TPTimestamp): m.Children {
   const relTime = toDomainTime(ts);
   const {dhhmmss, millis, micros, nanos} = new Timecode(relTime);
-  return [
-    m('span.pf-timecode-hms', dhhmmss),
-    '.',
-    m('span.pf-timecode-millis', millis),
-    m('span.pf-timecode-micros', micros),
-    m('span.pf-timecode-nanos', nanos),
-  ];
+  return m(
+      'span.pf-timecode',
+      m('span.pf-timecode-hms', dhhmmss),
+      '.',
+      m('span.pf-timecode-millis', millis),
+      m('span.pf-timecode-micros', micros),
+      m('span.pf-timecode-nanos', nanos),
+  );
 }
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
index 1a743fb..0451cbe 100644
--- a/ui/src/tracks/debug/details_tab.ts
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -87,7 +87,7 @@
 
 export class DebugSliceDetailsTab extends
     BottomTab<DebugSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.DebugSliceDetailsTab';
+  static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
 
   data?: {
     name: string,