Merge "[stdlib] Improvements to experimental_slice_flattened"
diff --git a/Android.bp b/Android.bp
index b78f538..4f29bca 100644
--- a/Android.bp
+++ b/Android.bp
@@ -4364,6 +4364,7 @@
srcs: [
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
"protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+ "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
"protos/perfetto/metrics/android/android_trusty_workqueues.proto",
"protos/perfetto/metrics/android/batt_metric.proto",
"protos/perfetto/metrics/android/binder_metric.proto",
@@ -4437,6 +4438,7 @@
srcs: [
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
"protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+ "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
"protos/perfetto/metrics/android/android_trusty_workqueues.proto",
"protos/perfetto/metrics/android/batt_metric.proto",
"protos/perfetto/metrics/android/binder_metric.proto",
@@ -4493,6 +4495,7 @@
srcs: [
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
"protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+ "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
"protos/perfetto/metrics/android/android_trusty_workqueues.proto",
"protos/perfetto/metrics/android/batt_metric.proto",
"protos/perfetto/metrics/android/binder_metric.proto",
@@ -10055,6 +10058,7 @@
"src/trace_processor/metrics/sql/android/android_simpleperf.sql",
"src/trace_processor/metrics/sql/android/android_startup.sql",
"src/trace_processor/metrics/sql/android/android_surfaceflinger.sql",
+ "src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql",
"src/trace_processor/metrics/sql/android/android_task_names.sql",
"src/trace_processor/metrics/sql/android/android_trace_quality.sql",
"src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql",
diff --git a/BUILD b/BUILD
index 36b9664..886e9a5 100644
--- a/BUILD
+++ b/BUILD
@@ -1790,6 +1790,7 @@
"src/trace_processor/metrics/sql/android/android_simpleperf.sql",
"src/trace_processor/metrics/sql/android/android_startup.sql",
"src/trace_processor/metrics/sql/android/android_surfaceflinger.sql",
+ "src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql",
"src/trace_processor/metrics/sql/android/android_task_names.sql",
"src/trace_processor/metrics/sql/android/android_trace_quality.sql",
"src/trace_processor/metrics/sql/android/android_trusty_workqueues.sql",
@@ -3934,6 +3935,7 @@
srcs = [
"protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
"protos/perfetto/metrics/android/android_frame_timeline_metric.proto",
+ "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto",
"protos/perfetto/metrics/android/android_trusty_workqueues.proto",
"protos/perfetto/metrics/android/batt_metric.proto",
"protos/perfetto/metrics/android/binder_metric.proto",
diff --git a/OWNERS b/OWNERS
index 5c57ecb..3c29a20 100644
--- a/OWNERS
+++ b/OWNERS
@@ -25,6 +25,9 @@
# UI, Chromium-related metrics and simpler trace processor changes.
altimin@google.com
+# UI
+stevegolton@google.com
+
# Most Android-related metrics.
ilkos@google.com
diff --git a/include/perfetto/tracing/internal/track_event_data_source.h b/include/perfetto/tracing/internal/track_event_data_source.h
index 32993ac..784cf84 100644
--- a/include/perfetto/tracing/internal/track_event_data_source.h
+++ b/include/perfetto/tracing/internal/track_event_data_source.h
@@ -557,9 +557,8 @@
// The DecayArgType method is used to avoid unnecessary instantiations of
// templates on:
// * string constants of different sizes.
- // * integers of different sizes or constness.
- // * floats of different sizes.
- // This allows to avoid extra instantiations of TraceForCategory templates.
+ // * primitive of different constness (or references).
+ // This avoids extra instantiations of TraceForCategory templates.
template <typename T>
static T&& DecayArgType(T&& t) {
return std::forward<T>(t);
@@ -567,15 +566,15 @@
static const char* DecayArgType(const char* s) { return s; }
static uint64_t DecayArgType(uint64_t u) { return u; }
- static uint64_t DecayArgType(uint32_t u) { return u; }
- static uint64_t DecayArgType(uint16_t u) { return u; }
- static uint64_t DecayArgType(uint8_t u) { return u; }
+ static uint32_t DecayArgType(uint32_t u) { return u; }
+ static uint16_t DecayArgType(uint16_t u) { return u; }
+ static uint8_t DecayArgType(uint8_t u) { return u; }
static int64_t DecayArgType(int64_t i) { return i; }
- static int64_t DecayArgType(int32_t i) { return i; }
- static int64_t DecayArgType(int16_t i) { return i; }
- static int64_t DecayArgType(int8_t i) { return i; }
+ static int32_t DecayArgType(int32_t i) { return i; }
+ static int16_t DecayArgType(int16_t i) { return i; }
+ static int8_t DecayArgType(int8_t i) { return i; }
static bool DecayArgType(bool b) { return b; }
- static double DecayArgType(float f) { return static_cast<double>(f); }
+ static float DecayArgType(float f) { return f; }
static double DecayArgType(double f) { return f; }
// Once we've determined tracing to be enabled for this category, actually
@@ -699,7 +698,7 @@
perfetto::protos::pbzero::TrackEvent::Type type,
CounterTrack track,
TimestampType timestamp,
- ValueType value) PERFETTO_ALWAYS_INLINE {
+ ValueType value) PERFETTO_NO_INLINE {
PERFETTO_DCHECK(type == perfetto::protos::pbzero::TrackEvent::TYPE_COUNTER);
TraceForCategoryImpl(
instances, category, /*name=*/nullptr, type, track, timestamp,
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index 2fed17a..5a48545 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -20,6 +20,7 @@
"source_set",
]
sources = [
+ "android_sysui_notifications_blocking_calls_metric.proto",
"android_blocking_calls_cuj_metric.proto",
"android_frame_timeline_metric.proto",
"android_trusty_workqueues.proto",
diff --git a/protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto b/protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
new file mode 100644
index 0000000..909c48c
--- /dev/null
+++ b/protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+import "protos/perfetto/metrics/android/process_metadata.proto";
+
+// Blocking calls inside System UI Notifications. Shows count and total duration for each.
+message AndroidSysUINotificationsBlockingCallsMetric {
+ repeated BlockingCall blocking_calls = 1;
+
+ // Blocking call on the main thread.
+ message BlockingCall {
+ // Name of the blocking call
+ optional string name = 1;
+ // Number of times it happened within the CUJ
+ optional int64 cnt = 2;
+ // Total duration within the CUJ
+ optional int64 total_dur_ms = 3;
+ // Maximal duration within the CUJ
+ optional int64 max_dur_ms = 4;
+ // Minimal duration within the CUJ
+ optional int64 min_dur_ms = 5;
+ // Total duration within the CUJ in nanoseconds
+ optional int64 total_dur_ns = 6;
+ // Maximal duration within the CUJ in nanoseconds
+ optional int64 max_dur_ns = 7;
+ // Minimal duration within the CUJ in nanoseconds
+ optional int64 min_dur_ns = 8;
+ }
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 8b5e5e4..1beec18 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -20,6 +20,7 @@
import "protos/perfetto/metrics/android/android_frame_timeline_metric.proto";
import "protos/perfetto/metrics/android/batt_metric.proto";
+import "protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto";
import "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto";
import "protos/perfetto/metrics/android/cpu_metric.proto";
import "protos/perfetto/metrics/android/camera_metric.proto";
@@ -102,7 +103,7 @@
// Root message for all Perfetto-based metrics.
//
-// Next id: 50
+// Next id: 52
message TraceMetrics {
reserved 4, 10, 13, 14, 16, 19;
@@ -239,6 +240,8 @@
optional AndroidMonitorContentionMetric android_monitor_contention = 50;
+ optional AndroidSysUINotificationsBlockingCallsMetric android_sysui_notifications_blocking_calls_metric = 51;
+
// Demo extensions.
extensions 450 to 499;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 393095c..0758d23 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -133,6 +133,35 @@
// End of protos/perfetto/metrics/android/android_frame_timeline_metric.proto
+// Begin of protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
+
+// Blocking calls inside System UI Notifications. Shows count and total duration for each.
+message AndroidSysUINotificationsBlockingCallsMetric {
+ repeated BlockingCall blocking_calls = 1;
+
+ // Blocking call on the main thread.
+ message BlockingCall {
+ // Name of the blocking call
+ optional string name = 1;
+ // Number of times it happened within the CUJ
+ optional int64 cnt = 2;
+ // Total duration within the CUJ
+ optional int64 total_dur_ms = 3;
+ // Maximal duration within the CUJ
+ optional int64 max_dur_ms = 4;
+ // Minimal duration within the CUJ
+ optional int64 min_dur_ms = 5;
+ // Total duration within the CUJ in nanoseconds
+ optional int64 total_dur_ns = 6;
+ // Maximal duration within the CUJ in nanoseconds
+ optional int64 max_dur_ns = 7;
+ // Minimal duration within the CUJ in nanoseconds
+ optional int64 min_dur_ns = 8;
+ }
+}
+
+// End of protos/perfetto/metrics/android/android_sysui_notifications_blocking_calls_metric.proto
+
// Begin of protos/perfetto/metrics/android/android_trusty_workqueues.proto
// Metric used to generate a simplified view of the Trusty kworker events.
@@ -2042,7 +2071,7 @@
// Root message for all Perfetto-based metrics.
//
-// Next id: 50
+// Next id: 52
message TraceMetrics {
reserved 4, 10, 13, 14, 16, 19;
@@ -2179,6 +2208,8 @@
optional AndroidMonitorContentionMetric android_monitor_contention = 50;
+ optional AndroidSysUINotificationsBlockingCallsMetric android_sysui_notifications_blocking_calls_metric = 51;
+
// Demo extensions.
extensions 450 to 499;
diff --git a/protos/perfetto/trace/test_extensions.proto b/protos/perfetto/trace/test_extensions.proto
index dca2db6..0d33c09 100644
--- a/protos/perfetto/trace/test_extensions.proto
+++ b/protos/perfetto/trace/test_extensions.proto
@@ -31,6 +31,7 @@
repeated int32 int_extension_for_testing = 9901;
optional string omitted_extension_for_testing = 9902;
optional TestExtensionChild nested_message_extension_for_testing = 9903;
+ optional uint32 uint_extension_for_testing = 9904;
}
}
diff --git a/python/BUILD.gn b/python/BUILD.gn
index 17ad6f3..dbd1e9d 100644
--- a/python/BUILD.gn
+++ b/python/BUILD.gn
@@ -26,7 +26,6 @@
sources = [
"generators/stdlib_docs/extractor.py",
"generators/stdlib_docs/parse.py",
- "generators/stdlib_docs/types.py",
"generators/stdlib_docs/utils.py",
]
}
diff --git a/python/generators/stdlib_docs/extractor.py b/python/generators/stdlib_docs/extractor.py
index 94db4d3..fce88f2 100644
--- a/python/generators/stdlib_docs/extractor.py
+++ b/python/generators/stdlib_docs/extractor.py
@@ -17,7 +17,7 @@
from re import Match
from typing import List, Optional, Tuple
-from python.generators.stdlib_docs.types import ObjKind
+from python.generators.stdlib_docs.utils import ObjKind
from python.generators.stdlib_docs.utils import extract_comment
from python.generators.stdlib_docs.utils import match_pattern
from python.generators.stdlib_docs.utils import PATTERN_BY_KIND
diff --git a/python/generators/stdlib_docs/parse.py b/python/generators/stdlib_docs/parse.py
index a78bcca..0bb4c28 100644
--- a/python/generators/stdlib_docs/parse.py
+++ b/python/generators/stdlib_docs/parse.py
@@ -20,7 +20,7 @@
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from python.generators.stdlib_docs.extractor import DocsExtractor
-from python.generators.stdlib_docs.types import ObjKind
+from python.generators.stdlib_docs.utils import ObjKind
from python.generators.stdlib_docs.utils import ARG_ANNOTATION_PATTERN
from python.generators.stdlib_docs.utils import NAME_AND_TYPE_PATTERN
from python.generators.stdlib_docs.utils import FUNCTION_RETURN_PATTERN
diff --git a/python/generators/stdlib_docs/types.py b/python/generators/stdlib_docs/types.py
deleted file mode 100644
index a9baad3..0000000
--- a/python/generators/stdlib_docs/types.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the 'License');
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an 'AS IS' BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from enum import Enum
-
-
-class ObjKind(str, Enum):
- table_view = 'table_view'
- function = 'function'
- view_function = 'view_function'
diff --git a/python/generators/stdlib_docs/utils.py b/python/generators/stdlib_docs/utils.py
index eeccc01..d635b08 100644
--- a/python/generators/stdlib_docs/utils.py
+++ b/python/generators/stdlib_docs/utils.py
@@ -12,14 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from enum import Enum
import re
from typing import Dict, List
-from python.generators.stdlib_docs.types import ObjKind
-
LOWER_NAME = r'[a-z_\d]+'
UPPER_NAME = r'[A-Z_\d]+'
ANY_WORDS = r'[^\s].*'
+ANY_NON_QUOTE = r'[^\']*.*'
TYPE = r'[A-Z]+'
SQL = r'[\s\S]*?'
@@ -47,10 +47,17 @@
# Args: anything before closing bracket with '.
fr"({ANY_WORDS})\)',\s*"
# Return columns: anything between two '.
- fr"'\s*({ANY_WORDS})',\s*"
+ fr"'\s*({ANY_NON_QUOTE})\s*',\s*"
# Sql: Anything between ' and ');. We are catching \'.
fr"'({SQL})'\s*\);")
+
+class ObjKind(str, Enum):
+ table_view = 'table_view'
+ function = 'function'
+ view_function = 'view_function'
+
+
PATTERN_BY_KIND = {
ObjKind.table_view: CREATE_TABLE_VIEW_PATTERN,
ObjKind.function: CREATE_FUNCTION_PATTERN,
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 536dff9..fcdc866 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/perfetto/trace_processor/metrics.descriptor.sha1 b/python/perfetto/trace_processor/metrics.descriptor.sha1
new file mode 100644
index 0000000..d0befe1
--- /dev/null
+++ b/python/perfetto/trace_processor/metrics.descriptor.sha1
@@ -0,0 +1,6 @@
+
+// SHA1(tools/gen_binary_descriptors)
+// 6886b319e65925c037179e71a803b8473d06dc7d
+// SHA1(protos/perfetto/metrics/metrics.proto)
+// fe539c6187d701d117ab2f757e1a6b20a035af9f
+
\ No newline at end of file
diff --git a/src/trace_processor/containers/row_map.h b/src/trace_processor/containers/row_map.h
index 45eedf5..a2a6a41 100644
--- a/src/trace_processor/containers/row_map.h
+++ b/src/trace_processor/containers/row_map.h
@@ -451,6 +451,12 @@
NoVariantMatched();
}
+ // Returns the data in RowMap BitVector, nullptr if RowMap is in a different
+ // mode.
+ const BitVector* GetIfBitVector() const {
+ return std::get_if<BitVector>(&data_);
+ }
+
// Returns the iterator over the rows in this RowMap.
Iterator IterateRows() const { return Iterator(this); }
diff --git a/src/trace_processor/db/overlays/null_overlay.cc b/src/trace_processor/db/overlays/null_overlay.cc
index ee27295..7bc1b43 100644
--- a/src/trace_processor/db/overlays/null_overlay.cc
+++ b/src/trace_processor/db/overlays/null_overlay.cc
@@ -16,6 +16,7 @@
#include "src/trace_processor/db/overlays/null_overlay.h"
#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/containers/bit_vector.h"
#include "src/trace_processor/db/overlays/types.h"
namespace perfetto {
@@ -28,7 +29,19 @@
uint32_t start = non_null_->CountSetBits(t_range.range.start);
uint32_t end = non_null_->CountSetBits(t_range.range.end);
- return StorageRange({Range(start, end)});
+ return StorageRange(start, end);
+}
+
+TableRangeOrBitVector NullOverlay::MapToTableRangeOrBitVector(
+ StorageRange s_range,
+ OverlayOp op) const {
+ PERFETTO_DCHECK(s_range.range.end <= non_null_->CountSetBits());
+
+ BitVector range_to_bv(s_range.range.start, false);
+ range_to_bv.Resize(s_range.range.end, true);
+
+ return TableRangeOrBitVector(
+ MapToTableBitVector(StorageBitVector{std::move(range_to_bv)}, op).bv);
}
TableBitVector NullOverlay::MapToTableBitVector(StorageBitVector s_bv,
diff --git a/src/trace_processor/db/overlays/null_overlay.h b/src/trace_processor/db/overlays/null_overlay.h
index 328dc9b..5753fa3 100644
--- a/src/trace_processor/db/overlays/null_overlay.h
+++ b/src/trace_processor/db/overlays/null_overlay.h
@@ -18,6 +18,7 @@
#define SRC_TRACE_PROCESSOR_DB_OVERLAYS_NULL_OVERLAY_H_
#include "src/trace_processor/db/overlays/storage_overlay.h"
+#include "src/trace_processor/db/overlays/types.h"
namespace perfetto {
namespace trace_processor {
@@ -31,6 +32,9 @@
StorageRange MapToStorageRange(TableRange) const override;
+ TableRangeOrBitVector MapToTableRangeOrBitVector(StorageRange,
+ OverlayOp) const override;
+
TableBitVector MapToTableBitVector(StorageBitVector,
OverlayOp) const override;
diff --git a/src/trace_processor/db/overlays/null_overlay_unittest.cc b/src/trace_processor/db/overlays/null_overlay_unittest.cc
index 0c02ad7..d69780c 100644
--- a/src/trace_processor/db/overlays/null_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/null_overlay_unittest.cc
@@ -25,7 +25,7 @@
TEST(NullOverlay, MapToStorageRangeOutsideBoundary) {
BitVector bv{0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0};
NullOverlay overlay(&bv);
- StorageRange r = overlay.MapToStorageRange({RowMap::Range(1, 6)});
+ StorageRange r = overlay.MapToStorageRange(TableRange(1, 6));
ASSERT_EQ(r.range.start, 0u);
ASSERT_EQ(r.range.end, 2u);
@@ -34,12 +34,31 @@
TEST(NullOverlay, MapToStorageRangeOnBoundary) {
BitVector bv{0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0};
NullOverlay overlay(&bv);
- StorageRange r = overlay.MapToStorageRange({RowMap::Range(3, 8)});
+ StorageRange r = overlay.MapToStorageRange(TableRange(3, 8));
ASSERT_EQ(r.range.start, 1u);
ASSERT_EQ(r.range.end, 4u);
}
+TEST(NullOverlay, MapToTableRangeOutsideBoundary) {
+ BitVector bv{0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0};
+ NullOverlay overlay(&bv);
+ auto r =
+ overlay.MapToTableRangeOrBitVector(StorageRange(1, 3), OverlayOp::kOther);
+
+ // All set bits between |bv| index 3 and 6.
+ ASSERT_EQ(r.TakeIfBitVector().CountSetBits(), 2u);
+}
+
+TEST(NullOverlay, MapToTableRangeOnBoundary) {
+ BitVector bv{0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0};
+ NullOverlay overlay(&bv);
+ auto r =
+ overlay.MapToTableRangeOrBitVector(StorageRange(0, 5), OverlayOp::kOther);
+
+ ASSERT_EQ(r.TakeIfBitVector().CountSetBits(), 5u);
+}
+
TEST(NullOverlay, MapToTableBitVector) {
BitVector bv{0, 1, 1, 0, 0, 1, 1, 0};
NullOverlay overlay(&bv);
diff --git a/src/trace_processor/db/overlays/selector_overlay.cc b/src/trace_processor/db/overlays/selector_overlay.cc
index bc6fcfb..fc40bc6 100644
--- a/src/trace_processor/db/overlays/selector_overlay.cc
+++ b/src/trace_processor/db/overlays/selector_overlay.cc
@@ -16,6 +16,7 @@
#include "src/trace_processor/db/overlays/selector_overlay.h"
#include "src/trace_processor/containers/bit_vector.h"
+#include "src/trace_processor/db/overlays/types.h"
namespace perfetto {
namespace trace_processor {
@@ -30,6 +31,18 @@
selected_->IndexOfNthSet(t_range.range.end - 1) + 1)};
}
+TableRangeOrBitVector SelectorOverlay::MapToTableRangeOrBitVector(
+ StorageRange s_range,
+ OverlayOp) const {
+ if (s_range.range.size() == 0)
+ return TableRangeOrBitVector(Range());
+
+ uint32_t start = selected_->CountSetBits(s_range.range.start);
+ uint32_t end = selected_->CountSetBits(s_range.range.end);
+
+ return TableRangeOrBitVector(Range(start, end));
+}
+
TableBitVector SelectorOverlay::MapToTableBitVector(StorageBitVector s_bv,
OverlayOp) const {
PERFETTO_DCHECK(selected_->size() >= s_bv.bv.size());
diff --git a/src/trace_processor/db/overlays/selector_overlay.h b/src/trace_processor/db/overlays/selector_overlay.h
index 1cad92c..ac591d3 100644
--- a/src/trace_processor/db/overlays/selector_overlay.h
+++ b/src/trace_processor/db/overlays/selector_overlay.h
@@ -27,10 +27,13 @@
// Overlay responsible for selecting specific rows from Storage.
class SelectorOverlay : public StorageOverlay {
public:
- explicit SelectorOverlay(BitVector* selected) : selected_(selected) {}
+ explicit SelectorOverlay(const BitVector* selected) : selected_(selected) {}
StorageRange MapToStorageRange(TableRange) const override;
+ TableRangeOrBitVector MapToTableRangeOrBitVector(StorageRange,
+ OverlayOp) const override;
+
TableBitVector MapToTableBitVector(StorageBitVector,
OverlayOp) const override;
@@ -44,7 +47,7 @@
CostEstimatePerRow EstimateCostPerRow(OverlayOp) const override;
private:
- BitVector* selected_;
+ const BitVector* selected_;
};
} // namespace overlays
diff --git a/src/trace_processor/db/overlays/selector_overlay_unittest.cc b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
index 8c743e8..1e4a130 100644
--- a/src/trace_processor/db/overlays/selector_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
@@ -15,6 +15,7 @@
*/
#include "src/trace_processor/db/overlays/selector_overlay.h"
+#include "src/trace_processor/db/overlays/types.h"
#include "test/gtest_and_gmock.h"
namespace perfetto {
@@ -25,7 +26,7 @@
TEST(SelectorOverlay, MapToStorageRangeFirst) {
BitVector selector{0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1};
SelectorOverlay overlay(&selector);
- StorageRange r = overlay.MapToStorageRange({RowMap::Range(1, 4)});
+ StorageRange r = overlay.MapToStorageRange(TableRange(1, 4));
ASSERT_EQ(r.range.start, 4u);
ASSERT_EQ(r.range.end, 8u);
@@ -34,12 +35,32 @@
TEST(SelectorOverlay, MapToStorageRangeSecond) {
BitVector selector{0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0};
SelectorOverlay overlay(&selector);
- StorageRange r = overlay.MapToStorageRange({RowMap::Range(1, 3)});
+ StorageRange r = overlay.MapToStorageRange(TableRange(1, 3));
ASSERT_EQ(r.range.start, 4u);
ASSERT_EQ(r.range.end, 7u);
}
+TEST(SelectorOverlay, MapToTableRangeFirst) {
+ BitVector selector{0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1};
+ SelectorOverlay overlay(&selector);
+ auto r =
+ overlay.MapToTableRangeOrBitVector(StorageRange(2, 5), OverlayOp::kOther);
+
+ ASSERT_EQ(r.TakeIfRange().start, 1u);
+ ASSERT_EQ(r.TakeIfRange().end, 3u);
+}
+
+TEST(SelectorOverlay, MapToTableRangeSecond) {
+ BitVector selector{0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0};
+ SelectorOverlay overlay(&selector);
+ auto r = overlay.MapToTableRangeOrBitVector(StorageRange(0, 10),
+ OverlayOp::kOther);
+
+ ASSERT_EQ(r.TakeIfRange().start, 0u);
+ ASSERT_EQ(r.TakeIfRange().end, 6u);
+}
+
TEST(SelectorOverlay, MapToTableBitVector) {
BitVector selector{0, 1, 1, 0, 0, 1, 1, 0};
SelectorOverlay overlay(&selector);
diff --git a/src/trace_processor/db/overlays/storage_overlay.h b/src/trace_processor/db/overlays/storage_overlay.h
index c31610b..d6ad8df 100644
--- a/src/trace_processor/db/overlays/storage_overlay.h
+++ b/src/trace_processor/db/overlays/storage_overlay.h
@@ -50,6 +50,11 @@
// indices in the storage space.
virtual StorageRange MapToStorageRange(TableRange) const = 0;
+ // Returns the smallest Range or BitVector containing all of the elements
+ // matching the OverlayOp.
+ virtual TableRangeOrBitVector MapToTableRangeOrBitVector(StorageRange,
+ OverlayOp) const = 0;
+
// Maps a BitVector of indices in storage space to an equivalent range of
// indices in the table space.
virtual TableBitVector MapToTableBitVector(StorageBitVector,
diff --git a/src/trace_processor/db/overlays/types.h b/src/trace_processor/db/overlays/types.h
index 7978ada..83af6bc 100644
--- a/src/trace_processor/db/overlays/types.h
+++ b/src/trace_processor/db/overlays/types.h
@@ -16,6 +16,8 @@
#ifndef SRC_TRACE_PROCESSOR_DB_OVERLAYS_TYPES_H_
#define SRC_TRACE_PROCESSOR_DB_OVERLAYS_TYPES_H_
+#include <variant>
+#include "perfetto/base/logging.h"
#include "src/trace_processor/containers/bit_vector.h"
#include "src/trace_processor/containers/row_map.h"
#include "src/trace_processor/db/storage/types.h"
@@ -24,14 +26,22 @@
namespace trace_processor {
namespace overlays {
+using Range = RowMap::Range;
+
// A range of indices in the table space.
struct TableRange {
- RowMap::Range range;
+ TableRange(uint32_t start, uint32_t end) : range(start, end) {}
+ explicit TableRange(Range r) : range(r) {}
+
+ Range range;
};
// A range of indices in the storage space.
struct StorageRange {
- RowMap::Range range;
+ StorageRange(uint32_t start, uint32_t end) : range(start, end) {}
+ explicit StorageRange(Range r) : range(r) {}
+
+ Range range;
};
// A BitVector with set bits corresponding to indices in the table space.
@@ -44,6 +54,27 @@
BitVector bv;
};
+using RangeOrBitVector = std::variant<Range, BitVector>;
+
+struct TableRangeOrBitVector {
+ explicit TableRangeOrBitVector(Range range) : val(range) {}
+ explicit TableRangeOrBitVector(BitVector bv) : val(std::move(bv)) {}
+
+ bool IsRange() const { return std::holds_alternative<Range>(val); }
+ bool IsBitVector() const { return std::holds_alternative<BitVector>(val); }
+
+ BitVector TakeIfBitVector() {
+ PERFETTO_DCHECK(IsBitVector());
+ return std::move(*std::get_if<BitVector>(&val));
+ }
+ Range TakeIfRange() {
+ PERFETTO_DCHECK(IsRange());
+ return std::move(*std::get_if<Range>(&val));
+ }
+
+ RangeOrBitVector val = Range();
+};
+
// Represents a vector of indices in the table space.
struct TableIndexVector {
std::vector<uint32_t> indices;
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 46cbd29..8fceb62 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -22,10 +22,14 @@
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/containers/row_map.h"
#include "src/trace_processor/db/overlays/null_overlay.h"
+#include "src/trace_processor/db/overlays/selector_overlay.h"
#include "src/trace_processor/db/overlays/storage_overlay.h"
+#include "src/trace_processor/db/overlays/types.h"
#include "src/trace_processor/db/query_executor.h"
#include "src/trace_processor/db/storage/numeric_storage.h"
+#include "src/trace_processor/db/storage/types.h"
#include "src/trace_processor/db/table.h"
namespace perfetto {
@@ -124,10 +128,16 @@
if (rm->empty())
return;
+ if (col.sorted_intrinsically && c.op != FilterOp::kNe) {
+ BinarySearch(c, col, rm);
+ return;
+ }
+
uint32_t rm_size = rm->size();
uint32_t rm_first = rm->Get(0);
uint32_t rm_last = rm->Get(rm_size - 1);
uint32_t range_size = rm_last - rm_first;
+
// If the number of elements in the rowmap is small or the number of elements
// is less than 1/10th of the range, use indexed filtering.
// TODO(b/283763282): use Overlay estimations.
@@ -157,13 +167,13 @@
const SimpleColumn& col,
RowMap* rm) {
// TODO(b/283763282): We should align these to word boundaries.
- TableRange table_range{Range(rm->Get(0), rm->Get(rm->size() - 1) + 1)};
+ TableRange table_range(rm->Get(0), rm->Get(rm->size() - 1) + 1);
base::SmallVector<Range, kMaxOverlayCount> overlay_bounds;
for (const auto& overlay : col.overlays) {
StorageRange storage_range = overlay->MapToStorageRange(table_range);
overlay_bounds.emplace_back(storage_range.range);
- table_range = TableRange({storage_range.range});
+ table_range = TableRange(storage_range.range);
}
// Use linear search algorithm on storage.
@@ -179,6 +189,45 @@
return std::move(filtered_storage.bv);
}
+void QueryExecutor::BinarySearch(const Constraint& c,
+ const SimpleColumn& col,
+ RowMap* rm) {
+ // TODO(b/283763282): We should align these to word boundaries.
+ TableRange table_range{Range(rm->Get(0), rm->Get(rm->size() - 1) + 1)};
+ base::SmallVector<Range, kMaxOverlayCount> overlay_bounds;
+
+ for (const auto& overlay : col.overlays) {
+ StorageRange storage_range = overlay->MapToStorageRange(table_range);
+ overlay_bounds.emplace_back(storage_range.range);
+ table_range = TableRange({storage_range.range});
+ }
+
+ // Use binary search algorithm on storage.
+ overlays::TableRangeOrBitVector res(
+ col.storage->BinarySearchIntrinsic(c.op, c.value, table_range.range));
+
+ OverlayOp op = overlays::FilterOpToOverlayOp(c.op);
+ for (uint32_t i = 0; i < col.overlays.size(); ++i) {
+ uint32_t rev_i = static_cast<uint32_t>(col.overlays.size()) - 1 - i;
+
+ if (res.IsBitVector()) {
+ TableBitVector t_bv = col.overlays[rev_i]->MapToTableBitVector(
+ StorageBitVector{res.TakeIfBitVector()}, op);
+ res.val = std::move(t_bv.bv);
+ } else {
+ res = col.overlays[rev_i]->MapToTableRangeOrBitVector(
+ StorageRange(res.TakeIfRange()), op);
+ }
+ }
+
+ if (res.IsBitVector()) {
+ rm->Intersect(RowMap(res.TakeIfBitVector()));
+ return;
+ }
+
+ rm->Intersect(RowMap(res.TakeIfRange().start, res.TakeIfRange().end));
+}
+
RowMap QueryExecutor::IndexSearch(const Constraint& c,
const SimpleColumn& col,
RowMap* rm) {
@@ -252,10 +301,12 @@
use_legacy = use_legacy || (overlays::FilterOpToOverlayOp(c.op) ==
overlays::OverlayOp::kOther &&
col.type() != c.value.type);
+ use_legacy = use_legacy || col.IsDense() || col.IsSetId();
+ use_legacy =
+ use_legacy || (col.overlay().size() != col.storage_base().size() &&
+ !col.overlay().row_map().IsBitVector());
use_legacy = use_legacy ||
- col.overlay().row_map().size() != col.storage_base().size();
- use_legacy = use_legacy || col.IsSorted() || col.IsDense() || col.IsSetId();
- use_legacy = use_legacy || col.overlay().row_map().IsIndexVector();
+ (col.IsSorted() && col.overlay().row_map().IsIndexVector());
if (use_legacy) {
col.FilterInto(c.op, c.value, &rm);
continue;
@@ -265,9 +316,14 @@
uint32_t s_size = col.storage_base().non_null_size();
storage::NumericStorage storage(s_data, s_size, col.col_type());
- overlays::NullOverlay null_overlay(col.storage_base().bv());
+ SimpleColumn s_col{OverlaysVec(), &storage, col.IsSorted()};
- SimpleColumn s_col{OverlaysVec(), &storage};
+ overlays::SelectorOverlay selector_overlay(
+ col.overlay().row_map().GetIfBitVector());
+ if (col.overlay().size() != col.storage_base().size())
+ s_col.overlays.emplace_back(&selector_overlay);
+
+ overlays::NullOverlay null_overlay(col.storage_base().bv());
if (col.IsNullable()) {
s_col.overlays.emplace_back(&null_overlay);
}
diff --git a/src/trace_processor/db/query_executor.h b/src/trace_processor/db/query_executor.h
index 2e04bb9..d875453 100644
--- a/src/trace_processor/db/query_executor.h
+++ b/src/trace_processor/db/query_executor.h
@@ -42,6 +42,8 @@
base::SmallVector<const overlays::StorageOverlay*, kMaxOverlayCount>
overlays;
const storage::Storage* storage;
+ // TODO(b/283763282): Move knowledge about sorted state to Storage
+ bool sorted_intrinsically = false;
};
// |row_count| is the size of the last overlay.
@@ -85,6 +87,12 @@
return IndexSearch(c, col, rm);
}
+ static void BinarySearchForTesting(const Constraint& c,
+ const SimpleColumn& col,
+ RowMap* rm) {
+ BinarySearch(c, col, rm);
+ }
+
private:
// Updates RowMap with result of filtering single column using the Constraint.
static void FilterColumn(const Constraint&, const SimpleColumn&, RowMap*);
@@ -95,6 +103,8 @@
const SimpleColumn&,
RowMap*);
+ static void BinarySearch(const Constraint&, const SimpleColumn&, RowMap*);
+
// Filters the column using Index algorithm - finds the indices to filter the
// storage with.
static RowMap IndexSearch(const Constraint&, const SimpleColumn&, RowMap*);
diff --git a/src/trace_processor/db/query_executor_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 55a7251..e6eb97d 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -15,18 +15,40 @@
*/
#include <benchmark/benchmark.h>
+#include <string>
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/string_utils.h"
#include "src/base/test/utils.h"
#include "src/trace_processor/db/query_executor.h"
+#include "src/trace_processor/db/table.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
#include "src/trace_processor/tables/slice_tables_py.h"
+#include "src/trace_processor/tables/track_tables_py.h"
namespace perfetto {
namespace trace_processor {
namespace {
using SliceTable = tables::SliceTable;
+using ThreadTrackTable = tables::ThreadTrackTable;
+using ExpectedFrameTimelineSliceTable = tables::ExpectedFrameTimelineSliceTable;
+using RawTable = tables::RawTable;
+using FtraceEventTable = tables::FtraceEventTable;
+
+// `SELECT * FROM SLICE` on android_monitor_contention_trace.at
+static char kSliceTable[] = "test/data/slice_table_for_benchmarks.csv";
+
+// `SELECT * FROM SLICE` on android_monitor_contention_trace.at
+static char kExpectedFrameTimelineTable[] =
+ "test/data/expected_frame_timeline_for_benchmarks.csv";
+
+// `SELECT id, cpu FROM raw` on chrome_android_systrace.pftrace.
+static char kRawTable[] = "test/data/raw_cpu_for_benchmarks.csv";
+
+// `SELECT id, cpu FROM ftrace_event` on chrome_android_systrace.pftrace.
+static char kFtraceEventTable[] =
+ "test/data/ftrace_event_cpu_for_benchmarks.csv";
enum DB { V1, V2 };
@@ -51,12 +73,10 @@
return output;
}
-std::vector<SliceTable::Row> LoadRowsFromCSVToSliceTable(
- benchmark::State& state) {
- std::vector<SliceTable::Row> rows;
+std::vector<std::string> ReadCSV(benchmark::State& state,
+ std::string file_name) {
std::string table_csv;
- static const char kTestTrace[] = "test/data/example_android_trace_30s.csv";
- perfetto::base::ReadFile(perfetto::base::GetTestDataPath(kTestTrace),
+ perfetto::base::ReadFile(perfetto::base::GetTestDataPath(file_name),
&table_csv);
if (table_csv.empty()) {
state.SkipWithError(
@@ -65,44 +85,112 @@
return {};
}
PERFETTO_CHECK(!table_csv.empty());
-
- std::vector<std::string> rows_strings = base::SplitString(table_csv, "\n");
- for (size_t i = 1; i < rows_strings.size(); ++i) {
- std::vector<std::string> row_vec = SplitCSVLine(rows_strings[i]);
- SliceTable::Row row;
- PERFETTO_CHECK(row_vec.size() >= 12);
- row.ts = *base::StringToInt64(row_vec[2]);
- row.dur = *base::StringToInt64(row_vec[3]);
- row.track_id =
- tables::ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
- row.depth = *base::StringToUInt32(row_vec[7]);
- row.stack_id = *base::StringToInt32(row_vec[8]);
- row.parent_stack_id = *base::StringToInt32(row_vec[9]);
- row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
- ? std::make_optional<SliceTable::Id>(
- *base::StringToUInt32(row_vec[11]))
- : std::nullopt;
- row.arg_set_id = *base::StringToUInt32(row_vec[11]);
- row.thread_ts = base::StringToInt64(row_vec[12]);
- row.thread_dur = base::StringToInt64(row_vec[13]);
- rows.emplace_back(row);
- }
- return rows;
+ return base::SplitString(table_csv, "\n");
}
-struct BenchmarkSliceTable {
- explicit BenchmarkSliceTable(benchmark::State& state) : table_{&pool_} {
- auto rows = LoadRowsFromCSVToSliceTable(state);
- for (uint32_t i = 0; i < rows.size(); ++i) {
- table_.Insert(rows[i]);
+SliceTable::Row GetSliceTableRow(std::string string_row) {
+ std::vector<std::string> row_vec = SplitCSVLine(string_row);
+ SliceTable::Row row;
+ PERFETTO_CHECK(row_vec.size() >= 12);
+ row.ts = *base::StringToInt64(row_vec[2]);
+ row.dur = *base::StringToInt64(row_vec[3]);
+ row.track_id = ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
+ row.depth = *base::StringToUInt32(row_vec[7]);
+ row.stack_id = *base::StringToInt32(row_vec[8]);
+ row.parent_stack_id = *base::StringToInt32(row_vec[9]);
+ row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
+ ? std::make_optional<SliceTable::Id>(
+ *base::StringToUInt32(row_vec[11]))
+ : std::nullopt;
+ row.arg_set_id = *base::StringToUInt32(row_vec[11]);
+ row.thread_ts = base::StringToInt64(row_vec[12]);
+ row.thread_dur = base::StringToInt64(row_vec[13]);
+ return row;
+}
+
+struct SliceTableForBenchmark {
+ explicit SliceTableForBenchmark(benchmark::State& state) : table_{&pool_} {
+ std::vector<std::string> rows_strings = ReadCSV(state, kSliceTable);
+
+ for (size_t i = 1; i < rows_strings.size(); ++i) {
+ table_.Insert(GetSliceTableRow(rows_strings[i]));
}
}
+
StringPool pool_;
SliceTable table_;
};
-void SliceTableBenchmark(benchmark::State& state,
- BenchmarkSliceTable& table,
+struct ExpectedFrameTimelineTableForBenchmark {
+ explicit ExpectedFrameTimelineTableForBenchmark(benchmark::State& state)
+ : table_{&pool_, &parent_} {
+ std::vector<std::string> table_rows_as_string =
+ ReadCSV(state, kExpectedFrameTimelineTable);
+ std::vector<std::string> parent_rows_as_string =
+ ReadCSV(state, kSliceTable);
+
+ uint32_t cur_idx = 0;
+ for (size_t i = 1; i < table_rows_as_string.size(); ++i, ++cur_idx) {
+ std::vector<std::string> row_vec = SplitCSVLine(table_rows_as_string[i]);
+
+ uint32_t idx = *base::StringToUInt32(row_vec[0]);
+ while (cur_idx < idx) {
+ parent_.Insert(GetSliceTableRow(parent_rows_as_string[cur_idx + 1]));
+ cur_idx++;
+ }
+
+ ExpectedFrameTimelineSliceTable::Row row;
+ row.ts = *base::StringToInt64(row_vec[2]);
+ row.dur = *base::StringToInt64(row_vec[3]);
+ row.track_id = ThreadTrackTable::Id(*base::StringToUInt32(row_vec[4]));
+ row.depth = *base::StringToUInt32(row_vec[7]);
+ row.stack_id = *base::StringToInt32(row_vec[8]);
+ row.parent_stack_id = *base::StringToInt32(row_vec[9]);
+ row.parent_id = base::StringToUInt32(row_vec[11]).has_value()
+ ? std::make_optional<SliceTable::Id>(
+ *base::StringToUInt32(row_vec[11]))
+ : std::nullopt;
+ row.arg_set_id = *base::StringToUInt32(row_vec[11]);
+ row.thread_ts = base::StringToInt64(row_vec[12]);
+ row.thread_dur = base::StringToInt64(row_vec[13]);
+ table_.Insert(row);
+ }
+ }
+ StringPool pool_;
+ SliceTable parent_{&pool_};
+ ExpectedFrameTimelineSliceTable table_;
+};
+
+struct FtraceEventTableForBenchmark {
+ explicit FtraceEventTableForBenchmark(benchmark::State& state) {
+ std::vector<std::string> raw_rows = ReadCSV(state, kRawTable);
+ std::vector<std::string> ftrace_event_rows =
+ ReadCSV(state, kFtraceEventTable);
+
+ uint32_t cur_idx = 0;
+ for (size_t i = 1; i < ftrace_event_rows.size(); ++i, cur_idx++) {
+ std::vector<std::string> row_vec = SplitCSVLine(ftrace_event_rows[i]);
+ uint32_t idx = *base::StringToUInt32(row_vec[0]);
+ while (cur_idx < idx) {
+ std::vector<std::string> raw_row = SplitCSVLine(raw_rows[cur_idx + 1]);
+ RawTable::Row r;
+ r.cpu = *base::StringToUInt32(raw_row[1]);
+ raw_.Insert(r);
+ cur_idx++;
+ }
+ FtraceEventTable::Row row;
+ row.cpu = *base::StringToUInt32(row_vec[1]);
+ table_.Insert(row);
+ }
+ }
+
+ StringPool pool_;
+ RawTable raw_{&pool_};
+ tables::FtraceEventTable table_{&pool_, &raw_};
+};
+
+void BenchmarkSliceTable(benchmark::State& state,
+ SliceTableForBenchmark& table,
Constraint c) {
Table::kUseFilterV2 = state.range(0) == 1;
for (auto _ : state) {
@@ -114,26 +202,73 @@
benchmark::Counter::kInvert);
}
-static void BM_DBv2SliceTableTrackIdEquals(benchmark::State& state) {
- BenchmarkSliceTable table(state);
- SliceTableBenchmark(state, table, table.table_.track_id().eq(100));
+void BenchmarkExpectedFrameTable(benchmark::State& state,
+ ExpectedFrameTimelineTableForBenchmark& table,
+ Constraint c) {
+ Table::kUseFilterV2 = state.range(0) == 1;
+ for (auto _ : state) {
+ benchmark::DoNotOptimize(table.table_.FilterToRowMap({c}));
+ }
+ state.counters["s/row"] =
+ benchmark::Counter(static_cast<double>(table.table_.row_count()),
+ benchmark::Counter::kIsIterationInvariantRate |
+ benchmark::Counter::kInvert);
}
-BENCHMARK(BM_DBv2SliceTableTrackIdEquals)->ArgsProduct({{DB::V1, DB::V2}});
-
-static void BM_DBv2SliceTableParentIdIsNotNull(benchmark::State& state) {
- BenchmarkSliceTable table(state);
- SliceTableBenchmark(state, table, table.table_.parent_id().is_not_null());
+void BenchmarkFtraceEventTable(benchmark::State& state,
+ FtraceEventTableForBenchmark& table,
+ Constraint c) {
+ Table::kUseFilterV2 = state.range(0) == 1;
+ for (auto _ : state) {
+ benchmark::DoNotOptimize(table.table_.FilterToRowMap({c}));
+ }
+ state.counters["s/row"] =
+ benchmark::Counter(static_cast<double>(table.table_.row_count()),
+ benchmark::Counter::kIsIterationInvariantRate |
+ benchmark::Counter::kInvert);
}
-BENCHMARK(BM_DBv2SliceTableParentIdIsNotNull)->ArgsProduct({{DB::V1, DB::V2}});
-
-static void BM_DBv2SliceTableParentIdEq(benchmark::State& state) {
- BenchmarkSliceTable table(state);
- SliceTableBenchmark(state, table, table.table_.parent_id().eq(88));
+static void BM_QESliceTableTrackIdEq(benchmark::State& state) {
+ SliceTableForBenchmark table(state);
+ BenchmarkSliceTable(state, table, table.table_.track_id().eq(100));
}
-BENCHMARK(BM_DBv2SliceTableParentIdEq)->ArgsProduct({{DB::V1, DB::V2}});
+BENCHMARK(BM_QESliceTableTrackIdEq)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QESliceTableParentIdIsNotNull(benchmark::State& state) {
+ SliceTableForBenchmark table(state);
+ BenchmarkSliceTable(state, table, table.table_.parent_id().is_not_null());
+}
+
+BENCHMARK(BM_QESliceTableParentIdIsNotNull)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QESliceTableParentIdEq(benchmark::State& state) {
+ SliceTableForBenchmark table(state);
+ BenchmarkSliceTable(state, table, table.table_.parent_id().eq(88));
+}
+
+BENCHMARK(BM_QESliceTableParentIdEq)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QESliceTableSorted(benchmark::State& state) {
+ SliceTableForBenchmark table(state);
+ BenchmarkSliceTable(state, table, table.table_.ts().gt(1000));
+}
+
+BENCHMARK(BM_QESliceTableSorted)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QEFilterWithSparseSelector(benchmark::State& state) {
+ ExpectedFrameTimelineTableForBenchmark table(state);
+ BenchmarkExpectedFrameTable(state, table, table.table_.track_id().eq(88));
+}
+
+BENCHMARK(BM_QEFilterWithSparseSelector)->ArgsProduct({{DB::V1, DB::V2}});
+
+static void BM_QEFilterWithDenseSelector(benchmark::State& state) {
+ FtraceEventTableForBenchmark table(state);
+ BenchmarkFtraceEventTable(state, table, table.table_.cpu().eq(4));
+}
+
+BENCHMARK(BM_QEFilterWithDenseSelector)->ArgsProduct({{DB::V1, DB::V2}});
} // namespace
} // namespace trace_processor
diff --git a/src/trace_processor/db/query_executor_unittest.cc b/src/trace_processor/db/query_executor_unittest.cc
index 01fff7a..3e02d79 100644
--- a/src/trace_processor/db/query_executor_unittest.cc
+++ b/src/trace_processor/db/query_executor_unittest.cc
@@ -224,51 +224,110 @@
}
TEST(QueryExecutor, SingleConstraintWithNullAndSelector) {
- std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 0, 1, 2, 3, 4};
+ std::vector<int64_t> storage_data{0, 1, 2, 3, 0, 1, 2, 3};
NumericStorage storage(storage_data.data(), 10, ColumnType::kInt64);
- // Select 6 elements from storage, resulting in a vector {0, 1, 3, 4, 1, 2}.
- BitVector selector_bv{1, 1, 0, 1, 1, 0, 1, 1, 0, 0};
- SelectorOverlay selector_overlay(&selector_bv);
-
- // Add nulls, final vector {0, 1, NULL, 3, 4, NULL, 1, 2, NULL}.
- BitVector null_bv{1, 1, 0, 1, 1, 0, 1, 1, 0};
+ // Current vector
+ // 0, 1, NULL, 2, 3, 0, NULL, NULL, 1, 2, 3, NULL
+ BitVector null_bv{1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0};
NullOverlay null_overlay(&null_bv);
+ // Final vector
+ // 0, NULL, 3, NULL, 1, 3
+ BitVector selector_bv{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0};
+ SelectorOverlay selector_overlay(&selector_bv);
+
// Create the column.
OverlaysVec overlays_vec;
- overlays_vec.emplace_back(&null_overlay);
overlays_vec.emplace_back(&selector_overlay);
+ overlays_vec.emplace_back(&null_overlay);
SimpleColumn col{overlays_vec, &storage};
// Filter.
Constraint c{0, FilterOp::kGe, SqlValue::Long(2)};
- QueryExecutor exec({col}, 9);
+ QueryExecutor exec({col}, 6);
RowMap res = exec.Filter({c});
- ASSERT_EQ(res.size(), 3u);
- ASSERT_EQ(res.Get(0), 3u);
- ASSERT_EQ(res.Get(1), 4u);
- ASSERT_EQ(res.Get(2), 7u);
+ ASSERT_EQ(res.size(), 2u);
+ ASSERT_EQ(res.Get(0), 2u);
+ ASSERT_EQ(res.Get(1), 5u);
}
TEST(QueryExecutor, IsNull) {
- std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 0, 1, 2, 3, 4};
+ std::vector<int64_t> storage_data{0, 1, 2, 3, 0, 1, 2, 3};
NumericStorage storage(storage_data.data(), 10, ColumnType::kInt64);
- // Select 6 elements from storage, resulting in a vector {0, 1, 3, 4, 1, 2}.
+ // Current vector
+ // 0, 1, NULL, 2, 3, 0, NULL, NULL, 1, 2, 3, NULL
+ BitVector null_bv{1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0};
+ NullOverlay null_overlay(&null_bv);
+
+ // Final vector
+ // 0, NULL, 3, NULL, 1, 3
+ BitVector selector_bv{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0};
+ SelectorOverlay selector_overlay(&selector_bv);
+
+ // Create the column.
+ OverlaysVec overlays_vec;
+ overlays_vec.emplace_back(&selector_overlay);
+ overlays_vec.emplace_back(&null_overlay);
+ SimpleColumn col{overlays_vec, &storage};
+
+ // Filter.
+ Constraint c{0, FilterOp::kIsNull, SqlValue::Long(0)};
+ QueryExecutor exec({col}, 6);
+ RowMap res = exec.Filter({c});
+
+ ASSERT_EQ(res.size(), 2u);
+ ASSERT_EQ(res.Get(0), 1u);
+ ASSERT_EQ(res.Get(1), 3u);
+}
+
+TEST(QueryExecutor, BinarySearch) {
+ std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 5, 6};
+ NumericStorage storage(storage_data.data(), 7, ColumnType::kInt64);
+
+ // Add nulls - {0, 1, NULL, NULL, 2, 3, NULL, NULL, 4, 5, 6, NULL}
+ BitVector null_bv{1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0};
+ NullOverlay null_overlay(&null_bv);
+
+ // Final vector {1, NULL, 3, NULL, 5, NULL}.
+ BitVector selector_bv{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1};
+ SelectorOverlay selector_overlay(&selector_bv);
+
+ // Create the column.
+ OverlaysVec overlays_vec;
+ overlays_vec.emplace_back(&selector_overlay);
+ overlays_vec.emplace_back(&null_overlay);
+ SimpleColumn col{overlays_vec, &storage, true};
+
+ // Filter.
+ Constraint c{0, FilterOp::kGe, SqlValue::Long(3)};
+ QueryExecutor exec({col}, 6);
+ RowMap res = exec.Filter({c});
+
+ ASSERT_EQ(res.size(), 2u);
+ ASSERT_EQ(res.Get(0), 2u);
+ ASSERT_EQ(res.Get(1), 4u);
+}
+
+TEST(QueryExecutor, BinarySearchIsNull) {
+ std::vector<int64_t> storage_data{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+ NumericStorage storage(storage_data.data(), 10, ColumnType::kInt64);
+
+ // Select 6 elements from storage, resulting in a vector {0, 1, 3, 4, 6, 7}.
BitVector selector_bv{1, 1, 0, 1, 1, 0, 1, 1, 0, 0};
SelectorOverlay selector_overlay(&selector_bv);
- // Add nulls, final vector {0, 1, NULL, 3, 4, NULL, 1, 2, NULL}.
- BitVector null_bv{1, 1, 0, 1, 1, 0, 1, 1, 0};
+ // Add nulls, final vector {NULL, NULL, NULL 0, 1, 3, 4, 6, 7}.
+ BitVector null_bv{0, 0, 0, 1, 1, 1, 1, 1, 1};
NullOverlay null_overlay(&null_bv);
// Create the column.
OverlaysVec overlays_vec;
overlays_vec.emplace_back(&null_overlay);
overlays_vec.emplace_back(&selector_overlay);
- SimpleColumn col{overlays_vec, &storage};
+ SimpleColumn col{overlays_vec, &storage, true};
// Filter.
Constraint c{0, FilterOp::kIsNull, SqlValue::Long(0)};
@@ -276,9 +335,9 @@
RowMap res = exec.Filter({c});
ASSERT_EQ(res.size(), 3u);
- ASSERT_EQ(res.Get(0), 2u);
- ASSERT_EQ(res.Get(1), 5u);
- ASSERT_EQ(res.Get(2), 8u);
+ ASSERT_EQ(res.Get(0), 0u);
+ ASSERT_EQ(res.Get(1), 1u);
+ ASSERT_EQ(res.Get(2), 2u);
}
} // namespace
diff --git a/src/trace_processor/db/storage/numeric_storage.cc b/src/trace_processor/db/storage/numeric_storage.cc
index 0c3f345..71eebec 100644
--- a/src/trace_processor/db/storage/numeric_storage.cc
+++ b/src/trace_processor/db/storage/numeric_storage.cc
@@ -321,7 +321,7 @@
RowMap::Range search_range) const {
std::optional<NumericValue> val = GetNumericTypeVariant(type_, sql_val);
if (op == FilterOp::kIsNotNull)
- return RowMap::Range(0, size());
+ return search_range;
if (!val.has_value() || op == FilterOp::kIsNull || op == FilterOp::kGlob)
return RowMap::Range();
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index a566661..d61d0e3 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -22,6 +22,7 @@
"android_batt.sql",
"android_binder.sql",
"android_blocking_calls_cuj_metric.sql",
+ "android_sysui_notifications_blocking_calls_metric.sql",
"android_camera.sql",
"android_camera_unagg.sql",
"android_cpu.sql",
diff --git a/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql b/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
new file mode 100644
index 0000000..470b245
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/android_sysui_notifications_blocking_calls_metric.sql
@@ -0,0 +1,53 @@
+--
+-- Copyright 2023 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+-- https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+
+SELECT IMPORT('android.slices');
+
+DROP TABLE IF EXISTS android_sysui_notifications_blocking_calls;
+CREATE TABLE android_sysui_notifications_blocking_calls AS
+SELECT
+ s.name name,
+ COUNT(s.name) count,
+ MAX(dur) AS max_dur_ns,
+ MIN(dur) AS min_dur_ns,
+ SUM(dur) AS total_dur_ns
+FROM slice s
+ JOIN thread_track ON s.track_id = thread_track.id
+ JOIN thread USING (utid)
+WHERE
+ thread.is_main_thread AND
+ s.dur > 0 AND (
+ s.name GLOB 'NotificationStackScrollLayout#onMeasure'
+ OR s.name GLOB 'NotificationToplineView#onMeasure'
+ OR s.name GLOB 'ExpNotRow#*'
+)
+GROUP BY s.name;
+
+DROP VIEW IF EXISTS android_sysui_notifications_blocking_calls_metric_output;
+CREATE VIEW android_sysui_notifications_blocking_calls_metric_output AS
+SELECT AndroidSysUINotificationsBlockingCallsMetric('blocking_calls', (
+ SELECT RepeatedField(
+ AndroidSysUINotificationsBlockingCallsMetric_BlockingCall(
+ 'name', a.name,
+ 'cnt', a.count,
+ 'total_dur_ns', a.total_dur_ns,
+ 'max_dur_ns', a.max_dur_ns,
+ 'min_dur_ns', a.min_dur_ns
+ )
+ )
+ FROM android_sysui_notifications_blocking_calls a
+ ORDER BY total_dur_ns DESC
+ )
+);
diff --git a/src/trace_processor/stdlib/experimental/thread_executing_span.sql b/src/trace_processor/stdlib/experimental/thread_executing_span.sql
index 338ec66..44bac68 100644
--- a/src/trace_processor/stdlib/experimental/thread_executing_span.sql
+++ b/src/trace_processor/stdlib/experimental/thread_executing_span.sql
@@ -339,6 +339,8 @@
-- @column blocked_dur Duration of blocking thread state before waking up.
-- @column blocked_state Thread state ('D' or 'S') of blocked thread_state before waking up.
-- @column blocked_function Kernel blocking function of thread state before waking up.
+-- @column is_root Whether this span is the root in the slice tree.
+-- @column is_leaf Whether this span is the leaf in the slice tree.
-- @column depth Tree depth from |root_id|
-- @column root_id Thread state id used to start the recursion. Helpful for SQL JOINs
SELECT CREATE_VIEW_FUNCTION(
@@ -410,6 +412,8 @@
-- @column blocked_dur Duration of blocking thread state before waking up.
-- @column blocked_state Thread state ('D' or 'S') of blocked thread_state before waking up.
-- @column blocked_function Kernel blocking function of thread state before waking up.
+-- @column is_root Whether this span is the root in the slice tree.
+-- @column is_leaf Whether this span is the leaf in the slice tree.
-- @column height Tree height from |leaf_id|
-- @column leaf_id Thread state id used to start the recursion. Helpful for SQL JOINs
SELECT CREATE_VIEW_FUNCTION(
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index be2a27c..18d302c 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -3092,13 +3092,15 @@
TRACE_EVENT(
"test", "TestEventWithExtensionArgs",
perfetto::protos::pbzero::TestExtension::kIntExtensionForTesting,
- std::vector<int>{42});
+ std::vector<int>{42},
+ perfetto::protos::pbzero::TestExtension::kUintExtensionForTesting, 42u);
}
std::vector<char> raw_trace = StopSessionAndReturnBytes(tracing_session);
EXPECT_GE(raw_trace.size(), 0u);
- bool found_extension = false;
+ bool found_int_extension = false;
+ bool found_uint_extension = false;
perfetto::protos::pbzero::Trace_Decoder trace(
reinterpret_cast<uint8_t*>(raw_trace.data()), raw_trace.size());
@@ -3116,12 +3118,17 @@
f = decoder.ReadField()) {
if (f.id() == perfetto::protos::pbzero::TestExtension::
FieldMetadata_IntExtensionForTesting::kFieldId) {
- found_extension = true;
+ found_int_extension = true;
+ } else if (f.id() ==
+ perfetto::protos::pbzero::TestExtension::
+ FieldMetadata_UintExtensionForTesting::kFieldId) {
+ found_uint_extension = true;
}
}
}
- EXPECT_TRUE(found_extension);
+ EXPECT_TRUE(found_int_extension);
+ EXPECT_TRUE(found_uint_extension);
}
TEST_P(PerfettoApiTest, TrackEventInstant) {
diff --git a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
new file mode 100644
index 0000000..5b69129
--- /dev/null
+++ b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.out
@@ -0,0 +1,23 @@
+android_sysui_notifications_blocking_calls_metric {
+ blocking_calls {
+ name: "ExpNotRow#onMeasure(BigTextStyle)"
+ cnt: 1
+ total_dur_ns: 10000000
+ max_dur_ns: 10000000
+ min_dur_ns: 10000000
+ }
+ blocking_calls {
+ name: "ExpNotRow#onMeasure(MessagingStyle)"
+ cnt: 1
+ total_dur_ns: 10000000
+ max_dur_ns: 10000000
+ min_dur_ns: 10000000
+ }
+ blocking_calls {
+ name: "NotificationStackScrollLayout#onMeasure"
+ cnt: 1
+ total_dur_ns: 10000000
+ max_dur_ns: 10000000
+ min_dur_ns: 10000000
+ }
+}
diff --git a/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
new file mode 100644
index 0000000..c5df8b4
--- /dev/null
+++ b/test/trace_processor/diff_tests/android/android_sysui_notifications_blocking_calls_metric.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from os import sys, path
+import synth_common
+
+# com.android.systemui
+SYSUI_PID = 1000
+
+THIRD_PROCESS_PID = 3000
+
+# List of blocking calls
+blocking_call_names = [
+ 'NotificationStackScrollLayout#onMeasure', 'ExpNotRow#onMeasure(MessagingStyle)',
+ 'ExpNotRow#onMeasure(BigTextStyle)',
+ 'Should not be in the metric'
+]
+
+
+def add_main_thread_atrace(trace, ts, ts_end, buf, pid):
+ trace.add_atrace_begin(ts=ts, tid=pid, pid=pid, buf=buf)
+ trace.add_atrace_end(ts=ts_end, tid=pid, pid=pid)
+
+
+def add_async_trace(trace, ts, ts_end, buf, pid):
+ trace.add_atrace_async_begin(ts=ts, tid=pid, pid=pid, buf=buf)
+ trace.add_atrace_async_end(ts=ts_end, tid=pid, pid=pid, buf=buf)
+
+# Creates a trace that contains one of each blocking call.
+def add_all_sysui_notifications_blocking_calls(trace, pid):
+ blocking_call_dur = 10_000_000
+ blocking_call_ts = 2_000_000
+
+ cuj_dur = len(blocking_call_names) * blocking_call_dur
+ add_async_trace(
+ trace,
+ ts=blocking_call_ts,
+ ts_end=blocking_call_ts + cuj_dur,
+ buf="L<TEST_WITH_MANY_BLOCKING_CALLS>",
+ pid=pid)
+
+ for blocking_call in blocking_call_names:
+ add_main_thread_atrace(
+ trace,
+ ts=blocking_call_ts,
+ ts_end=blocking_call_ts + blocking_call_dur,
+ buf=blocking_call,
+ pid=pid)
+ blocking_call_ts += blocking_call_dur
+
+
+def add_process(trace, package_name, uid, pid):
+ trace.add_package_list(ts=0, name=package_name, uid=uid, version_code=1)
+ trace.add_process(
+ pid=pid, ppid=0, cmdline=package_name, uid=uid)
+ trace.add_thread(tid=pid, tgid=pid, cmdline="MainThread", name="MainThread")
+
+
+def setup_trace():
+ trace = synth_common.create_trace()
+ trace.add_packet()
+ add_process(trace, package_name="com.android.systemui", uid=10001,
+ pid=SYSUI_PID)
+ trace.add_ftrace_packet(cpu=0)
+ return trace
+
+
+trace = setup_trace()
+
+
+add_all_sysui_notifications_blocking_calls(trace, pid=SYSUI_PID)
+
+# See test_android_sysui_notifications_blocking_calls.
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index 7d683cf..ba2d198 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -396,6 +396,12 @@
""",
out=Path('android_slice_standardization.out'))
+ def test_android_sysui_notifications_blocking_calls(self):
+ return DiffTestBlueprint(
+ trace=Path('android_sysui_notifications_blocking_calls_metric.py'),
+ query=Metric('android_sysui_notifications_blocking_calls_metric'),
+ out=Path('android_sysui_notifications_blocking_calls_metric.out'))
+
def test_monitor_contention_extraction(self):
return DiffTestBlueprint(
trace=DataPath('android_monitor_contention_trace.atr'),
diff --git a/tools/check_sql_modules.py b/tools/check_sql_modules.py
index 8037fca..f50fdbd 100755
--- a/tools/check_sql_modules.py
+++ b/tools/check_sql_modules.py
@@ -16,6 +16,7 @@
# This tool checks that every SQL object created without prefix
# 'internal_' is documented with proper schema.
+import argparse
import os
import sys
@@ -26,9 +27,13 @@
def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--stdlib-sources',
+ default=os.path.join(ROOT_DIR, "src", "trace_processor", "stdlib"))
+ args = parser.parse_args()
errors = []
- metrics_sources = os.path.join(ROOT_DIR, "src", "trace_processor", "stdlib")
- for root, _, files in os.walk(metrics_sources, topdown=True):
+ for root, _, files in os.walk(args.stdlib_sources, topdown=True):
for f in files:
path = os.path.join(root, f)
if not path.endswith(".sql"):
diff --git a/tools/gen_stdlib_docs_json.py b/tools/gen_stdlib_docs_json.py
index 1f2e5be..dcb4023 100755
--- a/tools/gen_stdlib_docs_json.py
+++ b/tools/gen_stdlib_docs_json.py
@@ -71,6 +71,11 @@
import_key = path.split(".sql")[0].replace("/", ".")
docs = parse_file_to_dict(path, sql)
+ if isinstance(docs, list):
+ for d in docs:
+ print(d)
+ return 1
+
assert isinstance(docs, dict)
if not any(docs.values()):
continue
diff --git a/ui/OWNERS b/ui/OWNERS
index 7b9c12b..aaf587f 100644
--- a/ui/OWNERS
+++ b/ui/OWNERS
@@ -1,5 +1,6 @@
hjd@google.com
primiano@google.com
+stevegolton@google.com
# Chrome-related bits (but also good escalations for many other UI changes).
ddrone@google.com
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 08c2ac6..ad91f01 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -219,7 +219,8 @@
}
td {
- padding: 2px 1px;
+ padding: 3px 5px;
+ white-space: nowrap;
i.material-icons {
// Shrink the icons inside the table cells to increase the information
@@ -261,14 +262,22 @@
}
}
-.query-table {
- width: 100%;
+.pf-query-panel {
+ display: contents;
+ .pf-query-warning {
+ padding: 4px;
+ position: sticky;
+ left: 0;
+ }
+}
+
+.pf-query-table {
+ min-width: 100%;
font-size: 14px;
border: 0;
thead td {
- // TODO(stevegolton): Get sticky working again.
- // position: sticky;
- // top: 0;
+ position: sticky;
+ top: 0;
background-color: hsl(214, 22%, 90%);
color: #262f3c;
text-align: center;
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 640b30e..0b1e705 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -26,20 +26,23 @@
@import "flags_page";
@import "hiring_banner";
@import "widgets_page";
+@import "widgets/anchor";
@import "widgets/button";
@import "widgets/checkbox";
-@import "widgets/text_input";
-@import "widgets/empty_state";
-@import "widgets/anchor";
-@import "widgets/popup";
-@import "widgets/multiselect";
-@import "widgets/select";
-@import "widgets/menu";
-@import "widgets/spinner";
-@import "widgets/tree";
-@import "widgets/switch";
-@import "widgets/form";
@import "widgets/details_shell";
+@import "widgets/empty_state";
+@import "widgets/error";
+@import "widgets/form";
@import "widgets/grid_layout";
+@import "widgets/menu";
+@import "widgets/multiselect";
+@import "widgets/popup";
@import "widgets/section";
@import "widgets/timestamp";
+@import "widgets/select";
+@import "widgets/spinner";
+@import "widgets/switch";
+@import "widgets/text_input";
+@import "widgets/tree";
+@import "widgets/virtual_scroll_container";
+@import "widgets/callout";
diff --git a/ui/src/assets/widgets/callout.scss b/ui/src/assets/widgets/callout.scss
new file mode 100644
index 0000000..da47edf
--- /dev/null
+++ b/ui/src/assets/widgets/callout.scss
@@ -0,0 +1,33 @@
+// 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 "theme";
+
+.pf-callout {
+ display: flex;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: 1;
+ color: $pf-minimal-foreground;
+ background: rgb(239, 239, 239);
+ border-radius: $pf-border-radius;
+ padding: 4px;
+ border: solid lightgray 1px;
+
+ & > .pf-left-icon {
+ font-size: inherit;
+ margin-right: 6px;
+ align-items: baseline;
+ }
+}
diff --git a/ui/src/assets/widgets/details_shell.scss b/ui/src/assets/widgets/details_shell.scss
index 75c6af9..a71f237 100644
--- a/ui/src/assets/widgets/details_shell.scss
+++ b/ui/src/assets/widgets/details_shell.scss
@@ -18,17 +18,10 @@
font-family: $pf-font;
display: flex;
flex-direction: column;
- min-height: 100%;
-
- &.pf-match-parent {
- height: 100%;
- }
.pf-header-bar {
- z-index: 1; // HACK: Make the header bar appear above the content
- position: sticky;
- top: 0;
- left: 0;
+ z-index: 1;
+
display: flex;
flex-direction: row;
align-items: baseline;
@@ -36,7 +29,6 @@
background-color: white;
color: black;
padding: 8px 8px 5px 8px;
- box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
.pf-header-title {
@@ -58,6 +50,7 @@
display: flex;
min-width: min-content;
gap: 4px;
+ align-items: baseline;
}
}
@@ -65,45 +58,13 @@
font-size: smaller;
flex-grow: 1;
font-weight: 300;
+ overflow-x: auto;
+ }
- table {
- @include transition(0.1s);
- @include table-font-size;
- width: 100%;
- // Aggregation panel uses multiple table elements that need to be aligned,
- // which is done by using fixed table layout.
- table-layout: fixed;
- word-wrap: break-word;
- padding: 0 10px;
- tr:hover {
- td,
- th {
- background-color: $table-hover-color;
-
- &.no-highlight {
- background-color: white;
- }
- }
- }
- th {
- text-align: left;
- width: 30%;
- font-weight: normal;
- vertical-align: top;
- }
- td.value {
- white-space: pre-wrap;
- }
- td.padding {
- min-width: 10px;
- }
- .array-index {
- text-align: right;
- }
- }
-
- .auto-layout {
- table-layout: auto;
+ &.pf-fill-parent {
+ height: 100%;
+ .pf-content {
+ overflow-y: auto;
}
}
}
diff --git a/ui/src/assets/widgets/details_table.scss b/ui/src/assets/widgets/details_table.scss
new file mode 100644
index 0000000..297c7ae
--- /dev/null
+++ b/ui/src/assets/widgets/details_table.scss
@@ -0,0 +1,53 @@
+// 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.
+
+table.pf-details-table {
+ @include transition(0.1s);
+ @include table-font-size;
+ width: 100%;
+ // Aggregation panel uses multiple table elements that need to be aligned,
+ // which is done by using fixed table layout.
+ table-layout: fixed;
+ word-wrap: break-word;
+ padding: 0 10px;
+ tr:hover {
+ td,
+ th {
+ background-color: $table-hover-color;
+
+ &.no-highlight {
+ background-color: white;
+ }
+ }
+ }
+ th {
+ text-align: left;
+ width: 30%;
+ font-weight: normal;
+ vertical-align: top;
+ }
+ td.value {
+ white-space: pre-wrap;
+ }
+ td.padding {
+ min-width: 10px;
+ }
+ .array-index {
+ text-align: right;
+ }
+}
+
+.auto-layout {
+ table-layout: auto;
+}
diff --git a/ui/src/assets/widgets/error.scss b/ui/src/assets/widgets/error.scss
new file mode 100644
index 0000000..dfee07c
--- /dev/null
+++ b/ui/src/assets/widgets/error.scss
@@ -0,0 +1,20 @@
+// 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.
+
+.pf-error {
+ padding: 20px 10px;
+ color: hsl(-10, 50%, 50%);
+ font-family: $pf-font;
+ font-weight: 300;
+}
diff --git a/ui/src/assets/widgets/timestamp.scss b/ui/src/assets/widgets/timestamp.scss
index f361ff4..c71bb8d 100644
--- a/ui/src/assets/widgets/timestamp.scss
+++ b/ui/src/assets/widgets/timestamp.scss
@@ -14,23 +14,17 @@
@import "theme";
-// Make millis, micros, & nanos slightly smaller than hh:mm:ss for readability.
-$subsec-font-size: 0.9em;
-
.pf-timecode {
// Spacing the sub sections using CSS rather than spaces makes the spaces
// disappear when copying.
.pf-timecode-millis {
margin-left: 1px;
- font-size: $subsec-font-size;
}
.pf-timecode-micros {
margin-left: 2px;
- font-size: $subsec-font-size;
}
.pf-timecode-nanos {
margin-left: 2px;
- font-size: $subsec-font-size;
}
.pf-button {
margin-left: 2px;
diff --git a/ui/src/assets/widgets/virtual_scroll_container.scss b/ui/src/assets/widgets/virtual_scroll_container.scss
new file mode 100644
index 0000000..d48fbeb
--- /dev/null
+++ b/ui/src/assets/widgets/virtual_scroll_container.scss
@@ -0,0 +1,20 @@
+// 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 "theme";
+
+.pf-virtual-scroll-container {
+ overflow: auto;
+ height: 100%;
+}
diff --git a/ui/src/assets/widgets_page.scss b/ui/src/assets/widgets_page.scss
index 96286bc..4d3309a 100644
--- a/ui/src/assets/widgets_page.scss
+++ b/ui/src/assets/widgets_page.scss
@@ -58,6 +58,7 @@
.widget-container {
display: flex;
min-width: 300px;
+ max-width: 600px;
min-height: 250px;
border-radius: 3px;
box-shadow: inset 2px 2px 10px #00000020;
diff --git a/ui/src/base/array_utils.ts b/ui/src/base/array_utils.ts
index a3a9980..7236a43 100644
--- a/ui/src/base/array_utils.ts
+++ b/ui/src/base/array_utils.ts
@@ -31,3 +31,12 @@
export function allUnique(x: string[]): boolean {
return x.length == new Set(x).size;
}
+
+export function arrayEquals(a: any[]|undefined, b: any[]|undefined): boolean {
+ if (a === undefined || b === undefined) return false;
+ if (a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) return false;
+ }
+ return true;
+}
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 3b95c53..42f2a48 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -178,7 +178,7 @@
};
this.sendMessage(response);
if (res.eof) return;
- this.readBuffers(offset + res.data.length);
+ this.readBuffers(offset + chunk.length);
}
async disableTracing() {
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
index 1d63819..bf2518a 100644
--- a/ui/src/common/queries.ts
+++ b/ui/src/common/queries.ts
@@ -28,8 +28,14 @@
statementWithOutputCount: number;
}
+export interface QueryRunParams {
+ // If true, replaces nulls with "NULL" string. Default is true.
+ convertNullsToString?: boolean;
+}
+
export async function runQuery(
- sqlQuery: string, engine: EngineProxy): Promise<QueryResponse> {
+ sqlQuery: string, engine: EngineProxy, params?: QueryRunParams):
+ Promise<QueryResponse> {
const startMs = performance.now();
const queryRes = engine.query(sqlQuery);
@@ -47,6 +53,8 @@
// errored, the frontend will show a graceful message instead.
}
+ const convertNullsToString = params?.convertNullsToString ?? true;
+
const durationMs = performance.now() - startMs;
const rows: Row[] = [];
const columns = queryRes.columns();
@@ -55,7 +63,7 @@
const row: Row = {};
for (const colName of columns) {
const value = iter.get(colName);
- row[colName] = value === null ? 'NULL' : value;
+ row[colName] = value === null && convertNullsToString ? 'NULL' : value;
}
rows.push(row);
if (++numRows >= MAX_DISPLAY_ROWS) break;
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
index 90fb8f4..9a9883c 100644
--- a/ui/src/common/query_result.ts
+++ b/ui/src/common/query_result.ts
@@ -71,6 +71,7 @@
export const LONG_NULL: bigint|null = 1n;
export type ColumnType = string|number|bigint|null|Uint8Array;
+export type SqlValue = ColumnType;
const SHIFT_32BITS = 32n;
@@ -159,7 +160,7 @@
// One row extracted from an SQL result:
export interface Row {
- [key: string]: ColumnType|undefined;
+ [key: string]: ColumnType;
}
// The methods that any iterator has to implement.
diff --git a/ui/src/frontend/analyze_page.ts b/ui/src/frontend/analyze_page.ts
index 85ad308..dc0025f 100644
--- a/ui/src/frontend/analyze_page.ts
+++ b/ui/src/frontend/analyze_page.ts
@@ -220,6 +220,7 @@
state.queryResult = undefined;
globals.rafScheduler.scheduleFullRedraw();
},
+ fillParent: false,
}),
m(QueryHistoryComponent));
},
diff --git a/ui/src/frontend/bottom_tab.ts b/ui/src/frontend/bottom_tab.ts
index f78bd0c..e192969 100644
--- a/ui/src/frontend/bottom_tab.ts
+++ b/ui/src/frontend/bottom_tab.ts
@@ -118,6 +118,10 @@
void;
abstract viewTab(): void|m.Children;
+ close(): void {
+ closeTab(this.uuid);
+ }
+
createPanelVnode(): m.Vnode<any, any> {
return m(
BottomTabAdapter,
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 4dd2fdf..1d34836 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -21,19 +21,27 @@
import {runQuery} from '../common/queries';
import {LONG, LONG_NULL, NUM, STR_NULL} from '../common/query_result';
import {
+ formatDuration,
TPDuration,
TPTime,
} from '../common/time';
import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
import {Anchor} from './anchor';
-import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
+import {
+ addTab,
+ BottomTab,
+ bottomTabRegistry,
+ NewBottomTabArgs,
+} from './bottom_tab';
import {FlowPoint, globals} from './globals';
import {PanelSize} from './panel';
import {runQueryInNewTab} from './query_result_tab';
import {Icons} from './semantic_icons';
import {Arg} from './sql/args';
import {getSlice, SliceDetails, SliceRef} from './sql/slice';
+import {SqlTableTab} from './sql_table/tab';
+import {SqlTables} from './sql_table/well_known_tables';
import {asSliceSqlId, asTPTimestamp} from './sql_types';
import {getProcessName, getThreadName} from './thread_and_process_info';
import {Button} from './widgets/button';
@@ -266,7 +274,7 @@
function computeDuration(ts: TPTime, dur: TPDuration): m.Children {
if (dur === -1n) {
const minDuration = globals.state.traceTime.end - ts;
- return [m(Duration, {dur: minDuration}), ' (Did not end)'];
+ return `${formatDuration(minDuration)} (Did not end)`;
} else {
return m(Duration, {dur});
}
@@ -326,7 +334,7 @@
export class ChromeSliceDetailsTab extends
BottomTab<ChromeSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.ChromeSliceDetailsTab';
+ static readonly kind = 'dev.perfetto.ChromeSliceDetailsTab';
private sliceDetails?: SliceDetails;
@@ -398,7 +406,28 @@
{title: 'Details'},
m(
Tree,
- m(TreeNode, {left: 'Name', right: slice.name}),
+ m(TreeNode, {
+ left: 'Name',
+ right: m(
+ PopupMenu2,
+ {
+ trigger: m(Anchor, slice.name),
+ },
+ m(MenuItem, {
+ label: 'Slices with the same name',
+ onclick: () => {
+ addTab({
+ kind: SqlTableTab.kind,
+ config: {
+ table: SqlTables.slice,
+ displayName: 'slice',
+ filters: [`name = ${sqliteString(slice.name)}`],
+ },
+ });
+ },
+ }),
+ ),
+ }),
m(TreeNode, {
left: 'Category',
right: !slice.category || slice.category === '[NULL]' ?
diff --git a/ui/src/frontend/ftrace_panel.ts b/ui/src/frontend/ftrace_panel.ts
index 84337a9..1fb0c2c 100644
--- a/ui/src/frontend/ftrace_panel.ts
+++ b/ui/src/frontend/ftrace_panel.ts
@@ -15,7 +15,6 @@
import m from 'mithril';
import {StringListPatch} from 'src/common/state';
-import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
import {colorForString} from '../common/colorizer';
import {TPTime} from '../common/time';
@@ -31,6 +30,7 @@
} from './widgets/multiselect';
import {PopupPosition} from './widgets/popup';
import {Timestamp} from './widgets/timestamp';
+import {VirtualScrollContainer} from './widgets/virtual_scroll_container';
const ROW_H = 20;
const PAGE_SIZE = 250;
@@ -63,23 +63,14 @@
title: this.renderTitle(),
buttons: this.renderFilterPanel(),
},
- m('.ftrace-panel', this.renderRows()));
- }
-
- private scrollContainer(dom: Element): HTMLElement {
- const el = dom.parentElement;
- return assertExists(el);
- }
-
- oncreate({dom}: m.CVnodeDOM) {
- const sc = this.scrollContainer(dom);
- sc.addEventListener('scroll', this.onScroll);
- this.recomputeVisibleRowsAndUpdate(sc);
- }
-
- onupdate({dom}: m.CVnodeDOM) {
- const sc = this.scrollContainer(dom);
- this.recomputeVisibleRowsAndUpdate(sc);
+ m(
+ VirtualScrollContainer,
+ {
+ onScroll: this.onScroll,
+ },
+ m('.ftrace-panel', this.renderRows()),
+ ),
+ );
}
recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
@@ -101,19 +92,15 @@
}
}
- onremove({dom}: m.CVnodeDOM) {
- const sc = this.scrollContainer(dom);
- sc.removeEventListener('scroll', this.onScroll);
-
+ onremove(_: m.CVnodeDOM) {
globals.dispatch(Actions.updateFtracePagination({
offset: 0,
count: 0,
}));
}
- onScroll = (e: Event) => {
- const scrollContainer = e.target as HTMLElement;
- this.recomputeVisibleRowsAndUpdate(scrollContainer);
+ onScroll = (container: HTMLElement) => {
+ this.recomputeVisibleRowsAndUpdate(container);
};
onRowOver(ts: TPTime) {
@@ -153,7 +140,6 @@
{
label: 'Filter',
minimal: true,
- compact: true,
icon: 'filter_list_alt',
popupPosition: PopupPosition.Top,
options,
diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts
index 0cf3043..5d206f7 100644
--- a/ui/src/frontend/generic_slice_details_tab.ts
+++ b/ui/src/frontend/generic_slice_details_tab.ts
@@ -46,7 +46,7 @@
// need to be rendered and how.
export class GenericSliceDetailsTab extends
BottomTab<GenericSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.GenericSliceDetailsTab';
+ static readonly kind = 'dev.perfetto.GenericSliceDetailsTab';
data: {[key: string]: ColumnType}|undefined;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index be1f6cd..dfa4e0f 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -578,12 +578,16 @@
this._ftracePanelData = data;
}
- makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') {
+ makeSelection(
+ action: DeferredAction<{}>, tab: string|null = 'current_selection') {
const previousState = this.state;
// A new selection should cancel the current search selection.
globals.dispatch(Actions.setSearchIndex({index: -1}));
- const tab = action.type === 'deselect' ? undefined : tabToOpen;
- globals.dispatch(Actions.setCurrentTab({tab}));
+ if (action.type === 'deselect') {
+ globals.dispatch(Actions.setCurrentTab({tab: undefined}));
+ } else if (tab !== null) {
+ globals.dispatch(Actions.setCurrentTab({tab: tab}));
+ }
globals.dispatch(action);
// HACK(stevegolton + altimin): This is a workaround to allow passing the
diff --git a/ui/src/frontend/logs_filters.ts b/ui/src/frontend/logs_filters.ts
index 432913f..dff5ef3 100644
--- a/ui/src/frontend/logs_filters.ts
+++ b/ui/src/frontend/logs_filters.ts
@@ -15,7 +15,11 @@
import m from 'mithril';
import {Actions} from '../common/actions';
+
import {globals} from './globals';
+import {Button} from './widgets/button';
+import {Select} from './widgets/select';
+import {TextInput} from './widgets/text_input';
export const LOG_PRIORITIES =
['-', '-', 'Verbose', 'Debug', 'Info', 'Warn', 'Error', 'Fatal'];
@@ -50,7 +54,7 @@
m('option', {value: i, selected}, attrs.options[i]));
}
return m(
- 'select',
+ Select,
{
onchange: (e: InputEvent) => {
const selectionValue = (e.target as HTMLSelectElement).value;
@@ -64,16 +68,11 @@
class LogTagChip implements m.ClassComponent<LogTagChipAttrs> {
view({attrs}: m.CVnode<LogTagChipAttrs>) {
- return m(
- '.chip',
- m('.chip-text', attrs.name),
- m('button.chip-button',
- {
- onclick: () => {
- attrs.removeTag(attrs.name);
- },
- },
- '×'));
+ return m(Button, {
+ label: attrs.name,
+ rightIcon: 'close',
+ onclick: () => attrs.removeTag(attrs.name),
+ });
}
}
@@ -84,13 +83,14 @@
view(vnode: m.Vnode<LogTagsWidgetAttrs>) {
const tags = vnode.attrs.tags;
- return m(
- '.tag-container',
- m('.chips', tags.map((tag) => m(LogTagChip, {
- name: tag,
- removeTag: this.removeTag.bind(this),
- }))),
- m(`input.chip-input[placeholder='Add new tag']`, {
+ return [
+ tags.map((tag) => m(LogTagChip, {
+ name: tag,
+ removeTag: this.removeTag.bind(this),
+ })),
+ m(TextInput,
+ {
+ placeholder: 'Filter by tag...',
onkeydown: (e: KeyboardEvent) => {
// This is to avoid zooming on 'w'(and other unexpected effects
// of key presses in this input field).
@@ -115,14 +115,16 @@
Actions.addLogTag({tag: htmlElement.value.trim()}));
htmlElement.value = '';
},
- }));
+ }),
+ ];
}
}
class LogTextWidget implements m.ClassComponent {
view() {
return m(
- '.tag-container', m(`input.chip-input[placeholder='Search log text']`, {
+ TextInput, {
+ placeholder: 'Search logs...',
onkeydown: (e: KeyboardEvent) => {
// This is to avoid zooming on 'w'(and other unexpected effects
// of key presses in this input field).
@@ -136,7 +138,7 @@
globals.dispatch(
Actions.updateLogFilterText({textEntry: htmlElement.value}));
},
- }));
+ });
}
}
@@ -145,35 +147,32 @@
const icon = attrs.hideNonMatching ? 'unfold_less' : 'unfold_more';
const tooltip = attrs.hideNonMatching ? 'Expand all and view highlighted' :
'Collapse all';
- return m(
- '.filter-widget',
- m('.tooltip', tooltip),
- m('i.material-icons',
- {
- onclick: () => {
- globals.dispatch(Actions.toggleCollapseByTextEntry({}));
- },
- },
- icon));
+ return m(Button, {
+ icon,
+ title: tooltip,
+ disabled: globals.state.logFilteringCriteria.textEntry === '',
+ minimal: true,
+ onclick: () => globals.dispatch(Actions.toggleCollapseByTextEntry({})),
+ });
}
}
export class LogsFilters implements m.ClassComponent {
view(_: m.CVnode<{}>) {
- return m(
- '.log-filters',
- m('.log-label', 'Log Level'),
- m(LogPriorityWidget, {
- options: LOG_PRIORITIES,
- selectedIndex: globals.state.logFilteringCriteria.minimumLevel,
- onSelect: (minimumLevel) => {
- globals.dispatch(Actions.setMinimumLogLevel({minimumLevel}));
- },
- }),
- m(LogTagsWidget, {tags: globals.state.logFilteringCriteria.tags}),
- m(LogTextWidget),
- m(FilterByTextWidget, {
- hideNonMatching: globals.state.logFilteringCriteria.hideNonMatching,
- }));
+ return [
+ m('.log-label', 'Log Level'),
+ m(LogPriorityWidget, {
+ options: LOG_PRIORITIES,
+ selectedIndex: globals.state.logFilteringCriteria.minimumLevel,
+ onSelect: (minimumLevel) => {
+ globals.dispatch(Actions.setMinimumLogLevel({minimumLevel}));
+ },
+ }),
+ m(LogTagsWidget, {tags: globals.state.logFilteringCriteria.tags}),
+ m(LogTextWidget),
+ m(FilterByTextWidget, {
+ hideNonMatching: globals.state.logFilteringCriteria.hideNonMatching,
+ }),
+ ];
}
}
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index eb9a5c2..71c3b20 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -14,7 +14,6 @@
import m from 'mithril';
-import {assertExists} from '../base/logging';
import {Actions} from '../common/actions';
import {HighPrecisionTimeSpan} from '../common/high_precision_time';
import {
@@ -30,21 +29,20 @@
import {LOG_PRIORITIES, LogsFilters} from './logs_filters';
import {Panel} from './panel';
import {asTPTimestamp} from './sql_types';
+import {DetailsShell} from './widgets/details_shell';
import {Timestamp} from './widgets/timestamp';
+import {VirtualScrollContainer} from './widgets/virtual_scroll_container';
const ROW_H = 20;
export class LogPanel extends Panel<{}> {
- private scrollContainer?: HTMLElement;
private bounds?: LogBounds;
private entries?: LogEntries;
private visibleRowOffset = 0;
private visibleRowCount = 0;
- recomputeVisibleRowsAndUpdate() {
- const scrollContainer = assertExists(this.scrollContainer);
-
+ recomputeVisibleRowsAndUpdate(scrollContainer: HTMLElement) {
const prevOffset = this.visibleRowOffset;
const prevCount = this.visibleRowCount;
this.visibleRowOffset = Math.floor(scrollContainer.scrollTop / ROW_H);
@@ -59,15 +57,11 @@
}
}
- oncreate({dom}: m.CVnodeDOM) {
- this.scrollContainer = assertExists(dom.parentElement as HTMLElement);
- this.scrollContainer.addEventListener(
- 'scroll', this.onScroll.bind(this), {passive: true});
+ oncreate(_: m.CVnodeDOM) {
// TODO(stevegolton): Type assersions are a source of bugs.
// Let's try to find another way of doing this.
this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
- this.recomputeVisibleRowsAndUpdate();
}
onbeforeupdate(_: m.CVnodeDOM) {
@@ -75,14 +69,12 @@
// Let's try to find another way of doing this.
this.bounds = globals.trackDataStore.get(LogBoundsKey) as LogBounds;
this.entries = globals.trackDataStore.get(LogEntriesKey) as LogEntries;
- this.recomputeVisibleRowsAndUpdate();
}
- onScroll() {
- if (this.scrollContainer === undefined) return;
- this.recomputeVisibleRowsAndUpdate();
+ onScroll = (scrollContainer: HTMLElement) => {
+ this.recomputeVisibleRowsAndUpdate(scrollContainer);
globals.rafScheduler.scheduleFullRedraw();
- }
+ };
onRowOver(ts: TPTime) {
globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
@@ -167,18 +159,22 @@
}
}
+ // TODO(stevegolton): Add a 'loading' state to DetailsShell, which shows a
+ // scrolling scrolly bar at the bottom of the banner & map isStale to it
return m(
- '.log-panel',
- m('header',
- {
- 'class': isStale ? 'stale' : '',
- },
- [
- m('.log-rows-label',
- `Logs rows [${offset}, ${offset + count}] / ${total}`),
- m(LogsFilters),
- ]),
- m('.rows', {style: {height: `${total * ROW_H}px`}}, rows));
+ DetailsShell,
+ {
+ title: 'Android Logs',
+ description: `[${offset}, ${offset + count}] / ${total}`,
+ buttons: m(LogsFilters),
+ },
+ m(
+ VirtualScrollContainer,
+ {onScroll: this.onScroll},
+ m('.log-panel',
+ m('.rows', {style: {height: `${total * ROW_H}px`}}, rows)),
+ ),
+ );
}
renderCanvas() {}
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index e0eb644..dfcaf8c 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -56,7 +56,7 @@
}
export class QueryResultTab extends BottomTab<QueryResultTabConfig> {
- static readonly kind = 'org.perfetto.QueryResultTab';
+ static readonly kind = 'dev.perfetto.QueryResultTab';
queryResponse?: QueryResponse;
sqlViewName?: string;
@@ -105,6 +105,7 @@
return m(QueryTable, {
query: this.config.query,
resp: this.queryResponse,
+ fillParent: true,
onClose: () => closeTab(this.uuid),
contextButtons: [
this.sqlViewName === undefined ?
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index be864fc..ad75cb6 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -14,8 +14,8 @@
import m from 'mithril';
-import {BigintMath} from '../base/bigint_math';
+import {BigintMath} from '../base/bigint_math';
import {Actions} from '../common/actions';
import {QueryResponse} from '../common/queries';
import {Row} from '../common/query_result';
@@ -28,6 +28,9 @@
import {Router} from './router';
import {reveal} from './scroll_helper';
import {Button} from './widgets/button';
+import {Callout} from './widgets/callout';
+import {DetailsShell} from './widgets/details_shell';
+import {exists} from './widgets/utils';
interface QueryTableRowAttrs {
row: Row;
@@ -183,7 +186,8 @@
if (resp.error) {
return m('.query-error', `SQL error: ${resp.error}`);
} else {
- return m('table.query-table', m('thead', tableHeader), m('tbody', rows));
+ return m(
+ 'table.pf-query-table', m('thead', tableHeader), m('tbody', rows));
}
}
}
@@ -193,59 +197,81 @@
onClose: () => void;
resp?: QueryResponse;
contextButtons?: m.Child[];
+ fillParent: boolean;
}
export class QueryTable extends Panel<QueryTableAttrs> {
- view(vnode: m.CVnode<QueryTableAttrs>) {
- const resp = vnode.attrs.resp;
+ view({attrs}: m.CVnode<QueryTableAttrs>) {
+ const {
+ resp,
+ query,
+ onClose,
+ contextButtons = [],
+ fillParent,
+ } = attrs;
- const header: m.Child[] = [
- m('span',
- resp ? `Query result - ${Math.round(resp.durationMs)} ms` :
- `Query - running`),
- m('span.code.text-select', vnode.attrs.query),
- m('span.spacer'),
- ...(vnode.attrs.contextButtons ?? []),
+ return m(
+ DetailsShell,
+ {
+ title: this.renderTitle(resp),
+ description: query,
+ buttons: this.renderButtons(query, onClose, contextButtons, resp),
+ fillParent,
+ },
+ resp && this.renderTableContent(resp),
+ );
+ }
+
+ renderTitle(resp?: QueryResponse) {
+ if (exists(resp)) {
+ return `Query result - ${Math.round(resp.durationMs)} ms`;
+ } else {
+ return 'Query - running';
+ }
+ }
+
+ renderButtons(
+ query: string, onClose: () => void, contextButtons: m.Child[],
+ resp?: QueryResponse) {
+ return [
+ contextButtons,
m(Button, {
label: 'Copy query',
minimal: true,
onclick: () => {
- copyToClipboard(vnode.attrs.query);
+ copyToClipboard(query);
},
}),
+ (resp && resp.error === undefined) && m(Button, {
+ label: 'Copy result (.tsv)',
+ minimal: true,
+ onclick: () => {
+ queryResponseToClipboard(resp);
+ },
+ }),
+ m(Button, {
+ minimal: true,
+ label: 'Close',
+ onclick: onClose,
+ }),
];
- if (resp) {
- if (resp.error === undefined) {
- header.push(m(Button, {
- label: 'Copy result (.tsv)',
- minimal: true,
- onclick: () => {
- queryResponseToClipboard(resp);
- },
- }));
- }
- }
- header.push(m(Button, {
- label: 'Close',
- minimal: true,
- onclick: () => vnode.attrs.onClose(),
- }));
+ }
- const headers = [m('header.overview', ...header)];
-
- if (resp === undefined) {
- return headers;
- }
-
- if (resp.statementWithOutputCount > 1) {
- headers.push(
- m('header.overview',
- `${resp.statementWithOutputCount} out of ${resp.statementCount} ` +
- `statements returned a result. Only the results for the last ` +
- `statement are displayed in the table below.`));
- }
-
- return [...headers, m(QueryTableContent, {resp})];
+ renderTableContent(resp: QueryResponse) {
+ return m(
+ '.pf-query-panel',
+ resp.statementWithOutputCount > 1 &&
+ m('.pf-query-warning',
+ m(
+ Callout,
+ {icon: 'warning'},
+ `${resp.statementWithOutputCount} out of ${
+ resp.statementCount} `,
+ 'statements returned a result. ',
+ 'Only the results for the last statement are displayed.',
+ )),
+ m(QueryTableContent, {resp}),
+ );
}
renderCanvas() {}
diff --git a/ui/src/frontend/semantic_icons.ts b/ui/src/frontend/semantic_icons.ts
index a80acca..da6326e 100644
--- a/ui/src/frontend/semantic_icons.ts
+++ b/ui/src/frontend/semantic_icons.ts
@@ -19,4 +19,12 @@
static readonly ContextMenu = 'arrow_drop_down'; // Could be 'more_vert'
static readonly Copy = 'content_copy';
static readonly Delete = 'delete';
+ static readonly SortedAsc = 'arrow_upward';
+ static readonly SortedDesc = 'arrow_downward';
+ static readonly GoBack = 'chevron_left';
+ static readonly GoForward = 'chevron_right';
+ static readonly AddColumn = 'add';
+ static readonly Close = 'close';
+ static readonly Hide = 'visibility_off';
+ static readonly Filter = 'filter_list';
}
diff --git a/ui/src/frontend/sql/args.ts b/ui/src/frontend/sql/args.ts
index a03a8b5..6d76ad4 100644
--- a/ui/src/frontend/sql/args.ts
+++ b/ui/src/frontend/sql/args.ts
@@ -27,7 +27,7 @@
} from '../sql_types';
export type ArgValue = bigint|string|number|boolean|null;
-type ArgValueType = 'int'|'uint'|'string'|'bool'|'real'|'null';
+type ArgValueType = 'int'|'uint'|'pointer'|'string'|'bool'|'real'|'null';
export interface Arg {
id: ArgsId;
@@ -90,6 +90,9 @@
case 'int':
case 'uint':
return value.intValue;
+ case 'pointer':
+ return value.intValue === null ? null :
+ `0x${value.intValue.toString(16)}`;
case 'string':
return value.stringValue;
case 'bool':
diff --git a/ui/src/frontend/sql/slice.ts b/ui/src/frontend/sql/slice.ts
index d29706b..fa9c5d6 100644
--- a/ui/src/frontend/sql/slice.ts
+++ b/ui/src/frontend/sql/slice.ts
@@ -185,10 +185,16 @@
readonly ts: TPTimestamp;
readonly dur: TPDuration;
readonly sqlTrackId: number;
+
+ // Whether clicking on the reference should change the current tab
+ // to "current selection" tab in addition to updating the selection
+ // and changing the viewport. True by default.
+ readonly switchToCurrentSelectionTab?: boolean;
}
export class SliceRef implements m.ClassComponent<SliceRefAttrs> {
view(vnode: m.Vnode<SliceRefAttrs>) {
+ const switchTab = vnode.attrs.switchToCurrentSelectionTab ?? true;
return m(
Anchor,
{
@@ -201,8 +207,10 @@
// Clamp duration to 1 - i.e. for instant events
const dur = BigintMath.max(1n, vnode.attrs.dur);
focusHorizontalRange(vnode.attrs.ts, vnode.attrs.ts + dur);
- globals.makeSelection(Actions.selectChromeSlice(
- {id: vnode.attrs.id, trackId: uiTrackId, table: 'slice'}));
+ globals.makeSelection(
+ Actions.selectChromeSlice(
+ {id: vnode.attrs.id, trackId: uiTrackId, table: 'slice'}),
+ switchTab ? 'current_selection' : null);
},
},
vnode.attrs.name);
diff --git a/ui/src/frontend/sql_table/argument_selector.ts b/ui/src/frontend/sql_table/argument_selector.ts
new file mode 100644
index 0000000..0189244
--- /dev/null
+++ b/ui/src/frontend/sql_table/argument_selector.ts
@@ -0,0 +1,85 @@
+// 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 {EngineProxy} from '../../common/engine';
+import {STR} from '../../common/query_result';
+import {globals} from '../globals';
+import {constraintsToQueryFragment} from '../sql_utils';
+import {FilterableSelect} from '../widgets/select';
+import {Spinner} from '../widgets/spinner';
+
+import {argColumn} from './column';
+import {ArgSetIdColumn} from './table_description';
+
+const MAX_ARGS_TO_DISPLAY = 15;
+
+interface ArgumentSelectorAttrs {
+ engine: EngineProxy;
+ argSetId: ArgSetIdColumn;
+ tableName: string;
+ filters: string[];
+ // List of aliases for existing columns by the table.
+ alreadySelectedColumns: Set<string>;
+ onArgumentSelected: (argument: string) => void;
+}
+
+// A widget which allows the user to select a new argument to display.
+// Dinamically queries Trace Processor to find the relevant set of arg_set_ids
+// and which args are present in these arg sets.
+export class ArgumentSelector implements
+ m.ClassComponent<ArgumentSelectorAttrs> {
+ argList?: string[];
+
+ constructor({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
+ this.load(attrs);
+ }
+
+ private async load(attrs: ArgumentSelectorAttrs) {
+ const queryResult = await attrs.engine.query(`
+ -- Encapsulate the query in a CTE to avoid clashes between filters
+ -- and columns of the 'args' table.
+ WITH arg_sets AS (
+ SELECT DISTINCT ${attrs.argSetId.name} as arg_set_id
+ FROM ${attrs.tableName}
+ ${constraintsToQueryFragment({
+ filters: attrs.filters,
+ })}
+ )
+ SELECT
+ DISTINCT args.key as key
+ FROM arg_sets
+ JOIN args USING (arg_set_id)
+ `);
+ this.argList = [];
+ const it = queryResult.iter({key: STR});
+ for (; it.valid(); it.next()) {
+ const arg = argColumn(attrs.argSetId, it.key);
+ if (attrs.alreadySelectedColumns.has(arg.alias)) continue;
+ this.argList.push(it.key);
+ }
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ view({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
+ if (this.argList === undefined) return m(Spinner);
+ return m(FilterableSelect, {
+ values: this.argList,
+ onSelected: (value: string) => attrs.onArgumentSelected(value),
+ maxDisplayedItems: MAX_ARGS_TO_DISPLAY,
+ autofocusInput: true,
+ });
+ }
+}
diff --git a/ui/src/frontend/sql_table/column.ts b/ui/src/frontend/sql_table/column.ts
new file mode 100644
index 0000000..e94da6f
--- /dev/null
+++ b/ui/src/frontend/sql_table/column.ts
@@ -0,0 +1,66 @@
+// 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 {sqliteString} from '../../base/string_utils';
+
+import {
+ ArgSetIdColumn,
+ dependendentColumns,
+ DisplayConfig,
+ RegularSqlTableColumn,
+} from './table_description';
+
+// This file contains the defintions of different column types that can be
+// displayed in the table viewer.
+
+export interface Column {
+ // SQL expression calculating the value of this column.
+ expression: string;
+ // Unique name for this column.
+ // The relevant bit of SQL fetching this column will be ${expression} as
+ // ${alias}.
+ alias: string;
+ // Title to be displayed in the table header.
+ title: string;
+ // How the value of this column should be rendered.
+ display?: DisplayConfig;
+}
+
+export function columnFromSqlTableColumn(c: RegularSqlTableColumn): Column {
+ return {
+ expression: c.name,
+ alias: c.name,
+ title: c.title || c.name,
+ display: c.display,
+ };
+}
+
+export function argColumn(c: ArgSetIdColumn, argName: string): Column {
+ const escape = (name: string) => name.replace(/\.|\[|\]/g, '_');
+ return {
+ expression: `extract_arg(${c.name}, ${sqliteString(argName)})`,
+ alias: `_arg_${c.name}_${escape(argName)}`,
+ title: `${c.title ?? c.name} ${argName}`,
+ };
+}
+
+// Returns a list of projections (i.e. parts of the SELECT clause) that should
+// be added to the query fetching the data to be able to display the given
+// column (e.g. `foo` or `f(bar) as baz`).
+// Some table columns are backed by multiple SQL columns (e.g. slice_id is
+// backed by id, ts, dur and track_id), so we need to return a list.
+export function sqlProjectionsForColumn(column: Column): string[] {
+ return [`${column.expression} as ${column.alias}`].concat(
+ dependendentColumns(column.display).map((c) => `${c} as ${c}`));
+}
diff --git a/ui/src/frontend/sql_table/render_cell.ts b/ui/src/frontend/sql_table/render_cell.ts
new file mode 100644
index 0000000..b3d0a4b
--- /dev/null
+++ b/ui/src/frontend/sql_table/render_cell.ts
@@ -0,0 +1,214 @@
+// 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 {sqliteString} from '../../base/string_utils';
+import {Row, SqlValue} from '../../common/query_result';
+import {formatDuration, TPTime} from '../../common/time';
+import {Anchor} from '../anchor';
+import {copyToClipboard} from '../clipboard';
+import {Icons} from '../semantic_icons';
+import {SliceRef} from '../sql/slice';
+import {asSliceSqlId, asTPTimestamp} from '../sql_types';
+import {sqlValueToString} from '../sql_utils';
+import {Err} from '../widgets/error';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+import {Timestamp} from '../widgets/timestamp';
+
+import {Column} from './column';
+import {SqlTableState} from './state';
+import {SliceIdDisplayConfig} from './table_description';
+
+// This file is responsible for rendering a value in a given sell based on the
+// column type.
+
+function filterOptionMenuItem(
+ label: string, filter: string, state: SqlTableState): m.Child {
+ return m(MenuItem, {
+ label,
+ onclick: () => {
+ state.addFilter(filter);
+ },
+ });
+}
+
+function getStandardFilters(
+ c: Column, value: SqlValue, state: SqlTableState): m.Child[] {
+ if (value === null) {
+ return [
+ filterOptionMenuItem('is null', `${c.expression} is null`, state),
+ filterOptionMenuItem('is not null', `${c.expression} is not null`, state),
+ ];
+ }
+ if (typeof value === 'string') {
+ return [
+ filterOptionMenuItem(
+ 'equals to', `${c.expression} = ${sqliteString(value)}`, state),
+ filterOptionMenuItem(
+ 'not equals to', `${c.expression} != ${sqliteString(value)}`, state),
+ ];
+ }
+ if (typeof value === 'bigint' || typeof value === 'number') {
+ return [
+ filterOptionMenuItem('equals to', `${c.expression} = ${value}`, state),
+ filterOptionMenuItem(
+ 'not equals to', `${c.expression} != ${value}`, state),
+ filterOptionMenuItem('greater than', `${c.expression} > ${value}`, state),
+ filterOptionMenuItem(
+ 'greater or equals than', `${c.expression} >= ${value}`, state),
+ filterOptionMenuItem('less than', `${c.expression} < ${value}`, state),
+ filterOptionMenuItem(
+ 'less or equals than', `${c.expression} <= ${value}`, state),
+ ];
+ }
+ return [];
+}
+
+function displayValue(value: SqlValue): m.Child {
+ if (value === null) {
+ return m('i', 'NULL');
+ }
+ return sqlValueToString(value);
+}
+
+function displayDuration(value: TPTime): string;
+function displayDuration(value: SqlValue): m.Children;
+function displayDuration(value: SqlValue): m.Children {
+ if (typeof value !== 'bigint') return displayValue(value);
+ return formatDuration(value);
+}
+
+function display(column: Column, row: Row): m.Children {
+ const value = row[column.alias];
+
+ // Handle all cases when we have non-trivial formatting.
+ switch (column.display?.type) {
+ case 'duration':
+ case 'thread_duration':
+ return displayDuration(value);
+ }
+
+ return displayValue(value);
+}
+
+function copyMenuItem(label: string, value: string): m.Child {
+ return m(MenuItem, {
+ icon: Icons.Copy,
+ label,
+ onclick: () => {
+ copyToClipboard(value);
+ },
+ });
+}
+
+function getContextMenuItems(
+ column: Column, row: Row, state: SqlTableState): m.Child[] {
+ const result: m.Child[] = [];
+ const value = row[column.alias];
+
+ if ((column.display?.type === 'duration' ||
+ column.display?.type === 'thread_duration') &&
+ typeof value === 'bigint') {
+ result.push(copyMenuItem('Copy raw duration', `${value}`));
+ result.push(
+ copyMenuItem('Copy formatted duration', displayDuration(value)));
+ }
+ if (typeof value === 'string') {
+ result.push(copyMenuItem('Copy', value));
+ }
+
+ const filters = getStandardFilters(column, value, state);
+ if (filters.length > 0) {
+ result.push(
+ m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters));
+ }
+
+ return result;
+}
+
+function renderStandardColumn(
+ column: Column, row: Row, state: SqlTableState): m.Children {
+ const displayValue = display(column, row);
+ const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
+ return m(
+ PopupMenu2,
+ {
+ trigger: m(Anchor, displayValue),
+ },
+ ...contextMenuItems,
+ );
+}
+
+function renderTimestampColumn(
+ column: Column, row: Row, state: SqlTableState): m.Children {
+ const value = row[column.alias];
+ if (typeof value !== 'bigint') {
+ return renderStandardColumn(column, row, state);
+ }
+
+ return m(Timestamp, {
+ ts: asTPTimestamp(value),
+ extraMenuItems: getContextMenuItems(column, row, state),
+ });
+}
+
+function renderSliceIdColumn(
+ column: {alias: string, display: SliceIdDisplayConfig},
+ row: Row): m.Children {
+ const config = column.display;
+ const id = row[column.alias];
+ const ts = row[config.ts];
+ const dur = row[config.dur] === null ? -1n : row[config.dur];
+ const trackId = row[config.trackId];
+
+ const columnNotFoundError = (type: string, name: string) =>
+ m(Err, `${type} column ${name} not found`);
+ const wrongTypeError = (type: string, name: string, value: SqlValue) =>
+ m(Err,
+ `Wrong type for ${type} column ${name}: bigint expected, ${
+ typeof value} found`);
+
+ if (typeof id !== 'bigint') return sqlValueToString(id);
+ if (ts === undefined) return columnNotFoundError('Timestamp', config.ts);
+ if (typeof ts !== 'bigint') return wrongTypeError('timestamp', config.ts, ts);
+ if (dur === undefined) return columnNotFoundError('Duration', config.dur);
+ if (typeof dur !== 'bigint') {
+ return wrongTypeError('duration', config.dur, ts);
+ }
+ if (trackId === undefined) return columnNotFoundError('Track id', trackId);
+ if (typeof trackId !== 'bigint') {
+ return wrongTypeError('track id', config.trackId, trackId);
+ }
+
+ return m(SliceRef, {
+ id: asSliceSqlId(Number(id)),
+ name: `${id}`,
+ ts: asTPTimestamp(ts as bigint),
+ dur: dur,
+ sqlTrackId: Number(trackId),
+ switchToCurrentSelectionTab: false,
+ });
+}
+
+export function renderCell(
+ column: Column, row: Row, state: SqlTableState): m.Children {
+ if (column.display && column.display.type === 'slice_id') {
+ return renderSliceIdColumn(
+ {alias: column.alias, display: column.display}, row);
+ } else if (column.display && column.display.type === 'timestamp') {
+ return renderTimestampColumn(column, row, state);
+ }
+ return renderStandardColumn(column, row, state);
+}
diff --git a/ui/src/frontend/sql_table/state.ts b/ui/src/frontend/sql_table/state.ts
new file mode 100644
index 0000000..409d409
--- /dev/null
+++ b/ui/src/frontend/sql_table/state.ts
@@ -0,0 +1,296 @@
+// 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 {arrayEquals} from '../../base/array_utils';
+import {SortDirection} from '../../base/comparison_utils';
+import {EngineProxy} from '../../common/engine';
+import {NUM, Row} from '../../common/query_result';
+import {globals} from '../globals';
+import {constraintsToQueryFragment} from '../sql_utils';
+
+import {
+ Column,
+ columnFromSqlTableColumn,
+ sqlProjectionsForColumn,
+} from './column';
+import {SqlTableDescription, startsHidden} from './table_description';
+
+interface ColumnOrderClause {
+ // We only allow the table to be sorted by the columns which are displayed to
+ // the user to avoid confusion, so we use a reference to the underlying Column
+ // here and compare it by reference down the line.
+ column: Column;
+ direction: SortDirection;
+}
+
+const ROW_LIMIT = 100;
+
+// Result of the execution of the query.
+interface Data {
+ // Rows to show, including pagination.
+ rows: Row[];
+ error?: string;
+}
+
+interface RowCount {
+ // Total number of rows in view, excluding the pagination.
+ // Undefined if the query returned an error.
+ count: number;
+ // Filters which were used to compute this row count.
+ // We need to recompute the totalRowCount only when filters change and not
+ // when the set of columns / order by changes.
+ filters: string[];
+}
+
+export class SqlTableState {
+ private readonly engine_: EngineProxy;
+ private readonly table_: SqlTableDescription;
+
+ get engine() {
+ return this.engine_;
+ }
+ get table() {
+ return this.table_;
+ }
+
+ private filters: string[];
+ private columns: Column[];
+ private orderBy: ColumnOrderClause[];
+ private offset = 0;
+ private data?: Data;
+ private rowCount?: RowCount;
+
+ constructor(
+ engine: EngineProxy, table: SqlTableDescription, filters?: string[]) {
+ this.engine_ = engine;
+ this.table_ = table;
+
+ this.filters = filters || [];
+ this.columns = [];
+ for (const column of this.table.columns) {
+ if (startsHidden(column)) continue;
+ this.columns.push(columnFromSqlTableColumn(column));
+ }
+ this.orderBy = [];
+
+ this.reload();
+ }
+
+ // Compute the actual columns to fetch.
+ private getSQLProjections(): string[] {
+ const result = new Set<string>();
+ for (const column of this.columns) {
+ for (const p of sqlProjectionsForColumn(column)) {
+ result.add(p);
+ }
+ }
+ return Array.from(result);
+ }
+
+ private getSQLImports() {
+ return (this.table.imports || [])
+ .map((i) => `SELECT IMPORT("${i}");`)
+ .join('\n');
+ }
+
+ private getCountRowsSQLQuery(): string {
+ return `
+ ${this.getSQLImports()}
+
+ SELECT
+ COUNT() AS count
+ FROM ${this.table.name}
+ ${constraintsToQueryFragment({
+ filters: this.filters,
+ })}
+ `;
+ }
+
+ getNonPaginatedSQLQuery(): string {
+ const orderBy = this.orderBy.map((c) => ({
+ fieldName: c.column.alias,
+ direction: c.direction,
+ }));
+ return `
+ ${this.getSQLImports()}
+
+ SELECT
+ ${this.getSQLProjections().join(',\n')}
+ FROM ${this.table.name}
+ ${constraintsToQueryFragment({
+ filters: this.filters,
+ orderBy: orderBy,
+ })}
+ `;
+ }
+
+ getPaginatedSQLQuery():
+ string { // We fetch one more row to determine if we can go forward.
+ return `
+ ${this.getNonPaginatedSQLQuery()}
+ LIMIT ${ROW_LIMIT + 1}
+ OFFSET ${this.offset}
+ `;
+ }
+
+ canGoForward(): boolean {
+ if (this.data === undefined) return false;
+ return this.data.rows.length > ROW_LIMIT;
+ }
+
+ canGoBack(): boolean {
+ if (this.data === undefined) return false;
+ return this.offset > 0;
+ }
+
+ goForward() {
+ if (!this.canGoForward()) return;
+ this.offset += ROW_LIMIT;
+ this.reload({offset: 'keep'});
+ }
+
+ goBack() {
+ if (!this.canGoBack()) return;
+ this.offset -= ROW_LIMIT;
+ this.reload({offset: 'keep'});
+ }
+
+ getDisplayedRange(): {from: number, to: number}|undefined {
+ if (this.data === undefined) return undefined;
+ return {
+ from: this.offset + 1,
+ to: this.offset + Math.min(this.data.rows.length, ROW_LIMIT),
+ };
+ }
+
+ private async loadRowCount(): Promise<RowCount|undefined> {
+ const filters = Array.from(this.filters);
+ const res = await this.engine.query(this.getCountRowsSQLQuery());
+ if (res.error() !== undefined) return undefined;
+ return {
+ count: res.firstRow({count: NUM}).count,
+ filters: filters,
+ };
+ }
+
+ private async loadData(): Promise<Data> {
+ const queryRes = await this.engine.query(this.getPaginatedSQLQuery());
+ const rows: Row[] = [];
+ for (const it = queryRes.iter({}); it.valid(); it.next()) {
+ const row: Row = {};
+ for (const column of queryRes.columns()) {
+ row[column] = it.get(column);
+ }
+ rows.push(row);
+ }
+
+ return {
+ rows,
+ error: queryRes.error(),
+ };
+ }
+
+ private async reload(params?: {offset: 'reset'|'keep'}) {
+ if ((params?.offset ?? 'reset') === 'reset') {
+ this.offset = 0;
+ }
+ const updateRowCount = !arrayEquals(this.rowCount?.filters, this.filters);
+ this.data = undefined;
+ if (updateRowCount) {
+ this.rowCount = undefined;
+ }
+
+ // Delay the visual update by 50ms to avoid flickering (if the query returns
+ // before the data is loaded.
+ setTimeout(() => globals.rafScheduler.scheduleFullRedraw(), 50);
+
+ if (updateRowCount) {
+ this.rowCount = await this.loadRowCount();
+ }
+ this.data = await this.loadData();
+
+ globals.rafScheduler.scheduleFullRedraw();
+ }
+
+ getTotalRowCount(): number|undefined {
+ return this.rowCount?.count;
+ }
+
+ getDisplayedRows(): Row[] {
+ return this.data?.rows || [];
+ }
+
+ getQueryError(): string|undefined {
+ return this.data?.error;
+ }
+
+ isLoading() {
+ return this.data === undefined;
+ }
+
+ removeFilter(filter: string) {
+ this.filters.splice(this.filters.indexOf(filter), 1);
+ this.reload();
+ }
+
+ addFilter(filter: string) {
+ this.filters.push(filter);
+ this.reload();
+ }
+
+ getFilters(): string[] {
+ return this.filters;
+ }
+
+ sortBy(clause: ColumnOrderClause) {
+ this.orderBy = this.orderBy || [];
+ // Remove previous sort by the same column.
+ this.orderBy = this.orderBy.filter((c) => c.column !== clause.column);
+ // Add the new sort clause to the front, so we effectively stable-sort the
+ // data currently displayed to the user.
+ this.orderBy.unshift(clause);
+ this.reload();
+ }
+
+ unsort() {
+ this.orderBy = [];
+ this.reload();
+ }
+
+ isSortedBy(column: Column): SortDirection|undefined {
+ if (!this.orderBy) return undefined;
+ if (this.orderBy.length === 0) return undefined;
+ if (this.orderBy[0].column !== column) return undefined;
+ return this.orderBy[0].direction;
+ }
+
+ addColumn(column: Column, index: number) {
+ this.columns.splice(index + 1, 0, column);
+ this.reload({offset: 'keep'});
+ }
+
+ hideColumnAtIndex(index: number) {
+ const column = this.columns[index];
+ this.columns.splice(index, 1);
+ // We can only filter by the visibile columns to avoid confusing the user,
+ // so we remove order by clauses that refer to the hidden column.
+ this.orderBy = this.orderBy.filter((c) => c.column !== column);
+ // TODO(altimin): we can avoid the fetch here if the orderBy hasn't changed.
+ this.reload({offset: 'keep'});
+ }
+
+ getSelectedColumns(): Column[] {
+ return this.columns;
+ }
+};
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
new file mode 100644
index 0000000..bc3318f
--- /dev/null
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -0,0 +1,106 @@
+// 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 {BottomTab, bottomTabRegistry, NewBottomTabArgs} from '../bottom_tab';
+import {copyToClipboard} from '../clipboard';
+import {Icons} from '../semantic_icons';
+import {Button} from '../widgets/button';
+import {DetailsShell} from '../widgets/details_shell';
+import {exists} from '../widgets/utils';
+
+import {SqlTableState} from './state';
+import {SqlTable} from './table';
+import {SqlTableDescription} from './table_description';
+
+interface SqlTableTabConfig {
+ table: SqlTableDescription;
+ displayName?: string;
+ filters?: string[];
+}
+
+export class SqlTableTab extends BottomTab<SqlTableTabConfig> {
+ static readonly kind = 'dev.perfetto.SqlTableTab';
+
+ private state: SqlTableState;
+
+ constructor(args: NewBottomTabArgs) {
+ super(args);
+
+ this.state =
+ new SqlTableState(this.engine, this.config.table, this.config.filters);
+ }
+
+ static create(args: NewBottomTabArgs): SqlTableTab {
+ return new SqlTableTab(args);
+ }
+
+ viewTab() {
+ const range = this.state.getDisplayedRange();
+ const rowCount = this.state.getTotalRowCount();
+ const navigation = [
+ exists(range) && exists(rowCount) &&
+ `Showing rows ${range.from}-${range.to} of ${rowCount}`,
+ m(Button, {
+ icon: Icons.GoBack,
+ disabled: !this.state.canGoBack(),
+ onclick: () => this.state.goBack(),
+ minimal: true,
+ }),
+ m(Button, {
+ icon: Icons.GoForward,
+ disabled: !this.state.canGoForward(),
+ onclick: () => this.state.goForward(),
+ minimal: true,
+ }),
+ ];
+
+ return m(
+ DetailsShell,
+ {
+ title: 'Table',
+ description: this.config.displayName ?? this.config.table.name,
+ buttons: [
+ ...navigation,
+ m(Button, {
+ label: 'Copy SQL query',
+ onclick: () =>
+ copyToClipboard(this.state.getNonPaginatedSQLQuery()),
+ }),
+ m(Button, {
+ label: 'Close',
+ onclick: () => this.close(),
+ }),
+ ],
+ },
+ m(SqlTable, {
+ state: this.state,
+ }));
+ }
+
+ renderTabCanvas() {}
+
+ getTitle(): string {
+ const rowCount = this.state.getTotalRowCount();
+ const rows = rowCount === undefined ? '' : `(${rowCount})`;
+ return `Table ${this.config.displayName ?? this.config.table.name} ${rows}`;
+ }
+
+ isLoading(): boolean {
+ return this.state.isLoading();
+ }
+}
+
+bottomTabRegistry.register(SqlTableTab);
diff --git a/ui/src/frontend/sql_table/table.ts b/ui/src/frontend/sql_table/table.ts
new file mode 100644
index 0000000..a42ad89
--- /dev/null
+++ b/ui/src/frontend/sql_table/table.ts
@@ -0,0 +1,165 @@
+// 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 {EngineProxy} from '../../common/engine';
+import {Row} from '../../common/query_result';
+import {Anchor} from '../anchor';
+import {Icons} from '../semantic_icons';
+import {BasicTable} from '../widgets/basic_table';
+import {Button} from '../widgets/button';
+import {MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
+
+import {ArgumentSelector} from './argument_selector';
+import {argColumn, Column, columnFromSqlTableColumn} from './column';
+import {renderCell} from './render_cell';
+import {SqlTableState} from './state';
+import {isArgSetIdColumn, SqlTableDescription} from './table_description';
+
+export interface SqlTableConfig {
+ readonly state: SqlTableState;
+}
+
+export class SqlTable implements m.ClassComponent<SqlTableConfig> {
+ private readonly table: SqlTableDescription;
+ private readonly engine: EngineProxy;
+
+ private state: SqlTableState;
+
+ constructor(vnode: m.Vnode<SqlTableConfig>) {
+ this.state = vnode.attrs.state;
+ this.table = this.state.table;
+ this.engine = this.state.engine;
+ }
+
+ renderFilters(): m.Children {
+ const filters: m.Child[] = [];
+ for (const filter of this.state.getFilters()) {
+ filters.push(m(Button, {
+ label: filter,
+ icon: 'close',
+ onclick: () => {
+ this.state.removeFilter(filter);
+ },
+ }));
+ }
+ return filters;
+ }
+
+ renderAddColumnOptions(addColumn: (column: Column) => void): m.Children {
+ // We do not want to add columns which already exist, so we track the
+ // columns which we are already showing here.
+ // TODO(altimin): Theoretically a single table can have two different
+ // arg_set_ids, so we should track (arg_set_id_column, arg_name) pairs here.
+ const existingColumns = new Set<string>();
+
+ for (const column of this.state.getSelectedColumns()) {
+ existingColumns.add(column.alias);
+ }
+
+ const result = [];
+ for (const column of this.table.columns) {
+ if (existingColumns.has(column.name)) continue;
+ if (isArgSetIdColumn(column)) {
+ result.push(
+ m(MenuItem,
+ {
+ label: column.name,
+ },
+ m(ArgumentSelector, {
+ engine: this.engine,
+ argSetId: column,
+ tableName: this.table.name,
+ filters: this.state.getFilters(),
+ alreadySelectedColumns: existingColumns,
+ onArgumentSelected: (argument: string) => {
+ addColumn(argColumn(column, argument));
+ },
+ })));
+ continue;
+ }
+ result.push(m(MenuItem, {
+ label: column.name,
+ onclick: () => addColumn(
+ columnFromSqlTableColumn(column),
+ ),
+ }));
+ }
+ return result;
+ }
+
+ renderColumnHeader(column: Column, index: number) {
+ const sorted = this.state.isSortedBy(column);
+ const icon = sorted === 'ASC' ?
+ Icons.SortedAsc :
+ sorted === 'DESC' ? Icons.SortedDesc : Icons.ContextMenu;
+ return m(
+ PopupMenu2,
+ {
+ trigger: m(Anchor, {icon}, column.title),
+ },
+ sorted !== 'DESC' && m(MenuItem, {
+ label: 'Sort: highest first',
+ icon: Icons.SortedDesc,
+ onclick: () => {
+ this.state.sortBy({column, direction: 'DESC'});
+ },
+ }),
+ sorted !== 'ASC' && m(MenuItem, {
+ label: 'Sort: lowest first',
+ icon: Icons.SortedAsc,
+ onclick: () => {
+ this.state.sortBy({column, direction: 'ASC'});
+ },
+ }),
+ sorted !== undefined && m(MenuItem, {
+ label: 'Unsort',
+ icon: Icons.Close,
+ onclick: () => this.state.unsort(),
+ }),
+ this.state.getSelectedColumns().length > 1 && m(MenuItem, {
+ label: 'Hide',
+ icon: Icons.Hide,
+ onclick: () => this.state.hideColumnAtIndex(index),
+ }),
+ m(MenuDivider),
+ m(MenuItem,
+ {label: 'Add column', icon: Icons.AddColumn},
+ this.renderAddColumnOptions((column) => {
+ this.state.addColumn(column, index);
+ })),
+ );
+ }
+
+ view() {
+ const rows = this.state.getDisplayedRows();
+
+ return [
+ m('div', this.renderFilters()),
+ m(BasicTable, {
+ data: rows,
+ columns: this.state.getSelectedColumns().map(
+ (column, i) => ({
+ title: this.renderColumnHeader(column, i),
+ render: (row: Row) => renderCell(column, row, this.state),
+ })),
+ }),
+ this.state.getQueryError() !== undefined &&
+ m('.query-error', this.state.getQueryError()),
+ ];
+ }
+};
+
+export {SqlTableDescription};
diff --git a/ui/src/frontend/sql_table/table_description.ts b/ui/src/frontend/sql_table/table_description.ts
new file mode 100644
index 0000000..1aef50b
--- /dev/null
+++ b/ui/src/frontend/sql_table/table_description.ts
@@ -0,0 +1,96 @@
+// 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.
+
+
+// Definition of the SQL table to be displayed in the SQL table widget,
+// including the semantic definitions of the columns (e.g. timestamp
+// column which requires special formatting). Also note that some of the
+// columns require other columns for advanced display features (e.g. timestamp
+// and duration taken together define "a time range", which can be used for
+// additional filtering.
+
+export type DisplayConfig =
+ SliceIdDisplayConfig|Timestamp|Duration|ThreadDuration;
+
+// Common properties for all columns.
+interface SqlTableColumnBase {
+ // Name of the column in the SQL table.
+ name: string;
+ // Display name of the column in the UI.
+ title?: string;
+}
+
+export interface ArgSetIdColumn extends SqlTableColumnBase {
+ type: 'arg_set_id';
+}
+
+export interface RegularSqlTableColumn extends SqlTableColumnBase {
+ // Special rendering instructions for this column, including the list
+ // of additional columns required for the rendering.
+ display?: DisplayConfig;
+ // Whether the column should be hidden by default.
+ startsHidden?: boolean;
+}
+
+export type SqlTableColumn = RegularSqlTableColumn|ArgSetIdColumn;
+
+export function startsHidden(c: SqlTableColumn): boolean {
+ if (isArgSetIdColumn(c)) return true;
+ return c.startsHidden ?? false;
+}
+
+export function isArgSetIdColumn(c: SqlTableColumn): c is ArgSetIdColumn {
+ return (c as {type?: string}).type === 'arg_set_id';
+}
+
+export interface SqlTableDescription {
+ readonly imports?: string[];
+ name: string;
+ columns: SqlTableColumn[];
+}
+
+// Additional columns needed to display the given column.
+export function dependendentColumns(display?: DisplayConfig): string[] {
+ switch (display?.type) {
+ case 'slice_id':
+ return [display.ts, display.dur, display.trackId];
+ default:
+ return [];
+ }
+}
+
+// Column displaying ids into the `slice` table. Requires the ts, dur and
+// track_id columns to be able to display the value, including the
+// "go-to-slice-on-click" functionality.
+export interface SliceIdDisplayConfig {
+ type: 'slice_id';
+ ts: string;
+ dur: string;
+ trackId: string;
+}
+
+// Column displaying timestamps.
+interface Timestamp {
+ type: 'timestamp';
+}
+
+// Column displaying durations.
+export interface Duration {
+ type: 'duration';
+}
+
+// Column displaying thread durations.
+export interface ThreadDuration {
+ type: 'thread_duration';
+}
diff --git a/ui/src/frontend/sql_table/well_known_tables.ts b/ui/src/frontend/sql_table/well_known_tables.ts
new file mode 100644
index 0000000..0af9690
--- /dev/null
+++ b/ui/src/frontend/sql_table/well_known_tables.ts
@@ -0,0 +1,112 @@
+// 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 {SqlTableDescription} from './table';
+
+const sliceTable: SqlTableDescription = {
+ imports: ['experimental.slices'],
+ name: 'experimental_slice_with_thread_and_process_info',
+ columns: [
+ {
+ name: 'id',
+ title: 'ID',
+ display: {
+ type: 'slice_id',
+ ts: 'ts',
+ dur: 'dur',
+ trackId: 'track_id',
+ },
+ },
+ {
+ name: 'ts',
+ title: 'Timestamp',
+ display: {
+ type: 'timestamp',
+ },
+ },
+ {
+ name: 'dur',
+ title: 'Duration',
+ display: {
+ type: 'duration',
+ },
+ },
+ {
+ name: 'thread_dur',
+ title: 'Thread duration',
+ display: {
+ type: 'thread_duration',
+ },
+ },
+ {
+ name: 'category',
+ title: 'Category',
+ },
+ {
+ name: 'name',
+ title: 'Name',
+ },
+ {
+ name: 'track_id',
+ title: 'Track ID',
+ startsHidden: true,
+ },
+ {
+ name: 'track_name',
+ title: 'Track name',
+ startsHidden: true,
+ },
+ {
+ name: 'thread_name',
+ title: 'Thread name',
+ },
+ {
+ name: 'utid',
+ startsHidden: true,
+ },
+ {
+ name: 'tid',
+ },
+ {
+ name: 'process_name',
+ title: 'Process name',
+ },
+ {
+ name: 'upid',
+ startsHidden: true,
+ },
+ {
+ name: 'pid',
+ },
+ {
+ name: 'depth',
+ title: 'Depth',
+ startsHidden: true,
+ },
+ {
+ name: 'parent_id',
+ title: 'Parent slice ID',
+ startsHidden: true,
+ },
+ {
+ name: 'arg_set_id',
+ title: 'Arg',
+ type: 'arg_set_id',
+ },
+ ],
+};
+
+export class SqlTables {
+ static readonly slice = sliceTable;
+}
diff --git a/ui/src/frontend/sql_utils.ts b/ui/src/frontend/sql_utils.ts
index 2bbcd0e..6ef80f6 100644
--- a/ui/src/frontend/sql_utils.ts
+++ b/ui/src/frontend/sql_utils.ts
@@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {EngineProxy} from '../common/engine';
+import {ColumnType, NUM} from '../common/query_result';
import {SortDirection} from '../common/state';
-import {ColumnType} from '../common/query_result';
-interface OrderClause {
+export interface OrderClause {
fieldName: string;
direction?: SortDirection;
}
@@ -81,7 +82,10 @@
return n;
}
-export function sqlValueToString(val: ColumnType): string {
+export function sqlValueToString(val: ColumnType): string;
+export function sqlValueToString(val?: ColumnType): string|undefined;
+export function sqlValueToString(val?: ColumnType): string|undefined {
+ if (val === undefined) return undefined;
if (val instanceof Uint8Array) {
return `<blob length=${val.length}>`;
}
@@ -90,3 +94,17 @@
}
return val.toString();
}
+
+export async function getTableRowCount(
+ engine: EngineProxy, tableName: string): Promise<number|undefined> {
+ const result =
+ await engine.query(`SELECT COUNT() as count FROM ${tableName}`);
+ if (result.numRows() === 0) {
+ return undefined;
+ }
+ return result
+ .firstRow({
+ count: NUM,
+ })
+ .count;
+}
diff --git a/ui/src/frontend/thread_and_process_info.ts b/ui/src/frontend/thread_and_process_info.ts
index 46ab39e..a255902 100644
--- a/ui/src/frontend/thread_and_process_info.ts
+++ b/ui/src/frontend/thread_and_process_info.ts
@@ -119,3 +119,11 @@
export function getThreadName(info?: ThreadInfo): string|undefined {
return getDisplayName(info?.name, info?.tid);
}
+
+// Return the full thread name, including the process name.
+export function getFullThreadName(info?: ThreadInfo): string|undefined {
+ if (info?.process === undefined) {
+ return getThreadName(info);
+ }
+ return `${getThreadName(info)} ${getProcessName(info.process)}`;
+}
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index a4a71a1..e2355d6 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -22,14 +22,17 @@
TPDuration,
TPTime,
} from '../common/time';
-import {Anchor} from './anchor';
+import {Anchor} from './anchor';
import {globals} from './globals';
import {scrollToTrackAndTs} from './scroll_helper';
+import {Icons} from './semantic_icons';
import {
+ asTPTimestamp,
asUtid,
SchedSqlId,
ThreadStateSqlId,
+ TPTimestamp,
Utid,
} from './sql_types';
import {
@@ -50,7 +53,7 @@
// Id of the corresponding entry in the |sched| table.
schedSqlId?: SchedSqlId;
// Timestamp of the beginning of this thread state in nanoseconds.
- ts: TPTime;
+ ts: TPTimestamp;
// Duration of this thread state in nanoseconds.
dur: TPDuration;
// CPU id if this thread state corresponds to a thread running on the CPU.
@@ -109,7 +112,7 @@
result.push({
threadStateSqlId: it.threadStateSqlId as ThreadStateSqlId,
schedSqlId: fromNumNull(it.schedSqlId) as (SchedSqlId | undefined),
- ts: it.ts,
+ ts: asTPTimestamp(it.ts),
dur: it.dur,
cpu: fromNumNull(it.cpu),
state: translateState(it.state || undefined, ioWait),
@@ -153,7 +156,7 @@
interface ThreadStateRefAttrs {
id: ThreadStateSqlId;
- ts: TPTime;
+ ts: TPTimestamp;
dur: TPDuration;
utid: Utid;
// If not present, a placeholder name will be used.
@@ -165,7 +168,7 @@
return m(
Anchor,
{
- icon: 'open_in_new',
+ icon: Icons.UpdateSelection,
onclick: () => {
let trackId: string|number|undefined;
for (const track of Object.values(globals.state.tracks)) {
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index c4a7295..3d38cc5 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -14,18 +14,25 @@
import m from 'mithril';
-import {TPTime} from '../common/time';
+import {formatDurationShort, TPTime} from '../common/time';
import {Anchor} from './anchor';
import {BottomTab, bottomTabRegistry, NewBottomTabArgs} from './bottom_tab';
import {globals} from './globals';
import {asTPTimestamp, SchedSqlId, ThreadStateSqlId} from './sql_types';
import {
+ getFullThreadName,
getProcessName,
getThreadName,
ThreadInfo,
} from './thread_and_process_info';
-import {getThreadState, goToSchedSlice, ThreadState} from './thread_state';
+import {
+ getThreadState,
+ getThreadStateFromConstraints,
+ goToSchedSlice,
+ ThreadState,
+ ThreadStateRef,
+} from './thread_state';
import {DetailsShell} from './widgets/details_shell';
import {Duration} from './widgets/duration';
import {GridLayout} from './widgets/grid_layout';
@@ -39,10 +46,18 @@
readonly id: ThreadStateSqlId;
}
+interface RelatedThreadStates {
+ prev?: ThreadState;
+ next?: ThreadState;
+ waker?: ThreadState;
+ wakee?: ThreadState[];
+}
+
export class ThreadStateTab extends BottomTab<ThreadStateTabConfig> {
- static readonly kind = 'org.perfetto.ThreadStateTab';
+ static readonly kind = 'dev.perfetto.ThreadStateTab';
state?: ThreadState;
+ relatedStates?: RelatedThreadStates;
loaded: boolean = false;
static create(args: NewBottomTabArgs): ThreadStateTab {
@@ -52,13 +67,55 @@
constructor(args: NewBottomTabArgs) {
super(args);
- getThreadState(this.engine, this.config.id).then((state?: ThreadState) => {
+ this.load().then(() => {
this.loaded = true;
- this.state = state;
globals.rafScheduler.scheduleFullRedraw();
});
}
+ async load() {
+ this.state = await getThreadState(this.engine, this.config.id);
+
+ if (!this.state) {
+ return;
+ }
+
+ const relatedStates: RelatedThreadStates = {};
+ relatedStates.prev = (await getThreadStateFromConstraints(this.engine, {
+ filters: [
+ `ts + dur = ${this.state.ts}`,
+ `utid = ${this.state.thread?.utid}`,
+ ],
+ limit: 1,
+ }))[0];
+ relatedStates.next = (await getThreadStateFromConstraints(this.engine, {
+ filters: [
+ `ts = ${this.state.ts + this.state.dur}`,
+ `utid = ${this.state.thread?.utid}`,
+ ],
+ limit: 1,
+ }))[0];
+ if (this.state.wakerThread?.utid !== undefined) {
+ relatedStates.waker = (await getThreadStateFromConstraints(this.engine, {
+ filters: [
+ `utid = ${this.state.wakerThread?.utid}`,
+ `ts <= ${this.state.ts}`,
+ `ts + dur >= ${this.state.ts}`,
+ ],
+ }))[0];
+ }
+ relatedStates.wakee = await getThreadStateFromConstraints(this.engine, {
+ filters: [
+ `waker_utid = ${this.state.thread?.utid}`,
+ `state = 'R'`,
+ `ts >= ${this.state.ts}`,
+ `ts <= ${this.state.ts + this.state.dur}`,
+ ],
+ });
+
+ this.relatedStates = relatedStates;
+ }
+
getTitle() {
// TODO(altimin): Support dynamic titles here.
return 'Current Selection';
@@ -75,7 +132,10 @@
Section,
{title: 'Details'},
this.state && this.renderTree(this.state),
- )),
+ ),
+ m(Section,
+ {title: 'Related thread states'},
+ this.renderRelatedThreadStates())),
);
}
@@ -154,8 +214,63 @@
);
}
+ private renderRelatedThreadStates(): m.Children {
+ if (this.state === undefined || this.relatedStates === undefined) {
+ return 'Loading';
+ }
+ const startTs = this.state.ts;
+ const renderRef = (state: ThreadState, name?: string) => m(ThreadStateRef, {
+ id: state.threadStateSqlId,
+ ts: state.ts,
+ dur: state.dur,
+ utid: state.thread!.utid,
+ name,
+ });
+
+ const nameForNextOrPrev = (state: ThreadState) =>
+ `${state.state} for ${formatDurationShort(state.dur)}`;
+ return m(
+ Tree,
+ this.relatedStates.waker && m(TreeNode, {
+ left: 'Waker',
+ right: renderRef(
+ this.relatedStates.waker,
+ getFullThreadName(this.relatedStates.waker.thread)),
+ }),
+ this.relatedStates.prev && m(TreeNode, {
+ left: 'Previous state',
+ right: renderRef(
+ this.relatedStates.prev,
+ nameForNextOrPrev(this.relatedStates.prev)),
+ }),
+ this.relatedStates.next && m(TreeNode, {
+ left: 'Next state',
+ right: renderRef(
+ this.relatedStates.next,
+ nameForNextOrPrev(this.relatedStates.next)),
+ }),
+ this.relatedStates.wakee && this.relatedStates.wakee.length > 0 &&
+ m(TreeNode,
+ {
+ left: 'Woken threads',
+ },
+ this.relatedStates.wakee.map(
+ (state) =>
+ m(TreeNode, ({
+ left: m(Timestamp, {
+ ts: state.ts,
+ display: `Start+${
+ formatDurationShort(state.ts - startTs)}`,
+ }),
+ right:
+ renderRef(state, getFullThreadName(state.thread)),
+ })))),
+ );
+ }
+
+
isLoading() {
- return this.state === undefined;
+ return this.state === undefined || this.relatedStates === undefined;
}
renderTabCanvas(): void {}
diff --git a/ui/src/frontend/widgets/basic_table.ts b/ui/src/frontend/widgets/basic_table.ts
new file mode 100644
index 0000000..578b9d7
--- /dev/null
+++ b/ui/src/frontend/widgets/basic_table.ts
@@ -0,0 +1,56 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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';
+
+export interface ColumnDescriptor<T> {
+ title: m.Children;
+ render: (row: T) => m.Children;
+}
+
+export interface TableAttrs<T> {
+ data: T[];
+ columns: ColumnDescriptor<T>[];
+}
+
+export class BasicTable implements m.ClassComponent<TableAttrs<any>> {
+ renderColumnHeader(
+ _vnode: m.Vnode<TableAttrs<any>>,
+ column: ColumnDescriptor<any>): m.Children {
+ return m('td', column.title);
+ }
+
+ view(vnode: m.Vnode<TableAttrs<any>>): m.Child {
+ const attrs = vnode.attrs;
+
+ return m(
+ 'table.generic-table',
+ {
+ // TODO(altimin, stevegolton): this should be the default for
+ // generic-table, but currently it is overriden by
+ // .pf-details-shell .pf-content table, so specify this here for now.
+ style: {
+ 'table-layout': 'auto',
+ },
+ },
+ m('thead',
+ m('tr.header',
+ attrs.columns.map(
+ (column) => this.renderColumnHeader(vnode, column)))),
+ attrs.data.map(
+ (row) =>
+ m('tr',
+ attrs.columns.map((column) => m('td', column.render(row))))));
+ }
+}
diff --git a/ui/src/frontend/widgets/callout.ts b/ui/src/frontend/widgets/callout.ts
new file mode 100644
index 0000000..cd583af
--- /dev/null
+++ b/ui/src/frontend/widgets/callout.ts
@@ -0,0 +1,36 @@
+// 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 {Icon} from './icon';
+
+interface CalloutAttrs {
+ icon?: string;
+ // Remaining attributes forwarded to the underlying HTML <button>.
+ [htmlAttrs: string]: any;
+}
+
+export class Callout implements m.ClassComponent<CalloutAttrs> {
+ view({attrs, children}: m.CVnode<CalloutAttrs>) {
+ const {icon, ...htmlAttrs} = attrs;
+
+ return m(
+ '.pf-callout',
+ {...htmlAttrs},
+ icon && m(Icon, {className: 'pf-left-icon', icon}),
+ children,
+ );
+ }
+}
diff --git a/ui/src/frontend/widgets/details_shell.ts b/ui/src/frontend/widgets/details_shell.ts
index 7fe684b..8b6aa41 100644
--- a/ui/src/frontend/widgets/details_shell.ts
+++ b/ui/src/frontend/widgets/details_shell.ts
@@ -19,10 +19,8 @@
title: m.Children;
description?: m.Children;
buttons?: m.Children;
- // If true, this container will fill the parent, and content scrolling is
- // expected to be handled internally.
- // Defaults to false.
- matchParent?: boolean;
+ // Stretch/shrink the content to fill the parent vertically.
+ fillParent?: boolean;
}
// A shell for details panels to be more visually consistent.
@@ -33,12 +31,12 @@
title,
description,
buttons,
- matchParent,
+ fillParent = true,
} = attrs;
return m(
'section.pf-details-shell',
- {class: classNames(matchParent && 'pf-match-parent')},
+ {class: classNames(fillParent && 'pf-fill-parent')},
m(
'header.pf-header-bar',
m('h1.pf-header-title', title),
diff --git a/ui/src/frontend/widgets/error.ts b/ui/src/frontend/widgets/error.ts
new file mode 100644
index 0000000..17c91df
--- /dev/null
+++ b/ui/src/frontend/widgets/error.ts
@@ -0,0 +1,21 @@
+// 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';
+
+export class Err implements m.Component {
+ view(vnode: m.Vnode) {
+ return m('.pf-error', vnode.children);
+ }
+}
diff --git a/ui/src/frontend/widgets/select.ts b/ui/src/frontend/widgets/select.ts
index 7fdbdd4..2ec2bc2 100644
--- a/ui/src/frontend/widgets/select.ts
+++ b/ui/src/frontend/widgets/select.ts
@@ -14,8 +14,16 @@
import m from 'mithril';
+import {globals} from '../globals';
+
+import {Menu, MenuItem} from './menu';
+import {TextInput} from './text_input';
+import {exists} from './utils';
+
export interface SelectAttrs {
disabled?: boolean;
+ // Whether to show a search box. Defaults to false.
+ filterable?: boolean;
[htmlAttrs: string]: any;
}
@@ -29,3 +37,50 @@
children);
}
}
+
+export interface FilterableSelectAttrs extends SelectAttrs {
+ values: string[];
+ onSelected: (value: string) => void;
+ maxDisplayedItems?: number;
+ autofocusInput?: boolean;
+}
+
+export class FilterableSelect implements
+ m.ClassComponent<FilterableSelectAttrs> {
+ searchText = '';
+
+ view({attrs}: m.CVnode<FilterableSelectAttrs>) {
+ const filteredValues = attrs.values.filter((name) => {
+ return name.toLowerCase().includes(this.searchText.toLowerCase());
+ });
+
+ const extraItems = exists(attrs.maxDisplayedItems) &&
+ Math.max(0, filteredValues.length - attrs.maxDisplayedItems);
+
+ // TODO(altimin): when the user presses enter and there is only one item,
+ // select the first one.
+ // MAYBE(altimin): when the user presses enter and there are multiple items,
+ // select the first one.
+ return m(
+ 'div',
+ m('.pf-search-bar',
+ m(TextInput, {
+ autofocus: attrs.autofocusInput,
+ oninput: (event: Event) => {
+ const eventTarget = event.target as HTMLTextAreaElement;
+ this.searchText = eventTarget.value;
+ globals.rafScheduler.scheduleFullRedraw();
+ },
+ value: this.searchText,
+ placeholder: 'Filter options...',
+ extraClasses: 'pf-search-box',
+ }),
+ m(Menu,
+ ...filteredValues.map(
+ (value) => m(MenuItem, {
+ label: value,
+ onclick: () => attrs.onSelected(value),
+ }),
+ extraItems && m('i', `+${extraItems} more`)))));
+ }
+}
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
index b945489..91b9b82 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -14,12 +14,14 @@
import m from 'mithril';
+import {Actions} from '../../common/actions';
import {Timecode, toDomainTime} from '../../common/time';
+import {Anchor} from '../anchor';
import {copyToClipboard} from '../clipboard';
+import {globals} from '../globals';
import {Icons} from '../semantic_icons';
import {TPTimestamp} from '../sql_types';
-import {Button} from './button';
import {MenuItem, PopupMenu2} from './menu';
// import {MenuItem, PopupMenu2} from './menu';
@@ -28,43 +30,51 @@
// The timestamp to print, this should be the absolute, raw timestamp as
// found in trace processor.
ts: TPTimestamp;
+ // Custom text value to show instead of the default HH:MM:SS.mmm uuu nnn
+ // formatting.
+ display?: m.Children;
+ extraMenuItems?: m.Child[];
}
export class Timestamp implements m.ClassComponent<TimestampAttrs> {
view({attrs}: m.Vnode<TimestampAttrs>) {
const {ts} = attrs;
return m(
- 'span.pf-timecode',
- renderTimecode(ts),
- m(
- PopupMenu2,
- {
- trigger: m(Button, {
- icon: Icons.ContextMenu,
- compact: true,
- minimal: true,
- }),
- },
- m(MenuItem, {
- icon: Icons.Copy,
- label: `Copy raw value`,
- onclick: () => {
- copyToClipboard(ts.toString());
+ PopupMenu2,
+ {
+ trigger: m(
+ Anchor,
+ {
+ onmouseover: () => {
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts}));
+ },
+ onmouseout: () => {
+ globals.dispatch(Actions.setHoverCursorTimestamp({ts: -1n}));
+ },
},
- }),
- ),
+ attrs.display ?? renderTimecode(ts)),
+ },
+ m(MenuItem, {
+ icon: Icons.Copy,
+ label: `Copy raw value`,
+ onclick: () => {
+ copyToClipboard(ts.toString());
+ },
+ }),
+ ...(attrs.extraMenuItems ?? []),
);
}
}
-function renderTimecode(ts: TPTimestamp): m.Children {
+export function renderTimecode(ts: TPTimestamp): m.Children {
const relTime = toDomainTime(ts);
const {dhhmmss, millis, micros, nanos} = new Timecode(relTime);
- return [
- m('span.pf-timecode-hms', dhhmmss),
- '.',
- m('span.pf-timecode-millis', millis),
- m('span.pf-timecode-micros', micros),
- m('span.pf-timecode-nanos', nanos),
- ];
+ return m(
+ 'span.pf-timecode',
+ m('span.pf-timecode-hms', dhhmmss),
+ '.',
+ m('span.pf-timecode-millis', millis),
+ m('span.pf-timecode-micros', micros),
+ m('span.pf-timecode-nanos', nanos),
+ );
}
diff --git a/ui/src/frontend/widgets/virtual_scroll_container.ts b/ui/src/frontend/widgets/virtual_scroll_container.ts
new file mode 100644
index 0000000..2e210ff
--- /dev/null
+++ b/ui/src/frontend/widgets/virtual_scroll_container.ts
@@ -0,0 +1,51 @@
+// 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 './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/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index e3fc2e3..ea0e632 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -23,6 +23,7 @@
import {Icons} from './semantic_icons';
import {TableShowcase} from './tables/table_showcase';
import {Button} from './widgets/button';
+import {Callout} from './widgets/callout';
import {Checkbox} from './widgets/checkbox';
import {EmptyState} from './widgets/empty_state';
import {Form, FormButtonBar, FormLabel} from './widgets/form';
@@ -649,6 +650,20 @@
}),
),
}),
+ m('h2', 'Callout'),
+ m(
+ WidgetShowcase, {
+ renderWidget: () => m(
+ Callout,
+ {
+ icon: 'info',
+ },
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' +
+ 'Nulla rhoncus tempor neque, sed malesuada eros dapibus vel. ' +
+ 'Aliquam in ligula vitae tortor porttitor laoreet iaculis ' +
+ 'finibus est.',
+ ),
+ }),
);
},
});
diff --git a/ui/src/tracks/debug/details_tab.ts b/ui/src/tracks/debug/details_tab.ts
index 1a743fb..0451cbe 100644
--- a/ui/src/tracks/debug/details_tab.ts
+++ b/ui/src/tracks/debug/details_tab.ts
@@ -87,7 +87,7 @@
export class DebugSliceDetailsTab extends
BottomTab<DebugSliceDetailsTabConfig> {
- static readonly kind = 'org.perfetto.DebugSliceDetailsTab';
+ static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
data?: {
name: string,