Merge "[ui] Remove superfluous args from getSliceRect()" into main
diff --git a/Android.bp b/Android.bp
index 328fc3d..ef5737b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -10974,6 +10974,7 @@
     name: "perfetto_src_trace_processor_db_storage_storage",
     srcs: [
         "src/trace_processor/db/storage/arrangement_storage.cc",
+        "src/trace_processor/db/storage/dense_null_storage.cc",
         "src/trace_processor/db/storage/dummy_storage.cc",
         "src/trace_processor/db/storage/id_storage.cc",
         "src/trace_processor/db/storage/null_storage.cc",
@@ -10990,6 +10991,7 @@
     name: "perfetto_src_trace_processor_db_storage_unittests",
     srcs: [
         "src/trace_processor/db/storage/arrangement_storage_unittest.cc",
+        "src/trace_processor/db/storage/dense_null_storage_unittest.cc",
         "src/trace_processor/db/storage/id_storage_unittest.cc",
         "src/trace_processor/db/storage/null_storage_unittest.cc",
         "src/trace_processor/db/storage/numeric_storage_unittest.cc",
@@ -15481,3 +15483,29 @@
         "LICENSE",
     ],
 }
+
+// TODO(b/315118713): use list of proto file sources instead of merged proto
+gensrcs {
+    name: "perfetto_trace_javastream_protos",
+    srcs: [
+        "protos/perfetto/trace/perfetto_trace.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "protoc-gen-javastream",
+        "soong_zip",
+    ],
+    cmd: "mkdir -p $(genDir)/$(in) " +
+      "&& $(location aprotoc) " +
+        "--plugin=$(location protoc-gen-javastream) " +
+        "--javastream_out=$(genDir)/$(in) " +
+        "-Iexternal/protobuf/src " +
+        "-Iexternal/perfetto " +
+        "-I . $(in) " +
+      "&& $(location soong_zip) " +
+        "-jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
+    data: [
+        ":libprotobuf-internal-protos",
+    ],
+    output_extension: "srcjar",
+}
diff --git a/Android.bp.extras b/Android.bp.extras
index 3292ad9..9c7b0b0 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -171,3 +171,29 @@
         "LICENSE",
     ],
 }
+
+// TODO(b/315118713): use list of proto file sources instead of merged proto
+gensrcs {
+    name: "perfetto_trace_javastream_protos",
+    srcs: [
+        "protos/perfetto/trace/perfetto_trace.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "protoc-gen-javastream",
+        "soong_zip",
+    ],
+    cmd: "mkdir -p $(genDir)/$(in) " +
+      "&& $(location aprotoc) " +
+        "--plugin=$(location protoc-gen-javastream) " +
+        "--javastream_out=$(genDir)/$(in) " +
+        "-Iexternal/protobuf/src " +
+        "-Iexternal/perfetto " +
+        "-I . $(in) " +
+      "&& $(location soong_zip) " +
+        "-jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
+    data: [
+        ":libprotobuf-internal-protos",
+    ],
+    output_extension: "srcjar",
+}
diff --git a/BUILD b/BUILD
index cddab6b..5c8fbe6 100644
--- a/BUILD
+++ b/BUILD
@@ -1302,6 +1302,8 @@
     srcs = [
         "src/trace_processor/db/storage/arrangement_storage.cc",
         "src/trace_processor/db/storage/arrangement_storage.h",
+        "src/trace_processor/db/storage/dense_null_storage.cc",
+        "src/trace_processor/db/storage/dense_null_storage.h",
         "src/trace_processor/db/storage/dummy_storage.cc",
         "src/trace_processor/db/storage/dummy_storage.h",
         "src/trace_processor/db/storage/id_storage.cc",
diff --git a/protos/perfetto/trace_processor/serialization.proto b/protos/perfetto/trace_processor/serialization.proto
index e79b7c9..f559bd9 100644
--- a/protos/perfetto/trace_processor/serialization.proto
+++ b/protos/perfetto/trace_processor/serialization.proto
@@ -92,6 +92,12 @@
       optional Storage storage = 2;
     }
 
+    // A schema for serialization of |storage::DenseNullStorage|.
+    message DenseNullStorage {
+      optional BitVector bit_vector = 1;
+      optional Storage storage = 2;
+    }
+
     oneof data {
       DummyStorage dummy_storage = 1;
       IdStorage id_storage = 2;
@@ -101,6 +107,7 @@
       NullStorage null_storage = 6;
       ArrangementStorage arrangement_storage = 7;
       SelectorStorage selector_storage = 8;
+      DenseNullStorage dense_null_storage = 9;
     }
   }
 
diff --git a/src/trace_processor/db/BUILD.gn b/src/trace_processor/db/BUILD.gn
index 869c9da..6b02d28 100644
--- a/src/trace_processor/db/BUILD.gn
+++ b/src/trace_processor/db/BUILD.gn
@@ -84,6 +84,7 @@
       "../../../gn:default_deps",
       "../../../include/perfetto/base",
       "../../../include/perfetto/ext/base",
+      "../../../include/perfetto/trace_processor:basic_types",
       "../../base:test_support",
       "../tables:tables_python",
     ]
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index c77cb38..046aee4 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -28,6 +28,7 @@
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/db/query_executor.h"
 #include "src/trace_processor/db/storage/arrangement_storage.h"
+#include "src/trace_processor/db/storage/dense_null_storage.h"
 #include "src/trace_processor/db/storage/dummy_storage.h"
 #include "src/trace_processor/db/storage/id_storage.h"
 #include "src/trace_processor/db/storage/null_storage.h"
@@ -170,9 +171,6 @@
                  (c.op != FilterOp::kIsNull && c.op != FilterOp::kIsNotNull &&
                   (int_with_double || double_with_int));
 
-    // Dense columns.
-    use_legacy = use_legacy || col.IsDense();
-
     // Extrinsically sorted columns.
     use_legacy = use_legacy ||
                  (col.IsSorted() && col.overlay().row_map().IsIndexVector());
@@ -255,8 +253,13 @@
       // String columns are inherently nullable: null values are signified
       // with Id::Null().
       PERFETTO_CHECK(col.col_type() != ColumnType::kString);
