Merge "Revert "tp: improve handling of !usable constraints in BestIndex"" into main
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..a35fb26
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+ui
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
index ee7f589..c46b914 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1318,8 +1318,8 @@
}
// GN: [//protos/perfetto/config:source_set]
-java_library {
- name: "perfetto_config_java_protos",
+filegroup {
+ name: "perfetto_config_filegroup_proto",
srcs: [
"protos/perfetto/common/android_energy_consumer_descriptor.proto",
"protos/perfetto/common/android_log_constants.proto",
@@ -1375,10 +1375,6 @@
"protos/perfetto/config/trace_config.proto",
"protos/perfetto/config/track_event/track_event_config.proto",
],
- proto: {
- type: "lite",
- canonical_path_from_root: false,
- },
}
// GN: //test/cts:perfetto_cts_deps
@@ -17265,6 +17261,38 @@
output_extension: "srcjar",
}
+java_library {
+ name: "perfetto_config_java_protos",
+ srcs: [
+ ":perfetto_config_filegroup_proto",
+ ],
+ static_libs: [
+ "libprotobuf-java-lite",
+ ],
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+}
+
+java_library {
+ name: "perfetto_config_java_protos_system_server_current",
+ srcs: [
+ ":perfetto_config_filegroup_proto",
+ ],
+ static_libs: [
+ "libprotobuf-java-lite",
+ ],
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_server_current",
+ apex_available: [
+ "com.android.profiling",
+ ],
+}
+
prebuilt_etc {
name: "perfetto_persistent_cfg.pbtxt",
filename: "persistent_cfg.pbtxt",
diff --git a/Android.bp.extras b/Android.bp.extras
index 75d3a81..56cfcdd 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -198,6 +198,38 @@
output_extension: "srcjar",
}
+java_library {
+ name: "perfetto_config_java_protos",
+ srcs: [
+ ":perfetto_config_filegroup_proto",
+ ],
+ static_libs: [
+ "libprotobuf-java-lite",
+ ],
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+}
+
+java_library {
+ name: "perfetto_config_java_protos_system_server_current",
+ srcs: [
+ ":perfetto_config_filegroup_proto",
+ ],
+ static_libs: [
+ "libprotobuf-java-lite",
+ ],
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_server_current",
+ apex_available: [
+ "com.android.profiling",
+ ],
+}
+
prebuilt_etc {
name: "perfetto_persistent_cfg.pbtxt",
filename: "persistent_cfg.pbtxt",
diff --git a/CHANGELOG b/CHANGELOG
index ae94f5f..70cb79c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,6 @@
Unreleased:
Tracing service and probes:
- *
+ *
SQL Standard library:
* Added megacycles support to CPU package. Added tables:
`cpu_cycles_per_process`, `cpu_cycles_per_thread` and
@@ -12,7 +12,10 @@
UI:
*
SDK:
- *
+ * The TRACE_COUNTER macro and CounterTrack constructor no longer accept
+ `const char *` track names. In case your code fails to compile,
+ https://perfetto.dev/docs/instrumentation/track-events#dynamic-event-names
+ explains how to fix the problem.
v45.0 - 2024-05-09:
diff --git a/include/perfetto/tracing/string_helpers.h b/include/perfetto/tracing/string_helpers.h
index 0d2819a..03fa9e4 100644
--- a/include/perfetto/tracing/string_helpers.h
+++ b/include/perfetto/tracing/string_helpers.h
@@ -38,6 +38,8 @@
constexpr explicit StaticString(const char* str) : value(str) {}
+ operator bool() const { return !!value; }
+
const char* value;
};
@@ -52,6 +54,9 @@
length = strlen(str);
}
DynamicString(const char* str, size_t len) : value(str), length(len) {}
+ constexpr DynamicString() : value(nullptr), length(0) {}
+
+ operator bool() const { return !!value; }
const char* value;
size_t length;
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 367e10a..831c290 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -202,7 +202,7 @@
perfetto::protos::gen::CounterDescriptor::BuiltinCounterType;
// |name| must outlive this object.
- constexpr explicit CounterTrack(const char* name,
+ constexpr explicit CounterTrack(StaticString name,
Track parent = MakeProcessTrack())
: CounterTrack(
name,
@@ -255,33 +255,39 @@
}
constexpr CounterTrack set_unit(Unit unit) const {
- return CounterTrack(uuid, parent_uuid, name_, category_, unit, unit_name_,
- unit_multiplier_, is_incremental_, type_);
+ return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+ category_, unit, unit_name_, unit_multiplier_,
+ is_incremental_, type_);
}
constexpr CounterTrack set_type(CounterType type) const {
- return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
- unit_multiplier_, is_incremental_, type);
+ return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+ category_, unit_, unit_name_, unit_multiplier_,
+ is_incremental_, type);
}
constexpr CounterTrack set_unit_name(const char* unit_name) const {
- return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name,
- unit_multiplier_, is_incremental_, type_);
+ return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+ category_, unit_, unit_name, unit_multiplier_,
+ is_incremental_, type_);
}
constexpr CounterTrack set_unit_multiplier(int64_t unit_multiplier) const {
- return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
- unit_multiplier, is_incremental_, type_);
+ return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+ category_, unit_, unit_name_, unit_multiplier,
+ is_incremental_, type_);
}
constexpr CounterTrack set_category(const char* category) const {
- return CounterTrack(uuid, parent_uuid, name_, category, unit_, unit_name_,
- unit_multiplier_, is_incremental_, type_);
+ return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+ category, unit_, unit_name_, unit_multiplier_,
+ is_incremental_, type_);
}
constexpr CounterTrack set_is_incremental(bool is_incremental = true) const {
- return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
- unit_multiplier_, is_incremental, type_);
+ return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+ category_, unit_, unit_name_, unit_multiplier_,
+ is_incremental, type_);
}
constexpr bool is_incremental() const { return is_incremental_; }
@@ -290,12 +296,12 @@
protos::gen::TrackDescriptor Serialize() const;
private:
- constexpr CounterTrack(const char* name,
+ constexpr CounterTrack(StaticString name,
Unit unit,
const char* unit_name,
Track parent)
- : Track(internal::Fnv1a(name) ^ kCounterMagic, parent),
- name_(name),
+ : Track(internal::Fnv1a(name.value) ^ kCounterMagic, parent),
+ static_name_(name),
category_(nullptr),
unit_(unit),
unit_name_(unit_name) {}
@@ -304,13 +310,15 @@
const char* unit_name,
Track parent)
: Track(internal::Fnv1a(name.value, name.length) ^ kCounterMagic, parent),
- name_(name.value),
+ static_name_(nullptr),
+ dynamic_name_(name),
category_(nullptr),
unit_(unit),
unit_name_(unit_name) {}
constexpr CounterTrack(uint64_t uuid_,
uint64_t parent_uuid_,
- const char* name,
+ StaticString static_name,
+ DynamicString dynamic_name,
const char* category,
Unit unit,
const char* unit_name,
@@ -318,7 +326,8 @@
bool is_incremental,
CounterType type)
: Track(uuid_, parent_uuid_),
- name_(name),
+ static_name_(static_name),
+ dynamic_name_(dynamic_name),
category_(category),
unit_(unit),
unit_name_(unit_name),
@@ -326,7 +335,8 @@
is_incremental_(is_incremental),
type_(type) {}
- const char* const name_;
+ StaticString static_name_;
+ DynamicString dynamic_name_;
const char* const category_;
Unit unit_ = perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED;
const char* const unit_name_ = nullptr;
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index c296aea..20612e4 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -14676,7 +14676,7 @@
// |TrackEvent::track_uuid|. It is possible but not necessary to emit a
// TrackDescriptor for this implicit track.
//
-// Next id: 10.
+// Next id: 11.
message TrackDescriptor {
// Unique ID that identifies this track. This ID is global to the whole trace.
// Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -14696,7 +14696,12 @@
// Name of the track. Optional - if unspecified, it may be derived from the
// process/thread name (process/thread tracks), the first event's name (async
// tracks), or counter name (counter tracks).
- optional string name = 2;
+ oneof static_or_dynamic_name {
+ string name = 2;
+ // This field is only set by the SDK when perfetto::StaticString is
+ // provided.
+ string static_name = 10;
+ }
// Associate the track with a process, making it the process-global track.
// There should only be one such track per process (usually for instant
diff --git a/protos/perfetto/trace/track_event/track_descriptor.proto b/protos/perfetto/trace/track_event/track_descriptor.proto
index d6db233..76890f2 100644
--- a/protos/perfetto/trace/track_event/track_descriptor.proto
+++ b/protos/perfetto/trace/track_event/track_descriptor.proto
@@ -37,7 +37,7 @@
// |TrackEvent::track_uuid|. It is possible but not necessary to emit a
// TrackDescriptor for this implicit track.
//
-// Next id: 10.
+// Next id: 11.
message TrackDescriptor {
// Unique ID that identifies this track. This ID is global to the whole trace.
// Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -57,7 +57,12 @@
// Name of the track. Optional - if unspecified, it may be derived from the
// process/thread name (process/thread tracks), the first event's name (async
// tracks), or counter name (counter tracks).
- optional string name = 2;
+ oneof static_or_dynamic_name {
+ string name = 2;
+ // This field is only set by the SDK when perfetto::StaticString is
+ // provided.
+ string static_name = 10;
+ }
// Associate the track with a process, making it the process-global track.
// There should only be one such track per process (usually for instant
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.cc b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
index 25cfb46..2dcc3e5 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.cc
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
@@ -376,12 +376,14 @@
base::Status PerfDataTokenizer::ParseFeature(uint8_t feature_id,
TraceBlobView data) {
switch (feature_id) {
- case feature::ID_EVENT_DESC: {
- RETURN_IF_ERROR(feature::EventDescription::Parse(
- std::move(data),
- [](feature::EventDescription) { return base::OkStatus(); }));
- break;
- }
+ case feature::ID_EVENT_DESC:
+ return feature::EventDescription::Parse(
+ std::move(data), [&](feature::EventDescription desc) {
+ for (auto id : desc.ids) {
+ perf_session_->SetEventName(id, std::move(desc.event_string));
+ }
+ return base::OkStatus();
+ });
case feature::ID_BUILD_ID:
return feature::BuildId::Parse(
@@ -399,6 +401,9 @@
feature::SimpleperfMetaInfo meta_info;
RETURN_IF_ERROR(
feature::SimpleperfMetaInfo::Parse(std::move(data), meta_info));
+ for (auto it = meta_info.event_type_info.GetIterator(); it; ++it) {
+ perf_session_->SetEventName(it.key().type, it.key().config, it.value());
+ }
break;
}
case feature::ID_SIMPLEPERF_FILE2: {
diff --git a/src/trace_processor/importers/perf/perf_event.h b/src/trace_processor/importers/perf/perf_event.h
index c45f4de..58077be 100644
--- a/src/trace_processor/importers/perf/perf_event.h
+++ b/src/trace_processor/importers/perf/perf_event.h
@@ -251,7 +251,7 @@
PERF_FORMAT_MAX = 1U << 5, /* non-ABI */
};
-enum perf_callchain_context {
+enum perf_callchain_context : uint64_t {
PERF_CONTEXT_HV = static_cast<uint64_t>(-32),
PERF_CONTEXT_KERNEL = static_cast<uint64_t>(-128),
PERF_CONTEXT_USER = static_cast<uint64_t>(-512),
diff --git a/src/trace_processor/importers/perf/perf_event_attr.cc b/src/trace_processor/importers/perf/perf_event_attr.cc
index f00f4eb..03fe71f 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.cc
+++ b/src/trace_processor/importers/perf/perf_event_attr.cc
@@ -21,6 +21,7 @@
#include <cstring>
#include <optional>
+#include "perfetto/ext/base/string_view.h"
#include "src/trace_processor/importers/perf/perf_counter.h"
#include "src/trace_processor/importers/perf/perf_event.h"
#include "src/trace_processor/storage/trace_storage.h"
@@ -115,14 +116,21 @@
}
PerfCounter PerfEventAttr::CreateCounter(uint32_t cpu) const {
- return PerfCounter(context_->storage->mutable_counter_table(),
- context_->storage->mutable_perf_counter_track_table()
- ->Insert({context_->storage->InternString(""),
- std::nullopt, std::nullopt, std::nullopt,
- context_->storage->InternString(""),
- context_->storage->InternString(""),
- perf_session_id_, cpu, is_timebase()})
- .row_reference);
+ return PerfCounter(
+ context_->storage->mutable_counter_table(),
+ context_->storage->mutable_perf_counter_track_table()
+ ->Insert({/*in_name=*/context_->storage->InternString(
+ base::StringView(event_name_)),
+ /*in_parent_id=*/std::nullopt,
+ /*in_source_arg_set_id=*/std::nullopt,
+ /*in_machine_id=*/std::nullopt,
+ /*in_unit=*/
+ context_->storage->InternString(base::StringView("")),
+ /*in_description=*/
+ context_->storage->InternString(base::StringView("")),
+ /*in_perf_session_id=*/perf_session_id_, /*in_cpu=*/cpu,
+ /*in_is_timebase=*/is_timebase()})
+ .row_reference);
}
} // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_event_attr.h b/src/trace_processor/importers/perf/perf_event_attr.h
index 77e52f4..f1af037 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.h
+++ b/src/trace_processor/importers/perf/perf_event_attr.h
@@ -23,7 +23,6 @@
#include <optional>
#include <unordered_map>
-#include "perfetto/ext/base/string_view.h"
#include "perfetto/trace_processor/ref_counted.h"
#include "src/trace_processor/importers/perf/perf_counter.h"
#include "src/trace_processor/importers/perf/perf_event.h"
@@ -41,6 +40,8 @@
uint32_t perf_session_id_,
perf_event_attr attr);
~PerfEventAttr();
+ uint32_t type() const { return attr_.type; }
+ uint64_t config() const { return attr_.config; }
uint64_t sample_type() const { return attr_.sample_type; }
uint64_t read_format() const { return attr_.read_format; }
bool sample_id_all() const { return !!attr_.sample_id_all; }
@@ -84,6 +85,10 @@
return id_offset_from_end_;
}
+ void set_event_name(std::string event_name) {
+ event_name_ = std::move(event_name);
+ }
+
PerfCounter& GetOrCreateCounter(uint32_t cpu) const;
private:
@@ -103,6 +108,7 @@
std::optional<size_t> id_offset_from_start_;
std::optional<size_t> id_offset_from_end_;
mutable std::unordered_map<uint32_t, PerfCounter> counters_;
+ std::string event_name_;
};
} // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/perf_session.cc b/src/trace_processor/importers/perf/perf_session.cc
index 7619066..db42ae7 100644
--- a/src/trace_processor/importers/perf/perf_session.cc
+++ b/src/trace_processor/importers/perf/perf_session.cc
@@ -135,4 +135,22 @@
return RefPtr<const PerfEventAttr>(it->get());
}
+void PerfSession::SetEventName(uint64_t event_id, std::string name) {
+ auto it = attrs_by_id_.Find(event_id);
+ if (!it) {
+ return;
+ }
+ (*it)->set_event_name(std::move(name));
+}
+
+void PerfSession::SetEventName(uint32_t type,
+ uint64_t config,
+ const std::string& name) {
+ for (auto it = attrs_by_id_.GetIterator(); it; ++it) {
+ if (it.value()->type() == type && it.value()->config() == config) {
+ it.value()->set_event_name(name);
+ }
+ }
+}
+
} // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_session.h b/src/trace_processor/importers/perf/perf_session.h
index eb509d0..c2a9e66 100644
--- a/src/trace_processor/importers/perf/perf_session.h
+++ b/src/trace_processor/importers/perf/perf_session.h
@@ -66,6 +66,9 @@
const perf_event_header& header,
const TraceBlobView& payload) const;
+ void SetEventName(uint64_t event_id, std::string name);
+ void SetEventName(uint32_t type, uint64_t config, const std::string& name);
+
private:
PerfSession(uint32_t perf_session_id,
base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id,
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 704a5a0..d958c0b 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -21,6 +21,7 @@
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/ext/base/utils.h"
#include "perfetto/protozero/proto_decoder.h"
@@ -119,6 +120,16 @@
HandlePreviousPacketDropped(decoder);
}
+ uint32_t sequence_id = decoder.trusted_packet_sequence_id();
+ if (sequence_id) {
+ auto [data_loss, inserted] =
+ packet_sequence_data_loss_.Insert(sequence_id, 0);
+
+ if (!inserted && decoder.previous_packet_dropped()) {
+ *data_loss += 1;
+ }
+ }
+
// It is important that we parse defaults before parsing other fields such as
// the timestamp, since the defaults could affect them.
if (decoder.has_trace_packet_defaults()) {
@@ -132,8 +143,7 @@
}
if (decoder.has_clock_snapshot()) {
- return ParseClockSnapshot(decoder.clock_snapshot(),
- decoder.trusted_packet_sequence_id());
+ return ParseClockSnapshot(decoder.clock_snapshot(), sequence_id);
}
if (decoder.has_trace_stats()) {
@@ -579,6 +589,23 @@
stats::traced_buf_trace_writer_packet_loss, buf_num,
static_cast<int64_t>(buf.trace_writer_packet_loss()));
}
+
+ base::FlatHashMap<int32_t, int64_t> data_loss_per_buffer;
+
+ for (auto it = evt.writer_stats(); it; ++it) {
+ protos::pbzero::TraceStats::WriterStats::Decoder writer(*it);
+ auto* data_loss = packet_sequence_data_loss_.Find(
+ static_cast<uint32_t>(writer.sequence_id()));
+ if (data_loss) {
+ data_loss_per_buffer[static_cast<int32_t>(writer.buffer())] +=
+ static_cast<int64_t>(*data_loss);
+ }
+ }
+
+ for (auto it = data_loss_per_buffer.GetIterator(); it; ++it) {
+ storage->SetIndexedStats(stats::traced_buf_sequence_packet_loss, it.key(),
+ it.value());
+ }
}
void ProtoTraceReader::NotifyEndOfFile() {}
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index c080837..4c4e2ca 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -105,6 +105,8 @@
base::FlatHashMap<uint32_t, PacketSequenceStateBuilder>
packet_sequence_state_builders_;
+ base::FlatHashMap<uint32_t, size_t> packet_sequence_data_loss_;
+
StringId skipped_packet_key_id_;
StringId invalid_incremental_state_key_id_;
};
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index a242890..657e308 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -1527,9 +1527,10 @@
}
// Override the name with the most recent name seen (after sorting by ts).
- if (decoder.has_name()) {
+ if (decoder.has_name() || decoder.has_static_name()) {
auto* tracks = context_->storage->mutable_track_table();
- const StringId raw_name_id = context_->storage->InternString(decoder.name());
+ const StringId raw_name_id = context_->storage->InternString(
+ decoder.has_name() ? decoder.name() : decoder.static_name());
const StringId name_id =
context_->process_track_translation_table->TranslateName(raw_name_id);
tracks->mutable_name()->Set(*tracks->id().IndexOf(track_id), name_id);
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index c836052..238e247 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -92,6 +92,8 @@
StringId name_id = kNullStringId;
if (track.has_name())
name_id = context_->storage->InternString(track.name());
+ else if (track.has_static_name())
+ name_id = context_->storage->InternString(track.static_name());
if (packet.has_trusted_pid()) {
context_->process_tracker->UpdateTrustedPid(
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
index 0d31b0d..1551ea6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
@@ -73,7 +73,9 @@
FROM awake_slice
UNION ALL
SELECT ts, dur, 'suspended' AS power_state
-FROM suspend_slice;
+FROM suspend_slice
+ORDER BY ts; -- Order by will cause Perfetto table to index by ts.
+
-- Extracts the duration without counting CPU suspended time from an event.
-- This is the same as converting an event duration from wall clock to monotonic clock.
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index 736af92..cc4f545 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -54,9 +54,6 @@
namespace perfetto::trace_processor {
namespace {
-// TODO(b/341230981): Enable after it's fixed.
-static constexpr bool kEnableDistinct = false;
-
std::optional<FilterOp> SqliteOpToFilterOp(int sqlite_op) {
switch (sqlite_op) {
case SQLITE_INDEX_CONSTRAINT_EQ:
@@ -574,7 +571,7 @@
// Distinct:
idx_str += "D";
- if (ob_idxes.size() == 1 && kEnableDistinct) {
+ if (ob_idxes.size() == 1 && PERFETTO_POPCOUNT(info->colUsed) == 1) {
switch (sqlite3_vtab_distinct(info)) {
case 0:
case 1:
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 52f0c20..c8d58ca 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -155,7 +155,13 @@
F(traced_buf_patches_succeeded, kIndexed, kInfo, kTrace, ""), \
F(traced_buf_readaheads_failed, kIndexed, kInfo, kTrace, ""), \
F(traced_buf_readaheads_succeeded, kIndexed, kInfo, kTrace, ""), \
- F(traced_buf_trace_writer_packet_loss, kIndexed, kDataLoss, kTrace, ""), \
+ F(traced_buf_trace_writer_packet_loss, kIndexed, kDataLoss, kTrace, \
+ "The tracing service observed packet loss for this buffer during this " \
+ "tracing session. This also counts packet loss that happened before " \
+ "the RING_BUFFER start or after the DISCARD buffer end."), \
+ F(traced_buf_sequence_packet_loss, kIndexed, kDataLoss, kAnalysis, \
+ "The number of groups of consecutive packets lost in each sequence for " \
+ "this buffer"), \
F(traced_buf_write_wrap_count, kIndexed, kInfo, kTrace, ""), \
F(traced_chunks_discarded, kSingle, kInfo, kTrace, ""), \
F(traced_data_sources_registered, kSingle, kInfo, kTrace, ""), \
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index eec75fd..7014c0d 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -79,10 +79,11 @@
redactor.emplace_transform<PrunePackageList>();
redactor.emplace_transform<ScrubProcessStats>();
+ auto* comms_harness = redactor.emplace_transform<RedactSchedSwitchHarness>();
+ comms_harness->emplace_transform<ClearComms>();
+
auto* redact_ftrace_events = redactor.emplace_transform<RedactFtraceEvent>();
redact_ftrace_events
- ->emplace_back<RedactSchedSwitch::kFieldId, RedactSchedSwitch>();
- redact_ftrace_events
->emplace_back<RedactTaskNewTask::kFieldId, RedactTaskNewTask>();
redact_ftrace_events
->emplace_back<RedactProcessFree::kFieldId, RedactProcessFree>();
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
index 83cb36f..87412c1 100644
--- a/src/trace_redaction/redact_sched_switch.cc
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -16,6 +16,8 @@
#include "src/trace_redaction/redact_sched_switch.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/trace_processor/util/status_macros.h"
#include "src/trace_redaction/proto_util.h"
#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
@@ -25,19 +27,12 @@
namespace perfetto::trace_redaction {
namespace {
-
-// TODO(vaage): Merge with RedactComm in redact_task_newtask.cc.
-protozero::ConstChars RedactComm(const Context& context,
- uint64_t ts,
- int32_t pid,
- protozero::ConstChars comm) {
- if (context.timeline->PidConnectsToUid(ts, pid, *context.package_uid)) {
- return comm;
- }
-
- return {};
+// TODO(vaage): While simple, this function saves us from declaring the sample
+// lambda each time we use the has_fields pattern. Once its usage increases, and
+// its value is obvious, remove this comment.
+bool IsTrue(bool value) {
+ return value;
}
-
} // namespace
// Redact sched switch trace events in an ftrace event bundle:
@@ -63,71 +58,170 @@
// collection of ftrace event messages) because data in a sched_switch message
// is needed in order to know if the event should be added to the bundle.
-base::Status RedactSchedSwitch::Redact(
- const Context& context,
- const protos::pbzero::FtraceEventBundle::Decoder&,
- protozero::ProtoDecoder& event,
- protos::pbzero::FtraceEvent* event_message) const {
- if (!context.package_uid.has_value()) {
- return base::ErrStatus("RedactSchedSwitch: missing package uid");
- }
+SchedSwitchTransform::~SchedSwitchTransform() = default;
- if (!context.timeline) {
- return base::ErrStatus("RedactSchedSwitch: missing timeline");
- }
+base::Status RedactSchedSwitchHarness::Transform(const Context& context,
+ std::string* packet) const {
+ protozero::HeapBuffered<protos::pbzero::TracePacket> message;
+ protozero::ProtoDecoder decoder(*packet);
- // The timestamp is needed to do the timeline look-up. If the packet has no
- // timestamp, don't add the sched switch event. This is the safest option.
- auto timestamp =
- event.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
- if (!timestamp.valid()) {
- return base::OkStatus();
- }
-
- auto sched_switch =
- event.FindField(protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber);
- if (!sched_switch.valid()) {
- return base::ErrStatus(
- "RedactSchedSwitch: was used for unsupported field type");
- }
-
- protozero::ProtoDecoder sched_switch_decoder(sched_switch.as_bytes());
-
- auto prev_pid = sched_switch_decoder.FindField(
- protos::pbzero::SchedSwitchFtraceEvent::kPrevPidFieldNumber);
- auto next_pid = sched_switch_decoder.FindField(
- protos::pbzero::SchedSwitchFtraceEvent::kNextPidFieldNumber);
-
- // There must be a prev pid and a next pid. Otherwise, the event is invalid.
- // Dropping the event is the safest option.
- if (!prev_pid.valid() || !next_pid.valid()) {
- return base::OkStatus();
- }
-
- // Avoid making the message until we know that we have prev and next pids.
- auto sched_switch_message = event_message->set_sched_switch();
-
- for (auto field = sched_switch_decoder.ReadField(); field.valid();
- field = sched_switch_decoder.ReadField()) {
- switch (field.id()) {
- case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber:
- sched_switch_message->set_next_comm(
- RedactComm(context, timestamp.as_uint64(), next_pid.as_int32(),
- field.as_string()));
- break;
-
- case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
- sched_switch_message->set_prev_comm(
- RedactComm(context, timestamp.as_uint64(), prev_pid.as_int32(),
- field.as_string()));
- break;
-
- default:
- proto_util::AppendField(field, sched_switch_message);
- break;
+ for (auto field = decoder.ReadField(); field.valid();
+ field = decoder.ReadField()) {
+ if (field.id() == protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+ RETURN_IF_ERROR(
+ TransformFtraceEvents(context, field, message->set_ftrace_events()));
+ } else {
+ proto_util::AppendField(field, message.get());
}
}
+ packet->assign(message.SerializeAsString());
+
+ return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEvents(
+ const Context& context,
+ protozero::Field ftrace_events,
+ protos::pbzero::FtraceEventBundle* message) const {
+ PERFETTO_DCHECK(ftrace_events.id() ==
+ protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
+
+ protozero::ProtoDecoder decoder(ftrace_events.as_bytes());
+
+ auto cpu =
+ decoder.FindField(protos::pbzero::FtraceEventBundle::kCpuFieldNumber);
+ if (!cpu.valid()) {
+ return base::ErrStatus(
+ "RedactSchedSwitchHarness: missing cpu in ftrace event bundle.");
+ }
+
+ for (auto field = decoder.ReadField(); field.valid();
+ field = decoder.ReadField()) {
+ if (field.id() == protos::pbzero::FtraceEventBundle::kEventFieldNumber) {
+ RETURN_IF_ERROR(TransformFtraceEvent(context, cpu.as_int32(), field,
+ message->add_event()));
+ continue;
+ }
+
+ if (field.id() ==
+ protos::pbzero::FtraceEventBundle::kCompactSchedFieldNumber) {
+ // TODO(vaage): Replace this with logic specific to the comp sched data
+ // type.
+ proto_util::AppendField(field, message);
+ continue;
+ }
+
+ proto_util::AppendField(field, message);
+ }
+
+ return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEvent(
+ const Context& context,
+ int32_t cpu,
+ protozero::Field ftrace_event,
+ protos::pbzero::FtraceEvent* message) const {
+ PERFETTO_DCHECK(ftrace_event.id() ==
+ protos::pbzero::FtraceEventBundle::kEventFieldNumber);
+
+ protozero::ProtoDecoder decoder(ftrace_event.as_bytes());
+
+ auto ts =
+ decoder.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
+ if (!ts.valid()) {
+ return base::ErrStatus(
+ "RedactSchedSwitchHarness: missing timestamp in ftrace event.");
+ }
+
+ std::string scratch_str;
+
+ for (auto field = decoder.ReadField(); field.valid();
+ field = decoder.ReadField()) {
+ if (field.id() == protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber) {
+ protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(
+ field.as_bytes());
+ RETURN_IF_ERROR(TransformFtraceEventSchedSwitch(
+ context, ts.as_uint64(), cpu, sched_switch, &scratch_str,
+ message->set_sched_switch()));
+ } else {
+ proto_util::AppendField(field, message);
+ }
+ }
+
+ return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEventSchedSwitch(
+ const Context& context,
+ uint64_t ts,
+ int32_t cpu,
+ protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch,
+ std::string* scratch_str,
+ protos::pbzero::SchedSwitchFtraceEvent* message) const {
+ auto has_fields = {
+ sched_switch.has_prev_comm(), sched_switch.has_prev_pid(),
+ sched_switch.has_prev_prio(), sched_switch.has_prev_state(),
+ sched_switch.has_next_comm(), sched_switch.has_next_pid(),
+ sched_switch.has_next_prio()};
+
+ if (!std::all_of(has_fields.begin(), has_fields.end(), IsTrue)) {
+ return base::ErrStatus(
+ "RedactSchedSwitchHarness: missing required SchedSwitchFtraceEvent "
+ "field.");
+ }
+
+ auto prev_pid = sched_switch.prev_pid();
+ auto prev_comm = sched_switch.prev_comm();
+
+ auto next_pid = sched_switch.next_pid();
+ auto next_comm = sched_switch.next_comm();
+
+ // There are 7 values in a sched switch message. Since 4 of the 7 can be
+ // replaced, it is easier/cleaner to go value-by-value. Go in proto-defined
+ // order.
+
+ scratch_str->assign(prev_comm.data, prev_comm.size);
+
+ for (const auto& transform : transforms_) {
+ RETURN_IF_ERROR(
+ transform->Transform(context, ts, cpu, &prev_pid, scratch_str));
+ }
+
+ message->set_prev_comm(*scratch_str); // FieldNumber = 1
+ message->set_prev_pid(prev_pid); // FieldNumber = 2
+ message->set_prev_prio(sched_switch.prev_prio()); // FieldNumber = 3
+ message->set_prev_state(sched_switch.prev_state()); // FieldNumber = 4
+
+ scratch_str->assign(next_comm.data, next_comm.size);
+
+ for (const auto& transform : transforms_) {
+ RETURN_IF_ERROR(
+ transform->Transform(context, ts, cpu, &next_pid, scratch_str));
+ }
+
+ message->set_next_comm(*scratch_str); // FieldNumber = 5
+ message->set_next_pid(next_pid); // FieldNumber = 6
+ message->set_next_prio(sched_switch.next_prio()); // FieldNumber = 7
+
+ return base::OkStatus();
+}
+
+// Switch event transformation: Clear the comm value if the thread/process is
+// not part of the target packet.
+base::Status ClearComms::Transform(const Context& context,
+ uint64_t ts,
+ int32_t,
+ int32_t* pid,
+ std::string* comm) const {
+ PERFETTO_DCHECK(pid);
+ PERFETTO_DCHECK(comm);
+
+ if (!context.timeline->PidConnectsToUid(ts, *pid, *context.package_uid)) {
+ comm->clear();
+ }
+
return base::OkStatus();
}
diff --git a/src/trace_redaction/redact_sched_switch.h b/src/trace_redaction/redact_sched_switch.h
index bcfa30d..55f9a75 100644
--- a/src/trace_redaction/redact_sched_switch.h
+++ b/src/trace_redaction/redact_sched_switch.h
@@ -17,23 +17,63 @@
#ifndef SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
#define SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
-#include "src/trace_redaction/redact_ftrace_event.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
#include "src/trace_redaction/trace_redaction_framework.h"
namespace perfetto::trace_redaction {
-// Goes through ftrace events and conditonally removes the comm values from
-// sched switch events.
-class RedactSchedSwitch : public FtraceEventRedaction {
+class SchedSwitchTransform {
public:
- static constexpr auto kFieldId =
- protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber;
+ virtual ~SchedSwitchTransform();
+ virtual base::Status Transform(const Context& context,
+ uint64_t ts,
+ int32_t cpu,
+ int32_t* pid,
+ std::string* comm) const = 0;
+};
- base::Status Redact(
+// Goes through all sched switch events are modifies them.
+class RedactSchedSwitchHarness : public TransformPrimitive {
+ public:
+ base::Status Transform(const Context& context,
+ std::string* packet) const override;
+
+ template <class Transform>
+ void emplace_transform() {
+ transforms_.emplace_back(new Transform());
+ }
+
+ private:
+ base::Status TransformFtraceEvents(
const Context& context,
- const protos::pbzero::FtraceEventBundle::Decoder& bundle,
- protozero::ProtoDecoder& event,
- protos::pbzero::FtraceEvent* event_message) const override;
+ protozero::Field ftrace_events,
+ protos::pbzero::FtraceEventBundle* message) const;
+
+ base::Status TransformFtraceEvent(const Context& context,
+ int32_t cpu,
+ protozero::Field ftrace_event,
+ protos::pbzero::FtraceEvent* message) const;
+
+ // scratch_str is a reusable string, allowing comm modifications to be done in
+ // a shared buffer, avoiding allocations when processing ftrace events.
+ base::Status TransformFtraceEventSchedSwitch(
+ const Context& context,
+ uint64_t ts,
+ int32_t cpu,
+ protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch,
+ std::string* scratch_str,
+ protos::pbzero::SchedSwitchFtraceEvent* message) const;
+
+ std::vector<std::unique_ptr<SchedSwitchTransform>> transforms_;
+};
+
+class ClearComms : public SchedSwitchTransform {
+ base::Status Transform(const Context& context,
+ uint64_t ts,
+ int32_t cpu,
+ int32_t* pid,
+ std::string* comm) const override;
};
} // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
index f14d6e9..247d44b 100644
--- a/src/trace_redaction/redact_sched_switch_integrationtest.cc
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -16,9 +16,9 @@
#include <cstdint>
#include <string>
+#include <unordered_map>
#include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
#include "src/base/test/status_matchers.h"
#include "src/trace_redaction/collect_timeline_events.h"
#include "src/trace_redaction/find_package_uid.h"
@@ -36,23 +36,6 @@
namespace perfetto::trace_redaction {
-class RedactSchedSwitchIntegrationTest
- : public testing::Test,
- protected TraceRedactionIntegrationFixure {
- protected:
- void SetUp() override {
- trace_redactor()->emplace_collect<FindPackageUid>();
- trace_redactor()->emplace_collect<CollectTimelineEvents>();
-
- auto* ftrace_event_redactions =
- trace_redactor()->emplace_transform<RedactFtraceEvent>();
- ftrace_event_redactions
- ->emplace_back<RedactSchedSwitch::kFieldId, RedactSchedSwitch>();
-
- context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
- }
-};
-
// >>> SELECT uid
// >>> FROM package_list
// >>> WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
@@ -103,6 +86,35 @@
// | 7950 | UnityGfxDeviceW |
// | 7969 | UnityGfxDeviceW |
// +------+-----------------+
+class RedactSchedSwitchIntegrationTest
+ : public testing::Test,
+ protected TraceRedactionIntegrationFixure {
+ protected:
+ void SetUp() override {
+ trace_redactor()->emplace_collect<FindPackageUid>();
+ trace_redactor()->emplace_collect<CollectTimelineEvents>();
+
+ auto* harness =
+ trace_redactor()->emplace_transform<RedactSchedSwitchHarness>();
+ harness->emplace_transform<ClearComms>();
+
+ context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
+ }
+
+ std::unordered_map<int32_t, std::string> expected_names_ = {
+ {7120, "Binder:7105_2"}, {7127, "UnityMain"},
+ {7142, "Job.worker 0"}, {7143, "Job.worker 1"},
+ {7144, "Job.worker 2"}, {7145, "Job.worker 3"},
+ {7146, "Job.worker 4"}, {7147, "Job.worker 5"},
+ {7148, "Job.worker 6"}, {7150, "Background Job."},
+ {7151, "Background Job."}, {7167, "UnityGfxDeviceW"},
+ {7172, "AudioTrack"}, {7174, "FMOD stream thr"},
+ {7180, "Binder:7105_3"}, {7184, "UnityChoreograp"},
+ {7945, "Filter0"}, {7946, "Filter1"},
+ {7947, "Thread-7"}, {7948, "FMOD mixer thre"},
+ {7950, "UnityGfxDeviceW"}, {7969, "UnityGfxDeviceW"},
+ };
+};
TEST_F(RedactSchedSwitchIntegrationTest, ClearsNonTargetSwitchComms) {
auto result = Redact();
@@ -114,30 +126,6 @@
auto redacted = LoadRedacted();
ASSERT_OK(redacted) << redacted.status().c_message();
- base::FlatHashMap<int32_t, std::string> expected_names;
- expected_names.Insert(7120, "Binder:7105_2");
- expected_names.Insert(7127, "UnityMain");
- expected_names.Insert(7142, "Job.worker 0");
- expected_names.Insert(7143, "Job.worker 1");
- expected_names.Insert(7144, "Job.worker 2");
- expected_names.Insert(7145, "Job.worker 3");
- expected_names.Insert(7146, "Job.worker 4");
- expected_names.Insert(7147, "Job.worker 5");
- expected_names.Insert(7148, "Job.worker 6");
- expected_names.Insert(7150, "Background Job.");
- expected_names.Insert(7151, "Background Job.");
- expected_names.Insert(7167, "UnityGfxDeviceW");
- expected_names.Insert(7172, "AudioTrack");
- expected_names.Insert(7174, "FMOD stream thr");
- expected_names.Insert(7180, "Binder:7105_3");
- expected_names.Insert(7184, "UnityChoreograp");
- expected_names.Insert(7945, "Filter0");
- expected_names.Insert(7946, "Filter1");
- expected_names.Insert(7947, "Thread-7");
- expected_names.Insert(7948, "FMOD mixer thre");
- expected_names.Insert(7950, "UnityGfxDeviceW");
- expected_names.Insert(7969, "UnityGfxDeviceW");
-
auto redacted_trace_data = LoadRedacted();
ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
@@ -164,29 +152,29 @@
event_decoder.sched_switch());
ASSERT_TRUE(sched_decoder.has_next_pid());
- ASSERT_TRUE(sched_decoder.has_prev_pid());
-
- auto next_pid = sched_decoder.next_pid();
- auto prev_pid = sched_decoder.prev_pid();
+ ASSERT_TRUE(sched_decoder.has_next_comm());
// If the pid is expected, make sure it has the right now. If it is not
// expected, it should be missing.
- const auto* next_comm = expected_names.Find(next_pid);
- const auto* prev_comm = expected_names.Find(prev_pid);
+ auto next_pid = sched_decoder.next_pid();
+ auto next_comm = expected_names_.find(next_pid);
- EXPECT_TRUE(sched_decoder.has_next_comm());
- EXPECT_TRUE(sched_decoder.has_prev_comm());
-
- if (next_comm) {
- EXPECT_EQ(sched_decoder.next_comm().ToStdString(), *next_comm);
+ if (next_comm == expected_names_.end()) {
+ ASSERT_EQ(sched_decoder.next_comm().size, 0u);
} else {
- EXPECT_EQ(sched_decoder.next_comm().size, 0u);
+ ASSERT_EQ(sched_decoder.next_comm().ToStdString(), next_comm->second);
}
- if (prev_comm) {
- EXPECT_EQ(sched_decoder.prev_comm().ToStdString(), *prev_comm);
+ ASSERT_TRUE(sched_decoder.has_prev_pid());
+ ASSERT_TRUE(sched_decoder.has_prev_comm());
+
+ auto prev_pid = sched_decoder.prev_pid();
+ auto prev_comm = expected_names_.find(prev_pid);
+
+ if (prev_comm == expected_names_.end()) {
+ ASSERT_EQ(sched_decoder.prev_comm().size, 0u);
} else {
- EXPECT_EQ(sched_decoder.prev_comm().size, 0u);
+ ASSERT_EQ(sched_decoder.prev_comm().ToStdString(), prev_comm->second);
}
}
}
diff --git a/src/trace_redaction/redact_sched_switch_unittest.cc b/src/trace_redaction/redact_sched_switch_unittest.cc
index 72b65f4..88280d7 100644
--- a/src/trace_redaction/redact_sched_switch_unittest.cc
+++ b/src/trace_redaction/redact_sched_switch_unittest.cc
@@ -15,7 +15,6 @@
*/
#include "src/trace_redaction/redact_sched_switch.h"
-#include "perfetto/protozero/scattered_heap_buffer.h"
#include "src/base/test/status_matchers.h"
#include "test/gtest_and_gmock.h"
@@ -28,6 +27,7 @@
namespace perfetto::trace_redaction {
namespace {
+
constexpr uint64_t kUidA = 1;
constexpr uint64_t kUidB = 2;
constexpr uint64_t kUidC = 3;
@@ -36,167 +36,130 @@
constexpr int32_t kPidA = 11;
constexpr int32_t kPidB = 12;
-constexpr std::string_view kCommA = "comm-a";
-constexpr std::string_view kCommB = "comm-b";
+constexpr int32_t kCpuA = 0;
+
+constexpr uint64_t kFullStep = 1000;
+constexpr uint64_t kTimeA = 0;
+constexpr uint64_t kTimeB = kFullStep;
+
+constexpr auto kCommA = "comm-a";
+constexpr auto kCommB = "comm-b";
+constexpr auto kCommNone = "";
} // namespace
-// Tests which nested messages and fields are removed.
class RedactSchedSwitchTest : public testing::Test {
protected:
void SetUp() override {
- auto* event = bundle_.add_event();
+ // Create a packet where two pids are swapping back-and-forth.
+ auto* bundle = packet_.mutable_ftrace_events();
+ bundle->set_cpu(kCpuA);
- event->set_timestamp(123456789);
- event->set_pid(kPidA);
+ {
+ auto* event = bundle->add_event();
- auto* sched_switch = event->mutable_sched_switch();
- sched_switch->set_prev_comm(std::string(kCommA));
- sched_switch->set_prev_pid(kPidA);
- sched_switch->set_next_comm(std::string(kCommB));
- sched_switch->set_next_pid(kPidB);
+ event->set_timestamp(kTimeA);
+ event->set_pid(kPidA);
+
+ auto* sched_switch = event->mutable_sched_switch();
+ sched_switch->set_prev_comm(kCommA);
+ sched_switch->set_prev_pid(kPidA);
+ sched_switch->set_prev_prio(0);
+ sched_switch->set_prev_state(0);
+ sched_switch->set_next_comm(kCommB);
+ sched_switch->set_next_pid(kPidB);
+ sched_switch->set_next_prio(0);
+ }
+
+ {
+ auto* event = bundle->add_event();
+
+ event->set_timestamp(kTimeB);
+ event->set_pid(kPidB);
+
+ auto* sched_switch = event->mutable_sched_switch();
+ sched_switch->set_prev_comm(kCommB);
+ sched_switch->set_prev_pid(kPidB);
+ sched_switch->set_prev_prio(0);
+ sched_switch->set_prev_state(0);
+ sched_switch->set_next_comm(kCommA);
+ sched_switch->set_next_pid(kPidA);
+ sched_switch->set_next_prio(0);
+ }
+
+ // PID A and PID B need to be attached to different packages (UID) so that
+ // its possible to include one but not the other.
+ context_.timeline = std::make_unique<ProcessThreadTimeline>();
+ context_.timeline->Append(
+ ProcessThreadTimeline::Event::Open(kTimeA, kPidA, kNoParent, kUidA));
+ context_.timeline->Append(
+ ProcessThreadTimeline::Event::Open(kTimeA, kPidB, kNoParent, kUidB));
+ context_.timeline->Sort();
}
- base::Status Redact(const Context& context,
- protos::pbzero::FtraceEvent* event_message) {
- RedactSchedSwitch redact;
-
- auto bundle_str = bundle_.SerializeAsString();
- protos::pbzero::FtraceEventBundle::Decoder bundle_decoder(bundle_str);
-
- auto event_str = bundle_.event().back().SerializeAsString();
- protos::pbzero::FtraceEvent::Decoder event_decoder(event_str);
-
- return redact.Redact(context, bundle_decoder, event_decoder, event_message);
- }
-
- const std::string& event_string() const { return event_string_; }
-
- // This test breaks the rules for task_newtask and the timeline. The
- // timeline will report the task existing before the new task event. This
- // should not happen in the field, but it makes the test more robust.
- std::unique_ptr<ProcessThreadTimeline> CreatePopulatedTimeline() {
- auto timeline = std::make_unique<ProcessThreadTimeline>();
-
- timeline->Append(
- ProcessThreadTimeline::Event::Open(0, kPidA, kNoParent, kUidA));
- timeline->Append(
- ProcessThreadTimeline::Event::Open(0, kPidB, kNoParent, kUidB));
- timeline->Sort();
-
- return timeline;
- }
-
- private:
- std::string event_string_;
-
- std::unique_ptr<ProcessThreadTimeline> timeline_;
-
- protos::gen::FtraceEventBundle bundle_;
+ protos::gen::TracePacket packet_;
+ Context context_;
};
-TEST_F(RedactSchedSwitchTest, RejectMissingPackageUid) {
- RedactSchedSwitch redact;
+// In this case, the target uid will be UID A. That means the comm values for
+// PID B should be removed, and the comm values for PID A should remain.
+TEST_F(RedactSchedSwitchTest, KeepsTargetCommValues) {
+ RedactSchedSwitchHarness redact;
+ redact.emplace_transform<ClearComms>();
- Context context;
- context.timeline = std::make_unique<ProcessThreadTimeline>();
+ context_.package_uid = kUidA;
- protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
- auto result = Redact(context, event_message.get());
- ASSERT_FALSE(result.ok());
+ auto packet_buffer = packet_.SerializeAsString();
+
+ ASSERT_OK(redact.Transform(context_, &packet_buffer));
+
+ protos::gen::TracePacket packet;
+ ASSERT_TRUE(packet.ParseFromString(packet_buffer));
+
+ const auto& bundle = packet.ftrace_events();
+ const auto& events = bundle.event();
+
+ ASSERT_EQ(events.size(), 2u);
+
+ ASSERT_EQ(events[0].sched_switch().prev_pid(), kPidA);
+ ASSERT_EQ(events[0].sched_switch().prev_comm(), kCommA);
+
+ ASSERT_EQ(events[0].sched_switch().next_pid(), kPidB);
+ ASSERT_EQ(events[0].sched_switch().next_comm(), kCommNone);
+
+ ASSERT_EQ(events[1].sched_switch().prev_pid(), kPidB);
+ ASSERT_EQ(events[1].sched_switch().prev_comm(), kCommNone);
+
+ ASSERT_EQ(events[1].sched_switch().next_pid(), kPidA);
+ ASSERT_EQ(events[1].sched_switch().next_comm(), kCommA);
}
-TEST_F(RedactSchedSwitchTest, RejectMissingTimeline) {
- RedactSchedSwitch redact;
+// This case is very similar to the "some are connected", expect that it
+// verifies all comm values will be removed when testing against an unused
+// uid.
+TEST_F(RedactSchedSwitchTest, RemovesAllCommsIfPackageDoesntExist) {
+ RedactSchedSwitchHarness redact;
+ redact.emplace_transform<ClearComms>();
- Context context;
- context.package_uid = kUidA;
+ context_.package_uid = kUidC;
- protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
- auto result = Redact(context, event_message.get());
- ASSERT_FALSE(result.ok());
-}
+ auto packet_buffer = packet_.SerializeAsString();
-TEST_F(RedactSchedSwitchTest, ReplacePrevAndNextWithEmptyStrings) {
- RedactSchedSwitch redact;
+ ASSERT_OK(redact.Transform(context_, &packet_buffer));
- Context context;
- context.timeline = CreatePopulatedTimeline();
+ protos::gen::TracePacket packet;
+ ASSERT_TRUE(packet.ParseFromString(packet_buffer));
- // Neither pid is connected to the target package (see timeline
- // initialization).
- context.package_uid = kUidC;
+ const auto& bundle = packet.ftrace_events();
+ const auto& events = bundle.event();
- protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
- auto result = Redact(context, event_message.get());
- ASSERT_OK(result) << result.c_message();
+ ASSERT_EQ(events.size(), 2u);
- protos::gen::FtraceEvent event;
- event.ParseFromString(event_message.SerializeAsString());
+ ASSERT_EQ(events[0].sched_switch().prev_comm(), kCommNone);
+ ASSERT_EQ(events[0].sched_switch().next_comm(), kCommNone);
- ASSERT_TRUE(event.has_sched_switch());
-
- // Cleared prev and next comm.
- ASSERT_TRUE(event.sched_switch().has_prev_comm());
- ASSERT_TRUE(event.sched_switch().prev_comm().empty());
-
- ASSERT_TRUE(event.sched_switch().has_next_comm());
- ASSERT_TRUE(event.sched_switch().next_comm().empty());
-}
-
-TEST_F(RedactSchedSwitchTest, ReplacePrevWithEmptyStrings) {
- RedactSchedSwitch redact;
-
- Context context;
- context.timeline = CreatePopulatedTimeline();
-
- // Only next pid is connected to the target package (see timeline
- // initialization).
- context.package_uid = kUidB;
-
- protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
- auto result = Redact(context, event_message.get());
-
- ASSERT_OK(result) << result.c_message();
-
- protos::gen::FtraceEvent event;
- event.ParseFromString(event_message.SerializeAsString());
-
- ASSERT_TRUE(event.has_sched_switch());
-
- // Only cleared the prev comm.
- ASSERT_TRUE(event.sched_switch().has_prev_comm());
- ASSERT_TRUE(event.sched_switch().prev_comm().empty());
-
- ASSERT_TRUE(event.sched_switch().has_next_comm());
- ASSERT_FALSE(event.sched_switch().next_comm().empty());
-}
-
-TEST_F(RedactSchedSwitchTest, ReplaceNextWithEmptyStrings) {
- RedactSchedSwitch redact;
-
- Context context;
- context.timeline = CreatePopulatedTimeline();
-
- // Only prev pid is connected to the target package (see timeline
- // initialization).
- context.package_uid = kUidA;
-
- protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
- auto result = Redact(context, event_message.get());
- ASSERT_OK(result) << result.c_message();
-
- protos::gen::FtraceEvent event;
- event.ParseFromString(event_message.SerializeAsString());
-
- ASSERT_TRUE(event.has_sched_switch());
-
- ASSERT_TRUE(event.sched_switch().has_prev_comm());
- ASSERT_FALSE(event.sched_switch().prev_comm().empty());
-
- // Only cleared the next comm.
- ASSERT_TRUE(event.sched_switch().has_next_comm());
- ASSERT_TRUE(event.sched_switch().next_comm().empty());
+ ASSERT_EQ(events[1].sched_switch().prev_comm(), kCommNone);
+ ASSERT_EQ(events[1].sched_switch().next_comm(), kCommNone);
}
} // namespace perfetto::trace_redaction
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 5e1e277..5072c3d 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -2485,7 +2485,7 @@
EXPECT_FALSE(found_counter_track_descriptor);
found_counter_track_descriptor = true;
thread_time_counter_uuid = packet.track_descriptor().uuid();
- EXPECT_EQ("thread_time", packet.track_descriptor().name());
+ EXPECT_EQ("thread_time", packet.track_descriptor().static_name());
auto counter = packet.track_descriptor().counter();
EXPECT_EQ(
perfetto::protos::gen::
@@ -5795,8 +5795,10 @@
auto& desc = packet.track_descriptor();
if (!desc.has_counter())
continue;
- counter_names[desc.uuid()] = desc.name();
- EXPECT_EQ((desc.name() != "Framerate3"), desc.counter().is_incremental());
+ counter_names[desc.uuid()] =
+ desc.has_name() ? desc.name() : desc.static_name();
+ EXPECT_EQ((desc.static_name() != "Framerate3"),
+ desc.counter().is_incremental());
}
if (packet.has_track_event()) {
auto event = packet.track_event();
@@ -5869,7 +5871,8 @@
continue;
}
auto desc = packet.track_descriptor();
- counter_names[desc.uuid()] = desc.name();
+ counter_names[desc.uuid()] =
+ desc.has_name() ? desc.name() : desc.static_name();
if (desc.name() == "Framerate") {
EXPECT_EQ("fps", desc.counter().unit_name());
} else if (desc.name() == "Goats teleported") {
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index 02e6e03..dc02609 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -118,8 +118,13 @@
protos::gen::TrackDescriptor CounterTrack::Serialize() const {
auto desc = Track::Serialize();
- desc.set_name(name_);
auto* counter = desc.mutable_counter();
+ if (static_name_) {
+ desc.set_static_name(static_name_.value);
+ } else {
+ desc.set_name(dynamic_name_.value);
+ }
+
if (category_)
counter->add_categories(category_);
if (unit_ != perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED)
diff --git a/src/tracing/track_event_state_tracker.cc b/src/tracing/track_event_state_tracker.cc
index 1574292..e65d86f 100644
--- a/src/tracing/track_event_state_tracker.cc
+++ b/src/tracing/track_event_state_tracker.cc
@@ -246,7 +246,11 @@
track.index = static_cast<uint32_t>(session_state->tracks.size() + 1);
track.uuid = track_descriptor.uuid();
- track.name = track_descriptor.name().ToStdString();
+ if (track_descriptor.has_name()) {
+ track.name = track_descriptor.name().ToStdString();
+ } else if (track_descriptor.has_static_name()) {
+ track.name = track_descriptor.static_name().ToStdString();
+ }
track.pid = 0;
track.tid = 0;
if (track_descriptor.has_process()) {
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 38e3ba2..45c972f 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -75,6 +75,7 @@
from diff_tests.parser.parsing.tests import Parsing
from diff_tests.parser.parsing.tests_debug_annotation import ParsingDebugAnnotation
from diff_tests.parser.parsing.tests_memory_counters import ParsingMemoryCounters
+from diff_tests.parser.parsing.tests_traced_stats import ParsingTracedStats
from diff_tests.parser.parsing.tests_rss_stats import ParsingRssStats
from diff_tests.parser.power.tests_energy_breakdown import PowerEnergyBreakdown
from diff_tests.parser.power.tests_entity_state_residency import EntityStateResidency
@@ -137,6 +138,7 @@
sys.path.pop()
+
def fetch_all_diff_tests(index_path: str) -> List['testing.TestCase']:
parser_tests = [
*AndroidBugreport(index_path, 'parser/android',
@@ -187,11 +189,11 @@
*SmokeJson(index_path, 'parser/smoke', 'SmokeJson').fetch(),
*SmokeSchedEvents(index_path, 'parser/smoke', 'SmokeSchedEvents').fetch(),
*InputMethodClients(index_path, 'parser/android',
- 'InputMethodClients').fetch(),
+ 'InputMethodClients').fetch(),
*InputMethodManagerService(index_path, 'parser/android',
- 'InputMethodManagerService').fetch(),
+ 'InputMethodManagerService').fetch(),
*InputMethodService(index_path, 'parser/android',
- 'InputMethodService').fetch(),
+ 'InputMethodService').fetch(),
*SurfaceFlingerLayers(index_path, 'parser/android',
'SurfaceFlingerLayers').fetch(),
*SurfaceFlingerTransactions(index_path, 'parser/android',
@@ -212,6 +214,8 @@
*ParsingMemoryCounters(index_path, 'parser/parsing',
'ParsingMemoryCounters').fetch(),
*FtraceCrop(index_path, 'parser/ftrace', 'FtraceCrop').fetch(),
+ *ParsingTracedStats(index_path, 'parser/parsing',
+ 'ParsingTracedStats').fetch(),
]
metrics_tests = [
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
new file mode 100644
index 0000000..af2f362
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+# 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 python.generators.diff_tests.testing import Csv, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ParsingTracedStats(TestSuite):
+ # Check that `previous_packed_dropped: true` maps to
+ # `traced_buf_sequence_packet_loss`.
+ def test_sequence_packet_loss(self):
+ return DiffTestBlueprint(
+ trace=TextProto(r"""
+ packet {
+ trusted_packet_sequence_id: 2
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 2
+ }
+ packet {
+ trusted_packet_sequence_id: 2
+ }
+ packet {
+ trusted_packet_sequence_id: 3
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 3
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 3
+ }
+ packet {
+ trusted_packet_sequence_id: 4
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 4
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 4
+ }
+ packet {
+ trusted_packet_sequence_id: 5
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 5
+ previous_packet_dropped: true
+ }
+ packet {
+ trusted_packet_sequence_id: 5
+ }
+ packet {
+ trusted_uid: 9999
+ trusted_packet_sequence_id: 1
+ trace_stats {
+ writer_stats {
+ sequence_id: 2
+ buffer: 0
+ }
+ writer_stats {
+ sequence_id: 3
+ buffer: 1
+ }
+ writer_stats {
+ sequence_id: 4
+ buffer: 2
+ }
+ writer_stats {
+ sequence_id: 5
+ buffer: 2
+ }
+ }
+ }
+ """),
+ query="""
+ SELECT idx, value
+ FROM stats
+ WHERE name = 'traced_buf_sequence_packet_loss'
+ ORDER BY idx;
+ """,
+ out=Csv("""
+ "idx","value"
+ 0,0
+ 1,1
+ 2,2
+ """))
diff --git a/test/trace_processor/diff_tests/syntax/table_tests.py b/test/trace_processor/diff_tests/syntax/table_tests.py
index 7de63e9..5785b6f 100644
--- a/test/trace_processor/diff_tests/syntax/table_tests.py
+++ b/test/trace_processor/diff_tests/syntax/table_tests.py
@@ -178,6 +178,37 @@
3073,8,4529,8
"""))
+ def test_distinct_multi_column(self):
+ return DiffTestBlueprint(
+ trace=TextProto(''),
+ query="""
+ CREATE PERFETTO TABLE foo AS
+ WITH data(a, b) AS (
+ VALUES
+ -- Needed to defeat any id/sorted detection.
+ (2, 3),
+ (0, 2),
+ (0, 1)
+ )
+ SELECT * FROM data;
+
+ CREATE TABLE bar AS
+ SELECT 1 AS b;
+
+ WITH multi_col_distinct AS (
+ SELECT DISTINCT a FROM foo CROSS JOIN bar USING (b)
+ ), multi_col_group_by AS (
+ SELECT a FROM foo CROSS JOIN bar USING (b) GROUP BY a
+ )
+ SELECT
+ (SELECT COUNT(*) FROM multi_col_distinct) AS cnt_distinct,
+ (SELECT COUNT(*) FROM multi_col_group_by) AS cnt_group_by
+ """,
+ out=Csv("""
+ "cnt_distinct","cnt_group_by"
+ 1,1
+ """))
+
def test_limit(self):
return DiffTestBlueprint(
trace=TextProto(''),
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index a0d92ed..2aa0b7f 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -137,7 +137,7 @@
]
},
'config': {
- 'types': ['lite'],
+ 'types': ['filegroup'],
'targets': [
'//protos/perfetto/config:source_set',
]
diff --git a/ui/src/assets/brand.png b/ui/src/assets/brand.png
index dc6f8b6..63fe6f3 100644
--- a/ui/src/assets/brand.png
+++ b/ui/src/assets/brand.png
Binary files differ
diff --git a/ui/src/assets/favicon.png b/ui/src/assets/favicon.png
index 837b75b..2844520 100644
--- a/ui/src/assets/favicon.png
+++ b/ui/src/assets/favicon.png
Binary files differ
diff --git a/ui/src/assets/logo-128.png b/ui/src/assets/logo-128.png
index 43a2cd1..6d13da5 100644
--- a/ui/src/assets/logo-128.png
+++ b/ui/src/assets/logo-128.png
Binary files differ
diff --git a/ui/src/assets/logo-3d.png b/ui/src/assets/logo-3d.png
index b832ae6..9528c48 100644
--- a/ui/src/assets/logo-3d.png
+++ b/ui/src/assets/logo-3d.png
Binary files differ
diff --git a/ui/src/assets/rec_atrace.png b/ui/src/assets/rec_atrace.png
index d63f2a2..9a6baa2 100644
--- a/ui/src/assets/rec_atrace.png
+++ b/ui/src/assets/rec_atrace.png
Binary files differ
diff --git a/ui/src/assets/rec_battery_counters.png b/ui/src/assets/rec_battery_counters.png
index b73603d..3f1557b 100644
--- a/ui/src/assets/rec_battery_counters.png
+++ b/ui/src/assets/rec_battery_counters.png
Binary files differ
diff --git a/ui/src/assets/rec_board_voltage.png b/ui/src/assets/rec_board_voltage.png
index bc4dd12..d5f5b42 100644
--- a/ui/src/assets/rec_board_voltage.png
+++ b/ui/src/assets/rec_board_voltage.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_coarse.png b/ui/src/assets/rec_cpu_coarse.png
index b7241bf..9296a19 100644
--- a/ui/src/assets/rec_cpu_coarse.png
+++ b/ui/src/assets/rec_cpu_coarse.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_fine.png b/ui/src/assets/rec_cpu_fine.png
index 3c8df8b..6d069c2 100644
--- a/ui/src/assets/rec_cpu_fine.png
+++ b/ui/src/assets/rec_cpu_fine.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_freq.png b/ui/src/assets/rec_cpu_freq.png
index 23d86f8..5bd9e7b 100644
--- a/ui/src/assets/rec_cpu_freq.png
+++ b/ui/src/assets/rec_cpu_freq.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_voltage.png b/ui/src/assets/rec_cpu_voltage.png
index d82d5f3..7e31de9 100644
--- a/ui/src/assets/rec_cpu_voltage.png
+++ b/ui/src/assets/rec_cpu_voltage.png
Binary files differ
diff --git a/ui/src/assets/rec_frame_timeline.png b/ui/src/assets/rec_frame_timeline.png
index 2c83762..da98c40 100644
--- a/ui/src/assets/rec_frame_timeline.png
+++ b/ui/src/assets/rec_frame_timeline.png
Binary files differ
diff --git a/ui/src/assets/rec_ftrace.png b/ui/src/assets/rec_ftrace.png
index a907f9e..e956d05 100644
--- a/ui/src/assets/rec_ftrace.png
+++ b/ui/src/assets/rec_ftrace.png
Binary files differ
diff --git a/ui/src/assets/rec_gpu_mem_total.png b/ui/src/assets/rec_gpu_mem_total.png
index 4b5a44a..537ce9f 100644
--- a/ui/src/assets/rec_gpu_mem_total.png
+++ b/ui/src/assets/rec_gpu_mem_total.png
Binary files differ
diff --git a/ui/src/assets/rec_java_heap_dump.png b/ui/src/assets/rec_java_heap_dump.png
index 229ebe0..e3ee7c4 100644
--- a/ui/src/assets/rec_java_heap_dump.png
+++ b/ui/src/assets/rec_java_heap_dump.png
Binary files differ
diff --git a/ui/src/assets/rec_lmk.png b/ui/src/assets/rec_lmk.png
index 7324cf9..3a42b2d 100644
--- a/ui/src/assets/rec_lmk.png
+++ b/ui/src/assets/rec_lmk.png
Binary files differ
diff --git a/ui/src/assets/rec_logcat.png b/ui/src/assets/rec_logcat.png
index b3b4905..dda6526 100644
--- a/ui/src/assets/rec_logcat.png
+++ b/ui/src/assets/rec_logcat.png
Binary files differ
diff --git a/ui/src/assets/rec_long_trace.png b/ui/src/assets/rec_long_trace.png
index 23aad0b..f801756 100644
--- a/ui/src/assets/rec_long_trace.png
+++ b/ui/src/assets/rec_long_trace.png
Binary files differ
diff --git a/ui/src/assets/rec_mem_hifreq.png b/ui/src/assets/rec_mem_hifreq.png
index f36ef3e..f2909f8 100644
--- a/ui/src/assets/rec_mem_hifreq.png
+++ b/ui/src/assets/rec_mem_hifreq.png
Binary files differ
diff --git a/ui/src/assets/rec_meminfo.png b/ui/src/assets/rec_meminfo.png
index 4280675..43f968e 100644
--- a/ui/src/assets/rec_meminfo.png
+++ b/ui/src/assets/rec_meminfo.png
Binary files differ
diff --git a/ui/src/assets/rec_native_heap_profiler.png b/ui/src/assets/rec_native_heap_profiler.png
index bb3b010..4915c2b 100644
--- a/ui/src/assets/rec_native_heap_profiler.png
+++ b/ui/src/assets/rec_native_heap_profiler.png
Binary files differ
diff --git a/ui/src/assets/rec_one_shot.png b/ui/src/assets/rec_one_shot.png
index 7539c72..8981075 100644
--- a/ui/src/assets/rec_one_shot.png
+++ b/ui/src/assets/rec_one_shot.png
Binary files differ
diff --git a/ui/src/assets/rec_profiling.png b/ui/src/assets/rec_profiling.png
index 385670c..024d9f5 100644
--- a/ui/src/assets/rec_profiling.png
+++ b/ui/src/assets/rec_profiling.png
Binary files differ
diff --git a/ui/src/assets/rec_ps_stats.png b/ui/src/assets/rec_ps_stats.png
index e37bab1..df02761 100644
--- a/ui/src/assets/rec_ps_stats.png
+++ b/ui/src/assets/rec_ps_stats.png
Binary files differ
diff --git a/ui/src/assets/rec_ring_buf.png b/ui/src/assets/rec_ring_buf.png
index a2490fa..9bbe231 100644
--- a/ui/src/assets/rec_ring_buf.png
+++ b/ui/src/assets/rec_ring_buf.png
Binary files differ
diff --git a/ui/src/assets/rec_syscalls.png b/ui/src/assets/rec_syscalls.png
index 734854a..90b4ad8 100644
--- a/ui/src/assets/rec_syscalls.png
+++ b/ui/src/assets/rec_syscalls.png
Binary files differ
diff --git a/ui/src/assets/rec_vmstat.png b/ui/src/assets/rec_vmstat.png
index 58a4e71..46a1006 100644
--- a/ui/src/assets/rec_vmstat.png
+++ b/ui/src/assets/rec_vmstat.png
Binary files differ
diff --git a/ui/src/assets/scheduling_latency.png b/ui/src/assets/scheduling_latency.png
index 36bcfb8..8b5a2e1 100644
--- a/ui/src/assets/scheduling_latency.png
+++ b/ui/src/assets/scheduling_latency.png
Binary files differ
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index 41a201f..d3c5f4b 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -19,3 +19,8 @@
export function exists<T>(value: T): value is NonNullable<T> {
return value !== undefined && value !== null;
}
+
+// Generic result type - similar to Rust's Result<T, E>
+export type Result<T, E = {}> =
+ | {success: true; result: T}
+ | {success: false; error: E};
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 4cebecb..15d8f6c 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -253,15 +253,15 @@
// the reducer.
args: {
name: string;
- id: string;
+ key: string;
summaryTrackKey?: string;
collapsed: boolean;
fixedOrdering?: boolean;
},
): void {
- state.trackGroups[args.id] = {
+ state.trackGroups[args.key] = {
name: args.name,
- id: args.id,
+ key: args.key,
collapsed: args.collapsed,
tracks: [],
summaryTrack: args.summaryTrackKey,
@@ -394,12 +394,8 @@
}
},
- toggleTrackGroupCollapsed(
- state: StateDraft,
- args: {trackGroupId: string},
- ): void {
- const id = args.trackGroupId;
- const trackGroup = assertExists(state.trackGroups[id]);
+ toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void {
+ const trackGroup = assertExists(state.trackGroups[args.groupKey]);
trackGroup.collapsed = !trackGroup.collapsed;
},
@@ -448,31 +444,6 @@
}
},
- createPermalink(state: StateDraft, args: {isRecordingConfig: boolean}): void {
- state.permalink = {
- requestId: generateNextId(state),
- hash: undefined,
- isRecordingConfig: args.isRecordingConfig,
- };
- },
-
- setPermalink(
- state: StateDraft,
- args: {requestId: string; hash: string},
- ): void {
- // Drop any links for old requests.
- if (state.permalink.requestId !== args.requestId) return;
- state.permalink = args;
- },
-
- loadPermalink(state: StateDraft, args: {hash: string}): void {
- state.permalink = {requestId: generateNextId(state), hash: args.hash};
- },
-
- clearPermalink(state: StateDraft, _: {}): void {
- state.permalink = {};
- },
-
updateStatus(state: StateDraft, args: Status): void {
if (statusTraceEvent) {
traceEventEnd(statusTraceEvent);
@@ -920,7 +891,7 @@
toggleTrackSelection(
state: StateDraft,
- args: {id: string; isTrackGroup: boolean},
+ args: {key: string; isTrackGroup: boolean},
) {
const selection = state.selection;
if (
@@ -930,12 +901,12 @@
return;
}
const areaId = selection.legacySelection.areaId;
- const index = state.areas[areaId].tracks.indexOf(args.id);
+ const index = state.areas[areaId].tracks.indexOf(args.key);
if (index > -1) {
state.areas[areaId].tracks.splice(index, 1);
if (args.isTrackGroup) {
// Also remove all child tracks.
- for (const childTrack of state.trackGroups[args.id].tracks) {
+ for (const childTrack of state.trackGroups[args.key].tracks) {
const childIndex = state.areas[areaId].tracks.indexOf(childTrack);
if (childIndex > -1) {
state.areas[areaId].tracks.splice(childIndex, 1);
@@ -943,10 +914,10 @@
}
}
} else {
- state.areas[areaId].tracks.push(args.id);
+ state.areas[areaId].tracks.push(args.key);
if (args.isTrackGroup) {
// Also add all child tracks.
- for (const childTrack of state.trackGroups[args.id].tracks) {
+ for (const childTrack of state.trackGroups[args.key].tracks) {
if (!state.areas[areaId].tracks.includes(childTrack)) {
state.areas[areaId].tracks.push(childTrack);
}
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index de497d7..734ac91 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -60,14 +60,14 @@
function fakeTrackGroup(
state: State,
- args: {id: string; summaryTrackId: string},
+ args: {key: string; summaryTrackKey: string},
): State {
return produce(state, (draft) => {
StateActions.addTrackGroup(draft, {
name: 'A group',
- id: args.id,
+ key: args.key,
collapsed: false,
- summaryTrackKey: args.summaryTrackId,
+ summaryTrackKey: args.summaryTrackKey,
});
});
}
@@ -117,7 +117,7 @@
const afterGroup = produce(state, (draft) => {
StateActions.addTrackGroup(draft, {
name: 'A track group',
- id: '123-123-123',
+ key: '123-123-123',
summaryTrackKey: 's',
collapsed: false,
});
@@ -151,19 +151,19 @@
});
});
- const firstTrackId = once.scrollingTracks[0];
- const secondTrackId = once.scrollingTracks[1];
+ const firstTrackKey = once.scrollingTracks[0];
+ const secondTrackKey = once.scrollingTracks[1];
const twice = produce(once, (draft) => {
StateActions.moveTrack(draft, {
- srcId: `${firstTrackId}`,
+ srcId: `${firstTrackKey}`,
op: 'after',
- dstId: `${secondTrackId}`,
+ dstId: `${secondTrackKey}`,
});
});
- expect(twice.scrollingTracks[0]).toBe(secondTrackId);
- expect(twice.scrollingTracks[1]).toBe(firstTrackId);
+ expect(twice.scrollingTracks[0]).toBe(secondTrackKey);
+ expect(twice.scrollingTracks[1]).toBe(firstTrackKey);
});
test('reorder pinned to scrolling', () => {
@@ -326,7 +326,7 @@
test('sortTracksByPriority', () => {
let state = createEmptyState();
- state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+ state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
state = fakeTrack(state, {
key: 'b',
uri: HEAP_PROFILE_TRACK_KIND,
@@ -351,7 +351,7 @@
test('sortTracksByPriorityAndKindAndName', () => {
let state = createEmptyState();
- state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+ state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
state = fakeTrack(state, {
key: 'a',
uri: PROCESS_SCHEDULING_TRACK_KIND,
@@ -416,7 +416,7 @@
test('sortTracksByTidThenName', () => {
let state = createEmptyState();
- state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'a'});
+ state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'a'});
state = fakeTrack(state, {
key: 'a',
uri: SLICE_TRACK_KIND,
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index e3ed2b7..c513d73 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -95,7 +95,6 @@
scrollingTracks: [],
areas: {},
queries: {},
- permalink: {},
notes: {},
recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get()
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 3a5899e..f729597 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -17,7 +17,7 @@
import {Disposable, Trash} from '../base/disposable';
import {Registry} from '../base/registry';
import {Span, duration, time} from '../base/time';
-import {globals} from '../frontend/globals';
+import {TraceContext, globals} from '../frontend/globals';
import {
Command,
DetailsPanel,
@@ -45,6 +45,7 @@
import {raf} from '../core/raf_scheduler';
import {defaultPlugins} from '../core/default_plugins';
import {HighPrecisionTimeSpan} from './high_precision_time';
+import {PromptOption} from '../frontend/omnibox_manager';
// Every plugin gets its own PluginContext. This is how we keep track
// what each plugin is doing and how we can blame issues on particular
@@ -275,10 +276,10 @@
};
return predicate(ref);
})
- .map((group) => group.id);
+ .map((group) => group.key);
- for (const trackGroupId of groupsToExpand) {
- globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+ for (const groupKey of groupsToExpand) {
+ globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
}
},
@@ -293,10 +294,10 @@
};
return predicate(ref);
})
- .map((group) => group.id);
+ .map((group) => group.key);
- for (const trackGroupId of groupsToCollapse) {
- globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+ for (const groupKey of groupsToCollapse) {
+ globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
}
},
@@ -342,11 +343,16 @@
return globals.store.createSubStore(['plugins', this.pluginId], migrate);
}
- readonly trace = {
- get span(): Span<time, duration> {
- return globals.stateTraceTimeTP();
- },
- };
+ get trace(): TraceContext {
+ return globals.traceContext;
+ }
+
+ async prompt(
+ text: string,
+ options?: PromptOption[] | undefined,
+ ): Promise<string> {
+ return globals.omnibox.prompt(text, options);
+ }
}
function isPinned(trackId: string): boolean {
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
index a6c461b..227b9cd 100644
--- a/ui/src/common/queries.ts
+++ b/ui/src/common/queries.ts
@@ -40,48 +40,59 @@
params?: QueryRunParams,
): Promise<QueryResponse> {
const startMs = performance.now();
- const queryRes = engine.execute(sqlQuery);
// TODO(primiano): once the controller thread is gone we should pass down
// the result objects directly to the frontend, iterate over the result
// and deal with pagination there. For now we keep the old behavior and
// truncate to 10k rows.
- try {
- await queryRes.waitAllRows();
- } catch {
+ const maybeResult = await engine.tryQuery(sqlQuery);
+
+ if (maybeResult.success) {
+ const queryRes = maybeResult.result;
+ const convertNullsToString = params?.convertNullsToString ?? true;
+
+ const durationMs = performance.now() - startMs;
+ const rows: Row[] = [];
+ const columns = queryRes.columns();
+ let numRows = 0;
+ for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
+ const row: Row = {};
+ for (const colName of columns) {
+ const value = iter.get(colName);
+ row[colName] = value === null && convertNullsToString ? 'NULL' : value;
+ }
+ rows.push(row);
+ if (++numRows >= MAX_DISPLAY_ROWS) break;
+ }
+
+ const result: QueryResponse = {
+ query: sqlQuery,
+ durationMs,
+ error: queryRes.error(),
+ totalRowCount: queryRes.numRows(),
+ columns,
+ rows,
+ statementCount: queryRes.statementCount(),
+ statementWithOutputCount: queryRes.statementWithOutputCount(),
+ lastStatementSql: queryRes.lastStatementSql(),
+ };
+ return result;
+ } else {
// In the case of a query error we don't want the exception to bubble up
// as a crash. The |queryRes| object will be populated anyways.
// queryRes.error() is used to tell if the query errored or not. If it
// errored, the frontend will show a graceful message instead.
+ return {
+ query: sqlQuery,
+ durationMs: performance.now() - startMs,
+ error: maybeResult.error.message,
+ totalRowCount: 0,
+ columns: [],
+ rows: [],
+ statementCount: 0,
+ statementWithOutputCount: 0,
+ lastStatementSql: '',
+ };
}
-
- const convertNullsToString = params?.convertNullsToString ?? true;
-
- const durationMs = performance.now() - startMs;
- const rows: Row[] = [];
- const columns = queryRes.columns();
- let numRows = 0;
- for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
- const row: Row = {};
- for (const colName of columns) {
- const value = iter.get(colName);
- row[colName] = value === null && convertNullsToString ? 'NULL' : value;
- }
- rows.push(row);
- if (++numRows >= MAX_DISPLAY_ROWS) break;
- }
-
- const result: QueryResponse = {
- query: sqlQuery,
- durationMs,
- error: queryRes.error(),
- totalRowCount: queryRes.numRows(),
- columns,
- rows,
- statementCount: queryRes.statementCount(),
- statementWithOutputCount: queryRes.statementWithOutputCount(),
- lastStatementSql: queryRes.lastStatementSql(),
- };
- return result;
}
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index fc3018e..94eb429 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -151,7 +151,8 @@
// 52. Update track group state - don't make the summary track the first track.
// 53. Remove android log state.
// 54. Remove traceTime.
-export const STATE_VERSION = 54;
+// 55. Rename TrackGroupState.id -> TrackGroupState.key.
+export const STATE_VERSION = 55;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -288,7 +289,7 @@
}
export interface TrackGroupState {
- id: string;
+ key: string;
name: string;
collapsed: boolean;
tracks: string[]; // Child track ids.
@@ -310,12 +311,6 @@
query: string;
}
-export interface PermalinkConfig {
- requestId?: string; // Set by the frontend to request a new permalink.
- hash?: string; // Set by the controller when the link has been created.
- isRecordingConfig?: boolean; // this permalink request is for a recording config only
-}
-
export interface FrontendLocalState {
visibleState: VisibleState;
}
@@ -476,7 +471,7 @@
newEngineMode: NewEngineMode;
engine?: EngineConfig;
traceUuid?: string;
- trackGroups: ObjectById<TrackGroupState>;
+ trackGroups: ObjectByKey<TrackGroupState>;
tracks: ObjectByKey<TrackState>;
utidToThreadSortKey: UtidToTrackSortKey;
areas: ObjectById<AreaById>;
@@ -486,7 +481,6 @@
debugTrackId?: string;
lastTrackReloadRequest?: number;
queries: ObjectById<QueryConfig>;
- permalink: PermalinkConfig;
notes: ObjectById<Note | AreaNote>;
status: Status;
selection: Selection;
@@ -915,20 +909,19 @@
];
}
-export function getContainingTrackId(
+export function getContainingGroupKey(
state: State,
trackKey: string,
): null | string {
const track = state.tracks[trackKey];
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (!track) {
+ if (track === undefined) {
return null;
}
- const parentId = track.trackGroup;
- if (!parentId) {
+ const parentGroupKey = track.trackGroup;
+ if (!parentGroupKey) {
return null;
}
- return parentId;
+ return parentGroupKey;
}
export function getLegacySelection(state: State): LegacySelection | null {
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index be66340..ff111de 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -15,7 +15,7 @@
import {PrimaryTrackSortKey} from '../public';
import {createEmptyState} from './empty_state';
-import {getContainingTrackId, State} from './state';
+import {getContainingGroupKey, State} from './state';
import {deserializeStateObject, serializeStateObject} from './upload_utils';
test('createEmptyState', () => {
@@ -40,9 +40,9 @@
trackGroup: 'containsB',
};
- expect(getContainingTrackId(state, 'z')).toEqual(null);
- expect(getContainingTrackId(state, 'a')).toEqual(null);
- expect(getContainingTrackId(state, 'b')).toEqual('containsB');
+ expect(getContainingGroupKey(state, 'z')).toEqual(null);
+ expect(getContainingGroupKey(state, 'a')).toEqual(null);
+ expect(getContainingGroupKey(state, 'b')).toEqual('containsB');
});
test('state is serializable', () => {
diff --git a/ui/src/controller/app_controller.ts b/ui/src/controller/app_controller.ts
index 4c8ad85..7d4d7cd 100644
--- a/ui/src/controller/app_controller.ts
+++ b/ui/src/controller/app_controller.ts
@@ -16,7 +16,6 @@
import {globals} from '../frontend/globals';
import {Child, Controller, ControllerInitializerAny} from './controller';
-import {PermalinkController} from './permalink_controller';
import {RecordController} from './record_controller';
import {TraceController} from './trace_controller';
@@ -40,9 +39,7 @@
// - An internal promise of a nested controller being resolved and manually
// re-triggering the controllers.
run() {
- const childControllers: ControllerInitializerAny[] = [
- Child('permalink', PermalinkController, {}),
- ];
+ const childControllers: ControllerInitializerAny[] = [];
if (!RECORDING_V2_FLAG.get()) {
childControllers.push(
Child('record', RecordController, {extensionPort: this.extensionPort}),
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
deleted file mode 100644
index 1380ebf..0000000
--- a/ui/src/controller/permalink_controller.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-// Copyright (C) 2018 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 {produce} from 'immer';
-
-import {assertExists} from '../base/logging';
-import {runValidator} from '../base/validators';
-import {Actions} from '../common/actions';
-import {ConversionJobStatus} from '../common/conversion_jobs';
-import {
- createEmptyNonSerializableState,
- createEmptyState,
-} from '../common/empty_state';
-import {EngineConfig, ObjectById, State, STATE_VERSION} from '../common/state';
-import {
- BUCKET_NAME,
- buggyToSha256,
- deserializeStateObject,
- saveState,
- toSha256,
- TraceGcsUploader,
-} from '../common/upload_utils';
-import {globals} from '../frontend/globals';
-import {publishConversionJobStatusUpdate} from '../frontend/publish';
-import {Router} from '../frontend/router';
-
-import {Controller} from './controller';
-import {RecordConfig, recordConfigValidator} from './record_config_types';
-import {showModal} from '../widgets/modal';
-
-interface MultiEngineState {
- currentEngineId?: string;
- engines: ObjectById<EngineConfig>;
-}
-
-function isMultiEngineState(
- state: State | MultiEngineState,
-): state is MultiEngineState {
- if ((state as MultiEngineState).engines !== undefined) {
- return true;
- }
- return false;
-}
-
-export class PermalinkController extends Controller<'main'> {
- private lastRequestId?: string;
- constructor() {
- super('main');
- }
-
- run() {
- if (
- globals.state.permalink.requestId === undefined ||
- globals.state.permalink.requestId === this.lastRequestId
- ) {
- return;
- }
- const requestId = assertExists(globals.state.permalink.requestId);
- this.lastRequestId = requestId;
-
- // if the |hash| is not set, this is a request to create a permalink.
- if (globals.state.permalink.hash === undefined) {
- const isRecordingConfig = assertExists(
- globals.state.permalink.isRecordingConfig,
- );
-
- const jobName = 'create_permalink';
- publishConversionJobStatusUpdate({
- jobName,
- jobStatus: ConversionJobStatus.InProgress,
- });
-
- PermalinkController.createPermalink(isRecordingConfig)
- .then((hash) => {
- globals.dispatch(Actions.setPermalink({requestId, hash}));
- })
- .finally(() => {
- publishConversionJobStatusUpdate({
- jobName,
- jobStatus: ConversionJobStatus.NotRunning,
- });
- });
- return;
- }
-
- // Otherwise, this is a request to load the permalink.
- PermalinkController.loadState(globals.state.permalink.hash).then(
- (stateOrConfig) => {
- if (PermalinkController.isRecordConfig(stateOrConfig)) {
- // This permalink state only contains a RecordConfig. Show the
- // recording page with the config, but keep other state as-is.
- const validConfig = runValidator(
- recordConfigValidator,
- stateOrConfig as unknown,
- ).result;
- globals.dispatch(Actions.setRecordConfig({config: validConfig}));
- Router.navigate('#!/record');
- return;
- }
- globals.dispatch(Actions.setState({newState: stateOrConfig}));
- this.lastRequestId = stateOrConfig.permalink.requestId;
- },
- );
- }
-
- private static upgradeState(state: State): State {
- if (state.engine !== undefined && state.engine.source.type !== 'URL') {
- // All permalink traces should be modified to have a source.type=URL
- // pointing to the uploaded trace. Due to a bug in some older version
- // of the UI (b/327049372), an upload failure can end up with a state that
- // has type=FILE but a null file object. If this happens, invalidate the
- // trace and show a message.
- showModal({
- title: 'Cannot load trace permalink',
- content: m(
- 'div',
- 'The permalink stored on the server is corrupted ' +
- 'and cannot be loaded.',
- ),
- });
- return createEmptyState();
- }
-
- if (state.version !== STATE_VERSION) {
- const newState = createEmptyState();
- // Old permalinks from state versions prior to version 24
- // have multiple engines of which only one is identified as the
- // current engine via currentEngineId. Handle this case:
- if (isMultiEngineState(state)) {
- const engineId = state.currentEngineId;
- if (engineId !== undefined) {
- newState.engine = state.engines[engineId];
- }
- } else {
- newState.engine = state.engine;
- }
-
- if (newState.engine !== undefined) {
- newState.engine.ready = false;
- }
- const message =
- `Unable to parse old state version. Discarding state ` +
- `and loading trace.`;
- console.warn(message);
- PermalinkController.updateStatus(message);
- return newState;
- } else {
- // Loaded state is presumed to be compatible with the State type
- // definition in the app. However, a non-serializable part has to be
- // recreated.
- state.nonSerializableState = createEmptyNonSerializableState();
- }
- return state;
- }
-
- private static isRecordConfig(
- stateOrConfig: State | RecordConfig,
- ): stateOrConfig is RecordConfig {
- const mode = (stateOrConfig as {mode?: string}).mode;
- return (
- mode !== undefined &&
- ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode)
- );
- }
-
- private static async createPermalink(
- isRecordingConfig: boolean,
- ): Promise<string> {
- let uploadState: State | RecordConfig = globals.state;
-
- if (isRecordingConfig) {
- uploadState = globals.state.recordConfig;
- } else {
- const engine = assertExists(globals.getCurrentEngine());
- let dataToUpload: File | ArrayBuffer | undefined = undefined;
- let traceName = `trace ${engine.id}`;
- if (engine.source.type === 'FILE') {
- dataToUpload = engine.source.file;
- traceName = dataToUpload.name;
- } else if (engine.source.type === 'ARRAY_BUFFER') {
- dataToUpload = engine.source.buffer;
- } else if (engine.source.type !== 'URL') {
- throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
- }
-
- if (dataToUpload !== undefined) {
- PermalinkController.updateStatus(`Uploading ${traceName}`);
- const uploader = new TraceGcsUploader(dataToUpload, () => {
- switch (uploader.state) {
- case 'UPLOADING':
- const statusTxt = `Uploading ${uploader.getEtaString()}`;
- PermalinkController.updateStatus(statusTxt);
- break;
- case 'UPLOADED':
- // Convert state to use URLs and remove permalink.
- const url = uploader.uploadedUrl;
- uploadState = produce(globals.state, (draft) => {
- assertExists(draft.engine).source = {type: 'URL', url};
- draft.permalink = {};
- });
- break;
- case 'ERROR':
- PermalinkController.updateStatus(
- `Upload failed ${uploader.error}`,
- );
- break;
- } // switch (state)
- }); // onProgress
- await uploader.waitForCompletion();
- }
- }
-
- // Upload state.
- PermalinkController.updateStatus(`Creating permalink...`);
- const hash = await saveState(uploadState);
- PermalinkController.updateStatus(`Permalink ready`);
- return hash;
- }
-
- private static async loadState(id: string): Promise<State | RecordConfig> {
- const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
- const response = await fetch(url);
- if (!response.ok) {
- throw new Error(
- `Could not fetch permalink.\n` +
- `Are you sure the id (${id}) is correct?\n` +
- `URL: ${url}`,
- );
- }
- const text = await response.text();
- const stateHash = await toSha256(text);
- const state = deserializeStateObject<State>(text);
- if (stateHash !== id) {
- // Old permalinks incorrectly dropped some digits from the
- // hexdigest of the SHA256. We don't want to invalidate those
- // links so we also compute the old string and try that here
- // also.
- const buggyStateHash = await buggyToSha256(text);
- if (buggyStateHash !== id) {
- throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
- }
- }
- if (!this.isRecordConfig(state)) {
- return this.upgradeState(state);
- }
- return state;
- }
-
- private static updateStatus(msg: string): void {
- // TODO(hjd): Unify loading updates.
- globals.dispatch(
- Actions.updateStatus({
- msg,
- timestamp: Date.now() / 1000,
- }),
- );
- }
-}
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index ba2d354..bd6a3b1 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -163,7 +163,7 @@
utids.push(it.utid);
}
- const cpus = await this.engine.getCpus();
+ const cpus = globals.traceContext.cpus;
const maxCpu = Math.max(...cpus, -1);
const res = await this.query(`
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 1b6dd0f..0a58c43 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -441,7 +441,7 @@
IFNULL(value, 0) as value
FROM counter WHERE ts < ${ts} and track_id = ${trackId}`);
const previousValue = previous.firstRow({value: NUM}).value;
- const endTs = rightTs !== -1n ? rightTs : globals.traceTime.end;
+ const endTs = rightTs !== -1n ? rightTs : globals.traceContext.end;
const delta = value - previousValue;
const duration = endTs - ts;
const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 445bd4a..a3e6a59 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -29,11 +29,11 @@
import {EngineMode, PendingDeeplinkState, ProfileType} from '../common/state';
import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags';
import {
- defaultTraceTime,
+ defaultTraceContext,
globals,
QuantizedLoad,
ThreadDesc,
- TraceTime,
+ TraceContext,
} from '../frontend/globals';
import {
clearOverviewData,
@@ -41,7 +41,7 @@
publishMetricError,
publishOverviewData,
publishThreads,
- publishTraceDetails,
+ publishTraceContext,
} from '../frontend/publish';
import {addQueryResultsTab} from '../frontend/query_result_tab';
import {Router} from '../frontend/router';
@@ -452,7 +452,7 @@
const traceUuid = await this.cacheCurrentTrace();
const traceDetails = await getTraceTimeDetails(this.engine);
- publishTraceDetails(traceDetails);
+ publishTraceContext(traceDetails);
const shownJsonWarning =
window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
@@ -1100,8 +1100,8 @@
// if we have non-default visible state, update the visible time to it
const previousVisibleState = globals.stateVisibleTime();
const defaultTraceSpan = new TimeSpan(
- defaultTraceTime.start,
- defaultTraceTime.end,
+ defaultTraceContext.start,
+ defaultTraceContext.end,
);
if (
!(
@@ -1122,7 +1122,7 @@
let visibleEnd = traceEnd;
// compare start and end with metadata computed by the trace processor
- const mdTime = await engine.getTracingMetadataTimeBounds();
+ const mdTime = await getTracingMetadataTimeBounds(engine);
// make sure the bounds hold
if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) {
visibleStart = Time.max(visibleStart, mdTime.start);
@@ -1145,8 +1145,8 @@
return HighPrecisionTimeSpan.fromTime(visibleStart, visibleEnd);
}
-async function getTraceTimeDetails(engine: EngineBase): Promise<TraceTime> {
- const traceTime = await engine.getTraceTimeBounds();
+async function getTraceTimeDetails(engine: Engine): Promise<TraceContext> {
+ const traceTime = await getTraceTimeBounds(engine);
// Find the first REALTIME or REALTIME_COARSE clock snapshot.
// Prioritize REALTIME over REALTIME_COARSE.
@@ -1216,5 +1216,67 @@
realtimeOffset,
utcOffset,
traceTzOffset,
+ cpus: await getCpus(engine),
+ gpuCount: await getNumberOfGpus(engine),
};
}
+
+async function getTraceTimeBounds(
+ engine: Engine,
+): Promise<Span<time, duration>> {
+ const result = await engine.query(
+ `select start_ts as startTs, end_ts as endTs from trace_bounds`,
+ );
+ const bounds = result.firstRow({
+ startTs: LONG,
+ endTs: LONG,
+ });
+ return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs));
+}
+
+// TODO(hjd): When streaming must invalidate this somehow.
+async function getCpus(engine: Engine): Promise<number[]> {
+ const cpus = [];
+ const queryRes = await engine.query(
+ 'select distinct(cpu) as cpu from sched order by cpu;',
+ );
+ for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+ cpus.push(it.cpu);
+ }
+ return cpus;
+}
+
+async function getNumberOfGpus(engine: Engine): Promise<number> {
+ const result = await engine.query(`
+ select count(distinct(gpu_id)) as gpuCount
+ from gpu_counter_track
+ where name = 'gpufreq';
+ `);
+ return result.firstRow({gpuCount: NUM}).gpuCount;
+}
+
+async function getTracingMetadataTimeBounds(
+ engine: Engine,
+): Promise<Span<time, duration>> {
+ const queryRes = await engine.query(`select
+ name,
+ int_value as intValue
+ from metadata
+ where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
+ or name = 'all_data_source_started_ns'`);
+ let startBound = Time.MIN;
+ let endBound = Time.MAX;
+ const it = queryRes.iter({name: STR, intValue: LONG_NULL});
+ for (; it.valid(); it.next()) {
+ const columnName = it.name;
+ const timestamp = it.intValue;
+ if (timestamp === null) continue;
+ if (columnName === 'tracing_disabled_ns') {
+ endBound = Time.min(endBound, Time.fromRaw(timestamp));
+ } else {
+ startBound = Time.max(startBound, Time.fromRaw(timestamp));
+ }
+ }
+
+ return new TimeSpan(startBound, endBound);
+}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index e1b907c..a9b186d 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -118,7 +118,7 @@
}
async addCpuSchedulingTracks(): Promise<void> {
- const cpus = await this.engine.getCpus();
+ const cpus = globals.traceContext.cpus;
const cpuToSize = await this.guessCpuSizes();
for (const cpu of cpus) {
@@ -134,7 +134,7 @@
}
async addCpuFreqTracks(engine: Engine): Promise<void> {
- const cpus = await this.engine.getCpus();
+ const cpus = globals.traceContext.cpus;
for (const cpu of cpus) {
// Only add a cpu freq track if we have
@@ -189,38 +189,38 @@
parentName: STR_NULL,
});
- const parentIdToGroupId = new Map<number, string>();
+ const parentIdToGroupKey = new Map<number, string>();
for (; it.valid(); it.next()) {
const kind = ASYNC_SLICE_TRACK_KIND;
const rawName = it.name === null ? undefined : it.name;
const rawParentName = it.parentName === null ? undefined : it.parentName;
const name = getTrackName({name: rawName, kind});
const parentTrackId = it.parentId;
- let trackGroup = SCROLLING_TRACK_GROUP;
+ let groupKey = SCROLLING_TRACK_GROUP;
if (parentTrackId !== null) {
- const groupId = parentIdToGroupId.get(parentTrackId);
- if (groupId === undefined) {
- trackGroup = uuidv4();
- parentIdToGroupId.set(parentTrackId, trackGroup);
+ const maybeGroupKey = parentIdToGroupKey.get(parentTrackId);
+ if (maybeGroupKey === undefined) {
+ groupKey = uuidv4();
+ parentIdToGroupKey.set(parentTrackId, groupKey);
const parentName = getTrackName({name: rawParentName, kind});
this.addTrackGroupActions.push(
Actions.addTrackGroup({
name: parentName,
- id: trackGroup,
+ key: groupKey,
collapsed: true,
}),
);
} else {
- trackGroup = groupId;
+ groupKey = maybeGroupKey;
}
}
const track: AddTrackArgs = {
uri: `perfetto.AsyncSlices#${rawName}.${it.parentId}`,
trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
- trackGroup,
+ trackGroup: groupKey,
name,
};
@@ -229,7 +229,7 @@
}
async addGpuFreqTracks(engine: Engine): Promise<void> {
- const numGpus = await this.engine.getNumberOfGpus();
+ const numGpus = globals.traceContext.gpuCount;
for (let gpu = 0; gpu < numGpus; gpu++) {
// Only add a gpu freq track if we have
// gpu freq data.
@@ -315,7 +315,7 @@
return;
}
- const id = uuidv4();
+ const groupUuid = uuidv4();
const summaryTrackKey = uuidv4();
let foundSummary = false;
@@ -328,14 +328,14 @@
track.key = summaryTrackKey;
track.trackGroup = undefined;
} else {
- track.trackGroup = id;
+ track.trackGroup = groupUuid;
}
}
const addGroup = Actions.addTrackGroup({
summaryTrackKey,
name: MEM_DMA_COUNTER_NAME,
- id,
+ key: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -369,7 +369,7 @@
const groupName = group + key;
const addGroup = Actions.addTrackGroup({
name: groupName,
- id: value,
+ key: value,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -408,7 +408,7 @@
const groupName = key;
const addGroup = Actions.addTrackGroup({
name: groupName,
- id: value,
+ key: value,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -441,7 +441,7 @@
if (groupUuid !== undefined) {
const addGroup = Actions.addTrackGroup({
name: groupName,
- id: groupUuid,
+ key: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -483,7 +483,7 @@
if (groupUuid !== undefined) {
const addGroup = Actions.addTrackGroup({
name: groupName,
- id: groupUuid,
+ key: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -511,7 +511,7 @@
if (groupUuid !== undefined) {
const addGroup = Actions.addTrackGroup({
name: groupName,
- id: groupUuid,
+ key: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -537,7 +537,7 @@
summaryTrackKey: string;
}
- const groupNameToIds = new Map<string, GroupIds>();
+ const groupNameToKeys = new Map<string, GroupIds>();
for (; sliceIt.valid(); sliceIt.next()) {
const id = sliceIt.id;
@@ -553,13 +553,13 @@
// If this is the first track encountered for a certain group,
// create an id for the group and use this track as the group's
// summary track.
- const groupIds = groupNameToIds.get(groupName);
- if (groupIds) {
- trackGroupId = groupIds.id;
+ const groupKeys = groupNameToKeys.get(groupName);
+ if (groupKeys) {
+ trackGroupId = groupKeys.id;
} else {
trackGroupId = uuidv4();
summaryTrackKey = uuidv4();
- groupNameToIds.set(groupName, {
+ groupNameToKeys.set(groupName, {
id: trackGroupId,
summaryTrackKey,
});
@@ -575,11 +575,11 @@
});
}
- for (const [groupName, groupIds] of groupNameToIds) {
+ for (const [groupName, groupKeys] of groupNameToKeys) {
const addGroup = Actions.addTrackGroup({
- summaryTrackKey: groupIds.summaryTrackKey,
+ summaryTrackKey: groupKeys.summaryTrackKey,
name: groupName,
- id: groupIds.id,
+ key: groupKeys.id,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -843,7 +843,7 @@
for (const [name, groupUuid] of groupMap) {
const addGroup = Actions.addTrackGroup({
name: name,
- id: groupUuid,
+ key: groupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addGroup);
@@ -1153,7 +1153,7 @@
const addTrackGroup = Actions.addTrackGroup({
summaryTrackKey,
name: `Kernel threads`,
- id: kthreadGroupUuid,
+ key: kthreadGroupUuid,
collapsed: true,
});
this.addTrackGroupActions.push(addTrackGroup);
@@ -1320,7 +1320,7 @@
const addTrackGroup = Actions.addTrackGroup({
summaryTrackKey,
name,
- id: this.getOrCreateUuid(utid, upid),
+ key: this.getOrCreateUuid(utid, upid),
// Perf profiling tracks remain collapsed, otherwise we would have too
// many expanded process tracks for some perf traces, leading to
// jankyness.
@@ -1373,7 +1373,7 @@
groupUuid = uuidv4();
const addGroup = Actions.addTrackGroup({
name: groupName,
- id: groupUuid,
+ key: groupUuid,
collapsed: true,
fixedOrdering: true,
});
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 15e5d2c..4f5460e 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -44,7 +44,6 @@
'perfetto.CpuProfile',
'perfetto.CpuSlices',
'perfetto.CriticalUserInteraction',
- 'perfetto.CustomSqlTrack',
'perfetto.DebugSlices',
'perfetto.Flows',
'perfetto.Frames',
@@ -58,4 +57,5 @@
'perfetto.ThreadState',
'perfetto.VisualisedArgs',
'org.kernel.LinuxKernelDevices',
+ 'perfetto.TrackUtils',
];
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
index bda66f3..43ccb9d 100644
--- a/ui/src/core_plugins/annotation/index.ts
+++ b/ui/src/core_plugins/annotation/index.ts
@@ -18,7 +18,8 @@
SLICE_TRACK_KIND,
} from '../chrome_slices/chrome_slice_track';
import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
-import {COUNTER_TRACK_KIND, TraceProcessorCounterTrack} from '../counter';
+import {COUNTER_TRACK_KIND} from '../counter';
+import {TraceProcessorCounterTrack} from '../counter/trace_processor_counter_track';
class AnnotationPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/core_plugins/async_slices/async_slice_track_v2.ts b/ui/src/core_plugins/async_slices/async_slice_track.ts
similarity index 95%
rename from ui/src/core_plugins/async_slices/async_slice_track_v2.ts
rename to ui/src/core_plugins/async_slices/async_slice_track.ts
index 659a89a..275c418 100644
--- a/ui/src/core_plugins/async_slices/async_slice_track_v2.ts
+++ b/ui/src/core_plugins/async_slices/async_slice_track.ts
@@ -17,7 +17,7 @@
import {NewTrackArgs} from '../../frontend/track';
import {Slice} from '../../public';
-export class AsyncSliceTrackV2 extends NamedSliceTrack {
+export class AsyncSliceTrack extends NamedSliceTrack {
constructor(
args: NewTrackArgs,
maxDepth: number,
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index 644d22f..b01cb38 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -16,7 +16,7 @@
import {getTrackName} from '../../public/utils';
import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-import {AsyncSliceTrackV2} from './async_slice_track_v2';
+import {AsyncSliceTrack} from './async_slice_track';
export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
@@ -71,7 +71,7 @@
trackIds,
kind: ASYNC_SLICE_TRACK_KIND,
trackFactory: ({trackKey}) => {
- return new AsyncSliceTrackV2({engine, trackKey}, maxDepth, trackIds);
+ return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
},
});
}
@@ -123,7 +123,7 @@
trackIds,
kind: ASYNC_SLICE_TRACK_KIND,
trackFactory: ({trackKey}) => {
- return new AsyncSliceTrackV2(
+ return new AsyncSliceTrack(
{engine: ctx.engine, trackKey},
maxDepth,
trackIds,
@@ -190,7 +190,7 @@
trackIds,
kind: ASYNC_SLICE_TRACK_KIND,
trackFactory: ({trackKey}) => {
- return new AsyncSliceTrackV2({engine, trackKey}, maxDepth, trackIds);
+ return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
},
});
}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
new file mode 100644
index 0000000..9268750
--- /dev/null
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
@@ -0,0 +1,180 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
+import {globals} from '../../frontend/globals';
+import {
+ NAMED_ROW,
+ NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {NUM, Slice, STR} from '../../public';
+import {
+ CustomSqlDetailsPanelConfig,
+ CustomSqlImportConfig,
+ CustomSqlTableDefConfig,
+ CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+
+import {PageLoadDetailsPanel} from './page_load_details_panel';
+import {StartupDetailsPanel} from './startup_details_panel';
+import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+
+export const CRITICAL_USER_INTERACTIONS_KIND =
+ 'org.chromium.CriticalUserInteraction.track';
+
+export const CRITICAL_USER_INTERACTIONS_ROW = {
+ ...NAMED_ROW,
+ scopedId: NUM,
+ type: STR,
+};
+export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
+
+export interface CriticalUserInteractionSlice extends Slice {
+ scopedId: number;
+ type: string;
+}
+
+export interface CriticalUserInteractionSliceTrackTypes
+ extends NamedSliceTrackTypes {
+ slice: CriticalUserInteractionSlice;
+ row: CriticalUserInteractionRow;
+}
+
+enum CriticalUserInteractionType {
+ UNKNOWN = 'Unknown',
+ PAGE_LOAD = 'chrome_page_loads',
+ STARTUP = 'chrome_startups',
+ WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
+}
+
+function convertToCriticalUserInteractionType(
+ cujType: string,
+): CriticalUserInteractionType {
+ switch (cujType) {
+ case CriticalUserInteractionType.PAGE_LOAD:
+ return CriticalUserInteractionType.PAGE_LOAD;
+ case CriticalUserInteractionType.STARTUP:
+ return CriticalUserInteractionType.STARTUP;
+ case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
+ return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
+ default:
+ return CriticalUserInteractionType.UNKNOWN;
+ }
+}
+
+export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack<CriticalUserInteractionSliceTrackTypes> {
+ static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
+
+ getSqlDataSource(): CustomSqlTableDefConfig {
+ return {
+ columns: [
+ // The scoped_id is not a unique identifier within the table; generate
+ // a unique id from type and scoped_id on the fly to use for slice
+ // selection.
+ 'hash(type, scoped_id) AS id',
+ 'scoped_id AS scopedId',
+ 'name',
+ 'ts',
+ 'dur',
+ 'type',
+ ],
+ sqlTableName: 'chrome_interactions',
+ };
+ }
+
+ getDetailsPanel(
+ args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
+ ): CustomSqlDetailsPanelConfig {
+ let detailsPanel = {
+ kind: GenericSliceDetailsTab.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Chrome Interaction',
+ },
+ };
+
+ switch (convertToCriticalUserInteractionType(args.slice.type)) {
+ case CriticalUserInteractionType.PAGE_LOAD:
+ detailsPanel = {
+ kind: PageLoadDetailsPanel.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Chrome Page Load',
+ },
+ };
+ break;
+ case CriticalUserInteractionType.STARTUP:
+ detailsPanel = {
+ kind: StartupDetailsPanel.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Chrome Startup',
+ },
+ };
+ break;
+ case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
+ detailsPanel = {
+ kind: WebContentInteractionPanel.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Chrome Web Content Interaction',
+ },
+ };
+ break;
+ default:
+ break;
+ }
+ return detailsPanel;
+ }
+
+ onSliceClick(
+ args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
+ ) {
+ const detailsPanelConfig = this.getDetailsPanel(args);
+ globals.makeSelection(
+ Actions.selectGenericSlice({
+ id: args.slice.scopedId,
+ sqlTableName: this.tableName,
+ start: args.slice.ts,
+ duration: args.slice.dur,
+ trackKey: this.trackKey,
+ detailsPanelConfig: {
+ kind: detailsPanelConfig.kind,
+ config: detailsPanelConfig.config,
+ },
+ }),
+ );
+ }
+
+ getSqlImports(): CustomSqlImportConfig {
+ return {
+ modules: ['chrome.interactions'],
+ };
+ }
+
+ getRowSpec(): CriticalUserInteractionSliceTrackTypes['row'] {
+ return CRITICAL_USER_INTERACTIONS_ROW;
+ }
+
+ rowToSlice(
+ row: CriticalUserInteractionSliceTrackTypes['row'],
+ ): CriticalUserInteractionSliceTrackTypes['slice'] {
+ const baseSlice = super.rowToSlice(row);
+ const scopedId = row.scopedId;
+ const type = row.type;
+ return {...baseSlice, scopedId, type};
+ }
+}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index faa863b..b8d7050 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -16,186 +16,23 @@
import {Actions} from '../../common/actions';
import {SCROLLING_TRACK_GROUP} from '../../common/state';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {
- GenericSliceDetailsTab,
- GenericSliceDetailsTabConfig,
-} from '../../frontend/generic_slice_details_tab';
+import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
import {globals} from '../../frontend/globals';
import {
- NAMED_ROW,
- NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {
BottomTabToSCSAdapter,
- NUM,
Plugin,
PluginContext,
PluginContextTrace,
PluginDescriptor,
PrimaryTrackSortKey,
- Slice,
- STR,
} from '../../public';
-import {
- CustomSqlDetailsPanelConfig,
- CustomSqlImportConfig,
- CustomSqlTableDefConfig,
- CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
import {PageLoadDetailsPanel} from './page_load_details_panel';
import {StartupDetailsPanel} from './startup_details_panel';
import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
-export const CRITICAL_USER_INTERACTIONS_KIND =
- 'org.chromium.CriticalUserInteraction.track';
-
-export const CRITICAL_USER_INTERACTIONS_ROW = {
- ...NAMED_ROW,
- scopedId: NUM,
- type: STR,
-};
-export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
-
-export interface CriticalUserInteractionSlice extends Slice {
- scopedId: number;
- type: string;
-}
-
-export interface CriticalUserInteractionSliceTrackTypes
- extends NamedSliceTrackTypes {
- slice: CriticalUserInteractionSlice;
- row: CriticalUserInteractionRow;
-}
-
-enum CriticalUserInteractionType {
- UNKNOWN = 'Unknown',
- PAGE_LOAD = 'chrome_page_loads',
- STARTUP = 'chrome_startups',
- WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
-}
-
-function convertToCriticalUserInteractionType(
- cujType: string,
-): CriticalUserInteractionType {
- switch (cujType) {
- case CriticalUserInteractionType.PAGE_LOAD:
- return CriticalUserInteractionType.PAGE_LOAD;
- case CriticalUserInteractionType.STARTUP:
- return CriticalUserInteractionType.STARTUP;
- case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
- return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
- default:
- return CriticalUserInteractionType.UNKNOWN;
- }
-}
-
-export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack<CriticalUserInteractionSliceTrackTypes> {
- static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
-
- getSqlDataSource(): CustomSqlTableDefConfig {
- return {
- columns: [
- // The scoped_id is not a unique identifier within the table; generate
- // a unique id from type and scoped_id on the fly to use for slice
- // selection.
- 'hash(type, scoped_id) AS id',
- 'scoped_id AS scopedId',
- 'name',
- 'ts',
- 'dur',
- 'type',
- ],
- sqlTableName: 'chrome_interactions',
- };
- }
-
- getDetailsPanel(
- args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
- ): CustomSqlDetailsPanelConfig {
- let detailsPanel = {
- kind: GenericSliceDetailsTab.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Interaction',
- },
- };
-
- switch (convertToCriticalUserInteractionType(args.slice.type)) {
- case CriticalUserInteractionType.PAGE_LOAD:
- detailsPanel = {
- kind: PageLoadDetailsPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Page Load',
- },
- };
- break;
- case CriticalUserInteractionType.STARTUP:
- detailsPanel = {
- kind: StartupDetailsPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Startup',
- },
- };
- break;
- case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
- detailsPanel = {
- kind: WebContentInteractionPanel.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Chrome Web Content Interaction',
- },
- };
- break;
- default:
- break;
- }
- return detailsPanel;
- }
-
- onSliceClick(
- args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
- ) {
- const detailsPanelConfig = this.getDetailsPanel(args);
- globals.makeSelection(
- Actions.selectGenericSlice({
- id: args.slice.scopedId,
- sqlTableName: this.tableName,
- start: args.slice.ts,
- duration: args.slice.dur,
- trackKey: this.trackKey,
- detailsPanelConfig: {
- kind: detailsPanelConfig.kind,
- config: detailsPanelConfig.config,
- },
- }),
- );
- }
-
- getSqlImports(): CustomSqlImportConfig {
- return {
- modules: ['chrome.interactions'],
- };
- }
-
- getRowSpec(): CriticalUserInteractionSliceTrackTypes['row'] {
- return CRITICAL_USER_INTERACTIONS_ROW;
- }
-
- rowToSlice(
- row: CriticalUserInteractionSliceTrackTypes['row'],
- ): CriticalUserInteractionSliceTrackTypes['slice'] {
- const baseSlice = super.rowToSlice(row);
- const scopedId = row.scopedId;
- const type = row.type;
- return {...baseSlice, scopedId, type};
- }
-}
-
-export function addCriticalUserInteractionTrack() {
+function addCriticalUserInteractionTrack() {
const trackKey = uuidv4();
globals.dispatchMultiple([
Actions.addTrack({
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
index 8c20cc4..523880d 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/common.ts
@@ -15,7 +15,7 @@
import {AddTrackArgs} from '../../common/actions';
import {ObjectByKey} from '../../common/state';
import {featureFlags} from '../../core/feature_flags';
-import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
+import {CustomSqlDetailsPanelConfig} from '../../frontend/tracks/custom_sql_table_slice_track';
export const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
index 6d89daa..98f3dd3 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
@@ -20,7 +20,7 @@
CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
import {JANK_COLOR} from './jank_colors';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index 4267f4c..aae51c4 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -84,7 +84,7 @@
const addTrackGroup = Actions.addTrackGroup({
name: 'Chrome Scroll Jank',
- id: SCROLL_JANK_GROUP_ID,
+ key: SCROLL_JANK_GROUP_ID,
collapsed: false,
fixedOrdering: true,
});
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
index f775740..11d6f1f 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -20,7 +20,7 @@
CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
import {EventLatencyTrackTypes} from './event_latency_track';
import {JANK_COLOR} from './jank_colors';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
index 123d8f9..2d69139 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -19,7 +19,7 @@
CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
import {
DecideTracksResult,
SCROLL_JANK_GROUP_ID,
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
index 55773cd..7ac31d0 100644
--- a/ui/src/core_plugins/counter/index.ts
+++ b/ui/src/core_plugins/counter/index.ts
@@ -14,14 +14,10 @@
import m from 'mithril';
-import {Time} from '../../base/time';
-import {Actions} from '../../common/actions';
import {CounterDetailsPanel} from '../../frontend/counter_panel';
-import {globals} from '../../frontend/globals';
import {
NUM_NULL,
STR_NULL,
- LONG,
LONG_NULL,
NUM,
Plugin,
@@ -31,11 +27,8 @@
STR,
} from '../../public';
import {getTrackName} from '../../public/utils';
-import {
- BaseCounterTrack,
- BaseCounterTrackArgs,
- CounterOptions,
-} from '../../frontend/base_counter_track';
+import {CounterOptions} from '../../frontend/base_counter_track';
+import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
export const COUNTER_TRACK_KIND = 'CounterTrack';
@@ -112,82 +105,6 @@
return options;
}
-interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
- trackId: number;
- rootTable?: string;
-}
-
-export class TraceProcessorCounterTrack extends BaseCounterTrack {
- private trackId: number;
- private rootTable: string;
-
- constructor(args: TraceProcessorCounterTrackArgs) {
- super(args);
- this.trackId = args.trackId;
- this.rootTable = args.rootTable ?? 'counter';
- }
-
- getSqlSource() {
- return `select ts, value from ${this.rootTable} where track_id = ${this.trackId}`;
- }
-
- onMouseClick({x}: {x: number}): boolean {
- const {visibleTimeScale} = globals.timeline;
- const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
-
- const query = `
- select
- id,
- ts as leftTs,
- (
- select ts
- from ${this.rootTable}
- where
- track_id = ${this.trackId}
- and ts >= ${time}
- order by ts
- limit 1
- ) as rightTs
- from ${this.rootTable}
- where
- track_id = ${this.trackId}
- and ts < ${time}
- order by ts DESC
- limit 1
- `;
-
- this.engine.query(query).then((result) => {
- const it = result.iter({
- id: NUM,
- leftTs: LONG,
- rightTs: LONG_NULL,
- });
- if (!it.valid()) {
- return;
- }
- const trackKey = this.trackKey;
- const id = it.id;
- const leftTs = Time.fromRaw(it.leftTs);
-
- // TODO(stevegolton): Don't try to guess times and durations here, make it
- // obvious to the user that this counter sample has no duration as it's
- // the last one in the series
- const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
-
- globals.makeSelection(
- Actions.selectCounter({
- leftTs,
- rightTs,
- id,
- trackKey,
- }),
- );
- });
-
- return true;
- }
-}
-
class CounterPlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
await this.addCounterTracks(ctx);
@@ -421,7 +338,7 @@
private async addGpuFrequencyTracks(ctx: PluginContextTrace) {
const engine = ctx.engine;
- const numGpus = await engine.getNumberOfGpus();
+ const numGpus = ctx.trace.gpuCount;
for (let gpu = 0; gpu < numGpus; gpu++) {
// Only add a gpu freq track if we have
diff --git a/ui/src/core_plugins/counter/trace_processor_counter_track.ts b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
new file mode 100644
index 0000000..a80ac4b
--- /dev/null
+++ b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
@@ -0,0 +1,98 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {globals} from '../../frontend/globals';
+import {LONG, LONG_NULL, NUM} from '../../public';
+import {
+ BaseCounterTrack,
+ BaseCounterTrackArgs,
+} from '../../frontend/base_counter_track';
+
+interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
+ trackId: number;
+ rootTable?: string;
+}
+
+export class TraceProcessorCounterTrack extends BaseCounterTrack {
+ private trackId: number;
+ private rootTable: string;
+
+ constructor(args: TraceProcessorCounterTrackArgs) {
+ super(args);
+ this.trackId = args.trackId;
+ this.rootTable = args.rootTable ?? 'counter';
+ }
+
+ getSqlSource() {
+ return `select ts, value from ${this.rootTable} where track_id = ${this.trackId}`;
+ }
+
+ onMouseClick({x}: {x: number}): boolean {
+ const {visibleTimeScale} = globals.timeline;
+ const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
+
+ const query = `
+ select
+ id,
+ ts as leftTs,
+ (
+ select ts
+ from ${this.rootTable}
+ where
+ track_id = ${this.trackId}
+ and ts >= ${time}
+ order by ts
+ limit 1
+ ) as rightTs
+ from ${this.rootTable}
+ where
+ track_id = ${this.trackId}
+ and ts < ${time}
+ order by ts DESC
+ limit 1
+ `;
+
+ this.engine.query(query).then((result) => {
+ const it = result.iter({
+ id: NUM,
+ leftTs: LONG,
+ rightTs: LONG_NULL,
+ });
+ if (!it.valid()) {
+ return;
+ }
+ const trackKey = this.trackKey;
+ const id = it.id;
+ const leftTs = Time.fromRaw(it.leftTs);
+
+ // TODO(stevegolton): Don't try to guess times and durations here, make it
+ // obvious to the user that this counter sample has no duration as it's
+ // the last one in the series
+ const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
+
+ globals.makeSelection(
+ Actions.selectCounter({
+ leftTs,
+ rightTs,
+ id,
+ trackKey,
+ }),
+ );
+ });
+
+ return true;
+ }
+}
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index 07b9b1d..a1d3947 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -73,14 +73,14 @@
async onCreate() {
if (this.config.idleTrackId === undefined) {
- await this.engine.execute(`
+ await this.engine.query(`
create view raw_freq_idle_${this.trackUuid} as
select ts, dur, value as freqValue, -1 as idleValue
from experimental_counter_dur c
where track_id = ${this.config.freqTrackId}
`);
} else {
- await this.engine.execute(`
+ await this.engine.query(`
create view raw_freq_${this.trackUuid} as
select ts, dur, value as freqValue
from experimental_counter_dur c
@@ -99,7 +99,7 @@
`);
}
- await this.engine.execute(`
+ await this.engine.query(`
create virtual table cpu_freq_${this.trackUuid}
using __intrinsic_counter_mipmap((
select ts, freqValue as value
@@ -119,13 +119,15 @@
}
async onDestroy(): Promise<void> {
- if (this.engine.isAlive) {
- await this.engine.query(`drop table cpu_freq_${this.trackUuid}`);
- await this.engine.query(`drop table cpu_idle_${this.trackUuid}`);
- await this.engine.query(`drop table raw_freq_idle_${this.trackUuid}`);
- await this.engine.query(`drop view if exists raw_freq_${this.trackUuid}`);
- await this.engine.query(`drop view if exists raw_idle_${this.trackUuid}`);
- }
+ await this.engine.tryQuery(`drop table cpu_freq_${this.trackUuid}`);
+ await this.engine.tryQuery(`drop table cpu_idle_${this.trackUuid}`);
+ await this.engine.tryQuery(`drop table raw_freq_idle_${this.trackUuid}`);
+ await this.engine.tryQuery(
+ `drop view if exists raw_freq_${this.trackUuid}`,
+ );
+ await this.engine.tryQuery(
+ `drop view if exists raw_idle_${this.trackUuid}`,
+ );
}
async onBoundsChange(
@@ -403,7 +405,7 @@
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const {engine} = ctx;
- const cpus = await engine.getCpus();
+ const cpus = ctx.trace.cpus;
const maxCpuFreqResult = await engine.query(`
select ifnull(max(value), 0) as freq
diff --git a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
new file mode 100644
index 0000000..802e341
--- /dev/null
+++ b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
@@ -0,0 +1,248 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {searchSegment} from '../../base/binary_search';
+import {duration, Time, time} from '../../base/time';
+import {getLegacySelection} from '../../common/state';
+import {Actions} from '../../common/actions';
+import {colorForSample} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {TimeScale} from '../../frontend/time_scale';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+
+const BAR_HEIGHT = 3;
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 30.5;
+
+interface Data extends TrackData {
+ ids: Float64Array;
+ tsStarts: BigInt64Array;
+ callsiteId: Uint32Array;
+}
+
+export class CpuProfileTrack implements Track {
+ private centerY = this.getHeight() / 2 + BAR_HEIGHT;
+ private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
+ private hoveredTs: time | undefined = undefined;
+ private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+ private engine: Engine;
+ private utid: number;
+
+ constructor(engine: Engine, utid: number) {
+ this.engine = engine;
+ this.utid = utid;
+ }
+
+ async onUpdate(): Promise<void> {
+ await this.fetcher.requestDataForCurrentTime();
+ }
+
+ async onBoundsChange(
+ start: time,
+ end: time,
+ resolution: duration,
+ ): Promise<Data> {
+ const query = `select
+ id,
+ ts,
+ callsite_id as callsiteId
+ from cpu_profile_stack_sample
+ where utid = ${this.utid}
+ order by ts`;
+
+ const result = await this.engine.query(query);
+ const numRows = result.numRows();
+ const data: Data = {
+ start,
+ end,
+ resolution,
+ length: numRows,
+ ids: new Float64Array(numRows),
+ tsStarts: new BigInt64Array(numRows),
+ callsiteId: new Uint32Array(numRows),
+ };
+
+ const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
+ for (let row = 0; it.valid(); it.next(), ++row) {
+ data.ids[row] = it.id;
+ data.tsStarts[row] = it.ts;
+ data.callsiteId[row] = it.callsiteId;
+ }
+
+ return data;
+ }
+
+ async onDestroy(): Promise<void> {
+ this.fetcher.dispose();
+ }
+
+ getHeight() {
+ return MARGIN_TOP + RECT_HEIGHT - 1;
+ }
+
+ render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
+ const {visibleTimeScale: timeScale} = globals.timeline;
+ const data = this.fetcher.data;
+
+ if (data === undefined) return;
+
+ for (let i = 0; i < data.tsStarts.length; i++) {
+ const centerX = Time.fromRaw(data.tsStarts[i]);
+ const selection = getLegacySelection(globals.state);
+ const isHovered = this.hoveredTs === centerX;
+ const isSelected =
+ selection !== null &&
+ selection.kind === 'CPU_PROFILE_SAMPLE' &&
+ selection.ts === centerX;
+ const strokeWidth = isSelected ? 3 : 0;
+ this.drawMarker(
+ ctx,
+ timeScale.timeToPx(centerX),
+ this.centerY,
+ isHovered,
+ strokeWidth,
+ data.callsiteId[i],
+ );
+ }
+
+ // Group together identical identical CPU profile samples by connecting them
+ // with an horizontal bar.
+ let clusterStartIndex = 0;
+ while (clusterStartIndex < data.tsStarts.length) {
+ const callsiteId = data.callsiteId[clusterStartIndex];
+
+ // Find the end of the cluster by searching for the next different CPU
+ // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
+ // inclusive and within array bounds.
+ let clusterEndIndex = clusterStartIndex;
+ while (
+ clusterEndIndex + 1 < data.tsStarts.length &&
+ data.callsiteId[clusterEndIndex + 1] === callsiteId
+ ) {
+ clusterEndIndex++;
+ }
+
+ // If there are multiple CPU samples in the cluster, draw a line.
+ if (clusterStartIndex !== clusterEndIndex) {
+ const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
+ const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
+ const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
+ const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
+ const width = rightPx - leftPx;
+ ctx.fillStyle = colorForSample(callsiteId, false);
+ ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
+ }
+
+ // Move to the next cluster.
+ clusterStartIndex = clusterEndIndex + 1;
+ }
+ }
+
+ drawMarker(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ isHovered: boolean,
+ strokeWidth: number,
+ callsiteId: number,
+ ): void {
+ ctx.beginPath();
+ ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
+ ctx.lineTo(x, y + this.markerWidth);
+ ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
+ ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
+ ctx.closePath();
+ ctx.fillStyle = colorForSample(callsiteId, isHovered);
+ ctx.fill();
+ if (strokeWidth > 0) {
+ ctx.strokeStyle = colorForSample(callsiteId, false);
+ ctx.lineWidth = strokeWidth;
+ ctx.stroke();
+ }
+ }
+
+ onMouseMove({x, y}: {x: number; y: number}) {
+ const data = this.fetcher.data;
+ if (data === undefined) return;
+ const {visibleTimeScale: timeScale} = globals.timeline;
+ const time = timeScale.pxToHpTime(x);
+ const [left, right] = searchSegment(data.tsStarts, time.toTime());
+ const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+ this.hoveredTs =
+ index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
+ }
+
+ onMouseOut() {
+ this.hoveredTs = undefined;
+ }
+
+ onMouseClick({x, y}: {x: number; y: number}) {
+ const data = this.fetcher.data;
+ if (data === undefined) return false;
+ const {visibleTimeScale: timeScale} = globals.timeline;
+
+ const time = timeScale.pxToHpTime(x);
+ const [left, right] = searchSegment(data.tsStarts, time.toTime());
+
+ const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+
+ if (index !== -1) {
+ const id = data.ids[index];
+ const ts = Time.fromRaw(data.tsStarts[index]);
+
+ globals.makeSelection(
+ Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
+ );
+ return true;
+ }
+ return false;
+ }
+
+ // If the markers overlap the rightmost one will be selected.
+ findTimestampIndex(
+ left: number,
+ timeScale: TimeScale,
+ data: Data,
+ x: number,
+ y: number,
+ right: number,
+ ): number {
+ let index = -1;
+ if (left !== -1) {
+ const start = Time.fromRaw(data.tsStarts[left]);
+ const centerX = timeScale.timeToPx(start);
+ if (this.isInMarker(x, y, centerX)) {
+ index = left;
+ }
+ }
+ if (right !== -1) {
+ const start = Time.fromRaw(data.tsStarts[right]);
+ const centerX = timeScale.timeToPx(start);
+ if (this.isInMarker(x, y, centerX)) {
+ index = right;
+ }
+ }
+ return index;
+ }
+
+ isInMarker(x: number, y: number, centerX: number) {
+ return (
+ Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+ );
+ }
+}
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
index 364225d..9e8fe84 100644
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ b/ui/src/core_plugins/cpu_profile/index.ts
@@ -12,255 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {getLegacySelection} from '../../common/state';
-import {Actions} from '../../common/actions';
-import {colorForSample} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
import {CpuProfileDetailsPanel} from '../../frontend/cpu_profile_panel';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
-import {TimeScale} from '../../frontend/time_scale';
-import {
- Engine,
- Plugin,
- PluginContextTrace,
- PluginDescriptor,
- Track,
-} from '../../public';
-import {
- LONG,
- NUM,
- NUM_NULL,
- STR_NULL,
-} from '../../trace_processor/query_result';
-
-const BAR_HEIGHT = 3;
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {CpuProfileTrack} from './cpu_profile_track';
export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
-interface Data extends TrackData {
- ids: Float64Array;
- tsStarts: BigInt64Array;
- callsiteId: Uint32Array;
-}
-
-class CpuProfileTrack implements Track {
- private centerY = this.getHeight() / 2 + BAR_HEIGHT;
- private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
- private hoveredTs: time | undefined = undefined;
- private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
- private engine: Engine;
- private utid: number;
-
- constructor(engine: Engine, utid: number) {
- this.engine = engine;
- this.utid = utid;
- }
-
- async onUpdate(): Promise<void> {
- await this.fetcher.requestDataForCurrentTime();
- }
-
- async onBoundsChange(
- start: time,
- end: time,
- resolution: duration,
- ): Promise<Data> {
- const query = `select
- id,
- ts,
- callsite_id as callsiteId
- from cpu_profile_stack_sample
- where utid = ${this.utid}
- order by ts`;
-
- const result = await this.engine.query(query);
- const numRows = result.numRows();
- const data: Data = {
- start,
- end,
- resolution,
- length: numRows,
- ids: new Float64Array(numRows),
- tsStarts: new BigInt64Array(numRows),
- callsiteId: new Uint32Array(numRows),
- };
-
- const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
- for (let row = 0; it.valid(); it.next(), ++row) {
- data.ids[row] = it.id;
- data.tsStarts[row] = it.ts;
- data.callsiteId[row] = it.callsiteId;
- }
-
- return data;
- }
-
- async onDestroy(): Promise<void> {
- this.fetcher.dispose();
- }
-
- getHeight() {
- return MARGIN_TOP + RECT_HEIGHT - 1;
- }
-
- render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
- const {visibleTimeScale: timeScale} = globals.timeline;
- const data = this.fetcher.data;
-
- if (data === undefined) return;
-
- for (let i = 0; i < data.tsStarts.length; i++) {
- const centerX = Time.fromRaw(data.tsStarts[i]);
- const selection = getLegacySelection(globals.state);
- const isHovered = this.hoveredTs === centerX;
- const isSelected =
- selection !== null &&
- selection.kind === 'CPU_PROFILE_SAMPLE' &&
- selection.ts === centerX;
- const strokeWidth = isSelected ? 3 : 0;
- this.drawMarker(
- ctx,
- timeScale.timeToPx(centerX),
- this.centerY,
- isHovered,
- strokeWidth,
- data.callsiteId[i],
- );
- }
-
- // Group together identical identical CPU profile samples by connecting them
- // with an horizontal bar.
- let clusterStartIndex = 0;
- while (clusterStartIndex < data.tsStarts.length) {
- const callsiteId = data.callsiteId[clusterStartIndex];
-
- // Find the end of the cluster by searching for the next different CPU
- // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
- // inclusive and within array bounds.
- let clusterEndIndex = clusterStartIndex;
- while (
- clusterEndIndex + 1 < data.tsStarts.length &&
- data.callsiteId[clusterEndIndex + 1] === callsiteId
- ) {
- clusterEndIndex++;
- }
-
- // If there are multiple CPU samples in the cluster, draw a line.
- if (clusterStartIndex !== clusterEndIndex) {
- const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
- const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
- const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
- const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
- const width = rightPx - leftPx;
- ctx.fillStyle = colorForSample(callsiteId, false);
- ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
- }
-
- // Move to the next cluster.
- clusterStartIndex = clusterEndIndex + 1;
- }
- }
-
- drawMarker(
- ctx: CanvasRenderingContext2D,
- x: number,
- y: number,
- isHovered: boolean,
- strokeWidth: number,
- callsiteId: number,
- ): void {
- ctx.beginPath();
- ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
- ctx.lineTo(x, y + this.markerWidth);
- ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
- ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
- ctx.closePath();
- ctx.fillStyle = colorForSample(callsiteId, isHovered);
- ctx.fill();
- if (strokeWidth > 0) {
- ctx.strokeStyle = colorForSample(callsiteId, false);
- ctx.lineWidth = strokeWidth;
- ctx.stroke();
- }
- }
-
- onMouseMove({x, y}: {x: number; y: number}) {
- const data = this.fetcher.data;
- if (data === undefined) return;
- const {visibleTimeScale: timeScale} = globals.timeline;
- const time = timeScale.pxToHpTime(x);
- const [left, right] = searchSegment(data.tsStarts, time.toTime());
- const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
- this.hoveredTs =
- index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
- }
-
- onMouseOut() {
- this.hoveredTs = undefined;
- }
-
- onMouseClick({x, y}: {x: number; y: number}) {
- const data = this.fetcher.data;
- if (data === undefined) return false;
- const {visibleTimeScale: timeScale} = globals.timeline;
-
- const time = timeScale.pxToHpTime(x);
- const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
- const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
-
- if (index !== -1) {
- const id = data.ids[index];
- const ts = Time.fromRaw(data.tsStarts[index]);
-
- globals.makeSelection(
- Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
- );
- return true;
- }
- return false;
- }
-
- // If the markers overlap the rightmost one will be selected.
- findTimestampIndex(
- left: number,
- timeScale: TimeScale,
- data: Data,
- x: number,
- y: number,
- right: number,
- ): number {
- let index = -1;
- if (left !== -1) {
- const start = Time.fromRaw(data.tsStarts[left]);
- const centerX = timeScale.timeToPx(start);
- if (this.isInMarker(x, y, centerX)) {
- index = left;
- }
- }
- if (right !== -1) {
- const start = Time.fromRaw(data.tsStarts[right]);
- const centerX = timeScale.timeToPx(start);
- if (this.isInMarker(x, y, centerX)) {
- index = right;
- }
- }
- return index;
- }
-
- isInMarker(x: number, y: number, centerX: number) {
- return (
- Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
- );
- }
-}
-
class CpuProfile implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
new file mode 100644
index 0000000..70acb78
--- /dev/null
+++ b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
@@ -0,0 +1,474 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {search, searchEq, searchSegment} from '../../base/binary_search';
+import {assertExists, assertTrue} from '../../base/logging';
+import {Duration, duration, Time, time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {getLegacySelection} from '../../common/state';
+import {
+ cropText,
+ drawDoubleHeadedArrow,
+ drawIncompleteSlice,
+ drawTrackHoverTooltip,
+} from '../../common/canvas_utils';
+import {Color} from '../../core/color';
+import {colorForThread} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+
+export interface Data extends TrackData {
+ // Slices are stored in a columnar fashion. All fields have the same length.
+ ids: Float64Array;
+ startQs: BigInt64Array;
+ endQs: BigInt64Array;
+ utids: Uint32Array;
+ flags: Uint8Array;
+ lastRowId: number;
+}
+
+const MARGIN_TOP = 3;
+const RECT_HEIGHT = 24;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
+const CPU_SLICE_FLAGS_INCOMPLETE = 1;
+const CPU_SLICE_FLAGS_REALTIME = 2;
+
+export class CpuSliceTrack implements Track {
+ private mousePos?: {x: number; y: number};
+ private utidHoveredInThisTrack = -1;
+ private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+ private lastRowId = -1;
+ private engine: Engine;
+ private cpu: number;
+ private trackKey: string;
+ private trackUuid = uuidv4Sql();
+
+ constructor(engine: Engine, trackKey: string, cpu: number) {
+ this.engine = engine;
+ this.trackKey = trackKey;
+ this.cpu = cpu;
+ }
+
+ async onCreate() {
+ await this.engine.query(`
+ create virtual table cpu_slice_${this.trackUuid}
+ using __intrinsic_slice_mipmap((
+ select
+ id,
+ ts,
+ iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
+ 0 as depth
+ from sched
+ where cpu = ${this.cpu} and utid != 0
+ ));
+ `);
+ const it = await this.engine.query(`
+ select coalesce(max(id), -1) as lastRowId
+ from sched
+ where cpu = ${this.cpu} and utid != 0
+ `);
+ this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
+ }
+
+ async onUpdate() {
+ await this.fetcher.requestDataForCurrentTime();
+ }
+
+ async onBoundsChange(
+ start: time,
+ end: time,
+ resolution: duration,
+ ): Promise<Data> {
+ assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+ const queryRes = await this.engine.query(`
+ select
+ (z.ts / ${resolution}) * ${resolution} as tsQ,
+ (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
+ s.utid,
+ s.id,
+ s.dur = -1 as isIncomplete,
+ ifnull(s.priority < 100, 0) as isRealtime
+ from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
+ cross join sched s using (id)
+ `);
+
+ const numRows = queryRes.numRows();
+ const slices: Data = {
+ start,
+ end,
+ resolution,
+ length: numRows,
+ lastRowId: this.lastRowId,
+ ids: new Float64Array(numRows),
+ startQs: new BigInt64Array(numRows),
+ endQs: new BigInt64Array(numRows),
+ utids: new Uint32Array(numRows),
+ flags: new Uint8Array(numRows),
+ };
+
+ const it = queryRes.iter({
+ tsQ: LONG,
+ tsEndQ: LONG,
+ utid: NUM,
+ id: NUM,
+ isIncomplete: NUM,
+ isRealtime: NUM,
+ });
+ for (let row = 0; it.valid(); it.next(), row++) {
+ slices.startQs[row] = it.tsQ;
+ slices.endQs[row] = it.tsEndQ;
+ slices.utids[row] = it.utid;
+ slices.ids[row] = it.id;
+
+ slices.flags[row] = 0;
+ if (it.isIncomplete) {
+ slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
+ }
+ if (it.isRealtime) {
+ slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
+ }
+ }
+ return slices;
+ }
+
+ async onDestroy() {
+ await this.engine.tryQuery(
+ `drop table if exists cpu_slice_${this.trackUuid}`,
+ );
+ this.fetcher.dispose();
+ }
+
+ getHeight(): number {
+ return TRACK_HEIGHT;
+ }
+
+ render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
+ // TODO: fonts and colors should come from the CSS and not hardcoded here.
+ const {visibleTimeScale} = globals.timeline;
+ const data = this.fetcher.data;
+
+ if (data === undefined) return; // Can't possibly draw anything.
+
+ // If the cached trace slices don't fully cover the visible time range,
+ // show a gray rectangle with a "Loading..." label.
+ checkerboardExcept(
+ ctx,
+ this.getHeight(),
+ 0,
+ size.width,
+ visibleTimeScale.timeToPx(data.start),
+ visibleTimeScale.timeToPx(data.end),
+ );
+
+ this.renderSlices(ctx, data);
+ }
+
+ renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
+ const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
+ globals.timeline;
+ assertTrue(data.startQs.length === data.endQs.length);
+ assertTrue(data.startQs.length === data.utids.length);
+
+ const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
+
+ ctx.textAlign = 'center';
+ ctx.font = '12px Roboto Condensed';
+ const charWidth = ctx.measureText('dbpqaouk').width / 8;
+
+ const startTime = visibleTimeSpan.start;
+ const endTime = visibleTimeSpan.end;
+
+ const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
+ const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+ const [, rawEndIdx] = searchSegment(data.startQs, endTime);
+ const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
+
+ for (let i = startIdx; i < endIdx; i++) {
+ const tStart = Time.fromRaw(data.startQs[i]);
+ let tEnd = Time.fromRaw(data.endQs[i]);
+ const utid = data.utids[i];
+
+ // If the last slice is incomplete, it should end with the end of the
+ // window, else it might spill over the window and the end would not be
+ // visible as a zigzag line.
+ if (
+ data.ids[i] === data.lastRowId &&
+ data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
+ ) {
+ tEnd = endTime;
+ }
+ const rectStart = visibleTimeScale.timeToPx(tStart);
+ const rectEnd = visibleTimeScale.timeToPx(tEnd);
+ const rectWidth = Math.max(1, rectEnd - rectStart);
+
+ const threadInfo = globals.threads.get(utid);
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+ const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
+
+ const isHovering = globals.state.hoveredUtid !== -1;
+ const isThreadHovered = globals.state.hoveredUtid === utid;
+ const isProcessHovered = globals.state.hoveredPid === pid;
+ const colorScheme = colorForThread(threadInfo);
+ let color: Color;
+ let textColor: Color;
+ if (isHovering && !isThreadHovered) {
+ if (!isProcessHovered) {
+ color = colorScheme.disabled;
+ textColor = colorScheme.textDisabled;
+ } else {
+ color = colorScheme.variant;
+ textColor = colorScheme.textVariant;
+ }
+ } else {
+ color = colorScheme.base;
+ textColor = colorScheme.textBase;
+ }
+ ctx.fillStyle = color.cssString;
+
+ if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
+ drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+ } else {
+ ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+ }
+
+ // Don't render text when we have less than 5px to play with.
+ if (rectWidth < 5) continue;
+
+ // Stylize real-time threads. We don't do it when zoomed out as the
+ // fillRect is expensive.
+ if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+ ctx.fillStyle = getHatchedPattern(ctx);
+ ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+ }
+
+ // TODO: consider de-duplicating this code with the copied one from
+ // chrome_slices/frontend.ts.
+ let title = `[utid:${utid}]`;
+ let subTitle = '';
+ if (threadInfo) {
+ /* eslint-disable @typescript-eslint/strict-boolean-expressions */
+ if (threadInfo.pid) {
+ /* eslint-enable */
+ let procName = threadInfo.procName || '';
+ if (procName.startsWith('/')) {
+ // Remove folder paths from name
+ procName = procName.substring(procName.lastIndexOf('/') + 1);
+ }
+ title = `${procName} [${threadInfo.pid}]`;
+ subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
+ } else {
+ title = `${threadInfo.threadName} [${threadInfo.tid}]`;
+ }
+ }
+
+ if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+ subTitle = subTitle + ' (RT)';
+ }
+
+ const right = Math.min(visWindowEndPx, rectEnd);
+ const left = Math.max(rectStart, 0);
+ const visibleWidth = Math.max(right - left, 1);
+ title = cropText(title, charWidth, visibleWidth);
+ subTitle = cropText(subTitle, charWidth, visibleWidth);
+ const rectXCenter = left + visibleWidth / 2;
+ ctx.fillStyle = textColor.cssString;
+ ctx.font = '12px Roboto Condensed';
+ ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
+ ctx.fillStyle = textColor.setAlpha(0.6).cssString;
+ ctx.font = '10px Roboto Condensed';
+ ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
+ }
+
+ const selection = getLegacySelection(globals.state);
+ const details = globals.sliceDetails;
+ if (selection !== null && selection.kind === 'SLICE') {
+ const [startIndex, endIndex] = searchEq(data.ids, selection.id);
+ if (startIndex !== endIndex) {
+ const tStart = Time.fromRaw(data.startQs[startIndex]);
+ const tEnd = Time.fromRaw(data.endQs[startIndex]);
+ const utid = data.utids[startIndex];
+ const color = colorForThread(globals.threads.get(utid));
+ const rectStart = visibleTimeScale.timeToPx(tStart);
+ const rectEnd = visibleTimeScale.timeToPx(tEnd);
+ const rectWidth = Math.max(1, rectEnd - rectStart);
+
+ // Draw a rectangle around the slice that is currently selected.
+ ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
+ ctx.beginPath();
+ ctx.lineWidth = 3;
+ ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
+ ctx.closePath();
+ // Draw arrow from wakeup time of current slice.
+ if (details.wakeupTs) {
+ const wakeupPos = visibleTimeScale.timeToPx(details.wakeupTs);
+ const latencyWidth = rectStart - wakeupPos;
+ drawDoubleHeadedArrow(
+ ctx,
+ wakeupPos,
+ MARGIN_TOP + RECT_HEIGHT,
+ latencyWidth,
+ latencyWidth >= 20,
+ );
+ // Latency time with a white semi-transparent background.
+ const latency = tStart - details.wakeupTs;
+ const displayText = Duration.humanise(latency);
+ const measured = ctx.measureText(displayText);
+ if (latencyWidth >= measured.width + 2) {
+ ctx.fillStyle = 'rgba(255,255,255,0.7)';
+ ctx.fillRect(
+ wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
+ MARGIN_TOP + RECT_HEIGHT - 12,
+ measured.width + 2,
+ 11,
+ );
+ ctx.textBaseline = 'bottom';
+ ctx.fillStyle = 'black';
+ ctx.fillText(
+ displayText,
+ wakeupPos + latencyWidth / 2,
+ MARGIN_TOP + RECT_HEIGHT - 1,
+ );
+ }
+ }
+ }
+
+ // Draw diamond if the track being drawn is the cpu of the waker.
+ if (this.cpu === details.wakerCpu && details.wakeupTs) {
+ const wakeupPos = Math.floor(
+ visibleTimeScale.timeToPx(details.wakeupTs),
+ );
+ ctx.beginPath();
+ ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
+ ctx.fillStyle = 'black';
+ ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
+ ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
+ ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
+ ctx.fill();
+ ctx.closePath();
+ }
+ }
+
+ const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
+ const maxHeight = this.getHeight();
+ if (hoveredThread !== undefined && this.mousePos !== undefined) {
+ const tidText = `T: ${hoveredThread.threadName}
+ [${hoveredThread.tid}]`;
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+ if (hoveredThread.pid) {
+ const pidText = `P: ${hoveredThread.procName}
+ [${hoveredThread.pid}]`;
+ drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, pidText, tidText);
+ } else {
+ drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, tidText);
+ }
+ }
+ }
+
+ onMouseMove(pos: {x: number; y: number}) {
+ const data = this.fetcher.data;
+ this.mousePos = pos;
+ if (data === undefined) return;
+ const {visibleTimeScale} = globals.timeline;
+ if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
+ this.utidHoveredInThisTrack = -1;
+ globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
+ return;
+ }
+ const t = visibleTimeScale.pxToHpTime(pos.x);
+ let hoveredUtid = -1;
+
+ for (let i = 0; i < data.startQs.length; i++) {
+ const tStart = Time.fromRaw(data.startQs[i]);
+ const tEnd = Time.fromRaw(data.endQs[i]);
+ const utid = data.utids[i];
+ if (t.gte(tStart) && t.lt(tEnd)) {
+ hoveredUtid = utid;
+ break;
+ }
+ }
+ this.utidHoveredInThisTrack = hoveredUtid;
+ const threadInfo = globals.threads.get(hoveredUtid);
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+ const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
+ globals.dispatch(
+ Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
+ );
+ }
+
+ onMouseOut() {
+ this.utidHoveredInThisTrack = -1;
+ globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
+ this.mousePos = undefined;
+ }
+
+ onMouseClick({x}: {x: number}) {
+ const data = this.fetcher.data;
+ if (data === undefined) return false;
+ const {visibleTimeScale} = globals.timeline;
+ const time = visibleTimeScale.pxToHpTime(x);
+ const index = search(data.startQs, time.toTime());
+ const id = index === -1 ? undefined : data.ids[index];
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+ if (!id || this.utidHoveredInThisTrack === -1) return false;
+
+ globals.setLegacySelection(
+ {
+ kind: 'SLICE',
+ id,
+ trackKey: this.trackKey,
+ },
+ {
+ clearSearch: true,
+ pendingScrollId: undefined,
+ switchToCurrentSelectionTab: true,
+ },
+ );
+
+ return true;
+ }
+}
+
+// Creates a diagonal hatched pattern to be used for distinguishing slices with
+// real-time priorities. The pattern is created once as an offscreen canvas and
+// is kept cached inside the Context2D of the main canvas, without making
+// assumptions on the lifetime of the main canvas.
+function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
+ const mctx = mainCtx as CanvasRenderingContext2D & {
+ sliceHatchedPattern?: CanvasPattern;
+ };
+ if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
+ const canvas = document.createElement('canvas');
+ const SIZE = 8;
+ canvas.width = canvas.height = SIZE;
+ const ctx = assertExists(canvas.getContext('2d'));
+ ctx.strokeStyle = 'rgba(255,255,255,0.3)';
+ ctx.beginPath();
+ ctx.lineWidth = 1;
+ ctx.moveTo(0, SIZE);
+ ctx.lineTo(SIZE, 0);
+ ctx.stroke();
+ mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
+ return mctx.sliceHatchedPattern;
+}
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index 1fc1e67..3f456a3 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -12,458 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {search, searchEq, searchSegment} from '../../base/binary_search';
-import {assertExists, assertTrue} from '../../base/logging';
-import {Duration, duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {getLegacySelection} from '../../common/state';
-import {
- cropText,
- drawDoubleHeadedArrow,
- drawIncompleteSlice,
- drawTrackHoverTooltip,
-} from '../../common/canvas_utils';
-import {Color} from '../../core/color';
-import {colorForThread} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
import {
Engine,
Plugin,
PluginContextTrace,
PluginDescriptor,
- Track,
} from '../../public';
-import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
+import {NUM, STR_NULL} from '../../trace_processor/query_result';
+import {CpuSliceTrack} from './cpu_slice_track';
export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
-export interface Data extends TrackData {
- // Slices are stored in a columnar fashion. All fields have the same length.
- ids: Float64Array;
- startQs: BigInt64Array;
- endQs: BigInt64Array;
- utids: Uint32Array;
- flags: Uint8Array;
- lastRowId: number;
-}
-
-const MARGIN_TOP = 3;
-const RECT_HEIGHT = 24;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-const CPU_SLICE_FLAGS_INCOMPLETE = 1;
-const CPU_SLICE_FLAGS_REALTIME = 2;
-
-class CpuSliceTrack implements Track {
- private mousePos?: {x: number; y: number};
- private utidHoveredInThisTrack = -1;
- private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
- private lastRowId = -1;
- private engine: Engine;
- private cpu: number;
- private trackKey: string;
- private trackUuid = uuidv4Sql();
-
- constructor(engine: Engine, trackKey: string, cpu: number) {
- this.engine = engine;
- this.trackKey = trackKey;
- this.cpu = cpu;
- }
-
- async onCreate() {
- await this.engine.query(`
- create virtual table cpu_slice_${this.trackUuid}
- using __intrinsic_slice_mipmap((
- select
- id,
- ts,
- iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
- 0 as depth
- from sched
- where cpu = ${this.cpu} and utid != 0
- ));
- `);
- const it = await this.engine.query(`
- select coalesce(max(id), -1) as lastRowId
- from sched
- where cpu = ${this.cpu} and utid != 0
- `);
- this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
- }
-
- async onUpdate() {
- await this.fetcher.requestDataForCurrentTime();
- }
-
- async onBoundsChange(
- start: time,
- end: time,
- resolution: duration,
- ): Promise<Data> {
- assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
- const queryRes = await this.engine.query(`
- select
- (z.ts / ${resolution}) * ${resolution} as tsQ,
- (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
- s.utid,
- s.id,
- s.dur = -1 as isIncomplete,
- ifnull(s.priority < 100, 0) as isRealtime
- from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
- cross join sched s using (id)
- `);
-
- const numRows = queryRes.numRows();
- const slices: Data = {
- start,
- end,
- resolution,
- length: numRows,
- lastRowId: this.lastRowId,
- ids: new Float64Array(numRows),
- startQs: new BigInt64Array(numRows),
- endQs: new BigInt64Array(numRows),
- utids: new Uint32Array(numRows),
- flags: new Uint8Array(numRows),
- };
-
- const it = queryRes.iter({
- tsQ: LONG,
- tsEndQ: LONG,
- utid: NUM,
- id: NUM,
- isIncomplete: NUM,
- isRealtime: NUM,
- });
- for (let row = 0; it.valid(); it.next(), row++) {
- slices.startQs[row] = it.tsQ;
- slices.endQs[row] = it.tsEndQ;
- slices.utids[row] = it.utid;
- slices.ids[row] = it.id;
-
- slices.flags[row] = 0;
- if (it.isIncomplete) {
- slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
- }
- if (it.isRealtime) {
- slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
- }
- }
- return slices;
- }
-
- async onDestroy() {
- if (this.engine.isAlive) {
- await this.engine.query(
- `drop table if exists cpu_slice_${this.trackUuid}`,
- );
- }
- this.fetcher.dispose();
- }
-
- getHeight(): number {
- return TRACK_HEIGHT;
- }
-
- render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
- // TODO: fonts and colors should come from the CSS and not hardcoded here.
- const {visibleTimeScale} = globals.timeline;
- const data = this.fetcher.data;
-
- if (data === undefined) return; // Can't possibly draw anything.
-
- // If the cached trace slices don't fully cover the visible time range,
- // show a gray rectangle with a "Loading..." label.
- checkerboardExcept(
- ctx,
- this.getHeight(),
- 0,
- size.width,
- visibleTimeScale.timeToPx(data.start),
- visibleTimeScale.timeToPx(data.end),
- );
-
- this.renderSlices(ctx, data);
- }
-
- renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
- const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
- globals.timeline;
- assertTrue(data.startQs.length === data.endQs.length);
- assertTrue(data.startQs.length === data.utids.length);
-
- const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
-
- ctx.textAlign = 'center';
- ctx.font = '12px Roboto Condensed';
- const charWidth = ctx.measureText('dbpqaouk').width / 8;
-
- const startTime = visibleTimeSpan.start;
- const endTime = visibleTimeSpan.end;
-
- const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
- const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
- const [, rawEndIdx] = searchSegment(data.startQs, endTime);
- const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
-
- for (let i = startIdx; i < endIdx; i++) {
- const tStart = Time.fromRaw(data.startQs[i]);
- let tEnd = Time.fromRaw(data.endQs[i]);
- const utid = data.utids[i];
-
- // If the last slice is incomplete, it should end with the end of the
- // window, else it might spill over the window and the end would not be
- // visible as a zigzag line.
- if (
- data.ids[i] === data.lastRowId &&
- data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
- ) {
- tEnd = endTime;
- }
- const rectStart = visibleTimeScale.timeToPx(tStart);
- const rectEnd = visibleTimeScale.timeToPx(tEnd);
- const rectWidth = Math.max(1, rectEnd - rectStart);
-
- const threadInfo = globals.threads.get(utid);
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
-
- const isHovering = globals.state.hoveredUtid !== -1;
- const isThreadHovered = globals.state.hoveredUtid === utid;
- const isProcessHovered = globals.state.hoveredPid === pid;
- const colorScheme = colorForThread(threadInfo);
- let color: Color;
- let textColor: Color;
- if (isHovering && !isThreadHovered) {
- if (!isProcessHovered) {
- color = colorScheme.disabled;
- textColor = colorScheme.textDisabled;
- } else {
- color = colorScheme.variant;
- textColor = colorScheme.textVariant;
- }
- } else {
- color = colorScheme.base;
- textColor = colorScheme.textBase;
- }
- ctx.fillStyle = color.cssString;
-
- if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
- drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
- } else {
- ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
- }
-
- // Don't render text when we have less than 5px to play with.
- if (rectWidth < 5) continue;
-
- // Stylize real-time threads. We don't do it when zoomed out as the
- // fillRect is expensive.
- if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
- ctx.fillStyle = getHatchedPattern(ctx);
- ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
- }
-
- // TODO: consider de-duplicating this code with the copied one from
- // chrome_slices/frontend.ts.
- let title = `[utid:${utid}]`;
- let subTitle = '';
- if (threadInfo) {
- /* eslint-disable @typescript-eslint/strict-boolean-expressions */
- if (threadInfo.pid) {
- /* eslint-enable */
- let procName = threadInfo.procName || '';
- if (procName.startsWith('/')) {
- // Remove folder paths from name
- procName = procName.substring(procName.lastIndexOf('/') + 1);
- }
- title = `${procName} [${threadInfo.pid}]`;
- subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
- } else {
- title = `${threadInfo.threadName} [${threadInfo.tid}]`;
- }
- }
-
- if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
- subTitle = subTitle + ' (RT)';
- }
-
- const right = Math.min(visWindowEndPx, rectEnd);
- const left = Math.max(rectStart, 0);
- const visibleWidth = Math.max(right - left, 1);
- title = cropText(title, charWidth, visibleWidth);
- subTitle = cropText(subTitle, charWidth, visibleWidth);
- const rectXCenter = left + visibleWidth / 2;
- ctx.fillStyle = textColor.cssString;
- ctx.font = '12px Roboto Condensed';
- ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
- ctx.fillStyle = textColor.setAlpha(0.6).cssString;
- ctx.font = '10px Roboto Condensed';
- ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
- }
-
- const selection = getLegacySelection(globals.state);
- const details = globals.sliceDetails;
- if (selection !== null && selection.kind === 'SLICE') {
- const [startIndex, endIndex] = searchEq(data.ids, selection.id);
- if (startIndex !== endIndex) {
- const tStart = Time.fromRaw(data.startQs[startIndex]);
- const tEnd = Time.fromRaw(data.endQs[startIndex]);
- const utid = data.utids[startIndex];
- const color = colorForThread(globals.threads.get(utid));
- const rectStart = visibleTimeScale.timeToPx(tStart);
- const rectEnd = visibleTimeScale.timeToPx(tEnd);
- const rectWidth = Math.max(1, rectEnd - rectStart);
-
- // Draw a rectangle around the slice that is currently selected.
- ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
- ctx.beginPath();
- ctx.lineWidth = 3;
- ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
- ctx.closePath();
- // Draw arrow from wakeup time of current slice.
- if (details.wakeupTs) {
- const wakeupPos = visibleTimeScale.timeToPx(details.wakeupTs);
- const latencyWidth = rectStart - wakeupPos;
- drawDoubleHeadedArrow(
- ctx,
- wakeupPos,
- MARGIN_TOP + RECT_HEIGHT,
- latencyWidth,
- latencyWidth >= 20,
- );
- // Latency time with a white semi-transparent background.
- const latency = tStart - details.wakeupTs;
- const displayText = Duration.humanise(latency);
- const measured = ctx.measureText(displayText);
- if (latencyWidth >= measured.width + 2) {
- ctx.fillStyle = 'rgba(255,255,255,0.7)';
- ctx.fillRect(
- wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
- MARGIN_TOP + RECT_HEIGHT - 12,
- measured.width + 2,
- 11,
- );
- ctx.textBaseline = 'bottom';
- ctx.fillStyle = 'black';
- ctx.fillText(
- displayText,
- wakeupPos + latencyWidth / 2,
- MARGIN_TOP + RECT_HEIGHT - 1,
- );
- }
- }
- }
-
- // Draw diamond if the track being drawn is the cpu of the waker.
- if (this.cpu === details.wakerCpu && details.wakeupTs) {
- const wakeupPos = Math.floor(
- visibleTimeScale.timeToPx(details.wakeupTs),
- );
- ctx.beginPath();
- ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
- ctx.fillStyle = 'black';
- ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
- ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
- ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
- ctx.fill();
- ctx.closePath();
- }
- }
-
- const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
- const maxHeight = this.getHeight();
- if (hoveredThread !== undefined && this.mousePos !== undefined) {
- const tidText = `T: ${hoveredThread.threadName}
- [${hoveredThread.tid}]`;
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (hoveredThread.pid) {
- const pidText = `P: ${hoveredThread.procName}
- [${hoveredThread.pid}]`;
- drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, pidText, tidText);
- } else {
- drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, tidText);
- }
- }
- }
-
- onMouseMove(pos: {x: number; y: number}) {
- const data = this.fetcher.data;
- this.mousePos = pos;
- if (data === undefined) return;
- const {visibleTimeScale} = globals.timeline;
- if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
- this.utidHoveredInThisTrack = -1;
- globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
- return;
- }
- const t = visibleTimeScale.pxToHpTime(pos.x);
- let hoveredUtid = -1;
-
- for (let i = 0; i < data.startQs.length; i++) {
- const tStart = Time.fromRaw(data.startQs[i]);
- const tEnd = Time.fromRaw(data.endQs[i]);
- const utid = data.utids[i];
- if (t.gte(tStart) && t.lt(tEnd)) {
- hoveredUtid = utid;
- break;
- }
- }
- this.utidHoveredInThisTrack = hoveredUtid;
- const threadInfo = globals.threads.get(hoveredUtid);
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
- globals.dispatch(
- Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
- );
- }
-
- onMouseOut() {
- this.utidHoveredInThisTrack = -1;
- globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
- this.mousePos = undefined;
- }
-
- onMouseClick({x}: {x: number}) {
- const data = this.fetcher.data;
- if (data === undefined) return false;
- const {visibleTimeScale} = globals.timeline;
- const time = visibleTimeScale.pxToHpTime(x);
- const index = search(data.startQs, time.toTime());
- const id = index === -1 ? undefined : data.ids[index];
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (!id || this.utidHoveredInThisTrack === -1) return false;
-
- globals.setLegacySelection(
- {
- kind: 'SLICE',
- id,
- trackKey: this.trackKey,
- },
- {
- clearSearch: true,
- pendingScrollId: undefined,
- switchToCurrentSelectionTab: true,
- },
- );
-
- return true;
- }
-}
-
class CpuSlices implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
- const cpus = await ctx.engine.getCpus();
+ const cpus = ctx.trace.cpus;
const cpuToSize = await this.guessCpuSizes(ctx.engine);
for (const cpu of cpus) {
@@ -516,29 +79,6 @@
}
}
-// Creates a diagonal hatched pattern to be used for distinguishing slices with
-// real-time priorities. The pattern is created once as an offscreen canvas and
-// is kept cached inside the Context2D of the main canvas, without making
-// assumptions on the lifetime of the main canvas.
-function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
- const mctx = mainCtx as CanvasRenderingContext2D & {
- sliceHatchedPattern?: CanvasPattern;
- };
- if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
- const canvas = document.createElement('canvas');
- const SIZE = 8;
- canvas.width = canvas.height = SIZE;
- const ctx = assertExists(canvas.getContext('2d'));
- ctx.strokeStyle = 'rgba(255,255,255,0.3)';
- ctx.beginPath();
- ctx.lineWidth = 1;
- ctx.moveTo(0, SIZE);
- ctx.lineTo(SIZE, 0);
- ctx.stroke();
- mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
- return mctx.sliceHatchedPattern;
-}
-
export const plugin: PluginDescriptor = {
pluginId: 'perfetto.CpuSlices',
plugin: CpuSlices,
diff --git a/ui/src/core_plugins/debug/counter_track.ts b/ui/src/core_plugins/debug/counter_track.ts
index 56274d3..5c0022d 100644
--- a/ui/src/core_plugins/debug/counter_track.ts
+++ b/ui/src/core_plugins/debug/counter_track.ts
@@ -67,8 +67,6 @@
}
private async dropTrackTable(): Promise<void> {
- if (this.engine.isAlive) {
- this.engine.query(`drop table if exists ${this.sqlTableName}`);
- }
+ this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
}
}
diff --git a/ui/src/core_plugins/debug/slice_track.ts b/ui/src/core_plugins/debug/slice_track.ts
index 49e5142..a0734d9 100644
--- a/ui/src/core_plugins/debug/slice_track.ts
+++ b/ui/src/core_plugins/debug/slice_track.ts
@@ -19,7 +19,7 @@
CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
import {DebugSliceDetailsTab} from './details_tab';
import {
@@ -111,8 +111,6 @@
}
private async destroyTrackTable() {
- if (this.engine.isAlive) {
- await this.engine.query(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
- }
+ await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
}
}
diff --git a/ui/src/core_plugins/frames/actual_frames_track_v2.ts b/ui/src/core_plugins/frames/actual_frames_track.ts
similarity index 100%
rename from ui/src/core_plugins/frames/actual_frames_track_v2.ts
rename to ui/src/core_plugins/frames/actual_frames_track.ts
diff --git a/ui/src/core_plugins/frames/expected_frames_track_v2.ts b/ui/src/core_plugins/frames/expected_frames_track.ts
similarity index 100%
rename from ui/src/core_plugins/frames/expected_frames_track_v2.ts
rename to ui/src/core_plugins/frames/expected_frames_track.ts
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
index 2cc877f..8e67de9 100644
--- a/ui/src/core_plugins/frames/index.ts
+++ b/ui/src/core_plugins/frames/index.ts
@@ -16,8 +16,8 @@
import {getTrackName} from '../../public/utils';
import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
-import {ActualFramesTrack as ActualFramesTrackV2} from './actual_frames_track_v2';
-import {ExpectedFramesTrack as ExpectedFramesTrackV2} from './expected_frames_track_v2';
+import {ActualFramesTrack} from './actual_frames_track';
+import {ExpectedFramesTrack} from './expected_frames_track';
export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
@@ -75,12 +75,7 @@
trackIds,
kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
trackFactory: ({trackKey}) => {
- return new ExpectedFramesTrackV2(
- engine,
- maxDepth,
- trackKey,
- trackIds,
- );
+ return new ExpectedFramesTrack(engine, maxDepth, trackKey, trackIds);
},
});
}
@@ -138,7 +133,7 @@
trackIds,
kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
trackFactory: ({trackKey}) => {
- return new ActualFramesTrackV2(engine, maxDepth, trackKey, trackIds);
+ return new ActualFramesTrack(engine, maxDepth, trackKey, trackIds);
},
});
}
diff --git a/ui/src/core_plugins/heap_profile/heap_profile_track.ts b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
new file mode 100644
index 0000000..9ecd947
--- /dev/null
+++ b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
@@ -0,0 +1,108 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {ProfileType, LegacySelection} from '../../common/state';
+import {profileType} from '../../controller/flamegraph_controller';
+import {
+ BASE_ROW,
+ BaseSliceTrack,
+ BaseSliceTrackTypes,
+ OnSliceClickArgs,
+ OnSliceOverArgs,
+} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+import {NewTrackArgs} from '../../frontend/track';
+import {Slice} from '../../public';
+import {STR} from '../../trace_processor/query_result';
+
+export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
+
+const HEAP_PROFILE_ROW = {
+ ...BASE_ROW,
+ type: STR,
+};
+type HeapProfileRow = typeof HEAP_PROFILE_ROW;
+interface HeapProfileSlice extends Slice {
+ type: ProfileType;
+}
+
+interface HeapProfileTrackTypes extends BaseSliceTrackTypes {
+ row: HeapProfileRow;
+ slice: HeapProfileSlice;
+}
+
+export class HeapProfileTrack extends BaseSliceTrack<HeapProfileTrackTypes> {
+ private upid: number;
+
+ constructor(args: NewTrackArgs, upid: number) {
+ super(args);
+ this.upid = upid;
+ }
+
+ getSqlSource(): string {
+ return `select
+ *,
+ 0 AS dur,
+ 0 AS depth
+ from (
+ select distinct
+ id,
+ ts,
+ 'heap_profile:' || (select group_concat(distinct heap_name) from heap_profile_allocation where upid = ${this.upid}) AS type
+ from heap_profile_allocation
+ where upid = ${this.upid}
+ union
+ select distinct
+ id,
+ graph_sample_ts AS ts,
+ 'graph' AS type
+ from heap_graph_object
+ where upid = ${this.upid}
+ )`;
+ }
+
+ getRowSpec(): HeapProfileRow {
+ return HEAP_PROFILE_ROW;
+ }
+
+ rowToSlice(row: HeapProfileRow): HeapProfileSlice {
+ const slice = super.rowToSlice(row);
+ let type = row.type;
+ if (type === 'heap_profile:libc.malloc,com.android.art') {
+ type = 'heap_profile:com.android.art,libc.malloc';
+ }
+ slice.type = profileType(type);
+ return slice;
+ }
+
+ onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
+ args.tooltip = [args.slice.type];
+ }
+
+ onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
+ globals.makeSelection(
+ Actions.selectHeapProfile({
+ id: args.slice.id,
+ upid: this.upid,
+ ts: args.slice.ts,
+ type: args.slice.type,
+ }),
+ );
+ }
+
+ protected isSelectionHandled(selection: LegacySelection): boolean {
+ return selection.kind === 'HEAP_PROFILE';
+ }
+}
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 512af1a..c3576f2 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -12,107 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {Actions} from '../../common/actions';
-import {ProfileType, LegacySelection} from '../../common/state';
-import {profileType} from '../../controller/flamegraph_controller';
-import {
- BASE_ROW,
- BaseSliceTrack,
- BaseSliceTrackTypes,
- OnSliceClickArgs,
- OnSliceOverArgs,
-} from '../../frontend/base_slice_track';
import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
-import {globals} from '../../frontend/globals';
-import {NewTrackArgs} from '../../frontend/track';
-import {
- Plugin,
- PluginContextTrace,
- PluginDescriptor,
- Slice,
-} from '../../public';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {HeapProfileTrack} from './heap_profile_track';
export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
-const HEAP_PROFILE_ROW = {
- ...BASE_ROW,
- type: STR,
-};
-type HeapProfileRow = typeof HEAP_PROFILE_ROW;
-interface HeapProfileSlice extends Slice {
- type: ProfileType;
-}
-
-interface HeapProfileTrackTypes extends BaseSliceTrackTypes {
- row: HeapProfileRow;
- slice: HeapProfileSlice;
-}
-
-class HeapProfileTrack extends BaseSliceTrack<HeapProfileTrackTypes> {
- private upid: number;
-
- constructor(args: NewTrackArgs, upid: number) {
- super(args);
- this.upid = upid;
- }
-
- getSqlSource(): string {
- return `select
- *,
- 0 AS dur,
- 0 AS depth
- from (
- select distinct
- id,
- ts,
- 'heap_profile:' || (select group_concat(distinct heap_name) from heap_profile_allocation where upid = ${this.upid}) AS type
- from heap_profile_allocation
- where upid = ${this.upid}
- union
- select distinct
- id,
- graph_sample_ts AS ts,
- 'graph' AS type
- from heap_graph_object
- where upid = ${this.upid}
- )`;
- }
-
- getRowSpec(): HeapProfileRow {
- return HEAP_PROFILE_ROW;
- }
-
- rowToSlice(row: HeapProfileRow): HeapProfileSlice {
- const slice = super.rowToSlice(row);
- let type = row.type;
- if (type === 'heap_profile:libc.malloc,com.android.art') {
- type = 'heap_profile:com.android.art,libc.malloc';
- }
- slice.type = profileType(type);
- return slice;
- }
-
- onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
- args.tooltip = [args.slice.type];
- }
-
- onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
- globals.makeSelection(
- Actions.selectHeapProfile({
- id: args.slice.id,
- upid: this.upid,
- ts: args.slice.ts,
- type: args.slice.type,
- }),
- );
- }
-
- protected isSelectionHandled(selection: LegacySelection): boolean {
- return selection.kind === 'HEAP_PROFILE';
- }
-}
-
class HeapProfilePlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 319fb25..df9b7c0 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -12,25 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {ProfileType, getLegacySelection} from '../../common/state';
import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
-import {TimeScale} from '../../frontend/time_scale';
-import {
- Engine,
- Plugin,
- PluginContextTrace,
- PluginDescriptor,
- Track,
-} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {PerfSamplesProfileTrack} from './perf_samples_profile_track';
export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
@@ -38,211 +24,6 @@
tsStarts: BigInt64Array;
}
-const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)';
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
-
-class PerfSamplesProfileTrack implements Track {
- private centerY = this.getHeight() / 2;
- private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
- private hoveredTs: time | undefined = undefined;
- private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
- private upid: number;
- private engine: Engine;
-
- constructor(engine: Engine, upid: number) {
- this.upid = upid;
- this.engine = engine;
- }
-
- async onUpdate(): Promise<void> {
- await this.fetcher.requestDataForCurrentTime();
- }
-
- async onDestroy(): Promise<void> {
- this.fetcher.dispose();
- }
-
- async onBoundsChange(
- start: time,
- end: time,
- resolution: duration,
- ): Promise<Data> {
- if (this.upid === undefined) {
- return {
- start,
- end,
- resolution,
- length: 0,
- tsStarts: new BigInt64Array(),
- };
- }
- const queryRes = await this.engine.query(`
- select ts, upid from perf_sample
- join thread using (utid)
- where upid = ${this.upid}
- and callsite_id is not null
- order by ts`);
- const numRows = queryRes.numRows();
- const data: Data = {
- start,
- end,
- resolution,
- length: numRows,
- tsStarts: new BigInt64Array(numRows),
- };
-
- const it = queryRes.iter({ts: LONG});
- for (let row = 0; it.valid(); it.next(), row++) {
- data.tsStarts[row] = it.ts;
- }
- return data;
- }
-
- getHeight() {
- return MARGIN_TOP + RECT_HEIGHT - 1;
- }
-
- render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
- const {visibleTimeScale} = globals.timeline;
- const data = this.fetcher.data;
-
- if (data === undefined) return;
-
- for (let i = 0; i < data.tsStarts.length; i++) {
- const centerX = Time.fromRaw(data.tsStarts[i]);
- const selection = getLegacySelection(globals.state);
- const isHovered = this.hoveredTs === centerX;
- const isSelected =
- selection !== null &&
- selection.kind === 'PERF_SAMPLES' &&
- selection.leftTs <= centerX &&
- selection.rightTs >= centerX;
- const strokeWidth = isSelected ? 3 : 0;
- this.drawMarker(
- ctx,
- visibleTimeScale.timeToPx(centerX),
- this.centerY,
- isHovered,
- strokeWidth,
- );
- }
- }
-
- drawMarker(
- ctx: CanvasRenderingContext2D,
- x: number,
- y: number,
- isHovered: boolean,
- strokeWidth: number,
- ): void {
- ctx.beginPath();
- ctx.moveTo(x, y - this.markerWidth);
- ctx.lineTo(x - this.markerWidth, y);
- ctx.lineTo(x, y + this.markerWidth);
- ctx.lineTo(x + this.markerWidth, y);
- ctx.lineTo(x, y - this.markerWidth);
- ctx.closePath();
- ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR;
- ctx.fill();
- if (strokeWidth > 0) {
- ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR;
- ctx.lineWidth = strokeWidth;
- ctx.stroke();
- }
- }
-
- onMouseMove({x, y}: {x: number; y: number}) {
- const data = this.fetcher.data;
- if (data === undefined) return;
- const {visibleTimeScale} = globals.timeline;
- const time = visibleTimeScale.pxToHpTime(x);
- const [left, right] = searchSegment(data.tsStarts, time.toTime());
- const index = this.findTimestampIndex(
- left,
- visibleTimeScale,
- data,
- x,
- y,
- right,
- );
- this.hoveredTs =
- index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
- }
-
- onMouseOut() {
- this.hoveredTs = undefined;
- }
-
- onMouseClick({x, y}: {x: number; y: number}) {
- const data = this.fetcher.data;
- if (data === undefined) return false;
- const {visibleTimeScale} = globals.timeline;
-
- const time = visibleTimeScale.pxToHpTime(x);
- const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
- const index = this.findTimestampIndex(
- left,
- visibleTimeScale,
- data,
- x,
- y,
- right,
- );
-
- if (index !== -1) {
- const ts = Time.fromRaw(data.tsStarts[index]);
- globals.makeSelection(
- Actions.selectPerfSamples({
- id: index,
- upid: this.upid,
- leftTs: ts,
- rightTs: ts,
- type: ProfileType.PERF_SAMPLE,
- }),
- );
- return true;
- }
- return false;
- }
-
- // If the markers overlap the rightmost one will be selected.
- findTimestampIndex(
- left: number,
- timeScale: TimeScale,
- data: Data,
- x: number,
- y: number,
- right: number,
- ): number {
- let index = -1;
- if (left !== -1) {
- const start = Time.fromRaw(data.tsStarts[left]);
- const centerX = timeScale.timeToPx(start);
- if (this.isInMarker(x, y, centerX)) {
- index = left;
- }
- }
- if (right !== -1) {
- const start = Time.fromRaw(data.tsStarts[right]);
- const centerX = timeScale.timeToPx(start);
- if (this.isInMarker(x, y, centerX)) {
- index = right;
- }
- }
- return index;
- }
-
- isInMarker(x: number, y: number, centerX: number) {
- return (
- Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
- );
- }
-}
-
class PerfSamplesProfilePlugin implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
new file mode 100644
index 0000000..aca33de
--- /dev/null
+++ b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
@@ -0,0 +1,237 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {searchSegment} from '../../base/binary_search';
+import {duration, Time, time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {ProfileType, getLegacySelection} from '../../common/state';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {TimeScale} from '../../frontend/time_scale';
+import {Engine, Track} from '../../public';
+import {LONG} from '../../trace_processor/query_result';
+
+export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
+
+export interface Data extends TrackData {
+ tsStarts: BigInt64Array;
+}
+
+const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)';
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 30.5;
+
+export class PerfSamplesProfileTrack implements Track {
+ private centerY = this.getHeight() / 2;
+ private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
+ private hoveredTs: time | undefined = undefined;
+ private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
+ private upid: number;
+ private engine: Engine;
+
+ constructor(engine: Engine, upid: number) {
+ this.upid = upid;
+ this.engine = engine;
+ }
+
+ async onUpdate(): Promise<void> {
+ await this.fetcher.requestDataForCurrentTime();
+ }
+
+ async onDestroy(): Promise<void> {
+ this.fetcher.dispose();
+ }
+
+ async onBoundsChange(
+ start: time,
+ end: time,
+ resolution: duration,
+ ): Promise<Data> {
+ if (this.upid === undefined) {
+ return {
+ start,
+ end,
+ resolution,
+ length: 0,
+ tsStarts: new BigInt64Array(),
+ };
+ }
+ const queryRes = await this.engine.query(`
+ select ts, upid from perf_sample
+ join thread using (utid)
+ where upid = ${this.upid}
+ and callsite_id is not null
+ order by ts`);
+ const numRows = queryRes.numRows();
+ const data: Data = {
+ start,
+ end,
+ resolution,
+ length: numRows,
+ tsStarts: new BigInt64Array(numRows),
+ };
+
+ const it = queryRes.iter({ts: LONG});
+ for (let row = 0; it.valid(); it.next(), row++) {
+ data.tsStarts[row] = it.ts;
+ }
+ return data;
+ }
+
+ getHeight() {
+ return MARGIN_TOP + RECT_HEIGHT - 1;
+ }
+
+ render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
+ const {visibleTimeScale} = globals.timeline;
+ const data = this.fetcher.data;
+
+ if (data === undefined) return;
+
+ for (let i = 0; i < data.tsStarts.length; i++) {
+ const centerX = Time.fromRaw(data.tsStarts[i]);
+ const selection = getLegacySelection(globals.state);
+ const isHovered = this.hoveredTs === centerX;
+ const isSelected =
+ selection !== null &&
+ selection.kind === 'PERF_SAMPLES' &&
+ selection.leftTs <= centerX &&
+ selection.rightTs >= centerX;
+ const strokeWidth = isSelected ? 3 : 0;
+ this.drawMarker(
+ ctx,
+ visibleTimeScale.timeToPx(centerX),
+ this.centerY,
+ isHovered,
+ strokeWidth,
+ );
+ }
+ }
+
+ drawMarker(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ isHovered: boolean,
+ strokeWidth: number,
+ ): void {
+ ctx.beginPath();
+ ctx.moveTo(x, y - this.markerWidth);
+ ctx.lineTo(x - this.markerWidth, y);
+ ctx.lineTo(x, y + this.markerWidth);
+ ctx.lineTo(x + this.markerWidth, y);
+ ctx.lineTo(x, y - this.markerWidth);
+ ctx.closePath();
+ ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR;
+ ctx.fill();
+ if (strokeWidth > 0) {
+ ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR;
+ ctx.lineWidth = strokeWidth;
+ ctx.stroke();
+ }
+ }
+
+ onMouseMove({x, y}: {x: number; y: number}) {
+ const data = this.fetcher.data;
+ if (data === undefined) return;
+ const {visibleTimeScale} = globals.timeline;
+ const time = visibleTimeScale.pxToHpTime(x);
+ const [left, right] = searchSegment(data.tsStarts, time.toTime());
+ const index = this.findTimestampIndex(
+ left,
+ visibleTimeScale,
+ data,
+ x,
+ y,
+ right,
+ );
+ this.hoveredTs =
+ index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
+ }
+
+ onMouseOut() {
+ this.hoveredTs = undefined;
+ }
+
+ onMouseClick({x, y}: {x: number; y: number}) {
+ const data = this.fetcher.data;
+ if (data === undefined) return false;
+ const {visibleTimeScale} = globals.timeline;
+
+ const time = visibleTimeScale.pxToHpTime(x);
+ const [left, right] = searchSegment(data.tsStarts, time.toTime());
+
+ const index = this.findTimestampIndex(
+ left,
+ visibleTimeScale,
+ data,
+ x,
+ y,
+ right,
+ );
+
+ if (index !== -1) {
+ const ts = Time.fromRaw(data.tsStarts[index]);
+ globals.makeSelection(
+ Actions.selectPerfSamples({
+ id: index,
+ upid: this.upid,
+ leftTs: ts,
+ rightTs: ts,
+ type: ProfileType.PERF_SAMPLE,
+ }),
+ );
+ return true;
+ }
+ return false;
+ }
+
+ // If the markers overlap the rightmost one will be selected.
+ findTimestampIndex(
+ left: number,
+ timeScale: TimeScale,
+ data: Data,
+ x: number,
+ y: number,
+ right: number,
+ ): number {
+ let index = -1;
+ if (left !== -1) {
+ const start = Time.fromRaw(data.tsStarts[left]);
+ const centerX = timeScale.timeToPx(start);
+ if (this.isInMarker(x, y, centerX)) {
+ index = left;
+ }
+ }
+ if (right !== -1) {
+ const start = Time.fromRaw(data.tsStarts[right]);
+ const centerX = timeScale.timeToPx(start);
+ if (this.isInMarker(x, y, centerX)) {
+ index = right;
+ }
+ }
+ return index;
+ }
+
+ isInMarker(x: number, y: number, centerX: number) {
+ return (
+ Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+ );
+ }
+}
diff --git a/ui/src/core_plugins/process_summary/index.ts b/ui/src/core_plugins/process_summary/index.ts
index f2a7475..da51bb0 100644
--- a/ui/src/core_plugins/process_summary/index.ts
+++ b/ui/src/core_plugins/process_summary/index.ts
@@ -34,6 +34,8 @@
}
private async addProcessTrackGroups(ctx: PluginContextTrace): Promise<void> {
+ const cpuCount = Math.max(...ctx.trace.cpus, -1) + 1;
+
const result = await ctx.engine.query(`
INCLUDE PERFETTO MODULE android.process_metadata;
@@ -106,7 +108,7 @@
isDebuggable,
},
trackFactory: () => {
- return new ProcessSchedulingTrack(ctx.engine, config);
+ return new ProcessSchedulingTrack(ctx.engine, config, cpuCount);
},
});
} else {
diff --git a/ui/src/core_plugins/process_summary/process_scheduling_track.ts b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
index 28b536f..4b26432 100644
--- a/ui/src/core_plugins/process_summary/process_scheduling_track.ts
+++ b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
@@ -56,23 +56,18 @@
private mousePos?: {x: number; y: number};
private utidHoveredInThisTrack = -1;
private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
- private maxCpu = 0;
+ private cpuCount: number;
private engine: Engine;
private trackUuid = uuidv4Sql();
private config: Config;
- constructor(engine: Engine, config: Config) {
+ constructor(engine: Engine, config: Config, cpuCount: number) {
this.engine = engine;
this.config = config;
+ this.cpuCount = cpuCount;
}
async onCreate(): Promise<void> {
- const cpus = await this.engine.getCpus();
-
- // A process scheduling track should only exist in a trace that has cpus.
- assertTrue(cpus.length > 0);
- this.maxCpu = Math.max(...cpus) + 1;
-
if (this.config.upid !== null) {
await this.engine.query(`
create virtual table process_scheduling_${this.trackUuid}
@@ -119,11 +114,9 @@
async onDestroy(): Promise<void> {
this.fetcher.dispose();
- if (this.engine.isAlive) {
- await this.engine.query(`
- drop table process_scheduling_${this.trackUuid}
- `);
- }
+ await this.engine.tryQuery(`
+ drop table process_scheduling_${this.trackUuid}
+ `);
}
async onBoundsChange(
@@ -142,7 +135,7 @@
end,
resolution,
length: numRows,
- maxCpu: this.maxCpu,
+ maxCpu: this.cpuCount,
starts: new BigInt64Array(numRows),
ends: new BigInt64Array(numRows),
cpus: new Uint32Array(numRows),
diff --git a/ui/src/core_plugins/process_summary/process_summary_track.ts b/ui/src/core_plugins/process_summary/process_summary_track.ts
index 5fa31de..57abd75 100644
--- a/ui/src/core_plugins/process_summary/process_summary_track.ts
+++ b/ui/src/core_plugins/process_summary/process_summary_track.ts
@@ -171,13 +171,11 @@
}
async onDestroy(): Promise<void> {
- if (this.engine.isAlive) {
- await this.engine.query(
- `drop table if exists ${this.tableName(
- 'window',
- )}; drop table if exists ${this.tableName('span')}`,
- );
- }
+ await this.engine.tryQuery(
+ `drop table if exists ${this.tableName(
+ 'window',
+ )}; drop table if exists ${this.tableName('span')}`,
+ );
this.fetcher.dispose();
}
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index 029f818..f6eaebb 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -15,7 +15,6 @@
import {uuidv4} from '../../base/uuid';
import {AddTrackArgs} from '../../common/actions';
import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
import {
BottomTabToSCSAdapter,
NUM,
@@ -25,34 +24,9 @@
PrimaryTrackSortKey,
} from '../../public';
import {Engine} from '../../trace_processor/engine';
-import {
- CustomSqlDetailsPanelConfig,
- CustomSqlTableDefConfig,
- CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
import {ScreenshotTab} from './screenshot_panel';
-
-class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
- static readonly kind = 'dev.perfetto.ScreenshotsTrack';
-
- getSqlDataSource(): CustomSqlTableDefConfig {
- return {
- sqlTableName: 'android_screenshots',
- columns: ['*'],
- };
- }
-
- getDetailsPanel(): CustomSqlDetailsPanelConfig {
- return {
- kind: ScreenshotTab.kind,
- config: {
- sqlTableName: this.tableName,
- title: 'Screenshots',
- },
- };
- }
-}
+import {ScreenshotsTrack} from './screenshots_track';
export type DecideTracksResult = {
tracksToAdd: AddTrackArgs[];
diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts
new file mode 100644
index 0000000..a880b33
--- /dev/null
+++ b/ui/src/core_plugins/screenshots/screenshots_track.ts
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+ CustomSqlDetailsPanelConfig,
+ CustomSqlTableDefConfig,
+ CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
+import {ScreenshotTab} from './screenshot_panel';
+
+export class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
+ static readonly kind = 'dev.perfetto.ScreenshotsTrack';
+
+ getSqlDataSource(): CustomSqlTableDefConfig {
+ return {
+ sqlTableName: 'android_screenshots',
+ columns: ['*'],
+ };
+ }
+
+ getDetailsPanel(): CustomSqlDetailsPanelConfig {
+ return {
+ kind: ScreenshotTab.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Screenshots',
+ },
+ };
+ }
+}
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
index 73e2faf..ab0bcc5 100644
--- a/ui/src/core_plugins/thread_state/index.ts
+++ b/ui/src/core_plugins/thread_state/index.ts
@@ -24,7 +24,7 @@
import {getTrackName} from '../../public/utils';
import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
-import {ThreadStateTrack as ThreadStateTrackV2} from './thread_state_v2';
+import {ThreadStateTrack} from './thread_state_track';
export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
@@ -64,7 +64,7 @@
kind: THREAD_STATE_TRACK_KIND,
utid,
trackFactory: ({trackKey}) => {
- return new ThreadStateTrackV2(
+ return new ThreadStateTrack(
{
engine: ctx.engine,
trackKey,
diff --git a/ui/src/core_plugins/thread_state/thread_state_v2.ts b/ui/src/core_plugins/thread_state/thread_state_track.ts
similarity index 100%
rename from ui/src/core_plugins/thread_state/thread_state_v2.ts
rename to ui/src/core_plugins/thread_state/thread_state_track.ts
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
new file mode 100644
index 0000000..937ba70
--- /dev/null
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -0,0 +1,100 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {
+ getTimeSpanOfSelectionOrVisibleWindow,
+ globals,
+} from '../../frontend/globals';
+import {OmniboxMode} from '../../frontend/omnibox_manager';
+import {verticalScrollToTrack} from '../../frontend/scroll_helper';
+import {
+ Plugin,
+ PluginContextTrace,
+ PluginDescriptor,
+ PromptOption,
+} from '../../public';
+
+class TrackUtilsPlugin implements Plugin {
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ ctx.registerCommand({
+ id: 'perfetto.RunQueryInSelectedTimeWindow',
+ name: `Run query in selected time window`,
+ callback: () => {
+ const window = getTimeSpanOfSelectionOrVisibleWindow();
+ globals.omnibox.setMode(OmniboxMode.Query);
+ globals.omnibox.setText(
+ `select where ts >= ${window.start} and ts < ${window.end}`,
+ );
+ globals.omnibox.focusOmnibox(7);
+ },
+ });
+
+ ctx.registerCommand({
+ // Selects & reveals the first track on the timeline with a given URI.
+ id: 'perfetto.FindTrack',
+ name: 'Find track by URI',
+ callback: async () => {
+ const tracks = globals.trackManager.getAllTracks();
+ const options = tracks.map(({uri}): PromptOption => {
+ return {key: uri, displayName: uri};
+ });
+
+ // Sort tracks in a natural sort order
+ const collator = new Intl.Collator('en', {
+ numeric: true,
+ sensitivity: 'base',
+ });
+ const sortedOptions = options.sort((a, b) => {
+ return collator.compare(a.displayName, b.displayName);
+ });
+
+ try {
+ const selectedUri = await ctx.prompt(
+ 'Choose a track...',
+ sortedOptions,
+ );
+
+ // Find the first track with this URI
+ const firstTrack = Object.values(globals.state.tracks).find(
+ ({uri}) => uri === selectedUri,
+ );
+ if (firstTrack) {
+ console.log(firstTrack);
+ verticalScrollToTrack(firstTrack.key, true);
+ const traceTime = globals.stateTraceTimeTP();
+ globals.makeSelection(
+ Actions.selectArea({
+ area: {
+ start: traceTime.start,
+ end: traceTime.end,
+ tracks: [firstTrack.key],
+ },
+ }),
+ );
+ } else {
+ alert(`No tracks with uri ${selectedUri} on the timeline`);
+ }
+ } catch {
+ // Prompt was probably cancelled - do nothing.
+ }
+ },
+ });
+ }
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: 'perfetto.TrackUtils',
+ plugin: TrackUtilsPlugin,
+};
diff --git a/ui/src/core_plugins/visualised_args/visualized_args_track.ts b/ui/src/core_plugins/visualised_args/visualized_args_track.ts
index 8de85d3..55cf49d 100644
--- a/ui/src/core_plugins/visualised_args/visualized_args_track.ts
+++ b/ui/src/core_plugins/visualised_args/visualized_args_track.ts
@@ -68,9 +68,7 @@
`);
return new DisposableCallback(() => {
- if (this.engine.isAlive) {
- this.engine.query(`drop view ${this.viewName}`);
- }
+ this.engine.tryQuery(`drop view ${this.viewName}`);
});
}
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 09b1036..6c90216 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -18,12 +18,10 @@
import {Trash} from '../base/disposable';
import {findRef} from '../base/dom_utils';
import {FuzzyFinder} from '../base/fuzzy';
-import {assertExists} from '../base/logging';
+import {assertExists, assertUnreachable} from '../base/logging';
import {undoCommonChatAppReplacements} from '../base/string_utils';
-import {duration, Span, time, TimeSpan} from '../base/time';
import {Actions} from '../common/actions';
import {getLegacySelection} from '../common/state';
-import {runQuery} from '../common/queries';
import {
DurationPrecision,
setDurationPrecision,
@@ -31,28 +29,22 @@
TimestampFormat,
} from '../core/timestamp_format';
import {raf} from '../core/raf_scheduler';
-import {Command} from '../public';
-import {Engine} from '../trace_processor/engine';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
+import {Command, Engine, addDebugSliceTrack} from '../public';
import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
import {onClickCopy} from './clipboard';
import {CookieConsent} from './cookie_consent';
-import {globals} from './globals';
+import {getTimeSpanOfSelectionOrVisibleWindow, globals} from './globals';
import {toggleHelp} from './help_modal';
import {Notes} from './notes';
import {Omnibox, OmniboxOption} from './omnibox';
import {addQueryResultsTab} from './query_result_tab';
-import {verticalScrollToTrack} from './scroll_helper';
import {executeSearch} from './search_handler';
import {Sidebar} from './sidebar';
-import {Utid} from './sql_types';
-import {getThreadInfo} from './thread_and_process_info';
import {Topbar} from './topbar';
import {shareTrace} from './trace_attrs';
-import {addDebugSliceTrack} from './debug_tracks';
import {AggregationsTabs} from './aggregation_tab';
import {addSqlTableTab} from './sql_table/tab';
import {SqlTables} from './sql_table/well_known_tables';
@@ -62,12 +54,16 @@
lockSliceSpan,
moveByFocusedFlow,
} from './keyboard_event_handler';
-import {exists} from '../base/utils';
+import {publishPermalinkHash} from './publish';
+import {OmniboxMode, PromptOption} from './omnibox_manager';
+import {Utid} from './sql_types';
+import {getThreadInfo} from './thread_and_process_info';
+import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
function renderPermalink(): m.Children {
- const permalink = globals.state.permalink;
- if (!permalink.requestId || !permalink.hash) return null;
- const url = `${self.location.origin}/#!/?s=${permalink.hash}`;
+ const hash = globals.permalinkHash;
+ if (!hash) return null;
+ const url = `${self.location.origin}/#!/?s=${hash}`;
const linkProps = {title: 'Click to copy the URL', onclick: onClickCopy(url)};
return m('.alert-permalink', [
@@ -75,7 +71,7 @@
m(
'button',
{
- onclick: () => globals.dispatch(Actions.clearPermalink({})),
+ onclick: () => publishPermalinkHash(undefined),
},
m('i.material-icons.disallow-selection', 'close'),
),
@@ -88,25 +84,6 @@
}
}
-interface PromptOption {
- key: string;
- displayName: string;
-}
-
-interface Prompt {
- text: string;
- options?: PromptOption[];
- resolve(result: string): void;
- reject(): void;
-}
-
-enum OmniboxMode {
- Search,
- Query,
- Command,
- Prompt,
-}
-
const criticalPathSliceColumns = {
ts: 'ts',
dur: 'dur',
@@ -138,14 +115,6 @@
export class App implements m.ClassComponent {
private trash = new Trash();
-
- private omniboxMode: OmniboxMode = OmniboxMode.Search;
- private omniboxText = '';
- private queryText = '';
- private omniboxSelectionIndex = 0;
- private focusOmniboxNextRender = false;
- private pendingCursorPlacement = -1;
- private pendingPrompt?: Prompt;
static readonly OMNIBOX_INPUT_REF = 'omnibox';
private omniboxInputEl?: HTMLInputElement;
private recentCommands: string[] = [];
@@ -164,80 +133,6 @@
return engine;
}
- private enterCommandMode(): void {
- this.omniboxMode = OmniboxMode.Command;
- this.resetOmnibox();
- this.rejectPendingPrompt();
- this.focusOmniboxNextRender = true;
-
- raf.scheduleFullRedraw();
- }
-
- private enterQueryMode(): void {
- this.omniboxMode = OmniboxMode.Query;
- this.resetOmnibox();
- this.rejectPendingPrompt();
- this.focusOmniboxNextRender = true;
-
- raf.scheduleFullRedraw();
- }
-
- private enterSearchMode(focusOmnibox: boolean): void {
- this.omniboxMode = OmniboxMode.Search;
- this.resetOmnibox();
- this.rejectPendingPrompt();
-
- if (focusOmnibox) {
- this.focusOmniboxNextRender = true;
- }
-
- globals.dispatch(Actions.setOmniboxMode({mode: 'SEARCH'}));
-
- raf.scheduleFullRedraw();
- }
-
- // Start a prompt. If options are supplied, the user must pick one from the
- // list, otherwise the input is free-form text.
- private prompt(text: string, options?: PromptOption[]): Promise<string> {
- this.omniboxMode = OmniboxMode.Prompt;
- this.resetOmnibox();
- this.rejectPendingPrompt();
-
- const promise = new Promise<string>((resolve, reject) => {
- this.pendingPrompt = {
- text,
- options,
- resolve,
- reject,
- };
- });
-
- this.focusOmniboxNextRender = true;
- raf.scheduleFullRedraw();
-
- return promise;
- }
-
- // Resolve the pending prompt with a value to return to the prompter.
- private resolvePrompt(value: string): void {
- if (this.pendingPrompt) {
- this.pendingPrompt.resolve(value);
- this.pendingPrompt = undefined;
- }
- this.enterSearchMode(false);
- }
-
- // Reject the prompt outright. Doing this will force the owner of the prompt
- // promise to catch, so only do this when things go seriously wrong.
- // Use |resolvePrompt(null)| to indicate cancellation.
- private rejectPrompt(): void {
- if (this.pendingPrompt) {
- this.pendingPrompt.reject();
- this.pendingPrompt = undefined;
- }
- this.enterSearchMode(false);
- }
-
private getFirstUtidOfSelectionOrVisibleWindow(): number {
const selection = getLegacySelection(globals.state);
if (selection && selection.kind === 'AREA') {
@@ -283,7 +178,7 @@
const promptText = 'Select format...';
try {
- const result = await this.prompt(promptText, options);
+ const result = await globals.omnibox.prompt(promptText, options);
setTimestampFormat(result as TimestampFormat);
raf.scheduleFullRedraw();
} catch {
@@ -305,7 +200,7 @@
const promptText = 'Select duration precision mode...';
try {
- const result = await this.prompt(promptText, options);
+ const result = await globals.omnibox.prompt(promptText, options);
setDurationPrecision(result as DurationPrecision);
raf.scheduleFullRedraw();
} catch {
@@ -322,9 +217,8 @@
const engine = this.getEngine();
if (engine !== undefined && trackUtid != 0) {
- await runQuery(
+ await engine.query(
`INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
- engine,
);
await addDebugSliceTrack(
engine,
@@ -365,9 +259,8 @@
const engine = this.getEngine();
if (engine !== undefined && trackUtid != 0) {
- await runQuery(
+ await engine.query(
`INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
- engine,
);
await addDebugSliceTrack(
engine,
@@ -454,19 +347,19 @@
{
id: 'perfetto.OpenCommandPalette',
name: 'Open command palette',
- callback: () => this.enterCommandMode(),
+ callback: () => globals.omnibox.setMode(OmniboxMode.Command),
defaultHotkey: '!Mod+Shift+P',
},
{
id: 'perfetto.RunQuery',
name: 'Run query',
- callback: () => this.enterQueryMode(),
+ callback: () => globals.omnibox.setMode(OmniboxMode.Query),
defaultHotkey: '!Mod+O',
},
{
id: 'perfetto.Search',
name: 'Search',
- callback: () => this.enterSearchMode(true),
+ callback: () => globals.omnibox.setMode(OmniboxMode.Search),
defaultHotkey: '/',
},
{
@@ -476,16 +369,6 @@
defaultHotkey: '?',
},
{
- id: 'perfetto.RunQueryInSelectedTimeWindow',
- name: `Run query in selected time window`,
- callback: () => {
- const window = getTimeSpanOfSelectionOrVisibleWindow();
- this.enterQueryMode();
- this.queryText = `select where ts >= ${window.start} and ts < ${window.end}`;
- this.pendingCursorPlacement = 7;
- },
- },
- {
id: 'perfetto.CopyTimeWindow',
name: `Copy selected time window to clipboard`,
callback: () => {
@@ -495,56 +378,6 @@
},
},
{
- // Selects & reveals the first track on the timeline with a given URI.
- id: 'perfetto.FindTrack',
- name: 'Find track by URI',
- callback: async () => {
- const tracks = globals.trackManager.getAllTracks();
- const options = tracks.map(({uri}): PromptOption => {
- return {key: uri, displayName: uri};
- });
-
- // Sort tracks in a natural sort order
- const collator = new Intl.Collator('en', {
- numeric: true,
- sensitivity: 'base',
- });
- const sortedOptions = options.sort((a, b) => {
- return collator.compare(a.displayName, b.displayName);
- });
-
- try {
- const selectedUri = await this.prompt(
- 'Choose a track...',
- sortedOptions,
- );
-
- // Find the first track with this URI
- const firstTrack = Object.values(globals.state.tracks).find(
- ({uri}) => uri === selectedUri,
- );
- if (firstTrack) {
- console.log(firstTrack);
- verticalScrollToTrack(firstTrack.key, true);
- const traceTime = globals.stateTraceTimeTP();
- globals.makeSelection(
- Actions.selectArea({
- area: {
- start: traceTime.start,
- end: traceTime.end,
- tracks: [firstTrack.key],
- },
- }),
- );
- } else {
- alert(`No tracks with uri ${selectedUri} on the timeline`);
- }
- } catch {
- // Prompt was probably cancelled - do nothing.
- }
- },
- },
- {
id: 'perfetto.FocusSelection',
name: 'Focus current selection',
callback: () => findCurrentSelection(),
@@ -620,8 +453,8 @@
if (selection !== null && selection.kind === 'AREA') {
const area = globals.state.areas[selection.areaId];
const coversEntireTimeRange =
- globals.traceTime.start === area.start &&
- globals.traceTime.end === area.end;
+ globals.traceContext.start === area.start &&
+ globals.traceContext.end === area.end;
if (!coversEntireTimeRange) {
// If the current selection is an area which does not cover the
// entire time range, preserve the list of selected tracks and
@@ -636,7 +469,7 @@
// If the current selection is not an area, select all.
tracksToSelect = Object.keys(globals.state.tracks);
}
- const {start, end} = globals.traceTime;
+ const {start, end} = globals.traceContext;
globals.dispatch(
Actions.selectArea({
area: {
@@ -655,18 +488,6 @@
return this.cmds;
}
- private rejectPendingPrompt() {
- if (this.pendingPrompt) {
- this.pendingPrompt.reject();
- this.pendingPrompt = undefined;
- }
- }
-
- private resetOmnibox() {
- this.omniboxText = '';
- this.omniboxSelectionIndex = 0;
- }
-
private renderOmnibox(): m.Children {
const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3;
const engineIsBusy =
@@ -683,22 +504,23 @@
);
}
- if (this.omniboxMode === OmniboxMode.Command) {
+ const omniboxMode = globals.omnibox.omniboxMode;
+
+ if (omniboxMode === OmniboxMode.Command) {
return this.renderCommandOmnibox();
- } else if (this.omniboxMode === OmniboxMode.Prompt) {
+ } else if (omniboxMode === OmniboxMode.Prompt) {
return this.renderPromptOmnibox();
- } else if (this.omniboxMode === OmniboxMode.Query) {
+ } else if (omniboxMode === OmniboxMode.Query) {
return this.renderQueryOmnibox();
- } else if (this.omniboxMode === OmniboxMode.Search) {
+ } else if (omniboxMode === OmniboxMode.Search) {
return this.renderSearchOmnibox();
} else {
- const x: never = this.omniboxMode;
- throw new Error(`Unhandled omnibox mode ${x}`);
+ assertUnreachable(omniboxMode);
}
}
renderPromptOmnibox(): m.Children {
- const prompt = assertExists(this.pendingPrompt);
+ const prompt = assertExists(globals.omnibox.pendingPrompt);
let options: OmniboxOption[] | undefined = undefined;
@@ -707,7 +529,7 @@
prompt.options,
({displayName}) => displayName,
);
- const result = fuzzy.find(this.omniboxText);
+ const result = fuzzy.find(globals.omnibox.text);
options = result.map((result) => {
return {
key: result.item.key,
@@ -717,27 +539,27 @@
}
return m(Omnibox, {
- value: this.omniboxText,
+ value: globals.omnibox.text,
placeholder: prompt.text,
inputRef: App.OMNIBOX_INPUT_REF,
extraClasses: 'prompt-mode',
closeOnOutsideClick: true,
options,
- selectedOptionIndex: this.omniboxSelectionIndex,
+ selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
onSelectedOptionChanged: (index) => {
- this.omniboxSelectionIndex = index;
+ globals.omnibox.setOmniboxSelectionIndex(index);
raf.scheduleFullRedraw();
},
onInput: (value) => {
- this.omniboxText = value;
- this.omniboxSelectionIndex = 0;
+ globals.omnibox.setText(value);
+ globals.omnibox.setOmniboxSelectionIndex(0);
raf.scheduleFullRedraw();
},
onSubmit: (value, _alt) => {
- this.resolvePrompt(value);
+ globals.omnibox.resolvePrompt(value);
},
onClose: () => {
- this.rejectPrompt();
+ globals.omnibox.rejectPrompt();
},
});
}
@@ -746,7 +568,7 @@
const cmdMgr = globals.commandManager;
// Fuzzy-filter commands by the filter string.
- const filteredCmds = cmdMgr.fuzzyFilterCommands(this.omniboxText);
+ const filteredCmds = cmdMgr.fuzzyFilterCommands(globals.omnibox.text);
// Create an array of commands with attached heuristics from the recent
// command register.
@@ -777,36 +599,35 @@
});
return m(Omnibox, {
- value: this.omniboxText,
+ value: globals.omnibox.text,
placeholder: 'Filter commands...',
inputRef: App.OMNIBOX_INPUT_REF,
extraClasses: 'command-mode',
options,
closeOnSubmit: true,
closeOnOutsideClick: true,
- selectedOptionIndex: this.omniboxSelectionIndex,
+ selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
onSelectedOptionChanged: (index) => {
- this.omniboxSelectionIndex = index;
+ globals.omnibox.setOmniboxSelectionIndex(index);
raf.scheduleFullRedraw();
},
onInput: (value) => {
- this.omniboxText = value;
- this.omniboxSelectionIndex = 0;
+ globals.omnibox.setText(value);
+ globals.omnibox.setOmniboxSelectionIndex(0);
raf.scheduleFullRedraw();
},
onClose: () => {
if (this.omniboxInputEl) {
this.omniboxInputEl.blur();
}
- this.enterSearchMode(false);
- raf.scheduleFullRedraw();
+ globals.omnibox.reset();
},
onSubmit: (key: string) => {
this.addRecentCommand(key);
cmdMgr.runCommand(key);
},
onGoBack: () => {
- this.enterSearchMode(false);
+ globals.omnibox.reset();
},
});
}
@@ -822,13 +643,13 @@
renderQueryOmnibox(): m.Children {
const ph = 'e.g. select * from sched left join thread using(utid) limit 10';
return m(Omnibox, {
- value: this.queryText,
+ value: globals.omnibox.text,
placeholder: ph,
inputRef: App.OMNIBOX_INPUT_REF,
extraClasses: 'query-mode',
onInput: (value) => {
- this.queryText = value;
+ globals.omnibox.setText(value);
raf.scheduleFullRedraw();
},
onSubmit: (query, alt) => {
@@ -840,15 +661,15 @@
addQueryResultsTab(config, tag);
},
onClose: () => {
- this.queryText = '';
+ globals.omnibox.setText('');
if (this.omniboxInputEl) {
this.omniboxInputEl.blur();
}
- this.enterSearchMode(false);
+ globals.omnibox.reset();
raf.scheduleFullRedraw();
},
onGoBack: () => {
- this.enterSearchMode(false);
+ globals.omnibox.reset();
},
});
}
@@ -865,10 +686,10 @@
onInput: (value, prev) => {
if (prev === '') {
if (value === '>') {
- this.enterCommandMode();
+ globals.omnibox.setMode(OmniboxMode.Command);
return;
} else if (value === ':') {
- this.enterQueryMode();
+ globals.omnibox.setMode(OmniboxMode.Query);
return;
}
}
@@ -987,32 +808,20 @@
}
private maybeFocusOmnibar() {
- if (this.focusOmniboxNextRender) {
+ if (globals.omnibox.focusOmniboxNextRender) {
const omniboxEl = this.omniboxInputEl;
if (omniboxEl) {
omniboxEl.focus();
- if (this.pendingCursorPlacement === -1) {
+ if (globals.omnibox.pendingCursorPlacement === undefined) {
omniboxEl.select();
} else {
omniboxEl.setSelectionRange(
- this.pendingCursorPlacement,
- this.pendingCursorPlacement,
+ globals.omnibox.pendingCursorPlacement,
+ globals.omnibox.pendingCursorPlacement,
);
- this.pendingCursorPlacement = -1;
}
}
- this.focusOmniboxNextRender = false;
+ globals.omnibox.clearOmniboxFocusFlag();
}
}
}
-
-// Returns the time span of the current selection, or the visible window if
-// there is no current selection.
-function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
- const range = globals.findTimeRangeOfSelection();
- if (exists(range)) {
- return new TimeSpan(range.start, range.end);
- } else {
- return globals.stateVisibleTime();
- }
-}
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 29a5966..d010f95 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -688,9 +688,7 @@
this.initState.dispose();
this.initState = undefined;
}
- if (this.engine.isAlive) {
- await this.engine.query(`drop table if exists ${this.getTableName()}`);
- }
+ await this.engine.tryQuery(`drop table if exists ${this.getTableName()}`);
}
// Compute the range of values to display and range label.
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 720ace2..253ed41 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -666,9 +666,7 @@
this.initState.dispose();
this.initState = undefined;
}
- if (this.engine.isAlive) {
- await this.engine.execute(`drop table ${this.getTableName()}`);
- }
+ await this.engine.tryQuery(`drop table ${this.getTableName()}`);
}
// This method figures out if the visible window is outside the bounds of
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 91cf54f..dd13a8d 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -17,7 +17,6 @@
import {Icons} from '../base/semantic_icons';
import {duration, Time, TimeSpan} from '../base/time';
import {exists} from '../base/utils';
-import {runQuery} from '../common/queries';
import {raf} from '../core/raf_scheduler';
import {Engine} from '../trace_processor/engine';
import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result';
@@ -104,17 +103,18 @@
run: (slice: SliceDetails) => {
const engine = getEngine();
if (engine === undefined) return;
- runQuery(
- `
+ engine
+ .query(
+ `
INCLUDE PERFETTO MODULE android.binder;
INCLUDE PERFETTO MODULE android.monitor_contention;
`,
- engine,
- ).then(() =>
- addDebugSliceTrack(
- engine,
- {
- sqlSource: `
+ )
+ .then(() =>
+ addDebugSliceTrack(
+ engine,
+ {
+ sqlSource: `
WITH merged AS (
SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
FROM android_binder_txns tx
@@ -151,14 +151,14 @@
AND short_blocked_method IS NOT NULL
ORDER BY depth
) SELECT ts, dur, name FROM merged`,
- },
- `Binder names (${getProcessNameFromSlice(
- slice,
- )}:${getThreadNameFromSlice(slice)})`,
- {ts: 'ts', dur: 'dur', name: 'name'},
- [],
- ),
- );
+ },
+ `Binder names (${getProcessNameFromSlice(
+ slice,
+ )}:${getThreadNameFromSlice(slice)})`,
+ {ts: 'ts', dur: 'dur', name: 'name'},
+ [],
+ ),
+ );
},
},
];
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4ffa6cf..69097ec 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -71,8 +71,8 @@
for (const trackId of getTrackIds(track)) {
this.trackIdToTrackPanel.set(trackId, {panel, yStart});
}
- } else if (exists(panel.trackGroupId)) {
- this.groupIdToTrackGroupPanel.set(panel.trackGroupId, {
+ } else if (exists(panel.groupKey)) {
+ this.groupIdToTrackGroupPanel.set(panel.groupKey, {
panel,
yStart,
height,
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 04cdc17..46d6cbe 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -56,6 +56,7 @@
import {PxSpan, TimeScale} from './time_scale';
import {SelectionManager, LegacySelection} from '../core/selection_manager';
import {exists} from '../base/utils';
+import {OmniboxManager} from './omnibox_manager';
const INSTANT_FOCUS_DURATION = 1n;
const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -221,7 +222,7 @@
pendingScrollId: number | undefined;
}
-export interface TraceTime {
+export interface TraceContext {
readonly start: time;
readonly end: time;
@@ -237,14 +238,22 @@
// Trace TZ is like UTC but keeps into account also the timezone_off_mins
// recorded into the trace, to show timestamps in the device local time.
readonly traceTzOffset: time;
+
+ // The list of CPUs in the trace
+ readonly cpus: number[];
+
+ // The number of gpus in the trace
+ readonly gpuCount: number;
}
-export const defaultTraceTime: TraceTime = {
+export const defaultTraceContext: TraceContext = {
start: Time.ZERO,
end: Time.fromSeconds(10),
realtimeOffset: Time.ZERO,
utcOffset: Time.ZERO,
traceTzOffset: Time.ZERO,
+ cpus: [],
+ gpuCount: 0,
};
/**
@@ -291,12 +300,15 @@
private _selectionManager = new SelectionManager(this._store);
private _hasFtrace: boolean = false;
+ omnibox = new OmniboxManager();
+
scrollToTrackKey?: string | number;
httpRpcState: HttpRpcState = {connected: false};
newVersionAvailable = false;
showPanningHint = false;
+ permalinkHash?: string;
- traceTime = defaultTraceTime;
+ traceContext = defaultTraceContext;
// TODO(hjd): Remove once we no longer need to update UUID on redraw.
private _publishRedraw?: () => void = undefined;
@@ -716,19 +728,19 @@
// Get a timescale that covers the entire trace
getTraceTimeScale(pxSpan: PxSpan): TimeScale {
- const {start, end} = this.traceTime;
+ const {start, end} = this.traceContext;
const traceTime = HighPrecisionTimeSpan.fromTime(start, end);
return TimeScale.fromHPTimeSpan(traceTime, pxSpan);
}
// Get the trace time bounds
stateTraceTime(): Span<HighPrecisionTime> {
- const {start, end} = this.traceTime;
+ const {start, end} = this.traceContext;
return HighPrecisionTimeSpan.fromTime(start, end);
}
stateTraceTimeTP(): Span<time, duration> {
- const {start, end} = this.traceTime;
+ const {start, end} = this.traceContext;
return new TimeSpan(start, end);
}
@@ -762,14 +774,14 @@
switch (fmt) {
case TimestampFormat.Timecode:
case TimestampFormat.Seconds:
- return this.traceTime.start;
+ return this.traceContext.start;
case TimestampFormat.Raw:
case TimestampFormat.RawLocale:
return Time.ZERO;
case TimestampFormat.UTC:
- return this.traceTime.utcOffset;
+ return this.traceContext.utcOffset;
case TimestampFormat.TraceTz:
- return this.traceTime.traceTzOffset;
+ return this.traceContext.traceTzOffset;
default:
const x: never = fmt;
throw new Error(`Unsupported format ${x}`);
@@ -859,4 +871,15 @@
}
}
+// Returns the time span of the current selection, or the visible window if
+// there is no current selection.
+export function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
+ const range = globals.findTimeRangeOfSelection();
+ if (exists(range)) {
+ return new TimeSpan(range.start, range.end);
+ } else {
+ return globals.stateVisibleTime();
+ }
+}
+
export const globals = new Globals();
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index c583e2d..0dc71bd 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -57,12 +57,9 @@
export class NotesPanel implements Panel {
readonly kind = 'panel';
readonly selectable = false;
- readonly trackKey = undefined;
hoveredX: null | number = null;
- constructor(readonly key: string) {}
-
render(): m.Children {
const allCollapsed = Object.values(globals.state.trackGroups).every(
(group) => group.collapsed,
diff --git a/ui/src/frontend/omnibox_manager.ts b/ui/src/frontend/omnibox_manager.ts
new file mode 100644
index 0000000..bb09810
--- /dev/null
+++ b/ui/src/frontend/omnibox_manager.ts
@@ -0,0 +1,155 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {raf} from '../core/raf_scheduler';
+
+export enum OmniboxMode {
+ Search,
+ Query,
+ Command,
+ Prompt,
+}
+
+export interface PromptOption {
+ key: string;
+ displayName: string;
+}
+
+interface Prompt {
+ text: string;
+ options?: PromptOption[];
+ resolve(result: string): void;
+ reject(): void;
+}
+
+const defaultMode = OmniboxMode.Search;
+
+export class OmniboxManager {
+ private _omniboxMode = defaultMode;
+ private _focusOmniboxNextRender = false;
+ private _pendingCursorPlacement?: number;
+ private _pendingPrompt?: Prompt;
+ private _text = '';
+ private _omniboxSelectionIndex = 0;
+
+ get omniboxMode(): OmniboxMode {
+ return this._omniboxMode;
+ }
+
+ get pendingPrompt(): Prompt | undefined {
+ return this._pendingPrompt;
+ }
+
+ get text(): string {
+ return this._text;
+ }
+
+ get omniboxSelectionIndex(): number {
+ return this._omniboxSelectionIndex;
+ }
+
+ get focusOmniboxNextRender(): boolean {
+ return this._focusOmniboxNextRender;
+ }
+
+ get pendingCursorPlacement(): number | undefined {
+ return this._pendingCursorPlacement;
+ }
+
+ setText(value: string): void {
+ this._text = value;
+ }
+
+ setOmniboxSelectionIndex(index: number): void {
+ this._omniboxSelectionIndex = index;
+ }
+
+ focusOmnibox(cursorPlacement?: number): void {
+ this._focusOmniboxNextRender = true;
+ this._pendingCursorPlacement = cursorPlacement;
+ raf.scheduleFullRedraw();
+ }
+
+ clearOmniboxFocusFlag(): void {
+ this._focusOmniboxNextRender = false;
+ this._pendingCursorPlacement = undefined;
+ }
+
+ setMode(mode: OmniboxMode): void {
+ this._omniboxMode = mode;
+ this.resetOmniboxText();
+ this.rejectPendingPrompt();
+ raf.scheduleFullRedraw();
+ }
+
+ // Start a prompt. If options are supplied, the user must pick one from the
+ // list, otherwise the input is free-form text.
+ prompt(text: string, options?: PromptOption[]): Promise<string> {
+ this._omniboxMode = OmniboxMode.Prompt;
+ this.resetOmniboxText();
+ this.rejectPendingPrompt();
+
+ const promise = new Promise<string>((resolve, reject) => {
+ this._pendingPrompt = {
+ text,
+ options,
+ resolve,
+ reject,
+ };
+ });
+
+ this._focusOmniboxNextRender = true;
+ raf.scheduleFullRedraw();
+
+ return promise;
+ }
+
+ // Resolve the pending prompt with a value to return to the prompter.
+ resolvePrompt(value: string): void {
+ if (this._pendingPrompt) {
+ this._pendingPrompt.resolve(value);
+ this._pendingPrompt = undefined;
+ }
+ this.setMode(OmniboxMode.Search);
+ }
+
+ // Reject the prompt outright. Doing this will force the owner of the prompt
+ // promise to catch, so only do this when things go seriously wrong.
+ // Use |resolvePrompt(null)| to indicate cancellation.
+ rejectPrompt(): void {
+ if (this._pendingPrompt) {
+ this._pendingPrompt.reject();
+ this._pendingPrompt = undefined;
+ }
+ this.setMode(OmniboxMode.Search);
+ }
+
+ reset(): void {
+ this.setMode(defaultMode);
+ this.resetOmniboxText();
+ raf.scheduleFullRedraw();
+ }
+
+ private rejectPendingPrompt() {
+ if (this._pendingPrompt) {
+ this._pendingPrompt.reject();
+ this._pendingPrompt = undefined;
+ }
+ }
+
+ private resetOmniboxText() {
+ this._text = '';
+ this._omniboxSelectionIndex = 0;
+ }
+}
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index a47434a..4ff95bb 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -42,7 +42,6 @@
private static HANDLE_SIZE_PX = 5;
readonly kind = 'panel';
readonly selectable = false;
- readonly trackKey = undefined;
private width = 0;
private gesture?: DragGestureHandler;
@@ -51,8 +50,6 @@
private dragStrategy?: DragStrategy;
private readonly boundOnMouseMove = this.onMouseMove.bind(this);
- constructor(readonly key: string) {}
-
// Must explicitly type now; arguments types are no longer auto-inferred.
// https://github.com/Microsoft/TypeScript/issues/1373
onupdate({dom}: m.CVnodeDOM) {
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 9214430..5611a19 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -47,22 +47,20 @@
const CANVAS_OVERDRAW_PX = 100;
export interface Panel {
- kind: 'panel';
+ readonly kind: 'panel';
render(): m.Children;
- selectable: boolean;
- key: string;
- trackKey?: string;
- trackGroupId?: string;
+ readonly selectable: boolean;
+ readonly trackKey?: string; // Defined if this panel represents are track
+ readonly groupKey?: string; // Defined if this panel represents a group - i.e. a group summary track
renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void;
getSliceRect?(tStart: time, tDur: time, depth: number): SliceRect | undefined;
}
export interface PanelGroup {
- kind: 'group';
- collapsed: boolean;
- header: Panel;
- childPanels: Panel[];
- trackGroupId: string;
+ readonly kind: 'group';
+ readonly collapsed: boolean;
+ readonly header: Panel;
+ readonly childPanels: Panel[];
}
export type PanelOrGroup = Panel | PanelGroup;
@@ -74,7 +72,7 @@
}
interface PanelInfo {
- id: string; // Can be == '' for singleton panels.
+ trackOrGroupKey: string; // Can be == '' for singleton panels.
panel: Panel;
height: number;
width: number;
@@ -91,7 +89,7 @@
private panelContainerHeight = 0;
// Updated every render cycle in the view() hook
- private panelByKey = new Map<string, Panel>();
+ private panelById = new Map<string, Panel>();
// Updated every render cycle in the oncreate/onupdate hook
private panelInfos: PanelInfo[] = [];
@@ -179,11 +177,11 @@
tracks.push(panel.trackKey);
continue;
}
- if (panel.trackGroupId !== undefined) {
- const trackGroup = globals.state.trackGroups[panel.trackGroupId];
+ if (panel.groupKey !== undefined) {
+ const trackGroup = globals.state.trackGroups[panel.groupKey];
// Only select a track group and all child tracks if it is closed.
if (trackGroup.collapsed) {
- tracks.push(panel.trackGroupId);
+ tracks.push(panel.groupKey);
for (const track of trackGroup.tracks) {
tracks.push(track);
}
@@ -258,37 +256,40 @@
this.trash.dispose();
}
- renderPanel(node: Panel, key: string, extraClass = ''): m.Vnode {
- assertFalse(this.panelByKey.has(key));
- this.panelByKey.set(key, node);
- return m(`.pf-panel${extraClass}`, {key, 'data-key': key}, node.render());
+ renderPanel(node: Panel, panelId: string, extraClass = ''): m.Vnode {
+ assertFalse(this.panelById.has(panelId));
+ this.panelById.set(panelId, node);
+ return m(
+ `.pf-panel${extraClass}`,
+ {'data-panel-id': panelId},
+ node.render(),
+ );
}
// Render a tree of panels into one vnode. Argument `path` is used to build
// `key` attribute for intermediate tree vnodes: otherwise Mithril internals
// will complain about keyed and non-keyed vnodes mixed together.
- renderTree(node: PanelOrGroup, path: string): m.Vnode {
+ renderTree(node: PanelOrGroup, panelId: string): m.Vnode {
if (node.kind === 'group') {
return m(
'div.pf-panel-group',
- {key: path},
this.renderPanel(
node.header,
- `${path}-header`,
+ `${panelId}-header`,
node.collapsed ? '' : '.pf-sticky',
),
...node.childPanels.map((child, index) =>
- this.renderTree(child, `${path}-${index}`),
+ this.renderTree(child, `${panelId}-${index}`),
),
);
}
- return this.renderPanel(node, assertExists(node.key));
+ return this.renderPanel(node, panelId);
}
view({attrs}: m.CVnode<PanelContainerAttrs>) {
- this.panelByKey.clear();
+ this.panelById.clear();
const children = attrs.panels.map((panel, index) =>
- this.renderTree(panel, `track-tree-${index}`),
+ this.renderTree(panel, `${index}`),
);
return m(
@@ -316,14 +317,15 @@
this.panelContainerHeight = domRect.height;
dom.querySelectorAll('.pf-panel').forEach((panelElement) => {
- const key = assertExists(panelElement.getAttribute('data-key'));
- const panel = assertExists(this.panelByKey.get(key));
+ const panelHTMLElement = toHTMLElement(panelElement);
+ const panelId = assertExists(panelHTMLElement.dataset.panelId);
+ const panel = assertExists(this.panelById.get(panelId));
// NOTE: the id can be undefined for singletons like overview timeline.
- const id = panel.trackKey || panel.trackGroupId || '';
+ const key = panel.trackKey || panel.groupKey || '';
const rect = panelElement.getBoundingClientRect();
this.panelInfos.push({
- id,
+ trackOrGroupKey: key,
height: rect.height,
width: rect.width,
clientX: rect.x,
@@ -440,7 +442,7 @@
let selectedTracksMaxY = this.panelContainerTop;
let trackFromCurrentContainerSelected = false;
for (let i = 0; i < this.panelInfos.length; i++) {
- if (area.tracks.includes(this.panelInfos[i].id)) {
+ if (area.tracks.includes(this.panelInfos[i].trackOrGroupKey)) {
trackFromCurrentContainerSelected = true;
selectedTracksMinY = Math.min(
selectedTracksMinY,
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
new file mode 100644
index 0000000..888fe55
--- /dev/null
+++ b/ui/src/frontend/permalink.ts
@@ -0,0 +1,250 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {produce} from 'immer';
+import {assertExists} from '../base/logging';
+import {runValidator} from '../base/validators';
+import {Actions} from '../common/actions';
+import {ConversionJobStatus} from '../common/conversion_jobs';
+import {
+ createEmptyNonSerializableState,
+ createEmptyState,
+} from '../common/empty_state';
+import {EngineConfig, ObjectById, STATE_VERSION, State} from '../common/state';
+import {
+ BUCKET_NAME,
+ TraceGcsUploader,
+ buggyToSha256,
+ deserializeStateObject,
+ saveState,
+ toSha256,
+} from '../common/upload_utils';
+import {
+ RecordConfig,
+ recordConfigValidator,
+} from '../controller/record_config_types';
+import {globals} from './globals';
+import {
+ publishConversionJobStatusUpdate,
+ publishPermalinkHash,
+} from './publish';
+import {Router} from './router';
+import {showModal} from '../widgets/modal';
+
+export interface PermalinkOptions {
+ isRecordingConfig?: boolean;
+}
+
+export async function createPermalink(
+ options: PermalinkOptions = {},
+): Promise<void> {
+ const {isRecordingConfig = false} = options;
+ const jobName = 'create_permalink';
+ publishConversionJobStatusUpdate({
+ jobName,
+ jobStatus: ConversionJobStatus.InProgress,
+ });
+
+ try {
+ const hash = await createPermalinkInternal(isRecordingConfig);
+ publishPermalinkHash(hash);
+ } finally {
+ publishConversionJobStatusUpdate({
+ jobName,
+ jobStatus: ConversionJobStatus.NotRunning,
+ });
+ }
+}
+
+async function createPermalinkInternal(
+ isRecordingConfig: boolean,
+): Promise<string> {
+ let uploadState: State | RecordConfig = globals.state;
+
+ if (isRecordingConfig) {
+ uploadState = globals.state.recordConfig;
+ } else {
+ const engine = assertExists(globals.getCurrentEngine());
+ let dataToUpload: File | ArrayBuffer | undefined = undefined;
+ let traceName = `trace ${engine.id}`;
+ if (engine.source.type === 'FILE') {
+ dataToUpload = engine.source.file;
+ traceName = dataToUpload.name;
+ } else if (engine.source.type === 'ARRAY_BUFFER') {
+ dataToUpload = engine.source.buffer;
+ } else if (engine.source.type !== 'URL') {
+ throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
+ }
+
+ if (dataToUpload !== undefined) {
+ updateStatus(`Uploading ${traceName}`);
+ const uploader = new TraceGcsUploader(dataToUpload, () => {
+ switch (uploader.state) {
+ case 'UPLOADING':
+ const statusTxt = `Uploading ${uploader.getEtaString()}`;
+ updateStatus(statusTxt);
+ break;
+ case 'UPLOADED':
+ // Convert state to use URLs and remove permalink.
+ const url = uploader.uploadedUrl;
+ uploadState = produce(globals.state, (draft) => {
+ assertExists(draft.engine).source = {type: 'URL', url};
+ });
+ break;
+ case 'ERROR':
+ updateStatus(`Upload failed ${uploader.error}`);
+ break;
+ } // switch (state)
+ }); // onProgress
+ await uploader.waitForCompletion();
+ }
+ }
+
+ // Upload state.
+ updateStatus(`Creating permalink...`);
+ const hash = await saveState(uploadState);
+ updateStatus(`Permalink ready`);
+ return hash;
+}
+
+function updateStatus(msg: string): void {
+ // TODO(hjd): Unify loading updates.
+ globals.dispatch(
+ Actions.updateStatus({
+ msg,
+ timestamp: Date.now() / 1000,
+ }),
+ );
+}
+
+export async function loadPermalink(hash: string): Promise<void> {
+ // Otherwise, this is a request to load the permalink.
+ const stateOrConfig = await loadState(hash);
+
+ if (isRecordConfig(stateOrConfig)) {
+ // This permalink state only contains a RecordConfig. Show the
+ // recording page with the config, but keep other state as-is.
+ const validConfig = runValidator(
+ recordConfigValidator,
+ stateOrConfig as unknown,
+ ).result;
+ globals.dispatch(Actions.setRecordConfig({config: validConfig}));
+ Router.navigate('#!/record');
+ return;
+ }
+ globals.dispatch(Actions.setState({newState: stateOrConfig}));
+}
+
+async function loadState(id: string): Promise<State | RecordConfig> {
+ const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(
+ `Could not fetch permalink.\n` +
+ `Are you sure the id (${id}) is correct?\n` +
+ `URL: ${url}`,
+ );
+ }
+ const text = await response.text();
+ const stateHash = await toSha256(text);
+ const state = deserializeStateObject<State>(text);
+ if (stateHash !== id) {
+ // Old permalinks incorrectly dropped some digits from the
+ // hexdigest of the SHA256. We don't want to invalidate those
+ // links so we also compute the old string and try that here
+ // also.
+ const buggyStateHash = await buggyToSha256(text);
+ if (buggyStateHash !== id) {
+ throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
+ }
+ }
+ if (!isRecordConfig(state)) {
+ return upgradeState(state);
+ }
+ return state;
+}
+
+function isRecordConfig(
+ stateOrConfig: State | RecordConfig,
+): stateOrConfig is RecordConfig {
+ const mode = (stateOrConfig as {mode?: string}).mode;
+ return (
+ mode !== undefined &&
+ ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode)
+ );
+}
+
+function upgradeState(state: State): State {
+ if (state.engine !== undefined && state.engine.source.type !== 'URL') {
+ // All permalink traces should be modified to have a source.type=URL
+ // pointing to the uploaded trace. Due to a bug in some older version
+ // of the UI (b/327049372), an upload failure can end up with a state that
+ // has type=FILE but a null file object. If this happens, invalidate the
+ // trace and show a message.
+ showModal({
+ title: 'Cannot load trace permalink',
+ content: m(
+ 'div',
+ 'The permalink stored on the server is corrupted ' +
+ 'and cannot be loaded.',
+ ),
+ });
+ return createEmptyState();
+ }
+
+ if (state.version !== STATE_VERSION) {
+ const newState = createEmptyState();
+ // Old permalinks from state versions prior to version 24
+ // have multiple engines of which only one is identified as the
+ // current engine via currentEngineId. Handle this case:
+ if (isMultiEngineState(state)) {
+ const engineId = state.currentEngineId;
+ if (engineId !== undefined) {
+ newState.engine = state.engines[engineId];
+ }
+ } else {
+ newState.engine = state.engine;
+ }
+
+ if (newState.engine !== undefined) {
+ newState.engine.ready = false;
+ }
+ const message =
+ `Unable to parse old state version. Discarding state ` +
+ `and loading trace.`;
+ console.warn(message);
+ updateStatus(message);
+ return newState;
+ } else {
+ // Loaded state is presumed to be compatible with the State type
+ // definition in the app. However, a non-serializable part has to be
+ // recreated.
+ state.nonSerializableState = createEmptyNonSerializableState();
+ }
+ return state;
+}
+
+interface MultiEngineState {
+ currentEngineId?: string;
+ engines: ObjectById<EngineConfig>;
+}
+
+function isMultiEngineState(
+ state: State | MultiEngineState,
+): state is MultiEngineState {
+ if ((state as MultiEngineState).engines !== undefined) {
+ return true;
+ }
+ return false;
+}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index bfb34ca..2fbc6df 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -31,7 +31,7 @@
SliceDetails,
ThreadDesc,
ThreadStateDetails,
- TraceTime,
+ TraceContext,
} from './globals';
import {findCurrentSelection} from './keyboard_event_handler';
@@ -96,8 +96,8 @@
globals.publishRedraw();
}
-export function publishTraceDetails(details: TraceTime): void {
- globals.traceTime = details;
+export function publishTraceContext(details: TraceContext): void {
+ globals.traceContext = details;
globals.publishRedraw();
}
@@ -208,3 +208,8 @@
globals.showPanningHint = true;
globals.publishRedraw();
}
+
+export function publishPermalinkHash(hash: string | undefined): void {
+ globals.permalinkHash = hash;
+ globals.publishRedraw();
+}
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index ad59ea8..49cc7b3 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -57,6 +57,7 @@
import {RecordingSectionAttrs} from './recording/recording_sections';
import {RecordingSettings} from './recording/recording_settings';
import {EtwSettings} from './recording/etw_settings';
+import {createPermalink} from './permalink';
export const PERSIST_CONFIG_FLAG = featureFlags.register({
id: 'persistConfigsUI',
@@ -176,9 +177,7 @@
'button.permalinkconfig',
{
onclick: () => {
- globals.dispatch(
- Actions.createPermalink({isRecordingConfig: true}),
- );
+ createPermalink({isRecordingConfig: true});
},
},
'Share recording settings',
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
index db87cfa..dc1d015 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/frontend/record_page_v2.ts
@@ -16,7 +16,6 @@
import {Attributes} from 'mithril';
import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
import {RecordingConfigUtils} from '../common/recordingV2/recording_config_utils';
import {
ChromeTargetInfo,
@@ -57,6 +56,7 @@
import {RecordingSettings} from './recording/recording_settings';
import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
import {showAddNewTargetModal} from './recording/reset_target_modal';
+import {createPermalink} from './permalink';
const START_RECORDING_MESSAGE = 'Start Recording';
@@ -186,9 +186,7 @@
'button.permalinkconfig',
{
onclick: () => {
- globals.dispatch(
- Actions.createPermalink({isRecordingConfig: true}),
- );
+ createPermalink({isRecordingConfig: true});
},
},
'Share recording settings',
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index f643001..cd7e94d 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -18,7 +18,7 @@
HighPrecisionTime,
HighPrecisionTimeSpan,
} from '../common/high_precision_time';
-import {getContainingTrackId} from '../common/state';
+import {getContainingGroupKey} from '../common/state';
import {globals} from './globals';
@@ -127,12 +127,12 @@
}
let trackGroup = null;
- const trackGroupId = getContainingTrackId(globals.state, trackKeyString);
- if (trackGroupId) {
- trackGroup = document.querySelector('#track_' + trackGroupId);
+ const groupKey = getContainingGroupKey(globals.state, trackKeyString);
+ if (groupKey) {
+ trackGroup = document.querySelector('#track_' + groupKey);
}
- if (!trackGroupId || !trackGroup) {
+ if (!groupKey || !trackGroup) {
console.error(`Can't scroll, track (${trackKeyString}) not found.`);
return;
}
@@ -142,7 +142,7 @@
if (openGroup) {
// After the track exists in the dom, it will be scrolled to.
globals.scrollToTrackKey = trackKey;
- globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+ globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
return;
} else {
trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 5b21ded..7bf0f0e 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -74,8 +74,6 @@
}
private async dropTrackTable(): Promise<void> {
- if (this.engine.isAlive) {
- await this.engine.query(`drop table if exists ${this.sqlTableName}`);
- }
+ await this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
}
}
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index c292fe6..3eebb83 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -17,7 +17,7 @@
CustomSqlDetailsPanelConfig,
CustomSqlTableDefConfig,
CustomSqlTableSliceTrack,
-} from '../core_plugins/custom_sql_table_slices';
+} from './tracks/custom_sql_table_slice_track';
import {NamedSliceTrackTypes} from './named_slice_track';
import {ARG_PREFIX, SliceColumns, SqlDataSource} from './debug_tracks';
import {uuidv4Sql} from '../base/uuid';
@@ -108,8 +108,6 @@
}
private async destroyTrackTable() {
- if (this.engine.isAlive) {
- await this.engine.query(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
- }
+ await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
}
}
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index fac6b8a..f028b15 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -15,7 +15,6 @@
import m from 'mithril';
import {Time, time} from '../base/time';
-import {runQuery} from '../common/queries';
import {raf} from '../core/raf_scheduler';
import {Anchor} from '../widgets/anchor';
import {Button} from '../widgets/button';
@@ -322,14 +321,13 @@
label: 'Critical path lite',
intent: Intent.Primary,
onclick: () =>
- runQuery(
- `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
- this.engine,
- ).then(() =>
- addDebugSliceTrack(
- this.engine,
- {
- sqlSource: `
+ this.engine
+ .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
+ .then(() =>
+ addDebugSliceTrack(
+ this.engine,
+ {
+ sqlSource: `
SELECT
cr.id,
cr.utid,
@@ -347,26 +345,27 @@
JOIN thread USING(utid)
JOIN process USING(upid)
`,
- columns: sliceLiteColumnNames,
- },
- `${this.state?.thread?.name}`,
- sliceLiteColumns,
- sliceLiteColumnNames,
+ columns: sliceLiteColumnNames,
+ },
+ `${this.state?.thread?.name}`,
+ sliceLiteColumns,
+ sliceLiteColumnNames,
+ ),
),
- ),
}),
m(Button, {
label: 'Critical path',
intent: Intent.Primary,
onclick: () =>
- runQuery(
- `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
- this.engine,
- ).then(() =>
- addDebugSliceTrack(
- this.engine,
- {
- sqlSource: `
+ this.engine
+ .query(
+ `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+ )
+ .then(() =>
+ addDebugSliceTrack(
+ this.engine,
+ {
+ sqlSource: `
SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
FROM
_thread_executing_span_critical_path_stack(
@@ -375,13 +374,13 @@
trace_bounds.end_ts - trace_bounds.start_ts) cr,
trace_bounds WHERE name IS NOT NULL
`,
- columns: sliceColumnNames,
- },
- `${this.state?.thread?.name}`,
- sliceColumns,
- sliceColumnNames,
+ columns: sliceColumnNames,
+ },
+ `${this.state?.thread?.name}`,
+ sliceColumns,
+ sliceColumnNames,
+ ),
),
- ),
}),
];
}
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 90f3e4c..632052d 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -31,9 +31,6 @@
export class TickmarkPanel implements Panel {
readonly kind = 'panel';
readonly selectable = false;
- readonly trackKey = undefined;
-
- constructor(readonly key: string) {}
render(): m.Children {
return m('.tickbar');
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index af1f3df..16c22ed 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -32,9 +32,6 @@
export class TimeAxisPanel implements Panel {
readonly kind = 'panel';
readonly selectable = false;
- readonly trackKey = undefined;
-
- constructor(readonly key: string) {}
render(): m.Children {
return m('.time-axis-panel');
@@ -57,16 +54,16 @@
break;
case TimestampFormat.UTC:
const offsetDate = Time.toDate(
- globals.traceTime.utcOffset,
- globals.traceTime.realtimeOffset,
+ globals.traceContext.utcOffset,
+ globals.traceContext.realtimeOffset,
);
const dateStr = toISODateOnly(offsetDate);
ctx.fillText(`UTC ${dateStr}`, 6, 10);
break;
case TimestampFormat.TraceTz:
const offsetTzDate = Time.toDate(
- globals.traceTime.traceTzOffset,
- globals.traceTime.realtimeOffset,
+ globals.traceContext.traceTzOffset,
+ globals.traceContext.realtimeOffset,
);
const dateTzStr = toISODateOnly(offsetTzDate);
ctx.fillText(dateTzStr, 6, 10);
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index d77b095..66e401c 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -140,9 +140,6 @@
export class TimeSelectionPanel implements Panel {
readonly kind = 'panel';
readonly selectable = false;
- readonly trackKey = undefined;
-
- constructor(readonly key: string) {}
render(): m.Children {
return m('.time-selection-panel');
diff --git a/ui/src/frontend/trace_attrs.ts b/ui/src/frontend/trace_attrs.ts
index 61f991e..ff80c85 100644
--- a/ui/src/frontend/trace_attrs.ts
+++ b/ui/src/frontend/trace_attrs.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
import {TraceArrayBufferSource} from '../common/state';
+import {createPermalink} from './permalink';
import {showModal} from '../widgets/modal';
import {onClickCopy} from './clipboard';
@@ -74,7 +74,7 @@
);
if (result) {
globals.logging.logEvent('Trace Actions', 'Create permalink');
- globals.dispatch(Actions.createPermalink({isRecordingConfig: false}));
+ createPermalink();
}
}
diff --git a/ui/src/frontend/trace_info_page.ts b/ui/src/frontend/trace_info_page.ts
index c8d7547..87f270a 100644
--- a/ui/src/frontend/trace_info_page.ts
+++ b/ui/src/frontend/trace_info_page.ts
@@ -14,12 +14,51 @@
import m from 'mithril';
-import {QueryResponse, runQuery} from '../common/queries';
import {raf} from '../core/raf_scheduler';
import {Engine} from '../trace_processor/engine';
import {globals} from './globals';
import {createPage} from './pages';
+import {QueryResult, UNKNOWN} from '../trace_processor/query_result';
+
+function getEngine(name: string): Engine | undefined {
+ const currentEngine = globals.getCurrentEngine();
+ if (currentEngine === undefined) return undefined;
+ const engineId = currentEngine.id;
+ return globals.engines.get(engineId)?.getProxy(name);
+}
+
+/**
+ * Extracts and copies fields from a source object based on the keys present in
+ * a spec object, effectively creating a new object that includes only the
+ * fields that are present in the spec object.
+ *
+ * @template S - A type representing the spec object, a subset of T.
+ * @template T - A type representing the source object, a superset of S.
+ *
+ * @param {T} source - The source object containing the full set of properties.
+ * @param {S} spec - The specification object whose keys determine which fields
+ * should be extracted from the source object.
+ *
+ * @returns {S} A new object containing only the fields from the source object
+ * that are also present in the specification object.
+ *
+ * @example
+ * const fullObject = { foo: 123, bar: '123', baz: true };
+ * const spec = { foo: 0, bar: '' };
+ * const result = pickFields(fullObject, spec);
+ * console.log(result); // Output: { foo: 123, bar: '123' }
+ */
+function pickFields<S extends Record<string, unknown>, T extends S>(
+ source: T,
+ spec: S,
+): S {
+ const result: Record<string, unknown> = {};
+ for (const key of Object.keys(spec)) {
+ result[key] = source[key];
+ }
+ return result as S;
+}
interface StatsSectionAttrs {
title: string;
@@ -29,58 +68,71 @@
queryId: string;
}
-function getEngine(name: string): Engine | undefined {
- const currentEngine = globals.getCurrentEngine();
- if (currentEngine === undefined) return undefined;
- const engineId = currentEngine.id;
- return globals.engines.get(engineId)?.getProxy(name);
-}
+const statsSpec = {
+ name: UNKNOWN,
+ value: UNKNOWN,
+ description: UNKNOWN,
+ idx: UNKNOWN,
+ severity: UNKNOWN,
+ source: UNKNOWN,
+};
+
+type StatsSectionRow = typeof statsSpec;
// Generic class that generate a <section> + <table> from the stats table.
// The caller defines the query constraint, title and styling.
// Used for errors, data losses and debugging sections.
class StatsSection implements m.ClassComponent<StatsSectionAttrs> {
- private queryResponse?: QueryResponse;
+ private data?: StatsSectionRow[];
constructor({attrs}: m.CVnode<StatsSectionAttrs>) {
const engine = getEngine('StatsSection');
if (engine === undefined) {
return;
}
- const query = `select name, value, cast(ifnull(idx, '') as text) as idx,
- description, severity, source from stats
- where ${attrs.sqlConstraints || '1=1'}
- order by name, idx`;
- runQuery(query, engine).then((resp: QueryResponse) => {
- this.queryResponse = resp;
+ const query = `
+ select
+ name,
+ value,
+ cast(ifnull(idx, '') as text) as idx,
+ description,
+ severity,
+ source from stats
+ where ${attrs.sqlConstraints || '1=1'}
+ order by name, idx
+ `;
+
+ engine.query(query).then((resp) => {
+ const data: StatsSectionRow[] = [];
+ const it = resp.iter(statsSpec);
+ for (; it.valid(); it.next()) {
+ data.push(pickFields(it, statsSpec));
+ }
+ this.data = data;
+
raf.scheduleFullRedraw();
});
}
view({attrs}: m.CVnode<StatsSectionAttrs>) {
- const resp = this.queryResponse;
- if (resp === undefined || resp.totalRowCount === 0) {
+ const data = this.data;
+ if (data === undefined || data.length === 0) {
return m('');
}
- if (resp.error) throw new Error(resp.error);
- const tableRows = [];
- for (const row of resp.rows) {
+ const tableRows = data.map((row) => {
const help = [];
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (row.description) {
+ if (Boolean(row.description)) {
help.push(m('i.material-icons.contextual-help', 'help_outline'));
}
const idx = row.idx !== '' ? `[${row.idx}]` : '';
- tableRows.push(
- m(
- 'tr',
- m('td.name', {title: row.description}, `${row.name}${idx}`, help),
- m('td', `${row.value}`),
- m('td', `${row.severity} (${row.source})`),
- ),
+ return m(
+ 'tr',
+ m('td.name', {title: row.description}, `${row.name}${idx}`, help),
+ m('td', `${row.value}`),
+ m('td', `${row.severity} (${row.source})`),
);
- }
+ });
return m(
`section${attrs.cssClass}`,
@@ -107,49 +159,63 @@
}
}
+const traceMetadataRowSpec = {name: UNKNOWN, value: UNKNOWN};
+
+type TraceMetadataRow = typeof traceMetadataRowSpec;
+
class TraceMetadata implements m.ClassComponent {
- private queryResponse?: QueryResponse;
+ private data?: TraceMetadataRow[];
constructor() {
const engine = getEngine('StatsSection');
if (engine === undefined) {
return;
}
- const query = `with
- metadata_with_priorities as (select
- name, ifnull(str_value, cast(int_value as text)) as value,
- name in (
- "trace_size_bytes",
- "cr-os-arch",
- "cr-os-name",
- "cr-os-version",
- "cr-physical-memory",
- "cr-product-version",
- "cr-hardware-class"
- ) as priority
- from metadata
- )
- select name, value
- from metadata_with_priorities
- order by priority desc, name`;
- runQuery(query, engine).then((resp: QueryResponse) => {
- this.queryResponse = resp;
+ const query = `
+ with metadata_with_priorities as (
+ select
+ name,
+ ifnull(str_value, cast(int_value as text)) as value,
+ name in (
+ "trace_size_bytes",
+ "cr-os-arch",
+ "cr-os-name",
+ "cr-os-version",
+ "cr-physical-memory",
+ "cr-product-version",
+ "cr-hardware-class"
+ ) as priority
+ from metadata
+ )
+ select
+ name,
+ value
+ from metadata_with_priorities
+ order by
+ priority desc,
+ name
+ `;
+
+ engine.query(query).then((resp: QueryResult) => {
+ const tableRows: TraceMetadataRow[] = [];
+ const it = resp.iter(traceMetadataRowSpec);
+ for (; it.valid(); it.next()) {
+ tableRows.push(pickFields(it, traceMetadataRowSpec));
+ }
+ this.data = tableRows;
raf.scheduleFullRedraw();
});
}
view() {
- const resp = this.queryResponse;
- if (resp === undefined || resp.totalRowCount === 0) {
+ const data = this.data;
+ if (data === undefined || data.length === 0) {
return m('');
}
- const tableRows = [];
- for (const row of resp.rows) {
- tableRows.push(
- m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`)),
- );
- }
+ const tableRows = data.map((row) => {
+ return m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`));
+ });
return m(
'section',
@@ -163,40 +229,68 @@
}
}
+const androidGameInterventionRowSpec = {
+ package_name: UNKNOWN,
+ uid: UNKNOWN,
+ current_mode: UNKNOWN,
+ standard_mode_supported: UNKNOWN,
+ standard_mode_downscale: UNKNOWN,
+ standard_mode_use_angle: UNKNOWN,
+ standard_mode_fps: UNKNOWN,
+ perf_mode_supported: UNKNOWN,
+ perf_mode_downscale: UNKNOWN,
+ perf_mode_use_angle: UNKNOWN,
+ perf_mode_fps: UNKNOWN,
+ battery_mode_supported: UNKNOWN,
+ battery_mode_downscale: UNKNOWN,
+ battery_mode_use_angle: UNKNOWN,
+ battery_mode_fps: UNKNOWN,
+};
+
+type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
+
class AndroidGameInterventionList implements m.ClassComponent {
- private queryResponse?: QueryResponse;
+ private data?: AndroidGameInterventionRow[];
constructor() {
const engine = getEngine('StatsSection');
if (engine === undefined) {
return;
}
- const query = `select
- package_name,
- uid,
- current_mode,
- standard_mode_supported,
- standard_mode_downscale,
- standard_mode_use_angle,
- standard_mode_fps,
- perf_mode_supported,
- perf_mode_downscale,
- perf_mode_use_angle,
- perf_mode_fps,
- battery_mode_supported,
- battery_mode_downscale,
- battery_mode_use_angle,
- battery_mode_fps
- from android_game_intervention_list`;
- runQuery(query, engine).then((resp: QueryResponse) => {
- this.queryResponse = resp;
+ const query = `
+ select
+ package_name,
+ uid,
+ current_mode,
+ standard_mode_supported,
+ standard_mode_downscale,
+ standard_mode_use_angle,
+ standard_mode_fps,
+ perf_mode_supported,
+ perf_mode_downscale,
+ perf_mode_use_angle,
+ perf_mode_fps,
+ battery_mode_supported,
+ battery_mode_downscale,
+ battery_mode_use_angle,
+ battery_mode_fps
+ from android_game_intervention_list
+ `;
+
+ engine.query(query).then((resp) => {
+ const data: AndroidGameInterventionRow[] = [];
+ const it = resp.iter(androidGameInterventionRowSpec);
+ for (; it.valid(); it.next()) {
+ data.push(pickFields(it, androidGameInterventionRowSpec));
+ }
+ this.data = data;
raf.scheduleFullRedraw();
});
}
view() {
- const resp = this.queryResponse;
- if (resp === undefined || resp.totalRowCount === 0) {
+ const data = this.data;
+ if (data === undefined || data.length === 0) {
return m('');
}
@@ -204,7 +298,8 @@
let standardInterventions = '';
let perfInterventions = '';
let batteryInterventions = '';
- for (const row of resp.rows) {
+
+ for (const row of data) {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (row.standard_mode_supported) {
standardInterventions = `angle=${row.standard_mode_use_angle},downscale=${row.standard_mode_downscale},fps=${row.standard_mode_fps}`;
@@ -268,46 +363,68 @@
}
}
-class PackageList implements m.ClassComponent {
- private queryResponse?: QueryResponse;
+const packageDataSpec = {
+ packageName: UNKNOWN,
+ versionCode: UNKNOWN,
+ debuggable: UNKNOWN,
+ profileableFromShell: UNKNOWN,
+};
+
+type PackageData = typeof packageDataSpec;
+
+class PackageListSection implements m.ClassComponent {
+ private packageList?: PackageData[];
constructor() {
const engine = getEngine('StatsSection');
if (engine === undefined) {
return;
}
- const query = `select package_name, version_code, debuggable,
- profileable_from_shell from package_list`;
- runQuery(query, engine).then((resp: QueryResponse) => {
- this.queryResponse = resp;
- raf.scheduleFullRedraw();
- });
+ this.loadData(engine);
+ }
+
+ private async loadData(engine: Engine): Promise<void> {
+ const query = `
+ select
+ package_name as packageName,
+ version_code as versionCode,
+ debuggable,
+ profileable_from_shell as profileableFromShell
+ from package_list
+ `;
+
+ const packageList: PackageData[] = [];
+ const result = await engine.query(query);
+ const it = result.iter(packageDataSpec);
+ for (; it.valid(); it.next()) {
+ packageList.push(pickFields(it, packageDataSpec));
+ }
+
+ this.packageList = packageList;
+ raf.scheduleFullRedraw();
}
view() {
- const resp = this.queryResponse;
- if (resp === undefined || resp.totalRowCount === 0) {
- return m('');
+ const packageList = this.packageList;
+ if (packageList === undefined || packageList.length === 0) {
+ return undefined;
}
- const tableRows = [];
- for (const row of resp.rows) {
- tableRows.push(
+ const tableRows = packageList.map((it) => {
+ return m(
+ 'tr',
+ m('td.name', `${it.packageName}`),
+ m('td', `${it.versionCode}`),
+ /* eslint-disable @typescript-eslint/strict-boolean-expressions */
m(
- 'tr',
- m('td.name', `${row.package_name}`),
- m('td', `${row.version_code}`),
- /* eslint-disable @typescript-eslint/strict-boolean-expressions */
- m(
- 'td',
- `${row.debuggable ? 'debuggable' : ''} ${
- row.profileable_from_shell ? 'profileable' : ''
- }`,
- ),
- /* eslint-enable */
+ 'td',
+ `${it.debuggable ? 'debuggable' : ''} ${
+ it.profileableFromShell ? 'profileable' : ''
+ }`,
),
+ /* eslint-enable */
);
- }
+ });
return m(
'section',
@@ -348,7 +465,7 @@
sqlConstraints: `severity = 'data_loss' and value > 0`,
}),
m(TraceMetadata),
- m(PackageList),
+ m(PackageListSection),
m(AndroidGameInterventionList),
m(StatsSection, {
queryId: 'info_all',
diff --git a/ui/src/frontend/trace_url_handler.ts b/ui/src/frontend/trace_url_handler.ts
index a50bed7..0e60724 100644
--- a/ui/src/frontend/trace_url_handler.ts
+++ b/ui/src/frontend/trace_url_handler.ts
@@ -18,6 +18,7 @@
import {tryGetTrace} from '../common/cache_manager';
import {showModal} from '../widgets/modal';
+import {loadPermalink} from './permalink';
import {loadAndroidBugToolInfo} from './android_bug_tool';
import {globals} from './globals';
import {Route, Router} from './router';
@@ -34,7 +35,7 @@
export function maybeOpenTraceFromRoute(route: Route) {
if (route.args.s) {
// /?s=xxxx for permalinks.
- globals.dispatch(Actions.loadPermalink({hash: route.args.s}));
+ loadPermalink(route.args.s);
return;
}
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index ec9a43a..10ca865 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -16,7 +16,7 @@
import {Icons} from '../base/semantic_icons';
import {Actions} from '../common/actions';
-import {getContainingTrackId, getLegacySelection} from '../common/state';
+import {getContainingGroupKey, getLegacySelection} from '../common/state';
import {TrackCacheEntry} from '../common/track_cache';
import {TrackTags} from '../public';
@@ -38,8 +38,7 @@
import {Button} from '../widgets/button';
interface Attrs {
- trackGroupId: string;
- key: string;
+ groupKey: string;
title: string;
collapsed: boolean;
trackFSM?: TrackCacheEntry;
@@ -50,16 +49,14 @@
export class TrackGroupPanel implements Panel {
readonly kind = 'panel';
readonly selectable = true;
- readonly key: string;
- readonly trackGroupId: string;
+ readonly groupKey: string;
constructor(private attrs: Attrs) {
- this.trackGroupId = attrs.trackGroupId;
- this.key = attrs.key;
+ this.groupKey = attrs.groupKey;
}
render(): m.Children {
- const {trackGroupId, title, labels, tags, collapsed, trackFSM} = this.attrs;
+ const {groupKey, title, labels, tags, collapsed, trackFSM} = this.attrs;
let name = title;
if (name[0] === '/') {
@@ -72,25 +69,25 @@
const searchIndex = globals.state.searchIndex;
if (searchIndex !== -1) {
const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
- const parentTrackId = getContainingTrackId(globals.state, trackKey);
- if (parentTrackId === trackGroupId) {
+ const containingGroupKey = getContainingGroupKey(globals.state, trackKey);
+ if (containingGroupKey === groupKey) {
highlightClass = 'flash';
}
}
const selection = getLegacySelection(globals.state);
- const trackGroup = globals.state.trackGroups[trackGroupId];
+ const trackGroup = globals.state.trackGroups[groupKey];
let checkBox = Icons.BlankCheckbox;
if (selection !== null && selection.kind === 'AREA') {
const selectedArea = globals.state.areas[selection.areaId];
if (
- selectedArea.tracks.includes(trackGroupId) &&
+ selectedArea.tracks.includes(groupKey) &&
trackGroup.tracks.every((id) => selectedArea.tracks.includes(id))
) {
checkBox = Icons.Checkbox;
} else if (
- selectedArea.tracks.includes(trackGroupId) ||
+ selectedArea.tracks.includes(groupKey) ||
trackGroup.tracks.some((id) => selectedArea.tracks.includes(id))
) {
checkBox = Icons.IndeterminateCheckbox;
@@ -107,7 +104,7 @@
return m(
`.track-group-panel[collapsed=${collapsed}]`,
{
- id: 'track_' + trackGroupId,
+ id: 'track_' + groupKey,
oncreate: () => this.onupdate(),
onupdate: () => this.onupdate(),
},
@@ -118,7 +115,7 @@
if (e.defaultPrevented) return;
globals.dispatch(
Actions.toggleTrackGroupCollapsed({
- trackGroupId,
+ groupKey,
}),
),
e.stopPropagation();
@@ -143,7 +140,7 @@
onclick: (e: MouseEvent) => {
globals.dispatch(
Actions.toggleTrackSelection({
- id: trackGroupId,
+ key: groupKey,
isTrackGroup: true,
}),
);
@@ -180,7 +177,7 @@
if (!selection || selection.kind !== 'AREA') return;
const selectedArea = globals.state.areas[selection.areaId];
const selectedAreaDuration = selectedArea.end - selectedArea.start;
- if (selectedArea.tracks.includes(this.trackGroupId)) {
+ if (selectedArea.tracks.includes(this.groupKey)) {
ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
ctx.fillRect(
visibleTimeScale.timeToPx(selectedArea.start) + TRACK_SHELL_WIDTH,
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 806b49c..e0aca05 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -199,7 +199,7 @@
onclick: (e: MouseEvent) => {
globals.dispatch(
Actions.toggleTrackSelection({
- id: attrs.trackKey,
+ key: attrs.trackKey,
isTrackGroup: false,
}),
);
@@ -423,10 +423,6 @@
constructor(private readonly attrs: TrackPanelAttrs) {}
- get key(): string {
- return this.attrs.trackKey;
- }
-
get trackKey(): string {
return this.attrs.trackKey;
}
diff --git a/ui/src/core_plugins/custom_sql_table_slices/index.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
similarity index 82%
rename from ui/src/core_plugins/custom_sql_table_slices/index.ts
rename to ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index 72f2075..cd31610 100644
--- a/ui/src/core_plugins/custom_sql_table_slices/index.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -18,15 +18,11 @@
import {Actions} from '../../common/actions';
import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
import {LegacySelection} from '../../common/state';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTabConfigBase} from '../../frontend/generic_slice_details_tab';
-import {globals} from '../../frontend/globals';
-import {
- NamedSliceTrack,
- NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginDescriptor} from '../../public';
+import {OnSliceClickArgs} from '../base_slice_track';
+import {GenericSliceDetailsTabConfigBase} from '../generic_slice_details_tab';
+import {globals} from '../globals';
+import {NamedSliceTrack, NamedSliceTrackTypes} from '../named_slice_track';
+import {NewTrackArgs} from '../track';
export interface CustomSqlImportConfig {
modules: string[];
@@ -94,10 +90,8 @@
});
await this.engine.query(sql);
return DisposableCallback.from(() => {
- if (this.engine.isAlive) {
- this.engine.query(`DROP VIEW ${this.tableName}`);
- config.dispose?.dispose();
- }
+ this.engine.tryQuery(`DROP VIEW ${this.tableName}`);
+ config.dispose?.dispose();
});
}
@@ -139,10 +133,3 @@
}
}
}
-
-class CustomSqlTrackPlugin implements Plugin {}
-
-export const plugin: PluginDescriptor = {
- pluginId: 'perfetto.CustomSqlTrack',
- plugin: CustomSqlTrackPlugin,
-};
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index b67f2e5..429c391 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -82,11 +82,11 @@
// Used to prevent global deselection if a pan/drag select occurred.
private keepCurrentSelection = false;
- private overviewTimelinePanel = new OverviewTimelinePanel('overview');
- private timeAxisPanel = new TimeAxisPanel('timeaxis');
- private timeSelectionPanel = new TimeSelectionPanel('timeselection');
- private notesPanel = new NotesPanel('notes');
- private tickmarkPanel = new TickmarkPanel('searchTickmarks');
+ private overviewTimelinePanel = new OverviewTimelinePanel();
+ private timeAxisPanel = new TimeAxisPanel();
+ private timeSelectionPanel = new TimeSelectionPanel();
+ private notesPanel = new NotesPanel();
+ private tickmarkPanel = new TickmarkPanel();
private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
@@ -128,7 +128,7 @@
currentY: number,
editing: boolean,
) => {
- const traceTime = globals.traceTime;
+ const traceTime = globals.traceContext;
const {visibleTimeScale} = timeline;
this.keepCurrentSelection = true;
if (editing) {
@@ -231,8 +231,7 @@
if (key) {
const trackBundle = this.resolveTrack(key);
headerPanel = new TrackGroupPanel({
- trackGroupId: group.id,
- key: `trackgroup-${group.id}`,
+ groupKey: group.key,
trackFSM: trackBundle.trackFSM,
labels: trackBundle.labels,
tags: trackBundle.tags,
@@ -241,8 +240,7 @@
});
} else {
headerPanel = new TrackGroupPanel({
- trackGroupId: group.id,
- key: `trackgroup-${group.id}`,
+ groupKey: group.key,
collapsed: group.collapsed,
title: group.name,
});
@@ -268,7 +266,6 @@
collapsed: group.collapsed,
childPanels: childTracks,
header: headerPanel,
- trackGroupId: group.id,
});
}
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index aaef484..b4f20ca 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {runQuery} from '../../common/queries';
import {addDebugSliceTrack} from '../../public';
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
@@ -170,7 +169,7 @@
id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
name: 'Add track: Android jank CUJs',
callback: () => {
- runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() => {
+ ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => {
addDebugSliceTrack(
ctx.engine,
{
@@ -189,9 +188,9 @@
id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
name: 'Run query: Android jank CUJs',
callback: () => {
- runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
- ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'),
- );
+ ctx.engine
+ .query(JANK_CUJ_QUERY_PRECONDITIONS)
+ .then(() => ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'));
},
});
@@ -223,7 +222,7 @@
id: 'dev.perfetto.AndroidCujs#PinBlockingCalls',
name: 'Add track: Android Blocking calls during CUJs',
callback: () => {
- runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
+ ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() =>
addDebugSliceTrack(
ctx.engine,
{
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 8f360b4..0666986 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -490,13 +490,25 @@
const HIGH_CPU = `
drop table if exists high_cpu;
create table high_cpu as
- with base as (
+ with cpu_cycles_args as (
select
- ts,
- EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.uid') as uid,
- EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.cluster') as cluster,
- sum(EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.time_millis')) as time_millis
- from track t join slice s on t.id = s.track_id
+ arg_set_id,
+ min(iif(key = 'cpu_cycles_per_uid_cluster.uid', int_value, null)) as uid,
+ min(iif(key = 'cpu_cycles_per_uid_cluster.cluster', int_value, null)) as cluster,
+ min(iif(key = 'cpu_cycles_per_uid_cluster.time_millis', int_value, null)) as time_millis
+ from args
+ where key in (
+ 'cpu_cycles_per_uid_cluster.uid',
+ 'cpu_cycles_per_uid_cluster.cluster',
+ 'cpu_cycles_per_uid_cluster.time_millis'
+ )
+ group by 1
+ ),
+ base as (
+ select ts, uid, cluster, sum(time_millis) as time_millis
+ from track t
+ join slice s on t.id = s.track_id
+ join cpu_cycles_args using (arg_set_id)
where t.name = 'Statsd Atoms'
and s.name = 'cpu_cycles_per_uid_cluster'
group by 1, 2, 3
@@ -521,8 +533,7 @@
with_ratio as (
select
ts,
- 100.0 * cpu_dur / dur as value,
- dur,
+ iif(dur is null, 0, 100.0 * cpu_dur / dur) as value,
case cluster when 0 then 'little' when 1 then 'mid' when 2 then 'big' else 'cl-' || cluster end as cluster,
case
when uid = 0 then 'AID_ROOT'
@@ -533,17 +544,9 @@
else pl.package_name
end as pkg
from with_windows left join app_package_list pl using(uid)
- where cpu_dur is not null
- ),
- with_zeros as (
- select ts, value, cluster, pkg
- from with_ratio
- union all
- select ts + dur as ts, 0 as value, cluster, pkg
- from with_ratio
)
select ts, sum(value) as value, cluster, pkg
- from with_zeros
+ from with_ratio
group by 1, 3, 4`;
const WAKEUPS = `
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 2ec9223..b8feb57 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -14,7 +14,6 @@
import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
import {addDebugSliceTrack} from '../../public';
-import {runQuery} from '../../common/queries';
const PERF_TRACE_COUNTERS_PRECONDITION = `
SELECT
@@ -27,8 +26,8 @@
class AndroidPerfTraceCounters implements Plugin {
async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
- const resp = await runQuery(PERF_TRACE_COUNTERS_PRECONDITION, ctx.engine);
- if (resp.totalRowCount === 0) return;
+ const resp = await ctx.engine.query(PERF_TRACE_COUNTERS_PRECONDITION);
+ if (resp.numRows() === 0) return;
ctx.registerCommand({
id: 'dev.perfetto.AndroidPerfTraceCounters#ThreadRuntimeIPC',
name: 'Add a track to show a thread runtime ipc',
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index eca39b4..9f627b8 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -20,7 +20,7 @@
STR_NULL,
} from '../../public';
import {ASYNC_SLICE_TRACK_KIND} from '../../core_plugins/async_slices';
-import {AsyncSliceTrackV2} from '../../core_plugins/async_slices/async_slice_track_v2';
+import {AsyncSliceTrack} from '../../core_plugins/async_slices/async_slice_track';
// This plugin renders visualizations of runtime power state transitions for
// Linux kernel devices (devices managed by Linux drivers).
@@ -50,7 +50,7 @@
trackIds: [trackId],
kind: ASYNC_SLICE_TRACK_KIND,
trackFactory: ({trackKey}) => {
- return new AsyncSliceTrackV2(
+ return new AsyncSliceTrack(
{
engine: ctx.engine,
trackKey,
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 7fb2e44..9e540fd 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -22,6 +22,8 @@
import {PanelSize} from '../frontend/panel';
import {Engine} from '../trace_processor/engine';
import {UntypedEventSet} from '../core/event_set';
+import {TraceContext} from '../frontend/globals';
+import {PromptOption} from '../frontend/omnibox_manager';
export {Engine} from '../trace_processor/engine';
export {
@@ -34,6 +36,7 @@
} from '../trace_processor/query_result';
export {BottomTabToSCSAdapter} from './utils';
export {createStore, Migrate, Store} from '../base/store';
+export {PromptOption} from '../frontend/omnibox_manager';
// This is a temporary fix until this is available in the plugin API.
export {
@@ -433,10 +436,9 @@
// Create a store mounted over the top of this plugin's persistent state.
mountStore<T>(migrate: Migrate<T>): Store<T>;
- trace: {
- // A span representing the start and end time of the trace
- readonly span: Span<time, duration>;
- };
+ trace: TraceContext;
+
+ prompt(text: string, options?: PromptOption[]): Promise<string>;
}
export interface Plugin {
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 90901c5..d0f70af 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -14,7 +14,6 @@
import {defer, Deferred} from '../base/deferred';
import {assertExists, assertTrue} from '../base/logging';
-import {duration, Span, Time, time, TimeSpan} from '../base/time';
import {
ComputeMetricArgs,
ComputeMetricResult,
@@ -31,17 +30,14 @@
import {ProtoRingBuffer} from './proto_ring_buffer';
import {
createQueryResult,
- LONG,
- LONG_NULL,
- NUM,
QueryError,
QueryResult,
- STR,
WritableQueryResult,
} from './query_result';
import TPM = TraceProcessorRpc.TraceProcessorMethod;
import {Disposable} from '../base/disposable';
+import {Result} from '../base/utils';
export interface LoadingTracker {
beginLoading(): void;
@@ -67,16 +63,43 @@
}
export interface Engine {
- execute(sqlQuery: string, tag?: string): Promise<QueryResult> & QueryResult;
- query(sqlQuery: string, tag?: string): Promise<QueryResult>;
- getCpus(): Promise<number[]>;
- getNumberOfGpus(): Promise<number>;
- getTracingMetadataTimeBounds(): Promise<Span<time, duration>>;
+ /**
+ * Execute a query against the database, returning a promise that resolves
+ * when the query has completed but rejected when the query fails for whatever
+ * reason. On success, the promise will only resolve once all the resulting
+ * rows have been received.
+ *
+ * The promise will be rejected if the query fails.
+ *
+ * @param sql The query to execute.
+ * @param tag An optional tag used to trace the origin of the query.
+ */
+ query(sql: string, tag?: string): Promise<QueryResult>;
+
+ /**
+ * Execute a query against the database, returning a promise that resolves
+ * when the query has completed or failed. The promise will never get
+ * rejected, it will always successfully resolve. Use the returned wrapper
+ * object to determine whether the query completed successfully.
+ *
+ * The promise will only resolve once all the resulting rows have been
+ * received.
+ *
+ * @param sql The query to execute.
+ * @param tag An optional tag used to trace the origin of the query.
+ */
+ tryQuery(sql: string, tag?: string): Promise<Result<QueryResult, Error>>;
+
+ /**
+ * Execute one or more metric and get the result.
+ *
+ * @param metrics The metrics to run.
+ * @param format The format of the response.
+ */
computeMetric(
metrics: string[],
format: 'json' | 'prototext' | 'proto',
): Promise<string | Uint8Array>;
- readonly isAlive: boolean;
}
// Abstract interface of a trace proccessor.
@@ -92,8 +115,6 @@
// 2. Call onRpcResponseBytes() when response data is received.
export abstract class EngineBase implements Engine {
abstract readonly id: string;
- private _cpus?: number[];
- private _numGpus?: number;
private loadingTracker: LoadingTracker;
private txSeqId = 0;
private rxSeqId = 0;
@@ -106,7 +127,6 @@
private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>();
private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
private _isMetatracingEnabled = false;
- readonly isAlive = false;
constructor(tracker?: LoadingTracker) {
this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
@@ -360,7 +380,10 @@
//
// Optional |tag| (usually a component name) can be provided to allow
// attributing trace processor workload to different UI components.
- execute(sqlQuery: string, tag?: string): Promise<QueryResult> & QueryResult {
+ private streamingQuery(
+ sqlQuery: string,
+ tag?: string,
+ ): Promise<QueryResult> & QueryResult {
const rpc = TraceProcessorRpc.create();
rpc.request = TPM.TPM_QUERY_STREAMING;
rpc.queryArgs = new QueryArgs();
@@ -376,13 +399,13 @@
return result;
}
- // Wraps .execute(), captures errors and re-throws with current stack.
+ // Wraps .streamingQuery(), captures errors and re-throws with current stack.
//
- // Note: This function is less flexible that .execute() as it only returns a
+ // Note: This function is less flexible than .execute() as it only returns a
// promise which must be unwrapped before the QueryResult may be accessed.
async query(sqlQuery: string, tag?: string): Promise<QueryResult> {
try {
- return await this.execute(sqlQuery, tag);
+ return await this.streamingQuery(sqlQuery, tag);
} catch (e) {
// Replace the error's stack trace with the one from here
// Note: It seems only V8 can trace the stack up the promise chain, so its
@@ -394,6 +417,19 @@
}
}
+ async tryQuery(
+ sql: string,
+ tag?: string,
+ ): Promise<Result<QueryResult, Error>> {
+ try {
+ const result = await this.query(sql, tag);
+ return {success: true, result};
+ } catch (error: unknown) {
+ // We know we only throw Error type objects so we can type assert safely
+ return {success: false, error: error as Error};
+ }
+ }
+
isMetatracingEnabled(): boolean {
return this._isMetatracingEnabled;
}
@@ -438,79 +474,6 @@
this.rpcSendRequestBytes(buf);
}
- // TODO(hjd): When streaming must invalidate this somehow.
- async getCpus(): Promise<number[]> {
- if (!this._cpus) {
- const cpus = [];
- const queryRes = await this.query(
- 'select distinct(cpu) as cpu from sched order by cpu;',
- );
- for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
- cpus.push(it.cpu);
- }
- this._cpus = cpus;
- }
- return this._cpus;
- }
-
- async getNumberOfGpus(): Promise<number> {
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
- if (!this._numGpus) {
- const result = await this.query(`
- select count(distinct(gpu_id)) as gpuCount
- from gpu_counter_track
- where name = 'gpufreq';
- `);
- this._numGpus = result.firstRow({gpuCount: NUM}).gpuCount;
- }
- return this._numGpus;
- }
-
- // TODO: This should live in code that's more specific to chrome, instead of
- // in engine.
- async getNumberOfProcesses(): Promise<number> {
- const result = await this.query('select count(*) as cnt from process;');
- return result.firstRow({cnt: NUM}).cnt;
- }
-
- async getTraceTimeBounds(): Promise<Span<time, duration>> {
- const result = await this.query(
- `select start_ts as startTs, end_ts as endTs from trace_bounds`,
- );
- const bounds = result.firstRow({
- startTs: LONG,
- endTs: LONG,
- });
- return new TimeSpan(
- Time.fromRaw(bounds.startTs),
- Time.fromRaw(bounds.endTs),
- );
- }
-
- async getTracingMetadataTimeBounds(): Promise<Span<time, duration>> {
- const queryRes = await this.query(`select
- name,
- int_value as intValue
- from metadata
- where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
- or name = 'all_data_source_started_ns'`);
- let startBound = Time.MIN;
- let endBound = Time.MAX;
- const it = queryRes.iter({name: STR, intValue: LONG_NULL});
- for (; it.valid(); it.next()) {
- const columnName = it.name;
- const timestamp = it.intValue;
- if (timestamp === null) continue;
- if (columnName === 'tracing_disabled_ns') {
- endBound = Time.min(endBound, Time.fromRaw(timestamp));
- } else {
- startBound = Time.max(startBound, Time.fromRaw(timestamp));
- }
- }
-
- return new TimeSpan(startBound, endBound);
- }
-
getProxy(tag: string): EngineProxy {
return new EngineProxy(this, tag);
}
@@ -522,58 +485,42 @@
private tag: string;
private _isAlive: boolean;
- get isAlive(): boolean {
- return this._isAlive;
- }
-
constructor(engine: EngineBase, tag: string) {
this.engine = engine;
this.tag = tag;
this._isAlive = true;
}
- execute(query: string, tag?: string): Promise<QueryResult> & QueryResult {
- if (!this.isAlive) {
- throw new Error(`EngineProxy ${this.tag} was disposed.`);
- }
- return this.engine.execute(query, tag || this.tag);
- }
-
async query(query: string, tag?: string): Promise<QueryResult> {
- if (!this.isAlive) {
+ if (!this._isAlive) {
throw new Error(`EngineProxy ${this.tag} was disposed.`);
}
return await this.engine.query(query, tag);
}
+ async tryQuery(
+ query: string,
+ tag?: string,
+ ): Promise<Result<QueryResult, Error>> {
+ if (!this._isAlive) {
+ return {
+ success: false,
+ error: new Error(`EngineProxy ${this.tag} was disposed.`),
+ };
+ }
+ return await this.engine.tryQuery(query, tag);
+ }
+
async computeMetric(
metrics: string[],
format: 'json' | 'prototext' | 'proto',
): Promise<string | Uint8Array> {
- if (!this.isAlive) {
+ if (!this._isAlive) {
return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
}
return this.engine.computeMetric(metrics, format);
}
- async getCpus(): Promise<number[]> {
- if (!this.isAlive) {
- return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
- }
- return this.engine.getCpus();
- }
-
- async getNumberOfGpus(): Promise<number> {
- if (!this.isAlive) {
- return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
- }
- return this.engine.getNumberOfGpus();
- }
-
- async getTracingMetadataTimeBounds(): Promise<Span<time, bigint>> {
- return this.engine.getTracingMetadataTimeBounds();
- }
-
get engineId(): string {
return this.engine.id;
}
diff --git a/ui/src/trace_processor/query_result.ts b/ui/src/trace_processor/query_result.ts
index ac48079..a29f4dc 100644
--- a/ui/src/trace_processor/query_result.ts
+++ b/ui/src/trace_processor/query_result.ts
@@ -57,6 +57,7 @@
import {utf8Decode} from '../base/string_utils';
import {Duration, duration, Time, time} from '../base/time';
+export const UNKNOWN: ColumnType = null;
export const NUM = 0;
export const STR = 'str';
export const NUM_NULL: number | null = 1;
@@ -203,6 +204,8 @@
return 'LONG';
case LONG_NULL:
return 'LONG_NULL';
+ case UNKNOWN:
+ return 'UNKNOWN';
default:
return `INVALID(${t})`;
}
@@ -215,21 +218,25 @@
expected === NUM_NULL ||
expected === STR_NULL ||
expected === BLOB_NULL ||
- expected === LONG_NULL
+ expected === LONG_NULL ||
+ expected === UNKNOWN
);
case CellType.CELL_VARINT:
return (
expected === NUM ||
expected === NUM_NULL ||
expected === LONG ||
- expected === LONG_NULL
+ expected === LONG_NULL ||
+ expected === UNKNOWN
);
case CellType.CELL_FLOAT64:
- return expected === NUM || expected === NUM_NULL;
+ return expected === NUM || expected === NUM_NULL || expected === UNKNOWN;
case CellType.CELL_STRING:
- return expected === STR || expected === STR_NULL;
+ return expected === STR || expected === STR_NULL || expected === UNKNOWN;
case CellType.CELL_BLOB:
- return expected === BLOB || expected === BLOB_NULL;
+ return (
+ expected === BLOB || expected === BLOB_NULL || expected === UNKNOWN
+ );
default:
throw new Error(`Unknown CellType ${actual}`);
}
@@ -285,11 +292,10 @@
// If true all rows have been fetched. Calling iter() will iterate through the
// last row. If false, iter() will return an iterator which might iterate
// through some rows (or none) but will surely not reach the end.
-
isComplete(): boolean;
// Returns a promise that is resolved only when all rows (i.e. all batches)
- // have been fetched. The promise return value is always the object iself.
+ // have been fetched. The promise return value is always the object itself.
waitAllRows(): Promise<QueryResult>;
// Returns a promise that is resolved when either:
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 39be606..89185c3 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -75,31 +75,32 @@
if (this.engine === undefined) {
return '';
}
- const result = this.engine.execute(uri);
try {
- await result.waitAllRows();
+ const result = await this.engine.query(uri);
+ const columns = result.columns();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const rows: any[] = [];
+ for (const it = result.iter({}); it.valid(); it.next()) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const row: any = {};
+ for (const name of columns) {
+ let value = it.get(name);
+ if (typeof value === 'bigint') {
+ value = Number(value);
+ }
+ row[name] = value;
+ }
+ rows.push(row);
+ }
+ return JSON.stringify(rows);
} catch (e) {
if (e instanceof QueryError) {
- console.error(result.error());
+ console.error(e);
return '';
+ } else {
+ throw e;
}
}
- const columns = result.columns();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const rows: any[] = [];
- for (const it = result.iter({}); it.valid(); it.next()) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const row: any = {};
- for (const name of columns) {
- let value = it.get(name);
- if (typeof value === 'bigint') {
- value = Number(value);
- }
- row[name] = value;
- }
- rows.push(row);
- }
- return JSON.stringify(rows);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any