Merge "[stdlib]: Add waker_id to the thread_state table" into main
diff --git a/Android.bp b/Android.bp
index 56e9063..a0269f3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5139,6 +5139,7 @@
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_garbage_collection_unagg_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/anr_metric.proto",
@@ -5226,6 +5227,7 @@
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_garbage_collection_unagg_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/anr_metric.proto",
@@ -5296,6 +5298,7 @@
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_garbage_collection_unagg_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/anr_metric.proto",
@@ -11652,6 +11655,7 @@
         "src/trace_processor/metrics/sql/android/android_dvfs.sql",
         "src/trace_processor/metrics/sql/android/android_fastrpc.sql",
         "src/trace_processor/metrics/sql/android/android_frame_timeline_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_garbage_collection_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_gpu.sql",
         "src/trace_processor/metrics/sql/android/android_hwcomposer.sql",
         "src/trace_processor/metrics/sql/android/android_hwui_metric.sql",
@@ -12058,6 +12062,10 @@
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_executing_span.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_level_parallelism.sql",
         "src/trace_processor/perfetto_sql/stdlib/sched/thread_state_flattened.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/general.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/process.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/system.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/thread.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/flat_slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/slices/with_context.sql",
diff --git a/BUILD b/BUILD
index d38009a..bd40277 100644
--- a/BUILD
+++ b/BUILD
@@ -1957,6 +1957,7 @@
         "src/trace_processor/metrics/sql/android/android_dvfs.sql",
         "src/trace_processor/metrics/sql/android/android_fastrpc.sql",
         "src/trace_processor/metrics/sql/android/android_frame_timeline_metric.sql",
+        "src/trace_processor/metrics/sql/android/android_garbage_collection_unagg.sql",
         "src/trace_processor/metrics/sql/android/android_gpu.sql",
         "src/trace_processor/metrics/sql/android/android_hwcomposer.sql",
         "src/trace_processor/metrics/sql/android/android_hwui_metric.sql",
@@ -2491,6 +2492,17 @@
     ],
 )
 
+# GN target: //src/trace_processor/perfetto_sql/stdlib/sched/utilization:utilization
+perfetto_filegroup(
+    name = "src_trace_processor_perfetto_sql_stdlib_sched_utilization_utilization",
+    srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/general.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/process.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/system.sql",
+        "src/trace_processor/perfetto_sql/stdlib/sched/utilization/thread.sql",
+    ],
+)
+
 # GN target: //src/trace_processor/perfetto_sql/stdlib/sched:sched
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_sched_sched",
@@ -2537,6 +2549,7 @@
         ":src_trace_processor_perfetto_sql_stdlib_pkvm_pkvm",
         ":src_trace_processor_perfetto_sql_stdlib_prelude_prelude",
         ":src_trace_processor_perfetto_sql_stdlib_sched_sched",
+        ":src_trace_processor_perfetto_sql_stdlib_sched_utilization_utilization",
         ":src_trace_processor_perfetto_sql_stdlib_slices_slices",
         ":src_trace_processor_perfetto_sql_stdlib_time_time",
     ],
@@ -4304,6 +4317,7 @@
         "protos/perfetto/metrics/android/android_boot.proto",
         "protos/perfetto/metrics/android/android_boot_unagg.proto",
         "protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+        "protos/perfetto/metrics/android/android_garbage_collection_unagg_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/anr_metric.proto",
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 9caeaef..33f4195 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -26,6 +26,7 @@
     "android_boot.proto",
     "android_boot_unagg.proto",
     "android_frame_timeline_metric.proto",
+    "android_garbage_collection_unagg_metric.proto",
     "android_sysui_notifications_blocking_calls_metric.proto",
     "android_trusty_workqueues.proto",
     "anr_metric.proto",
diff --git a/protos/perfetto/metrics/android/android_boot_unagg.proto b/protos/perfetto/metrics/android/android_boot_unagg.proto
index 12bf12b..c209840 100644
--- a/protos/perfetto/metrics/android/android_boot_unagg.proto
+++ b/protos/perfetto/metrics/android/android_boot_unagg.proto
@@ -19,7 +19,11 @@
 package perfetto.protos;
 
 import "protos/perfetto/metrics/android/app_process_starts_metric.proto";
+import "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto";
 
 message AndroidBootUnagg {
+  // Looks at all apps started after boot complete broadcast.
   optional AndroidAppProcessStartsMetric android_app_process_start_metric = 1;
+  // Looks at all GC that occurs after boot complete broadcast.
+  optional AndroidGarbageCollectionUnaggMetric android_post_boot_gc_metric = 2;
 }
diff --git a/protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto b/protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto
new file mode 100644
index 0000000..075b9a0
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")= ;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+message AndroidGarbageCollectionUnaggMetric {
+  message GarbageCollectionEvent {
+    // Name of thread running garbage collection.
+    optional string thread_name = 1;
+    // Name of process running garbage collection.
+    optional string process_name = 2;
+    // Type of garbage collection.
+    optional string gc_type = 3;
+    // Whether gargage collection is mark compact or copying.
+    optional int64 is_mark_compact = 4;
+    // MB reclaimed after garbage collection.
+    optional double reclaimed_mb = 5;
+    // Minimum heap size in MB during garbage collection.
+    optional double min_heap_mb = 6;
+    // Maximum heap size in MB during garbage collection.
+    optional double max_heap_mb = 7;
+    // Memory reclaimed per ms of running time.
+    optional double mb_per_ms_of_running_gc = 8;
+    // Memory reclaimed per ms of wall time.
+    optional double mb_per_ms_of_wall_gc = 9;
+    // Garbage collection wall duration.
+    optional int64 gc_dur = 10;
+    // Garbage collection duration spent executing on CPU.
+    optional int64 gc_running_dur = 11;
+    // Garbage collection duration spent waiting for CPU.
+    optional int64 gc_runnable_dur = 12;
+    // Garbage collection duration spent waiting in the Linux kernel on IO.
+    optional int64 gc_unint_io_dur = 13;
+    // Garbage collection duration spent waiting in the Linux kernel without IO.
+    optional int64 gc_unint_non_io_dur = 14;
+    // Garbage collection duration spent waiting in interruptible sleep.
+    optional int64 gc_int_dur = 15;
+    // ts of the event in trace.
+    optional int64 gc_ts = 16;
+    // pid of the event in trace.
+    optional int64 pid = 17;
+    // tid of the event in trace.
+    optional int64 tid = 18;
+    // monotonic duration of event.
+    optional int64 gc_monotonic_dur = 19;
+  }
+  repeated GarbageCollectionEvent gc_events = 1;
+}
\ No newline at end of file
diff --git a/protos/perfetto/metrics/android/app_process_starts_metric.proto b/protos/perfetto/metrics/android/app_process_starts_metric.proto
index dafce94..e8457df 100644
--- a/protos/perfetto/metrics/android/app_process_starts_metric.proto
+++ b/protos/perfetto/metrics/android/app_process_starts_metric.proto
@@ -18,7 +18,6 @@
 
 package perfetto.protos;
 
