Merge "Add plugin to save and restore pinned track on a best effort" into main
diff --git a/Android.bp b/Android.bp
index e435f99..94b84ff 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5323,6 +5323,7 @@
 genrule {
     name: "perfetto_protos_perfetto_metrics_webview_descriptor",
     srcs: [
+        ":libprotobuf-internal-descriptor-proto",
         "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
@@ -5388,7 +5389,7 @@
     tools: [
         "aprotoc",
     ],
-    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --descriptor_set_out=$(out) $(in)",
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --proto_path=external/protobuf/src --descriptor_set_out=$(out) $(in)",
     out: [
         "perfetto_protos_perfetto_metrics_webview_descriptor.bin",
     ],
@@ -11347,6 +11348,7 @@
     srcs: [
         "src/trace_processor/db/column/arrangement_overlay_unittest.cc",
         "src/trace_processor/db/column/dense_null_overlay_unittest.cc",
+        "src/trace_processor/db/column/fake_storage_unittest.cc",
         "src/trace_processor/db/column/id_storage_unittest.cc",
         "src/trace_processor/db/column/null_overlay_unittest.cc",
         "src/trace_processor/db/column/numeric_storage_unittest.cc",
diff --git a/CHANGELOG b/CHANGELOG
index b600600..99ff035 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,7 +4,18 @@
   Trace Processor:
     *
   UI:
-    *
+    * Add tracks to the list of searchable items.
+    * Use mipmaps to improve track query performance on large traces.
+    * Fix slow scrolling bug in ftrace explorer tab on low DPI machines.
+    * Overhaul track decider queries to improve trace load times.
+    * Add track
+    * Tidy up command names & remove some example ones.
+    * Remove arg auto-completion in pivot table.
+    * Show dominator tree views by default.
+    * Fix counter event selection off-by-one error.
+    * Add viewport control to the plugin API.
+    * Sticky track titles to improve track button accessibility in tall tracks.
+    * A handful of small bugfixes.
   SDK:
     * The TRACE_EVENT macro used to reject `const char *` event names: either
       `StaticString` or `DynamicString` needed to be specified. In the last year
diff --git a/gn/proto_library.gni b/gn/proto_library.gni
index f69d3ce..a7a77c6 100644
--- a/gn/proto_library.gni
+++ b/gn/proto_library.gni
@@ -371,7 +371,7 @@
 
         metadata = {
           proto_library_sources = invoker.sources
-          import_dirs = import_dirs_
+          proto_import_dirs = import_dirs_
           exports = get_path_info(public_deps_, "abspath")
         }
         forward_variables_from(invoker, vars_to_forward)
diff --git a/gn/standalone/proto_library.gni b/gn/standalone/proto_library.gni
index 07b8140..1a23a97 100644
--- a/gn/standalone/proto_library.gni
+++ b/gn/standalone/proto_library.gni
@@ -170,6 +170,10 @@
       ]
     }
 
+    metadata = {
+      proto_import_dirs = import_dirs
+    }
+
     if (generate_cc) {
       cc_generator_options_ = ""
       if (defined(invoker.cc_generator_options)) {
diff --git a/src/trace_processor/db/column/BUILD.gn b/src/trace_processor/db/column/BUILD.gn
index 74d1611..d53e7ae 100644
--- a/src/trace_processor/db/column/BUILD.gn
+++ b/src/trace_processor/db/column/BUILD.gn
@@ -75,6 +75,7 @@
   sources = [
     "arrangement_overlay_unittest.cc",
     "dense_null_overlay_unittest.cc",
+    "fake_storage_unittest.cc",
     "id_storage_unittest.cc",
     "null_overlay_unittest.cc",
     "numeric_storage_unittest.cc",
diff --git a/src/trace_processor/db/column/fake_storage.cc b/src/trace_processor/db/column/fake_storage.cc
index f587c77..babfb7c 100644
--- a/src/trace_processor/db/column/fake_storage.cc
+++ b/src/trace_processor/db/column/fake_storage.cc
@@ -41,6 +41,7 @@
 SingleSearchResult FakeStorageChain::SingleSearch(FilterOp,
                                                   SqlValue,
                                                   uint32_t i) const {
+  PERFETTO_CHECK(i < size_);
   switch (strategy_) {
     case kAll:
       return SingleSearchResult::kMatch;
@@ -115,37 +116,37 @@
     FilterOp,
     SqlValue,
     const OrderedIndices& indices) const {
-  if (strategy_ == kAll) {
-    return {0, indices.size};
-  }
-
-  if (strategy_ == kNone) {
-    return {};
-  }
-
-  if (strategy_ == kRange) {
-    // We are looking at intersection of |range_| and |indices_|.
-    const uint32_t* first_in_range = std::partition_point(
-        indices.data, indices.data + indices.size,
-        [this](uint32_t i) { return !range_.Contains(i); });
-    const uint32_t* first_outside_range =
-        std::partition_point(first_in_range, indices.data + indices.size,
-                             [this](uint32_t i) { return range_.Contains(i); });
-    return {static_cast<uint32_t>(std::distance(indices.data, first_in_range)),
-            static_cast<uint32_t>(
-                std::distance(indices.data, first_outside_range))};
-  }
-
-  PERFETTO_DCHECK(strategy_ == kBitVector);
-  // We are looking at intersection of |range_| and |bit_vector_|.
-  const uint32_t* first_set = std::partition_point(
-      indices.data, indices.data + indices.size,
-      [this](uint32_t i) { return !bit_vector_.IsSet(i); });
-  const uint32_t* first_non_set =
-      std::partition_point(first_set, indices.data + indices.size,
-                           [this](uint32_t i) { return bit_vector_.IsSet(i); });
-  return {static_cast<uint32_t>(std::distance(indices.data, first_set)),
+  switch (strategy_) {
+    case kAll:
+      return {0, indices.size};
+    case kNone:
+      return {};
+    case kRange: {
+      // We are looking at intersection of |range_| and |indices_|.
+      const uint32_t* first_in_range = std::partition_point(
+          indices.data, indices.data + indices.size,
+          [this](uint32_t i) { return !range_.Contains(i); });
+      const uint32_t* first_outside_range = std::partition_point(
+          first_in_range, indices.data + indices.size,
+          [this](uint32_t i) { return range_.Contains(i); });
+      return {
+          static_cast<uint32_t>(std::distance(indices.data, first_in_range)),
+          static_cast<uint32_t>(
+              std::distance(indices.data, first_outside_range))};
+    }
+    case kBitVector:
+      // We are looking at intersection of |range_| and |bit_vector_|.
+      const uint32_t* first_set = std::partition_point(
+          indices.data, indices.data + indices.size,
+          [this](uint32_t i) { return !bit_vector_.IsSet(i); });
+      const uint32_t* first_non_set = std::partition_point(
+          first_set, indices.data + indices.size,
+          [this](uint32_t i) { return bit_vector_.IsSet(i); });
+      return {
+          static_cast<uint32_t>(std::distance(indices.data, first_set)),
           static_cast<uint32_t>(std::distance(indices.data, first_non_set))};
+  }
+  PERFETTO_FATAL("For GCC");
 }
 
 void FakeStorageChain::StableSort(SortToken*, SortToken*, SortDirection) const {
diff --git a/src/trace_processor/db/column/fake_storage_unittest.cc b/src/trace_processor/db/column/fake_storage_unittest.cc
new file mode 100644
index 0000000..0bc0211
--- /dev/null
+++ b/src/trace_processor/db/column/fake_storage_unittest.cc
@@ -0,0 +1,210 @@
+/*
+ * 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.
+ */
+#include "src/trace_processor/db/column/fake_storage.h"
+
+#include <cstdint>
+#include <limits>
+#include <vector>
+
+#include "perfetto/trace_processor/basic_types.h"
+#include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/column/data_layer.h"
+#include "src/trace_processor/db/column/types.h"
+#include "src/trace_processor/db/column/utils.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+
+inline bool operator==(const Range& a, const Range& b) {
+  return std::tie(a.start, a.end) == std::tie(b.start, b.end);
+}
+
+inline bool operator==(const BitVector& a, const BitVector& b) {
+  return a.size() == b.size() && a.CountSetBits() == b.CountSetBits();
+}
+
+namespace column {
+namespace {
+
+using testing::ElementsAre;
+using testing::IsEmpty;
+
+using Indices = DataLayerChain::Indices;
+using OrderedIndices = DataLayerChain::OrderedIndices;
+
+TEST(FakeStorage, ValidateSearchConstraints) {
+  {
+    // All passes
+    auto fake = FakeStorageChain::SearchAll(10);
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // None passes
+    auto fake = FakeStorageChain::SearchNone(10);
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // Index vector
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3, 4, 5});
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // BitVector
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 1, 0, 1, 0});
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+  {
+    // Range
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    EXPECT_EQ(fake->ValidateSearchConstraints(FilterOp::kEq, SqlValue()),
+              SearchValidationResult::kOk);
+  }
+}
+
+TEST(FakeStorage, SingleSearch) {
+  {
+    // All passes
+    auto fake = FakeStorageChain::SearchAll(10);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 5u),
+              SingleSearchResult::kMatch);
+  }
+  {
+    // None passes
+    auto fake = FakeStorageChain::SearchNone(10);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 5u),
+              SingleSearchResult::kNoMatch);
+  }
+  {
+    // Index vector
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3, 4, 5});
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 0u),
+              SingleSearchResult::kNoMatch);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 1u),
+              SingleSearchResult::kMatch);
+  }
+  {
+    // BitVector
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 1, 0, 1, 0});
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 0),
+              SingleSearchResult::kNoMatch);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 1u),
+              SingleSearchResult::kMatch);
+  }
+  {
+    // Range
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 0),
+              SingleSearchResult::kNoMatch);
+    EXPECT_EQ(fake->SingleSearch(FilterOp::kEq, SqlValue(), 1u),
+              SingleSearchResult::kMatch);
+  }
+}
+
+TEST(FakeStorage, IndexSearchValidated) {
+  {
+    // All passes
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchAll(5);
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 1, 2));
+  }
+  {
+    // None passes
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchNone(5);
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_TRUE(utils::ExtractPayloadForTesting(indices).empty());
+  }
+  {
+    // BitVector
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 1, 0, 1, 0});
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
+  }
+  {
+    // Index vector
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3});
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
+  }
+  {
+    // Range
+    Indices indices = Indices::CreateWithIndexPayloadForTesting(
+        {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    fake->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
+  }
+}
+
+TEST(FakeStorage, OrderedIndexSearchValidated) {
+  std::vector<uint32_t> table_idx{4, 3, 2, 1};
+  OrderedIndices indices{table_idx.data(), uint32_t(table_idx.size()),
+                         Indices::State::kNonmonotonic};
+  {
+    // All passes
+    auto fake = FakeStorageChain::SearchAll(5);
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(0, 4));
+  }
+  {
+    // None passes
+    auto fake = FakeStorageChain::SearchNone(5);
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(0, 0));
+  }
+  {
+    // BitVector
+    auto fake = FakeStorageChain::SearchSubset(5, BitVector{0, 0, 1, 1, 1});
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(0, 3));
+  }
+  {
+    // Index vector
+    auto fake =
+        FakeStorageChain::SearchSubset(5, std::vector<uint32_t>{1, 2, 3});
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(1, 4));
+  }
+  {
+    // Range
+    auto fake = FakeStorageChain::SearchSubset(5, Range(1, 4));
+    Range ret =
+        fake->OrderedIndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
+    EXPECT_EQ(ret, Range(1, 4));
+  }
+}
+
+}  // namespace
+}  // namespace column
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/db/column/range_overlay_unittest.cc b/src/trace_processor/db/column/range_overlay_unittest.cc
index a965be3..240e998 100644
--- a/src/trace_processor/db/column/range_overlay_unittest.cc
+++ b/src/trace_processor/db/column/range_overlay_unittest.cc
@@ -95,14 +95,17 @@
 TEST(RangeOverlay, IndexSearch) {
   auto fake =
       FakeStorageChain::SearchSubset(8, BitVector({0, 1, 0, 1, 0, 1, 0, 0}));
+
+  // {true, false}
   Range range(3, 5);
   RangeOverlay storage(&range);
   auto chain = storage.MakeChain(std::move(fake));
 
+  // {true, false, true}
   Indices indices = Indices::CreateWithIndexPayloadForTesting(
-      {1u, 0u, 3u}, Indices::State::kNonmonotonic);
+      {0, 1, 0}, Indices::State::kNonmonotonic);
   chain->IndexSearch(FilterOp::kGe, SqlValue::Long(0u), indices);