-      storage = std::make_unique<storage::NullStorage>(std::move(storage),
-                                                       col.storage_base().bv());
+      if (col.IsDense()) {
+        storage = std::make_unique<storage::DenseNullStorage>(
+            std::move(storage), col.storage_base().bv());
+      } else {
+        storage = std::make_unique<storage::NullStorage>(
+            std::move(storage), col.storage_base().bv());
+      }
     }
     if (col.overlay().row_map().IsIndexVector()) {
       storage = std::make_unique<storage::ArrangementStorage>(
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 5bbc99f..eb23903 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -20,9 +20,11 @@
 
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/string_utils.h"
+#include "perfetto/trace_processor/basic_types.h"
 #include "src/base/test/utils.h"
 #include "src/trace_processor/db/table.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
 #include "src/trace_processor/tables/slice_tables_py.h"
 #include "src/trace_processor/tables/track_tables_py.h"
 
@@ -35,6 +37,7 @@
 using ExpectedFrameTimelineSliceTable = tables::ExpectedFrameTimelineSliceTable;
 using RawTable = tables::RawTable;
 using FtraceEventTable = tables::FtraceEventTable;
+using HeapGraphObjectTable = tables::HeapGraphObjectTable;
 
 // `SELECT * FROM SLICE` on android_monitor_contention_trace.at
 static char kSliceTable[] = "test/data/slice_table_for_benchmarks.csv";
@@ -50,6 +53,10 @@
 static char kFtraceEventTable[] =
     "test/data/ftrace_event_cpu_for_benchmarks.csv";
 
+// `SELECT id, upid, reference_set_id FROM heap_graph_object` on
+static char kHeapGraphObjectTable[] =
+    "test/data/heap_pgraph_object_for_benchmarks_query.csv";
+
 enum DB { V1, V2 };
 
 std::vector<std::string> SplitCSVLine(const std::string& line) {
@@ -193,6 +200,24 @@
   tables::FtraceEventTable table_{&pool_, &raw_};
 };
 
+struct HeapGraphObjectTableForBenchmark {
+  explicit HeapGraphObjectTableForBenchmark(benchmark::State& state) {
+    std::vector<std::string> table_rows_as_string =
+        ReadCSV(state, kHeapGraphObjectTable);
+
+    for (size_t i = 1; i < table_rows_as_string.size(); ++i) {
+      std::vector<std::string> row_vec = SplitCSVLine(table_rows_as_string[i]);
+
+      HeapGraphObjectTable::Row row;
+      row.upid = *base::StringToUInt32(row_vec[1]);
+      row.reference_set_id = base::StringToUInt32(row_vec[2]);
+      table_.Insert(row);
+    }
+  }
+  StringPool pool_;
+  HeapGraphObjectTable table_{&pool_};
+};
+
 void BenchmarkSliceTable(benchmark::State& state,
                          SliceTableForBenchmark& table,
                          std::initializer_list<Constraint> c) {
@@ -346,6 +371,38 @@
 
 BENCHMARK(BM_QEFilterWithArrangement)->ArgsProduct({{DB::V1, DB::V2}});
 
+static void BM_QEDenseNullFilter(benchmark::State& state) {
+  Table::kUseFilterV2 = state.range(0) == 1;
+
+  HeapGraphObjectTableForBenchmark table(state);
+  Constraint c{table.table_.reference_set_id().index_in_table(), FilterOp::kGt,
+               SqlValue::Long(1000)};
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(table.table_.FilterToRowMap({c}));
+  }
+  state.counters["s/row"] =
+      benchmark::Counter(static_cast<double>(table.table_.row_count()),
+                         benchmark::Counter::kIsIterationInvariantRate |
+                             benchmark::Counter::kInvert);
+}
+BENCHMARK(BM_QEDenseNullFilter)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QEDenseNullFilterIsNull(benchmark::State& state) {
+  Table::kUseFilterV2 = state.range(0) == 1;
+
+  HeapGraphObjectTableForBenchmark table(state);
+  Constraint c{table.table_.reference_set_id().index_in_table(),
+               FilterOp::kIsNull, SqlValue()};
+  for (auto _ : state) {
+    benchmark::DoNotOptimize(table.table_.FilterToRowMap({c}));
+  }
+  state.counters["s/row"] =
+      benchmark::Counter(static_cast<double>(table.table_.row_count()),
+                         benchmark::Counter::kIsIterationInvariantRate |
+                             benchmark::Counter::kInvert);
+}
+BENCHMARK(BM_QEDenseNullFilterIsNull)->ArgsProduct({{DB::V1, DB::V2}});
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/db/storage/BUILD.gn b/src/trace_processor/db/storage/BUILD.gn
index 55b7eb8..bd80ec7 100644
--- a/src/trace_processor/db/storage/BUILD.gn
+++ b/src/trace_processor/db/storage/BUILD.gn
@@ -18,6 +18,8 @@
   sources = [
     "arrangement_storage.cc",
     "arrangement_storage.h",
+    "dense_null_storage.cc",
+    "dense_null_storage.h",
     "dummy_storage.cc",
     "dummy_storage.h",
     "id_storage.cc",
@@ -67,6 +69,7 @@
   testonly = true
   sources = [
     "arrangement_storage_unittest.cc",
+    "dense_null_storage_unittest.cc",
     "id_storage_unittest.cc",
     "null_storage_unittest.cc",
     "numeric_storage_unittest.cc",
diff --git a/src/trace_processor/db/storage/dense_null_storage.cc b/src/trace_processor/db/storage/dense_null_storage.cc
new file mode 100644
index 0000000..9f764a4
--- /dev/null
+++ b/src/trace_processor/db/storage/dense_null_storage.cc
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/db/storage/dense_null_storage.h"
+
+#include <cstdint>
+#include <variant>
+
+#include "protos/perfetto/trace_processor/serialization.pbzero.h"
+#include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/storage/types.h"
+#include "src/trace_processor/tp_metatrace.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace storage {
+
+DenseNullStorage::DenseNullStorage(std::unique_ptr<Storage> inner,
+                                   const BitVector* non_null)
+    : inner_(std::move(inner)), non_null_(non_null) {}
+
+Storage::SearchValidationResult DenseNullStorage::ValidateSearchConstraints(
+    SqlValue sql_val,
+    FilterOp op) const {
+  return inner_->ValidateSearchConstraints(sql_val, op);
+}
+
+RangeOrBitVector DenseNullStorage::Search(FilterOp op,
+                                          SqlValue sql_val,
+                                          RowMap::Range in) const {
+  PERFETTO_TP_TRACE(metatrace::Category::DB, "DenseNullStorage::Search");
+
+  RangeOrBitVector inner_res = inner_->Search(op, sql_val, in);
+  BitVector res;
+  if (inner_res.IsRange()) {
+    // If the inner storage returns a range, mask out the appropriate values in
+    // |non_null_| which matches the range. Then, resize to |in.end| as this
+    // is mandated by the API contract of |Storage::Search|.
+    RowMap::Range inner_range = std::move(inner_res).TakeIfRange();
+    PERFETTO_DCHECK(inner_range.end <= in.end);
+    PERFETTO_DCHECK(inner_range.start >= in.start);
+    res = non_null_->IntersectRange(inner_range.start, inner_range.end);
+    res.Resize(in.end, false);
+  } else {
+    res = std::move(inner_res).TakeIfBitVector();
+  }
+  PERFETTO_DCHECK(res.size() == in.end);
+
+  if (op == FilterOp::kIsNull) {
+    // For IS NULL, we need to add any rows in |non_null_| which are zeros: we
+    // do this by taking the appropriate number of rows, inverting it and then
+    // bitwise or-ing the result with it.
+    BitVector non_null_copy = non_null_->Copy();
+    non_null_copy.Resize(in.end);
+    non_null_copy.Not();
+    res.Or(non_null_copy);
+  } else {
+    // For anything else, we just need to ensure that any rows which are null
+    // are removed as they would not match.
+    res.And(*non_null_);
+  }
+  return RangeOrBitVector(std::move(res));
+}
+
+RangeOrBitVector DenseNullStorage::IndexSearch(FilterOp op,
+                                               SqlValue sql_val,
+                                               uint32_t* indices,
+                                               uint32_t indices_size,
+                                               bool sorted) const {
+  PERFETTO_TP_TRACE(metatrace::Category::DB, "DenseNullStorage::IndexSearch");
+
+  RangeOrBitVector inner_res =
+      inner_->IndexSearch(op, sql_val, indices, indices_size, sorted);
+  if (inner_res.IsRange()) {
+    RowMap::Range inner_range = std::move(inner_res).TakeIfRange();
+    BitVector::Builder builder(indices_size, inner_range.start);
+    for (uint32_t i = inner_range.start; i < inner_range.end; ++i) {
+      builder.Append(non_null_->IsSet(indices[i]));
+    }
+    return RangeOrBitVector(std::move(builder).Build());
+  }
+
+  BitVector::Builder builder(indices_size);
+  for (uint32_t i = 0; i < indices_size; ++i) {
+    builder.Append(non_null_->IsSet(indices[i]));
+  }
+  BitVector non_null = std::move(builder).Build();
+  PERFETTO_DCHECK(non_null.size() == indices_size);
+
+  BitVector res = std::move(inner_res).TakeIfBitVector();
+  PERFETTO_DCHECK(res.size() == indices_size);
+
+  if (op == FilterOp::kIsNull) {
+    BitVector null = std::move(non_null);
+    null.Not();
+    res.Or(null);
+  } else {
+    res.And(non_null);
+  }
+  return RangeOrBitVector(std::move(res));
+}
+
+void DenseNullStorage::StableSort(uint32_t*, uint32_t) const {
+  // TODO(b/307482437): Implement.
+  PERFETTO_FATAL("Not implemented");
+}
+
+void DenseNullStorage::Sort(uint32_t*, uint32_t) const {
+  // TODO(b/307482437): Implement.
+  PERFETTO_FATAL("Not implemented");
+}
+
+void DenseNullStorage::Serialize(StorageProto* storage) const {
+  auto* null_storage = storage->set_dense_null_storage();
+  non_null_->Serialize(null_storage->set_bit_vector());
+  inner_->Serialize(null_storage->set_storage());
+}
+
+}  // namespace storage
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/db/storage/dense_null_storage.h b/src/trace_processor/db/storage/dense_null_storage.h
new file mode 100644
index 0000000..ec7b6e9
--- /dev/null
+++ b/src/trace_processor/db/storage/dense_null_storage.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_DB_STORAGE_DENSE_NULL_STORAGE_H_
+#define SRC_TRACE_PROCESSOR_DB_STORAGE_DENSE_NULL_STORAGE_H_
+
+#include <memory>
+#include <variant>
+
+#include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/storage/storage.h"
+#include "src/trace_processor/db/storage/types.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace storage {
+
+// Storage which introduces the layer of nullability but without changing the
+// "spacing" of the underlying storage i.e. this storage simply "masks" out
+// rows in the underlying storage with nulls.
+class DenseNullStorage : public Storage {
+ public:
+  DenseNullStorage(std::unique_ptr<Storage> inner, const BitVector* non_null);
+
+  SearchValidationResult ValidateSearchConstraints(SqlValue,
+                                                   FilterOp) const override;
+
+  RangeOrBitVector Search(FilterOp op,
+                          SqlValue value,
+                          RowMap::Range range) const override;
+
+  RangeOrBitVector IndexSearch(FilterOp op,
+                               SqlValue value,
+                               uint32_t* indices,
+                               uint32_t indices_count,
+                               bool sorted) const override;
+
+  void StableSort(uint32_t* rows, uint32_t rows_size) const override;
+
+  void Sort(uint32_t* rows, uint32_t rows_size) const override;
+
+  void Serialize(StorageProto*) const override;
+
+  uint32_t size() const override { return non_null_->size(); }
+
+ private:
+  std::unique_ptr<Storage> inner_;
+  const BitVector* non_null_ = nullptr;
+};
+
+}  // namespace storage
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_DB_STORAGE_DENSE_NULL_STORAGE_H_
diff --git a/src/trace_processor/db/storage/dense_null_storage_unittest.cc b/src/trace_processor/db/storage/dense_null_storage_unittest.cc
new file mode 100644
index 0000000..d8ec93c
--- /dev/null
+++ b/src/trace_processor/db/storage/dense_null_storage_unittest.cc
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/db/storage/dense_null_storage.h"
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/storage/fake_storage.h"
+#include "src/trace_processor/db/storage/numeric_storage.h"
+#include "src/trace_processor/db/storage/types.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace storage {
+namespace {
+
+using testing::ElementsAre;
+using testing::IsEmpty;
+using Range = RowMap::Range;
+
+std::vector<uint32_t> ToIndexVector(RangeOrBitVector& r_or_bv) {
+  RowMap rm;
+  if (r_or_bv.IsBitVector()) {
+    rm = RowMap(std::move(r_or_bv).TakeIfBitVector());
+  } else {
+    Range range = std::move(r_or_bv).TakeIfRange();
+    rm = RowMap(range.start, range.end);
+  }
+  return rm.GetAllIndices();
+}
+
+TEST(DenseNullStorage, NoFilteringSearch) {
+  std::vector<uint32_t> data{0, 1, 0, 1, 0};
+  auto numeric =
+      std::make_unique<NumericStorage<uint32_t>>(&data, ColumnType::kUint32);
+
+  BitVector bv{0, 1, 0, 1, 0};
+  DenseNullStorage storage(std::move(numeric), &bv);
+
+  auto res = storage.Search(FilterOp::kGe, SqlValue::Long(0), Range(0, 5));
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1, 3));
+}
+
+TEST(DenseNullStorage, RestrictInputSearch) {
+  std::vector<uint32_t> data{0, 1, 0, 1, 0};
+  auto numeric =
+      std::make_unique<NumericStorage<uint32_t>>(&data, ColumnType::kUint32);
+
+  BitVector bv{0, 1, 0, 1, 0};
+  DenseNullStorage storage(std::move(numeric), &bv);
+
+  auto res = storage.Search(FilterOp::kGe, SqlValue::Long(0), Range(1, 3));
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+}
+
+TEST(DenseNullStorage, RangeFilterSearch) {
+  auto fake = FakeStorage::SearchSubset(5, Range(1, 3));
+
+  BitVector bv{0, 1, 0, 1, 0};
+  DenseNullStorage storage(std::move(fake), &bv);
+
+  auto res = storage.Search(FilterOp::kGe, SqlValue::Long(0), Range(0, 5));
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+}
+
+TEST(DenseNullStorage, BitvectorFilterSearch) {
+  auto fake = FakeStorage::SearchSubset(5, BitVector({0, 1, 1, 0, 0}));
+
+  BitVector bv{0, 1, 0, 1, 0};
+  DenseNullStorage storage(std::move(fake), &bv);
+
+  auto res = storage.Search(FilterOp::kGe, SqlValue::Long(0), Range(0, 5));
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(1));
+}
+
+TEST(DenseNullStorage, IsNullSearch) {
+  auto fake = FakeStorage::SearchSubset(5, BitVector({1, 1, 0, 0, 1}));
+
+  BitVector bv{1, 0, 0, 1, 1};
+  DenseNullStorage storage(std::move(fake), &bv);
+
+  auto res = storage.Search(FilterOp::kIsNull, SqlValue(), Range(0, 5));
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 4));
+}
+
+TEST(DenseNullStorage, IndexSearch) {
+  std::vector<uint32_t> data{1, 0, 0, 1, 1, 1};
+  auto numeric =
+      std::make_unique<NumericStorage<uint32_t>>(&data, ColumnType::kUint32);
+
+  BitVector bv{1, 0, 0, 1, 1, 1};
+  DenseNullStorage storage(std::move(numeric), &bv);
+
+  std::vector<uint32_t> index({5, 2, 3, 4, 1});
+  auto res = storage.IndexSearch(FilterOp::kGe, SqlValue::Long(0), index.data(),
+                                 static_cast<uint32_t>(index.size()), false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 2, 3));
+}
+
+TEST(DenseNullStorage, IsNullIndexSearch) {
+  auto fake = FakeStorage::SearchSubset(6, BitVector({0, 0, 0, 1, 1, 1}));
+
+  BitVector bv{0, 1, 0, 1, 1, 1};
+  DenseNullStorage storage(std::move(fake), &bv);
+
+  std::vector<uint32_t> index({5, 2, 3, 4, 1});
+  auto res = storage.IndexSearch(FilterOp::kIsNull, SqlValue(), index.data(),
+                                 static_cast<uint32_t>(index.size()), false);
+  ASSERT_THAT(ToIndexVector(res), ElementsAre(0, 1, 2, 3));
+}
+
+}  // namespace
+}  // namespace storage
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/db/storage/id_storage_unittest.cc b/src/trace_processor/db/storage/id_storage_unittest.cc
index 2262c43..0db3159 100644
--- a/src/trace_processor/db/storage/id_storage_unittest.cc
+++ b/src/trace_processor/db/storage/id_storage_unittest.cc
@@ -108,7 +108,7 @@
   ASSERT_EQ(search_result, empty_range);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicEqSimple) {
+TEST(IdStorageUnittest, SearchEqSimple) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kEq, SqlValue::Long(15), Range(10, 20))
                     .TakeIfRange();