-// Looks at all apps started after boot complete broadcast.
 message AndroidAppProcessStartsMetric {
   // Next id : 4
   message ProcessStart {
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 8bd4d38..5130b29 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -21,6 +21,7 @@
 import "protos/perfetto/metrics/android/ad_services_metric.proto";
 import "protos/perfetto/metrics/android/android_boot.proto";
 import "protos/perfetto/metrics/android/android_boot_unagg.proto";
+import "protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto";
 import "protos/perfetto/metrics/android/sysui_notif_shade_list_builder_metric.proto";
 import "protos/perfetto/metrics/android/sysui_update_notif_on_ui_mode_changed_metric.proto";
 import "protos/perfetto/metrics/android/android_frame_timeline_metric.proto";
@@ -286,6 +287,9 @@
   // Android boot unaggregated metrics.
   optional AndroidBootUnagg android_boot_unagg = 62;
 
+  // Android garbage collection metrics
+  optional AndroidGarbageCollectionUnaggMetric android_garbage_collection_unagg = 63;
+
   // 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 58f9731..eb9ab74 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -193,9 +193,55 @@
 
 // End of protos/perfetto/metrics/android/android_boot.proto
 
+// Begin of protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto
+
+message AndroidGarbageCollectionUnaggMetric {
+  message GarbageCollectionEvent {
+    // Name of thread running garbage collection.
+    optional string thread_name = 1;
+    // Name of process running garbage collection.
+    optional string process_name = 2;
+    // Type of garbage collection.
+    optional string gc_type = 3;
+    // Whether gargage collection is mark compact or copying.
+    optional int64 is_mark_compact = 4;
+    // MB reclaimed after garbage collection.
+    optional double reclaimed_mb = 5;
+    // Minimum heap size in MB during garbage collection.
+    optional double min_heap_mb = 6;
+    // Maximum heap size in MB during garbage collection.
+    optional double max_heap_mb = 7;
+    // Memory reclaimed per ms of running time.
+    optional double mb_per_ms_of_running_gc = 8;
+    // Memory reclaimed per ms of wall time.
+    optional double mb_per_ms_of_wall_gc = 9;
+    // Garbage collection wall duration.
+    optional int64 gc_dur = 10;
+    // Garbage collection duration spent executing on CPU.
+    optional int64 gc_running_dur = 11;
+    // Garbage collection duration spent waiting for CPU.
+    optional int64 gc_runnable_dur = 12;
+    // Garbage collection duration spent waiting in the Linux kernel on IO.
+    optional int64 gc_unint_io_dur = 13;
+    // Garbage collection duration spent waiting in the Linux kernel without IO.
+    optional int64 gc_unint_non_io_dur = 14;
+    // Garbage collection duration spent waiting in interruptible sleep.
+    optional int64 gc_int_dur = 15;
+    // ts of the event in trace.
+    optional int64 gc_ts = 16;
+    // pid of the event in trace.
+    optional int64 pid = 17;
+    // tid of the event in trace.
+    optional int64 tid = 18;
+    // monotonic duration of event.
+    optional int64 gc_monotonic_dur = 19;
+  }
+  repeated GarbageCollectionEvent gc_events = 1;
+}
+// End of protos/perfetto/metrics/android/android_garbage_collection_unagg_metric.proto
+
 // Begin of protos/perfetto/metrics/android/app_process_starts_metric.proto
 
-// Looks at all apps started after boot complete broadcast.
 message AndroidAppProcessStartsMetric {
   // Next id : 4
   message ProcessStart {
@@ -223,7 +269,10 @@
 // Begin of protos/perfetto/metrics/android/android_boot_unagg.proto
 
 message AndroidBootUnagg {
+  // Looks at all apps started after boot complete broadcast.
   optional AndroidAppProcessStartsMetric android_app_process_start_metric = 1;
+  // Looks at all GC that occurs after boot complete broadcast.
+  optional AndroidGarbageCollectionUnaggMetric android_post_boot_gc_metric = 2;
 }
 
 // End of protos/perfetto/metrics/android/android_boot_unagg.proto
@@ -2642,6 +2691,9 @@
   // Android boot unaggregated metrics.
   optional AndroidBootUnagg android_boot_unagg = 62;
 
+  // Android garbage collection metrics
+  optional AndroidGarbageCollectionUnaggMetric android_garbage_collection_unagg = 63;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 9783b76..5c04c0a 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/containers/BUILD.gn b/src/trace_processor/containers/BUILD.gn
index 08cddfc..59f42c9 100644
--- a/src/trace_processor/containers/BUILD.gn
+++ b/src/trace_processor/containers/BUILD.gn
@@ -66,6 +66,7 @@
       ":containers",
       "../../../gn:benchmark",
       "../../../gn:default_deps",
+      "../../base",
     ]
     sources = [
       "bit_vector_benchmark.cc",
diff --git a/src/trace_processor/containers/bit_vector.cc b/src/trace_processor/containers/bit_vector.cc
index 43a5279..c4f2fe7 100644
--- a/src/trace_processor/containers/bit_vector.cc
+++ b/src/trace_processor/containers/bit_vector.cc
@@ -16,17 +16,28 @@
 
 #include "src/trace_processor/containers/bit_vector.h"
 
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <initializer_list>
 #include <limits>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/build_config.h"
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/public/compiler.h"
+#include "src/trace_processor/containers/bit_vector_iterators.h"
 
 #include "protos/perfetto/trace_processor/serialization.pbzero.h"
-#include "src/trace_processor/containers/bit_vector_iterators.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_X64_CPU_OPT)
 #include <immintrin.h>
 #endif
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 // This function implements the PDEP instruction in x64 as a loop.
@@ -103,7 +114,7 @@
 
   // Compute the address of the new last bit in the bitvector.
   Address last_addr = IndexToAddress(new_size - 1);
-  uint32_t old_blocks_size = static_cast<uint32_t>(counts_.size());
+  auto old_blocks_size = static_cast<uint32_t>(counts_.size());
   uint32_t new_blocks_size = last_addr.block_idx + 1;
 
   // Resize the block and count vectors to have the correct number of entries.
@@ -158,22 +169,27 @@
 }
 
 BitVector BitVector::Copy() const {
-  return BitVector(words_, counts_, size_);
-}
-
-BitVector::AllBitsIterator BitVector::IterateAllBits() const {
-  return AllBitsIterator(this);
+  return {words_, counts_, size_};
 }
 
 BitVector::SetBitsIterator BitVector::IterateSetBits() const {
-  return SetBitsIterator(this);
+  return {this};
 }
 
 void BitVector::Not() {
-  for (uint32_t i = 0; i < words_.size(); ++i) {
-    BitWord(&words_[i]).Not();
+  if (size_ == 0) {
+    return;
   }
 
+  for (uint64_t& word : words_) {
+    BitWord(&word).Not();
+  }
+
+  // Make sure to reset the last block's trailing bits to zero to preserve the
+  // invariant of BitVector.
+  Address last_addr = IndexToAddress(size_ - 1);
+  BlockFromIndex(last_addr.block_idx).ClearAfter(last_addr.block_offset);
+
   for (uint32_t i = 1; i < counts_.size(); ++i) {
     counts_[i] = kBitsInBlock * i - counts_[i];
   }
@@ -238,7 +254,7 @@
     if (PERFETTO_UNLIKELY(current == 0))
       continue;
 
-    uint8_t popcount = static_cast<uint8_t>(PERFETTO_POPCOUNT(current));
+    auto popcount = static_cast<uint8_t>(PERFETTO_POPCOUNT(current));
     PERFETTO_DCHECK(popcount >= 1);
 
     // Check if we have enough unused bits from the previous iteration - if so,
@@ -297,17 +313,27 @@
   PERFETTO_DCHECK(update.CountSetBits() == CountSetBits());
 }
 
+void BitVector::SelectBits(const BitVector& mask_bv) {
+  BitVector::Builder res(mask_bv.CountSetBits(size_));
+  for (auto it = mask_bv.IterateSetBits(); it && it.index() < size();
+       it.Next()) {
+    res.Append(IsSet(it.index()));
+  }
+  *this = std::move(res).Build();
+}
+
 BitVector BitVector::FromSortedIndexVector(
     const std::vector<int64_t>& indices) {
   // The rest of the algorithm depends on |indices| being non empty.
   if (indices.empty()) {
-    return BitVector();
+    return {};
   }
 
-  // We are creating the smallest BitVector that can have all of the values from
-  // |indices| set. As we assume that |indices| is sorted, the size would be the
-  // last element + 1 and the last bit of the final BitVector will be set.
-  uint32_t size = static_cast<uint32_t>(indices.back() + 1);
+  // We are creating the smallest BitVector that can have all of the values
+  // from |indices| set. As we assume that |indices| is sorted, the size would
+  // be the last element + 1 and the last bit of the final BitVector will be
+  // set.
+  auto size = static_cast<uint32_t>(indices.back() + 1);
 
   uint32_t block_count = BlockCount(size);
   std::vector<uint64_t> words(block_count * Block::kWords);
@@ -319,13 +345,13 @@
 
   std::vector<uint32_t> counts(block_count);
   for (uint32_t i = 1; i < counts.size(); ++i) {
-    // The number of set bits in each block is the number of set bits before and
-    // in the previous block.
+    // The number of set bits in each block is the number of set bits before
+    // and in the previous block.
     counts[i] = counts[i - 1] +
                 ConstBlock(&words[Block::kWords * (i - 1)]).CountSetBits();
   }
 
-  return BitVector(words, counts, size);
+  return {words, counts, size};
 }
 
 BitVector BitVector::IntersectRange(uint32_t range_start,
@@ -335,7 +361,7 @@
   uint32_t end_idx = std::min(range_end, size());
 
   if (range_start >= end_idx)
-    return BitVector();
+    return {};
 
   Builder builder(end_idx, range_start);
   uint32_t front_bits = builder.BitsUntilWordBoundaryOrFull();
@@ -361,6 +387,15 @@
   return std::move(builder).Build();
 }
 
+std::vector<uint32_t> BitVector::GetSetBitIndices() const {
+  std::vector<uint32_t> res;
+  res.reserve(CountSetBits());
+  for (auto it = IterateSetBits(); it; it.Next()) {
+    res.push_back(it.index());
+  }
+  return res;
+}
+
 void BitVector::Serialize(
     protos::pbzero::SerializedColumn::BitVector* msg) const {
   msg->set_size(size_);
@@ -394,5 +429,4 @@
   }
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/containers/bit_vector.h b/src/trace_processor/containers/bit_vector.h
index c7d67c5..b50165d 100644
--- a/src/trace_processor/containers/bit_vector.h
+++ b/src/trace_processor/containers/bit_vector.h
@@ -17,33 +17,27 @@
 #ifndef SRC_TRACE_PROCESSOR_CONTAINERS_BIT_VECTOR_H_
 #define SRC_TRACE_PROCESSOR_CONTAINERS_BIT_VECTOR_H_
 
-#include <stddef.h>
-#include <stdint.h>
-#include <stdio.h>
-
 #include <algorithm>
-#include <array>
-#include <cstring>
-#include <optional>
+#include <cstdint>
+#include <initializer_list>
+#include <iterator>
+#include <utility>
 #include <vector>
 
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/public/compiler.h"
 
 namespace perfetto {
-
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class SerializedColumn_BitVector;
 class SerializedColumn_BitVector_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+}  // namespace protos::pbzero
 
 namespace trace_processor {
-
 namespace internal {
 
 class BaseIterator;
-class AllBitsIterator;
 class SetBitsIterator;
 
 }  // namespace internal
@@ -52,9 +46,6 @@
 // for each bool.
 class BitVector {
  public:
-  using AllBitsIterator = internal::AllBitsIterator;
-  using SetBitsIterator = internal::SetBitsIterator;
-
   static constexpr uint32_t kBitsInWord = 64;
 
   // Builder class which allows efficiently creating a BitVector by appending
@@ -95,7 +86,7 @@
     // Creates a BitVector from this Builder.
     BitVector Build() && {
       if (size_ == 0)
-        return BitVector();
+        return {};
 
       std::vector<uint32_t> counts(BlockCount(size_));
       PERFETTO_CHECK(skipped_blocks_ <= counts.size());
@@ -103,13 +94,13 @@
         counts[i] = counts[i - 1] +
                     ConstBlock(&words_[Block::kWords * (i - 1)]).CountSetBits();
       }
-      return BitVector(std::move(words_), std::move(counts), size_);
+      return {std::move(words_), std::move(counts), size_};
     }
 
     // Returns the number of bits which are in complete words which can be
     // appended to this builder before having to fallback to |Append| due to
     // being close to the end.
-    uint32_t BitsInCompleteWordsUntilFull() {
+    uint32_t BitsInCompleteWordsUntilFull() const {
       uint32_t next_word = WordCount(global_bit_offset_);
       uint32_t end_word = WordFloor(size_);
       uint32_t complete_words = next_word < end_word ? end_word - next_word : 0;
@@ -120,7 +111,7 @@
     // hitting a word boundary (and thus able to use |AppendWord|) or until the
     // BitVector is full (i.e. no more Appends should happen), whichever would
     // happen first.
-    uint32_t BitsUntilWordBoundaryOrFull() {
+    uint32_t BitsUntilWordBoundaryOrFull() const {
       if (global_bit_offset_ == 0 && size_ < BitWord::kBits) {
         return size_;
       }
@@ -132,7 +123,7 @@
     // Returns the number of bits which should be appended using |Append| before
     // hitting a word boundary (and thus able to use |AppendWord|) or until the
     // BitVector is full (i.e. no more Appends should happen).
-    uint32_t BitsUntilFull() { return size_ - global_bit_offset_; }
+    uint32_t BitsUntilFull() const { return size_ - global_bit_offset_; }
 
    private:
     std::vector<uint64_t> words_;
@@ -144,11 +135,14 @@
   // Creates an empty BitVector.
   BitVector();
 
-  explicit BitVector(std::initializer_list<bool> init);
+  BitVector(std::initializer_list<bool> init);
 
   // Creates a BitVector of |count| size filled with |value|.
   explicit BitVector(uint32_t count, bool value = false);
 
+  BitVector(const BitVector&) = delete;
+  BitVector& operator=(const BitVector&) = delete;
+
   // Enable moving BitVectors as they have no unmovable state.
   BitVector(BitVector&&) noexcept = default;
   BitVector& operator=(BitVector&&) = default;
@@ -238,7 +232,7 @@
     if (PERFETTO_LIKELY(!old_value)) {
       BlockFromIndex(addr.block_idx).Set(addr.block_offset);
 
-      uint32_t size = static_cast<uint32_t>(counts_.size());
+      auto size = static_cast<uint32_t>(counts_.size());
       for (uint32_t i = addr.block_idx + 1; i < size; ++i) {
         counts_[i]++;
       }
@@ -259,7 +253,7 @@
     if (PERFETTO_LIKELY(old_value)) {
       BlockFromIndex(addr.block_idx).Clear(addr.block_offset);
 
-      uint32_t size = static_cast<uint32_t>(counts_.size());
+      auto size = static_cast<uint32_t>(counts_.size());
       for (uint32_t i = addr.block_idx + 1; i < size; ++i) {
         counts_[i]--;
       }
@@ -381,24 +375,19 @@
   // other: 0 1 1 0
   // This will change this to the following:
   // this:  0 1 0 0 1 0 0
-  // TODO(lalitm): investigate whether we should just change this to And.
   void UpdateSetBits(const BitVector& other);
 
-  // Iterate all the bits in the BitVector.
+  // For each set bit position  in |other|, Selects the value of each bit in
+  // |this| and stores them contiguously in |this|.
   //
-  // Usage:
-  // for (auto it = bv.IterateAllBits(); it; it.Next()) {
-  //   ...
-  // }
-  AllBitsIterator IterateAllBits() const;
-
-  // Iterate all the set bits in the BitVector.
+  // Precondition: |this.size()| <= |other.size()|.
   //
-  // Usage:
-  // for (auto it = bv.IterateSetBits(); it; it.Next()) {
-  //   ...
-  // }
-  SetBitsIterator IterateSetBits() const;
+  // For example suppose the following:
+  // this:  1 1 0 0 1 0 1
+  // other: 0 1 0 1 0 1 0 0 1 0
+  // |this| will change this to the following:
+  // this:  1 0 0
+  void SelectBits(const BitVector& other);
 
   // Returns the approximate cost (in bytes) of storing a BitVector with size
   // |n|. This can be used to make decisions about whether using a BitVector is
@@ -411,6 +400,10 @@
     return BlockCount(n) * Block::kBits + BlockCount(n) * sizeof(uint32_t);
   }
 
+  // Returns a vector<uint32_t> containing the indices of all the set bits
+  // in the BitVector.
+  std::vector<uint32_t> GetSetBitIndices() const;
+
   // Serialize internals of BitVector to proto.
   void Serialize(protos::pbzero::SerializedColumn_BitVector* msg) const;
 
@@ -419,8 +412,8 @@
       const protos::pbzero::SerializedColumn_BitVector_Decoder& bv_msg);
 
  private:
+  using SetBitsIterator = internal::SetBitsIterator;
   friend class internal::BaseIterator;
-  friend class internal::AllBitsIterator;
   friend class internal::SetBitsIterator;
 
   // Represents the offset of a bit within a block.
@@ -636,9 +629,6 @@
   // On x86 architectures we generally target for trace processor, the
   // size of a cache line is 64 bytes (or 512 bits). For this reason,
   // we make the size of the block contain 8 atoms as 8 * 64 == 512.
-  //
-  // TODO(lalitm): investigate whether we should tune this value for
-  // WASM and ARM.
   class Block {
    public:
     // See class documentation for how these constants are chosen.
@@ -812,9 +802,6 @@
             std::vector<uint32_t> counts,
             uint32_t size);
 
-  BitVector(const BitVector&) = delete;
-  BitVector& operator=(const BitVector&) = delete;
-
   // Returns the number of 8 elements blocks in the BitVector.
   uint32_t BlockCount() {
     return static_cast<uint32_t>(words_.size()) / Block::kWords;
@@ -876,6 +863,14 @@
     }
   }
 
+  // Iterate all the set bits in the BitVector.
+  //
+  // Usage:
+  // for (auto it = bv.IterateSetBits(); it; it.Next()) {
+  //   ...
+  // }
+  SetBitsIterator IterateSetBits() const;
+
   // Returns the index of the word which would store |idx|.
   static constexpr uint32_t WordFloor(uint32_t idx) {
     return idx / BitWord::kBits;
diff --git a/src/trace_processor/containers/bit_vector_benchmark.cc b/src/trace_processor/containers/bit_vector_benchmark.cc
index 55d3307..de4e85d 100644
--- a/src/trace_processor/containers/bit_vector_benchmark.cc
+++ b/src/trace_processor/containers/bit_vector_benchmark.cc
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <cstdint>
 #include <limits>
 #include <random>
+#include <vector>
 
 #include <benchmark/benchmark.h>
 
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/containers/bit_vector.h"
-#include "src/trace_processor/containers/bit_vector_iterators.h"
 
 namespace {
 
@@ -48,7 +50,7 @@
   }
 }
 
-void UpdateSetBitsArgs(benchmark::internal::Benchmark* b) {
+void UpdateSetBitsSelectBitsArgs(benchmark::internal::Benchmark* b) {
   if (IsBenchmarkFunctionalOnly()) {
     b->Args({64, 50, 50});
   } else {
@@ -199,6 +201,29 @@
 }
 BENCHMARK(BM_BitVectorCountSetBits)->Apply(BitVectorArgs);
 
+static void BM_BitVectorGetSetBitIndices(benchmark::State& state) {
+  static constexpr uint32_t kRandomSeed = 42;
+  std::minstd_rand0 rnd_engine(kRandomSeed);
+
+  auto size = static_cast<uint32_t>(state.range(0));
+  auto set_percentage = static_cast<uint32_t>(state.range(1));
+
+  BitVector bv;
+  for (uint32_t i = 0; i < size; ++i) {
+    bool value = rnd_engine() % 100 < set_percentage;
+    if (value) {
+      bv.AppendTrue();
+    } else {
+      bv.AppendFalse();
+    }
+  }
+
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(bv.GetSetBitIndices());
+  }
+}
+BENCHMARK(BM_BitVectorGetSetBitIndices)->Apply(BitVectorArgs);
+
 static void BM_BitVectorResize(benchmark::State& state) {
   static constexpr uint32_t kRandomSeed = 42;
   std::minstd_rand0 rnd_engine(kRandomSeed);
@@ -265,20 +290,50 @@
       picker_set_bit_count, benchmark::Counter::kIsIterationInvariantRate |
                                 benchmark::Counter::kInvert);
 }
-BENCHMARK(BM_BitVectorUpdateSetBits)->Apply(UpdateSetBitsArgs);
+BENCHMARK(BM_BitVectorUpdateSetBits)->Apply(UpdateSetBitsSelectBitsArgs);
 
-static void BM_BitVectorSetBitsIterator(benchmark::State& state) {
-  uint32_t size = static_cast<uint32_t>(state.range(0));
-  uint32_t set_percentage = static_cast<uint32_t>(state.range(1));
+static void BM_BitVectorSelectBits(benchmark::State& state) {
+  static constexpr uint32_t kRandomSeed = 42;
+  std::minstd_rand0 rnd_engine(kRandomSeed);
 
-  BitVector bv = BvWithSizeAndSetPercentage(size, set_percentage);
-  for (auto _ : state) {
-    for (auto it = bv.IterateSetBits(); it; it.Next()) {
-      benchmark::DoNotOptimize(it.index());
+  auto size = static_cast<uint32_t>(state.range(0));
+  auto set_percentage = static_cast<uint32_t>(state.range(1));
+  auto mask_set_percentage = static_cast<uint32_t>(state.range(2));
+
+  BitVector bv;
+  BitVector mask;
+  for (uint32_t i = 0; i < size; ++i) {
+    bool value = rnd_engine() % 100 < set_percentage;
+    if (value) {
+      bv.AppendTrue();
+    } else {
+      bv.AppendFalse();
+    }
+    bool mask_value = rnd_engine() % 100 < mask_set_percentage;
+    if (mask_value) {
+      mask.AppendTrue();
+    } else {
+      mask.AppendFalse();
     }
   }
+
+  uint32_t set_bit_count = bv.CountSetBits();
+  uint32_t mask_set_bit_count = mask.CountSetBits();
+
+  for (auto _ : state) {
+    BitVector copy = bv.Copy();
+    copy.SelectBits(mask);
+    benchmark::DoNotOptimize(copy);
+  }
+
+  state.counters["s/set bit"] = benchmark::Counter(
+      set_bit_count, benchmark::Counter::kIsIterationInvariantRate |
+                         benchmark::Counter::kInvert);
+  state.counters["s/mask bit"] = benchmark::Counter(
+      mask_set_bit_count, benchmark::Counter::kIsIterationInvariantRate |
+                              benchmark::Counter::kInvert);
 }
-BENCHMARK(BM_BitVectorSetBitsIterator)->Apply(BitVectorArgs);
+BENCHMARK(BM_BitVectorSelectBits)->Apply(UpdateSetBitsSelectBitsArgs);
 
 static void BM_BitVectorFromIndexVector(benchmark::State& state) {
   std::vector<int64_t> indices;
diff --git a/src/trace_processor/containers/bit_vector_iterators.cc b/src/trace_processor/containers/bit_vector_iterators.cc
index 076a129..271f0fb 100644
--- a/src/trace_processor/containers/bit_vector_iterators.cc
+++ b/src/trace_processor/containers/bit_vector_iterators.cc
@@ -16,49 +16,12 @@
 
 #include "src/trace_processor/containers/bit_vector_iterators.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace internal {
+namespace perfetto::trace_processor::internal {
 
 BaseIterator::BaseIterator(BitVector* bv)
     : size_(bv->size()), bv_(bv), block_(bv_->words_.data()) {}
 
-BaseIterator::~BaseIterator() {
-  if (size_ > 0) {
-    uint32_t block_idx = bv_->IndexToAddress(index_).block_idx;
-    uint32_t last_block_idx = bv_->BlockCount() - 1;
-
-    // If |index_| == |size_| and the last index was on a block boundary, we
-    // can end up one block past the end of the bitvector. Take the
-    // min of the block index and the last block
-    OnBlockChange(std::min(block_idx, last_block_idx), last_block_idx);
-  }
-}
-
-void BaseIterator::OnBlockChange(uint32_t old_block_idx,
-                                 uint32_t new_block_idx) {
-  if (set_bit_count_diff_ != 0) {
-    // If the count of set bits has changed, go through all the counts between
-    // the old and new blocks and modify them.
-    // We only need to go to new_block and not to the end of the bitvector as
-    // the blocks after new_block will either be updated in a future call to
-    // OnBlockChange or in the destructor.
-    for (uint32_t i = old_block_idx + 1; i <= new_block_idx; ++i) {
-      int32_t new_count =
-          static_cast<int32_t>(bv_->counts_[i]) + set_bit_count_diff_;
-      PERFETTO_DCHECK(new_count >= 0);
-
-      bv_->counts_[i] = static_cast<uint32_t>(new_count);
-    }
-  }
-
-  // Reset the changed flag and cache the new block.
-  is_block_changed_ = false;
-  block_ = bv_->BlockFromIndex(new_block_idx);
-}
-
-AllBitsIterator::AllBitsIterator(const BitVector* bv)
-    : BaseIterator(const_cast<BitVector*>(bv)) {}
+BaseIterator::~BaseIterator() = default;
 
 SetBitsIterator::SetBitsIterator(const BitVector* bv)
     : BaseIterator(const_cast<BitVector*>(bv)) {
@@ -121,6 +84,4 @@
   PERFETTO_DCHECK(set_bit_count_until_i == set_bit_count_);
 }
 
-}  // namespace internal
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::internal
diff --git a/src/trace_processor/containers/bit_vector_iterators.h b/src/trace_processor/containers/bit_vector_iterators.h
index e4ee878..32cd85b 100644
--- a/src/trace_processor/containers/bit_vector_iterators.h
+++ b/src/trace_processor/containers/bit_vector_iterators.h
@@ -17,6 +17,8 @@
 #ifndef SRC_TRACE_PROCESSOR_CONTAINERS_BIT_VECTOR_ITERATORS_H_
 #define SRC_TRACE_PROCESSOR_CONTAINERS_BIT_VECTOR_ITERATORS_H_
 
+#include <array>
+
 #include "src/trace_processor/containers/bit_vector.h"
 
 namespace perfetto {
@@ -30,32 +32,15 @@
 // block.
 class BaseIterator {
  public:
-  BaseIterator(BitVector* bv);
+  explicit BaseIterator(BitVector* bv);
   ~BaseIterator();
 
+  BaseIterator(const BaseIterator&) = delete;
+  BaseIterator& operator=(const BaseIterator&) = delete;
+
   BaseIterator(BaseIterator&&) noexcept = default;
   BaseIterator& operator=(BaseIterator&&) = default;
 
-  // Sets the current bit the iterator points to.
-  void Set() {
-    if (!IsSet()) {
-      block_.Set(block_offset());
-
-      is_block_changed_ = true;
-      ++set_bit_count_diff_;
-    }
-  }
-
-  // Clears the current bit the iterator points to.
-  void Clear() {
-    if (IsSet()) {
-      block_.Clear(block_offset());
-
-      is_block_changed_ = true;
-      --set_bit_count_diff_;
-    }
-  }
-
   // Returns whether the current bit the iterator points to is set.
   bool IsSet() { return BitVector::ConstBlock(block_).IsSet(block_offset()); }
 
@@ -87,22 +72,14 @@
     if (PERFETTO_LIKELY(old_block == new_block))
       return;
 
-    // Slow path: we have to change block so this will involve flushing the old
-    // block and counts (if necessary).
-    OnBlockChange(old_block, new_block);
+    block_ = bv_->BlockFromIndex(new_block);
   }
 
-  // Handles flushing count changes and caches a new block.
-  void OnBlockChange(uint32_t old_block, uint32_t new_block);
-
   uint32_t size() const { return size_; }
 
   const BitVector& bv() const { return *bv_; }
 
  private:
-  BaseIterator(const BaseIterator&) = delete;
-  BaseIterator& operator=(const BaseIterator&) = delete;
-
   BitVector::BlockOffset block_offset() const {
     uint16_t bit_idx_inside_block = index_ % BitVector::Block::kBits;
 
@@ -115,35 +92,10 @@
   uint32_t index_ = 0;
   uint32_t size_ = 0;
 
-  bool is_block_changed_ = false;
-  int32_t set_bit_count_diff_ = 0;
-
   BitVector* bv_;
   BitVector::Block block_{bv_->words_.data()};
 };
 
-// Iterator over all the bits in a bitvector.
-class AllBitsIterator : public BaseIterator {
- public:
-  AllBitsIterator(const BitVector*);
-
-  // Increments the iterator to point to the next bit.
-  void Next() { SetIndex(index() + 1); }
-
-  // Increments the iterator to skip the next |n| bits and point to the
-  // following one.
-  // Precondition: n >= 1 & index() + n <= size().
-  void Skip(uint32_t n) {
-    PERFETTO_DCHECK(n >= 1);
-    PERFETTO_DCHECK(index() + n <= size());
-
-    SetIndex(index() + n);
-  }
-
-  // Returns whether the iterator is valid.
-  operator bool() const { return index() < size(); }
-};
-
 // Iterator over all the set bits in a bitvector.
 //
 // This iterator works by first finding a batch of indices of set bits.
diff --git a/src/trace_processor/containers/bit_vector_unittest.cc b/src/trace_processor/containers/bit_vector_unittest.cc
index ee7452f..57fc0d0 100644
--- a/src/trace_processor/containers/bit_vector_unittest.cc
+++ b/src/trace_processor/containers/bit_vector_unittest.cc
@@ -17,16 +17,17 @@
 #include "src/trace_processor/containers/bit_vector.h"
 
 #include <bitset>
+#include <cstdint>
 #include <limits>
 #include <random>
+#include <utility>
+#include <vector>
 
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "protos/perfetto/trace_processor/serialization.pbzero.h"
-#include "src/trace_processor/containers/bit_vector_iterators.h"
 #include "test/gtest_and_gmock.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 TEST(BitVectorUnittest, CreateAllTrue) {
@@ -309,150 +310,101 @@
   }
 }
 
-TEST(BitVectorUnittest, IterateAllBitsConst) {
-  BitVector bv;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    if (i % 7 == 0 || i % 13 == 0) {
-      bv.AppendTrue();
-    } else {
-      bv.AppendFalse();
-    }
-  }
+TEST(BitVectorUnittest, SelectBitsSimple) {
+  BitVector bv = {true, false, true, false, true, true, true};
+  BitVector mask = {true, false, true, true, false, false, true};
+  bv.SelectBits(mask);
 
-  uint32_t i = 0;
-  for (auto it = bv.IterateAllBits(); it; it.Next(), ++i) {
-    ASSERT_EQ(it.IsSet(), i % 7 == 0 || i % 13 == 0);
-    ASSERT_EQ(it.index(), i);
-  }
+  ASSERT_EQ(bv.size(), 4u);
+  ASSERT_EQ(bv.IsSet(0), true);
+  ASSERT_EQ(bv.IsSet(1), true);
+  ASSERT_EQ(bv.IsSet(2), false);
+  ASSERT_EQ(bv.IsSet(3), true);
+  ASSERT_EQ(bv.CountSetBits(), 3u);
 }
 
-TEST(BitVectorUnittest, IterateAllBitsSet) {
-  BitVector bv;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    if (i % 7 == 0 || i % 13 == 0) {
-      bv.AppendTrue();
-    } else {
-      bv.AppendFalse();
-    }
-  }
+TEST(BitVectorUnittest, SelectBitsSmallerMain) {
+  BitVector bv = {true, false, true, false};
+  BitVector mask = {true, false, true, true, false, false, true};
+  bv.SelectBits(mask);
 
-  // Unset every 15th bit.
-  for (auto it = bv.IterateAllBits(); it; it.Next()) {
-    if (it.index() % 15 == 0) {
-      it.Set();
-    }
-  }
-
-  // Go through the iterator manually and check it has updated
-  // to not have every 15th bit set.
-  uint32_t count = 0;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    bool is_set = i % 15 == 0 || i % 7 == 0 || i % 13 == 0;
-
-    ASSERT_EQ(bv.IsSet(i), is_set);
-    ASSERT_EQ(bv.CountSetBits(i), count);
-
-    if (is_set) {
-      ASSERT_EQ(bv.IndexOfNthSet(count++), i);
-    }
-  }
+  ASSERT_EQ(bv.size(), 3u);
+  ASSERT_EQ(bv.IsSet(0), true);
+  ASSERT_EQ(bv.IsSet(1), true);
+  ASSERT_EQ(bv.IsSet(2), false);
+  ASSERT_EQ(bv.CountSetBits(), 2u);
 }
 
-TEST(BitVectorUnittest, IterateAllBitsClear) {
-  BitVector bv;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    if (i % 7 == 0 || i % 13 == 0) {
-      bv.AppendTrue();
-    } else {
-      bv.AppendFalse();
-    }
+TEST(BitVectorUnittest, SelectBitsLarge) {
+  BitVector bv = BitVector::RangeForTesting(
+      0, 813, [](uint32_t idx) { return idx % 7 == 0; });
+  BitVector mask = BitVector::RangeForTesting(
+      0, 813, [](uint32_t idx) { return idx % 3 == 0; });
+  bv.SelectBits(mask);
+
+  BitVector expected = BitVector::RangeForTesting(
+      0, 271u, [](uint32_t idx) { return (idx * 3) % 7 == 0; });
+
+  ASSERT_EQ(bv.size(), 271u);
+  for (uint32_t i = 0; i < expected.size(); ++i) {
+    ASSERT_EQ(expected.IsSet(i), bv.IsSet(i)) << "Index " << i;
+    ASSERT_EQ(expected.CountSetBits(i), bv.CountSetBits(i)) << "Index " << i;
   }
-
-  // Unset every 15th bit.
-  for (auto it = bv.IterateAllBits(); it; it.Next()) {
-    if (it.index() % 15 == 0) {
-      it.Clear();
-    }
-  }
-
-  // Go through the iterator manually and check it has updated
-  // to not have every 15th bit set.
-  uint32_t count = 0;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    bool is_set = i % 15 != 0 && (i % 7 == 0 || i % 13 == 0);
-
-    ASSERT_EQ(bv.IsSet(i), is_set);
-    ASSERT_EQ(bv.CountSetBits(i), count);
-
-    if (is_set) {
-      ASSERT_EQ(bv.IndexOfNthSet(count++), i);
-    }
-  }
+  ASSERT_EQ(expected.CountSetBits(), bv.CountSetBits());
 }
 
-TEST(BitVectorUnittest, IterateSetBitsConst) {
-  BitVector bv;
-  std::vector<uint32_t> set_indices;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    if (i % 7 == 0 || i % 13 == 0) {
-      bv.AppendTrue();
-      set_indices.emplace_back(i);
-    } else {
-      bv.AppendFalse();
-    }
-  }
+TEST(BitVectorUnittest, SelectBitsLargeSmallerMain) {
+  BitVector bv = BitVector::RangeForTesting(
+      0, 279, [](uint32_t idx) { return idx % 7 == 0; });
+  BitVector mask = BitVector::RangeForTesting(
+      0, 813, [](uint32_t idx) { return idx % 3 == 0; });
+  bv.SelectBits(mask);
 
-  uint32_t i = 0;
-  for (auto it = bv.IterateSetBits(); it; it.Next(), ++i) {
-    ASSERT_EQ(it.IsSet(), true);
-    ASSERT_EQ(it.index(), set_indices[i]);
+  BitVector expected = BitVector::RangeForTesting(
+      0, 93, [](uint32_t idx) { return (idx * 3) % 7 == 0; });
+
+  ASSERT_EQ(bv.size(), 93u);
+  for (uint32_t i = 0; i < expected.size(); ++i) {
+    ASSERT_EQ(expected.IsSet(i), bv.IsSet(i)) << "Index " << i;
+    ASSERT_EQ(expected.CountSetBits(i), bv.CountSetBits(i)) << "Index " << i;
   }
-  ASSERT_EQ(i, set_indices.size());
+  ASSERT_EQ(expected.CountSetBits(), bv.CountSetBits());
 }
 
-TEST(BitVectorUnittest, IterateSetBitsClear) {
-  BitVector bv;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    if (i % 7 == 0 || i % 13 == 0) {
-      bv.AppendTrue();
-    } else {
-      bv.AppendFalse();
-    }
+TEST(BitVectorUnittest, SelectBitsDense) {
+  BitVector bv =
+      BitVector::RangeForTesting(0, 279, [](uint32_t) { return true; });
+  BitVector mask =
+      BitVector::RangeForTesting(0, 279, [](uint32_t idx) { return idx < 80; });
+  bv.SelectBits(mask);
+
+  BitVector expected =
+      BitVector::RangeForTesting(0, 80, [](uint32_t) { return true; });
+
+  ASSERT_EQ(bv.size(), 80u);
+  for (uint32_t i = 0; i < expected.size(); ++i) {
+    ASSERT_EQ(expected.IsSet(i), bv.IsSet(i)) << "Index " << i;
+    ASSERT_EQ(expected.CountSetBits(i), bv.CountSetBits(i)) << "Index " << i;
   }
-
-  for (auto it = bv.IterateSetBits(); it; it.Next()) {
-    if (it.index() % 15 == 0) {
-      it.Clear();
-    }
-  }
-
-  // Go through the iterator manually and check it has updated
-  // to not have every 15th bit set.
-  uint32_t count = 0;
-  for (uint32_t i = 0; i < 12345; ++i) {
-    bool is_set = i % 15 != 0 && (i % 7 == 0 || i % 13 == 0);
-
-    ASSERT_EQ(bv.IsSet(i), is_set);
-    ASSERT_EQ(bv.CountSetBits(i), count);
-
-    if (is_set) {
-      ASSERT_EQ(bv.IndexOfNthSet(count++), i);
-    }
-  }
+  ASSERT_EQ(expected.CountSetBits(), bv.CountSetBits());
 }
 
-TEST(BitVectorUnittest, IterateSetBitsStartsCorrectly) {
-  BitVector bv;
-  bv.AppendFalse();
-  bv.AppendTrue();
+TEST(BitVectorUnittest, SelectBitsEnd) {
+  BitVector bv = BitVector::RangeForTesting(
+      0, 279, [](uint32_t idx) { return idx % 7 == 0; });
+  BitVector mask = BitVector::RangeForTesting(
+      0, 813, [](uint32_t idx) { return idx % 3 == 0; });
+  bv.SelectBits(mask);
 
-  auto it = bv.IterateSetBits();
-  ASSERT_TRUE(it);
-  ASSERT_EQ(it.index(), 1u);
-  ASSERT_TRUE(it.IsSet());
+  BitVector expected = BitVector::RangeForTesting(
+      0, 93, [](uint32_t idx) { return (idx * 3) % 7 == 0; });
 
-  it.Next();
-  ASSERT_FALSE(it);
+  ASSERT_EQ(bv.size(), 93u);
+  for (uint32_t i = 0; i < expected.size(); ++i) {
+    ASSERT_EQ(expected.IsSet(i), bv.IsSet(i)) << "Index " << i;
+    ASSERT_EQ(expected.CountSetBits(i), bv.CountSetBits(i)) << "Index " << i;
+  }
+  ASSERT_EQ(expected.CountSetBits(), bv.CountSetBits());
 }
 
 TEST(BitVectorUnittest, IntersectRange) {
@@ -515,6 +467,18 @@
   ASSERT_EQ(intersected.CountSetBits(), 217u);
 }
 
+TEST(BitVectorUnittest, IntersectRangeAppendFalse) {
+  BitVector bv(70u, true);
+  BitVector out = bv.IntersectRange(10, 12u);
+  out.Resize(70u);
+
+  ASSERT_TRUE(out.IsSet(10u));
+  ASSERT_TRUE(out.IsSet(11u));
+  ASSERT_FALSE(out.IsSet(12u));
+  ASSERT_FALSE(out.IsSet(60u));
+  ASSERT_FALSE(out.IsSet(69u));
+}
+
 TEST(BitVectorUnittest, Range) {
   BitVector bv =
       BitVector::RangeForTesting(1, 9, [](uint32_t t) { return t % 3 == 0; });
@@ -698,6 +662,14 @@
   EXPECT_EQ(bv.CountSetBits(), 820u);
 }
 
+TEST(BitVectorUnittest, NotAppendAfter) {
+  BitVector bv(30);
+  bv.Not();
+  bv.AppendFalse();
+
+  ASSERT_FALSE(bv.IsSet(30));
+}
+
 TEST(BitVectorUnittest, Or) {
   BitVector bv{1, 1, 0, 0};
   BitVector bv_second{1, 0, 1, 0};
@@ -740,31 +712,11 @@
     if (res)
       int_vec.emplace_back(i);
   }
+}
 
-  auto all_it = bv.IterateAllBits();
-  for (uint32_t i = 0; i < kCount; ++i) {
-    uint32_t count = static_cast<uint32_t>(std::count(
-        bool_vec.begin(), bool_vec.begin() + static_cast<int32_t>(i), true));
-    ASSERT_EQ(bv.IsSet(i), bool_vec[i]);
-    ASSERT_EQ(bv.CountSetBits(i), count);
-
-    ASSERT_TRUE(all_it);
-    ASSERT_EQ(all_it.IsSet(), bool_vec[i]);
-    ASSERT_EQ(all_it.index(), i);
-    all_it.Next();
-  }
-  ASSERT_FALSE(all_it);
-
-  auto set_it = bv.IterateSetBits();
-  for (uint32_t i = 0; i < int_vec.size(); ++i) {
-    ASSERT_EQ(bv.IndexOfNthSet(i), int_vec[i]);
-
-    ASSERT_TRUE(set_it);
-    ASSERT_EQ(set_it.IsSet(), true);
-    ASSERT_EQ(set_it.index(), int_vec[i]);
-    set_it.Next();
-  }
-  ASSERT_FALSE(set_it);
+TEST(BitVectorUnittest, GetSetBitIndices) {
+  BitVector bv = {true, false, true, false, true, true, false, false};
+  ASSERT_THAT(bv.GetSetBitIndices(), testing::ElementsAre(0u, 2u, 4u, 5u));
 }
 
 TEST(BitVectorUnittest, SerializeSimple) {
@@ -800,5 +752,4 @@
 }
 
 }  // namespace
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/containers/row_map.cc b/src/trace_processor/containers/row_map.cc
index f7925ae..3ae6ee7 100644
--- a/src/trace_processor/containers/row_map.cc
+++ b/src/trace_processor/containers/row_map.cc
@@ -15,8 +15,16 @@
  */
 
 #include "src/trace_processor/containers/row_map.h"
-#include <unordered_set>
 
+#include <algorithm>
+#include <cstdint>
+#include <unordered_set>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/containers/bit_vector.h"
 #include "src/trace_processor/containers/row_map_algorithms.h"
 
 namespace perfetto {
@@ -69,20 +77,16 @@
 
 RowMap Select(const BitVector& bv, Range selector) {
   PERFETTO_DCHECK(selector.end <= bv.CountSetBits());
-
+  if (selector.empty()) {
+    return {};
+  }
   // If we're simply selecting every element in the bitvector, just
   // return a copy of the BitVector without iterating.
-  BitVector ret = bv.Copy();
   if (selector.start == 0 && selector.end == bv.CountSetBits()) {
-    return RowMap(std::move(ret));
+    return RowMap(bv.Copy());
   }
-
-  for (auto it = ret.IterateSetBits(); it; it.Next()) {
-    auto set_idx = it.ordinal();
-    if (set_idx < selector.start || set_idx >= selector.end)
-      it.Clear();
-  }
-  return RowMap(std::move(ret));
+  return RowMap(bv.IntersectRange(bv.IndexOfNthSet(selector.start),
+                                  bv.IndexOfNthSet(selector.end - 1) + 1));
 }
 
 RowMap Select(const BitVector& bv, const BitVector& selector) {
@@ -230,26 +234,26 @@
 RowMap::RowMap(IndexVector vec) : data_(vec) {}
 
 RowMap RowMap::Copy() const {
-  if (auto* range = std::get_if<Range>(&data_)) {
+  if (const auto* range = std::get_if<Range>(&data_)) {
     return RowMap(*range);
   }
-  if (auto* bv = std::get_if<BitVector>(&data_)) {
+  if (const auto* bv = std::get_if<BitVector>(&data_)) {
     return RowMap(bv->Copy());
   }
-  if (auto* vec = std::get_if<IndexVector>(&data_)) {
+  if (const auto* vec = std::get_if<IndexVector>(&data_)) {
     return RowMap(*vec);
   }
   NoVariantMatched();
 }
 
 OutputIndex RowMap::Max() const {
-  if (auto* range = std::get_if<Range>(&data_)) {
+  if (const auto* range = std::get_if<Range>(&data_)) {
     return range->end;
   }
-  if (auto* bv = std::get_if<BitVector>(&data_)) {
+  if (const auto* bv = std::get_if<BitVector>(&data_)) {
     return bv->size();
   }
-  if (auto* vec = std::get_if<IndexVector>(&data_)) {
+  if (const auto* vec = std::get_if<IndexVector>(&data_)) {
     return vec->empty() ? 0 : *std::max_element(vec->begin(), vec->end()) + 1;
   }
   NoVariantMatched();
@@ -272,14 +276,15 @@
 }
 
 RowMap::Iterator::Iterator(const RowMap* rm) : rm_(rm) {
-  if (auto* range = std::get_if<Range>(&rm_->data_)) {
+  if (const auto* range = std::get_if<Range>(&rm_->data_)) {
     ordinal_ = range->start;
     return;
   }
-  if (auto* bv = std::get_if<BitVector>(&rm_->data_)) {
-    set_bits_it_.reset(new BitVector::SetBitsIterator(bv->IterateSetBits()));
+  if (const auto* bv = std::get_if<BitVector>(&rm_->data_)) {
+    results_ = bv->GetSetBitIndices();
     return;
   }
 }
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/containers/row_map.h b/src/trace_processor/containers/row_map.h
index b8f1d3d..88fe7a1 100644
--- a/src/trace_processor/containers/row_map.h
+++ b/src/trace_processor/containers/row_map.h
@@ -17,17 +17,18 @@
 #ifndef SRC_TRACE_PROCESSOR_CONTAINERS_ROW_MAP_H_
 #define SRC_TRACE_PROCESSOR_CONTAINERS_ROW_MAP_H_
 
-#include <stdint.h>
-
-#include <memory>
+#include <algorithm>
+#include <cstdint>
+#include <iterator>
 #include <numeric>
 #include <optional>
+#include <utility>
 #include <variant>
 #include <vector>
 
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "src/trace_processor/containers/bit_vector.h"
-#include "src/trace_processor/containers/bit_vector_iterators.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -82,16 +83,16 @@
 
   struct Range {
     Range(OutputIndex start_index, OutputIndex end_index)
-        : start(start_index), end(end_index) {}
+        : start(start_index), end(end_index) {
+      PERFETTO_DCHECK(start_index <= end_index);
+    }
     Range() : start(0), end(0) {}
 
-    OutputIndex start = 0;  // This is an inclusive index.
-    OutputIndex end = 0;    // This is an exclusive index.
+    OutputIndex start;  // This is an inclusive index.
+    OutputIndex end;    // This is an exclusive index.
 
-    uint32_t size() const {
-      PERFETTO_DCHECK(end >= start);
-      return end - start;
-    }
+    bool empty() const { return size() == 0; }
+    uint32_t size() const { return end - start; }
     inline bool Contains(uint32_t val) const {
       return val >= start && val < end;
     }
@@ -106,29 +107,24 @@
    public:
     explicit Iterator(const RowMap* rm);
 
+    Iterator(const Iterator&) = delete;
+    Iterator& operator=(const Iterator&) = delete;
+
     Iterator(Iterator&&) noexcept = default;
     Iterator& operator=(Iterator&&) = default;
 
     // Forwards the iterator to the next row of the RowMap.
-    void Next() {
-      if (std::get_if<Range>(&rm_->data_)) {
-        ++ordinal_;
-      } else if (std::get_if<BitVector>(&rm_->data_)) {
-        set_bits_it_->Next();
-      } else if (std::get_if<IndexVector>(&rm_->data_)) {
-        ++ordinal_;
-      }
-    }
+    void Next() { ++ordinal_; }
 
     // Returns if the iterator is still valid.
-    operator bool() const {
-      if (auto* range = std::get_if<Range>(&rm_->data_)) {
+    explicit operator bool() const {
+      if (const auto* range = std::get_if<Range>(&rm_->data_)) {
         return ordinal_ < range->end;
       }
       if (std::get_if<BitVector>(&rm_->data_)) {
-        return bool(*set_bits_it_);
+        return ordinal_ < results_.size();
       }
-      if (auto* vec = std::get_if<IndexVector>(&rm_->data_)) {
+      if (const auto* vec = std::get_if<IndexVector>(&rm_->data_)) {
         return ordinal_ < vec->size();
       }
       PERFETTO_FATAL("Didn't match any variant type.");
@@ -140,9 +136,9 @@
         return ordinal_;
       }
       if (std::get_if<BitVector>(&rm_->data_)) {
-        return set_bits_it_->index();
+        return results_[ordinal_];
       }
-      if (auto* vec = std::get_if<IndexVector>(&rm_->data_)) {
+      if (const auto* vec = std::get_if<IndexVector>(&rm_->data_)) {
         return (*vec)[ordinal_];
       }
       PERFETTO_FATAL("Didn't match any variant type.");
@@ -150,26 +146,21 @@
 
     // Returns the row of the index the iterator points to.
     InputRow row() const {
-      if (auto* range = std::get_if<Range>(&rm_->data_)) {
+      if (const auto* range = std::get_if<Range>(&rm_->data_)) {
         return ordinal_ - range->start;
       }
-      if (std::get_if<BitVector>(&rm_->data_)) {
-        return set_bits_it_->ordinal();
-      }
-      if (std::get_if<IndexVector>(&rm_->data_)) {
+      if (std::get_if<BitVector>(&rm_->data_) ||
+          std::get_if<IndexVector>(&rm_->data_)) {
         return ordinal_;
       }
       PERFETTO_FATAL("Didn't match any variant type.");
     }
 
    private:
-    Iterator(const Iterator&) = delete;
-    Iterator& operator=(const Iterator&) = delete;
-
     // Ordinal will not be used for BitVector based RowMap.
     uint32_t ordinal_ = 0;
-    // Not nullptr for BitVector based RowMap.
-    std::unique_ptr<BitVector::SetBitsIterator> set_bits_it_;
+    // Not empty for BitVector based RowMap.
+    std::vector<uint32_t> results_;
 
     const RowMap* rm_ = nullptr;
   };
@@ -209,13 +200,13 @@
   // Returns the size of the RowMap; that is the number of indices in the
   // RowMap.
   uint32_t size() const {
-    if (auto* range = std::get_if<Range>(&data_)) {
+    if (const auto* range = std::get_if<Range>(&data_)) {
       return range->size();
     }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
+    if (const auto* bv = std::get_if<BitVector>(&data_)) {
       return bv->CountSetBits();
     }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
+    if (const auto* vec = std::get_if<IndexVector>(&data_)) {
       return static_cast<uint32_t>(vec->size());
     }
     NoVariantMatched();
@@ -226,13 +217,13 @@
 
   // Returns the index at the given |row|.
   OutputIndex Get(InputRow row) const {
-    if (auto* range = std::get_if<Range>(&data_)) {
+    if (const auto* range = std::get_if<Range>(&data_)) {
       return GetRange(*range, row);
     }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
+    if (const auto* bv = std::get_if<BitVector>(&data_)) {
       return GetBitVector(*bv, row);
     }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
+    if (const auto* vec = std::get_if<IndexVector>(&data_)) {
       return GetIndexVector(*vec, row);
     }
     NoVariantMatched();
@@ -240,19 +231,15 @@
 
   // Returns the vector of all indices in the RowMap.
   std::vector<OutputIndex> GetAllIndices() const {
-    if (auto* range = std::get_if<Range>(&data_)) {
+    if (const auto* range = std::get_if<Range>(&data_)) {
       std::vector<uint32_t> res(range->size());
       std::iota(res.begin(), res.end(), range->start);
       return res;
     }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
-      std::vector<uint32_t> res;
-      for (auto it = bv->IterateSetBits(); it; it.Next()) {
-        res.push_back(it.index());
-      }
-      return res;
+    if (const auto* bv = std::get_if<BitVector>(&data_)) {
+      return bv->GetSetBitIndices();
     }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
+    if (const auto* vec = std::get_if<IndexVector>(&data_)) {
       return *vec;
     }
     NoVariantMatched();
@@ -263,13 +250,13 @@
 
   // Returns whether the RowMap contains the given index.
   bool Contains(OutputIndex index) const {
-    if (auto* range = std::get_if<Range>(&data_)) {
+    if (const auto* range = std::get_if<Range>(&data_)) {
       return index >= range->start && index < range->end;
     }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
+    if (const auto* bv = std::get_if<BitVector>(&data_)) {
       return index < bv->size() && bv->IsSet(index);
     }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
+    if (const auto* vec = std::get_if<IndexVector>(&data_)) {
       return std::find(vec->begin(), vec->end(), index) != vec->end();
     }
     NoVariantMatched();
@@ -277,17 +264,17 @@
 
   // Returns the first row of the given |index| in the RowMap.
   std::optional<InputRow> RowOf(OutputIndex index) const {
-    if (auto* range = std::get_if<Range>(&data_)) {
+    if (const auto* range = std::get_if<Range>(&data_)) {
       if (index < range->start || index >= range->end)
         return std::nullopt;
       return index - range->start;
     }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
+    if (const auto* bv = std::get_if<BitVector>(&data_)) {
       return index < bv->size() && bv->IsSet(index)
                  ? std::make_optional(bv->CountSetBits(index))
                  : std::nullopt;
     }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
+    if (const auto* vec = std::get_if<IndexVector>(&data_)) {
       auto it = std::find(vec->begin(), vec->end(), index);
       return it != vec->end() ? std::make_optional(static_cast<InputRow>(
                                     std::distance(vec->begin(), it)))
@@ -362,7 +349,7 @@
 
     // If the selector is empty, just return an empty RowMap.
     if (size == 0u)
-      return RowMap();
+      return {};
 
     // If the selector is just picking a single row, just return that row
     // without any additional overhead.
@@ -398,69 +385,16 @@
   // Clears this RowMap by resetting it to a newly constructed state.
   void Clear() { *this = RowMap(); }
 
-  template <typename Comparator = bool(uint32_t, uint32_t)>
-  void StableSort(IndexVector* out, Comparator c) const {
-    if (auto* range = std::get_if<Range>(&data_)) {
-      std::stable_sort(out->begin(), out->end(),
-                       [range, c](uint32_t a, uint32_t b) {
-                         return c(GetRange(*range, a), GetRange(*range, b));
-                       });
-      return;
-    }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
-      std::stable_sort(out->begin(), out->end(),
-                       [&bv, c](uint32_t a, uint32_t b) {
-                         return c(GetBitVector(*bv, a), GetBitVector(*bv, b));
-                       });
-      return;
-    }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
-      std::stable_sort(
-          out->begin(), out->end(), [vec, c](uint32_t a, uint32_t b) {
-            return c(GetIndexVector(*vec, a), GetIndexVector(*vec, b));
-          });
-      return;
-    }
-    NoVariantMatched();
-  }
-
-  // Filters the indices in |out| by keeping those which meet |p|.
-  template <typename Predicate = bool(OutputIndex)>
-  void Filter(Predicate p) {
-    if (auto* range = std::get_if<Range>(&data_)) {
-      data_ = FilterRange(p, *range);
-      return;
-    }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
-      for (auto it = bv->IterateSetBits(); it; it.Next()) {
-        if (!p(it.index()))
-          it.Clear();
-      }
-      return;
-    }
-    if (auto* vec = std::get_if<IndexVector>(&data_)) {
-      auto ret = std::remove_if(vec->begin(), vec->end(),
-                                [p](uint32_t i) { return !p(i); });
-      vec->erase(ret, vec->end());
-      return;
-    }
-    NoVariantMatched();
-  }
-
   // Converts this RowMap to an index vector in the most efficient way
   // possible.
-  std::vector<uint32_t> TakeAsIndexVector() const&& {
-    if (auto* range = std::get_if<Range>(&data_)) {
+  std::vector<uint32_t> TakeAsIndexVector() && {
+    if (const auto* range = std::get_if<Range>(&data_)) {
       std::vector<uint32_t> rm(range->size());
       std::iota(rm.begin(), rm.end(), range->start);
       return rm;
     }
-    if (auto* bv = std::get_if<BitVector>(&data_)) {
-      std::vector<uint32_t> rm(bv->CountSetBits());
-      for (auto it = bv->IterateSetBits(); it; it.Next()) {
-        rm[it.ordinal()] = it.index();
-      }
-      return rm;
+    if (const auto* bv = std::get_if<BitVector>(&data_)) {
+      return bv->GetSetBitIndices();
     }
     if (auto* vec = std::get_if<IndexVector>(&data_)) {
       return std::move(*vec);
@@ -530,7 +464,7 @@
     bv.Set(row);
   }
 
-  PERFETTO_NORETURN void NoVariantMatched() const {
+  PERFETTO_NORETURN static void NoVariantMatched() {
     PERFETTO_FATAL("Didn't match any variant type.");
   }
 
diff --git a/src/trace_processor/containers/row_map_algorithms.h b/src/trace_processor/containers/row_map_algorithms.h
index 6b79141..5398fa6 100644
--- a/src/trace_processor/containers/row_map_algorithms.h
+++ b/src/trace_processor/containers/row_map_algorithms.h
@@ -51,11 +51,7 @@
 inline std::vector<uint32_t> SelectBvWithIvByConvertToIv(
     const BitVector& bv,
     const std::vector<uint32_t>& selector) {
-  std::vector<uint32_t> bv_conv(bv.CountSetBits());
-  for (auto it = bv.IterateSetBits(); it; it.Next()) {
-    bv_conv[it.ordinal()] = it.index();
-  }
-  return SelectIvWithIv(bv_conv, selector);
+  return SelectIvWithIv(bv.GetSetBitIndices(), selector);
 }
 
 // Returns a vector containing elements from |bv| by selecting indices from
diff --git a/src/trace_processor/db/column/data_layer.h b/src/trace_processor/db/column/data_layer.h
index 8f34845..f43999c 100644
--- a/src/trace_processor/db/column/data_layer.h
+++ b/src/trace_processor/db/column/data_layer.h
@@ -130,9 +130,9 @@
   //    to positions in the storage.
   //
   // Notes for implementors:
-  //  * Implementations should ensure that the return value *only* includes
-  //    positions in |range| as callers will expect this to be true and can
-  //    optimize based on this.
+  //  * Implementations should ensure that the return value is empty or *only*
+  //    includes positions in |range|. Callers are free to assume this and can
+  //    optimize based on it.
   //  * Implementations should ensure that, if they return a BitVector, it is
   //    precisely of size |range.end|.
   PERFETTO_ALWAYS_INLINE RangeOrBitVector Search(FilterOp op,
diff --git a/src/trace_processor/db/column/dense_null_overlay.cc b/src/trace_processor/db/column/dense_null_overlay.cc
index d9a7fb1..970eb0f 100644
--- a/src/trace_processor/db/column/dense_null_overlay.cc
+++ b/src/trace_processor/db/column/dense_null_overlay.cc
@@ -109,8 +109,8 @@
     // |non_null_| which matches the range. Then, resize to |in.end| as this
     // is mandated by the API contract of |Storage::Search|.
     Range inner_range = std::move(inner_res).TakeIfRange();
-    PERFETTO_DCHECK(inner_range.end <= in.end);
-    PERFETTO_DCHECK(inner_range.start >= in.start);
+    PERFETTO_DCHECK(inner_range.empty() || inner_range.end <= in.end);
+    PERFETTO_DCHECK(inner_range.empty() || inner_range.start >= in.start);
     res = non_null_->IntersectRange(inner_range.start, inner_range.end);
     res.Resize(in.end, false);
   } else {
diff --git a/src/trace_processor/db/column/id_storage.cc b/src/trace_processor/db/column/id_storage.cc
index 6fdb800..df662af 100644
--- a/src/trace_processor/db/column/id_storage.cc
+++ b/src/trace_processor/db/column/id_storage.cc
@@ -327,13 +327,13 @@
     case FilterOp::kEq:
       return {val, val + (range.start <= val && val < range.end)};
     case FilterOp::kLe:
-      return {range.start, std::min(val + 1, range.end)};
+      return {range.start, std::clamp(val + 1, range.start, range.end)};
     case FilterOp::kLt:
-      return {range.start, std::min(val, range.end)};
+      return {range.start, std::clamp(val, range.start, range.end)};
     case FilterOp::kGe:
-      return {std::max(val, range.start), range.end};
+      return {std::clamp(val, range.start, range.end), range.end};
     case FilterOp::kGt:
-      return {std::max(val + 1, range.start), range.end};
+      return {std::clamp(val + 1, range.start, range.end), range.end};
     case FilterOp::kIsNotNull:
     case FilterOp::kNe:
     case FilterOp::kIsNull:
diff --git a/src/trace_processor/db/column/null_overlay.cc b/src/trace_processor/db/column/null_overlay.cc
index 7984656..8634eb6 100644
--- a/src/trace_processor/db/column/null_overlay.cc
+++ b/src/trace_processor/db/column/null_overlay.cc
@@ -48,7 +48,7 @@
   BitVector res;
   if (storage_result.IsRange()) {
     Range range = std::move(storage_result).TakeIfRange();
-    if (range.size() > 0) {
+    if (!range.empty()) {
       res = non_null.IntersectRange(non_null.IndexOfNthSet(range.start),
                                     non_null.IndexOfNthSet(range.end - 1) + 1);
 
diff --git a/src/trace_processor/db/column/numeric_storage.cc b/src/trace_processor/db/column/numeric_storage.cc
index 529e93d..2c16edd 100644
--- a/src/trace_processor/db/column/numeric_storage.cc
+++ b/src/trace_processor/db/column/numeric_storage.cc
@@ -22,11 +22,11 @@
 #include <cstdint>
 #include <functional>
 #include <limits>
-#include <memory>
 #include <optional>
 #include <string>
 #include <utility>
 #include <variant>
+#include <vector>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/public/compiler.h"
diff --git a/src/trace_processor/db/column/range_overlay.cc b/src/trace_processor/db/column/range_overlay.cc
index 0b8275b..338c989 100644
--- a/src/trace_processor/db/column/range_overlay.cc
+++ b/src/trace_processor/db/column/range_overlay.cc
@@ -65,6 +65,9 @@
   auto inner_res = inner_->SearchValidated(op, sql_val, inner_search_range);
   if (inner_res.IsRange()) {
     Range inner_res_range = std::move(inner_res).TakeIfRange();
+    if (inner_res_range.empty()) {
+      return RangeOrBitVector(Range());
+    }
     return RangeOrBitVector(Range(inner_res_range.start - range_->start,
                                   inner_res_range.end - range_->start));
   }
diff --git a/src/trace_processor/db/column/selector_overlay.cc b/src/trace_processor/db/column/selector_overlay.cc
index ff1e463..9ebbb71 100644
--- a/src/trace_processor/db/column/selector_overlay.cc
+++ b/src/trace_processor/db/column/selector_overlay.cc
@@ -65,6 +65,9 @@
       inner_->SearchValidated(op, sql_val, Range(start_idx, end_idx));
   if (storage_result.IsRange()) {
     Range storage_range = std::move(storage_result).TakeIfRange();
+    if (storage_range.empty()) {
+      return RangeOrBitVector(Range());
+    }
     uint32_t out_start = selector_->CountSetBits(storage_range.start);
     uint32_t out_end = selector_->CountSetBits(storage_range.end);
     return RangeOrBitVector(Range(out_start, out_end));
@@ -72,15 +75,12 @@
 
   BitVector storage_bitvector = std::move(storage_result).TakeIfBitVector();
   PERFETTO_DCHECK(storage_bitvector.size() <= selector_->size());
-
-  // TODO(b/283763282): implement ParallelExtractBits to optimize this
-  // operation.
-  BitVector::Builder res(in.end);
-  for (auto it = selector_->IterateSetBits();
-       it && it.index() < storage_bitvector.size(); it.Next()) {
-    res.Append(storage_bitvector.IsSet(it.index()));
+  storage_bitvector.SelectBits(*selector_);
+  if (storage_bitvector.size() == 0) {
+    return RangeOrBitVector(std::move(storage_bitvector));
   }
-  return RangeOrBitVector(std::move(res).Build());
+  PERFETTO_DCHECK(storage_bitvector.size() == in.end);
+  return RangeOrBitVector(std::move(storage_bitvector));
 }
 
 RangeOrBitVector SelectorOverlay::ChainImpl::IndexSearchValidated(
diff --git a/src/trace_processor/db/column_storage_overlay.h b/src/trace_processor/db/column_storage_overlay.h
index c13c095..3e68687 100644
--- a/src/trace_processor/db/column_storage_overlay.h
+++ b/src/trace_processor/db/column_storage_overlay.h
@@ -20,6 +20,7 @@
 #include <stdint.h>
 
 #include <optional>
+#include <utility>
 #include <vector>
 
 #include "src/trace_processor/containers/bit_vector.h"
@@ -41,7 +42,7 @@
   // Allows efficient iteration over the rows of a ColumnStorageOverlay.
   class Iterator {
    public:
-    Iterator(RowMap::Iterator it) : it_(std::move(it)) {}
+    explicit Iterator(RowMap::Iterator it) : it_(std::move(it)) {}
 
     Iterator(Iterator&&) noexcept = default;
     Iterator& operator=(Iterator&&) = default;
@@ -50,7 +51,7 @@
     void Next() { return it_.Next(); }
 
     // Returns if the iterator is still valid.
-    operator bool() const { return it_; }
+    explicit operator bool() const { return bool(it_); }
 
     // Returns the index pointed to by this iterator.
     OutputIndex index() const { return it_.index(); }
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index dd1e8a9..8502e04 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -103,22 +103,27 @@
   return base::SplitString(table_csv, "\n");
 }
 
+StringPool::Id StripAndIntern(StringPool& pool, const std::string& data) {
+  std::string res = base::StripSuffix(base::StripPrefix(data, "\""), "\"");
+  return pool.InternString(base::StringView(res));
+}
+
 SliceTable::Row GetSliceTableRow(const std::string& string_row,
                                  StringPool& pool) {
   std::vector<std::string> row_vec = SplitCSVLine(string_row);
   SliceTable::Row row;
-  PERFETTO_CHECK(row_vec.size() >= 12);
+  PERFETTO_CHECK(row_vec.size() >= 14);
   row.ts = *base::StringToInt64(row_vec[2]);
   row.dur = *base::StringToInt64(row_vec[3]);
   row.track_id = ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
-  row.category = pool.InternString(base::StringView(row_vec[5]));
-  row.name = pool.InternString(base::StringView(row_vec[6]));
+  row.category = StripAndIntern(pool, row_vec[5]);
+  row.name = StripAndIntern(pool, row_vec[6]);
   row.depth = *base::StringToUInt32(row_vec[7]);
   row.stack_id = *base::StringToInt32(row_vec[8]);
   row.parent_stack_id = *base::StringToInt32(row_vec[9]);
-  row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
+  row.parent_id = base::StringToUInt32(row_vec[10]).has_value()
                       ? std::make_optional<SliceTable::Id>(
-                            *base::StringToUInt32(row_vec[11]))
+                            *base::StringToUInt32(row_vec[10]))
                       : std::nullopt;
   row.arg_set_id = *base::StringToUInt32(row_vec[11]);
   row.thread_ts = base::StringToInt64(row_vec[12]);
@@ -236,6 +241,10 @@
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap(c, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 
 void BenchmarkSliceTableSort(benchmark::State& state,
@@ -261,6 +270,10 @@
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 
 void BenchmarkFtraceEventTableFilter(benchmark::State& state,
@@ -273,6 +286,10 @@
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 
 void BenchmarkFtraceEventTableSort(benchmark::State& state,
@@ -289,7 +306,7 @@
 
 void BM_QESliceTableTrackIdEq(benchmark::State& state) {
   SliceTableForBenchmark table(state);
-  BenchmarkSliceTableFilter(state, table, {table.table_.track_id().eq(100)});
+  BenchmarkSliceTableFilter(state, table, {table.table_.track_id().eq(1213)});
 }
 
 BENCHMARK(BM_QESliceTableTrackIdEq);
@@ -304,29 +321,33 @@
 
 void BM_QESliceTableParentIdEq(benchmark::State& state) {
   SliceTableForBenchmark table(state);
-  BenchmarkSliceTableFilter(state, table, {table.table_.parent_id().eq(88)});
+  BenchmarkSliceTableFilter(state, table, {table.table_.parent_id().eq(26711)});
 }
 
 BENCHMARK(BM_QESliceTableParentIdEq);
 
 void BM_QESliceTableNameEq(benchmark::State& state) {
   SliceTableForBenchmark table(state);
-  BenchmarkSliceTableFilter(state, table, {table.table_.name().eq("cheese")});
+  BenchmarkSliceTableFilter(
+      state, table,
+      {table.table_.name().eq("MarkFromReadBarrierWithMeasurements")});
 }
 
 BENCHMARK(BM_QESliceTableNameEq);
 
 void BM_QESliceTableNameGlobNoStars(benchmark::State& state) {
   SliceTableForBenchmark table(state);
-  BenchmarkSliceTableFilter(state, table, {table.table_.name().glob("cheese")});
+  BenchmarkSliceTableFilter(
+      state, table,
+      {table.table_.name().glob("MarkFromReadBarrierWithMeasurements")});
 }
 
 BENCHMARK(BM_QESliceTableNameGlobNoStars);
 
 void BM_QESliceTableNameGlob(benchmark::State& state) {
   SliceTableForBenchmark table(state);
-  BenchmarkSliceTableFilter(state, table,
-                            {table.table_.name().glob("chee*se")});
+  BenchmarkSliceTableFilter(
+      state, table, {table.table_.name().glob("HIDL::IMapper::unlock::*")});
 }
 
 BENCHMARK(BM_QESliceTableNameGlob);
@@ -341,7 +362,9 @@
 
 void BM_QESliceTableSorted(benchmark::State& state) {
   SliceTableForBenchmark table(state);
-  BenchmarkSliceTableFilter(state, table, {table.table_.ts().gt(1000)});
+  BenchmarkSliceTableFilter(state, table,
+                            {table.table_.ts().gt(1738923505854),
+                             table.table_.ts().lt(1738950140556)});
 }
 
 BENCHMARK(BM_QESliceTableSorted);
@@ -349,7 +372,7 @@
 void BM_QEFilterWithSparseSelector(benchmark::State& state) {
   ExpectedFrameTimelineTableForBenchmark table(state);
   BenchmarkExpectedFrameTableFilter(state, table,
-                                    table.table_.track_id().eq(88));
+                                    table.table_.track_id().eq(1445));
 }
 
 BENCHMARK(BM_QEFilterWithSparseSelector);
@@ -379,8 +402,8 @@
   SliceTableForBenchmark table(state);
   BenchmarkSliceTableFilter(
       state, table,
-      {table.table_.ts().ge(1740530419866), table.table_.ts().le(1740530474097),
-       table.table_.track_id().eq(100)});
+      {table.table_.ts().ge(1738923505854), table.table_.ts().le(1738950140556),
+       table.table_.track_id().eq(1422)});
 }
 
 BENCHMARK(BM_QESliceTableTsAndTrackId);
@@ -388,7 +411,8 @@
 void BM_QEFilterOneElement(benchmark::State& state) {
   SliceTableForBenchmark table(state);
   BenchmarkSliceTableFilter(
-      state, table, {table.table_.id().eq(10), table.table_.dur().eq(100)});
+      state, table,
+      {table.table_.id().eq(11732), table.table_.track_id().eq(1422)});
 }
 
 BENCHMARK(BM_QEFilterOneElement);
@@ -407,6 +431,10 @@
       static_cast<double>(slice_sorted_with_duration.row_count()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 
 BENCHMARK(BM_QEFilterWithArrangement);
@@ -422,6 +450,10 @@
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 BENCHMARK(BM_QEDenseNullFilter);
 
@@ -436,6 +468,10 @@
       benchmark::Counter(static_cast<double>(table.table_.row_count()),
                          benchmark::Counter::kIsIterationInvariantRate |
                              benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 BENCHMARK(BM_QEDenseNullFilterIsNull);
 
@@ -471,10 +507,32 @@
       static_cast<double>(slice_sorted_with_duration.row_count()),
       benchmark::Counter::kIsIterationInvariantRate |
           benchmark::Counter::kInvert);
+  state.counters["s/out"] = benchmark::Counter(
+      static_cast<double>(table.table_.QueryToRowMap({c}, {}).size()),
+      benchmark::Counter::kIsIterationInvariantRate |
+          benchmark::Counter::kInvert);
 }
 
 BENCHMARK(BM_QEFilterOrderedArrangement);
 
+void BM_QESliceFilterIndexSearchOneElement(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTableFilter(
+      state, table,
+      {table.table_.track_id().eq(1422), table.table_.id().eq(11732)});
+}
+
+BENCHMARK(BM_QESliceFilterIndexSearchOneElement);
+
+void BM_QESliceFilterIndexSearch(benchmark::State& state) {
+  SliceTableForBenchmark table(state);
+  BenchmarkSliceTableFilter(state, table,
+                            {table.table_.track_id().eq(1422),
+                             table.table_.name().eq("notifyFramePending")});
+}
+
+BENCHMARK(BM_QESliceFilterIndexSearch);
+
 void BM_QESliceSortNumericAsc(benchmark::State& state) {
   SliceTableForBenchmark table(state);
   BenchmarkSliceTableSort(state, table, {table.table_.track_id().ascending()});
diff --git a/src/trace_processor/db/table.h b/src/trace_processor/db/table.h
index eab9f92..7d53106 100644
--- a/src/trace_processor/db/table.h
+++ b/src/trace_processor/db/table.h
@@ -23,7 +23,6 @@
 #include <utility>
 #include <vector>
 
-#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/ref_counted.h"
@@ -33,7 +32,6 @@
 #include "src/trace_processor/db/column/data_layer.h"
 #include "src/trace_processor/db/column/types.h"
 #include "src/trace_processor/db/column_storage_overlay.h"
-#include "src/trace_processor/db/query_executor.h"
 
 namespace perfetto::trace_processor {
 
@@ -77,7 +75,7 @@
     }
 
     // Returns whether the row the iterator is pointing at is valid.
-    explicit operator bool() const { return its_[0]; }
+    explicit operator bool() const { return bool(its_[0]); }
 
     // Returns the value at the current row for column |col_idx|.
     SqlValue Get(uint32_t col_idx) const {
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index c92ba32..0aa6e64 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -35,6 +35,7 @@
     "android_dvfs.sql",
     "android_fastrpc.sql",
     "android_frame_timeline_metric.sql",
+    "android_garbage_collection_unagg.sql",
     "android_gpu.sql",
     "android_hwcomposer.sql",
     "android_hwui_metric.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_boot_unagg.sql b/src/trace_processor/metrics/sql/android/android_boot_unagg.sql
index 2444b22..a1ba096 100644
--- a/src/trace_processor/metrics/sql/android/android_boot_unagg.sql
+++ b/src/trace_processor/metrics/sql/android/android_boot_unagg.sql
@@ -15,6 +15,8 @@
 --
 
 INCLUDE PERFETTO MODULE android.app_process_starts;
+INCLUDE PERFETTO MODULE android.garbage_collection;
+INCLUDE PERFETTO MODULE android.suspend;
 
 DROP VIEW IF EXISTS android_boot_unagg_output;
 CREATE PERFETTO VIEW android_boot_unagg_output AS
@@ -71,6 +73,35 @@
             ASC LIMIT 1 )
             AND reason = "service"
         )
-    )
-  )
+    )),
+    'android_post_boot_gc_metric', (SELECT AndroidGarbageCollectionUnaggMetric(
+        'gc_events', (
+            SELECT RepeatedField(
+                AndroidGarbageCollectionUnaggMetric_GarbageCollectionEvent(
+                    'thread_name', thread_name,
+                    'process_name', process_name,
+                    'gc_type', gc_type,
+                    'is_mark_compact', is_mark_compact,
+                    'reclaimed_mb', reclaimed_mb,
+                    'min_heap_mb', min_heap_mb,
+                    'max_heap_mb', max_heap_mb,
+                    'mb_per_ms_of_running_gc', reclaimed_mb/(gc_running_dur/1e6),
+                    'mb_per_ms_of_wall_gc', reclaimed_mb/(gc_dur/1e6),
+                    'gc_dur', gc_dur,
+                    'gc_running_dur', gc_running_dur,
+                    'gc_runnable_dur', gc_runnable_dur,
+                    'gc_unint_io_dur', gc_unint_io_dur,
+                    'gc_unint_non_io_dur', gc_unint_non_io_dur,
+                    'gc_int_dur', gc_int_dur,
+                    'gc_ts', gc_ts,
+                    'tid', tid,
+                    'pid', pid,
+                    'gc_monotonic_dur', _extract_duration_without_suspend(gc_ts, gc_dur)
+                )
+            ) FROM android_garbage_collection_events WHERE gc_ts > (SELECT COALESCE(MIN(ts), 0)
+                FROM thread_slice WHERE name GLOB "*android.intent.action.USER_UNLOCKED*" ORDER BY ts
+                ASC LIMIT 1
+            )
+        )
+    ))
 );
diff --git a/src/trace_processor/metrics/sql/android/android_garbage_collection_unagg.sql b/src/trace_processor/metrics/sql/android/android_garbage_collection_unagg.sql
new file mode 100644
index 0000000..ee54b7e
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_garbage_collection_unagg.sql
@@ -0,0 +1,47 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     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.
+--
+
+INCLUDE PERFETTO MODULE android.garbage_collection;
+INCLUDE PERFETTO MODULE android.suspend;
+
+DROP VIEW IF EXISTS android_garbage_collection_unagg_output;
+CREATE PERFETTO VIEW android_garbage_collection_unagg_output AS
+SELECT AndroidGarbageCollectionUnaggMetric(
+  'gc_events', (
+    SELECT RepeatedField(
+      AndroidGarbageCollectionUnaggMetric_GarbageCollectionEvent(
+        'thread_name', thread_name,
+        'process_name', process_name,
+        'gc_type', gc_type,
+        'is_mark_compact', is_mark_compact,
+        'reclaimed_mb', reclaimed_mb,
+        'min_heap_mb', min_heap_mb,
+        'max_heap_mb', max_heap_mb,
+        'mb_per_ms_of_running_gc', reclaimed_mb/(gc_running_dur/1e6),
+        'mb_per_ms_of_wall_gc', reclaimed_mb/(gc_dur/1e6),
+        'gc_dur', gc_dur,
+        'gc_running_dur', gc_running_dur,
+        'gc_runnable_dur', gc_runnable_dur,
+        'gc_unint_io_dur', gc_unint_io_dur,
+        'gc_unint_non_io_dur', gc_unint_non_io_dur,
+        'gc_int_dur', gc_int_dur,
+        'gc_ts', gc_ts,
+        'tid', tid,
+        'pid', pid,
+        'gc_monotonic_dur', _extract_duration_without_suspend(gc_ts, gc_dur)
+      )) FROM android_garbage_collection_events
+  )
+);
diff --git a/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc b/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc
index 7680b24..f102b1f 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc
+++ b/src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.cc
@@ -70,6 +70,10 @@
     // Update the loop variable by looking up the next parent_id.
     maybe_parent_id = ref.parent_id();
   }
+  // We traverse the tree in reverse id order. To ensure we meet the
+  // requirements of the extension vectors being sorted, ensure that we reverse
+  // the row numbers to be in id order.
+  std::reverse(row_numbers_accumulator.begin(), row_numbers_accumulator.end());
   return base::OkStatus();
 }
 
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
index fa5ba77..642b822 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
@@ -15,6 +15,7 @@
 import("../../../../../gn/perfetto_sql.gni")
 
 perfetto_sql_source_set("sched") {
+  deps = [ "utilization" ]
   sources = [
     "states.sql",
     "thread_executing_span.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/utilization/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/BUILD.gn
new file mode 100644
index 0000000..d4be2cc
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/BUILD.gn
@@ -0,0 +1,24 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../../../gn/perfetto_sql.gni")
+
+perfetto_sql_source_set("utilization") {
+  sources = [
+    "general.sql",
+    "process.sql",
+    "system.sql",
+    "thread.sql",
+  ]
+}
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/utilization/general.sql b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/general.sql
new file mode 100644
index 0000000..629e5a3
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/general.sql
@@ -0,0 +1,75 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     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.
+
+-- Returns the timestamp of the start of the partition that contains the |ts|.
+CREATE PERFETTO FUNCTION _partition_start(ts INT, size INT) RETURNS INT AS
+-- Division of two ints would result in floor(ts/size).
+SELECT ($ts/$size)*$size;
+
+-- Returns the number of partitions required to cover all of the trace
+-- timestamps.
+CREATE PERFETTO FUNCTION _partition_count(size INT) RETURNS INT AS
+SELECT
+    (_partition_start(TRACE_END(), $size) -
+    _partition_start(TRACE_START(), $size))/$size + 1;
+
+-- Returns a table of partitions with first partition containing the
+-- TRACE_START() and last one containing TRACE_END().
+CREATE PERFETTO FUNCTION _partitions(size INT)
+RETURNS TABLE (ts INT, ts_end INT) AS
+WITH no_ends AS (
+SELECT
+    _partition_start(TRACE_START(), $size) + (id * $size) AS ts
+-- We are using the sched table for source of ids. If the table is too small
+-- for specified size, the results would be invalid none the less.
+FROM sched
+LIMIT _partition_count($size))
+SELECT ts, ts + $size AS ts_end FROM no_ends;
+
+-- Partitions any |intervals| table with partitions defined in the |partitions|
+-- table.
+CREATE PERFETTO MACRO _interval_partitions(
+  -- Requires |ts| and |ts_end| columns.
+  partitions TableOrSubquery,
+  -- Requires |ts| and |ts_end| column.
+  intervals TableOrSubquery
+) RETURNS TableOrSubquery AS (
+SELECT
+  p.ts AS partition_ts,
+  IIF(i.ts_end < p.ts_end, i.ts_end, p.ts_end) AS ts_end,
+  IIF(i.ts < p.ts, p.ts, i.ts) AS ts
+FROM $intervals i
+JOIN $partitions p
+ON (p.ts <= i.ts AND i.ts < p.ts_end));
+
+-- Returns a table of utilization per given period.
+-- Utilization is calculated as sum of average utilization of each CPU in each
+-- period, which is defined as a multiply of |interval|. For this reason
+-- first and last period might have lower then real utilization.
+CREATE PERFETTO MACRO _sched_avg_utilization_per_period(
+  -- Length of the period on which utilization should be averaged.
+  interval Expr,
+  -- Either sched table or its filtered down version.
+  sched_table TableOrSubquery
+)
+-- The returned table has the schema (ts UINT32, utilization DOUBLE,
+-- unnormalized_utilization DOUBLE).
+RETURNS TableOrSubquery AS (
+SELECT
+  partition_ts AS ts,
+  SUM(ts_end - ts)/(cast_double!($interval) * (SELECT MAX(cpu) + 1 FROM sched)) AS utilization,
+  SUM(ts_end - ts)/cast_double!($interval) AS unnormalized_utilization
+FROM _interval_partitions!(_partitions($interval), $sched_table)
+GROUP BY 1);
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/utilization/process.sql b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/process.sql
new file mode 100644
index 0000000..8437cb4
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/process.sql
@@ -0,0 +1,72 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     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.
+
+INCLUDE PERFETTO MODULE sched.utilization.general;
+INCLUDE PERFETTO MODULE time.conversion;
+
+-- Returns a table of process utilization per given period.
+-- Utilization is calculated as sum of average utilization of each CPU in each
+-- period, which is defined as a multiply of |interval|. For this reason
+-- first and last period might have lower then real utilization.
+CREATE PERFETTO FUNCTION sched_process_utilization_per_period(
+    -- Length of the period on which utilization should be averaged.
+    interval INT,
+    -- Upid of the process.
+    upid INT
+)
+RETURNS TABLE(
+  -- Timestamp of start of a second.
+  ts INT,
+  -- Sum of average utilization over period.
+  -- Note: as the data is normalized, the values will be in the
+  -- [0, 1] range.
+  utilization DOUBLE,
+  -- Sum of average utilization over all CPUs over period.
+  -- Note: as the data is unnormalized, the values will be in the
+  -- [0, cpu_count] range.
+  unnormalized_utilization DOUBLE
+) AS
+WITH sched_for_upid AS (
+  SELECT
+    ts,
+    ts_end,
+    utid
+  FROM sched
+  JOIN thread USING (utid)
+  JOIN process USING (upid)
+  WHERE upid = $upid AND utid != 0)
+SELECT * FROM _sched_avg_utilization_per_period!($interval, sched_for_upid);
+
+-- Returns a table of process utilization per second.
+-- Utilization is calculated as sum of average utilization of each CPU in each
+-- period, which is defined as a multiply of |interval|. For this reason
+-- first and last period might have lower then real utilization.
+CREATE PERFETTO FUNCTION sched_process_utilization_per_second(
+  -- Upid of the process.
+  upid INT
+)
+RETURNS TABLE (
+  -- Timestamp of start of a second.
+  ts INT,
+  -- Sum of average utilization over period.
+  -- Note: as the data is normalized, the values will be in the
+  -- [0, 1] range.
+  utilization DOUBLE,
+  -- Sum of average utilization over all CPUs over period.
+  -- Note: as the data is unnormalized, the values will be in the
+  -- [0, cpu_count] range.
+  unnormalized_utilization DOUBLE
+) AS
+SELECT * FROM sched_process_utilization_per_period(time_from_s(1), $upid);
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/utilization/system.sql b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/system.sql
new file mode 100644
index 0000000..7dd671d
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/system.sql
@@ -0,0 +1,67 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     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.
+
+INCLUDE PERFETTO MODULE sched.utilization.general;
+INCLUDE PERFETTO MODULE time.conversion;
+
+-- The purpose of this module is to provide high level aggregates of system
+-- utilization, akin to /proc/stat results.
+
+-- Returns a table of system utilization per given period.
+-- Utilization is calculated as sum of average utilization of each CPU in each
+-- period, which is defined as a multiply of |interval|. For this reason
+-- first and last period might have lower then real utilization.
+CREATE PERFETTO FUNCTION sched_utilization_per_period(
+  -- Length of the period on which utilization should be averaged.
+  interval INT)
+RETURNS TABLE (
+  -- Timestamp of start of a second.
+  ts INT,
+  -- Sum of average utilization over period.
+  -- Note: as the data is normalized, the values will be in the
+  -- [0, 1] range.
+  utilization DOUBLE,
+  -- Sum of average utilization over all CPUs over period.
+  -- Note: as the data is unnormalized, the values will be in the
+  -- [0, cpu_count] range.
+  unnormalized_utilization DOUBLE
+) AS
+SELECT *
+FROM _sched_avg_utilization_per_period!(
+  $interval,
+  (SELECT * FROM sched WHERE utid != 0)
+);
+
+-- Table with system utilization per second.
+-- Utilization is calculated by sum of average utilization of each CPU every
+-- second. For this reason first and last second might have lower then real
+-- utilization.
+CREATE PERFETTO TABLE sched_utilization_per_second(
+  -- Timestamp of start of a second.
+  ts INT,
+  -- Sum of average utilization over period.
+  -- Note: as the data is normalized, the values will be in the
+  -- [0, 1] range.
+  utilization DOUBLE,
+  -- Sum of average utilization over all CPUs over period.
+  -- Note: as the data is unnormalized, the values will be in the
+  -- [0, cpu_count] range.
+  unnormalized_utilization DOUBLE
+) AS
+SELECT
+  ts,
+  utilization,
+  unnormalized_utilization
+FROM sched_utilization_per_period(time_from_s(1));
\ No newline at end of file
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/utilization/thread.sql b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/thread.sql
new file mode 100644
index 0000000..ae1d3f6
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/utilization/thread.sql
@@ -0,0 +1,70 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     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.
+
+INCLUDE PERFETTO MODULE sched.utilization.general;
+INCLUDE PERFETTO MODULE time.conversion;
+
+-- Returns a table of thread utilization per given period.
+-- Utilization is calculated as sum of average utilization of each CPU in each
+-- period, which is defined as a multiply of |interval|. For this reason
+-- first and last period might have lower then real utilization.
+CREATE PERFETTO FUNCTION sched_thread_utilization_per_period(
+    -- Length of the period on which utilization should be averaged.
+    interval INT,
+    -- Utid of the thread.
+    utid INT
+)
+RETURNS TABLE(
+  -- Timestamp of start of a second.
+  ts INT,
+  -- Sum of average utilization over period.
+  -- Note: as the data is normalized, the values will be in the
+  -- [0, 1] range.
+  utilization DOUBLE,
+  -- Sum of average utilization over all CPUs over period.
+  -- Note: as the data is unnormalized, the values will be in the
+  -- [0, cpu_count] range.
+  unnormalized_utilization DOUBLE
+) AS
+WITH sched_for_utid AS (
+  SELECT
+    ts,
+    ts_end,
+    utid
+  FROM sched
+  WHERE utid = $utid
+) SELECT * FROM _sched_avg_utilization_per_period!($interval, sched_for_utid);
+
+-- Returns a table of thread utilization per second.
+-- Utilization is calculated as sum of average utilization of each CPU in each
+-- period, which is defined as a multiply of |interval|. For this reason
+-- first and last period might have lower then real utilization.
+CREATE PERFETTO FUNCTION sched_thread_utilization_per_second(
+  -- Utid of the thread.
+  utid INT
+)
+RETURNS TABLE (
+  -- Timestamp of start of a second.
+  ts INT,
+  -- Sum of average utilization over period.
+  -- Note: as the data is normalized, the values will be in the
+  -- [0, 1] range.
+  utilization DOUBLE,
+  -- Sum of average utilization over all CPUs over period.
+  -- Note: as the data is unnormalized, the values will be in the
+  -- [0, cpu_count] range.
+  unnormalized_utilization DOUBLE
+) AS
+SELECT * FROM sched_thread_utilization_per_period(time_from_s(1), $utid);
\ No newline at end of file
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 9ad4635..692b105 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -1672,7 +1672,7 @@
     RETURN_IF_ERROR(ExportTraceToDatabase(options.sqlite_file_path));
   }
 
-  if (options.enable_httpd || options.enable_stdiod) {
+  if (options.enable_httpd) {
 #if PERFETTO_HAS_SIGNAL_H()
     if (options.metatrace_path.empty()) {
       // Restore the default signal handler to allow the user to terminate
@@ -1688,18 +1688,16 @@
     }
 #endif
 
-    if (options.enable_httpd) {
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_HTTPD)
-      RunHttpRPCServer(std::move(tp), options.port_number);
-      PERFETTO_FATAL("Should never return");
+    RunHttpRPCServer(std::move(tp), options.port_number);
+    PERFETTO_FATAL("Should never return");
 #else
-      PERFETTO_FATAL("HTTP not available");
+    PERFETTO_FATAL("HTTP not available");
 #endif
-    }
+  }
 
-    if (options.enable_stdiod) {
-      return RunStdioRpcServer(std::move(tp));
-    }
+  if (options.enable_stdiod) {
+    return RunStdioRpcServer(std::move(tp));
   }
 
   if (options.launch_shell) {
diff --git a/src/trace_redaction/find_package_uid.cc b/src/trace_redaction/find_package_uid.cc
index d069fe2..0c46ff5 100644
--- a/src/trace_redaction/find_package_uid.cc
+++ b/src/trace_redaction/find_package_uid.cc
@@ -17,7 +17,9 @@
 #include "src/trace_redaction/find_package_uid.h"
 
 #include "perfetto/ext/base/status_or.h"
-#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
 #include "protos/perfetto/trace/android/packages_list.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
diff --git a/src/trace_redaction/find_package_uid.h b/src/trace_redaction/find_package_uid.h
index 4ae34d4..d43b2af 100644
--- a/src/trace_redaction/find_package_uid.h
+++ b/src/trace_redaction/find_package_uid.h
@@ -18,9 +18,10 @@
 #define SRC_TRACE_REDACTION_FIND_PACKAGE_UID_H_
 
 #include "perfetto/ext/base/status_or.h"
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
 namespace perfetto::trace_redaction {
 
 // Writes the uid for the package matching `Context.package_name`. Returns
diff --git a/src/trace_redaction/find_package_uid_unittest.cc b/src/trace_redaction/find_package_uid_unittest.cc
index 44f0a45..5f390ab 100644
--- a/src/trace_redaction/find_package_uid_unittest.cc
+++ b/src/trace_redaction/find_package_uid_unittest.cc
@@ -15,18 +15,18 @@
  * limitations under the License.
  */
 
-#include "find_package_uid.h"
+#include "src/trace_redaction/find_package_uid.h"
 
 #include <cstdint>
 #include <string>
 
-#include "protos/perfetto/trace/android/packages_list.gen.h"
-#include "protos/perfetto/trace/ps/process_tree.gen.h"
-#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
-#include "protos/perfetto/trace/trace_packet.gen.h"
 #include "src/base/test/status_matchers.h"
 #include "test/gtest_and_gmock.h"
 
+#include "protos/perfetto/trace/android/packages_list.gen.h"
+#include "protos/perfetto/trace/ps/process_tree.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+
 namespace perfetto::trace_redaction {
 
 namespace {
diff --git a/src/trace_redaction/prune_package_list.cc b/src/trace_redaction/prune_package_list.cc
index 52c1394..83d2355 100644
--- a/src/trace_redaction/prune_package_list.cc
+++ b/src/trace_redaction/prune_package_list.cc
@@ -16,7 +16,12 @@
 
 #include "src/trace_redaction/prune_package_list.h"
 
+#include <string>
+
+#include "perfetto/base/status.h"
+
 #include "protos/perfetto/trace/android/packages_list.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
 
 namespace perfetto::trace_redaction {
 
diff --git a/src/trace_redaction/prune_package_list.h b/src/trace_redaction/prune_package_list.h
index cf1298f..24d9ec2 100644
--- a/src/trace_redaction/prune_package_list.h
+++ b/src/trace_redaction/prune_package_list.h
@@ -19,6 +19,7 @@
 
 #include <string>
 
+#include "perfetto/base/status.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 namespace perfetto::trace_redaction {
diff --git a/src/trace_redaction/prune_package_list_unittest.cc b/src/trace_redaction/prune_package_list_unittest.cc
index 6a9af6c..0f78cba 100644
--- a/src/trace_redaction/prune_package_list_unittest.cc
+++ b/src/trace_redaction/prune_package_list_unittest.cc
@@ -16,13 +16,15 @@
  */
 
 #include <cstdint>
+#include <memory>
 #include <string>
 
+#include "src/trace_redaction/prune_package_list.h"
+#include "test/gtest_and_gmock.h"
+
 #include "protos/perfetto/trace/android/packages_list.gen.h"
 #include "protos/perfetto/trace/ps/process_tree.gen.h"
 #include "protos/perfetto/trace/trace_packet.gen.h"
-#include "src/trace_redaction/prune_package_list.h"
-#include "test/gtest_and_gmock.h"
 
 namespace perfetto::trace_redaction {
 
diff --git a/src/trace_redaction/scrub_trace_packet.cc b/src/trace_redaction/scrub_trace_packet.cc
index e4f03cf..e56acf9 100644
--- a/src/trace_redaction/scrub_trace_packet.cc
+++ b/src/trace_redaction/scrub_trace_packet.cc
@@ -14,8 +14,12 @@
  * limitations under the License.
  */
 
+#include <string>
+
 #include "src/trace_redaction/scrub_trace_packet.h"
 
+#include "perfetto/base/status.h"
+
 namespace perfetto::trace_redaction {
 // The TracePacket message has a simple structure. At its core its one sub
 // message (e.g. ProcessTree) and some additional context (e.g. timestamp).
diff --git a/src/trace_redaction/scrub_trace_packet.h b/src/trace_redaction/scrub_trace_packet.h
index 06710d3..fbf89ca 100644
--- a/src/trace_redaction/scrub_trace_packet.h
+++ b/src/trace_redaction/scrub_trace_packet.h
@@ -19,8 +19,6 @@
 
 #include "src/trace_redaction/trace_redaction_framework.h"
 
-#include "protos/perfetto/trace/trace_packet.pbzero.h"
-
 namespace perfetto::trace_redaction {
 
 // Drops whole trace packets based on an allow-list (e.g. retain ProcessTree
diff --git a/src/trace_redaction/scrub_trace_packet_unittest.cc b/src/trace_redaction/scrub_trace_packet_unittest.cc
index 82b99d4..f203165 100644
--- a/src/trace_redaction/scrub_trace_packet_unittest.cc
+++ b/src/trace_redaction/scrub_trace_packet_unittest.cc
@@ -21,6 +21,7 @@
 #include "test/gtest_and_gmock.h"
 
 #include "protos/perfetto/trace/ps/process_tree.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
 namespace perfetto::trace_redaction {
diff --git a/src/trace_redaction/trace_redaction_framework.h b/src/trace_redaction/trace_redaction_framework.h
index b481961..76b8594 100644
--- a/src/trace_redaction/trace_redaction_framework.h
+++ b/src/trace_redaction/trace_redaction_framework.h
@@ -20,12 +20,11 @@
 #include <cstdint>
 #include <optional>
 #include <string>
-#include <vector>
 
 #include "perfetto/base/flat_set.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
 
-#include "protos/perfetto/trace/trace_packet.gen.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
 namespace perfetto::trace_redaction {
diff --git a/src/trace_redaction/trace_redactor.h b/src/trace_redaction/trace_redactor.h
index 99a8832..82ec371 100644
--- a/src/trace_redaction/trace_redactor.h
+++ b/src/trace_redaction/trace_redactor.h
@@ -17,8 +17,12 @@
 #ifndef SRC_TRACE_REDACTION_TRACE_REDACTOR_H_
 #define SRC_TRACE_REDACTION_TRACE_REDACTOR_H_
 
+#include <memory>
+#include <string>
 #include <string_view>
+#include <vector>
 
+#include "perfetto/base/status.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
diff --git a/src/trace_redaction/trace_redactor_integrationtest.cc b/src/trace_redaction/trace_redactor_integrationtest.cc
index 8e10095..8f14744 100644
--- a/src/trace_redaction/trace_redactor_integrationtest.cc
+++ b/src/trace_redaction/trace_redactor_integrationtest.cc
@@ -14,11 +14,19 @@
  * limitations under the License.
  */
 
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/temp_file.h"
 #include "src/base/test/utils.h"
 #include "src/trace_redaction/find_package_uid.h"
 #include "src/trace_redaction/prune_package_list.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
 #include "src/trace_redaction/trace_redactor.h"
 #include "test/gtest_and_gmock.h"
 
@@ -36,9 +44,6 @@
 constexpr std::string_view kTracePath =
     "test/data/trace-redaction-general.pftrace";
 
-constexpr std::string_view kPackageName =
-    "com.Unity.com.unity.multiplayer.samples.coop";
-
 constexpr uint64_t kPackageUid = 10252;
 
 class TraceRedactorIntegrationTest : public testing::Test {
@@ -85,7 +90,7 @@
   redaction.transformers()->emplace_back(new PrunePackageList());
 
   Context context;
-  context.package_name = kPackageName;
+  context.package_name = "com.Unity.com.unity.multiplayer.samples.coop";
 
   auto result = redaction.Redact(src_trace(), dest_trace(), &context);
 
@@ -97,27 +102,73 @@
   Trace::Decoder redacted_trace(redacted_buffer);
   std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
 
+  ASSERT_TRUE(context.package_uid.has_value());
+  ASSERT_EQ(NormalizeUid(context.package_uid.value()),
+            NormalizeUid(kPackageUid));
+
   // It is possible for two packages_list to appear in the trace. The
   // find_package_uid will stop after the first one is found. Package uids are
   // appear as n * 1,000,000 where n is some integer. It is also possible for
   // two packages_list to contain copies of each other - for example
   // "com.Unity.com.unity.multiplayer.samples.coop" appears in both
   // packages_list.
-  ASSERT_GE(infos.size(), 1u);
+  ASSERT_EQ(infos.size(), 2u);
 
-  for (const auto& info_buffer : infos) {
-    PackageInfo::Decoder info(info_buffer);
+  std::array<PackageInfo::Decoder, 2> decoders = {
+      PackageInfo::Decoder(infos[0]), PackageInfo::Decoder(infos[1])};
 
+  for (auto& decoder : decoders) {
+    ASSERT_TRUE(decoder.has_name());
+    ASSERT_EQ(decoder.name().ToStdString(),
+              "com.Unity.com.unity.multiplayer.samples.coop");
+
+    ASSERT_TRUE(decoder.has_uid());
+    ASSERT_EQ(NormalizeUid(decoder.uid()), NormalizeUid(kPackageUid));
+  }
+}
+
+// It is possible for multiple packages to share a uid. The names will appears
+// across multiple package lists. The only time the package name appears is in
+// the package list, so there is no way to differentiate these packages (only
+// the uid is used later), so each entry should remain.
+TEST_F(TraceRedactorIntegrationTest, RetainsAllInstancesOfUid) {
+  TraceRedactor redaction;
+  redaction.collectors()->emplace_back(new FindPackageUid());
+  redaction.transformers()->emplace_back(new PrunePackageList());
+
+  Context context;
+  context.package_name = "com.google.android.networkstack.tethering";
+
+  auto result = redaction.Redact(src_trace(), dest_trace(), &context);
+  ASSERT_TRUE(result.ok()) << result.message();
+
+  std::string redacted_buffer;
+  ASSERT_TRUE(base::ReadFile(dest_trace(), &redacted_buffer));
+
+  Trace::Decoder redacted_trace(redacted_buffer);
+  std::vector<protozero::ConstBytes> infos = GetPackageInfos(redacted_trace);
+
+  ASSERT_EQ(infos.size(), 8u);
+
+  std::array<std::string, 8> package_names;
+
+  for (size_t i = 0; i < infos.size(); ++i) {
+    PackageInfo::Decoder info(infos[i]);
     ASSERT_TRUE(info.has_name());
-    ASSERT_EQ(info.name().ToStdString(), kPackageName);
-
-    ASSERT_TRUE(info.has_uid());
-    ASSERT_EQ(NormalizeUid(info.uid()), NormalizeUid(kPackageUid));
+    package_names[i] = info.name().ToStdString();
   }
 
-  ASSERT_TRUE(context.package_uid.has_value());
-  ASSERT_EQ(NormalizeUid(context.package_uid.value()),
-            NormalizeUid(kPackageUid));
+  std::sort(package_names.begin(), package_names.end());
+  ASSERT_EQ(package_names[0], "com.google.android.cellbroadcastservice");
+  ASSERT_EQ(package_names[1], "com.google.android.cellbroadcastservice");
+  ASSERT_EQ(package_names[2], "com.google.android.networkstack");
+  ASSERT_EQ(package_names[3], "com.google.android.networkstack");
+  ASSERT_EQ(package_names[4],
+            "com.google.android.networkstack.permissionconfig");
+  ASSERT_EQ(package_names[5],
+            "com.google.android.networkstack.permissionconfig");
+  ASSERT_EQ(package_names[6], "com.google.android.networkstack.tethering");
+  ASSERT_EQ(package_names[7], "com.google.android.networkstack.tethering");
 }
 
 }  // namespace
diff --git a/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out b/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out
index 568da0b..cb0d712 100644
--- a/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out
+++ b/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out
@@ -214,4 +214,80 @@
       total_dur: 583162190
     }
   }
+  android_post_boot_gc_metric {
+    gc_events {
+      thread_name: "HeapTaskDaemon"
+      process_name: "com.google.android.deskclock"
+      gc_type: "full"
+      is_mark_compact: 1
+      reclaimed_mb: 3.1430000000000002
+      min_heap_mb: 2.754000
+      max_heap_mb: 5.897000
+      mb_per_ms_of_running_gc: 0.3262420579054222
+      mb_per_ms_of_wall_gc: 0.004718018639325977
+      gc_dur: 666169475
+      gc_running_dur: 9633951
+      gc_runnable_dur: 44371461
+      gc_unint_io_dur: 0
+      gc_unint_non_io_dur: 0
+      gc_int_dur: 0
+      gc_ts: 39041818011
+      tid: 3185
+      pid: 3162
+      gc_monotonic_dur: 666169475
+    }
+    gc_events {
+      thread_name: "HeapTaskDaemon"
+      process_name: "com.google.android.apps.nexuslauncher"
+      gc_type: "full"
+      is_mark_compact: 1
+      reclaimed_mb: 10.640000
+      min_heap_mb: 6.861000
+      max_heap_mb: 17.501000
+      mb_per_ms_of_running_gc: 93.68918788028213
+      mb_per_ms_of_wall_gc: 0.18073523433053545
+      gc_dur: 58870646
+      gc_running_dur: 113567
+      gc_runnable_dur: 0
+      gc_unint_io_dur: 0
+      gc_unint_non_io_dur: 0
+      gc_int_dur: 0
+      gc_ts: 39849615741
+      tid: 2534
+      pid: 2523
+      gc_monotonic_dur: 58870646
+    }
+    gc_events {
+      thread_name: "HeapTaskDaemon"
+      process_name: "com.google.android.apps.wellbeing"
+      gc_type: "full"
+      is_mark_compact: 1
+      gc_dur: 168393585
+      gc_running_dur: 1714233
+      gc_runnable_dur: 28482219
+      gc_unint_io_dur: 0
+      gc_unint_non_io_dur: 0
+      gc_int_dur: -1
+      gc_ts: 40022150450
+      tid: 2917
+      pid: 2909
+      gc_monotonic_dur: 168393585
+    }
+    gc_events {
+      thread_name: "HeapTaskDaemon"
+      process_name: "com.google.android.euicc"
+      gc_type: "full"
+      is_mark_compact: 1
+      gc_dur: 148630605
+      gc_running_dur: 4148763
+      gc_runnable_dur: -1
+      gc_unint_io_dur: 0
+      gc_unint_non_io_dur: 0
+      gc_int_dur: 0
+      gc_ts: 40041913430
+      tid: 2865
+      pid: 2855
+      gc_monotonic_dur: 148630605
+    }
+  }
 }
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out b/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out
new file mode 100644
index 0000000..1341536
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out
@@ -0,0 +1,76 @@
+android_garbage_collection_unagg {
+  gc_events {
+    thread_name: "HeapTaskDaemon"
+    process_name: "com.google.android.deskclock"
+    gc_type: "full"
+    is_mark_compact: 1
+    reclaimed_mb: 3.1430000000000002
+    min_heap_mb: 2.754000
+    max_heap_mb: 5.897000
+    mb_per_ms_of_running_gc: 0.3262420579054222
+    mb_per_ms_of_wall_gc: 0.004718018639325977
+    gc_dur: 666169475
+    gc_running_dur: 9633951
+    gc_runnable_dur: 44371461
+    gc_unint_io_dur: 0
+    gc_unint_non_io_dur: 0
+    gc_int_dur: 0
+    gc_ts: 39041818011
+    tid: 3185
+    pid: 3162
+    gc_monotonic_dur: 666169475
+  }
+  gc_events {
+    thread_name: "HeapTaskDaemon"
+    process_name: "com.google.android.apps.nexuslauncher"
+    gc_type: "full"
+    is_mark_compact: 1
+    reclaimed_mb: 10.640000
+    min_heap_mb: 6.861000
+    max_heap_mb: 17.501000
+    mb_per_ms_of_running_gc: 93.68918788028213
+    mb_per_ms_of_wall_gc: 0.18073523433053545
+    gc_dur: 58870646
+    gc_running_dur: 113567
+    gc_runnable_dur: 0
+    gc_unint_io_dur: 0
+    gc_unint_non_io_dur: 0
+    gc_int_dur: 0
+    gc_ts: 39849615741
+    tid: 2534
+    pid: 2523
+    gc_monotonic_dur: 58870646
+  }
+  gc_events {
+    thread_name: "HeapTaskDaemon"
+    process_name: "com.google.android.apps.wellbeing"
+    gc_type: "full"
+    is_mark_compact: 1
+    gc_dur: 168393585
+    gc_running_dur: 1714233
+    gc_runnable_dur: 28482219
+    gc_unint_io_dur: 0
+    gc_unint_non_io_dur: 0
+    gc_int_dur: -1
+    gc_ts: 40022150450
+    tid: 2917
+    pid: 2909
+    gc_monotonic_dur: 168393585
+  }
+  gc_events {
+    thread_name: "HeapTaskDaemon"
+    process_name: "com.google.android.euicc"
+    gc_type: "full"
+    is_mark_compact: 1
+    gc_dur: 148630605
+    gc_running_dur: 4148763
+    gc_runnable_dur: -1
+    gc_unint_io_dur: 0
+    gc_unint_non_io_dur: 0
+    gc_int_dur: 0
+    gc_ts: 40041913430
+    tid: 2865
+    pid: 2855
+    gc_monotonic_dur: 148630605
+  }
+}
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index cb0c0a2..f5de58f 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -229,4 +229,10 @@
       trace=DataPath('android_postboot_unlock.pftrace'),
       query=Metric("android_app_process_starts"),
       out=Path('android_app_process_starts.out')
-    )
\ No newline at end of file
+    )
+
+  def test_android_garbage_collection(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_postboot_unlock.pftrace'),
+        query=Metric('android_garbage_collection_unagg'),
+        out=Path('android_garbage_collection_unagg.out'))
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/stdlib/sched/tests.py b/test/trace_processor/diff_tests/stdlib/sched/tests.py
index ee96746..443d049 100644
--- a/test/trace_processor/diff_tests/stdlib/sched/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/sched/tests.py
@@ -45,6 +45,7 @@
         trace=Path('../../common/synth_1.py'),
         query="""
       INCLUDE PERFETTO MODULE sched.thread_level_parallelism;
+      
       SELECT * FROM sched_active_cpu_count;
       """,
         out=Csv("""
@@ -58,3 +59,105 @@
       250,2
       390,2
       """))
+
+  def test_sched_utilization_per_second(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.utilization.system;
+
+        SELECT * FROM sched_utilization_per_second;
+        """,
+        out=Csv("""
+        "ts","utilization","unnormalized_utilization"
+        70000000000,0.004545,0.036362
+        71000000000,0.022596,0.180764
+        72000000000,0.163393,1.307146
+        73000000000,0.452122,3.616972
+        74000000000,0.525557,4.204453
+        75000000000,0.388632,3.109057
+        76000000000,0.425447,3.403579
+        77000000000,0.201112,1.608896
+        78000000000,0.280247,2.241977
+        79000000000,0.345228,2.761827
+        80000000000,0.303258,2.426064
+        81000000000,0.487522,3.900172
+        82000000000,0.080542,0.644336
+        83000000000,0.362450,2.899601
+        84000000000,0.076438,0.611501
+        85000000000,0.110689,0.885514
+        86000000000,0.681488,5.451901
+        87000000000,0.808331,6.466652
+        88000000000,0.941768,7.534142
+        89000000000,0.480556,3.844446
+        90000000000,0.453268,3.626142
+        91000000000,0.280310,2.242478
+        92000000000,0.006381,0.051049
+        93000000000,0.030991,0.247932
+        94000000000,0.031981,0.255845
+        95000000000,0.027931,0.223446
+        96000000000,0.063066,0.504529
+        97000000000,0.023847,0.190773
+        98000000000,0.011291,0.090328
+        99000000000,0.024065,0.192518
+        100000000000,0.001964,0.015711
+        """))
+
+  def test_sched_process_utilization_per_second(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.utilization.process;
+
+        SELECT * 
+        FROM sched_process_utilization_per_second(10);
+        """,
+        out=Csv("""
+        "ts","utilization","unnormalized_utilization"
+        72000000000,0.000187,0.001495
+        73000000000,0.000182,0.001460
+        77000000000,0.000072,0.000579
+        78000000000,0.000275,0.002204
+        82000000000,0.000300,0.002404
+        83000000000,0.000004,0.000034
+        87000000000,0.000133,0.001065
+        88000000000,0.000052,0.000416
+        89000000000,0.000212,0.001697
+        92000000000,0.000207,0.001658
+        97000000000,0.000353,0.002823
+        """))
+
+  def test_sched_thread_utilization_per_second(self):
+    return DiffTestBlueprint(
+        trace=DataPath('example_android_trace_30s.pb'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.utilization.thread;
+
+        SELECT * 
+        FROM sched_thread_utilization_per_second(10);
+        """,
+        out=Csv("""
+        "ts","utilization","unnormalized_utilization"
+        70000000000,0.000024,0.000195
+        72000000000,0.000025,0.000200
+        73000000000,0.000053,0.000420
+        74000000000,0.000044,0.000352
+        75000000000,0.000058,0.000461
+        76000000000,0.000075,0.000603
+        77000000000,0.000051,0.000407
+        78000000000,0.000047,0.000374
+        79000000000,0.000049,0.000396
+        80000000000,0.000084,0.000673
+        81000000000,0.000041,0.000329
+        82000000000,0.000048,0.000383
+        83000000000,0.000040,0.000323
+        84000000000,0.000018,0.000145
+        85000000000,0.000053,0.000421
+        86000000000,0.000121,0.000972
+        87000000000,0.000049,0.000392
+        88000000000,0.000036,0.000285
+        89000000000,0.000033,0.000266
+        90000000000,0.000050,0.000401
+        91000000000,0.000025,0.000201
+        92000000000,0.000009,0.000071
+        """))
diff --git a/tools/open_trace_in_ui b/tools/open_trace_in_ui
index 0bf8ea7..289b44a 100755
--- a/tools/open_trace_in_ui
+++ b/tools/open_trace_in_ui
@@ -41,14 +41,14 @@
 
   def do_GET(self):
     if self.path != '/' + self.server.expected_fname:
-      self.send_error(404, "File not found")
+      self.send_error(404, 'File not found')
       return
 
     self.server.fname_get_completed = True
     super().do_GET()
 
   def do_POST(self):
-    self.send_error(404, "File not found")
+    self.send_error(404, 'File not found')
 
 
 def prt(msg, colors=ANSI.END):
@@ -77,23 +77,31 @@
 
 
 def main():
-  examples = '\n'.join(
-      [ANSI.BOLD + 'Usage:' + ANSI.END, '  -i path/trace_file_name [-n]'])
+  examples = '\n'.join([
+      ANSI.BOLD + 'Examples:' + ANSI.END,
+      '  tools/open_trace_in_ui trace.pftrace',
+  ])
   parser = argparse.ArgumentParser(
       epilog=examples, formatter_class=argparse.RawTextHelpFormatter)
 
-  help = 'Input trace filename'
-  parser.add_argument('-i', '--trace', help=help)
+  parser.add_argument('positional_trace', metavar='trace', nargs='?')
   parser.add_argument(
       '-n', '--no-open-browser', action='store_true', default=False)
   parser.add_argument('--origin', default='https://ui.perfetto.dev')
+  parser.add_argument(
+      '-i', '--trace', help='input filename (overrides positional argument)')
 
   args = parser.parse_args()
-  trace_file = args.trace
   open_browser = not args.no_open_browser
 
+  trace_file = None
+  if args.positional_trace is not None:
+    trace_file = args.positional_trace
+  if args.trace is not None:
+    trace_file = args.trace
+
   if trace_file is None:
-    prt('Please specify trace file name with -i/--trace argument', ANSI.RED)
+    prt('Please specify trace file name', ANSI.RED)
     sys.exit(1)
   elif not os.path.exists(trace_file):
     prt('%s not found ' % trace_file, ANSI.RED)
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 7ce3c1b..b1943e4 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-@use "sass:math";
-
 @import "widgets/theme";
 @import "typefaces";
 @import "fonts";
@@ -45,17 +43,6 @@
   content: $content;
 }
 
-@mixin track_shell_title() {
-  font-size: 14px;
-  max-height: 30px;
-  overflow: hidden;
-  text-align: left;
-  overflow-wrap: break-word;
-  font-family: "Roboto Condensed", sans-serif;
-  font-weight: 300;
-  line-break: anywhere;
-}
-
 * {
   box-sizing: border-box;
   -webkit-tap-highlight-color: transparent;
@@ -159,18 +146,6 @@
 
 .page {
   grid-area: page;
-  position: relative;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-
-.split-panel {
-  flex: 1;
-  display: flex;
-  flex-flow: row;
-  position: relative;
-  overflow: hidden;
 }
 
 .alerts {
@@ -377,169 +352,6 @@
   }
 }
 
-.track-content.pf-track-content-error {
-  // Necessary trig because we have a 45deg stripes
-  $pattern-density: 1px * math.sqrt(2);
-  $pattern-col: #ddd;
-
-  // box-shadow: inset 0 0 0 5px red;
-  background: repeating-linear-gradient(
-    -45deg,
-    $pattern-col,
-    $pattern-col $pattern-density,
-    white $pattern-density,
-    white $pattern-density * 2
-  );
-}
-
-.track {
-  display: grid;
-  grid-template-columns: auto 1fr;
-  grid-template-rows: 1fr 0;
-
-  &::after {
-    display: block;
-    content: "";
-    grid-column: 1 / span 2;
-    border-top: 1px solid var(--track-border-color);
-    margin-top: -1px;
-    z-index: 2;
-  }
-
-  .track-shell {
-    @include transition();
-    padding-left: 10px;
-    display: grid;
-    cursor: grab;
-    grid-template-areas: "title buttons";
-    grid-template-columns: 1fr auto;
-    align-items: center;
-    width: var(--track-shell-width);
-    background: #fff;
-    border-right: 1px solid #c7d0db;
-    overflow: hidden;
-
-    &.drag {
-      background-color: #eee;
-      box-shadow: 0 4px 12px -4px #999 inset;
-    }
-    &.drop-before {
-      box-shadow: 0 4px 2px -1px hsl(213, 40%, 50%) inset;
-    }
-    &.drop-after {
-      box-shadow: 0 -4px 2px -1px hsl(213, 40%, 50%) inset;
-    }
-
-    &.selected {
-      background-color: #ebeef9;
-    }
-
-    &.alternating-thread-track {
-      background: hsl(214, 22%, 95%);
-    }
-
-    .chip {
-      background-color: #bed6ff;
-      border-radius: $pf-border-radius;
-      font-size: smaller;
-      padding: 0 0.1rem;
-      margin-left: 1ch;
-    }
-
-    h1 {
-      grid-area: title;
-      color: hsl(213, 22%, 30%);
-      @include track_shell_title();
-    }
-    .track-buttons {
-      grid-area: buttons;
-      display: flex;
-      height: 100%;
-      align-items: center;
-    }
-    .track-button {
-      @include transition();
-      color: rgb(60, 86, 136);
-      cursor: pointer;
-      width: 22px;
-      font-size: 18px;
-      opacity: 0;
-    }
-
-    .track-button.show {
-      opacity: 1;
-    }
-    .track-button.full-height {
-      display: flex;
-      height: 100%;
-      align-items: center;
-      justify-content: center;
-
-      &:hover {
-        background-color: #ebeef9;
-      }
-    }
-
-    &:hover .track-button {
-      opacity: 1;
-    }
-    &.flash {
-      background-color: #ffe263;
-    }
-  }
-}
-
-.pinned-panel-container {
-  max-height: 50%;
-  box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
-  z-index: 1;
-  flex-grow: 0;
-  flex-shrink: 0;
-  overflow: hidden;
-  overflow-y: auto;
-}
-
-.scrolling-panel-container {
-  overflow-x: hidden;
-  overflow-y: auto;
-  flex: 1 1 auto;
-  will-change: transform; // Force layer creation.
-}
-
-.details-panel-container {
-  box-shadow: #0000003b 0px 0px 3px 1px;
-  overflow: auto;
-}
-
-.header-panel-container {
-  overflow: visible;
-  box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
-  z-index: 2;
-}
-
-.pan-and-zoom-content {
-  flex: 1;
-  position: relative;
-  display: flex;
-  flex-flow: column nowrap;
-}
-
-.overview-timeline {
-  height: 70px;
-}
-
-.time-axis-panel {
-  height: 22px;
-}
-
-.tickbar {
-  height: 5px;
-}
-
-.notes-panel {
-  height: 20px;
-}
-
 .x-scrollable {
   overflow-x: auto;
 }
@@ -594,15 +406,6 @@
   margin: auto 0 auto 1rem;
 }
 
-.debug-panel-border {
-  position: absolute;
-  top: 0;
-  height: 100%;
-  width: 100%;
-  border: 1px solid rgba(69, 187, 73, 0.5);
-  pointer-events: none;
-}
-
 .perf-stats {
   --stroke-color: hsl(217, 39%, 94%);
   position: fixed;
@@ -649,117 +452,6 @@
   }
 }
 
-.track-group-panel {
-  --collapsed-transparent: hsla(190, 49%, 97%, 0);
-  --expanded-transparent: hsl(215, 22%, 19%, 0);
-  display: grid;
-  grid-template-columns: auto 1fr;
-  grid-template-rows: 1fr;
-  transition: background-color 0.4s, color 0.4s;
-  height: 40px;
-  &::after {
-    display: block;
-    content: "";
-    grid-column: 1 / span 2;
-    border-top: 1px solid var(--track-border-color);
-    margin-top: -1px;
-  }
-  &[collapsed="true"] {
-    background-color: var(--collapsed-transparent);
-    .shell {
-      border-right: 1px solid #c7d0db;
-      background-color: var(--collapsed-background);
-    }
-    .track-button {
-      color: rgb(60, 86, 136);
-    }
-  }
-  &[collapsed="false"] {
-    background-color: var(--expanded-transparent);
-    color: white;
-    font-weight: bold;
-    .shell.flash {
-      color: #121212;
-    }
-    .track-button {
-      color: white;
-    }
-    span.chip {
-      color: #121212;
-    }
-  }
-  .shell {
-    padding: 4px 4px;
-    display: grid;
-    grid-template-areas: "fold-button title buttons check";
-    grid-template-columns: 28px 1fr auto 20px;
-    align-items: center;
-    line-height: 1;
-    width: var(--track-shell-width);
-    min-height: 40px;
-    transition: background-color 0.4s;
-
-    .track-title {
-      user-select: text;
-    }
-
-    .track-subtitle {
-      font-size: 0.6rem;
-      font-weight: normal;
-      overflow: hidden;
-      white-space: nowrap;
-      text-overflow: ellipsis;
-      // Maximum width according to grid-template-columns value for .shell
-      width: calc(var(--track-shell-width) - 56px);
-    }
-
-    .chip {
-      background-color: #bed6ff;
-      border-radius: 3px;
-      font-size: smaller;
-      padding: 0 0.1rem;
-      margin-left: 1ch;
-    }
-
-    .title-wrapper {
-      grid-area: title;
-      overflow: hidden;
-    }
-    h1 {
-      @include track_shell_title();
-    }
-    .fold-button {
-      grid-area: fold-button;
-    }
-    .track-button {
-      font-size: 20px;
-    }
-    &:hover {
-      cursor: pointer;
-      .fold-button {
-        color: hsl(45, 100%, 48%);
-      }
-    }
-    &.flash {
-      background-color: #ffe263;
-    }
-    &.selected {
-      background-color: #ebeef9;
-    }
-  }
-  .track-content {
-    display: grid;
-    span {
-      @include track_shell_title();
-      align-self: center;
-    }
-  }
-}
-
-.time-selection-panel {
-  height: 10px;
-}
-
 .cookie-consent {
   position: absolute;
   z-index: 10;
diff --git a/ui/src/assets/panel_container.scss b/ui/src/assets/panel_container.scss
index 9790dd0..6163353 100644
--- a/ui/src/assets/panel_container.scss
+++ b/ui/src/assets/panel_container.scss
@@ -12,42 +12,39 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-.panel-container {
-  position: relative;
-  // display: grid;
-  // grid-template-columns: 1fr;
-  // grid-template-rows: 1fr;
-  // grid-template-areas: "space";
-}
-
-// In the scrolling case, since the canvas is overdrawn and continuously
-// repositioned, we need the canvas to be in a div with overflow hidden and
-// height equaling the total height of the content to prevent scrolling
-// height from growing.
-.scroll-limiter {
-  position: absolute;
-  top: 0;
-  left: 0;
-  overflow: hidden;
-  height: 100%;
-}
-
-canvas.main-canvas {
-  z-index: -1;
-}
-
-.panels {
-  position: relative;
+.pf-panel-container {
+  // We need to drag over this element for various reasons, so just disable
+  // selection over the entire thing.
+  // TODO(stevegolton): If we enable this, we can get scrolling while dragging,
+  // so we might want to enable this here and disable selection in titles
+  // instead.
   user-select: none;
-}
 
-.panel {
-  position: relative; // Otherwise canvas covers panel dom.
+  .pf-panels {
+    // Make this a positioned element so .pf-scroll-limiter is positioned
+    // relative to this element.
+    position: relative;
 
-  &.sticky {
-    position: sticky;
-    z-index: 3;
-    top: 0;
-    background-color: hsl(215, 22%, 19%);
+    // In the scrolling case, since the canvas is overdrawn and continuously
+    // repositioned, we need the canvas to be in a div with overflow hidden and
+    // height equalling the total height of the content to prevent scrolling
+    // height from growing.
+    .pf-scroll-limiter {
+      position: absolute;
+      top: 0;
+      left: 0;
+      bottom: 0;
+      overflow: hidden;
+
+      // Make this overlay invisible to pointer events.
+      pointer-events: none;
+    }
+
+    .pf-panel {
+      &.pf-sticky {
+        position: sticky;
+        top: 0;
+      }
+    }
   }
 }
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 0cf20b8..8bb1cdc 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -15,6 +15,8 @@
 @import "typefaces";
 @import "common";
 @import "panel_container";
+@import "viewer_page";
+@import "track_panel";
 @import "home_page";
 @import "query_page";
 @import "metrics_page";
diff --git a/ui/src/assets/track_panel.scss b/ui/src/assets/track_panel.scss
new file mode 100644
index 0000000..0091fba
--- /dev/null
+++ b/ui/src/assets/track_panel.scss
@@ -0,0 +1,235 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@use "sass:math";
+
+@mixin track_shell_title() {
+  font-size: 14px;
+  max-height: 30px;
+  overflow: hidden;
+  text-align: left;
+  overflow-wrap: break-word;
+  font-family: "Roboto Condensed", sans-serif;
+  font-weight: 300;
+  line-break: anywhere;
+}
+
+.track-content.pf-track-content-error {
+  // Necessary trig because we have a 45deg stripes
+  $pattern-density: 1px * math.sqrt(2);
+  $pattern-col: #ddd;
+
+  // box-shadow: inset 0 0 0 5px red;
+  background: repeating-linear-gradient(
+    -45deg,
+    $pattern-col,
+    $pattern-col $pattern-density,
+    white $pattern-density,
+    white $pattern-density * 2
+  );
+}
+
+.track {
+  display: grid;
+  grid-template-columns: auto 1fr;
+  grid-template-rows: 1fr 0;
+
+  &::after {
+    display: block;
+    content: "";
+    grid-column: 1 / span 2;
+    border-top: 1px solid var(--track-border-color);
+    margin-top: -1px;
+    z-index: 2;
+  }
+
+  .track-shell {
+    @include transition();
+    padding-left: 10px;
+    display: grid;
+    cursor: grab;
+    grid-template-areas: "title buttons";
+    grid-template-columns: 1fr auto;
+    align-items: center;
+    width: var(--track-shell-width);
+    border-right: 1px solid #c7d0db;
+    overflow: hidden;
+
+    &.drag {
+      background-color: #eee;
+      box-shadow: 0 4px 12px -4px #999 inset;
+    }
+    &.drop-before {
+      box-shadow: 0 4px 2px -1px hsl(213, 40%, 50%) inset;
+    }
+    &.drop-after {
+      box-shadow: 0 -4px 2px -1px hsl(213, 40%, 50%) inset;
+    }
+
+    &.selected {
+      background-color: #ebeef9;
+    }
+
+    .chip {
+      background-color: #bed6ff;
+      border-radius: $pf-border-radius;
+      font-size: smaller;
+      padding: 0 0.1rem;
+      margin-left: 1ch;
+    }
+
+    h1 {
+      grid-area: title;
+      color: hsl(213, 22%, 30%);
+      @include track_shell_title();
+    }
+    .track-buttons {
+      grid-area: buttons;
+      display: flex;
+      height: 100%;
+      align-items: center;
+    }
+    .track-button {
+      @include transition();
+      color: rgb(60, 86, 136);
+      cursor: pointer;
+      width: 22px;
+      font-size: 18px;
+      opacity: 0;
+    }
+
+    .track-button.show {
+      opacity: 1;
+    }
+    .track-button.full-height {
+      display: flex;
+      height: 100%;
+      align-items: center;
+      justify-content: center;
+
+      &:hover {
+        background-color: #ebeef9;
+      }
+    }
+
+    &:hover .track-button {
+      opacity: 1;
+    }
+    &.flash {
+      background-color: #ffe263;
+    }
+  }
+}
+
+.track-group-panel {
+  display: grid;
+  grid-template-columns: auto 1fr;
+  grid-template-rows: 1fr;
+  height: 40px;
+  &::after {
+    display: block;
+    content: "";
+    grid-column: 1 / span 2;
+    border-top: 1px solid var(--track-border-color);
+    margin-top: -1px;
+  }
+  &[collapsed="true"] {
+    background-color: var(--collapsed-background);
+    .shell {
+      border-right: 1px solid #c7d0db;
+    }
+    .track-button {
+      color: rgb(60, 86, 136);
+    }
+  }
+  &[collapsed="false"] {
+    background-color: var(--expanded-background);
+    color: white;
+    font-weight: bold;
+    .shell.flash {
+      color: #121212;
+    }
+    .track-button {
+      color: white;
+    }
+    span.chip {
+      color: #121212;
+    }
+  }
+  .shell {
+    padding: 4px 4px;
+    display: grid;
+    grid-template-areas: "fold-button title buttons check";
+    grid-template-columns: 28px 1fr auto 20px;
+    align-items: center;
+    line-height: 1;
+    width: var(--track-shell-width);
+    min-height: 40px;
+
+    .track-title {
+      user-select: text;
+    }
+
+    .track-subtitle {
+      font-size: 0.6rem;
+      font-weight: normal;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      // Maximum width according to grid-template-columns value for .shell
+      width: calc(var(--track-shell-width) - 56px);
+    }
+
+    .chip {
+      background-color: #bed6ff;
+      border-radius: 3px;
+      font-size: smaller;
+      padding: 0 0.1rem;
+      margin-left: 1ch;
+    }
+
+    .title-wrapper {
+      grid-area: title;
+      overflow: hidden;
+    }
+    h1 {
+      @include track_shell_title();
+    }
+    .fold-button {
+      grid-area: fold-button;
+    }
+    .track-button {
+      font-size: 20px;
+    }
+    &:hover {
+      cursor: pointer;
+      .fold-button {
+        color: hsl(45, 100%, 48%);
+      }
+    }
+    &.flash {
+      background-color: #ffe263;
+    }
+    &.selected {
+      background-color: #ebeef9;
+    }
+  }
+  .track-content {
+    display: grid;
+    span {
+      @include track_shell_title();
+      align-self: center;
+    }
+  }
+}
diff --git a/ui/src/assets/viewer_page.scss b/ui/src/assets/viewer_page.scss
new file mode 100644
index 0000000..3fdd737
--- /dev/null
+++ b/ui/src/assets/viewer_page.scss
@@ -0,0 +1,76 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+.viewer-page {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+
+  .pinned-panel-container {
+    max-height: 50%;
+    box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
+    z-index: 1;
+    flex-grow: 0;
+    flex-shrink: 0;
+    overflow: hidden;
+    overflow-y: auto;
+  }
+
+  .scrolling-panel-container {
+    overflow-x: hidden;
+    overflow-y: auto;
+    flex: 1 1 auto;
+    will-change: transform; // Force layer creation.
+  }
+
+  .details-panel-container {
+    box-shadow: #0000003b 0px 0px 3px 1px;
+    overflow: auto;
+  }
+
+  .header-panel-container {
+    overflow: visible;
+    box-shadow: 1px 3px 15px rgba(23, 32, 44, 0.3);
+    z-index: 2;
+  }
+
+  .pan-and-zoom-content {
+    flex: 1;
+    position: relative;
+    display: flex;
+    flex-flow: column nowrap;
+    overflow: hidden;
+  }
+
+  .overview-timeline {
+    height: 70px;
+  }
+
+  .time-axis-panel {
+    height: 22px;
+  }
+
+  .tickbar {
+    height: 5px;
+  }
+
+  .notes-panel {
+    height: 20px;
+  }
+
+  .time-selection-panel {
+    height: 10px;
+  }
+}
diff --git a/ui/src/base/classnames.ts b/ui/src/base/classnames.ts
index b2f6007..d69a244 100644
--- a/ui/src/base/classnames.ts
+++ b/ui/src/base/classnames.ts
@@ -18,8 +18,14 @@
 type ArgType = string|false|undefined|ArgType[];
 
 // Join class names together into valid HTML class attributes
-// Falsey elements are ignored
+// Falsy elements are ignored
 // Nested arrays are flattened
-export function classNames(...args: ArgType[]): string {
-  return args.flat().filter((x) => x).join(' ');
+// If all elements are falsy, returns undefined
+export function classNames(...args: ArgType[]): string|undefined {
+  const filtered = args.flat().filter((x) => x);
+  if (filtered.length === 0) {
+    return undefined;
+  } else {
+    return filtered.join(' ');
+  }
 }
diff --git a/ui/src/base/dom_utils.ts b/ui/src/base/dom_utils.ts
index 5caf9d9..1c20a34 100644
--- a/ui/src/base/dom_utils.ts
+++ b/ui/src/base/dom_utils.ts
@@ -31,7 +31,7 @@
 // Throws if the element is not an HTMLElement.
 export function toHTMLElement(el: Element): HTMLElement {
   if (!(el instanceof HTMLElement)) {
-    throw new Error('Element is not an HTLMElement');
+    throw new Error('Element is not an HTMLElement');
   }
   return el as HTMLElement;
 }
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
index e0111f9..3d94ee4 100644
--- a/ui/src/common/canvas_utils.ts
+++ b/ui/src/common/canvas_utils.ts
@@ -175,3 +175,14 @@
     ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
   }
 }
+
+export function canvasClip(
+  ctx: CanvasRenderingContext2D,
+  x: number,
+  y: number,
+  w: number,
+  h: number): void {
+  ctx.beginPath();
+  ctx.rect(x, y, w, h);
+  ctx.clip();
+}
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 5efacf7..1690267 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -67,7 +67,7 @@
 
   constructor(readonly key: string) {}
 
-  get mithril(): m.Children {
+  render(): m.Children {
     const allCollapsed = Object.values(globals.state.trackGroups)
       .every((group) => group.collapsed);
 
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 0accdb8..b1cc984 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -94,7 +94,7 @@
       .removeEventListener('mousemove', this.boundOnMouseMove);
   }
 
-  get mithril(): m.Children {
+  render(): m.Children {
     return m('.overview-timeline', {
       oncreate: (vnode) => this.oncreate(vnode),
       onupdate: (vnode) => this.onupdate(vnode),
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 9b19db0..52ecf86 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -41,6 +41,7 @@
 } from './flow_events_renderer';
 import {globals} from './globals';
 import {PanelSize} from './panel';
+import {canvasClip} from '../common/canvas_utils';
 
 // If the panel container scrolls, the backing canvas height is
 // SCROLLING_CANVAS_OVERDRAW_FACTOR * parent container height.
@@ -48,7 +49,7 @@
 
 export interface Panel {
   kind: 'panel';
-  mithril: m.Children;
+  render(): m.Children;
   selectable: boolean;
   key: string;
   trackKey?: string;
@@ -67,7 +68,7 @@
 
 export type PanelOrGroup = Panel|PanelGroup;
 
-export interface Attrs {
+export interface PanelContainerAttrs {
   panels: PanelOrGroup[];
   doesScroll: boolean;
   kind: 'TRACKS'|'OVERVIEW';
@@ -83,7 +84,7 @@
   y: number;
 }
 
-export class PanelContainer implements m.ClassComponent<Attrs>,
+export class PanelContainer implements m.ClassComponent<PanelContainerAttrs>,
                                        PerfStatsSource {
   // These values are updated with proper values in oncreate.
   private parentWidth = 0;
@@ -108,7 +109,7 @@
   // Attrs received in the most recent mithril redraw. We receive a new vnode
   // with new attrs on every redraw, and we cache it here so that resize
   // listeners and canvas redraw callbacks can access it.
-  private attrs: Attrs;
+  private attrs: PanelContainerAttrs;
 
   private ctx?: CanvasRenderingContext2D;
 
@@ -116,6 +117,7 @@
 
   private readonly SCROLL_LIMITER_REF = 'scroll-limiter';
   private readonly PANELS_REF = 'panels';
+  private readonly OVERLAY_CANVAS_REF = 'canvas';
 
   get canvasOverdrawFactor() {
     return this.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
@@ -188,12 +190,12 @@
     globals.timeline.selectArea(area.start, area.end, tracks);
   }
 
-  constructor(vnode: m.CVnode<Attrs>) {
+  constructor(vnode: m.CVnode<PanelContainerAttrs>) {
     this.attrs = vnode.attrs;
     this.flowEventsRenderer = new FlowEventsRenderer();
     this.trash = new Trash();
 
-    const onRedraw = () => this.redrawCanvas();
+    const onRedraw = () => this.renderCanvas();
     raf.addRedrawCallback(onRedraw);
     this.trash.addCallback(() => {
       raf.removeRedrawCallback(onRedraw);
@@ -205,9 +207,9 @@
     });
   }
 
-  oncreate({dom}: m.CVnodeDOM<Attrs>) {
+  oncreate({dom}: m.CVnodeDOM<PanelContainerAttrs>) {
     // Save the canvas context in the state.
-    const canvas = dom.querySelector('.main-canvas') as HTMLCanvasElement;
+    const canvas = findRef(dom, this.OVERLAY_CANVAS_REF) as HTMLCanvasElement;
     const ctx = canvas.getContext('2d');
     if (!ctx) {
       throw Error('Cannot create canvas context');
@@ -226,7 +228,7 @@
       if (parentSizeChanged) {
         this.updateCanvasDimensions();
         this.repositionCanvas();
-        this.redrawCanvas();
+        this.renderCanvas();
       }
     }));
 
@@ -252,12 +254,7 @@
   renderPanel(node: Panel, key: string, extraClass = ''): m.Vnode {
     assertFalse(this.panelByKey.has(key));
     this.panelByKey.set(key, node);
-    const mithril = node.mithril;
-
-    return m(`.panel${extraClass}`, {key, 'data-key': key},
-      perfDebug() ?
-        [mithril, m('.debug-panel-border')] :
-        mithril);
+    return m(`.pf-panel${extraClass}`, {key, 'data-key': key}, node.render());
   }
 
   // Render a tree of panels into one vnode. Argument `path` is used to build
@@ -269,30 +266,30 @@
         'div',
         {key: path},
         this.renderPanel(
-          node.header, `${path}-header`, node.collapsed ? '' : '.sticky'),
+          node.header, `${path}-header`, node.collapsed ? '' : '.pf-sticky'),
         ...node.childTracks.map(
           (child, index) => this.renderTree(child, `${path}-${index}`)));
     }
     return this.renderPanel(node, assertExists(node.key));
   }
 
-  view({attrs}: m.CVnode<Attrs>) {
+  view({attrs}: m.CVnode<PanelContainerAttrs>) {
     this.attrs = attrs;
     this.panelByKey.clear();
     const children = attrs.panels.map(
       (panel, index) => this.renderTree(panel, `track-tree-${index}`));
 
-    return m('.panel-container', {className: attrs.className},
-      m('.panels', {ref: this.PANELS_REF},
-        m('.scroll-limiter', {ref: this.SCROLL_LIMITER_REF},
-          m('canvas.main-canvas'),
+    return m('.pf-panel-container', {className: attrs.className},
+      m('.pf-panels', {ref: this.PANELS_REF},
+        m('.pf-scroll-limiter', {ref: this.SCROLL_LIMITER_REF},
+          m('canvas.pf-overlay-canvas', {ref: this.OVERLAY_CANVAS_REF}),
         ),
         children,
       ),
     );
   }
 
-  onupdate({dom}: m.CVnodeDOM<Attrs>) {
+  onupdate({dom}: m.CVnodeDOM<PanelContainerAttrs>) {
     const totalPanelHeightChanged = this.readPanelHeightsFromDom(dom);
     const parentSizeChanged = this.readParentSizeFromDom(dom);
     const canvasSizeShouldChange =
@@ -304,7 +301,7 @@
         globals.timeline.updateLocalLimits(
           0, this.parentWidth - TRACK_SHELL_WIDTH);
       }
-      this.redrawCanvas();
+      this.renderCanvas();
     }
   }
 
@@ -364,7 +361,7 @@
     this.panelContainerTop = domRect.y;
     this.panelContainerHeight = domRect.height;
 
-    dom.querySelectorAll('.panel').forEach((panelElement) => {
+    dom.querySelectorAll('.pf-panel').forEach((panelElement) => {
       const key = assertExists(panelElement.getAttribute('data-key'));
       const panel = assertExists(this.panelByKey.get(key));
 
@@ -389,7 +386,7 @@
     return yEnd > 0 && yStart < this.canvasHeight;
   }
 
-  private redrawCanvas() {
+  private renderCanvas() {
     const redrawStart = debugNow();
     if (!this.ctx) return;
     this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
@@ -480,6 +477,12 @@
     const canvasYStart =
         Math.floor(this.scrollTop - this.getCanvasOverdrawHeightPerSide());
     this.ctx.translate(TRACK_SHELL_WIDTH, -canvasYStart);
+
+    // Clip off any drawing happening outside the bounds of the timeline area
+    canvasClip(
+      this.ctx,
+      0, 0, this.parentWidth - TRACK_SHELL_WIDTH, this.totalPanelHeight);
+
     this.ctx.strokeRect(
       startX,
       selectedTracksMaxY,
@@ -499,6 +502,16 @@
     }
     renderStats.addValue(renderTime);
 
+    // Draw a green box around the whole panel
+    ctx.strokeStyle = 'rgba(69, 187, 73, 0.5)';
+    const lineWidth = 1;
+    ctx.lineWidth = lineWidth;
+    ctx.strokeRect(
+      lineWidth/2,
+      lineWidth/2,
+      size.width - lineWidth,
+      size.height - lineWidth);
+
     const statW = 300;
     ctx.fillStyle = 'hsl(97, 100%, 96%)';
     ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 7600d05..2ed6249 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -35,7 +35,7 @@
 
   constructor(readonly key: string) {}
 
-  get mithril(): m.Children {
+  render(): m.Children {
     return m('.tickbar');
   }
 
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 43e7244..ec918b8 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -40,7 +40,7 @@
 
   constructor(readonly key: string) {}
 
-  get mithril() {
+  render(): m.Children {
     return m('.time-axis-panel');
   }
 
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 67358b3..ee7838e 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -138,7 +138,7 @@
 
   constructor(readonly key: string) {}
 
-  get mithril(): m.Children {
+  render(): m.Children {
     return m('.time-selection-panel');
   }
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 58071ce..3a83c81 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {hex} from 'color-convert';
 import m from 'mithril';
 
 import {Icons} from '../base/semantic_icons';
@@ -24,18 +23,14 @@
 import {TrackTags} from '../public';
 
 import {
-  COLLAPSED_BACKGROUND,
-  EXPANDED_BACKGROUND,
   TRACK_SHELL_WIDTH,
 } from './css_constants';
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
 import {PanelSize} from './panel';
 import {Panel} from './panel_container';
-import {CrashButton, renderChips, TrackContent} from './track_panel';
-import {
-  drawVerticalLineAtTime,
-} from './vertical_line_helper';
+import {CrashButton, renderChips, renderHoveredCursorVertical, renderHoveredNoteVertical, renderNoteVerticals, renderWakeupVertical, TrackContent} from './track_panel';
+import {canvasClip} from '../common/canvas_utils';
 
 interface Attrs {
   trackGroupId: string;
@@ -58,7 +53,7 @@
     this.key = attrs.key;
   }
 
-  get mithril(): m.Children {
+  render(): m.Children {
     const {
       trackGroupId,
       title,
@@ -187,86 +182,37 @@
       trackFSM: track,
     } = this.attrs;
 
-    ctx.fillStyle = collapsed ? COLLAPSED_BACKGROUND : EXPANDED_BACKGROUND;
-    ctx.fillRect(0, 0, size.width, size.height);
-
     if (!collapsed) return;
 
-    this.highlightIfTrackSelected(ctx, size);
-
+    ctx.save();
+    canvasClip(
+      ctx, TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
     drawGridLines(
       ctx,
       size.width,
       size.height);
 
-    ctx.save();
-    ctx.translate(TRACK_SHELL_WIDTH, 0);
     if (track) {
+      ctx.save();
+      ctx.translate(TRACK_SHELL_WIDTH, 0);
       const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
       if (!track.getError()) {
         track.update();
         track.track.render(ctx, trackSize);
       }
+      ctx.restore();
     }
-    ctx.restore();
 
     this.highlightIfTrackSelected(ctx, size);
 
     const {visibleTimeScale} = globals.timeline;
     // Draw vertical line when hovering on the notes panel.
-    if (globals.state.hoveredNoteTimestamp !== -1n) {
-      drawVerticalLineAtTime(
-        ctx,
-        visibleTimeScale,
-        globals.state.hoveredNoteTimestamp,
-        size.height,
-        `#aaa`);
-    }
-    if (globals.state.hoverCursorTimestamp !== -1n) {
-      drawVerticalLineAtTime(
-        ctx,
-        visibleTimeScale,
-        globals.state.hoverCursorTimestamp,
-        size.height,
-        `#344596`);
-    }
+    renderHoveredNoteVertical(ctx, visibleTimeScale, size);
+    renderHoveredCursorVertical(ctx, visibleTimeScale, size);
+    renderWakeupVertical(ctx, visibleTimeScale, size);
+    renderNoteVerticals(ctx, visibleTimeScale, size);
 
-    if (globals.state.currentSelection !== null) {
-      if (globals.state.currentSelection.kind === 'SLICE' &&
-          globals.sliceDetails.wakeupTs !== undefined) {
-        drawVerticalLineAtTime(
-          ctx,
-          visibleTimeScale,
-          globals.sliceDetails.wakeupTs,
-          size.height,
-          `black`);
-      }
-    }
-    // All marked areas should have semi-transparent vertical lines
-    // marking the start and end.
-    for (const note of Object.values(globals.state.notes)) {
-      if (note.noteType === 'AREA') {
-        const transparentNoteColor =
-            'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
-        drawVerticalLineAtTime(
-          ctx,
-          visibleTimeScale,
-          globals.state.areas[note.areaId].start,
-          size.height,
-          transparentNoteColor,
-          1);
-        drawVerticalLineAtTime(
-          ctx,
-          visibleTimeScale,
-          globals.state.areas[note.areaId].end,
-          size.height,
-          transparentNoteColor,
-          1);
-      } else if (note.noteType === 'DEFAULT') {
-        drawVerticalLineAtTime(
-          ctx, visibleTimeScale, note.timestamp, size.height, note.color);
-      }
-    }
+    ctx.restore();
   }
 }
 
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 697e45a..bb0b664 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -36,6 +36,8 @@
 import {classNames} from '../base/classnames';
 import {Button} from '../widgets/button';
 import {Popup} from '../widgets/popup';
+import {canvasClip} from '../common/canvas_utils';
+import {TimeScale} from './time_scale';
 
 function getTitleSize(title: string): string|undefined {
   const length = title.length;
@@ -133,7 +135,7 @@
   view({attrs}: m.CVnode<TrackShellAttrs>) {
     // The shell should be highlighted if the current search result is inside
     // this track.
-    let highlightClass = '';
+    let highlightClass = undefined;
     const searchIndex = globals.state.searchIndex;
     if (searchIndex !== -1) {
       const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
@@ -142,12 +144,14 @@
       }
     }
 
-    const dragClass = this.dragging ? `drag` : '';
-    const dropClass = this.dropping ? `drop-${this.dropping}` : '';
     return m(
       `.track-shell[draggable=true]`,
       {
-        class: `${highlightClass} ${dragClass} ${dropClass}`,
+        className: classNames(
+          highlightClass,
+          this.dragging && 'drag',
+          this.dropping && `drop-${this.dropping}`,
+        ),
         ondragstart: (e: DragEvent) => this.ondragstart(e, attrs.trackKey),
         ondragend: this.ondragend.bind(this),
         ondragover: this.ondragover.bind(this),
@@ -421,7 +425,7 @@
     return this.attrs.trackKey;
   }
 
-  get mithril(): m.Children {
+  render(): m.Children {
     const attrs = this.attrs;
 
     if (attrs.trackFSM) {
@@ -472,6 +476,8 @@
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     ctx.save();
+    canvasClip(
+      ctx, TRACK_SHELL_WIDTH, 0, size.width - TRACK_SHELL_WIDTH, size.height);
 
     drawGridLines(
       ctx,
@@ -480,6 +486,7 @@
 
     const track = this.attrs.trackFSM;
 
+    ctx.save();
     ctx.translate(TRACK_SHELL_WIDTH, 0);
     if (track !== undefined) {
       const trackSize = {...size, width: size.width - TRACK_SHELL_WIDTH};
@@ -496,59 +503,12 @@
 
     const {visibleTimeScale} = globals.timeline;
     // Draw vertical line when hovering on the notes panel.
-    if (globals.state.hoveredNoteTimestamp !== -1n) {
-      drawVerticalLineAtTime(
-        ctx,
-        visibleTimeScale,
-        globals.state.hoveredNoteTimestamp,
-        size.height,
-        `#aaa`);
-    }
-    if (globals.state.hoverCursorTimestamp !== -1n) {
-      drawVerticalLineAtTime(
-        ctx,
-        visibleTimeScale,
-        globals.state.hoverCursorTimestamp,
-        size.height,
-        `#344596`);
-    }
+    renderHoveredNoteVertical(ctx, visibleTimeScale, size);
+    renderHoveredCursorVertical(ctx, visibleTimeScale, size);
+    renderWakeupVertical(ctx, visibleTimeScale, size);
+    renderNoteVerticals(ctx, visibleTimeScale, size);
 
-    if (globals.state.currentSelection !== null) {
-      if (globals.state.currentSelection.kind === 'SLICE' &&
-          globals.sliceDetails.wakeupTs !== undefined) {
-        drawVerticalLineAtTime(
-          ctx,
-          visibleTimeScale,
-          globals.sliceDetails.wakeupTs,
-          size.height,
-          `black`);
-      }
-    }
-    // All marked areas should have semi-transparent vertical lines
-    // marking the start and end.
-    for (const note of Object.values(globals.state.notes)) {
-      if (note.noteType === 'AREA') {
-        const transparentNoteColor =
-            'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
-        drawVerticalLineAtTime(
-          ctx,
-          visibleTimeScale,
-          globals.state.areas[note.areaId].start,
-          size.height,
-          transparentNoteColor,
-          1);
-        drawVerticalLineAtTime(
-          ctx,
-          visibleTimeScale,
-          globals.state.areas[note.areaId].end,
-          size.height,
-          transparentNoteColor,
-          1);
-      } else if (note.noteType === 'DEFAULT') {
-        drawVerticalLineAtTime(
-          ctx, visibleTimeScale, note.timestamp, size.height, note.color);
-      }
-    }
+    ctx.restore();
   }
 
   getSliceRect(tStart: time, tDur: time, depth: number): SliceRect|undefined {
@@ -558,3 +518,71 @@
     return this.attrs.trackFSM.track.getSliceRect?.(tStart, tDur, depth);
   }
 }
+
+export function renderHoveredCursorVertical(
+  ctx: CanvasRenderingContext2D, visibleTimeScale: TimeScale, size: PanelSize) {
+  if (globals.state.hoverCursorTimestamp !== -1n) {
+    drawVerticalLineAtTime(
+      ctx,
+      visibleTimeScale,
+      globals.state.hoverCursorTimestamp,
+      size.height,
+      `#344596`);
+  }
+}
+
+export function renderHoveredNoteVertical(
+  ctx: CanvasRenderingContext2D, visibleTimeScale: TimeScale, size: PanelSize) {
+  if (globals.state.hoveredNoteTimestamp !== -1n) {
+    drawVerticalLineAtTime(
+      ctx,
+      visibleTimeScale,
+      globals.state.hoveredNoteTimestamp,
+      size.height,
+      `#aaa`);
+  }
+}
+
+export function renderWakeupVertical(
+  ctx: CanvasRenderingContext2D, visibleTimeScale: TimeScale, size: PanelSize) {
+  if (globals.state.currentSelection !== null) {
+    if (globals.state.currentSelection.kind === 'SLICE' &&
+      globals.sliceDetails.wakeupTs !== undefined) {
+      drawVerticalLineAtTime(
+        ctx,
+        visibleTimeScale,
+        globals.sliceDetails.wakeupTs,
+        size.height,
+        `black`);
+    }
+  }
+}
+
+export function renderNoteVerticals(
+  ctx: CanvasRenderingContext2D, visibleTimeScale: TimeScale, size: PanelSize) {
+  // All marked areas should have semi-transparent vertical lines
+  // marking the start and end.
+  for (const note of Object.values(globals.state.notes)) {
+    if (note.noteType === 'AREA') {
+      const transparentNoteColor = 'rgba(' + hex.rgb(note.color.substr(1)).toString() + ', 0.65)';
+      drawVerticalLineAtTime(
+        ctx,
+        visibleTimeScale,
+        globals.state.areas[note.areaId].start,
+        size.height,
+        transparentNoteColor,
+        1);
+      drawVerticalLineAtTime(
+        ctx,
+        visibleTimeScale,
+        globals.state.areas[note.areaId].end,
+        size.height,
+        transparentNoteColor,
+        1);
+    } else if (note.noteType === 'DEFAULT') {
+      drawVerticalLineAtTime(
+        ctx, visibleTimeScale, note.timestamp, size.height, note.color);
+    }
+  }
+}
+
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 973fdaf..91f7490 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 
-import {getScrollbarWidth} from '../base/dom_utils';
+import {findRef, getScrollbarWidth, toHTMLElement} from '../base/dom_utils';
 import {clamp} from '../base/math_utils';
 import {Time} from '../base/time';
 import {Actions} from '../common/actions';
@@ -39,6 +39,7 @@
 import {DISMISSED_PANNING_HINT_KEY} from './topbar';
 import {TrackGroupPanel} from './track_group_panel';
 import {TrackPanel} from './track_panel';
+import {assertExists} from '../base/logging';
 
 const OVERVIEW_PANEL_FLAG = featureFlags.register({
   id: 'overviewVisible',
@@ -88,6 +89,8 @@
   private notesPanel = new NotesPanel('notes');
   private tickmarkPanel = new TickmarkPanel('searchTickmarks');
 
+  private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
+
   oncreate(vnode: m.CVnodeDOM) {
     const timeline = globals.timeline;
     const updateDimensions = () => {
@@ -107,8 +110,8 @@
     // Once ResizeObservers are out, we can stop accessing the window here.
     window.addEventListener('resize', this.onResize);
 
-    const panZoomEl =
-        vnode.dom.querySelector('.pan-and-zoom-content') as HTMLElement;
+    const panZoomElRaw = findRef(vnode.dom, this.PAN_ZOOM_CONTENT_REF);
+    const panZoomEl = toHTMLElement(assertExists(panZoomElRaw));
 
     this.zoomContent = new PanAndZoomHandler({
       element: panZoomEl,
@@ -279,53 +282,52 @@
     }
 
     const result = m(
-      '.page',
-      m('.split-panel',
-        m('.pan-and-zoom-content',
-          {
-            onclick: () => {
-              // We don't want to deselect when panning/drag selecting.
-              if (this.keepCurrentSelection) {
-                this.keepCurrentSelection = false;
-                return;
-              }
-              globals.makeSelection(Actions.deselect({}));
-            },
+      '.page.viewer-page',
+      m('.pan-and-zoom-content',
+        {
+          ref: this.PAN_ZOOM_CONTENT_REF,
+          onclick: () => {
+            // We don't want to deselect when panning/drag selecting.
+            if (this.keepCurrentSelection) {
+              this.keepCurrentSelection = false;
+              return;
+            }
+            globals.makeSelection(Actions.deselect({}));
           },
-          m(PanelContainer, {
-            className: 'header-panel-container',
-            doesScroll: false,
-            panels: [
-              ...overviewPanel,
-              this.timeAxisPanel,
-              this.timeSelectionPanel,
-              this.notesPanel,
-              this.tickmarkPanel,
-            ],
-            kind: 'OVERVIEW',
+        },
+        m(PanelContainer, {
+          className: 'header-panel-container',
+          doesScroll: false,
+          panels: [
+            ...overviewPanel,
+            this.timeAxisPanel,
+            this.timeSelectionPanel,
+            this.notesPanel,
+            this.tickmarkPanel,
+          ],
+          kind: 'OVERVIEW',
+        }),
+        m(PanelContainer, {
+          className: 'pinned-panel-container',
+          doesScroll: true,
+          panels: globals.state.pinnedTracks.map((key) => {
+            const trackBundle = this.resolveTrack(key);
+            return new TrackPanel({
+              trackKey: key,
+              title: trackBundle.title,
+              tags: trackBundle.tags,
+              trackFSM: trackBundle.trackFSM,
+              revealOnCreate: true,
+            });
           }),
-          m(PanelContainer, {
-            className: 'pinned-panel-container',
-            doesScroll: true,
-            panels: globals.state.pinnedTracks.map((key) => {
-              const trackBundle = this.resolveTrack(key);
-              return new TrackPanel({
-                trackKey: key,
-                title: trackBundle.title,
-                tags: trackBundle.tags,
-                trackFSM: trackBundle.trackFSM,
-                revealOnCreate: true,
-              });
-            }),
-            kind: 'TRACKS',
-          }),
-          m(PanelContainer, {
-            className: 'scrolling-panel-container',
-            doesScroll: true,
-            panels: scrollingPanels,
-            kind: 'TRACKS',
-          }),
-        ),
+          kind: 'TRACKS',
+        }),
+        m(PanelContainer, {
+          className: 'scrolling-panel-container',
+          doesScroll: true,
+          panels: scrollingPanels,
+          kind: 'TRACKS',
+        }),
       ),
       this.renderTabPanel());
 
diff --git a/ui/src/test/ui_integrationtest.ts b/ui/src/test/ui_integrationtest.ts
index d876ab8..861a798 100644
--- a/ui/src/test/ui_integrationtest.ts
+++ b/ui/src/test/ui_integrationtest.ts
@@ -86,7 +86,7 @@
   });
 
   test('expand_camera', async () => {
-    await page.click('.main-canvas');
+    await page.click('.pf-overlay-canvas');
     await page.click('h1[title="com.google.android.GoogleCamera 5506"]');
     await page.evaluate(() => {
       document.querySelector('.scrolling-panel-container')!.scrollTo(0, 400);
@@ -114,7 +114,7 @@
 
   test('expand_browser_proc', async () => {
     const page = await getPage();
-    await page.click('.main-canvas');
+    await page.click('.pf-overlay-canvas');
     await page.click('h1[title="Browser 12685"]');
     await waitForPerfettoIdle(page);
   });