Merge changes Iaf5a8954,I8d879833
* changes:
track_event_data_source: Make every TraceForCategoryBody NO_INLINE
track_event_data_source: Preserve size of arguments
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..204f113 100644
--- a/src/trace_processor/db/overlays/null_overlay.cc
+++ b/src/trace_processor/db/overlays/null_overlay.cc
@@ -28,7 +28,7 @@
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);
}
TableBitVector NullOverlay::MapToTableBitVector(StorageBitVector s_bv,
diff --git a/src/trace_processor/db/overlays/null_overlay_unittest.cc b/src/trace_processor/db/overlays/null_overlay_unittest.cc
index 0c02ad7..5782c3e 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,7 +34,7 @@
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);
diff --git a/src/trace_processor/db/overlays/selector_overlay.h b/src/trace_processor/db/overlays/selector_overlay.h
index 1cad92c..73499b2 100644
--- a/src/trace_processor/db/overlays/selector_overlay.h
+++ b/src/trace_processor/db/overlays/selector_overlay.h
@@ -27,7 +27,7 @@
// 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;
@@ -44,7 +44,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..ccc6980 100644
--- a/src/trace_processor/db/overlays/selector_overlay_unittest.cc
+++ b/src/trace_processor/db/overlays/selector_overlay_unittest.cc
@@ -25,7 +25,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,7 +34,7 @@
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);
diff --git a/src/trace_processor/db/overlays/types.h b/src/trace_processor/db/overlays/types.h
index 7978ada..0ea50c5 100644
--- a/src/trace_processor/db/overlays/types.h
+++ b/src/trace_processor/db/overlays/types.h
@@ -26,11 +26,17 @@
// A range of indices in the table space.
struct TableRange {
+ TableRange(uint32_t start, uint32_t end) : range(start, end) {}
+ explicit TableRange(RowMap::Range r) : range(r) {}
+
RowMap::Range range;
};
// A range of indices in the storage space.
struct StorageRange {
+ StorageRange(uint32_t start, uint32_t end) : range(start, end) {}
+ explicit StorageRange(RowMap::Range r) : range(r) {}
+
RowMap::Range range;
};
diff --git a/src/trace_processor/db/query_executor.cc b/src/trace_processor/db/query_executor.cc
index 46cbd29..923e321 100644
--- a/src/trace_processor/db/query_executor.cc
+++ b/src/trace_processor/db/query_executor.cc
@@ -23,6 +23,7 @@
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/status_or.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/query_executor.h"
#include "src/trace_processor/db/storage/numeric_storage.h"
@@ -157,13 +158,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.
@@ -252,10 +253,10 @@
use_legacy = use_legacy || (overlays::FilterOpToOverlayOp(c.op) ==
overlays::OverlayOp::kOther &&
col.type() != c.value.type);
- 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();
+ use_legacy =
+ use_legacy || (col.overlay().size() != col.storage_base().size() &&
+ !col.overlay().row_map().IsBitVector());
if (use_legacy) {
col.FilterInto(c.op, c.value, &rm);
continue;
@@ -265,9 +266,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};
+
+ 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_benchmark.cc b/src/trace_processor/db/query_executor_benchmark.cc
index 55a7251..9024a50 100644
--- a/src/trace_processor/db/query_executor_benchmark.cc
+++ b/src/trace_processor/db/query_executor_benchmark.cc
@@ -20,6 +20,8 @@
#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"
namespace perfetto {
@@ -27,6 +29,24 @@
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 +71,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 +83,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 +200,66 @@
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_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..fa914c0 100644
--- a/src/trace_processor/db/query_executor_unittest.cc
+++ b/src/trace_processor/db/query_executor_unittest.cc
@@ -224,61 +224,63 @@
}
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}.
- 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::kIsNull, SqlValue::Long(0)};
- QueryExecutor exec({col}, 9);
+ QueryExecutor exec({col}, 6);
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.size(), 2u);
+ ASSERT_EQ(res.Get(0), 1u);
+ ASSERT_EQ(res.Get(1), 3u);
}
} // namespace
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/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 08c2ac6..94a9c0e 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
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 640b30e..95ed422 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -26,20 +26,21 @@
@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";
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/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/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/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..54bfb5d 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});
}
@@ -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/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/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/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..a57a85b
--- /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..4fdc99b
--- /dev/null
+++ b/ui/src/frontend/sql_table/render_cell.ts
@@ -0,0 +1,206 @@
+// 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 {renderTimecode} 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 displayTimestamp(value: SqlValue): m.Children {
+ if (typeof value !== 'bigint') return displayValue(value);
+ return renderTimecode(asTPTimestamp(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 'timestamp':
+ return displayTimestamp(value);
+ 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 === 'timestamp' && typeof value === 'bigint') {
+ result.push(copyMenuItem('Copy raw timestamp', `${value}`));
+ // result.push(
+ // copyMenuItem('Copy formatted timestamp', displayTimestamp(value)));
+ }
+ 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 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);
+ }
+ const displayValue = display(column, row);
+ const contextMenuItems: m.Child[] = getContextMenuItems(column, row, state);
+ return m(
+ PopupMenu2,
+ {
+ trigger: m(Anchor, displayValue),
+ },
+ ...contextMenuItems,
+ );
+}
diff --git a/ui/src/frontend/sql_table/state.ts b/ui/src/frontend/sql_table/state.ts
new file mode 100644
index 0000000..81fd2e7
--- /dev/null
+++ b/ui/src/frontend/sql_table/state.ts
@@ -0,0 +1,295 @@
+// 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} 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 (column.startsHidden) 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..177ce43
--- /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 = 'org.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..b3fa312
--- /dev/null
+++ b/ui/src/frontend/sql_table/table_description.ts
@@ -0,0 +1,97 @@
+// 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|ArgSetId;
+
+// 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;
+ // Whether the column should be hidden by default.
+ startsHidden?: boolean;
+}
+
+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;
+}
+
+export type SqlTableColumn = RegularSqlTableColumn|ArgSetIdColumn;
+
+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';
+}
+
+// Column corresponding to an arg_set_id. Will never be directly displayed,
+// but will allow the user select an argument to display from the arg_set.
+export interface ArgSetId {
+ type: 'arg_set_id';
+}
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..4b503a4
--- /dev/null
+++ b/ui/src/frontend/sql_table/well_known_tables.ts
@@ -0,0 +1,114 @@
+// 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',
+ display: {
+ 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/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/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..a138f17 100644
--- a/ui/src/frontend/widgets/timestamp.ts
+++ b/ui/src/frontend/widgets/timestamp.ts
@@ -57,7 +57,7 @@
}
}
-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 [