@@ -117,21 +117,21 @@
   ASSERT_EQ(range.end, 16u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicEqOnRangeBoundary) {
+TEST(IdStorageUnittest, SearchEqOnRangeBoundary) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kEq, SqlValue::Long(20), Range(10, 20))
                     .TakeIfRange();
   ASSERT_EQ(range.size(), 0u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicEqOutsideRange) {
+TEST(IdStorageUnittest, SearchEqOutsideRange) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kEq, SqlValue::Long(25), Range(10, 20))
                     .TakeIfRange();
   ASSERT_EQ(range.size(), 0u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicEqTooBig) {
+TEST(IdStorageUnittest, SearchEqTooBig) {
   IdStorage storage(100);
   Range range =
       storage.Search(FilterOp::kEq, SqlValue::Long(125), Range(10, 20))
@@ -139,7 +139,7 @@
   ASSERT_EQ(range.size(), 0u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicLe) {
+TEST(IdStorageUnittest, SearchLe) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kLe, SqlValue::Long(50), Range(30, 70))
                     .TakeIfRange();
@@ -147,7 +147,7 @@
   ASSERT_EQ(range.end, 51u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicLt) {
+TEST(IdStorageUnittest, SearchLt) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kLt, SqlValue::Long(50), Range(30, 70))
                     .TakeIfRange();
@@ -155,7 +155,7 @@
   ASSERT_EQ(range.end, 50u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicGe) {
+TEST(IdStorageUnittest, SearchGe) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kGe, SqlValue::Long(40), Range(30, 70))
                     .TakeIfRange();
@@ -163,7 +163,7 @@
   ASSERT_EQ(range.end, 70u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicGt) {
+TEST(IdStorageUnittest, SearchGt) {
   IdStorage storage(100);
   Range range = storage.Search(FilterOp::kGt, SqlValue::Long(40), Range(30, 70))
                     .TakeIfRange();
@@ -171,7 +171,7 @@
   ASSERT_EQ(range.end, 70u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicNe) {
+TEST(IdStorageUnittest, SearchNe) {
   IdStorage storage(100);
   BitVector bv =
       storage.Search(FilterOp::kNe, SqlValue::Long(40), Range(30, 70))
@@ -179,13 +179,112 @@
   ASSERT_EQ(bv.CountSetBits(), 39u);
 }
 
-TEST(IdStorageUnittest, BinarySearchIntrinsicNeInvalidNum) {
+TEST(IdStorageUnittest, SearchNeInvalidNum) {
   IdStorage storage(100);
   Range r = storage.Search(FilterOp::kNe, SqlValue::Long(-1), Range(30, 70))
                 .TakeIfRange();
   ASSERT_EQ(r.size(), 40u);
 }
 
+TEST(IdStorageUnittest, IndexSearchEqSimple) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kEq, SqlValue::Long(3), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 1u);
+  ASSERT_TRUE(bv.IsSet(1));
+}
+
+TEST(IdStorageUnittest, IndexSearchEqTooBig) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kEq, SqlValue::Long(20), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 0u);
+}
+
+TEST(IdStorageUnittest, IndexSearchNe) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kNe, SqlValue::Long(3), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 7u);
+  ASSERT_FALSE(bv.IsSet(1));
+}
+
+TEST(IdStorageUnittest, IndexSearchLe) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kLe, SqlValue::Long(3), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 3u);
+  ASSERT_TRUE(bv.IsSet(0));
+  ASSERT_TRUE(bv.IsSet(1));
+  ASSERT_TRUE(bv.IsSet(6));
+}
+
+TEST(IdStorageUnittest, IndexSearchLt) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kLt, SqlValue::Long(3), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 2u);
+}
+
+TEST(IdStorageUnittest, IndexSearchGe) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kGe, SqlValue::Long(6), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 3u);
+}
+
+TEST(IdStorageUnittest, IndexSearchGt) {
+  IdStorage storage(12);
+  std::vector<uint32_t> indices{1, 3, 5, 7, 9, 11, 2, 4};
+
+  BitVector bv =
+      storage
+          .IndexSearch(FilterOp::kGt, SqlValue::Long(6), indices.data(),
+                       static_cast<uint32_t>(indices.size()), false)
+          .TakeIfBitVector();
+
+  ASSERT_EQ(bv.CountSetBits(), 3u);
+  ASSERT_TRUE(bv.IsSet(3));
+  ASSERT_TRUE(bv.IsSet(4));
+  ASSERT_TRUE(bv.IsSet(5));
+}
+
 TEST(IdStorageUnittest, Sort) {
   std::vector<uint32_t> order{4, 3, 6, 1, 5};
   IdStorage storage(10);
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
index c699ab9..711415f 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
@@ -53,6 +53,47 @@
   return context->clock_tracker->ToTraceTime(clock_id, ts);
 }
 
