Merge "Update chrome_input_with_frame_view.pftrace.sh256 to point to the most current file." 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/assets/details.scss b/ui/src/assets/details.scss
index d09690a..ff66708 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -446,112 +446,46 @@
header.stale {
color: grey;
}
+}
- .rows {
- position: relative;
- direction: ltr;
- width: 100%;
+.pf-ftrace-explorer {
+ height: 100%;
+ font-size: 11px;
+ font-family: var(--monospace-font);
- .row {
- @include transition();
- position: absolute;
- width: 100%;
- height: 20px;
- line-height: 20px;
- background-color: hsl(214, 22%, 100%);
+ .colour {
+ display: inline-block;
+ height: 10px;
+ width: 10px;
+ margin-right: 4px;
+ }
+}
- &.D {
- color: hsl(122, 20%, 40%);
- }
- &.V {
- color: hsl(122, 20%, 30%);
- }
- &.I {
- color: hsl(0, 0%, 20%);
- }
- &.W {
- color: hsl(45, 60%, 45%);
- }
- &.E {
- color: hsl(4, 90%, 58%);
- }
- &.F {
- color: hsl(291, 64%, 42%);
- }
- &.stale {
- color: #aaa;
- }
- &:nth-child(even) {
- background-color: hsl(214, 22%, 95%);
- }
- &:hover {
- background-color: $table-hover-color;
- }
- .cell {
- font-size: 11px;
- font-family: var(--monospace-font);
- white-space: nowrap;
- overflow: scroll;
- padding-left: 10px;
- padding-right: 10px;
- display: inline-block;
- &:first-child {
- padding-left: 5px;
- }
- &:last-child {
- padding-right: 5px;
- }
- &:only-child {
- width: 100%;
- }
+.pf-android-logs-table {
+ height: 100%;
+ font-size: 11px;
+ font-family: var(--monospace-font);
- // The following children will be used as columns in the table showing
- // Android logs.
-
- // 1.Timestamp
- &:nth-child(1) {
- width: 7rem;
- text-overflow: clip;
- text-align: right;
- }
- // 2.Level
- &:nth-child(2) {
- width: 4rem;
- }
- // 3.Tag
- &:nth-child(3) {
- width: 13rem;
- }
-
- &.with-process {
- // 4.Process name
- &:nth-child(4) {
- width: 18rem;
- }
- // 5.Message - a long string, will take most of the display space.
- &:nth-child(5) {
- width: calc(100% - 42rem);
- }
- }
-
- &.no-process {
- // 4.Message - a long string, will take most of the display space.
- &:nth-child(4) {
- width: calc(100% - 24rem);
- }
- }
-
- &.row-header {
- text-align: left;
- font-weight: bold;
- font-size: 13px;
- }
-
- &.row-header:first-child {
- padding-left: 15px;
- }
- }
- }
+ .D {
+ color: hsl(122, 20%, 40%);
+ }
+ .V {
+ color: hsl(122, 20%, 30%);
+ }
+ .I {
+ color: hsl(0, 0%, 20%);
+ }
+ .W {
+ color: hsl(45, 60%, 45%);
+ }
+ .E {
+ color: hsl(4, 90%, 58%);
+ }
+ .F {
+ color: hsl(291, 64%, 42%);
+ }
+ .pf-highlighted {
+ background: #d2efe0;
}
}
@@ -559,109 +493,6 @@
margin: 10px;
}
-.ftrace-panel {
- display: contents;
-
- .sticky {
- position: sticky;
- top: 0;
- left: 0;
- z-index: 1;
- background-color: white;
- color: #3c4b5d;
- padding: 5px 10px;
- display: grid;
- grid-template-columns: auto auto;
- justify-content: space-between;
- }
-
- .ftrace-rows-label {
- display: flex;
- align-items: center;
- }
-
- header.stale {
- color: grey;
- }
-
- .rows {
- position: relative;
- direction: ltr;
- min-width: 100%;
- font-size: 12px;
-
- .row {
- @include transition();
- position: absolute;
- min-width: 100%;
- line-height: 20px;
- background-color: hsl(214, 22%, 100%);
- white-space: nowrap;
-
- &:nth-child(even) {
- background-color: hsl(214, 22%, 95%);
- }
-
- &:hover {
- background-color: $table-hover-color;
- }
-
- .cell {
- font-family: var(--monospace-font);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- margin-right: 8px;
- display: inline-block;
-
- .colour {
- display: inline-block;
- height: 10px;
- width: 10px;
- margin-right: 4px;
- }
-
- &:first-child {
- margin-left: 8px;
- }
-
- &:last-child {
- margin-right: 8px;
- }
-
- &:only-child {
- width: 100%;
- }
-
- // Timestamp
- &:nth-child(1) {
- width: 13em;
- // text-align: right;
- }
-
- // Name
- &:nth-child(2) {
- width: 24em;
- }
-
- // CPU
- &:nth-child(3) {
- width: 3em;
- }
-
- // Process
- &:nth-child(4) {
- width: 24em;
- }
-
- &.row-header {
- font-weight: bold;
- }
- }
- }
- }
-}
-
.screenshot-panel {
height: 100%;
img {
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 8bb1cdc..567deae 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -56,3 +56,4 @@
@import "widgets/hotkey";
@import "widgets/text_paragraph";
@import "widgets/treetable";
+@import "widgets/virtual_table";
diff --git a/ui/src/assets/widgets/virtual_table.scss b/ui/src/assets/widgets/virtual_table.scss
new file mode 100644
index 0000000..acd22d5
--- /dev/null
+++ b/ui/src/assets/widgets/virtual_table.scss
@@ -0,0 +1,89 @@
+// 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";
+@import "theme";
+
+// Adding these to a new layer makes other rules take precedence
+@layer widgets {
+ .pf-vtable {
+ overflow: auto;
+ font-family: $pf-font;
+ position: relative;
+ background: white; // Performance tweak - see b/335451611
+
+ .pf-vtable-content {
+ display: inline-flex;
+ flex-direction: column;
+ min-width: 100%;
+
+ .pf-vtable-header {
+ font-weight: bold;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: white;
+ white-space: nowrap;
+ padding-inline: 4px;
+
+ // A shadow improves distinction between header and content
+ box-shadow: #0001 0px 0px 8px;
+ }
+
+ .pf-vtable-data {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-right: 8px;
+ display: inline-block;
+ }
+
+ .pf-vtable-slider {
+ overflow: hidden;
+
+ // Necessary trig because we have a 45deg stripes
+ $pattern-density: 1px * math.sqrt(2);
+ $pattern-col: #ddd;
+ overflow: hidden;
+
+ background: repeating-linear-gradient(
+ -45deg,
+ $pattern-col,
+ $pattern-col $pattern-density,
+ white $pattern-density,
+ white $pattern-density * 2
+ );
+
+ .pf-vtable-puck {
+ .pf-vtable-row {
+ white-space: nowrap;
+ padding-inline: 4px;
+
+ &:nth-child(odd) {
+ background-color: hsl(214, 22%, 95%);
+ }
+
+ &:nth-child(even) {
+ background-color: white;
+ }
+
+ &:hover {
+ background-color: $table-hover-color;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/base/geom.ts b/ui/src/base/geom.ts
index f569400..5a20023 100644
--- a/ui/src/base/geom.ts
+++ b/ui/src/base/geom.ts
@@ -24,6 +24,11 @@
readonly height: number;
}
+export interface Vector {
+ readonly x: number;
+ readonly y: number;
+}
+
export function intersectRects(a: Rect, b: Rect): Rect {
return {
top: Math.max(a.top, b.top),
@@ -57,3 +62,28 @@
height: r.bottom - r.top,
};
}
+
+/**
+ * Return true if rect a contains rect b.
+ *
+ * @param a A rect.
+ * @param b Another rect.
+ * @returns True if rect a contains rect b, false otherwise.
+ */
+export function containsRect(a: Rect, b: Rect): boolean {
+ return !(
+ b.top < a.top ||
+ b.bottom > a.bottom ||
+ b.left < a.left ||
+ b.right > a.right
+ );
+}
+
+export function translateRect(a: Rect, b: Vector): Rect {
+ return {
+ top: a.top + b.y,
+ left: a.left + b.x,
+ bottom: a.bottom + b.y,
+ right: a.right + b.x,
+ };
+}
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/common/plugins.ts b/ui/src/common/plugins.ts
index ba60270..0750e2e 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -299,11 +299,20 @@
},
get tracks(): TrackRef[] {
- return Object.values(globals.state.tracks).map((trackState) => {
+ const tracks = Object.values(globals.state.tracks);
+ const pinnedTracks = globals.state.pinnedTracks;
+ const groups = globals.state.trackGroups;
+ return tracks.map((trackState) => {
+ const group = trackState.trackGroup
+ ? groups[trackState.trackGroup]
+ : undefined;
return {
displayName: trackState.name,
uri: trackState.uri,
params: trackState.params,
+ key: trackState.key,
+ groupName: group?.name,
+ isPinned: pinnedTracks.includes(trackState.key),
};
});
},
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 583e6bc..a9f8395 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -32,6 +32,7 @@
'dev.perfetto.BookmarkletApi',
'dev.perfetto.CoreCommands',
'dev.perfetto.LargeScreensPerf',
+ 'dev.perfetto.RestorePinnedTrack',
'perfetto.AndroidLog',
'perfetto.Annotation',
'perfetto.AsyncSlices',
@@ -48,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/css_constants.ts b/ui/src/frontend/css_constants.ts
index 1a37c1e..f756136 100644
--- a/ui/src/frontend/css_constants.ts
+++ b/ui/src/frontend/css_constants.ts
@@ -23,7 +23,6 @@
export let SELECTION_FILL_COLOR = '#8398e64d';
export let OVERVIEW_TIMELINE_NON_VISIBLE_COLOR = '#c8c8c8cc';
export let DEFAULT_DETAILS_CONTENT_HEIGHT = 280;
-export const SELECTED_LOG_ROWS_COLOR = '#D2EFE0';
export let BACKGROUND_COLOR = '#ffffff';
export let FOREGROUND_COLOR = '#222';
export let COLLAPSED_BACKGROUND = '#ffffff';
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/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index ea80972..65fb04e 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -50,6 +50,11 @@
import {TableShowcase} from './tables/table_showcase';
import {TreeTable, TreeTableAttrs} from './widgets/treetable';
import {Intent} from '../widgets/common';
+import {
+ VirtualTable,
+ VirtualTableAttrs,
+ VirtualTableRow,
+} from '../widgets/virtual_table';
const DATA_ENGLISH_LETTER_FREQUENCY = {
table: [
@@ -569,6 +574,11 @@
},
];
+let virtualTableData: {offset: number; rows: VirtualTableRow[]} = {
+ offset: 0,
+ rows: [],
+};
+
export const WidgetsPage = createPage({
view() {
return m(
@@ -1154,10 +1164,38 @@
return m(TreeTable<File>, attrs);
},
}),
+ m(WidgetShowcase, {
+ label: 'VirtualTable',
+ description: `Virtualized table for efficient rendering of large datasets`,
+ renderWidget: () => {
+ const attrs: VirtualTableAttrs = {
+ columns: [
+ {header: 'x', width: '4em'},
+ {header: 'x^2', width: '8em'},
+ ],
+ rows: virtualTableData.rows,
+ firstRowOffset: virtualTableData.offset,
+ rowHeight: 20,
+ numRows: 500_000,
+ style: {height: '200px'},
+ onReload: (rowOffset, rowCount) => {
+ const rows = [];
+ for (let i = rowOffset; i < rowOffset + rowCount; i++) {
+ rows.push({id: i, cells: [i, i ** 2]});
+ }
+ virtualTableData = {
+ offset: rowOffset,
+ rows,
+ };
+ raf.scheduleFullRedraw();
+ },
+ };
+ return m(VirtualTable, attrs);
+ },
+ }),
);
},
});
-
class ModalShowcase implements m.ClassComponent {
private static counter = 0;
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/plugins/dev.perfetto.RestorePinnedTracks/OWNERS b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/OWNERS
new file mode 100644
index 0000000..987684d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/OWNERS
@@ -0,0 +1,2 @@
+nicomazz@google.com
+nickchameyev@google.com
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
new file mode 100644
index 0000000..81036db
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -0,0 +1,135 @@
+// 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 {
+ Plugin,
+ PluginContext,
+ PluginContextTrace,
+ PluginDescriptor,
+ TrackRef,
+} from '../../public';
+
+const PLUGIN_ID = 'dev.perfetto.RestorePinnedTrack';
+const SAVED_TRACKS_KEY = `${PLUGIN_ID}#savedPerfettoTracks`;
+
+/**
+ * Fuzzy save and restore of pinned tracks.
+ *
+ * Tries to persist pinned tracks. Uses full string matching between track name
+ * and group name. When no match is found for a saved track, it tries again
+ * without numbers.
+ */
+class RestorePinnedTrack implements Plugin {
+ onActivate(_ctx: PluginContext): void {}
+
+ private ctx!: PluginContextTrace;
+
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ this.ctx = ctx;
+ ctx.registerCommand({
+ id: `${PLUGIN_ID}#save`,
+ name: 'Save: Pinned tracks',
+ callback: () => {
+ this.saveTracks();
+ },
+ });
+ ctx.registerCommand({
+ id: `${PLUGIN_ID}#restore`,
+ name: 'Restore: Pinned tracks',
+ callback: () => {
+ this.restoreTracks();
+ },
+ });
+ }
+
+ private saveTracks() {
+ const pinnedTracks = this.ctx.timeline.tracks.filter(
+ (trackRef) => trackRef.isPinned,
+ );
+ const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((trackRef) => ({
+ groupName: trackRef.groupName,
+ trackName: trackRef.displayName,
+ }));
+ window.localStorage.setItem(SAVED_TRACKS_KEY, JSON.stringify(tracksToSave));
+ }
+
+ private restoreTracks() {
+ const savedTracks = window.localStorage.getItem(SAVED_TRACKS_KEY);
+ if (!savedTracks) {
+ alert('No saved tracks. Use the Save command first');
+ return;
+ }
+ const tracksToRestore: SavedPinnedTrack[] = JSON.parse(savedTracks);
+ const tracks: TrackRef[] = this.ctx.timeline.tracks;
+ tracksToRestore.forEach((trackToRestore) => {
+ // Check for an exact match
+ const exactMatch = tracks.find((track) => {
+ return (
+ track.key &&
+ trackToRestore.trackName === track.displayName &&
+ trackToRestore.groupName === track.groupName
+ );
+ });
+
+ if (exactMatch) {
+ this.ctx.timeline.pinTrack(exactMatch.key!);
+ } else {
+ // We attempt a match after removing numbers to potentially pin a
+ // "similar" track from a different trace. Removing numbers allows
+ // flexibility; for instance, with multiple 'sysui' processes (e.g.
+ // track group name: "com.android.systemui 123") without this approach,
+ // any could be mistakenly pinned. The goal is to restore specific
+ // tracks within the same trace, ensuring that a previously pinned track
+ // is pinned again.
+ // If the specific process with that PID is unavailable, pinning any
+ // other process matching the package name is attempted.
+ const fuzzyMatch = tracks.find((track) => {
+ return (
+ track.key &&
+ this.removeNumbers(trackToRestore.trackName) ===
+ this.removeNumbers(track.displayName) &&
+ this.removeNumbers(trackToRestore.groupName) ===
+ this.removeNumbers(track.groupName)
+ );
+ });
+
+ if (fuzzyMatch) {
+ this.ctx.timeline.pinTrack(fuzzyMatch.key!);
+ } else {
+ console.warn(
+ '[RestorePinnedTracks] No track found that matches',
+ trackToRestore,
+ );
+ }
+ }
+ });
+ }
+
+ private removeNumbers(inputString?: string): string | undefined {
+ return inputString?.replace(/\d+/g, '');
+ }
+}
+
+interface SavedPinnedTrack {
+ // Optional: group name for the track. Usually matches with process name.
+ groupName?: string;
+
+ // Track name to restore.
+ trackName: string;
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: PLUGIN_ID,
+ plugin: RestorePinnedTrack,
+};
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index ece6ea0..f3e4338 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -482,6 +482,12 @@
// Optional: Add tracks to a group with this name.
groupName?: string;
+
+ // Optional: Track key
+ key?: string;
+
+ // Optional: Whether the track is pinned
+ isPinned?: boolean;
}
// A predicate for selecting a subset of tracks.
diff --git a/ui/src/tracks/android_log/logs_panel.ts b/ui/src/tracks/android_log/logs_panel.ts
index 51e0889..3970190 100644
--- a/ui/src/tracks/android_log/logs_panel.ts
+++ b/ui/src/tracks/android_log/logs_panel.ts
@@ -18,12 +18,10 @@
import {Actions} from '../../common/actions';
import {raf} from '../../core/raf_scheduler';
import {DetailsShell} from '../../widgets/details_shell';
-import {VirtualScrollContainer} from '../../widgets/virtual_scroll_container';
-import {SELECTED_LOG_ROWS_COLOR} from '../../frontend/css_constants';
import {globals} from '../../frontend/globals';
import {Timestamp} from '../../frontend/widgets/timestamp';
-import {createStore, EngineProxy, LONG, NUM, Store, STR} from '../../public';
+import {EngineProxy, LONG, NUM, NUM_NULL, Store, STR} from '../../public';
import {Monitor} from '../../base/monitor';
import {AsyncLimiter} from '../../base/async_limiter';
import {escapeGlob, escapeQuery} from '../../trace_processor/query_utils';
@@ -31,6 +29,8 @@
import {Button} from '../../widgets/button';
import {TextInput} from '../../widgets/text_input';
import {Intent} from '../../widgets/common';
+import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
+import {classNames} from '../../base/classnames';
const ROW_H = 20;
@@ -63,15 +63,12 @@
}
export class LogPanel implements m.ClassComponent<LogPanelAttrs> {
- private readonly SKIRT_SIZE = 50;
private entries?: LogEntries;
- private isStale = true;
- private viewportBounds = {top: 0, bottom: 0};
- private readonly paginationStore = createStore<Pagination>({
+ private pagination: Pagination = {
offset: 0,
count: 0,
- });
+ };
private readonly rowsMonitor: Monitor;
private readonly filterMonitor: Monitor;
private readonly queryLimiter = new AsyncLimiter();
@@ -81,7 +78,6 @@
() => attrs.filterStore.state,
() => globals.state.frontendLocalState.visibleState.start,
() => globals.state.frontendLocalState.visibleState.end,
- () => this.paginationStore.state,
]);
this.filterMonitor = new Monitor([() => attrs.filterStore.state]);
@@ -89,148 +85,104 @@
view({attrs}: m.CVnode<LogPanelAttrs>) {
if (this.rowsMonitor.ifStateChanged()) {
- this.queryLimiter.schedule(async () => {
- this.isStale = true;
- raf.scheduleFullRedraw();
-
- const visibleState = globals.state.frontendLocalState.visibleState;
- const visibleSpan = new TimeSpan(visibleState.start, visibleState.end);
-
- if (this.filterMonitor.ifStateChanged()) {
- await updateLogView(attrs.engine, attrs.filterStore.state);
- }
-
- this.entries = await updateLogEntries(
- attrs.engine,
- visibleSpan,
- this.paginationStore.state,
- );
-
- raf.scheduleFullRedraw();
- this.isStale = false;
- });
+ this.reloadData(attrs);
}
const hasProcessNames =
this.entries &&
this.entries.processName.filter((name) => name).length > 0;
+ const totalEvents = this.entries?.totalEvents ?? 0;
- const rows: m.Children = [];
- rows.push(
- m(
- `.row`,
- m('.cell.row-header', 'Timestamp'),
- m('.cell.row-header', 'Level'),
- m('.cell.row-header', 'Tag'),
- hasProcessNames
- ? m('.cell.with-process.row-header', 'Process name')
- : undefined,
- hasProcessNames
- ? m('.cell.with-process.row-header', 'Message')
- : m('.cell.no-process.row-header', 'Message'),
- m('br'),
- ),
- );
- if (this.entries) {
- const offset = this.entries.offset;
- const timestamps = this.entries.timestamps;
- const priorities = this.entries.priorities;
- const tags = this.entries.tags;
- const messages = this.entries.messages;
- const processNames = this.entries.processName;
- const totalEvents = this.entries.totalEvents;
-
- for (let i = 0; i < this.entries.timestamps.length; i++) {
- const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
- const ts = timestamps[i];
- const prioClass = priorityLetter || '';
- const style: {top: string; backgroundColor?: string} = {
- // 1.5 is for the width of the header
- top: `${(offset + i + 1.5) * ROW_H}px`,
- };
- if (this.entries.isHighlighted[i]) {
- style.backgroundColor = SELECTED_LOG_ROWS_COLOR;
- }
-
- rows.push(
- m(
- `.row.${prioClass}`,
- {
- class: this.isStale ? 'stale' : '',
- style,
- onmouseover: () => {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
- },
- onmouseout: () => {
- globals.dispatch(
- Actions.setHoverCursorTimestamp({ts: Time.INVALID}),
- );
- },
- },
- m('.cell', m(Timestamp, {ts})),
- m('.cell', priorityLetter || '?'),
- m('.cell', tags[i]),
- hasProcessNames
- ? m('.cell.with-process', processNames[i])
- : undefined,
- hasProcessNames
- ? m('.cell.with-process', messages[i])
- : m('.cell.no-process', messages[i]),
- m('br'),
- ),
- );
- }
-
- return m(
- DetailsShell,
- {
- title: 'Android Logs',
- description: `[${this.viewportBounds.top}, ${this.viewportBounds.bottom}] / ${totalEvents}`,
- buttons: m(LogsFilters, {store: attrs.filterStore}),
+ return m(
+ DetailsShell,
+ {
+ title: 'Android Logs',
+ description: `Total messages: ${totalEvents}`,
+ buttons: m(LogsFilters, {store: attrs.filterStore}),
+ },
+ m(VirtualTable, {
+ className: 'pf-android-logs-table',
+ columns: [
+ {header: 'Timestamp', width: '7rem'},
+ {header: 'Level', width: '4rem'},
+ {header: 'Tag', width: '13rem'},
+ ...(hasProcessNames ? [{header: 'Process', width: '18rem'}] : []),
+ {header: 'Message', width: '42rem'},
+ ],
+ rows: this.renderRows(hasProcessNames),
+ firstRowOffset: this.entries?.offset ?? 0,
+ numRows: this.entries?.totalEvents ?? 0,
+ rowHeight: ROW_H,
+ onReload: (offset, count) => {
+ this.pagination = {offset, count};
+ this.reloadData(attrs);
},
- m(
- VirtualScrollContainer,
- {
- onScroll: (scrollContainer: HTMLElement) => {
- this.recomputeVisibleRowsAndUpdate(scrollContainer);
- raf.scheduleFullRedraw();
- },
- },
- m(
- '.log-panel',
- m('.rows', {style: {height: `${totalEvents * ROW_H}px`}}, rows),
- ),
- ),
- );
- }
-
- return null;
+ onRowHover: (id) => {
+ const timestamp = this.entries?.timestamps[id];
+ if (timestamp !== undefined) {
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts: timestamp}));
+ }
+ },
+ onRowOut: () => {
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
+ },
+ }),
+ );
}
- recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
- const viewportTop = Math.floor(scrollContainer.scrollTop / ROW_H);
- const viewportHeight = Math.ceil(scrollContainer.clientHeight / ROW_H);
- const viewportBottom = viewportTop + viewportHeight;
+ private reloadData(attrs: LogPanelAttrs) {
+ this.queryLimiter.schedule(async () => {
+ const visibleState = globals.state.frontendLocalState.visibleState;
+ const visibleSpan = new TimeSpan(visibleState.start, visibleState.end);
- this.viewportBounds = {
- top: viewportTop,
- bottom: viewportBottom,
- };
+ if (this.filterMonitor.ifStateChanged()) {
+ await updateLogView(attrs.engine, attrs.filterStore.state);
+ }
- const curPage = this.paginationStore.state;
+ this.entries = await updateLogEntries(
+ attrs.engine,
+ visibleSpan,
+ this.pagination,
+ );
- if (
- viewportTop < curPage.offset ||
- viewportBottom >= curPage.offset + curPage.count
- ) {
- this.paginationStore.edit((draft) => {
- const offset = Math.max(0, viewportTop - this.SKIRT_SIZE);
- // Make it even so alternating coloured rows line up
- const offsetEven = Math.floor(offset / 2) * 2;
- draft.offset = offsetEven;
- draft.count = viewportHeight + this.SKIRT_SIZE * 2;
+ raf.scheduleFullRedraw();
+ });
+ }
+
+ private renderRows(hasProcessNames: boolean | undefined): VirtualTableRow[] {
+ if (!this.entries) {
+ return [];
+ }
+
+ const timestamps = this.entries.timestamps;
+ const priorities = this.entries.priorities;
+ const tags = this.entries.tags;
+ const messages = this.entries.messages;
+ const processNames = this.entries.processName;
+
+ const rows: VirtualTableRow[] = [];
+ for (let i = 0; i < this.entries.timestamps.length; i++) {
+ const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
+ const ts = timestamps[i];
+ const prioClass = priorityLetter || '';
+
+ rows.push({
+ id: i,
+ className: classNames(
+ prioClass,
+ this.entries.isHighlighted[i] && 'pf-highlighted',
+ ),
+ cells: [
+ m(Timestamp, {ts}),
+ priorityLetter || '?',
+ tags[i],
+ ...(hasProcessNames ? [processNames[i]] : []),
+ messages[i],
+ ],
});
}
+
+ return rows;
}
}
@@ -460,7 +412,7 @@
prio: NUM,
tag: STR,
msg: STR,
- isMsgHighlighted: NUM,
+ isMsgHighlighted: NUM_NULL,
isProcessHighlighted: NUM,
processName: STR,
});
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/ftrace/ftrace_explorer.ts b/ui/src/tracks/ftrace/ftrace_explorer.ts
index 9481b86..5e7eb32 100644
--- a/ui/src/tracks/ftrace/ftrace_explorer.ts
+++ b/ui/src/tracks/ftrace/ftrace_explorer.ts
@@ -24,27 +24,18 @@
PopupMultiSelect,
} from '../../widgets/multiselect';
import {PopupPosition} from '../../widgets/popup';
-import {VirtualScrollContainer} from '../../widgets/virtual_scroll_container';
import {globals} from '../../frontend/globals';
import {Timestamp} from '../../frontend/widgets/timestamp';
import {FtraceFilter, FtraceStat} from './common';
-import {
- createStore,
- EngineProxy,
- LONG,
- NUM,
- Store,
- STR,
- STR_NULL,
-} from '../../public';
+import {EngineProxy, LONG, NUM, Store, STR, STR_NULL} from '../../public';
import {raf} from '../../core/raf_scheduler';
import {AsyncLimiter} from '../../base/async_limiter';
import {Monitor} from '../../base/monitor';
import {Button} from '../../widgets/button';
+import {VirtualTable, VirtualTableRow} from '../../widgets/virtual_table';
const ROW_H = 20;
-const PAGE_SIZE = 250;
interface FtraceExplorerAttrs {
cache: FtraceExplorerCache;
@@ -69,8 +60,8 @@
}
interface Pagination {
- page: number;
- pageCount: number;
+ offset: number;
+ count: number;
}
export interface FtraceExplorerCache {
@@ -104,10 +95,10 @@
}
export class FtraceExplorer implements m.ClassComponent<FtraceExplorerAttrs> {
- private readonly paginationStore = createStore<Pagination>({
- page: 0,
- pageCount: 0,
- });
+ private pagination: Pagination = {
+ offset: 0,
+ count: 0,
+ };
private readonly monitor: Monitor;
private readonly queryLimiter = new AsyncLimiter();
@@ -119,7 +110,6 @@
() => globals.state.frontendLocalState.visibleState.start,
() => globals.state.frontendLocalState.visibleState.end,
() => attrs.filterStore.state,
- () => this.paginationStore.state,
]);
if (attrs.cache.state === 'blank') {
@@ -136,60 +126,85 @@
}
view({attrs}: m.CVnode<FtraceExplorerAttrs>) {
- this.monitor.ifStateChanged(() =>
- this.queryLimiter.schedule(async () => {
- this.data = await lookupFtraceEvents(
- attrs.engine,
- this.paginationStore.state.page * PAGE_SIZE,
- this.paginationStore.state.pageCount * PAGE_SIZE,
- attrs.filterStore.state,
- );
- raf.scheduleFullRedraw();
- }),
- );
+ this.monitor.ifStateChanged(() => {
+ this.reloadData(attrs);
+ });
return m(
DetailsShell,
{
title: this.renderTitle(),
buttons: this.renderFilterPanel(attrs),
+ fillParent: true,
},
- m(
- VirtualScrollContainer,
- {
- onScroll: this.onScroll.bind(this),
+ m(VirtualTable, {
+ className: 'pf-ftrace-explorer',
+ columns: [
+ {header: 'ID', width: '5em'},
+ {header: 'Timestamp', width: '13em'},
+ {header: 'Name', width: '24em'},
+ {header: 'CPU', width: '3em'},
+ {header: 'Process', width: '24em'},
+ {header: 'Args', width: '200em'},
+ ],
+ firstRowOffset: this.data?.offset ?? 0,
+ numRows: this.data?.numEvents ?? 0,
+ rowHeight: ROW_H,
+ rows: this.renderData(),
+ onReload: (offset, count) => {
+ this.pagination = {offset, count};
+ this.reloadData(attrs);
},
- m('.ftrace-panel', this.renderRows()),
- ),
+ onRowHover: this.onRowOver.bind(this),
+ onRowOut: this.onRowOut.bind(this),
+ }),
);
}
- onScroll(scrollContainer: HTMLElement) {
- const paginationState = this.paginationStore.state;
- const prevPage = paginationState.page;
- const prevPageCount = paginationState.pageCount;
-
- const visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
- const visibleRowCount = Math.ceil(scrollContainer.clientHeight / ROW_H);
-
- // Work out which "page" we're on
- const page = Math.max(0, Math.floor(visibleRowOffset / PAGE_SIZE) - 1);
- const pageCount = Math.ceil(visibleRowCount / PAGE_SIZE) + 2;
-
- if (page !== prevPage || pageCount !== prevPageCount) {
- this.paginationStore.edit((draft) => {
- draft.page = page;
- draft.pageCount = pageCount;
- });
+ private reloadData(attrs: FtraceExplorerAttrs): void {
+ this.queryLimiter.schedule(async () => {
+ this.data = await lookupFtraceEvents(
+ attrs.engine,
+ this.pagination.offset,
+ this.pagination.count,
+ attrs.filterStore.state,
+ );
raf.scheduleFullRedraw();
+ });
+ }
+
+ private renderData(): VirtualTableRow[] {
+ if (!this.data) {
+ return [];
+ }
+
+ return this.data.events.map((event) => {
+ const {ts, name, cpu, process, args, id} = event;
+ const timestamp = m(Timestamp, {ts});
+ const color = colorForFtrace(name).base.cssString;
+
+ return {
+ id,
+ cells: [
+ id,
+ timestamp,
+ m('', m('span.colour', {style: {background: color}}), name),
+ cpu,
+ process,
+ args,
+ ],
+ };
+ });
+ }
+
+ private onRowOver(id: number) {
+ const event = this.data?.events.find((event) => event.id === id);
+ if (event) {
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts: event.ts}));
}
}
- onRowOver(ts: time) {
- globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
- }
-
- onRowOut() {
+ private onRowOut() {
globals.dispatch(Actions.setHoverCursorTimestamp({ts: Time.INVALID}));
}
@@ -242,55 +257,6 @@
},
});
}
-
- // Render all the rows including the first title row
- private renderRows() {
- const data = this.data;
- const rows: m.Children = [];
-
- rows.push(
- m(
- `.row`,
- m('.cell.row-header', 'Timestamp'),
- m('.cell.row-header', 'Name'),
- m('.cell.row-header', 'CPU'),
- m('.cell.row-header', 'Process'),
- m('.cell.row-header', 'Args'),
- ),
- );
-
- if (data) {
- const {events, offset, numEvents} = data;
- for (let i = 0; i < events.length; i++) {
- const {ts, name, cpu, process, args} = events[i];
-
- const timestamp = m(Timestamp, {ts});
-
- const rank = i + offset;
-
- const color = colorForFtrace(name).base.cssString;
-
- rows.push(
- m(
- `.row`,
- {
- style: {top: `${(rank + 1.0) * ROW_H}px`},
- onmouseover: this.onRowOver.bind(this, ts),
- onmouseout: this.onRowOut.bind(this),
- },
- m('.cell', timestamp),
- m('.cell', m('span.colour', {style: {background: color}}), name),
- m('.cell', cpu),
- m('.cell', process),
- m('.cell', args),
- ),
- );
- }
- return m('.rows', {style: {height: `${numEvents * ROW_H}px`}}, rows);
- } else {
- return m('.rows', rows);
- }
- }
}
async function lookupFtraceEvents(
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)
diff --git a/ui/src/widgets/virtual_scroll_container.ts b/ui/src/widgets/virtual_scroll_container.ts
deleted file mode 100644
index f6bd052..0000000
--- a/ui/src/widgets/virtual_scroll_container.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {findRef, toHTMLElement} from '../base/dom_utils';
-
-interface VirtualScrollContainerAttrs {
- // Called when the scrolling element is created, updates, or scrolls.
- onScroll?: (dom: HTMLElement) => void;
-}
-
-export class VirtualScrollContainer
- implements m.ClassComponent<VirtualScrollContainerAttrs>
-{
- private readonly REF = 'virtual-scroll-container';
- view({attrs, children}: m.Vnode<VirtualScrollContainerAttrs>) {
- const {onScroll = () => {}} = attrs;
-
- return m(
- '.pf-virtual-scroll-container',
- {
- ref: this.REF,
- onscroll: (e: Event) => onScroll(e.target as HTMLElement),
- },
- children,
- );
- }
-
- oncreate({dom, attrs}: m.VnodeDOM<VirtualScrollContainerAttrs, this>) {
- const {onScroll = () => {}} = attrs;
-
- const element = findRef(dom, this.REF);
- if (element) {
- onScroll(toHTMLElement(element));
- }
- }
-}
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
new file mode 100644
index 0000000..4fbe5c1
--- /dev/null
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -0,0 +1,150 @@
+// 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 {Trash} from '../base/disposable';
+import * as Geometry from '../base/geom';
+
+export interface VirtualScrollHelperOpts {
+ overdrawPx: number;
+
+ // How close we can get to undrawn regions before updating
+ tolerancePx: number;
+
+ callback: (r: Geometry.Rect) => void;
+}
+
+export interface Data {
+ opts: VirtualScrollHelperOpts;
+ rect?: Geometry.Rect;
+}
+
+export class VirtualScrollHelper {
+ private readonly _trash = new Trash();
+ private readonly _data: Data[] = [];
+
+ constructor(
+ sliderElement: HTMLElement,
+ containerElement: Element,
+ opts: VirtualScrollHelperOpts[] = [],
+ ) {
+ this._data = opts.map((opts) => {
+ return {opts};
+ });
+
+ const recalculateRects = () => {
+ this._data.forEach((data) =>
+ recalculatePuckRect(sliderElement, containerElement, data),
+ );
+ };
+
+ containerElement.addEventListener('scroll', recalculateRects, {
+ passive: true,
+ });
+ this._trash.addCallback(() =>
+ containerElement.removeEventListener('scroll', recalculateRects),
+ );
+
+ // Resize observer callbacks are called once immediately
+ const resizeObserver = new ResizeObserver(() => {
+ recalculateRects();
+ });
+
+ resizeObserver.observe(containerElement);
+ resizeObserver.observe(sliderElement);
+ this._trash.addCallback(() => {
+ resizeObserver.disconnect();
+ });
+ }
+
+ dispose() {
+ this._trash.dispose();
+ }
+}
+
+function recalculatePuckRect(
+ sliderElement: HTMLElement,
+ containerElement: Element,
+ data: Data,
+): void {
+ const {tolerancePx, overdrawPx, callback} = data.opts;
+ if (!data.rect) {
+ const targetPuckRect = getTargetPuckRect(
+ sliderElement,
+ containerElement,
+ overdrawPx,
+ );
+ callback(targetPuckRect);
+ data.rect = targetPuckRect;
+ } else {
+ const viewportRect = containerElement.getBoundingClientRect();
+
+ // Expand the viewportRect by the tolerance
+ const viewportExpandedRect = Geometry.expandRect(viewportRect, tolerancePx);
+
+ const sliderClientRect = sliderElement.getBoundingClientRect();
+ const viewportClamped = Geometry.intersectRects(
+ viewportExpandedRect,
+ sliderClientRect,
+ );
+
+ // Translate the puck rect into client space (currently in slider space)
+ const puckClientRect = Geometry.translateRect(data.rect, {
+ x: sliderClientRect.x,
+ y: sliderClientRect.y,
+ });
+
+ // Check if the tolerance rect entirely contains the expanded viewport rect
+ // If not, request an update
+ if (!Geometry.containsRect(puckClientRect, viewportClamped)) {
+ const targetPuckRect = getTargetPuckRect(
+ sliderElement,
+ containerElement,
+ overdrawPx,
+ );
+ callback(targetPuckRect);
+ data.rect = targetPuckRect;
+ }
+ }
+}
+
+// Returns what the puck rect should look like
+function getTargetPuckRect(
+ sliderElement: HTMLElement,
+ containerElement: Element,
+ overdrawPx: number,
+) {
+ const sliderElementRect = sliderElement.getBoundingClientRect();
+ const containerRect = containerElement.getBoundingClientRect();
+
+ // Calculate the intersection of the container's viewport and the target
+ const intersection = Geometry.intersectRects(
+ containerRect,
+ sliderElementRect,
+ );
+
+ // Pad the intersection by the overdraw amount
+ const intersectionExpanded = Geometry.expandRect(intersection, overdrawPx);
+
+ // Intersect with the original target rect unless we want to avoid resizes
+ const targetRect = Geometry.intersectRects(
+ intersectionExpanded,
+ sliderElementRect,
+ );
+
+ return Geometry.rebaseRect(
+ targetRect,
+ sliderElementRect.x,
+ sliderElementRect.y,
+ );
+}
diff --git a/ui/src/widgets/virtual_table.ts b/ui/src/widgets/virtual_table.ts
new file mode 100644
index 0000000..0b97b96
--- /dev/null
+++ b/ui/src/widgets/virtual_table.ts
@@ -0,0 +1,262 @@
+// 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 m from 'mithril';
+import {Trash} from '../base/disposable';
+import {findRef, toHTMLElement} from '../base/dom_utils';
+import {Rect} from '../base/geom';
+import {assertExists} from '../base/logging';
+import {Style} from './common';
+import {scheduleFullRedraw} from './raf';
+import {VirtualScrollHelper} from './virtual_scroll_helper';
+
+/**
+ * The |VirtualTable| widget can be useful when attempting to render a large
+ * amount of tabular data - i.e. dumping the entire contents of a database
+ * table.
+ *
+ * A naive approach would be to load the entire dataset from the table and
+ * render it into the DOM. However, this has a number of disadvantages:
+ * - The query could potentially be very slow on large enough datasets.
+ * - The amount of data pulled could be larger than the available memory.
+ * - Rendering thousands of DOM elements using Mithril can get be slow.
+ * - Asking the browser to create and update thousands of elements on the DOM
+ * can also be slow.
+ *
+ * This implementation takes advantage of the fact that computer monitors are
+ * only so tall, so most will only be able to display a small subset of rows at
+ * a given time, and the user will have to scroll to reveal more data.
+ *
+ * Thus, this widgets operates in such a way as to only render the DOM elements
+ * that are visible within the given scrolling container's viewport. To avoid
+ * spamming render updates, we render a few more rows above and below the
+ * current viewport, and only trigger an update once the user scrolls too close
+ * to the edge of the rendered data. These margins and tolerances are
+ * configurable with the |renderOverdrawPx| and |renderTolerancePx| attributes.
+ *
+ * When it comes to loading data, it's often more performant to run fewer large
+ * queries compared to more frequent smaller queries. Running a new query every
+ * time we want to update the DOM is usually too frequent, and results in
+ * flickering as the data is usually not loaded at the time the relevant row
+ * scrolls into view.
+ *
+ * Thus, this implementation employs two sets of limits, one to refresh the DOM
+ * and one larger one to re-query the data. The latter may be configured using
+ * the |queryOverdrawPx| and |queryTolerancePx| attributes.
+ *
+ * The smaller DOM refreshes and handled internally, but the user must be called
+ * to invoke a new query update. When new data is required, the |onReload|
+ * callback is called with the row offset and count.
+ *
+ * The data must be passed in the |data| attribute which contains the offset of
+ * the currently loaded data and a number of rows.
+ *
+ * Row and column content is flexible as m.Children are accepted and passed
+ * straight to mithril.
+ *
+ * The widget is quite opinionated in terms of its styling, but the entire
+ * widget and each row may be tweaked using |className| and |style| attributes
+ * which behave in the same way as they do on other Mithril components.
+ */
+
+export interface VirtualTableAttrs {
+ // A list of columns containing the header row content and column widths
+ columns: VirtualTableColumn[];
+
+ // Row height in px (each row must have the same height)
+ rowHeight: number;
+
+ // Offset of the first row
+ firstRowOffset: number;
+
+ // Total number of rows
+ numRows: number;
+
+ // The row data to render
+ rows: VirtualTableRow[];
+
+ // Optional: Called when we need to reload data
+ onReload?: (rowOffset: number, rowCount: number) => void;
+
+ // Additional class name applied to the table container element
+ className?: string;
+
+ // Additional styles applied to the table container element
+ style?: Style;
+
+ // Optional: Called when a row is hovered, passing the hovered row's id
+ onRowHover?: (id: number) => void;
+
+ // Optional: Called when a row is un-hovered, passing the un-hovered row's id
+ onRowOut?: (id: number) => void;
+
+ // Optional: Number of pixels equivalent of rows to overdraw above and below
+ // the viewport
+ // Defaults to a sensible value
+ renderOverdrawPx?: number;
+
+ // Optional: How close we can get to the edge before triggering a DOM redraw
+ // Defaults to a sensible value
+ renderTolerancePx?: number;
+
+ // Optional: Number of pixels equivalent of rows to query above and below the
+ // viewport
+ // Defaults to a sensible value
+ queryOverdrawPx?: number;
+
+ // Optional: How close we can get to the edge if the loaded data before we
+ // trigger another query
+ // Defaults to a sensible value
+ queryTolerancePx?: number;
+}
+
+export interface VirtualTableColumn {
+ // Content to render in the header row
+ header: m.Children;
+
+ // CSS width e.g. 12px, 4em, etc...
+ width: string;
+}
+
+export interface VirtualTableRow {
+ // Id for this row (must be unique within this dataset)
+ // Used for callbacks and as a Mithril key.
+ id: number;
+
+ // Data for each column in this row - must match number of elements in columns
+ cells: m.Children[];
+
+ // Optional: Additional class name applied to the row element
+ className?: string;
+}
+
+export class VirtualTable implements m.ClassComponent<VirtualTableAttrs> {
+ private readonly CONTAINER_REF = 'CONTAINER';
+ private readonly SLIDER_REF = 'SLIDER';
+ private readonly trash = new Trash();
+ private renderBounds = {rowStart: 0, rowEnd: 0};
+
+ view({attrs}: m.Vnode<VirtualTableAttrs>): m.Children {
+ const {columns, className, numRows, rowHeight, style} = attrs;
+ return m(
+ '.pf-vtable',
+ {className, style, ref: this.CONTAINER_REF},
+ m(
+ '.pf-vtable-content',
+ m(
+ '.pf-vtable-header',
+ columns.map((col) =>
+ m('.pf-vtable-data', {style: {width: col.width}}, col.header),
+ ),
+ ),
+ m(
+ '.pf-vtable-slider',
+ {ref: this.SLIDER_REF, style: {height: `${rowHeight * numRows}px`}},
+ m(
+ '.pf-vtable-puck',
+ {
+ style: {
+ transform: `translateY(${
+ this.renderBounds.rowStart * rowHeight
+ }px)`,
+ },
+ },
+ this.renderContent(attrs),
+ ),
+ ),
+ ),
+ );
+ }
+
+ private renderContent(attrs: VirtualTableAttrs): m.Children {
+ const rows: m.ChildArray = [];
+ for (
+ let i = this.renderBounds.rowStart;
+ i < this.renderBounds.rowEnd;
+ ++i
+ ) {
+ rows.push(this.renderRow(attrs, i));
+ }
+ return rows;
+ }
+
+ private renderRow(attrs: VirtualTableAttrs, i: number): m.Children {
+ const {rows, firstRowOffset, rowHeight, columns, onRowHover, onRowOut} =
+ attrs;
+ if (i >= firstRowOffset && i < firstRowOffset + rows.length) {
+ // Render the row...
+ const index = i - firstRowOffset;
+ const rowData = rows[index];
+ return m(
+ '.pf-vtable-row',
+ {
+ className: rowData.className,
+ style: {height: `${rowHeight}px`},
+ onmouseover: () => {
+ onRowHover?.(rowData.id);
+ },
+ onmouseout: () => {
+ onRowOut?.(rowData.id);
+ },
+ },
+ rowData.cells.map((data, colIndex) =>
+ m('.pf-vtable-data', {style: {width: columns[colIndex].width}}, data),
+ ),
+ );
+ } else {
+ // Render a placeholder div with the same height as a row but a
+ // transparent background
+ return m('', {style: {height: `${rowHeight}px`}});
+ }
+ }
+
+ oncreate({dom, attrs}: m.VnodeDOM<VirtualTableAttrs>) {
+ const {
+ renderOverdrawPx = 200,
+ renderTolerancePx = 100,
+ queryOverdrawPx = 10_000,
+ queryTolerancePx = 5_000,
+ } = attrs;
+
+ const sliderEl = toHTMLElement(assertExists(findRef(dom, this.SLIDER_REF)));
+ const containerEl = assertExists(findRef(dom, this.CONTAINER_REF));
+ const virtualScrollHelper = new VirtualScrollHelper(sliderEl, containerEl, [
+ {
+ overdrawPx: renderOverdrawPx,
+ tolerancePx: renderTolerancePx,
+ callback: ({top, bottom}: Rect) => {
+ const height = bottom - top;
+ const rowStart = Math.floor(top / attrs.rowHeight / 2) * 2;
+ const rowCount = Math.ceil(height / attrs.rowHeight / 2) * 2;
+ this.renderBounds = {rowStart, rowEnd: rowStart + rowCount};
+ scheduleFullRedraw();
+ },
+ },
+ {
+ overdrawPx: queryOverdrawPx,
+ tolerancePx: queryTolerancePx,
+ callback: ({top, bottom}: Rect) => {
+ const rowStart = Math.floor(top / attrs.rowHeight / 2) * 2;
+ const rowEnd = Math.ceil(bottom / attrs.rowHeight);
+ attrs.onReload?.(rowStart, rowEnd - rowStart);
+ },
+ },
+ ]);
+ this.trash.add(virtualScrollHelper);
+ }
+
+ onremove(_: m.VnodeDOM<VirtualTableAttrs>) {
+ this.trash.dispose();
+ }
+}