-  ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(1u));
+  ASSERT_THAT(utils::ExtractPayloadForTesting(indices), ElementsAre(0, 2));
 }
 
 TEST(RangeOverlay, StableSort) {
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index d71b0dc..c8a2b1a 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -768,7 +768,6 @@
   # The .proto filegroup will be added to `tool_files` of rdeps so that the
   # genrules can be sandboxed.
 
-  tool_files = set()
   for proto_dep in target.proto_deps().union(target.transitive_proto_deps()):
     tool_files.add(":" + label_to_module_name(proto_dep.name))
 
diff --git a/tools/gen_tp_table_headers.py b/tools/gen_tp_table_headers.py
index f15245e..91e4cdb 100755
--- a/tools/gen_tp_table_headers.py
+++ b/tools/gen_tp_table_headers.py
@@ -72,7 +72,8 @@
   ]
   headers: Dict[str, Header] = {}
   for table in parse_tables_from_modules(modules):
-    input_path = os.path.relpath(table.table.python_module, ROOT_DIR)
+    raw_path = table.table.python_module
+    input_path = raw_path[raw_path.rfind('/src') + 1:]
     header = headers.get(input_path, Header([]))
     header.tables.append(table)
     headers[input_path] = header
diff --git a/tools/gn_utils.py b/tools/gn_utils.py
index d0417d7..904760e 100644
--- a/tools/gn_utils.py
+++ b/tools/gn_utils.py
@@ -529,9 +529,8 @@
     return metadata.get('exports', [])
 
   def get_proto_paths(self, proto_desc):