+// Fast path for parsing the event id of an ftrace event.
+// Speculate on the fact that, if the timestamp was found, the common pid
+// will appear immediately after and the event id immediately after that.
+uint64_t TryFastParseFtraceEventId(const uint8_t* start, const uint8_t* end) {
+  constexpr auto kPidFieldNumber = protos::pbzero::FtraceEvent::kPidFieldNumber;
+  constexpr auto kPidFieldTag = MakeTagVarInt(kPidFieldNumber);
+
+  // If the next byte is not the common pid's tag, just skip the field.
+  constexpr uint32_t kMaxPidLength = 5;
+  if (PERFETTO_UNLIKELY(static_cast<uint32_t>(end - start) <= kMaxPidLength ||
+                        start[0] != kPidFieldTag)) {
+    return 0;
+  }
+
+  // Skip the common pid.
+  uint64_t common_pid = 0;
+  const uint8_t* common_pid_end = ParseVarInt(start + 1, end, &common_pid);
+  if (PERFETTO_UNLIKELY(common_pid_end == start + 1)) {
+    return 0;
+  }
+
+  // Read the next varint: this should be the event id tag.
+  uint64_t event_tag = 0;
+  const uint8_t* event_id_end = ParseVarInt(common_pid_end, end, &event_tag);
+  if (event_id_end == common_pid_end) {
+    return 0;
+  }
+
+  constexpr uint8_t kFieldTypeNumBits = 3;
+  constexpr uint64_t kFieldTypeMask =
+      (1 << kFieldTypeNumBits) - 1;  // 0000 0111;
+
+  // The event wire type should be length delimited.
+  auto wire_type = static_cast<protozero::proto_utils::ProtoWireType>(
+      event_tag & kFieldTypeMask);
+  if (wire_type != protozero::proto_utils::ProtoWireType::kLengthDelimited) {
+    return 0;
+  }
+  return event_tag >> kFieldTypeNumBits;
+}
+
 }  // namespace
 
 PERFETTO_ALWAYS_INLINE
