Merge "ui: Hide slice implementation details from subclasses"
diff --git a/CHANGELOG b/CHANGELOG
index dabf6bf..36136a7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,16 @@
Unreleased:
Tracing service and probes:
+ *
+ Trace Processor:
+ *
+ UI:
+ *
+ SDK:
+ *
+
+
+v22.0 - 2021-12-03:
+ Tracing service and probes:
* Added Android SDK version to the SystemInfo trace packet.
* Changed compiler flags. Assume recent x64 CPUs (-msse4.2 -mavx -mpopcnt).
This behavior affects only standalone builds and can be changed by setting
@@ -7,13 +18,30 @@
* The java heap profiler now rescans all the processes every time for
continous_dump_config. The scan_pids_only_on_start can be used to restore
the old behavior.
+ * Added support for building on ARM Macs.
+ * Added support for the rss_stat_throttled event on Android via
+ FtraceConfig.throttle_rss_stat. In newer Android kernels the base rss_stat
+ event is now unthrottled. rss_stat used to be throttled by a downstream
+ kernel change, unnecessary after https://lkml.org/lkml/2021/10/25/1411 .
+ atrace.rc configures throttling from userspace at boot.
+ * Fixed a bug that caused IPCs to stall traced and hit the watchdog if in
+ the middle of a suspend/resume. Switched from SND_TIMEO to poll(POLLOUT).
+ * Added "linux.sysfs_power" data source to poll /sys/class/power_supply/
+ and report periodically battery charge and drain rate if supported.
Trace Processor:
+ * Speeded up proto trace ingestion by 2x (~20 MB/s -> ~40 MB/s).
* Changed LIKE comparisions to be case-senstive. This may break existing
queries but was a necessary from a performance perspective.
+ Going forward, GLOB must be used, instead of LIKE, for checked in metrics
+ to avoid unnecessary slowdowns.
* Changed compiler flags, assume recent x64 CPUs (see above).
* Changed how displayTimeUnit is handled in JSON traces to match catapult.
+ * Added websocket endpoint to RPC interface to reduce query latency.
+ * Added support for hot-reloading metrics (see //docs/analysis/metrics.md).
UI:
- *
+ * Added ability to save/restore record config. Remember last used config.
+ * Fixed bug causing the recording page to hold onto the USB interface making
+ adb unusable after a recording session.
SDK:
* Added UpdateDataSource() descriptor IPC endpoint to allow updates of the
data source capabilities (e.g., category list for track event).
diff --git a/docs/data-sources/java-heap-profiler.md b/docs/data-sources/java-heap-profiler.md
index 1187b7a..f67cf03 100644
--- a/docs/data-sources/java-heap-profiler.md
+++ b/docs/data-sources/java-heap-profiler.md
@@ -74,7 +74,10 @@
```sql
select name, cumulative_size
- from experimental_flamegraph(56785646801, 1, 'graph')
+ from experimental_flamegraph
+ where ts = 56785646801
+ and upid = 1
+ and profile_type = 'graph'
order by 2 desc;
```
diff --git a/docs/data-sources/native-heap-profiler.md b/docs/data-sources/native-heap-profiler.md
index 47c3122..56ef95c 100644
--- a/docs/data-sources/native-heap-profiler.md
+++ b/docs/data-sources/native-heap-profiler.md
@@ -566,9 +566,12 @@
```sql
select name, map_name, cumulative_size
- from experimental_flamegraph(8300973884377,1,'native')
+ from experimental_flamegraph
+ where ts = 8300973884377
+ and upid = 1
+ and profile_type = 'native'
order by abs(cumulative_size) desc;
-```
+```
| name | map_name | cumulative_size |
|------|----------|----------------|
diff --git a/gn/standalone/toolchain/BUILD.gn b/gn/standalone/toolchain/BUILD.gn
index 8dba119..408f1f6 100644
--- a/gn/standalone/toolchain/BUILD.gn
+++ b/gn/standalone/toolchain/BUILD.gn
@@ -119,6 +119,8 @@
_target_triplet = "x86_64-apple-darwin"
} else if (target_os == "mac" && target_cpu == "x86") {
_target_triplet = "i686-apple-darwin"
+ } else if (target_os == "mac" && target_cpu == "arm64") {
+ _target_triplet = "aarch64-apple-darwin"
} else if (target_os == "linux" && target_cpu == "arm64") {
_target_triplet = "aarch64-linux-gnu"
_default_target_sysroot =
diff --git a/src/base/base64.cc b/src/base/base64.cc
index 437ad67..bb2e3c4 100644
--- a/src/base/base64.cc
+++ b/src/base/base64.cc
@@ -146,7 +146,7 @@
PERFETTO_CHECK(res <= static_cast<ssize_t>(dst.size()));
dst.resize(static_cast<size_t>(res));
- return make_optional(dst);
+ return base::make_optional(dst);
}
} // namespace base
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index ef1492d..92c8bfa 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -514,8 +514,9 @@
trace_config_.reset(new TraceConfig());
bool parsed = false;
- const bool will_trace = !is_attach() && !query_service_ && !bugreport_;
- if (!will_trace) {
+ const bool will_trace_or_trigger =
+ !is_attach() && !query_service_ && !bugreport_;
+ if (!will_trace_or_trigger) {
if ((!trace_config_raw.empty() || has_config_options)) {
PERFETTO_ELOG("Cannot specify a trace config with this option");
return 1;
@@ -545,7 +546,7 @@
if (parsed) {
*trace_config_->mutable_statsd_metadata() = std::move(statsd_metadata);
trace_config_raw.clear();
- } else if (will_trace) {
+ } else if (will_trace_or_trigger) {
PERFETTO_ELOG("The trace config is invalid, bailing out.");
return 1;
}
@@ -573,21 +574,23 @@
return 1;
}
- if (trace_config_->activate_triggers().empty() &&
- trace_config_->incident_report_config().destination_package().empty() &&
+ // Only save to incidentd if:
+ // 1) --upload is set
+ // 2) |skip_incidentd| is absent or false.
+ // 3) we are not simply activating triggers.
+ save_to_incidentd_ =
+ upload_flag_ &&
!trace_config_->incident_report_config().skip_incidentd() &&
- upload_flag_) {
+ trace_config_->activate_triggers().empty();
+
+ if (save_to_incidentd_ &&
+ trace_config_->incident_report_config().destination_package().empty()) {
PERFETTO_ELOG(
"Missing IncidentReportConfig.destination_package with --dropbox / "
"--upload.");
return 1;
}
- // Only save to incidentd if both --upload is set and |skip_incidentd| is
- // absent or false.
- save_to_incidentd_ =
- upload_flag_ && !trace_config_->incident_report_config().skip_incidentd();
-
// Respect the wishes of the config with respect to statsd logging or fall
// back on the presence of the --upload flag if not set.
switch (trace_config_->statsd_logging()) {
@@ -644,7 +647,7 @@
}
bool open_out_file = true;
- if (!will_trace) {
+ if (!will_trace_or_trigger) {
open_out_file = false;
if (!trace_out_path_.empty() || upload_flag_) {
PERFETTO_ELOG("Can't pass an --out file (or --upload) with this option");
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index b983d7b..619a9e6 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -158,7 +158,7 @@
if (size == 0)
return kUnknownTraceType;
std::string start(reinterpret_cast<const char*>(data),
- std::min<size_t>(size, 32));
+ std::min<size_t>(size, kGuessTraceMaxLookahead));
if (size >= 8) {
uint64_t first_word;
memcpy(&first_word, data, sizeof(first_word));
@@ -180,10 +180,16 @@
base::StartsWith(start, "<html>"))
return kSystraceTraceType;
- // Ctrace is deflate'ed systrace.
- if (base::Contains(start, "TRACE:"))
+ // Traces obtained from atrace -z (compress).
+ // They all have the string "TRACE:" followed by 78 9C which is a zlib header
+ // for "deflate, default compression, window size=32K" (see b/208691037)
+ if (base::Contains(start, "TRACE:\n\x78\x9c"))
return kCtraceTraceType;
+ // Traces obtained from atrace without -z (no compression).
+ if (base::Contains(start, "TRACE:\n"))
+ return kSystraceTraceType;
+
// Ninja's buils log (.ninja_log).
if (base::StartsWith(start, "# ninja log"))
return kNinjaLogTraceType;
diff --git a/src/trace_processor/forwarding_trace_parser.h b/src/trace_processor/forwarding_trace_parser.h
index a5164cf..566b3b8 100644
--- a/src/trace_processor/forwarding_trace_parser.h
+++ b/src/trace_processor/forwarding_trace_parser.h
@@ -24,6 +24,8 @@
namespace perfetto {
namespace trace_processor {
+constexpr size_t kGuessTraceMaxLookahead = 64;
+
enum TraceType {
kUnknownTraceType,
kProtoTraceType,
diff --git a/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc b/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
index 1579f4a..67184e5 100644
--- a/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
+++ b/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
@@ -93,7 +93,7 @@
TraceStorage* storage,
base::Optional<UniquePid> upid,
base::Optional<std::string> upid_group,
- base::Optional<int64_t> timestamp,
+ int64_t default_timestamp,
StringId profile_type) {
const tables::StackProfileCallsiteTable& callsites_tbl =
storage->stack_profile_callsite_table();
@@ -136,18 +136,19 @@
} else {
row.depth = 0;
}
- // For heap profiling, the 'ts' column is always the arbitrary value
- // inputed in a query of the form below, not the actual time
- // when the allocation happened:
- // `select * form experimental_flamegraph(605908369259172, 1, 'native')`
- // However, removing this value would break the query with constraints
- // such as the one above because SQLite will do an equality check on the
- // `ts` column: `ts == 605908369259172`.
- // TODO(octaviant) find a way of removing this or giving it a meaningful
- // value
- if (timestamp) {
- row.ts = *timestamp;
- }
+
+ // The 'ts' column is given a default value, taken from the query.
+ // So if the query is:
+ // `select * form experimental_flamegraph
+ // where ts = 605908369259172
+ // and upid = 1
+ // and profile_type = 'native'`
+ // then row.ts == 605908369259172, for all rows
+ // This is not accurate. However, at present there is no other
+ // straightforward way of assigning timestamps to non-leaf nodes in the
+ // flamegraph tree. Non-leaf nodes would have to be assigned >= 1
+ // timestamps, which would increase data size without an advantage.
+ row.ts = default_timestamp;
if (upid) {
row.upid = *upid;
}
@@ -399,14 +400,33 @@
SqlValue::Long(tc.value)};
filtered = filtered.Filter({cs});
}
-
if (filtered.row_count() == 0) {
- return nullptr;
+ std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> empty_tbl(
+ new tables::ExperimentalFlamegraphNodesTable(
+ storage->mutable_string_pool(), nullptr));
+ return empty_tbl;
+ }
+
+ // The logic underneath is selecting a default timestamp to be used by all
+ // frames which do not have a timestamp. The timestamp is taken from the query
+ // value and it's not meaningful for the row. It prevents however the rows
+ // with no timestamp from being filtered out by Sqlite, after we create the
+ // table ExperimentalFlamegraphNodesTable in this class.
+ int64_t default_timestamp = 0;
+ if (!time_constraints.empty()) {
+ auto& tc = time_constraints[0];
+ if (tc.op == FilterOp::kGt) {
+ default_timestamp = tc.value + 1;
+ } else if (tc.op == FilterOp::kLt) {
+ default_timestamp = tc.value - 1;
+ } else {
+ default_timestamp = tc.value;
+ }
}
StringId profile_type = storage->InternString("perf");
FlamegraphTableAndMergedCallsites table_and_callsites =
BuildFlamegraphTableTreeStructure(storage, upid, upid_group,
- base::nullopt, profile_type);
+ default_timestamp, profile_type);
return BuildFlamegraphTableCallstackSizeAndCount(
std::move(table_and_callsites.tbl),
table_and_callsites.callsite_to_merged_callsite, filtered);
diff --git a/src/trace_processor/importers/systrace/systrace_trace_parser.cc b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
index 445dda1..0fe8781 100644
--- a/src/trace_processor/importers/systrace/systrace_trace_parser.cc
+++ b/src/trace_processor/importers/systrace/systrace_trace_parser.cc
@@ -19,6 +19,7 @@
#include "perfetto/base/logging.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
+#include "src/trace_processor/forwarding_trace_parser.h"
#include "src/trace_processor/importers/common/process_tracker.h"
#include "src/trace_processor/trace_sorter.h"
@@ -71,6 +72,18 @@
blob.data() + blob.size());
if (state_ == ParseState::kBeforeParse) {
+ // Remove anything before the TRACE:\n marker, which is emitted when
+ // obtaining traces via `adb shell "atrace -t 1 sched" > out.txt`.
+ std::array<uint8_t, 7> kAtraceMarker = {'T', 'R', 'A', 'C', 'E', ':', '\n'};
+ auto search_end = partial_buf_.begin() +
+ static_cast<int>(std::min(partial_buf_.size(),
+ kGuessTraceMaxLookahead));
+ auto it = std::search(partial_buf_.begin(), search_end,
+ kAtraceMarker.begin(), kAtraceMarker.end());
+ if (it != search_end)
+ partial_buf_.erase(partial_buf_.begin(), it + kAtraceMarker.size());
+
+ // Deal with HTML traces.
state_ = partial_buf_[0] == '<' ? ParseState::kHtmlBeforeSystrace
: ParseState::kSystrace;
}
diff --git a/src/traced/probes/ftrace/cpu_reader.cc b/src/traced/probes/ftrace/cpu_reader.cc
index 6e09c01..7867ff6 100644
--- a/src/traced/probes/ftrace/cpu_reader.cc
+++ b/src/traced/probes/ftrace/cpu_reader.cc
@@ -72,18 +72,14 @@
uint32_t time_delta : 27;
};
-bool ReadIntoString(const uint8_t* start,
- const uint8_t* end,
+// Reads a string from `start` until the first '\0' byte or until fixed_len
+// characters have been read. Appends it to `*out` as field `field_id`.
+void ReadIntoString(const uint8_t* start,
+ size_t fixed_len,
uint32_t field_id,
protozero::Message* out) {
- for (const uint8_t* c = start; c < end; c++) {
- if (*c != '\0')
- continue;
- out->AppendBytes(field_id, reinterpret_cast<const char*>(start),
- static_cast<uintptr_t>(c - start));
- return true;
- }
- return false;
+ size_t len = strnlen(reinterpret_cast<const char*>(start), fixed_len);
+ out->AppendBytes(field_id, reinterpret_cast<const char*>(start), len);
}
bool ReadDataLoc(const uint8_t* start,
@@ -104,12 +100,11 @@
const uint16_t offset = data & 0xffff;
const uint16_t len = (data >> 16) & 0xffff;
const uint8_t* const string_start = start + offset;
- const uint8_t* const string_end = string_start + len;
- if (string_start <= start || string_end > end) {
+ if (string_start <= start || string_start + len > end) {
PERFETTO_DFATAL("Buffer overflowed.");
return false;
}
- ReadIntoString(string_start, string_end, field.proto_field_id, message);
+ ReadIntoString(string_start, len, field.proto_field_id, message);
return true;
}
@@ -752,12 +747,14 @@
ReadIntoVarInt<int64_t>(field_start, field_id, message);
return true;
case kFixedCStringToString:
- // TODO(hjd): Add AppendMaxLength string to protozero.
- return ReadIntoString(field_start, field_start + field.ftrace_size,
- field_id, message);
+ // TODO(hjd): Kernel-dive to check this how size:0 char fields work.
+ ReadIntoString(field_start, field.ftrace_size, field_id, message);
+ return true;
case kCStringToString:
// TODO(hjd): Kernel-dive to check this how size:0 char fields work.
- return ReadIntoString(field_start, end, field_id, message);
+ ReadIntoString(field_start, static_cast<size_t>(end - field_start),
+ field_id, message);
+ return true;
case kStringPtrToString: {
uint64_t n = 0;
// The ftrace field may be 8 or 4 bytes and we need to copy it into the
diff --git a/src/traced/probes/ftrace/cpu_reader_unittest.cc b/src/traced/probes/ftrace/cpu_reader_unittest.cc
index 84e6c6f..c918af8 100644
--- a/src/traced/probes/ftrace/cpu_reader_unittest.cc
+++ b/src/traced/probes/ftrace/cpu_reader_unittest.cc
@@ -39,6 +39,7 @@
#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
#include "protos/perfetto/trace/ftrace/power.gen.h"
#include "protos/perfetto/trace/ftrace/sched.gen.h"
+#include "protos/perfetto/trace/ftrace/task.gen.h"
#include "protos/perfetto/trace/trace_packet.gen.h"
#include "src/traced/probes/ftrace/test/test_messages.gen.h"
#include "src/traced/probes/ftrace/test/test_messages.pbzero.h"
@@ -540,20 +541,20 @@
}
// This event is as the event for ParseSinglePrint above except the string
-// is extended to overflow the page size written in the header.
-static ExamplePage g_single_print_malformed{
+// is extended and not null terminated.
+static ExamplePage g_single_print_non_null_terminated{
"synthetic",
R"(
- 00000000: ba12 6a33 c628 0200 2c00 0000 0000 0000 ................
- 00000010: def0 ec67 8d21 0000 0800 0000 0500 0001 ................
- 00000020: 2870 0000 ac5d 1661 86ff ffff 4865 6c6c ................
- 00000030: 6f2c 2077 6f72 6c64 2120 776f 726c 6421 ................
- 00000040: 0a00 ff00 0000 0000 0000 0000 0000 0000 ................
+ 00000000: ba12 6a33 c628 0200 2c00 0000 0000 0000 ..j3.(..,.......
+ 00000010: def0 ec67 8d21 0000 0800 0000 0500 0001 ...g.!..........
+ 00000020: 2870 0000 ac5d 1661 86ff ffff 4865 6c6c (p...].a....Hell
+ 00000030: 6f2c 2077 6f72 6c64 2161 6161 6161 6161 o, world!aaaaaaa
+ 00000040: 6161 6161 6161 6161 6161 6161 6161 6161 aaaaaaaaaaaaaaaa
)",
};
-TEST(CpuReaderTest, ParseSinglePrintMalformed) {
- const ExamplePage* test_case = &g_single_print_malformed;
+TEST(CpuReaderTest, ParseSinglePrintNonNullTerminated) {
+ const ExamplePage* test_case = &g_single_print_non_null_terminated;
BundleProvider bundle_provider(base::kPageSize);
ProtoTranslationTable* table = GetTable(test_case->name);
@@ -579,19 +580,68 @@
parse_pos, &page_header.value(), table, &ds_config, compact_buffer.get(),
bundle_provider.writer(), &metadata);
- ASSERT_EQ(0u, evt_bytes);
+ ASSERT_EQ(44u, evt_bytes);
auto bundle = bundle_provider.ParseProto();
ASSERT_TRUE(bundle);
ASSERT_EQ(bundle->event().size(), 1u);
- // Although one field is malformed we still see data for the rest
- // since we write the fields as we parse them for speed.
const protos::gen::FtraceEvent& event = bundle->event()[0];
EXPECT_EQ(event.pid(), 28712ul);
EXPECT_TRUE(WithinOneMicrosecond(event.timestamp(), 608934, 535199));
+ EXPECT_EQ(event.print().buf(), "Hello, world!aaa");
+}
+
+static ExamplePage g_single_print_zero_size{
+ "synthetic",
+ R"(
+ 00000000: ba12 6a33 c628 0200 2c00 0000 0000 0000 ..j3.(..,.......
+ 00000010: def0 ec67 8d21 0000 0800 0000 0500 0001 ...g.!..........
+ 00000020: 2870 0000 ac5d 1661 86ff ffff 0000 0000 (p...].a........
+ 00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
+ 00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
+ )",
+};
+
+TEST(CpuReaderTest, ParseSinglePrintZeroSize) {
+ const ExamplePage* test_case = &g_single_print_zero_size;
+
+ BundleProvider bundle_provider(base::kPageSize);
+ ProtoTranslationTable* table = GetTable(test_case->name);
+ auto page = PageFromXxd(test_case->data);
+
+ FtraceDataSourceConfig ds_config = EmptyConfig();
+ ds_config.event_filter.AddEnabledEvent(
+ table->EventToFtraceId(GroupAndName("ftrace", "print")));
+
+ FtraceMetadata metadata{};
+ std::unique_ptr<CompactSchedBuffer> compact_buffer(new CompactSchedBuffer());
+ const uint8_t* parse_pos = page.get();
+ base::Optional<CpuReader::PageHeader> page_header =
+ CpuReader::ParsePageHeader(&parse_pos, table->page_header_size_len());
+
+ const uint8_t* page_end = page.get() + base::kPageSize;
+ ASSERT_TRUE(page_header.has_value());
+ EXPECT_FALSE(page_header->lost_events);
+ EXPECT_TRUE(parse_pos < page_end);
+ EXPECT_TRUE(parse_pos + page_header->size < page_end);
+
+ size_t evt_bytes = CpuReader::ParsePagePayload(
+ parse_pos, &page_header.value(), table, &ds_config, compact_buffer.get(),
+ bundle_provider.writer(), &metadata);
+
+ ASSERT_EQ(44u, evt_bytes);
+
+ auto bundle = bundle_provider.ParseProto();
+ ASSERT_TRUE(bundle);
+ ASSERT_EQ(bundle->event().size(), 1u);
+ const protos::gen::FtraceEvent& event = bundle->event()[0];
+ EXPECT_EQ(event.pid(), 28712ul);
+ EXPECT_TRUE(WithinOneMicrosecond(event.timestamp(), 608934, 535199));
+ EXPECT_TRUE(event.print().has_buf());
EXPECT_EQ(event.print().buf(), "");
}
+
TEST(CpuReaderTest, FilterByEvent) {
const ExamplePage* test_case = &g_single_print;
@@ -1136,6 +1186,42 @@
EXPECT_THAT(metadata.pids, Contains(9999));
}
+// Regression test for b/205763418: Kernels without f0a515780393("tracing: Don't
+// make assumptions about length of string on task rename") can output non
+// zero-terminated strings in some cases. Even though it's a kernel bug, there's
+// no point in rejecting that.
+TEST(CpuReaderTest, EventNonZeroTerminated) {
+ BundleProvider bundle_provider(base::kPageSize);
+
+ BinaryWriter writer;
+ ProtoTranslationTable* table = GetTable("android_seed_N2F62_3.10.49");
+
+ constexpr uint32_t kTaskRenameId = 19;
+
+ writer.Write<int32_t>(1001); // Common field.
+ writer.Write<int32_t>(9999); // Common pid
+ writer.Write<int32_t>(9999); // Pid
+ writer.WriteFixedString(16, "Hello"); // Old Comm
+ std::array<char, 16> newcomm;
+ memcpy(&newcomm, "0123456789abcdef", sizeof newcomm);
+ writer.Write(newcomm); // New Comm - not null terminated
+ writer.Write<uint64_t>(10); // flags
+ writer.Write<int16_t>(10); // oom_score_adj
+
+ auto input = writer.GetCopy();
+ auto length = writer.written();
+ FtraceMetadata metadata{};
+
+ ASSERT_TRUE(CpuReader::ParseEvent(
+ kTaskRenameId, input.get(), input.get() + length, table,
+ bundle_provider.writer()->add_event(), &metadata));
+ std::unique_ptr<protos::gen::FtraceEventBundle> a =
+ bundle_provider.ParseProto();
+ ASSERT_NE(a, nullptr);
+ ASSERT_EQ(a->event().size(), 1u);
+ ASSERT_EQ(a->event()[0].task_rename().newcomm(), "0123456789abcdef");
+}
+
// Page with a single sched_switch, no data loss.
static char g_switch_page[] =
R"(
diff --git a/src/traced/probes/ftrace/proto_translation_table.cc b/src/traced/probes/ftrace/proto_translation_table.cc
index 9a69745..a644390 100644
--- a/src/traced/probes/ftrace/proto_translation_table.cc
+++ b/src/traced/probes/ftrace/proto_translation_table.cc
@@ -261,6 +261,9 @@
// are both fixed size and null terminated meaning that we can't just drop
// them directly into the protobuf (since if the string is shorter than 15
// characters we want only the bit up to the null terminator).
+ //
+ // In some rare cases (e.g. old kernel bugs) these strings might not be null
+ // terminated (b/205763418).
if (Match(type_and_name.c_str(), R"(char [a-zA-Z_]+\[[0-9]+\])")) {
*out = kFtraceFixedCString;
return true;
diff --git a/test/data/atrace_compressed.ctrace.sha256 b/test/data/atrace_compressed.ctrace.sha256
new file mode 100644
index 0000000..d39c5ea
--- /dev/null
+++ b/test/data/atrace_compressed.ctrace.sha256
@@ -0,0 +1 @@
+db92be3d78ab0619af5068240b88bad78a655f3e403c5f7c7876956eb5b260b0
\ No newline at end of file
diff --git a/test/data/atrace_uncompressed_b_208691037.sha256 b/test/data/atrace_uncompressed_b_208691037.sha256
new file mode 100644
index 0000000..7348172
--- /dev/null
+++ b/test/data/atrace_uncompressed_b_208691037.sha256
@@ -0,0 +1 @@
+9872b827df72895a98af2977c117ad3bc31f8c62be79d211bdf254170db4e1de
\ No newline at end of file
diff --git a/test/end_to_end_integrationtest.cc b/test/end_to_end_integrationtest.cc
index 1ec13d3..3d2b65e 100644
--- a/test/end_to_end_integrationtest.cc
+++ b/test/end_to_end_integrationtest.cc
@@ -296,10 +296,30 @@
#define TEST_PRODUCER_SOCK_NAME ::perfetto::GetProducerSocket()
#endif
+// Defining this macro out-of-line works around C/C++'s macro rules (see
+// https://stackoverflow.com/questions/26284393/nested-operator-in-c-preprocessor).
+#define DisableTest(x) DISABLED_##x
+
#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
#define TreeHuggerOnly(x) x
#else
-#define TreeHuggerOnly(x) DISABLED_##x
+#define TreeHuggerOnly(x) DisableTest(x)
+#endif
+
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#define AndroidOnly(x) x
+#else
+#define AndroidOnly(x) DisableTest(x)
+#endif
+
+// Disable cmdline tests on sanitizets because they use fork() and that messes
+// up leak / races detections, which has been fixed only recently (see
+// https://github.com/google/sanitizers/issues/836 ).
+#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
+ defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER)
+#define NoSanitizers(X) DisableTest(X)
+#else
+#define NoSanitizers(X) X
#endif
// TODO(b/73453011): reenable on more platforms (including standalone Android).
@@ -1190,16 +1210,6 @@
Property(&protos::gen::TestEvent::str, SizeIs(kMsgSize)))));
}
-// Disable cmdline tests on sanitizets because they use fork() and that messes
-// up leak / races detections, which has been fixed only recently (see
-// https://github.com/google/sanitizers/issues/836 ).
-#if defined(ADDRESS_SANITIZER) || defined(THREAD_SANITIZER) || \
- defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER)
-#define NoSanitizers(X) DISABLED_##X
-#else
-#define NoSanitizers(X) X
-#endif
-
TEST_F(PerfettoCmdlineTest, NoSanitizers(InvalidCases)) {
std::string cfg("duration_ms: 100");
@@ -1518,11 +1528,8 @@
// Dropbox on the commandline client only works on android builds. So disable
// this test on all other builds.
-#if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
-TEST_F(PerfettoCmdlineTest, NoSanitizers(NoDataNoFileWithoutTrigger)) {
-#else
-TEST_F(PerfettoCmdlineTest, DISABLED_NoDataNoFileWithoutTrigger) {
-#endif
+TEST_F(PerfettoCmdlineTest,
+ NoSanitizers(TreeHuggerOnly(NoDataNoFileWithoutTrigger))) {
// See |message_count| and |message_size| in the TraceConfig above.
constexpr size_t kMessageCount = 11;
constexpr size_t kMessageSize = 32;
@@ -1746,4 +1753,86 @@
EXPECT_EQ(0, query_raw.Run(&stderr_)) << stderr_;
}
+TEST_F(PerfettoCmdlineTest,
+ NoSanitizers(AndroidOnly(CmdTriggerWithUploadFlag))) {
+ // See |message_count| and |message_size| in the TraceConfig above.
+ constexpr size_t kMessageCount = 2;
+ constexpr size_t kMessageSize = 2;
+ protos::gen::TraceConfig trace_config;
+ trace_config.add_buffers()->set_size_kb(1024);
+ auto* ds_config = trace_config.add_data_sources()->mutable_config();
+ ds_config->set_name("android.perfetto.FakeProducer");
+ ds_config->mutable_for_testing()->set_message_count(kMessageCount);
+ ds_config->mutable_for_testing()->set_message_size(kMessageSize);
+ auto* trigger_cfg = trace_config.mutable_trigger_config();
+ trigger_cfg->set_trigger_mode(
+ protos::gen::TraceConfig::TriggerConfig::STOP_TRACING);
+ trigger_cfg->set_trigger_timeout_ms(15000);
+ auto* trigger = trigger_cfg->add_triggers();
+ trigger->set_name("trigger_name");
+ // |stop_delay_ms| must be long enough that we can write the packets in
+ // before the trace finishes. This has to be long enough for the slowest
+ // emulator. But as short as possible to prevent the test running a long
+ // time.
+ trigger->set_stop_delay_ms(500);
+
+ // We have to construct all the processes we want to fork before we start the
+ // service with |StartServiceIfRequired()|. this is because it is unsafe
+ // (could deadlock) to fork after we've spawned some threads which might
+ // printf (and thus hold locks).
+ const std::string path = RandomTraceFileName();
+ auto perfetto_proc = ExecPerfetto(
+ {
+ "-o",
+ path,
+ "-c",
+ "-",
+ },
+ trace_config.SerializeAsString());
+
+ std::string triggers = R"(
+ activate_triggers: "trigger_name"
+ )";
+ auto perfetto_proc_2 = ExecPerfetto(
+ {
+ "--upload",
+ "-c",
+ "-",
+ "--txt",
+ },
+ triggers);
+
+ // Start the service and connect a simple fake producer.
+ StartServiceIfRequiredNoNewExecsAfterThis();
+ auto* fake_producer = ConnectFakeProducer();
+ EXPECT_TRUE(fake_producer);
+
+ std::thread background_trace([&perfetto_proc]() {
+ std::string stderr_str;
+ EXPECT_EQ(0, perfetto_proc.Run(&stderr_str)) << stderr_str;
+ });
+
+ WaitForProducerEnabled();
+ // Wait for the producer to start, and then write out 11 packets, before the
+ // trace actually starts (the trigger is seen).
+ auto on_data_written = task_runner_.CreateCheckpoint("data_written_1");
+ fake_producer->ProduceEventBatch(WrapTask(on_data_written));
+ task_runner_.RunUntilCheckpoint("data_written_1");
+
+ EXPECT_EQ(0, perfetto_proc_2.Run(&stderr_)) << "stderr: " << stderr_;
+
+ background_trace.join();
+
+ std::string trace_str;
+ base::ReadFile(path, &trace_str);
+ protos::gen::Trace trace;
+ ASSERT_TRUE(trace.ParseFromString(trace_str));
+ EXPECT_LT(static_cast<int>(kMessageCount), trace.packet_size());
+ for (const auto& packet : trace.packet()) {
+ if (packet.has_trigger()) {
+ EXPECT_EQ("trigger_name", packet.trigger().trigger_name());
+ }
+ }
+}
+
} // namespace perfetto
diff --git a/test/trace_processor/parsing/atrace_compressed_sched_count.out b/test/trace_processor/parsing/atrace_compressed_sched_count.out
new file mode 100644
index 0000000..c03bd11
--- /dev/null
+++ b/test/trace_processor/parsing/atrace_compressed_sched_count.out
@@ -0,0 +1,2 @@
+"COUNT(1)"
+1120
diff --git a/test/trace_processor/parsing/atrace_uncompressed_sched_count.out b/test/trace_processor/parsing/atrace_uncompressed_sched_count.out
new file mode 100644
index 0000000..41c340b
--- /dev/null
+++ b/test/trace_processor/parsing/atrace_uncompressed_sched_count.out
@@ -0,0 +1,2 @@
+"COUNT(1)"
+9
diff --git a/test/trace_processor/parsing/index b/test/trace_processor/parsing/index
index b586e28..ce6f074 100644
--- a/test/trace_processor/parsing/index
+++ b/test/trace_processor/parsing/index
@@ -163,3 +163,10 @@
# Multiuser
android_multiuser_switch.textproto android_multiuser android_multiuser_switch.out
+
+# Output of atrace -z.
+../../data/atrace_compressed.ctrace sched_smoke.sql atrace_compressed_sched_count.out
+
+# Output of adb shell "atrace -t 1 sched" > out.txt". It has extra garbage
+# coming from stderr before the TRACE: marker. See b/208691037.
+../../data/atrace_uncompressed_b_208691037 sched_smoke.sql atrace_uncompressed_sched_count.out
diff --git a/test/trace_processor/profiling/heap_graph_flamegraph.sql b/test/trace_processor/profiling/heap_graph_flamegraph.sql
index 92367f9..34692ef 100644
--- a/test/trace_processor/profiling/heap_graph_flamegraph.sql
+++ b/test/trace_processor/profiling/heap_graph_flamegraph.sql
@@ -8,8 +8,8 @@
size,
cumulative_size,
parent_id
-FROM experimental_flamegraph(
- (select max(graph_sample_ts) from heap_graph_object),
- (select max(upid) from heap_graph_object),
- 'graph')
+FROM experimental_flamegraph
+where upid = (select max(upid) from heap_graph_object)
+ and profile_type = 'graph'
+ and ts = (select max(graph_sample_ts) from heap_graph_object)
LIMIT 10
diff --git a/test/trace_processor/profiling/heap_graph_flamegraph_focused.sql b/test/trace_processor/profiling/heap_graph_flamegraph_focused.sql
index 95d498a..e6b514c 100644
--- a/test/trace_processor/profiling/heap_graph_flamegraph_focused.sql
+++ b/test/trace_processor/profiling/heap_graph_flamegraph_focused.sql
@@ -7,9 +7,9 @@
size,
cumulative_size,
parent_id
-FROM experimental_flamegraph(
- (select max(graph_sample_ts) from heap_graph_object),
- (select max(upid) from heap_graph_object),
- 'graph')
-WHERE focus_str = 'left'
+FROM experimental_flamegraph
+where upid = (select max(upid) from heap_graph_object)
+ and profile_type = 'graph'
+ and ts = (select max(graph_sample_ts) from heap_graph_object)
+ and focus_str = 'left'
LIMIT 10
diff --git a/test/trace_processor/profiling/heap_profile_flamegraph.sql b/test/trace_processor/profiling/heap_profile_flamegraph.sql
index fa90b15..e1feb37 100644
--- a/test/trace_processor/profiling/heap_profile_flamegraph.sql
+++ b/test/trace_processor/profiling/heap_profile_flamegraph.sql
@@ -1 +1,5 @@
-select * from experimental_flamegraph(605908369259172, 1, 'native') limit 10;
+select * from experimental_flamegraph
+where ts = 605908369259172
+ and upid = 1
+ and profile_type = 'native'
+limit 10;
diff --git a/tools/build_all_configs.py b/tools/build_all_configs.py
index 95fe044..0c0ec5d 100755
--- a/tools/build_all_configs.py
+++ b/tools/build_all_configs.py
@@ -100,6 +100,11 @@
else:
assert False, 'Unsupported system %r' % system
+ machine = platform.machine()
+ if machine == 'arm64':
+ for name, config in configs.items():
+ configs[name] = config + ('host_cpu="arm64"',)
+
if args.ccache:
for config_name, gn_args in iteritems(configs):
configs[config_name] = gn_args + ('cc_wrapper="ccache"',)
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 7c42009..8ab5034 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -60,6 +60,7 @@
TrackState,
VisibleState,
} from './state';
+import {toNs} from './time';
type StateDraft = Draft<State>;
@@ -616,8 +617,13 @@
ts: args.ts,
type: args.type,
};
- this.openFlamegraph(
- state, {...args, viewingOption: DEFAULT_VIEWING_OPTION});
+ this.openFlamegraph(state, {
+ type: args.type,
+ startNs: toNs(state.traceTime.startSec),
+ endNs: args.ts,
+ upids: [args.upid],
+ viewingOption: DEFAULT_VIEWING_OPTION
+ });
},
selectPerfSamples(
@@ -630,21 +636,27 @@
ts: args.ts,
type: args.type,
};
- this.openFlamegraph(state, {...args, viewingOption: PERF_SAMPLES_KEY});
+ this.openFlamegraph(state, {
+ type: args.type,
+ startNs: toNs(state.traceTime.startSec),
+ endNs: args.ts,
+ upids: [args.upid],
+ viewingOption: PERF_SAMPLES_KEY
+ });
},
openFlamegraph(state: StateDraft, args: {
- id: number,
- upid: number,
- ts: number,
+ upids: number[],
+ startNs: number,
+ endNs: number,
type: string,
viewingOption: FlamegraphStateViewingOption
}): void {
state.currentFlamegraphState = {
kind: 'FLAMEGRAPH_STATE',
- id: args.id,
- upid: args.upid,
- ts: args.ts,
+ upids: args.upids,
+ startNs: args.startNs,
+ endNs: args.endNs,
type: args.type,
viewingOption: args.viewingOption,
focusRegex: ''
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 9ceff3f..865982f 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -72,7 +72,8 @@
// 11: Rename updateChromeCategories to fetchChromeCategories.
// 12: Add a field to cache mapping from UI track ID to trace track ID in order
// to speed up flow arrows rendering.
-export const STATE_VERSION = 12;
+// 13: FlamegraphState changed to support area selection.
+export const STATE_VERSION = 13;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -244,9 +245,9 @@
export interface FlamegraphState {
kind: 'FLAMEGRAPH_STATE';
- id: number;
- upid: number;
- ts: number;
+ upids: number[];
+ startNs: number;
+ endNs: number;
type: string;
viewingOption: FlamegraphStateViewingOption;
focusRegex: string;
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
index 06c3cee..af41ec7 100644
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -26,6 +26,7 @@
import {NUM} from '../../common/query_result';
import {Area, Sorting} from '../../common/state';
import {publishAggregateData} from '../../frontend/publish';
+import {AreaSelectionHandler} from '../area_selection_handler';
import {Controller} from '../controller';
import {globals} from '../globals';
@@ -38,17 +39,9 @@
return column.kind === 'STRING' || column.kind === 'STATE';
}
-function isAreaEqual(area: Area, previousArea?: Area) {
- if (previousArea === undefined || area.startSec !== previousArea.startSec ||
- area.endSec !== previousArea.endSec) {
- return false;
- }
- return area.tracks.every((element, i) => element === previousArea.tracks[i]);
-}
-
export abstract class AggregationController extends Controller<'main'> {
readonly kind: string;
- private previousArea?: Area;
+ private areaSelectionHandler: AreaSelectionHandler;
private previousSorting?: Sorting;
private requestingData = false;
private queuedRequest = false;
@@ -64,6 +57,7 @@
constructor(private args: AggregationControllerArgs) {
super('main');
this.kind = this.args.kind;
+ this.areaSelectionHandler = new AreaSelectionHandler();
}
run() {
@@ -82,22 +76,20 @@
});
return;
}
- const selectedArea = globals.state.areas[selection.areaId];
const aggregatePreferences =
globals.state.aggregatePreferences[this.args.kind];
- const areaChanged = !isAreaEqual(selectedArea, this.previousArea);
const sortingChanged = aggregatePreferences &&
this.previousSorting !== aggregatePreferences.sorting;
- if (!areaChanged && !sortingChanged) return;
+ const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange();
+ if ((!hasAreaChanged && !sortingChanged) || !area) return;
if (this.requestingData) {
this.queuedRequest = true;
} else {
this.requestingData = true;
if (sortingChanged) this.previousSorting = aggregatePreferences.sorting;
- if (areaChanged) this.previousArea = Object.assign({}, selectedArea);
- this.getAggregateData(selectedArea, areaChanged)
+ this.getAggregateData(area, hasAreaChanged)
.then(data => publishAggregateData({data, kind: this.args.kind}))
.finally(() => {
this.requestingData = false;
diff --git a/ui/src/controller/area_selection_handler.ts b/ui/src/controller/area_selection_handler.ts
new file mode 100644
index 0000000..3f5368d
--- /dev/null
+++ b/ui/src/controller/area_selection_handler.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2021 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 {Area, AreaById} from '../common/state';
+import {globals as frontendGlobals} from '../frontend/globals';
+
+export class AreaSelectionHandler {
+ private previousArea?: Area;
+
+ getAreaChange(): [boolean, AreaById|undefined] {
+ const currentSelection = frontendGlobals.state.currentSelection;
+ if (currentSelection === null || currentSelection.kind !== 'AREA') {
+ return [false, undefined];
+ }
+
+ const selectedArea = frontendGlobals.state.areas[currentSelection.areaId];
+ // Area is considered changed if:
+ // 1. The new area is defined and the old area undefined.
+ // 2. The new area is undefined and the old area defined (viceversa from 1).
+ // 3. Both areas are defined but their start or end times differ.
+ // 4. Both areas are defined but their tracks differ.
+ let hasAreaChanged = (!!this.previousArea !== !!selectedArea);
+ if (selectedArea && this.previousArea) {
+ // There seems to be an issue with clang-format http://shortn/_Pt98d5MCjG
+ // where `a ||= b` is formatted to `a || = b`, by inserting a space which
+ // breaks the operator.
+ // Therefore, we are using the pattern `a = a || b` instead.
+ hasAreaChanged = hasAreaChanged ||
+ selectedArea.startSec !== this.previousArea.startSec;
+ hasAreaChanged =
+ hasAreaChanged || selectedArea.endSec !== this.previousArea.endSec;
+ hasAreaChanged = hasAreaChanged ||
+ selectedArea.tracks.length !== this.previousArea.tracks.length;
+ for (let i = 0; i < selectedArea.tracks.length; ++i) {
+ hasAreaChanged = hasAreaChanged ||
+ selectedArea.tracks[i] !== this.previousArea.tracks[i];
+ }
+ }
+
+ if (hasAreaChanged) {
+ this.previousArea = selectedArea;
+ }
+
+ return [hasAreaChanged, selectedArea];
+ }
+}
diff --git a/ui/src/controller/area_selection_handler_unittest.ts b/ui/src/controller/area_selection_handler_unittest.ts
new file mode 100644
index 0000000..bbc3159
--- /dev/null
+++ b/ui/src/controller/area_selection_handler_unittest.ts
@@ -0,0 +1,136 @@
+// Copyright (C) 2021 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 {AreaById} from '../common/state';
+import {globals as frontendGlobals} from '../frontend/globals';
+
+import {AreaSelectionHandler} from './area_selection_handler';
+import {createEmptyState} from '../common/empty_state';
+
+test('validAreaAfterUndefinedArea', () => {
+ const areaId = '0';
+ const latestArea: AreaById = {startSec: 0, endSec: 1, tracks: [], id: areaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId};
+ frontendGlobals.state.areas[areaId] = latestArea;
+
+ const areaSelectionHandler = new AreaSelectionHandler();
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(latestArea);
+});
+
+test('UndefinedAreaAfterValidArea', () => {
+ const previousAreaId = '0';
+ const previous:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: previousAreaId
+ };
+ frontendGlobals.state.areas[previousAreaId] = previous;
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ const currentAreaId = '1';
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: currentAreaId
+ };
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(undefined);
+});
+
+test('UndefinedAreaAfterUndefinedArea', () => {
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId: '0'};
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId: '1'};
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(undefined);
+});
+
+test('validAreaAfterValidArea', () => {
+ const previousAreaId = '0';
+ const previous:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: previousAreaId
+ };
+ frontendGlobals.state.areas[previousAreaId] = previous;
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ const currentAreaId = '1';
+ const current:
+ AreaById = {startSec: 1, endSec: 2, tracks: [], id: currentAreaId};
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: currentAreaId
+ };
+ frontendGlobals.state.areas[currentAreaId] = current;
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(current);
+});
+
+test('sameAreaSelected', () => {
+ const previousAreaId = '0';
+ const previous:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: previousAreaId
+ };
+ frontendGlobals.state.areas[previousAreaId] = previous;
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ const currentAreaId = '0';
+ const current:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: currentAreaId};
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: currentAreaId
+ };
+ frontendGlobals.state.areas[currentAreaId] = current;
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(false);
+ expect(selectedArea).toEqual(current);
+});
+
+test('NonAreaSelectionAfterUndefinedArea', () => {
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId: '0'};
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ frontendGlobals.state
+ .currentSelection = {kind: 'COUNTER', leftTs: 0, rightTs: 0, id: 1};
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(false);
+ expect(selectedArea).toEqual(undefined);
+});
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index 7459c3f..cb7ea7e 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {Actions} from '../common/actions';
import {Engine} from '../common/engine';
import {
ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
@@ -26,10 +27,18 @@
} from '../common/flamegraph_util';
import {NUM, STR} from '../common/query_result';
import {CallsiteInfo, FlamegraphState} from '../common/state';
-import {fromNs} from '../common/time';
-import {FlamegraphDetails} from '../frontend/globals';
+import {toNs} from '../common/time';
+import {
+ FlamegraphDetails,
+ globals as frontendGlobals
+} from '../frontend/globals';
import {publishFlamegraphDetails} from '../frontend/publish';
+import {
+ Config as PerfSampleConfig,
+ PERF_SAMPLES_PROFILE_TRACK_KIND
+} from '../tracks/perf_samples_profile/common';
+import {AreaSelectionHandler} from './area_selection_handler';
import {Controller} from './controller';
import {globals} from './globals';
@@ -78,15 +87,46 @@
private requestingData = false;
private queuedRequest = false;
private flamegraphDetails: FlamegraphDetails = {};
+ private areaSelectionHandler: AreaSelectionHandler;
private cache: TablesCache;
constructor(private args: FlamegraphControllerArgs) {
super('main');
this.cache = new TablesCache(args.engine, 'grouped_callsites');
+ this.areaSelectionHandler = new AreaSelectionHandler();
}
run() {
- const selection = globals.state.currentFlamegraphState;
+ const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange();
+ if (hasAreaChanged) {
+ const upids = [];
+ if (!area) {
+ publishFlamegraphDetails(
+ {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
+ return;
+ }
+ for (const trackId of area.tracks) {
+ const trackState = frontendGlobals.state.tracks[trackId];
+ if (!trackState ||
+ trackState.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
+ continue;
+ }
+ upids.push((trackState.config as PerfSampleConfig).upid);
+ }
+ if (upids.length === 0) {
+ publishFlamegraphDetails(
+ {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
+ return;
+ }
+ frontendGlobals.dispatch(Actions.openFlamegraph({
+ upids,
+ startNs: toNs(area.startSec),
+ endNs: toNs(area.endSec),
+ type: 'perf',
+ viewingOption: PERF_SAMPLES_KEY
+ }));
+ }
+ const selection = frontendGlobals.state.currentFlamegraphState;
if (!selection || !this.shouldRequestData(selection)) {
return;
}
@@ -96,15 +136,17 @@
}
this.requestingData = true;
- this.assembleFlamegraphDetails(selection);
+ this.assembleFlamegraphDetails(selection, hasAreaChanged);
}
- private async assembleFlamegraphDetails(selection: FlamegraphState) {
+ private async assembleFlamegraphDetails(
+ selection: FlamegraphState, hasAreaChanged: boolean) {
const selectedFlamegraphState = {...selection};
const flamegraphMetadata = await this.getFlamegraphMetadata(
selection.type,
- selectedFlamegraphState.ts,
- selectedFlamegraphState.upid);
+ selectedFlamegraphState.startNs,
+ selectedFlamegraphState.endNs,
+ selectedFlamegraphState.upids);
if (flamegraphMetadata !== undefined) {
Object.assign(this.flamegraphDetails, flamegraphMetadata);
}
@@ -124,7 +166,8 @@
undefined :
selectedFlamegraphState.expandedCallsite.totalSize;
- const key = `${selectedFlamegraphState.upid};${selectedFlamegraphState.ts}`;
+ const key = `${selectedFlamegraphState.upids};${
+ selectedFlamegraphState.startNs};${selectedFlamegraphState.endNs}`;
try {
const flamegraphData = await this.getFlamegraphData(
@@ -132,19 +175,21 @@
selectedFlamegraphState.viewingOption ?
selectedFlamegraphState.viewingOption :
DEFAULT_VIEWING_OPTION,
- selection.ts,
- selectedFlamegraphState.upid,
+ selection.startNs,
+ selection.endNs,
+ selectedFlamegraphState.upids,
selectedFlamegraphState.type,
selectedFlamegraphState.focusRegex);
if (flamegraphData !== undefined && selection &&
selection.kind === selectedFlamegraphState.kind &&
- selection.id === selectedFlamegraphState.id &&
- selection.ts === selectedFlamegraphState.ts) {
+ selection.startNs === selectedFlamegraphState.startNs &&
+ selection.endNs === selectedFlamegraphState.endNs) {
const expandedFlamegraphData =
expandCallsites(flamegraphData, expandedId);
this.prepareAndMergeCallsites(
expandedFlamegraphData,
this.lastSelectedFlamegraphState.viewingOption,
+ hasAreaChanged,
rootSize,
this.lastSelectedFlamegraphState.expandedCallsite);
}
@@ -160,10 +205,11 @@
private shouldRequestData(selection: FlamegraphState) {
return selection.kind === 'FLAMEGRAPH_STATE' &&
(this.lastSelectedFlamegraphState === undefined ||
- (this.lastSelectedFlamegraphState.id !== selection.id ||
- this.lastSelectedFlamegraphState.ts !== selection.ts ||
+ (this.lastSelectedFlamegraphState.startNs !== selection.startNs ||
+ this.lastSelectedFlamegraphState.endNs !== selection.endNs ||
this.lastSelectedFlamegraphState.type !== selection.type ||
- this.lastSelectedFlamegraphState.upid !== selection.upid ||
+ !FlamegraphController.areArraysEqual(
+ this.lastSelectedFlamegraphState.upids, selection.upids) ||
this.lastSelectedFlamegraphState.viewingOption !==
selection.viewingOption ||
this.lastSelectedFlamegraphState.focusRegex !==
@@ -175,17 +221,20 @@
private prepareAndMergeCallsites(
flamegraphData: CallsiteInfo[],
viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
- rootSize?: number, expandedCallsite?: CallsiteInfo) {
+ hasAreaChanged: boolean, rootSize?: number,
+ expandedCallsite?: CallsiteInfo) {
this.flamegraphDetails.flamegraph = mergeCallsites(
flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
this.flamegraphDetails.expandedCallsite = expandedCallsite;
this.flamegraphDetails.viewingOption = viewingOption;
+ this.flamegraphDetails.isInAreaSelection = hasAreaChanged;
publishFlamegraphDetails(this.flamegraphDetails);
}
async getFlamegraphData(
- baseKey: string, viewingOption: string, ts: number, upid: number,
- type: string, focusRegex: string): Promise<CallsiteInfo[]> {
+ baseKey: string, viewingOption: string, startNs: number, endNs: number,
+ upids: number[], type: string,
+ focusRegex: string): Promise<CallsiteInfo[]> {
let currentData: CallsiteInfo[];
const key = `${baseKey}-${viewingOption}`;
if (this.flamegraphDatasets.has(key)) {
@@ -196,8 +245,8 @@
// Collecting data for drawing flamegraph for selected profile.
// Data needs to be in following format:
// id, name, parent_id, depth, total_size
- const tableName =
- await this.prepareViewsAndTables(ts, upid, type, focusRegex);
+ const tableName = await this.prepareViewsAndTables(
+ startNs, endNs, upids, type, focusRegex);
currentData = await this.getFlamegraphDataFromTables(
tableName, viewingOption, focusRegex);
this.flamegraphDatasets.set(key, currentData);
@@ -329,34 +378,42 @@
}
private async prepareViewsAndTables(
- ts: number, upid: number, type: string,
+ startNs: number, endNs: number, upids: number[], type: string,
focusRegex: string): Promise<string> {
// Creating unique names for views so we can reuse and not delete them
// for each marker.
let focusRegexConditional = '';
if (focusRegex !== '') {
- const linkingWord = type === 'perf' ? 'and' : 'where';
- focusRegexConditional = `${linkingWord} focus_str = '${focusRegex}'`;
+ focusRegexConditional = `and focus_str = '${focusRegex}'`;
}
/*
* TODO(octaviant) this branching should be eliminated for simplicity.
*/
if (type === 'perf') {
+ let upidConditional = `upid = ${upids[0]}`;
+ if (upids.length > 1) {
+ upidConditional =
+ `upid_group = '${FlamegraphController.serializeUpidGroup(upids)}'`;
+ }
return this.cache.getTableName(
`select id, name, map_name, parent_id, depth, cumulative_size,
cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
size, alloc_size, count, alloc_count, source_file, line_number
from experimental_flamegraph
- where profile_type = "${type}" and ts <= ${ts} and upid = ${upid}
+ where profile_type = '${type}' and ${startNs} <= ts and ts <= ${
+ endNs} and ${upidConditional}
${focusRegexConditional}`);
}
return this.cache.getTableName(
`select id, name, map_name, parent_id, depth, cumulative_size,
cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
size, alloc_size, count, alloc_count, source_file, line_number
- from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
- focusRegexConditional}`);
+ from experimental_flamegraph
+ where profile_type = '${type}'
+ and ts = ${endNs}
+ and upid = ${upids[0]}
+ ${focusRegexConditional}`);
}
getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
@@ -371,20 +428,44 @@
return MIN_PIXEL_DISPLAYED * rootSize / width;
}
- async getFlamegraphMetadata(type: string, ts: number, upid: number) {
+ async getFlamegraphMetadata(
+ type: string, startNs: number, endNs: number, upids: number[]) {
// Don't do anything if selection of the marker stayed the same.
if ((this.lastSelectedFlamegraphState !== undefined &&
- ((this.lastSelectedFlamegraphState.ts === ts &&
- this.lastSelectedFlamegraphState.upid === upid)))) {
+ ((this.lastSelectedFlamegraphState.startNs === startNs &&
+ this.lastSelectedFlamegraphState.endNs === endNs &&
+ FlamegraphController.areArraysEqual(
+ this.lastSelectedFlamegraphState.upids, upids))))) {
return undefined;
}
// Collecting data for more information about profile, such as:
// total memory allocated, memory that is allocated and not freed.
+ const upidGroup = FlamegraphController.serializeUpidGroup(upids);
+
const result = await this.args.engine.query(
- `select pid from process where upid = ${upid}`);
- const pid = result.firstRow({pid: NUM}).pid;
- const startTime = fromNs(ts) - globals.state.traceTime.startSec;
- return {ts: startTime, tsNs: ts, pid, upid, type};
+ `select pid from process where upid in (${upidGroup})`);
+ const it = result.iter({pid: NUM});
+ const pids = [];
+ for (let i = 0; it.valid(); ++i, it.next()) {
+ pids.push(it.pid);
+ }
+ return {startNs, durNs: endNs - startNs, pids, upids, type};
+ }
+
+ private static areArraysEqual(a: number[], b: number[]) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static serializeUpidGroup(upids: number[]) {
+ return new Array(upids).join();
}
}
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 9f95ab3..5a98bd1 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -247,6 +247,13 @@
})
});
}
+ if (globals.flamegraphDetails.isInAreaSelection) {
+ detailsPanels.push({
+ key: 'flamegraph_selection',
+ name: 'Flamegraph Selection',
+ vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'})
+ });
+ }
break;
case 'SLICE':
detailsPanels.push({
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index c9817e0..35a4d9f 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -14,7 +14,7 @@
import * as m from 'mithril';
-import {assertExists} from '../base/logging';
+import {assertExists, assertTrue} from '../base/logging';
import {Actions} from '../common/actions';
import {
ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
@@ -74,7 +74,7 @@
export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> {
private profileType?: ProfileType = undefined;
private ts = 0;
- private pid = 0;
+ private pids: number[] = [];
private flamegraph: Flamegraph = new Flamegraph([]);
private focusRegex = '';
private updateFocusRegexDebounced = debounce(() => {
@@ -84,13 +84,13 @@
view() {
const flamegraphDetails = globals.flamegraphDetails;
if (flamegraphDetails && flamegraphDetails.type !== undefined &&
- flamegraphDetails.ts !== undefined &&
- flamegraphDetails.tsNs !== undefined &&
- flamegraphDetails.pid !== undefined &&
- flamegraphDetails.upid !== undefined) {
+ flamegraphDetails.startNs !== undefined &&
+ flamegraphDetails.durNs !== undefined &&
+ flamegraphDetails.pids !== undefined &&
+ flamegraphDetails.upids !== undefined) {
this.profileType = toProfileType(flamegraphDetails.type);
- this.ts = flamegraphDetails.tsNs;
- this.pid = flamegraphDetails.pid;
+ this.ts = flamegraphDetails.durNs;
+ this.pids = flamegraphDetails.pids;
if (flamegraphDetails.flamegraph) {
this.flamegraph.updateDataIfChanged(
this.nodeRendering(), flamegraphDetails.flamegraph);
@@ -134,7 +134,7 @@
toSelectedCallsite(
flamegraphDetails.expandedCallsite)}`),
m('div.time',
- `Snapshot time: ${timeToCode(flamegraphDetails.ts)}`),
+ `Snapshot time: ${timeToCode(flamegraphDetails.durNs)}`),
m('input[type=text][placeholder=Focus]', {
oninput: (e: Event) => {
const target = (e.target as HTMLInputElement);
@@ -216,7 +216,10 @@
if (!engine) return;
getCurrentTrace()
.then(file => {
- convertTraceToPprofAndDownload(file, this.pid, this.ts);
+ assertTrue(
+ this.pids.length === 1,
+ 'Native profiles can only contain one pid.');
+ convertTraceToPprofAndDownload(file, this.pids[0], this.ts);
})
.catch(error => {
throw new Error(`Failed to get current trace ${error}`);
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index c854da6..f730b1d 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -40,6 +40,7 @@
type PivotTableHelperStore = Map<string, PivotTableHelper>;
type AggregateDataStore = Map<string, AggregateData>;
type Description = Map<string, string>;
+
export interface SliceDetails {
ts?: number;
dur?: number;
@@ -104,14 +105,17 @@
export interface FlamegraphDetails {
type?: string;
id?: number;
- ts?: number;
- tsNs?: number;
- pid?: number;
- upid?: number;
+ startNs?: number;
+ durNs?: number;
+ pids?: number[];
+ upids?: number[];
flamegraph?: CallsiteInfo[];
expandedCallsite?: CallsiteInfo;
viewingOption?: string;
expandedId?: number;
+ // isInAreaSelection is true if a flamegraph is part of the current area
+ // selection.
+ isInAreaSelection?: boolean;
}
export interface CpuProfileDetails {
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index eb2d5fe..1edcc76 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -144,6 +144,7 @@
export function publishQueryResult(args: {id: string, data?: {}}) {
globals.queryResults.set(args.id, args.data);
+ globals.dispatch(Actions.setCurrentTab({tab: 'query_result'}));
globals.publishRedraw();
}
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index f319614..5cd1e9b 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -197,7 +197,7 @@
const yOffsetPx = textMetrics.fontBoundingBoxAscent +
textMetrics.fontBoundingBoxDescent +
text2Metrics.fontBoundingBoxAscent;
- ctx.fillText(text, x + paddingPx, y + paddingPx + yOffsetPx);
+ ctx.fillText(text2, x + paddingPx, y + paddingPx + yOffsetPx);
}
}