-    # import_dirs in metadata will be available for source_set targets.
     metadata = proto_desc.get('metadata', {})
-    return metadata.get('import_dirs', [])
+    return metadata.get('proto_import_dirs', [])
 
   def get_proto_target_type(self, target: Target
                            ) -> Tuple[Optional[str], Optional[Dict]]:
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
index 0817ebf..acf2ee8 100644
--- a/ui/src/common/flamegraph_util.ts
+++ b/ui/src/common/flamegraph_util.ts
@@ -24,7 +24,7 @@
   id: 'showHeapGraphDominatorTree',
   name: 'Show heap graph dominator tree',
   description: 'Show dominated size and objects tabs in Java heap graph view.',
-  defaultValue: false,
+  defaultValue: true,
 });
 
 export function viewingOptions(profileType: ProfileType): Array<ViewingOption> {
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 564b006..a9f8395 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -49,7 +49,6 @@
   'perfetto.Frames',
   'perfetto.FtraceRaw',
   'perfetto.HeapProfile',
-  'perfetto.NullTrack',
   'perfetto.PerfSamplesProfile',
   'perfetto.PivotTable',
   'perfetto.ProcessSummary',
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index caf2ef6..91b0939 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -30,6 +30,7 @@
 import {NewTrackArgs} from './track';
 import {CacheKey} from '../core/timeline_cache';
 import {featureFlags} from '../core/feature_flags';
+import {uuidv4Sql} from '../base/uuid';
 
 export const COUNTER_DEBUG_MENU_ITEMS = featureFlags.register({
   id: 'counterDebugMenuItems',
@@ -195,6 +196,7 @@
 export abstract class BaseCounterTrack implements Track {
   protected engine: EngineProxy;
   protected trackKey: string;
+  protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
   private countersKey: CacheKey = CacheKey.zero();
@@ -256,7 +258,7 @@
 
   constructor(args: BaseCounterTrackArgs) {
     this.engine = args.engine;
-    this.trackKey = args.trackKey.replaceAll('-', '_');
+    this.trackKey = args.trackKey;
     this.defaultOptions = args.options ?? {};
   }
 
@@ -449,6 +451,33 @@
 
   async onCreate(): Promise<void> {
     this.initState = await this.onInit();
+
+    const displayValueQuery = await this.engine.query(`
+        create virtual table ${this.getTableName()}
+        using __intrinsic_counter_mipmap((
+          SELECT
+            ts,
+            ${this.getValueExpression()} as value
+          FROM (${this.getSqlSource()})
+        ));
+
+        select
+          min_value as minDisplayValue,
+          max_value as maxDisplayValue
+        from ${this.getTableName()}(
+          trace_start(), trace_end(), trace_dur()
+        );
+      `);
+
+    const {minDisplayValue, maxDisplayValue} = displayValueQuery.firstRow({
+      minDisplayValue: NUM,
+      maxDisplayValue: NUM,
+    });
+
+    this.limits = {
+      minDisplayValue,
+      maxDisplayValue,
+    };
   }
 
   async onUpdate(): Promise<void> {
@@ -691,11 +720,14 @@
     this.hover = undefined;
   }
 
-  onDestroy(): void {
+  async onDestroy(): Promise<void> {
     if (this.initState) {
       this.initState.dispose();
       this.initState = undefined;
     }
+    if (this.engine.isAlive) {
+      await this.engine.query(`drop table if exists ${this.getTableName()}`);
+    }
   }
 
   // Compute the range of values to display and range label.
@@ -811,37 +843,11 @@
     }
   }
 
+  private getTableName(): string {
+    return `counter_${this.trackUuid}`;
+  }
+
   private async maybeRequestData(rawCountersKey: CacheKey) {
-    let limits = this.limits;
-    if (limits === undefined) {
-      const displayValueQuery = await this.engine.query(`
-        drop table if exists counter_${this.trackKey};
-
-        create virtual table counter_${this.trackKey}
-        using __intrinsic_counter_mipmap((
-          SELECT
-            ts,
-            ${this.getValueExpression()} as value
-          FROM (${this.getSqlSource()})
-        ));
-
-        select
-          min_value as minDisplayValue,
-          max_value as maxDisplayValue
-        from counter_${this.trackKey}(
-          trace_start(), trace_end(), trace_dur()
-        );
-      `);
-      const {minDisplayValue, maxDisplayValue} = displayValueQuery.firstRow({
-        minDisplayValue: NUM,
-        maxDisplayValue: NUM,
-      });
-      limits = this.limits = {
-        minDisplayValue,
-        maxDisplayValue,
-      };
-    }
-
     if (rawCountersKey.isCoveredBy(this.countersKey)) {
       return; // We have the data already, no need to re-query.
     }
@@ -859,7 +865,7 @@
         max_value as maxDisplayValue,
         last_ts as ts,
         last_value as lastDisplayValue
-      FROM counter_${this.trackKey}(
+      FROM ${this.getTableName()}(
         ${countersKey.start},
         ${countersKey.end},
         ${countersKey.bucketSize}
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index ca4fc41..e4b03a2 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -41,6 +41,7 @@
 import {DEFAULT_SLICE_LAYOUT, SliceLayout} from './slice_layout';
 import {NewTrackArgs} from './track';
 import {BUCKETS_PER_PIXEL, CacheKey} from '../core/timeline_cache';
+import {uuidv4Sql} from '../base/uuid';
 
 // The common class that underpins all tracks drawing slices.
 
@@ -174,6 +175,7 @@
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
   protected engine: EngineProxy;
   protected trackKey: string;
+  protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
   private slicesKey: CacheKey = CacheKey.zero();
@@ -307,6 +309,10 @@
     return `${size}px Roboto Condensed`;
   }
 
+  private getTableName(): string {
+    return `slice_${this.trackUuid}`;
+  }
+
   async onCreate(): Promise<void> {
     this.initState = await this.onInit();
 
@@ -357,7 +363,7 @@
     this.incomplete = incomplete;
 
     await this.engine.query(`
-      create virtual table slice_${this.trackKey}
+      create virtual table ${this.getTableName()}
       using __intrinsic_slice_mipmap((
         select id, ts, dur, ${this.depthColumn()}
         from (${this.getSqlSource()})
@@ -654,7 +660,9 @@
       this.initState.dispose();
       this.initState = undefined;
     }
-    await this.engine.execute(`drop table slice_${this.trackKey}`);
+    if (this.engine.isAlive) {
+      await this.engine.execute(`drop table ${this.getTableName()}`);
+    }
   }
 
   // This method figures out if the visible window is outside the bounds of
@@ -681,7 +689,7 @@
         s.id,
         z.depth
         ${extraCols ? ',' + extraCols : ''}
-      FROM slice_${this.trackKey}(
+      FROM ${this.getTableName()}(
         ${slicesKey.start},
         ${slicesKey.end},
         ${slicesKey.bucketSize}
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 084c14f..361480b 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -17,6 +17,7 @@
 import {BaseCounterTrack, CounterOptions} from './base_counter_track';
 import {CounterColumns, SqlDataSource} from './debug_tracks';
 import {Disposable, DisposableCallback} from '../base/disposable';
+import {uuidv4Sql} from '../base/uuid';
 
 export type SimpleCounterTrackConfig = {
   data: SqlDataSource;
@@ -39,7 +40,7 @@
       options: config.options,
     });
     this.config = config;
-    this.sqlTableName = `__simple_counter_${this.trackKey}`;
+    this.sqlTableName = `__simple_counter_${uuidv4Sql()}`;
   }
 
   async onInit(): Promise<Disposable> {
@@ -74,7 +75,7 @@
 
   private async dropTrackTable(): Promise<void> {
     if (this.engine.isAlive) {
-      this.engine.query(`drop table if exists ${this.sqlTableName}`);
+      await this.engine.query(`drop table if exists ${this.sqlTableName}`);
     }
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 2c13c76..166a007 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -64,15 +64,17 @@
       sf_callback_missed_frames,
       hwui_callback_missed_frames,
       cuj_layer.layer_name,
-      cuj.ts,
-      cuj.dur,
+      /* Boundaries table doesn't contain ts and dur when a CUJ didn't complete successfully.
+        In that case we still want to show that it was canceled, so let's take the slice timestamps. */
+      CASE WHEN boundaries.ts IS NOT NULL THEN boundaries.ts ELSE cuj.ts END AS ts,
+      CASE WHEN boundaries.dur IS NOT NULL THEN boundaries.dur ELSE cuj.dur END AS dur,
       cuj.track_id,
       cuj.slice_id
     FROM slice AS cuj
-           JOIN process_track AS pt
-                ON cuj.track_id = pt.id
+           JOIN process_track AS pt ON cuj.track_id = pt.id
            LEFT JOIN android_jank_cuj jc
                      ON pt.upid = jc.upid AND cuj.name = jc.cuj_slice_name AND cuj.ts = jc.ts
+           LEFT JOIN android_jank_cuj_main_thread_cuj_boundary boundaries using (cuj_id)
            LEFT JOIN android_jank_cuj_layer_name cuj_layer USING (cuj_id)
            LEFT JOIN android_jank_cuj_counter_metrics USING (cuj_id)
     WHERE cuj.name GLOB 'J<*>'
@@ -140,7 +142,7 @@
             },
             'Jank CUJs',
             {ts: 'ts', dur: 'dur', name: 'name'},
-            [],
+            JANK_COLUMNS,
           );
         });
       },
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index 92e30f9..b5fa5fa 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -31,6 +31,7 @@
   Track,
 } from '../../public';
 import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
 
 export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
 
@@ -63,30 +64,29 @@
 
   private engine: EngineProxy;
   private config: Config;
-  private trackKey: string;
+  private trackUuid = uuidv4Sql();
 
-  constructor(config: Config, engine: EngineProxy, trackKey: string) {
+  constructor(config: Config, engine: EngineProxy) {
     this.config = config;
     this.engine = engine;
-    this.trackKey = trackKey.split('-').join('_');
   }
 
   async onCreate() {
     if (this.config.idleTrackId === undefined) {
       await this.engine.execute(`
-        create view raw_freq_idle_${this.trackKey} as
+        create view raw_freq_idle_${this.trackUuid} as
         select ts, dur, value as freqValue, -1 as idleValue
         from experimental_counter_dur c
         where track_id = ${this.config.freqTrackId}
       `);
     } else {
       await this.engine.execute(`
-        create view raw_freq_${this.trackKey} as
+        create view raw_freq_${this.trackUuid} as
         select ts, dur, value as freqValue
         from experimental_counter_dur c
         where track_id = ${this.config.freqTrackId};
 
-        create view raw_idle_${this.trackKey} as
+        create view raw_idle_${this.trackUuid} as
         select
           ts,
           dur,
@@ -94,22 +94,22 @@
         from experimental_counter_dur c
         where track_id = ${this.config.idleTrackId};
 
-        create virtual table raw_freq_idle_${this.trackKey}
-        using span_join(raw_freq_${this.trackKey}, raw_idle_${this.trackKey});
+        create virtual table raw_freq_idle_${this.trackUuid}
+        using span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid});
       `);
     }
 
     await this.engine.execute(`
-      create virtual table cpu_freq_${this.trackKey}
+      create virtual table cpu_freq_${this.trackUuid}
       using __intrinsic_counter_mipmap((
         select ts, freqValue as value
-        from raw_freq_idle_${this.trackKey}
+        from raw_freq_idle_${this.trackUuid}
       ));
 
-      create virtual table cpu_idle_${this.trackKey}
+      create virtual table cpu_idle_${this.trackUuid}
       using __intrinsic_counter_mipmap((
         select ts, idleValue as value
-        from raw_freq_idle_${this.trackKey}
+        from raw_freq_idle_${this.trackUuid}
       ));
     `);
   }
@@ -120,11 +120,11 @@
 
   async onDestroy(): Promise<void> {
     if (this.engine.isAlive) {
-      await this.engine.query(`drop table cpu_freq_${this.trackKey}`);
-      await this.engine.query(`drop table cpu_idle_${this.trackKey}`);
-      await this.engine.query(`drop table raw_freq_idle_${this.trackKey}`);
-      await this.engine.query(`drop view if exists raw_freq_${this.trackKey}`);
-      await this.engine.query(`drop view if exists raw_idle_${this.trackKey}`);
+      await this.engine.query(`drop table cpu_freq_${this.trackUuid}`);
+      await this.engine.query(`drop table cpu_idle_${this.trackUuid}`);
+      await this.engine.query(`drop table raw_freq_idle_${this.trackUuid}`);
+      await this.engine.query(`drop view if exists raw_freq_${this.trackUuid}`);
+      await this.engine.query(`drop view if exists raw_idle_${this.trackUuid}`);
     }
   }
 
@@ -143,7 +143,7 @@
         max_value as maxFreq,
         last_ts as ts,
         last_value as lastFreq
-      FROM cpu_freq_${this.trackKey}(
+      FROM cpu_freq_${this.trackUuid}(
         ${start},
         ${end},
         ${resolution}
@@ -151,7 +151,7 @@
     `);
     const idleResult = await this.engine.query(`
       SELECT last_value as lastIdle
-      FROM cpu_idle_${this.trackKey}(
+      FROM cpu_idle_${this.trackUuid}(
         ${start},
         ${end},
         ${resolution}
@@ -450,7 +450,7 @@
           displayName: `Cpu ${cpu} Frequency`,
           kind: CPU_FREQ_TRACK_KIND,
           cpu,
-          trackFactory: (c) => new CpuFreqTrack(config, ctx.engine, c.trackKey),
+          trackFactory: () => new CpuFreqTrack(config, ctx.engine),
         });
       }
     }
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 552092c..5c042dc 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -40,6 +40,7 @@
   Track,
 } from '../../public';
 import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
 
 export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
 
@@ -69,6 +70,7 @@
   private engine: EngineProxy;
   private cpu: number;
   private trackKey: string;
+  private trackUuid = uuidv4Sql();
 
   constructor(engine: EngineProxy, trackKey: string, cpu: number) {
     this.engine = engine;
@@ -78,7 +80,7 @@
 
   async onCreate() {
     await this.engine.query(`
-      create virtual table cpu_slice_${this.trackKey}
+      create virtual table cpu_slice_${this.trackUuid}
       using __intrinsic_slice_mipmap((
         select
           id,
@@ -116,7 +118,7 @@
         s.id,
         s.dur = -1 as isIncomplete,
         ifnull(s.priority < 100, 0) as isRealtime
-      from cpu_slice_${this.trackKey}(${start}, ${end}, ${resolution}) z
+      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
       cross join sched s using (id)
     `);
 
@@ -165,7 +167,7 @@
   async onDestroy() {
     if (this.engine.isAlive) {
       await this.engine.query(
-        `drop table if exists cpu_slice_${this.trackKey}`,
+        `drop table if exists cpu_slice_${this.trackUuid}`,
       );
     }
     this.fetcher.dispose();
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index 37ac68f..ea6845f 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -103,8 +103,8 @@
           tags: {
             isDebuggable,
           },
-          trackFactory: ({trackKey}) => {
-            return new ProcessSchedulingTrack(ctx.engine, trackKey, config);
+          trackFactory: () => {
+            return new ProcessSchedulingTrack(ctx.engine, config);
           },
         });
       } else {
diff --git a/ui/src/tracks/process_summary/process_scheduling_track.ts b/ui/src/tracks/process_summary/process_scheduling_track.ts
index 5905de2..9725c29 100644
--- a/ui/src/tracks/process_summary/process_scheduling_track.ts
+++ b/ui/src/tracks/process_summary/process_scheduling_track.ts
@@ -27,6 +27,7 @@
 import {PanelSize} from '../../frontend/panel';
 import {EngineProxy, Track} from '../../public';
 import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
 
 export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
 
@@ -57,13 +58,12 @@
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
   private maxCpu = 0;
   private engine: EngineProxy;
-  private trackKey: string;
+  private trackUuid = uuidv4Sql();
   private config: Config;
 
-  constructor(engine: EngineProxy, trackKey: string, config: Config) {
+  constructor(engine: EngineProxy, config: Config) {
     this.engine = engine;
     this.config = config;
-    this.trackKey = trackKey.split('-').join('_');
   }
 
   async onCreate(): Promise<void> {
@@ -75,7 +75,7 @@
 
     if (this.config.upid !== null) {
       await this.engine.query(`
-        create virtual table process_scheduling_${this.trackKey}
+        create virtual table process_scheduling_${this.trackUuid}
         using __intrinsic_slice_mipmap((
           select
             id,
@@ -95,7 +95,7 @@
     } else {
       assertExists(this.config.utid);
       await this.engine.query(`
-        create virtual table process_scheduling_${this.trackKey}
+        create virtual table process_scheduling_${this.trackUuid}
         using __intrinsic_slice_mipmap((
           select
             id,
@@ -121,7 +121,7 @@
     this.fetcher.dispose();
     if (this.engine.isAlive) {
       await this.engine.query(`
-        drop table process_scheduling_${this.trackKey}
+        drop table process_scheduling_${this.trackUuid}
       `);
     }
   }
@@ -182,7 +182,7 @@
         s.id,
         z.depth as cpu,
         utid
-      from process_scheduling_${this.trackKey}(
+      from process_scheduling_${this.trackUuid}(
         ${start}, ${end}, ${bucketSize}
       ) z
       cross join sched s using (id)