@@ -125,29 +166,53 @@
 
   const uint8_t* data = event.data();
   const size_t length = event.length();
-  ProtoDecoder decoder(data, length);
 
-  // Speculate on the fact that the timestamp is often the 1st field of the
-  // event.
+  // Speculate on the following sequence of varints
+  //  - timestamp tag
+  //  - timestamp (64 bit)
+  //  - common pid tag
+  //  - common pid (32 bit)
+  //  - event tag
   uint64_t raw_timestamp = 0;
   bool timestamp_found = false;
+  uint64_t event_id = 0;
   if (PERFETTO_LIKELY(length > 10 && data[0] == kTimestampFieldTag)) {
     // Fastpath.
-    const uint8_t* next = ParseVarInt(data + 1, data + 11, &raw_timestamp);
-    timestamp_found = next != data + 1;
-    decoder.Reset(next);
-  } else {
-    // Slowpath.
+    const uint8_t* ts_end = ParseVarInt(data + 1, data + 11, &raw_timestamp);
+    timestamp_found = ts_end != data + 1;
+    if (PERFETTO_LIKELY(timestamp_found)) {
+      event_id = TryFastParseFtraceEventId(ts_end, data + length);
+    }
+  }
+
+  // Slowpath for finding the timestamp.
+  if (PERFETTO_UNLIKELY(!timestamp_found)) {
+    ProtoDecoder decoder(data, length);
     if (auto ts_field = decoder.FindField(kTimestampFieldNumber)) {
       timestamp_found = true;
       raw_timestamp = ts_field.as_uint64();
     }
+    if (PERFETTO_UNLIKELY(!timestamp_found)) {
+      context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
+      return;
+    }
   }
 
-  if (PERFETTO_UNLIKELY(!timestamp_found)) {
-    PERFETTO_ELOG("Timestamp field not found in FtraceEvent");
-    context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
-    return;
+  // Slowpath for finding the event id.
+  if (PERFETTO_UNLIKELY(event_id == 0)) {
+    ProtoDecoder decoder(data, length);
+    for (auto f = decoder.ReadField(); f.valid(); f = decoder.ReadField()) {
+      // Find the first length-delimited tag as this corresponds to the ftrace
+      // event.
+      if (f.type() == protozero::proto_utils::ProtoWireType::kLengthDelimited) {
+        event_id = f.id();
+        break;
+      }
+    }
+    if (PERFETTO_UNLIKELY(!event_id)) {
+      context_->storage->IncrementStats(stats::ftrace_bundle_tokenizer_errors);
+      return;
+    }
   }
 
   // ClockTracker will increment some error stats if it failed to convert the
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index 162dbc6..9c32a97 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -587,6 +587,10 @@
                                   SqlValue& out,
                                   Destructors&) {
   State* state = static_cast<State*>(ctx);
+
+  // Enter the function and ensure that we have a statement allocated.
+  RETURN_IF_ERROR(state->PushStackEntry());
+
   if (argc != state->prototype().arguments.size()) {
     return base::ErrStatus(
         "%s: invalid number of args; expected %zu, received %zu",
@@ -608,9 +612,6 @@
     }
   }
 
-  // Enter the function and ensure that we have a statement allocated.
-  RETURN_IF_ERROR(state->PushStackEntry());
-
   std::optional<Memoizer::MemoizedArgs> memoized_args =
       Memoizer::AsMemoizedArgs(argc, argv);
 
diff --git a/test/data/heap_graph_object_for_benchmarks.pftrace.sha256 b/test/data/heap_graph_object_for_benchmarks.pftrace.sha256
new file mode 100644
index 0000000..dd7380f
--- /dev/null
+++ b/test/data/heap_graph_object_for_benchmarks.pftrace.sha256
@@ -0,0 +1 @@
+d0ee1affa7afdb325620a251f20ff16d5e19a5dae76508bb6db746d55dabd1cb
\ No newline at end of file
diff --git a/test/data/heap_pgraph_object_for_benchmarks_query.csv.sha256 b/test/data/heap_pgraph_object_for_benchmarks_query.csv.sha256
new file mode 100644
index 0000000..35a79ea
--- /dev/null
+++ b/test/data/heap_pgraph_object_for_benchmarks_query.csv.sha256
@@ -0,0 +1 @@
+62d757e7de34b466929f0444f2e097123b857c675fbe160f300f998a9309a3ae
\ No newline at end of file
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 6d00144..c1c1ea1 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -536,6 +536,7 @@
     self.apex_available = set()
     self.min_sdk_version = None
     self.proto = dict()
+    self.output_extension: Optional[str] = None
     # The genrule_XXX below are properties that must to be propagated back
     # on the module(s) that depend on the genrule.
     self.genrule_headers = set()
@@ -587,6 +588,7 @@
     self._output_field(output, 'stubs')
     self._output_field(output, 'proto')
     self._output_field(output, 'main')
+    self._output_field(output, 'output_extension')
 
     target_out = []
     self._output_field(target_out, 'android')
diff --git a/tools/install-build-deps b/tools/install-build-deps
index c3822f6..fd17b81 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -63,7 +63,7 @@
     'buildtools/test_data',  # Moved to test/data by r.android.com/1539381 .
     'buildtools/d8',  # Removed by r.android.com/1424334 .
 
-    # Build toools moved to third_party/ by r.android.com/2327602 .
+    # Build tools moved to third_party/ by r.android.com/2327602 .
     'buildtools/mac/clang-format',
     'buildtools/mac/gn',
     'buildtools/mac/ninja',
@@ -95,6 +95,11 @@
         'f706aaa0676e3e22f5fc9ca482295d7caee8535d1869f99efa2358177b64f5cd',
         'linux', 'x64'),
     Dependency(
+        'third_party/gn/gn',
+        'https://storage.googleapis.com/perfetto/gn-linux-arm64-1968-0725d782',
+        'c2a372cd4f911028d8bc351fbf24835c9b1194fcc92beadf6c5a2b3addae973c',
+        'linux', 'arm64'),
+    Dependency(
         'third_party/gn/gn.exe',
         'https://storage.googleapis.com/perfetto/gn-win-1968-0725d782',
         '001f777f023c7a6959c778fb3a6b6cfc63f6baef953410ecdeaec350fb12285b',
@@ -150,6 +155,11 @@
         'https://storage.googleapis.com/perfetto/ninja-win-182',
         '09ced0fcd1a4dec7d1b798a2cf9ce5d20e5d2fbc2337343827f192ce47d0f491',
         'windows', 'x64'),
+    Dependency(
+        'third_party/ninja/ninja',
+        'https://storage.googleapis.com/perfetto/ninja-linux-arm64-1111',
+        '05031a734ec4310a51b2cfe9f0096b26fce25ab4ff19e5b51abe6371de066cc5',
+        'linux', 'arm64'),
 
     # Keep the revision in sync with Chrome's PACKAGE_VERSION in
     # tools/clang/scripts/update.py.
@@ -463,6 +473,8 @@
   arch = machine()
   if arch == 'arm64':
     return 'arm64'
+  elif arch == 'aarch64':
+    return 'arm64'
   else:
     # Assume everything else is x64 matching previous behaviour.
     return 'x64'
diff --git a/ui/src/base/static_initializers.ts b/ui/src/base/static_initializers.ts
index 5abfcd9..f927471 100644
--- a/ui/src/base/static_initializers.ts
+++ b/ui/src/base/static_initializers.ts
@@ -38,7 +38,7 @@
   // from the global state (which is frozen) and later try to update the copies.
   // By doing so, we  accidentally the local copy of global state, which is
   // supposed to be immutable.
-  setAutoFreeze(false);
+  setAutoFreeze(true);
 }
 
 function initializeProtobuf() {
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index a524dfb..ec6e7e6 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -24,7 +24,7 @@
   TrackGroupState,
   TrackState,
 } from '../common/state';
-import {Migrate, Track, TrackContext} from '../public';
+import {Migrate, Track, TrackContext, TrackTags} from '../public';
 
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
@@ -44,6 +44,7 @@
   private shellWidth = 0;
   private backgroundColor = '#ffffff';  // Updated from CSS later.
   private summaryTrack?: Track;
+  private summaryTrackTags?: TrackTags;
 
   constructor({attrs}: m.CVnode<Attrs>) {
     super();
@@ -70,6 +71,7 @@
     };
 
     this.summaryTrack = pluginManager.createTrack(uri, ctx);
+    this.summaryTrackTags = pluginManager.resolveTrackInfo(uri)?.tags;
   }
 
   get trackGroupState(): TrackGroupState {
@@ -148,7 +150,7 @@
                 'h1.track-title',
                 {title: name},
                 name,
-                renderChips(this.summaryTrackState),
+                renderChips(this.summaryTrackTags),
                 ),
             (this.trackGroupState.collapsed && child !== null) ?
                 m('h2.track-subtitle', child) :
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 5c3df0f..dec3a65 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -22,8 +22,9 @@
 import {pluginManager} from '../common/plugins';
 import {TrackState} from '../common/state';
 import {raf} from '../core/raf_scheduler';
-import {Migrate, SliceRect, Track, TrackContext} from '../public';
+import {Migrate, SliceRect, Track, TrackContext, TrackTags} from '../public';
 
+import {checkerboard} from './checkerboard';
 import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
@@ -74,29 +75,24 @@
   }
 }
 
-export function renderChips({uri}: TrackState) {
-  const tagElements: m.Children = [];
-  const trackInfo = pluginManager.resolveTrackInfo(uri);
-  const tags = trackInfo?.tags;
-  tags?.metric && tagElements.push(m(TrackChip, {text: 'metric'}));
-  tags?.debuggable && tagElements.push(m(TrackChip, {text: 'debuggable'}));
-  return tagElements;
+export function renderChips(tags?: TrackTags) {
+  return [
+    tags?.metric && m(TrackChip, {text: 'metric'}),
+    tags?.debuggable && m(TrackChip, {text: 'debuggable'}),
+  ];
 }
 
 interface TrackShellAttrs {
-  track: Track;
-  trackState: TrackState;
+  trackKey: string;
+  title: string;
+  buttons: m.Children;
+  tags?: TrackTags;
 }
 
 class TrackShell implements m.ClassComponent<TrackShellAttrs> {
   // Set to true when we click down and drag the
   private dragging = false;
   private dropping: 'before'|'after'|undefined = undefined;
-  private attrs?: TrackShellAttrs;
-
-  oninit(vnode: m.Vnode<TrackShellAttrs>) {
-    this.attrs = vnode.attrs;
-  }
 
   view({attrs}: m.CVnode<TrackShellAttrs>) {
     // The shell should be highlighted if the current search result is inside
@@ -105,7 +101,7 @@
     const searchIndex = globals.state.searchIndex;
     if (searchIndex !== -1) {
       const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      if (trackKey === attrs.trackState.key) {
+      if (trackKey === attrs.trackKey) {
         highlightClass = 'flash';
       }
     }
@@ -116,34 +112,34 @@
         `.track-shell[draggable=true]`,
         {
           class: `${highlightClass} ${dragClass} ${dropClass}`,
-          ondragstart: this.ondragstart.bind(this),
+          ondragstart: (e: DragEvent) => this.ondragstart(e, attrs.trackKey),
           ondragend: this.ondragend.bind(this),
           ondragover: this.ondragover.bind(this),
           ondragleave: this.ondragleave.bind(this),
-          ondrop: this.ondrop.bind(this),
+          ondrop: (e: DragEvent) => this.ondrop(e, attrs.trackKey),
         },
         m(
             'h1',
             {
-              title: attrs.trackState.name,
+              title: attrs.title,
               style: {
-                'font-size': getTitleSize(attrs.trackState.name),
+                'font-size': getTitleSize(attrs.title),
               },
             },
-            attrs.trackState.name,
-            renderChips(attrs.trackState),
+            attrs.title,
+            renderChips(attrs.tags),
             ),
         m('.track-buttons',
-          attrs.track.getTrackShellButtons(),
+          attrs.buttons,
           m(TrackButton, {
             action: () => {
               globals.dispatch(
-                  Actions.toggleTrackPinned({trackKey: attrs.trackState.key}));
+                  Actions.toggleTrackPinned({trackKey: attrs.trackKey}));
             },
             i: Icons.Pin,
-            filledIcon: isPinned(attrs.trackState.key),
-            tooltip: isPinned(attrs.trackState.key) ? 'Unpin' : 'Pin to top',
-            showButton: isPinned(attrs.trackState.key),
+            filledIcon: isPinned(attrs.trackKey),
+            tooltip: isPinned(attrs.trackKey) ? 'Unpin' : 'Pin to top',
+            showButton: isPinned(attrs.trackKey),
             fullHeight: true,
           }),
           globals.state.currentSelection !== null &&
@@ -151,25 +147,24 @@
               m(TrackButton, {
                 action: (e: MouseEvent) => {
                   globals.dispatch(Actions.toggleTrackSelection(
-                      {id: attrs.trackState.key, isTrackGroup: false}));
+                      {id: attrs.trackKey, isTrackGroup: false}));
                   e.stopPropagation();
                 },
-                i: isSelected(attrs.trackState.key) ? Icons.Checkbox :
-                                                      Icons.BlankCheckbox,
-                tooltip: isSelected(attrs.trackState.key) ?
-                    'Remove track' :
-                    'Add track to selection',
+                i: isSelected(attrs.trackKey) ? Icons.Checkbox :
+                                                Icons.BlankCheckbox,
+                tooltip: isSelected(attrs.trackKey) ? 'Remove track' :
+                                                      'Add track to selection',
                 showButton: true,
               }) :
               ''));
   }
 
-  ondragstart(e: DragEvent) {
+  ondragstart(e: DragEvent, trackKey: string) {
     const dataTransfer = e.dataTransfer;
     if (dataTransfer === null) return;
     this.dragging = true;
     raf.scheduleFullRedraw();
-    dataTransfer.setData('perfetto/track', `${this.attrs!.trackState.key}`);
+    dataTransfer.setData('perfetto/track', `${trackKey}`);
     dataTransfer.setDragImage(new Image(), 0, 0);
   }
 
@@ -202,13 +197,13 @@
     raf.scheduleFullRedraw();
   }
 
-  ondrop(e: DragEvent) {
+  ondrop(e: DragEvent, trackKey: string) {
     if (this.dropping === undefined) return;
     const dataTransfer = e.dataTransfer;
     if (dataTransfer === null) return;
     raf.scheduleFullRedraw();
     const srcId = dataTransfer.getData('perfetto/track');
-    const dstId = this.attrs!.trackState.key;
+    const dstId = trackKey;
     globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
     this.dropping = undefined;
   }
@@ -273,9 +268,14 @@
 }
 
 interface TrackComponentAttrs {
-  trackState: TrackState;
-  track: Track;
+  trackKey: string;
+  heightPx?: number;
+  title: string;
+  buttons?: m.Children;
+  tags?: TrackTags;
+  track?: Track;
 }
+
 class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
   view({attrs}: m.CVnode<TrackComponentAttrs>) {
     // TODO(hjd): The min height below must match the track_shell_title
@@ -285,19 +285,24 @@
         '.track',
         {
           style: {
-            height: `${Math.max(18, attrs.track.getHeight())}px`,
+            height: `${Math.max(18, attrs.heightPx ?? 0)}px`,
           },
-          id: 'track_' + attrs.trackState.key,
+          id: 'track_' + attrs.trackKey,
         },
         [
-          m(TrackShell, {track: attrs.track, trackState: attrs.trackState}),
-          m(TrackContent, {track: attrs.track}),
+          m(TrackShell, {
+            buttons: attrs.buttons,
+            title: attrs.title,
+            trackKey: attrs.trackKey,
+            tags: attrs.tags,
+          }),
+          attrs.track && m(TrackContent, {track: attrs.track}),
         ]);
   }
 
   oncreate({attrs}: m.CVnode<TrackComponentAttrs>) {
-    if (globals.scrollToTrackKey === attrs.trackState.key) {
-      verticalScrollToTrack(attrs.trackState.key);
+    if (globals.scrollToTrackKey === attrs.trackKey) {
+      verticalScrollToTrack(attrs.trackKey);
       globals.scrollToTrackKey = undefined;
     }
   }
@@ -340,6 +345,7 @@
   // has disappeared.
   private track: Track|undefined;
   private trackState: TrackState|undefined;
+  private tags: TrackTags|undefined;
 
   private tryLoadTrack(vnode: m.CVnode<TrackPanelAttrs>) {
     const trackKey = vnode.attrs.trackKey;
@@ -363,6 +369,7 @@
     };
 
     this.track = pluginManager.createTrack(uri, trackCtx);
+    this.tags = pluginManager.resolveTrackInfo(uri)?.tags;
 
     this.track?.onCreate(trackCtx);
     this.trackState = trackState;
@@ -374,9 +381,19 @@
     }
 
     if (this.track === undefined || this.trackState === undefined) {
-      return m('div', 'No such track');
+      return m(TrackComponent, {
+        trackKey: vnode.attrs.trackKey,
+        title: this.trackState?.name ?? 'Loading...',
+      });
     }
-    return m(TrackComponent, {trackState: this.trackState, track: this.track});
+    return m(TrackComponent, {
+      tags: this.tags,
+      heightPx: this.track.getHeight(),
+      title: this.trackState.name,
+      trackKey: this.trackState.key,
+      buttons: this.track.getTrackShellButtons(),
+      track: this.track,
+    });
   }
 
   oncreate() {
@@ -428,6 +445,8 @@
     ctx.translate(TRACK_SHELL_WIDTH, 0);
     if (this.track !== undefined) {
       this.track.render(ctx);
+    } else {
+      checkerboard(ctx, size.height, 0, size.width - TRACK_SHELL_WIDTH);
     }
     ctx.restore();
 
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index 7658fb6..a521724 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -58,6 +58,59 @@
            SELECT * FROM android_binder_graph(-1000, 1000, -1000, 1000)`,
           'all process binder graph'),
     });
+
+    ctx.registerCommand({
+      id: 'dev.perfetto.AndroidPerf#ThreadClusterDistribution',
+      name: 'Run query: runtime cluster distribution for a thread',
+      callback: async (tid) => {
+        if (tid === undefined) {
+          tid = prompt('Enter a thread tid', '');
+          if (tid === null) return;
+        }
+        ctx.tabs.openQuery(`
+          INCLUDE PERFETTO MODULE common.cpus;
+          WITH
+            total_runtime AS (
+              SELECT sum(dur) AS total_runtime
+              FROM sched s
+              LEFT JOIN thread t
+                USING (utid)
+              WHERE t.tid = ${tid}
+            )
+            SELECT
+              c.size AS cluster,
+              sum(dur)/1e6 AS total_dur_ms,
+              sum(dur) * 1.0 / (SELECT * FROM total_runtime) AS percentage
+            FROM sched s
+            LEFT JOIN thread t
+              USING (utid)
+            LEFT JOIN cpus c
+              ON s.cpu = c.cpu_index
+            WHERE t.tid = ${tid}
+            GROUP BY 1`, `runtime cluster distrubtion for tid ${tid}`);
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'dev.perfetto.AndroidPerf#SchedLatency',
+      name: 'Run query: top 50 sched latency for a thread',
+      callback: async (tid) => {
+        if (tid === undefined) {
+          tid = prompt('Enter a thread tid', '');
+          if (tid === null) return;
+        }
+        ctx.tabs.openQuery(`
+          SELECT ts.*, t.tid, t.name, tt.id AS track_id
+          FROM thread_state ts
+          LEFT JOIN thread_track tt
+           USING (utid)
+          LEFT JOIN thread t
+           USING (utid)
+          WHERE ts.state IN ('R', 'R+') AND tid = ${tid}
+           ORDER BY dur DESC
+          LIMIT 50`, `top 50 sched latency slice for tid ${tid}`);
+      },
+    });
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/OWNERS b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/OWNERS
new file mode 100644
index 0000000..e5632b1
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/OWNERS
@@ -0,0 +1 @@
+lukechang@google.com
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
new file mode 100644
index 0000000..46fbb46
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -0,0 +1,109 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {addDebugSliceTrack} from '../../tracks/debug/slice_track';
+
+class AndroidPerfTraceCounters implements Plugin {
+
+  onActivate(_: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    ctx.registerCommand({
+      id: 'dev.perfetto.AndroidPerfTraceCounters#ThreadRuntimeIPC',
+      name: 'Add a track to show a thread runtime ipc',
+      callback: async (tid) => {
+        if (tid === undefined) {
+          tid = prompt('Enter a thread tid', '');
+          if (tid === null) return;
+        }
+        const sql_prefix = `
+WITH
+  sched_switch_ipc AS (
+    SELECT
+      ts,
+      EXTRACT_ARG(arg_set_id, 'prev_pid') AS tid,
+      EXTRACT_ARG(arg_set_id, 'prev_comm') AS thread_name,
+      EXTRACT_ARG(arg_set_id, 'inst') / (EXTRACT_ARG(arg_set_id, 'cyc') * 1.0) AS ipc,
+      EXTRACT_ARG(arg_set_id, 'inst') AS instruction,
+      EXTRACT_ARG(arg_set_id, 'cyc') AS cycle,
+      EXTRACT_ARG(arg_set_id, 'stallbm') AS stall_backend_mem,
+      EXTRACT_ARG(arg_set_id, 'l3dm') AS l3_cache_miss
+    FROM ftrace_event
+    WHERE name = 'sched_switch_with_ctrs' AND tid = ${tid}
+  ),
+  target_thread_sched_slice AS (
+    SELECT s.*, t.tid, t.name FROM sched s LEFT JOIN thread t USING (utid) WHERE t.tid = ${tid}
+  ),
+  target_thread_ipc_slice AS (
+    SELECT
+      (
+        SELECT
+          ts
+        FROM target_thread_sched_slice ts
+        WHERE ts.tid = ssi.tid AND ts.ts < ssi.ts
+        ORDER BY ts.ts DESC
+        LIMIT 1
+      ) AS ts,
+      (
+        SELECT
+          dur
+        FROM target_thread_sched_slice ts
+        WHERE ts.tid = ssi.tid AND ts.ts < ssi.ts
+        ORDER BY ts.ts DESC
+        LIMIT 1
+      ) AS dur,
+      ssi.ipc,
+      ssi.instruction,
+      ssi.cycle,
+      ssi.stall_backend_mem,
+      ssi.l3_cache_miss
+    FROM sched_switch_ipc ssi
+  )
+`
+
+        await addDebugSliceTrack(
+          ctx.engine,
+          {
+            sqlSource: sql_prefix + `
+SELECT * FROM target_thread_ipc_slice WHERE ts IS NOT NULL`,
+          },
+          'Rutime IPC:' + tid,
+          {ts: 'ts', dur: 'dur', name: 'ipc'},
+          ['instruction', 'cycle', 'stall_backend_mem', 'l3_cache_miss' ],
+        );
+        ctx.tabs.openQuery(sql_prefix + `
+SELECT
+  (sum(instruction) * 1.0 / sum(cycle)*1.0) AS avg_ipc,
+  sum(dur)/1e6 as total_runtime_ms,
+  sum(instruction) AS total_instructions,
+  sum(cycle) AS total_cycles,
+  sum(stall_backend_mem) as total_stall_backend_mem,
+  sum(l3_cache_miss) as total_l3_cache_miss
+FROM target_thread_ipc_slice WHERE ts IS NOT NULL`,
+          'target thread ipc statistic');
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'dev.perfetto.AndroidPerfTraceCounters',
+  plugin: AndroidPerfTraceCounters,
+};
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index f07f263..aa421f6 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -383,7 +383,7 @@
     // Draw CPU idle rectangles that overlay the CPU freq graph.
     ctx.fillStyle = `rgba(240, 240, 240, 1)`;
 
-    for (let i = 0; i < data.lastIdleValues.length; i++) {
+    for (let i = startIdx; i < endIdx; i++) {
       if (data.lastIdleValues[i] < 0) {
         continue;
       }