Merge "Java heap stats and histogram"
diff --git a/gn/perfetto.gni b/gn/perfetto.gni
index 3d27339..b2fe750 100644
--- a/gn/perfetto.gni
+++ b/gn/perfetto.gni
@@ -258,6 +258,9 @@
# Allows to build the UI (TypeScript/ HTML / WASM)
enable_perfetto_ui =
perfetto_build_standalone && enable_perfetto_trace_processor_sqlite
+
+ # Skip buildtools dependency checks (needed for ChromeOS).
+ skip_buildtools_check = false
}
# +---------------------------------------------------------------------------+
diff --git a/gn/perfetto_check_build_deps.gni b/gn/perfetto_check_build_deps.gni
index 481a6c3..a2e5a03 100644
--- a/gn/perfetto_check_build_deps.gni
+++ b/gn/perfetto_check_build_deps.gni
@@ -18,7 +18,7 @@
# tools/install-build-deps --check-only ${args}.
# It's used to ensure that deps are current before building.
template("perfetto_check_build_deps") {
- if (perfetto_build_standalone) {
+ if (perfetto_build_standalone && !skip_buildtools_check) {
action(target_name) {
out_file = "$target_gen_dir/$target_name.check"
out_file_path = rebase_path(out_file, root_build_dir)
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index a2ea1d1..aca3208 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -3411,6 +3411,10 @@
// Individual performance sampling packet payload. Typically corresponds to a
// stack sample on a configration-dependent counter overflow.
// Timestamps are within the root packet (in the CLOCK_BOOTTIME domain).
+// There are three distinct views of this message:
+// * completely processed sample (callstack_iid set)
+// * indication of kernel buffer data loss (kernel_records_lost set)
+// * indication of skipped samples (sample_skipped_reason set)
message PerfSample {
optional uint32 cpu = 1;
optional uint32 pid = 2;
@@ -3424,10 +3428,8 @@
optional uint64 callstack_iid = 4;
// If set, stack unwinding was incomplete due to an error.
- // Unset values should be treated as UNWIND_ERROR_NONE. Decoder is responsible
- // for correctly handling the "unknown on-the-wire values are decoded as the
- // first enum value" caveat.
- optional Profiling.StackUnwindError unwind_error = 16;
+ // Unset values should be treated as UNWIND_ERROR_NONE.
+ oneof optional_unwind_error { Profiling.StackUnwindError unwind_error = 16; };
// If set, indicates that this message is not a sample, but rather an
// indication of data loss in the ring buffer allocated for |cpu|. Such data
@@ -3442,6 +3444,18 @@
// specific time bounds of that loss (which would require tracking precedessor
// & successor timestamps, which is not deemed necessary at the moment).
optional uint64 kernel_records_lost = 17;
+
+ // If set, indicates that the profiler encountered a sample that was relevant,
+ // but was skipped as it was considered to be not unwindable (e.g. the process
+ // no longer exists).
+ enum ProfilerStage {
+ PROFILER_STAGE_UNKNOWN = 0;
+ PROFILER_STAGE_READ = 1;
+ PROFILER_STAGE_UNWIND = 2;
+ }
+ oneof optional_sample_skipped_reason {
+ ProfilerStage sample_skipped_reason = 18;
+ };
}
// End of protos/perfetto/trace/profiling/profile_packet.proto
diff --git a/protos/perfetto/trace/profiling/profile_packet.proto b/protos/perfetto/trace/profiling/profile_packet.proto
index c1cac95..000b550 100644
--- a/protos/perfetto/trace/profiling/profile_packet.proto
+++ b/protos/perfetto/trace/profiling/profile_packet.proto
@@ -176,6 +176,10 @@
// Individual performance sampling packet payload. Typically corresponds to a
// stack sample on a configration-dependent counter overflow.
// Timestamps are within the root packet (in the CLOCK_BOOTTIME domain).
+// There are three distinct views of this message:
+// * completely processed sample (callstack_iid set)
+// * indication of kernel buffer data loss (kernel_records_lost set)
+// * indication of skipped samples (sample_skipped_reason set)
message PerfSample {
optional uint32 cpu = 1;
optional uint32 pid = 2;
@@ -189,10 +193,8 @@
optional uint64 callstack_iid = 4;
// If set, stack unwinding was incomplete due to an error.
- // Unset values should be treated as UNWIND_ERROR_NONE. Decoder is responsible
- // for correctly handling the "unknown on-the-wire values are decoded as the
- // first enum value" caveat.
- optional Profiling.StackUnwindError unwind_error = 16;
+ // Unset values should be treated as UNWIND_ERROR_NONE.
+ oneof optional_unwind_error { Profiling.StackUnwindError unwind_error = 16; };
// If set, indicates that this message is not a sample, but rather an
// indication of data loss in the ring buffer allocated for |cpu|. Such data
@@ -207,4 +209,16 @@
// specific time bounds of that loss (which would require tracking precedessor
// & successor timestamps, which is not deemed necessary at the moment).
optional uint64 kernel_records_lost = 17;
+
+ // If set, indicates that the profiler encountered a sample that was relevant,
+ // but was skipped as it was considered to be not unwindable (e.g. the process
+ // no longer exists).
+ enum ProfilerStage {
+ PROFILER_STAGE_UNKNOWN = 0;
+ PROFILER_STAGE_READ = 1;
+ PROFILER_STAGE_UNWIND = 2;
+ }
+ oneof optional_sample_skipped_reason {
+ ProfilerStage sample_skipped_reason = 18;
+ };
}
\ No newline at end of file
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index c6fd0db..fba6fe4 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -51,6 +51,7 @@
// TODO(rsavitski): this is better calculated (at setup) from the buffer and
// sample sizes.
constexpr size_t kMaxSamplesPerCpuPerReadTick = 32;
+constexpr uint32_t kProcDescriptorTimeoutMs = 200;
constexpr size_t kUnwindingMaxFrames = 1000;
@@ -220,10 +221,11 @@
InitiateReaderStop(&ds);
}
-// TODO(rsavitski): ignoring flushes for now, as it is involved given
-// out-of-order unwinding and proc-fd timeouts. Instead of responding to
-// explicit flushes, we can ensure that we're otherwise well-behaved (do not
-// reorder packets too much).
+// The perf data sources ignore flush requests, as flushing would be
+// unnecessarily complicated given out-of-order unwinding and proc-fd timeouts.
+// Instead of responding to explicit flushes, we can ensure that we're otherwise
+// well-behaved (do not reorder packets too much), and let the service scrape
+// the SMB.
void PerfProducer::Flush(FlushRequestID flush_id,
const DataSourceInstanceID* data_source_ids,
size_t num_data_sources) {
@@ -316,13 +318,14 @@
PERFETTO_DLOG("New pid: [%d]", static_cast<int>(pid));
fd_entry.status = Status::kResolving;
proc_fd_getter_->GetDescriptorsForPid(pid); // response is async
- PostDescriptorLookupTimeout(ds_id, pid, /*timeout_ms=*/1000);
+ PostDescriptorLookupTimeout(ds_id, pid, kProcDescriptorTimeoutMs);
}
- // TODO(rsavitski): consider recording skipped entries in the trace.
if (fd_entry.status == Status::kSkip) {
PERFETTO_DLOG("Skipping sample for previously poisoned pid [%d]",
static_cast<int>(pid));
+ PostEmitSkippedSample(ds_id, ProfilerStage::kRead,
+ std::move(sample.value()));
continue;
}
@@ -461,6 +464,8 @@
if (fd_status == Status::kSkip) {
PERFETTO_DLOG("Skipping sample for pid [%d]",
static_cast<int>(sample.pid));
+ PostEmitSkippedSample(ds_id, ProfilerStage::kUnwind,
+ std::move(entry.sample));
entry.valid = false;
continue;
}
@@ -608,8 +613,8 @@
perf_sample->set_cpu(sample.cpu);
perf_sample->set_pid(static_cast<uint32_t>(sample.pid));
perf_sample->set_tid(static_cast<uint32_t>(sample.tid));
- perf_sample->set_callstack_iid(callstack_iid);
perf_sample->set_cpu_mode(ToCpuModeEnum(sample.cpu_mode));
+ perf_sample->set_callstack_iid(callstack_iid);
if (sample.unwind_error != unwindstack::ERROR_NONE) {
perf_sample->set_unwind_error(ToProtoEnum(sample.unwind_error));
}
@@ -642,6 +647,49 @@
perf_sample->set_kernel_records_lost(records_lost);
}
+void PerfProducer::PostEmitSkippedSample(DataSourceInstanceID ds_id,
+ ProfilerStage stage,
+ ParsedSample sample) {
+ // hack: c++11 lambdas can't be moved into, so stash the sample on the heap.
+ ParsedSample* raw_sample = new ParsedSample(std::move(sample));
+ auto weak_this = weak_factory_.GetWeakPtr();
+ task_runner_->PostTask([weak_this, ds_id, stage, raw_sample] {
+ if (weak_this)
+ weak_this->EmitSkippedSample(ds_id, stage, std::move(*raw_sample));
+ delete raw_sample;
+ });
+}
+
+void PerfProducer::EmitSkippedSample(DataSourceInstanceID ds_id,
+ ProfilerStage stage,
+ ParsedSample sample) {
+ auto ds_it = data_sources_.find(ds_id);
+ if (ds_it == data_sources_.end()) {
+ PERFETTO_DLOG("EmitSkippedSample(%zu): source gone",
+ static_cast<size_t>(ds_id));
+ return;
+ }
+ DataSource& ds = ds_it->second;
+
+ auto packet = ds.trace_writer->NewTracePacket();
+ packet->set_timestamp(sample.timestamp);
+ auto* perf_sample = packet->set_perf_sample();
+ perf_sample->set_cpu(sample.cpu);
+ perf_sample->set_pid(static_cast<uint32_t>(sample.pid));
+ perf_sample->set_tid(static_cast<uint32_t>(sample.tid));
+ perf_sample->set_cpu_mode(ToCpuModeEnum(sample.cpu_mode));
+
+ using PerfSample = protos::pbzero::PerfSample;
+ switch (stage) {
+ case ProfilerStage::kRead:
+ perf_sample->set_sample_skipped_reason(PerfSample::PROFILER_STAGE_READ);
+ break;
+ case ProfilerStage::kUnwind:
+ perf_sample->set_sample_skipped_reason(PerfSample::PROFILER_STAGE_UNWIND);
+ break;
+ }
+}
+
void PerfProducer::InitiateReaderStop(DataSource* ds) {
PERFETTO_DLOG("InitiateReaderStop");
ds->reader_stopping = true;
@@ -701,8 +749,9 @@
void PerfProducer::ConnectService() {
PERFETTO_DCHECK(state_ == kNotConnected);
state_ = kConnecting;
- endpoint_ = ProducerIPCClient::Connect(producer_socket_name_, this,
- kProducerName, task_runner_);
+ endpoint_ = ProducerIPCClient::Connect(
+ producer_socket_name_, this, kProducerName, task_runner_,
+ TracingService::ProducerSMBScrapingMode::kEnabled);
}
void PerfProducer::IncreaseConnectionBackoff() {
diff --git a/src/profiling/perf/perf_producer.h b/src/profiling/perf/perf_producer.h
index 1b127c7..5472178 100644
--- a/src/profiling/perf/perf_producer.h
+++ b/src/profiling/perf/perf_producer.h
@@ -152,6 +152,11 @@
unwindstack::ErrorCode unwind_error = unwindstack::ERROR_NONE;
};
+ enum class ProfilerStage {
+ kRead = 0,
+ kUnwind,
+ };
+
void ConnectService();
void Restart();
void ResetConnectionBackoff();
@@ -187,6 +192,14 @@
void EmitRingBufferLoss(DataSourceInstanceID ds_id,
size_t cpu,
uint64_t records_lost);
+ // Emit a packet indicating that a sample was relevant, but skipped as it was
+ // considered to be not unwindable (e.g. the process no longer exists).
+ void PostEmitSkippedSample(DataSourceInstanceID ds_id,
+ ProfilerStage stage,
+ ParsedSample sample);
+ void EmitSkippedSample(DataSourceInstanceID ds_id,
+ ProfilerStage stage,
+ ParsedSample sample);
// Starts the shutdown of the given data source instance, starting with the
// reader frontend.
diff --git a/src/trace_processor/importers/proto/proto_trace_parser.cc b/src/trace_processor/importers/proto/proto_trace_parser.cc
index a049043..bfbc0b7 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser.cc
@@ -333,9 +333,11 @@
int64_t ts,
PacketSequenceStateGeneration* sequence_state,
ConstBytes blob) {
- protos::pbzero::PerfSample::Decoder sample(blob.data, blob.size);
+ using PerfSample = protos::pbzero::PerfSample;
+ PerfSample::Decoder sample(blob.data, blob.size);
- // Not a sample, but an indication of data loss.
+ // Not a sample, but an indication of data loss in the ring buffer shared with
+ // the kernel.
if (sample.kernel_records_lost() > 0) {
PERFETTO_DCHECK(sample.pid() == 0);
@@ -345,6 +347,13 @@
return;
}
+ // Sample that wasn't unwound (likely because we failed to look up the
+ // proc-fds for it).
+ if (sample.has_sample_skipped_reason()) {
+ context_->storage->IncrementStats(stats::perf_samples_skipped);
+ return;
+ }
+
uint64_t callstack_iid = sample.callstack_iid();
StackProfileTracker& stack_tracker =
sequence_state->state()->stack_profile_tracker();
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 5c54c47..d2a562c 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -132,7 +132,8 @@
F(compact_sched_waking_skipped, kSingle, kInfo, kAnalysis), \
F(empty_chrome_metadata, kSingle, kError, kTrace), \
F(perf_cpu_lost_records, kIndexed, kDataLoss, kTrace), \
- F(ninja_parse_errors, kSingle, kError, kTrace)
+ F(ninja_parse_errors, kSingle, kError, kTrace), \
+ F(perf_samples_skipped, kSingle, kInfo, kTrace)
// clang-format on
enum Type {
diff --git a/src/traced/probes/ftrace/format_parser.cc b/src/traced/probes/ftrace/format_parser.cc
index d4e119f..2be687d 100644
--- a/src/traced/probes/ftrace/format_parser.cc
+++ b/src/traced/probes/ftrace/format_parser.cc
@@ -72,8 +72,9 @@
if (IsCommonFieldName(GetNameFromTypeAndName(type_and_name))) {
if (common_fields)
common_fields->push_back(field);
- } else if (fields)
+ } else if (fields) {
fields->push_back(field);
+ }
continue;
}
diff --git a/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc b/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
index 8dd848f..b002581 100644
--- a/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs_integrationtest.cc
@@ -30,6 +30,16 @@
using testing::Not;
using testing::UnorderedElementsAre;
+// These tests run only on Android because on linux they require access to
+// ftrace, which would be problematic in the CI when multiple tests run
+// concurrently on the same machine. Android instead uses one emulator instance
+// for each worker.
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
+#define ANDROID_ONLY_TEST(x) x
+#else
+#define ANDROID_ONLY_TEST(x) DISABLED_##x
+#endif
+
namespace perfetto {
namespace {
@@ -41,12 +51,6 @@
return std::string(FtraceController::kTracingPaths[i]);
}
-void ResetFtrace(FtraceProcfs* ftrace) {
- ftrace->DisableAllEvents();
- ftrace->ClearTrace();
- ftrace->EnableTracing();
-}
-
std::string ReadFile(const std::string& name) {
std::string result;
PERFETTO_CHECK(base::ReadFile(GetFtracePath() + name, &result));
@@ -61,156 +65,106 @@
return output;
}
-} // namespace
+class FtraceProcfsIntegrationTest : public testing::Test {
+ public:
+ void SetUp() override;
+ void TearDown() override;
-// TODO(lalitm): reenable these tests (see b/72306171).
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_CreateWithGoodPath CreateWithGoodPath
-#else
-#define MAYBE_CreateWithGoodPath DISABLED_CreateWithGoodPath
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_CreateWithGoodPath) {
- EXPECT_TRUE(FtraceProcfs::Create(GetFtracePath()));
+ std::unique_ptr<FtraceProcfs> ftrace_;
+};
+
+void FtraceProcfsIntegrationTest::SetUp() {
+ ftrace_ = FtraceProcfs::Create(GetFtracePath());
+ ASSERT_TRUE(ftrace_);
+ if (ftrace_->IsTracingEnabled()) {
+ GTEST_SKIP() << "Something else is using ftrace, skipping";
+ }
+
+ ftrace_->DisableAllEvents();
+ ftrace_->ClearTrace();
+ ftrace_->EnableTracing();
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_CreateWithBadPath CreateWithBadPath
-#else
-#define MAYBE_CreateWithBadPath DISABLED_CreateWithBadath
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_CreateWithBadPath) {
+void FtraceProcfsIntegrationTest::TearDown() {
+ ftrace_->DisableAllEvents();
+ ftrace_->ClearTrace();
+ ftrace_->DisableTracing();
+}
+
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(CreateWithBadPath)) {
EXPECT_FALSE(FtraceProcfs::Create(GetFtracePath() + std::string("bad_path")));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_ClearTrace ClearTrace
-#else
-#define MAYBE_ClearTrace DISABLED_ClearTrace
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_ClearTrace) {
- FtraceProcfs ftrace(GetFtracePath());
- ResetFtrace(&ftrace);
- ftrace.WriteTraceMarker("Hello, World!");
- ftrace.ClearTrace();
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(ClearTrace)) {
+ ftrace_->WriteTraceMarker("Hello, World!");
+ ftrace_->ClearTrace();
EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("Hello, World!")));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_TraceMarker TraceMarker
-#else
-#define MAYBE_TraceMarker DISABLED_TraceMarker
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_TraceMarker) {
- FtraceProcfs ftrace(GetFtracePath());
- ResetFtrace(&ftrace);
- ftrace.WriteTraceMarker("Hello, World!");
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(TraceMarker)) {
+ ftrace_->WriteTraceMarker("Hello, World!");
EXPECT_THAT(GetTraceOutput(), HasSubstr("Hello, World!"));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_EnableDisableEvent EnableDisableEvent
-#else
-#define MAYBE_EnableDisableEvent DISABLED_EnableDisableEvent
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_EnableDisableEvent) {
- FtraceProcfs ftrace(GetFtracePath());
- ResetFtrace(&ftrace);
- ftrace.EnableEvent("sched", "sched_switch");
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(EnableDisableEvent)) {
+ ftrace_->EnableEvent("sched", "sched_switch");
sleep(1);
EXPECT_THAT(GetTraceOutput(), HasSubstr("sched_switch"));
- ftrace.DisableEvent("sched", "sched_switch");
- ftrace.ClearTrace();
+ ftrace_->DisableEvent("sched", "sched_switch");
+ ftrace_->ClearTrace();
sleep(1);
EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("sched_switch")));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_EnableDisableTracing EnableDisableTracing
-#else
-#define MAYBE_EnableDisableTracing DISABLED_EnableDisableTracing
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_EnableDisableTracing) {
- FtraceProcfs ftrace(GetFtracePath());
- ResetFtrace(&ftrace);
- EXPECT_TRUE(ftrace.IsTracingEnabled());
- ftrace.WriteTraceMarker("Before");
- ftrace.DisableTracing();
- EXPECT_FALSE(ftrace.IsTracingEnabled());
- ftrace.WriteTraceMarker("During");
- ftrace.EnableTracing();
- EXPECT_TRUE(ftrace.IsTracingEnabled());
- ftrace.WriteTraceMarker("After");
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(EnableDisableTracing)) {
+ EXPECT_TRUE(ftrace_->IsTracingEnabled());
+ ftrace_->WriteTraceMarker("Before");
+ ftrace_->DisableTracing();
+ EXPECT_FALSE(ftrace_->IsTracingEnabled());
+ ftrace_->WriteTraceMarker("During");
+ ftrace_->EnableTracing();
+ EXPECT_TRUE(ftrace_->IsTracingEnabled());
+ ftrace_->WriteTraceMarker("After");
EXPECT_THAT(GetTraceOutput(), HasSubstr("Before"));
EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("During")));
EXPECT_THAT(GetTraceOutput(), HasSubstr("After"));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_ReadFormatFile ReadFormatFile
-#else
-#define MAYBE_ReadFormatFile DISABLED_ReadFormatFile
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_ReadFormatFile) {
- FtraceProcfs ftrace(GetFtracePath());
- std::string format = ftrace.ReadEventFormat("ftrace", "print");
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(ReadFormatFile)) {
+ std::string format = ftrace_->ReadEventFormat("ftrace", "print");
EXPECT_THAT(format, HasSubstr("name: print"));
EXPECT_THAT(format, HasSubstr("field:char buf"));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_CanOpenTracePipeRaw CanOpenTracePipeRaw
-#else
-#define MAYBE_CanOpenTracePipeRaw DISABLED_CanOpenTracePipeRaw
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_CanOpenTracePipeRaw) {
- FtraceProcfs ftrace(GetFtracePath());
- EXPECT_TRUE(ftrace.OpenPipeForCpu(0));
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(CanOpenTracePipeRaw)) {
+ EXPECT_TRUE(ftrace_->OpenPipeForCpu(0));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_Clock Clock
-#else
-#define MAYBE_Clock DISABLED_Clock
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_Clock) {
- FtraceProcfs ftrace(GetFtracePath());
- std::set<std::string> clocks = ftrace.AvailableClocks();
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(Clock)) {
+ std::set<std::string> clocks = ftrace_->AvailableClocks();
EXPECT_THAT(clocks, Contains("local"));
EXPECT_THAT(clocks, Contains("global"));
- EXPECT_TRUE(ftrace.SetClock("global"));
- EXPECT_EQ(ftrace.GetClock(), "global");
- EXPECT_TRUE(ftrace.SetClock("local"));
- EXPECT_EQ(ftrace.GetClock(), "local");
+ EXPECT_TRUE(ftrace_->SetClock("global"));
+ EXPECT_EQ(ftrace_->GetClock(), "global");
+ EXPECT_TRUE(ftrace_->SetClock("local"));
+ EXPECT_EQ(ftrace_->GetClock(), "local");
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_CanSetBufferSize CanSetBufferSize
-#else
-#define MAYBE_CanSetBufferSize DISABLED_CanSetBufferSize
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_CanSetBufferSize) {
- FtraceProcfs ftrace(GetFtracePath());
- EXPECT_TRUE(ftrace.SetCpuBufferSizeInPages(4ul));
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(CanSetBufferSize)) {
+ EXPECT_TRUE(ftrace_->SetCpuBufferSizeInPages(4ul));
EXPECT_EQ(ReadFile("buffer_size_kb"), "16\n"); // (4096 * 4) / 1024
- EXPECT_TRUE(ftrace.SetCpuBufferSizeInPages(5ul));
+ EXPECT_TRUE(ftrace_->SetCpuBufferSizeInPages(5ul));
EXPECT_EQ(ReadFile("buffer_size_kb"), "20\n"); // (4096 * 5) / 1024
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_FtraceControllerHardReset FtraceControllerHardReset
-#else
-#define MAYBE_FtraceControllerHardReset DISABLED_FtraceControllerHardReset
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_FtraceControllerHardReset) {
- FtraceProcfs ftrace(GetFtracePath());
- ResetFtrace(&ftrace);
-
- ftrace.SetCpuBufferSizeInPages(4ul);
- ftrace.EnableTracing();
- ftrace.EnableEvent("sched", "sched_switch");
- ftrace.WriteTraceMarker("Hello, World!");
+TEST_F(FtraceProcfsIntegrationTest,
+ ANDROID_ONLY_TEST(FtraceControllerHardReset)) {
+ ftrace_->SetCpuBufferSizeInPages(4ul);
+ ftrace_->EnableTracing();
+ ftrace_->EnableEvent("sched", "sched_switch");
+ ftrace_->WriteTraceMarker("Hello, World!");
EXPECT_EQ(ReadFile("buffer_size_kb"), "16\n");
EXPECT_EQ(ReadFile("tracing_on"), "1\n");
@@ -225,27 +179,20 @@
EXPECT_THAT(GetTraceOutput(), Not(HasSubstr("Hello")));
}
-#if PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
-#define MAYBE_ReadEnabledEvents ReadEnabledEvents
-#else
-#define MAYBE_ReadEnabledEvents DISABLED_ReadEnabledEvents
-#endif
-TEST(FtraceProcfsIntegrationTest, MAYBE_ReadEnabledEvents) {
- FtraceProcfs ftrace(GetFtracePath());
- ResetFtrace(&ftrace);
+TEST_F(FtraceProcfsIntegrationTest, ANDROID_ONLY_TEST(ReadEnabledEvents)) {
+ EXPECT_THAT(ftrace_->ReadEnabledEvents(), IsEmpty());
- EXPECT_THAT(ftrace.ReadEnabledEvents(), IsEmpty());
+ ftrace_->EnableEvent("sched", "sched_switch");
+ ftrace_->EnableEvent("kmem", "kmalloc");
- ftrace.EnableEvent("sched", "sched_switch");
- ftrace.EnableEvent("kmem", "kmalloc");
-
- EXPECT_THAT(ftrace.ReadEnabledEvents(),
+ EXPECT_THAT(ftrace_->ReadEnabledEvents(),
UnorderedElementsAre("sched/sched_switch", "kmem/kmalloc"));
- ftrace.DisableEvent("sched", "sched_switch");
- ftrace.DisableEvent("kmem", "kmalloc");
+ ftrace_->DisableEvent("sched", "sched_switch");
+ ftrace_->DisableEvent("kmem", "kmalloc");
- EXPECT_THAT(ftrace.ReadEnabledEvents(), IsEmpty());
+ EXPECT_THAT(ftrace_->ReadEnabledEvents(), IsEmpty());
}
+} // namespace
} // namespace perfetto
diff --git a/test/cts/traced_perf_test_cts.cc b/test/cts/traced_perf_test_cts.cc
index d6745fc..e0cef1f 100644
--- a/test/cts/traced_perf_test_cts.cc
+++ b/test/cts/traced_perf_test_cts.cc
@@ -15,6 +15,7 @@
*/
#include <stdlib.h>
+#include <sys/system_properties.h>
#include <sys/types.h>
#include "perfetto/base/logging.h"
@@ -32,6 +33,16 @@
namespace perfetto {
namespace {
+// Skip these tests if the device in question doesn't have the necessary kernel
+// LSM hooks in perf_event_open. This comes up when a device with an older
+// kernel upgrades to R.
+bool HasPerfLsmHooks() {
+ char buf[PROP_VALUE_MAX + 1] = {};
+ int ret = __system_property_get("sys.init.perf_lsm_hooks", buf);
+ PERFETTO_CHECK(ret >= 0);
+ return std::string(buf) == "1";
+}
+
std::vector<protos::gen::TracePacket> ProfileSystemWide(std::string app_name) {
base::TestTaskRunner task_runner;
@@ -51,8 +62,9 @@
helper.WaitForConsumerConnect();
TraceConfig trace_config;
- trace_config.add_buffers()->set_size_kb(10 * 1024);
- trace_config.set_duration_ms(2000);
+ trace_config.add_buffers()->set_size_kb(20 * 1024);
+ trace_config.set_duration_ms(3000);
+ trace_config.set_data_source_stop_timeout_ms(8000);
auto* ds_config = trace_config.add_data_sources()->mutable_config();
ds_config->set_name("linux.perf");
@@ -66,7 +78,7 @@
// start tracing
helper.StartTracing(trace_config);
- helper.WaitForTracingDisabled(10000 /*ms*/);
+ helper.WaitForTracingDisabled(15000 /*ms*/);
helper.ReadData();
helper.WaitForReadData();
@@ -77,39 +89,50 @@
int target_pid) {
ASSERT_GT(packets.size(), 0u);
- int samples_found = 0;
+ int total_perf_packets = 0;
+ int total_samples = 0;
+ int target_samples = 0;
for (const auto& packet : packets) {
if (!packet.has_perf_sample())
continue;
- EXPECT_GT(packet.timestamp(), 0u) << "all samples should have a timestamp";
+ total_perf_packets++;
+ EXPECT_GT(packet.timestamp(), 0u) << "all packets should have a timestamp";
const auto& sample = packet.perf_sample();
- if (sample.pid() != static_cast<uint32_t>(target_pid))
- continue;
-
- // TODO(rsavitski): include |sample.has_sample_skipped_reason| once that is
- // merged.
if (sample.has_kernel_records_lost())
continue;
+ if (sample.has_sample_skipped_reason())
+ continue;
- // A full sample
+ total_samples++;
EXPECT_GT(sample.tid(), 0u);
EXPECT_GT(sample.callstack_iid(), 0u);
- samples_found += 1;
+
+ if (sample.pid() == static_cast<uint32_t>(target_pid))
+ target_samples++;
}
- EXPECT_GT(samples_found, 0);
+
+ EXPECT_GT(target_samples, 0) << "packets.size(): " << packets.size()
+ << ", total_perf_packets: " << total_perf_packets
+ << ", total_samples: " << total_samples << "\n";
}
void AssertNoStacksForPid(std::vector<protos::gen::TracePacket> packets,
int target_pid) {
+ // The process can still be sampled, but the stacks should be discarded
+ // without unwinding.
for (const auto& packet : packets) {
if (packet.perf_sample().pid() == static_cast<uint32_t>(target_pid)) {
EXPECT_EQ(packet.perf_sample().callstack_iid(), 0u);
+ EXPECT_TRUE(packet.perf_sample().has_sample_skipped_reason());
}
}
}
TEST(TracedPerfCtsTest, SystemWideDebuggableApp) {
+ if (!HasPerfLsmHooks())
+ return;
+
std::string app_name = "android.perfetto.cts.app.debuggable";
const auto& packets = ProfileSystemWide(app_name);
int app_pid = PidForProcessName(app_name);
@@ -120,6 +143,9 @@
}
TEST(TracedPerfCtsTest, SystemWideProfileableApp) {
+ if (!HasPerfLsmHooks())
+ return;
+
std::string app_name = "android.perfetto.cts.app.profileable";
const auto& packets = ProfileSystemWide(app_name);
int app_pid = PidForProcessName(app_name);
@@ -130,6 +156,9 @@
}
TEST(TracedPerfCtsTest, SystemWideReleaseApp) {
+ if (!HasPerfLsmHooks())
+ return;
+
std::string app_name = "android.perfetto.cts.app.release";
const auto& packets = ProfileSystemWide(app_name);
int app_pid = PidForProcessName(app_name);
diff --git a/test/cts/utils.cc b/test/cts/utils.cc
index c61eff7..79e5d4d 100644
--- a/test/cts/utils.cc
+++ b/test/cts/utils.cc
@@ -54,7 +54,7 @@
// note: cannot use gtest macros due to return type
bool IsAppRunning(const std::string& name) {
- std::string cmd = "pgrep -f " + name;
+ std::string cmd = "pgrep -f ^" + name + "$";
int retcode = system(cmd.c_str());
PERFETTO_CHECK(retcode >= 0);
int exit_status = WEXITSTATUS(retcode);
@@ -66,9 +66,7 @@
}
int PidForProcessName(const std::string& name) {
- // quirk: need to exclude ourselves from the result as the pgrep's cmdline
- // matches itself when invoked via popen.
- std::string cmd = "pgrep -f " + name + " | grep -v $$";
+ std::string cmd = "pgrep -f ^" + name + "$";
FILE* fp = popen(cmd.c_str(), "re");
if (!fp)
return -1;
diff --git a/tools/dev_server b/tools/dev_server
index bd74df7..fb836b8 100755
--- a/tools/dev_server
+++ b/tools/dev_server
@@ -68,6 +68,7 @@
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
+ self.wfile.write("<pre>")
self.wfile.write(e.stdout_and_stderr)
return
return SimpleHTTPRequestHandler.do_GET(self)
diff --git a/ui/index.html b/ui/index.html
index a893d30..5a9190f 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -2,8 +2,6 @@
<html lang="en-us">
<head>
<title>Perfetto UI</title>
- <!-- See b/149573396 for CSP rationale -->
- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; connect-src 'self' http://127.0.0.1:9001 https://*.googleapis.com; navigate-to https://*.perfetto.dev;">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<!-- WebComponents V0 origin trial token for https://ui.perfetto.dev Expires 17 Dec 2020.
See https://crbug.com/1021137. -->
diff --git a/ui/src/common/aggregation_data.ts b/ui/src/common/aggregation_data.ts
index 3823150..f4f3d74 100644
--- a/ui/src/common/aggregation_data.ts
+++ b/ui/src/common/aggregation_data.ts
@@ -12,12 +12,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-export interface AggregateCpuData {
- strings: string[];
- procNameId: Uint16Array;
- pid: Uint32Array;
- threadNameId: Uint16Array;
- tid: Uint32Array;
- totalDur: Float64Array;
- occurrences: Uint16Array;
+export type Column =
+ (StringColumn|TimestampColumn|NumberColumn)&{title: string};
+
+export interface StringColumn {
+ kind: 'STRING';
+ data: Uint16Array;
}
+
+export interface TimestampColumn {
+ kind: 'TIMESTAMP_NS';
+ data: Float64Array;
+}
+
+export interface NumberColumn {
+ kind: 'NUMBER';
+ data: Uint16Array;
+}
+
+export interface AggregateData {
+ columns: Column[];
+ // For string interning.
+ strings: string[];
+}
\ No newline at end of file
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
new file mode 100644
index 0000000..5c41231
--- /dev/null
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -0,0 +1,70 @@
+// Copyright (C) 2019 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 {AggregateData} from '../../common/aggregation_data';
+
+import {Engine} from '../../common/engine';
+import {TimestampedAreaSelection} from '../../common/state';
+
+import {Controller} from '../controller';
+import {globals} from '../globals';
+
+export interface AggregationControllerArgs {
+ engine: Engine;
+ kind: string;
+}
+
+export abstract class AggregationController extends Controller<'main'> {
+ private previousArea: TimestampedAreaSelection = {lastUpdate: 0};
+ private requestingData = false;
+ private queuedRequest = false;
+
+ // Must be overridden by the aggregation implementation. It is invoked
+ // whenever the selected area is changed and returns data to be published.
+ abstract async onAreaSelectionChange(
+ engine: Engine, area: TimestampedAreaSelection): Promise<AggregateData>;
+
+ constructor(private args: AggregationControllerArgs) {
+ super('main');
+ }
+
+ run() {
+ const selectedArea = globals.state.frontendLocalState.selectedArea;
+ if (this.previousArea &&
+ this.previousArea.lastUpdate >= selectedArea.lastUpdate) {
+ return;
+ }
+ if (this.requestingData) {
+ this.queuedRequest = true;
+ } else {
+ this.requestingData = true;
+ Object.assign(this.previousArea, selectedArea);
+ this.onAreaSelectionChange(this.args.engine, selectedArea)
+ .then(
+ data => globals.publish(
+ 'AggregateData', {data, kind: this.args.kind}))
+ .catch(reason => {
+ console.error(reason);
+ })
+ .finally(() => {
+ this.requestingData = false;
+ if (this.queuedRequest) {
+ this.queuedRequest = false;
+ this.run();
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
new file mode 100644
index 0000000..6ca135a
--- /dev/null
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -0,0 +1,94 @@
+// Copyright (C) 2020 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 {AggregateData} from '../../common/aggregation_data';
+import {Engine} from '../../common/engine';
+import {TimestampedAreaSelection} from '../../common/state';
+import {toNs} from '../../common/time';
+import {AggregationController} from './aggregation_controller';
+
+export class CpuAggregationController extends AggregationController {
+ async onAreaSelectionChange(
+ engine: Engine, selectedArea: TimestampedAreaSelection) {
+ const area = selectedArea.area;
+ if (area === undefined) {
+ return {columns: [], strings: []};
+ }
+
+ const cpusInTrace = await engine.getCpus();
+ const selectedCpuTracks =
+ cpusInTrace.filter(x => area.tracks.includes((x + 1).toString()));
+
+ const query =
+ `SELECT process.name, pid, thread.name, tid, sum(dur) AS total_dur,
+ sum(dur)/count(1) as avg_dur,
+ count(1) as occurences
+ FROM process
+ JOIN thread USING(upid)
+ JOIN thread_state USING(utid)
+ WHERE cpu IN (${selectedCpuTracks}) AND
+ state = "Running" AND
+ thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
+ thread_state.ts < ${toNs(area.endSec)}
+ GROUP BY utid ORDER BY total_dur DESC`;
+
+ const result = await engine.query(query);
+
+ const numRows = +result.numRecords;
+ const aggregateData: AggregateData = {
+ columns: [
+ {title: 'Process', kind: 'STRING', data: new Uint16Array(numRows)},
+ {title: 'PID', kind: 'NUMBER', data: new Uint16Array(numRows)},
+ {title: 'Thread', kind: 'STRING', data: new Uint16Array(numRows)},
+ {title: 'TID', kind: 'NUMBER', data: new Uint16Array(numRows)},
+ {
+ title: 'Wall duration (ms)',
+ kind: 'TIMESTAMP_NS',
+ data: new Float64Array(numRows)
+ },
+ {
+ title: 'Avg Wall duration (ms)',
+ kind: 'TIMESTAMP_NS',
+ data: new Float64Array(numRows)
+ },
+ {title: 'Occurrences', kind: 'NUMBER', data: new Uint16Array(numRows)}
+ ],
+ strings: [],
+ };
+
+ const stringIndexes = new Map<string, number>();
+ function internString(str: string) {
+ let idx = stringIndexes.get(str);
+ if (idx !== undefined) return idx;
+ idx = aggregateData.strings.length;
+ aggregateData.strings.push(str);
+ stringIndexes.set(str, idx);
+ return idx;
+ }
+
+ for (let row = 0; row < numRows; row++) {
+ const cols = result.columns;
+ aggregateData.columns[0].data[row] =
+ internString(cols[0].stringValues![row]);
+ aggregateData.columns[1].data[row] = cols[1].longValues![row] as number;
+ aggregateData.columns[2].data[row] =
+ internString(cols[2].stringValues![row]);
+ aggregateData.columns[3].data[row] = cols[3].longValues![row] as number;
+ aggregateData.columns[4].data[row] = cols[4].longValues![row] as number;
+ aggregateData.columns[5].data[row] = cols[5].longValues![row] as number;
+ aggregateData.columns[6].data[row] = cols[6].longValues![row] as number;
+ }
+ return aggregateData;
+ }
+}
diff --git a/ui/src/controller/aggregation/thread_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
new file mode 100644
index 0000000..f522b8c
--- /dev/null
+++ b/ui/src/controller/aggregation/thread_aggregation_controller.ts
@@ -0,0 +1,110 @@
+// Copyright (C) 2020 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 {AggregateData} from '../../common/aggregation_data';
+import {Engine} from '../../common/engine';
+import {TimestampedAreaSelection} from '../../common/state';
+import {translateState} from '../../common/thread_state';
+import {toNs} from '../../common/time';
+import {
+ Config,
+ THREAD_STATE_TRACK_KIND
+} from '../../tracks/thread_state/common';
+import {globals} from '../globals';
+
+import {AggregationController} from './aggregation_controller';
+
+
+export class ThreadAggregationController extends AggregationController {
+ async onAreaSelectionChange(
+ engine: Engine, selectedArea: TimestampedAreaSelection) {
+ const area = selectedArea.area;
+ if (area === undefined) {
+ return {columns: [], strings: []};
+ }
+ // TODO(taylori): Thread state tracks should have a real track id in the
+ // trace processor.
+ const utids = [];
+ for (const trackId of area.tracks) {
+ const track = globals.state.tracks[trackId];
+ if (track.kind === THREAD_STATE_TRACK_KIND) {
+ utids.push((track.config as Config).utid);
+ }
+ }
+
+ const query = `SELECT process.name, pid, thread.name, tid,
+ state,
+ sum(dur) AS total_dur,
+ sum(dur)/count(1) as avg_dur,
+ count(1) as occurences
+ FROM process
+ JOIN thread USING(upid)
+ JOIN thread_state USING(utid)
+ WHERE utid IN (${utids}) AND
+ thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
+ thread_state.ts < ${toNs(area.endSec)}
+ GROUP BY utid, state ORDER BY total_dur DESC`;
+
+ const result = await engine.query(query);
+
+ const numRows = +result.numRecords;
+ const aggregateData: AggregateData = {
+ columns: [
+ {title: 'Process', kind: 'STRING', data: new Uint16Array(numRows)},
+ {title: 'PID', kind: 'NUMBER', data: new Uint16Array(numRows)},
+ {title: 'Thread', kind: 'STRING', data: new Uint16Array(numRows)},
+ {title: 'TID', kind: 'NUMBER', data: new Uint16Array(numRows)},
+ {title: 'State', kind: 'STRING', data: new Uint16Array(numRows)},
+ {
+ title: 'Wall duration (ms)',
+ kind: 'TIMESTAMP_NS',
+ data: new Float64Array(numRows)
+ },
+ {
+ title: 'Avg Wall duration (ms)',
+ kind: 'TIMESTAMP_NS',
+ data: new Float64Array(numRows)
+ },
+ {title: 'Occurrences', kind: 'NUMBER', data: new Uint16Array(numRows)}
+ ],
+ strings: [],
+ };
+
+ const stringIndexes = new Map<string, number>();
+ function internString(str: string) {
+ let idx = stringIndexes.get(str);
+ if (idx !== undefined) return idx;
+ idx = aggregateData.strings.length;
+ aggregateData.strings.push(str);
+ stringIndexes.set(str, idx);
+ return idx;
+ }
+
+ for (let row = 0; row < numRows; row++) {
+ const cols = result.columns;
+ aggregateData.columns[0].data[row] =
+ internString(cols[0].stringValues![row]);
+ aggregateData.columns[1].data[row] = cols[1].longValues![row] as number;
+ aggregateData.columns[2].data[row] =
+ internString(cols[2].stringValues![row]);
+ aggregateData.columns[3].data[row] = cols[3].longValues![row] as number;
+ aggregateData.columns[4].data[row] =
+ internString(translateState(cols[4].stringValues![row]));
+ aggregateData.columns[5].data[row] = cols[5].longValues![row] as number;
+ aggregateData.columns[6].data[row] = cols[6].longValues![row] as number;
+ aggregateData.columns[7].data[row] = cols[7].longValues![row] as number;
+ }
+ return aggregateData;
+ }
+}
diff --git a/ui/src/controller/aggregation_controller.ts b/ui/src/controller/aggregation_controller.ts
deleted file mode 100644
index cdbb44f..0000000
--- a/ui/src/controller/aggregation_controller.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright (C) 2019 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 {AggregateCpuData} from '../common/aggregation_data';
-import {Engine} from '../common/engine';
-import {TimestampedAreaSelection} from '../common/state';
-import {toNs} from '../common/time';
-
-import {Controller} from './controller';
-import {globals} from './globals';
-
-export interface AggregationControllerArgs {
- engine: Engine;
-}
-
-export class AggregationController extends Controller<'main'> {
- private previousArea: TimestampedAreaSelection = {lastUpdate: 0};
- private requestingData = false;
- private queuedRequest = false;
- constructor(private args: AggregationControllerArgs) {
- super('main');
- }
-
- run() {
- const selectedArea = globals.state.frontendLocalState.selectedArea;
- const area = selectedArea.area;
- if (!area ||
- this.previousArea &&
- this.previousArea.lastUpdate >= selectedArea.lastUpdate) {
- return;
- }
- if (this.requestingData) {
- this.queuedRequest = true;
- } else {
- this.requestingData = true;
- Object.assign(this.previousArea, selectedArea);
-
- this.args.engine.getCpus().then(cpusInTrace => {
- const selectedCpuTracks =
- cpusInTrace.filter(x => area.tracks.includes((x + 1).toString()));
-
- const query =
- `SELECT process.name, pid, thread.name, tid, sum(dur) AS total_dur,
- count(1)
- FROM process
- JOIN thread USING(upid)
- JOIN thread_state USING(utid)
- WHERE cpu IN (${selectedCpuTracks}) AND
- state = "Running" AND
- thread_state.ts + thread_state.dur > ${toNs(area.startSec)} AND
- thread_state.ts < ${toNs(area.endSec)}
- GROUP BY utid ORDER BY total_dur DESC`;
-
- this.args.engine.query(query)
- .then(result => {
- if (globals.state.frontendLocalState.selectedArea.lastUpdate >
- selectedArea.lastUpdate) {
- return;
- }
-
- const numRows = +result.numRecords;
- const data: AggregateCpuData = {
- strings: [],
- procNameId: new Uint16Array(numRows),
- pid: new Uint32Array(numRows),
- threadNameId: new Uint16Array(numRows),
- tid: new Uint32Array(numRows),
- totalDur: new Float64Array(numRows),
- occurrences: new Uint16Array(numRows)
- };
-
- const stringIndexes = new Map<string, number>();
- function internString(str: string) {
- let idx = stringIndexes.get(str);
- if (idx !== undefined) return idx;
- idx = data.strings.length;
- data.strings.push(str);
- stringIndexes.set(str, idx);
- return idx;
- }
-
- for (let row = 0; row < numRows; row++) {
- const cols = result.columns;
- data.procNameId[row] = internString(cols[0].stringValues![row]);
- data.pid[row] = cols[1].longValues![row] as number;
- data.threadNameId[row] =
- internString(cols[2].stringValues![row]);
- data.tid[row] = cols[3].longValues![row] as number;
- data.totalDur[row] = cols[4].longValues![row] as number;
- data.occurrences[row] = cols[5].longValues![row] as number;
- }
- globals.publish('AggregateCpuData', data);
- })
- .catch(reason => {
- console.error(reason);
- })
- .finally(() => {
- this.requestingData = false;
- if (this.queuedRequest) {
- this.queuedRequest = false;
- this.run();
- }
- });
- });
- }
- }
-}
\ No newline at end of file
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index e9d3652..7e99ab6 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -23,7 +23,7 @@
type PublishKinds = 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|
'LegacyTrace'|'SliceDetails'|'CounterDetails'|'HeapProfileDetails'|
'HeapProfileFlamegraph'|'FileDownload'|'Loading'|'Search'|'BufferUsage'|
- 'RecordingLog'|'SearchResult'|'AggregateCpuData';
+ 'RecordingLog'|'SearchResult'|'AggregateData';
export interface App {
state: State;
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 327a411..5f112d7 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -49,7 +49,12 @@
import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary/common';
import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state/common';
-import {AggregationController} from './aggregation_controller';
+import {
+ CpuAggregationController
+} from './aggregation/cpu_aggregation_controller';
+import {
+ ThreadAggregationController
+} from './aggregation/thread_aggregation_controller';
import {Child, Children, Controller} from './controller';
import {globals} from './globals';
import {
@@ -154,8 +159,14 @@
const heapProfileArgs: HeapProfileControllerArgs = {engine};
childControllers.push(
Child('heapProfile', HeapProfileController, heapProfileArgs));
- childControllers.push(
- Child('aggregation', AggregationController, {engine}));
+ childControllers.push(Child(
+ 'cpu_aggregation',
+ CpuAggregationController,
+ {engine, kind: 'cpu'}));
+ childControllers.push(Child(
+ 'thread_aggregation',
+ ThreadAggregationController,
+ {engine, kind: 'thread_state'}));
childControllers.push(Child('search', SearchController, {
engine,
app: globals,
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index 67f7f8f..ba2347d 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -14,52 +14,50 @@
import * as m from 'mithril';
-import {AggregateCpuData} from '../common/aggregation_data';
-
-import {globals} from './globals';
+import {AggregateData} from '../common/aggregation_data';
import {Panel} from './panel';
-export class AggregationPanel extends Panel {
- view() {
- const data = globals.aggregateCpuData;
+export interface AggregationPanelAttrs {
+ data: AggregateData;
+}
+
+export class AggregationPanel extends Panel<AggregationPanelAttrs> {
+ view({attrs}: m.CVnode<AggregationPanelAttrs>) {
return m(
'.details-panel',
m('.details-panel-heading.aggregation',
m('table',
- m('tr',
- m('th', 'Process'),
- m('th', 'Thread'),
- m('th', 'Wall duration (ms)'),
- m('th', 'Avg. Wall duration (ms)'),
- m('th', 'Occurrences')))),
+ m('tr', attrs.data.columns.map(col => (m('th', col.title)))))),
m(
'.details-table.aggregation',
- m('table', this.getRows(data)),
+ m('table', this.getRows(attrs.data)),
));
}
- getRows(data: AggregateCpuData) {
- if (!data.strings || !data.procNameId || !data.threadNameId || !data.pid ||
- !data.tid || !data.totalDur || !data.occurrences) {
- return;
- }
+ getRows(data: AggregateData) {
+ if (data.columns.length === 0) return;
const rows = [];
- for (let i = 0; i < data.pid.length; i++) {
- const row =
- [m('tr',
- m('td', `${data.strings[data.procNameId[i]]} [${data.pid[i]}]`),
- m('td', `${data.strings[data.threadNameId[i]]} [${data.tid[i]}]`),
- m('td', `${data.totalDur[i] / 1000000}`),
- m('td',
- `${
- +
- (data.totalDur[i] / data.occurrences[i] / 1000000)
- .toFixed(6)}`),
- m('td', `${data.occurrences[i]}`))];
- rows.push(row);
+ for (let i = 0; i < data.columns[0].data.length; i++) {
+ const row = [];
+ for (let j = 0; j < data.columns.length; j++) {
+ row.push(m('td', this.getFormattedData(data, i, j)));
+ }
+ rows.push(m('tr', row));
}
return rows;
}
+ getFormattedData(data: AggregateData, rowIndex: number, columnIndex: number) {
+ switch (data.columns[columnIndex].kind) {
+ case 'STRING':
+ return `${data.strings[data.columns[columnIndex].data[rowIndex]]}`;
+ case 'TIMESTAMP_NS':
+ return `${data.columns[columnIndex].data[rowIndex] / 1000000}`;
+ case 'NUMBER':
+ default:
+ return `${data.columns[columnIndex].data[rowIndex]}`;
+ }
+ }
+
renderCanvas() {}
-}
+}
\ No newline at end of file
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 0e47ffe..8501d73 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -51,11 +51,9 @@
interface DragHandleAttrs {
height: number;
resize: (height: number) => void;
- tabs: Tab[];
+ tabs: string[];
}
-export type Tab = 'current_selection'|'cpu_slices'|'android_logs';
-
class DragHandle implements m.ClassComponent<DragHandleAttrs> {
private dragStartHeight = 0;
private height = 0;
@@ -65,10 +63,11 @@
private isFullscreen = false;
// We can't get real fullscreen height until the pan_and_zoom_handler exists.
private fullscreenHeight = DEFAULT_DETAILS_HEIGHT_PX;
- private tabNames = new Map<Tab, string>([
+ private tabNames = new Map<string, string>([
['current_selection', 'Current Selection'],
- ['cpu_slices', 'CPU Slices'],
- ['android_logs', 'Android Logs']
+ ['cpu', 'CPU Slices'],
+ ['android_logs', 'Android Logs'],
+ ['thread_state', 'Thread States']
]);
@@ -109,11 +108,14 @@
view({attrs}: m.CVnode<DragHandleAttrs>) {
const icon = this.isClosed ? UP_ICON : DOWN_ICON;
const title = this.isClosed ? 'Show panel' : 'Hide panel';
- const renderTab = (key: Tab) => {
+ const renderTab = (key: string) => {
if (globals.frontendLocalState.currentTab === key ||
globals.frontendLocalState.currentTab === undefined &&
attrs.tabs[0] === key) {
- return m('.tab[active]', this.tabNames.get(key));
+ return m(
+ '.tab[active]',
+ this.tabNames.get(key) === undefined ? key :
+ this.tabNames.get(key));
}
return m(
'.tab',
@@ -167,7 +169,7 @@
private showDetailsPanel = true;
view() {
- const detailsPanels: Map<Tab, AnyAttrsVnode> = new Map();
+ const detailsPanels: Map<string, AnyAttrsVnode> = new Map();
const curSelection = globals.state.currentSelection;
if (curSelection) {
switch (curSelection.kind) {
@@ -213,8 +215,10 @@
detailsPanels.set('android_logs', m(LogPanel, {}));
}
- if (globals.frontendLocalState.selectedArea.area !== undefined) {
- detailsPanels.set('cpu_slices', m(AggregationPanel));
+ for (const [key, value] of globals.aggregateDataStore.entries()) {
+ if (value.columns.length > 0 && value.columns[0].data.length > 0) {
+ detailsPanels.set(key, m(AggregationPanel, {data: value}));
+ }
}
const wasShowing = this.showDetailsPanel;
@@ -224,7 +228,8 @@
this.detailsHeight = DEFAULT_DETAILS_HEIGHT_PX;
}
- const panel = globals.frontendLocalState.currentTab ?
+ const panel = globals.frontendLocalState.currentTab &&
+ detailsPanels.has(globals.frontendLocalState.currentTab) ?
detailsPanels.get(globals.frontendLocalState.currentTab) :
detailsPanels.values().next().value;
const panels = panel ? [panel] : [];
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index a966a45..f66b500 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -24,7 +24,6 @@
import {TimeSpan} from '../common/time';
import {randomColor} from './colorizer';
-import {Tab} from './details_panel';
import {globals} from './globals';
import {TimeScale} from './time_scale';
@@ -103,7 +102,7 @@
visibleTracks = new Set<string>();
prevVisibleTracks = new Set<string>();
searchIndex = -1;
- currentTab?: Tab;
+ currentTab?: string;
scrollToTrackId?: string|number;
httpRpcState: HttpRpcState = {connected: false};
newVersionAvailable = false;
@@ -234,7 +233,6 @@
lastUpdate: Date.now() / 1000
};
this.selectAreaDebounced();
- globals.frontendLocalState.currentTab = 'cpu_slices';
globals.rafScheduler.scheduleFullRedraw();
}
@@ -284,7 +282,6 @@
}));
}
- globals.frontendLocalState.currentTab = 'cpu_slices';
globals.rafScheduler.scheduleFullRedraw();
}
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index d865749..4ac3260 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -14,7 +14,7 @@
import {assertExists} from '../base/logging';
import {DeferredAction} from '../common/actions';
-import {AggregateCpuData} from '../common/aggregation_data';
+import {AggregateData} from '../common/aggregation_data';
import {CurrentSearchResults, SearchSummary} from '../common/search_data';
import {CallsiteInfo, createEmptyState, State} from '../common/state';
@@ -25,6 +25,7 @@
type Dispatch = (action: DeferredAction) => void;
type TrackDataStore = Map<string, {}>;
type QueryResultsStore = Map<string, {}>;
+type AggregateDataStore = Map<string, AggregateData>;
type Args = Map<string, string>;
export interface SliceDetails {
ts?: number;
@@ -94,6 +95,7 @@
private _trackDataStore?: TrackDataStore = undefined;
private _queryResults?: QueryResultsStore = undefined;
private _overviewStore?: OverviewStore = undefined;
+ private _aggregateDataStore?: AggregateDataStore = undefined;
private _threadMap?: ThreadMap = undefined;
private _sliceDetails?: SliceDetails = undefined;
private _counterDetails?: CounterDetails = undefined;
@@ -101,15 +103,7 @@
private _numQueriesQueued = 0;
private _bufferUsage?: number = undefined;
private _recordingLog?: string = undefined;
- private _aggregateCpuData: AggregateCpuData = {
- strings: [],
- procNameId: new Uint16Array(0),
- pid: new Uint32Array(0),
- threadNameId: new Uint16Array(0),
- tid: new Uint32Array(0),
- totalDur: new Float64Array(0),
- occurrences: new Uint16Array(0)
- };
+
private _currentSearchResults: CurrentSearchResults = {
sliceIds: new Float64Array(0),
tsStarts: new Float64Array(0),
@@ -136,6 +130,7 @@
this._trackDataStore = new Map<string, {}>();
this._queryResults = new Map<string, {}>();
this._overviewStore = new Map<string, QuantizedLoad[]>();
+ this._aggregateDataStore = new Map<string, AggregateData>();
this._threadMap = new Map<number, ThreadDesc>();
this._sliceDetails = {};
this._counterDetails = {};
@@ -199,12 +194,8 @@
this._counterDetails = assertExists(click);
}
- get aggregateCpuData(): AggregateCpuData {
- return assertExists(this._aggregateCpuData);
- }
-
- set aggregateCpuData(value: AggregateCpuData) {
- this._aggregateCpuData = value;
+ get aggregateDataStore(): AggregateDataStore {
+ return assertExists(this._aggregateDataStore);
}
get heapProfileDetails() {
@@ -251,6 +242,10 @@
this._recordingLog = recordingLog;
}
+ setAggregateData(kind: string, data: AggregateData) {
+ this.aggregateDataStore.set(kind, data);
+ }
+
getCurResolution() {
// Truncate the resolution to the closest power of 2.
// This effectively means the resolution changes every 6 zoom levels.
@@ -279,6 +274,7 @@
this._overviewStore = undefined;
this._threadMap = undefined;
this._sliceDetails = undefined;
+ this._aggregateDataStore = undefined;
this._numQueriesQueued = 0;
this._currentSearchResults = {
sliceIds: new Float64Array(0),
@@ -288,15 +284,6 @@
sources: [],
totalResults: 0,
};
- this._aggregateCpuData = {
- strings: [],
- procNameId: new Uint16Array(0),
- pid: new Uint32Array(0),
- threadNameId: new Uint16Array(0),
- tid: new Uint32Array(0),
- totalDur: new Float64Array(0),
- occurrences: new Uint16Array(0)
- };
}
// Used when switching to the legacy TraceViewer UI.
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index e1d5c64..7823f28 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -21,7 +21,7 @@
import {assertExists, reportError, setErrorHandler} from '../base/logging';
import {forwardRemoteCalls} from '../base/remote';
import {Actions} from '../common/actions';
-import {AggregateCpuData} from '../common/aggregation_data';
+import {AggregateData} from '../common/aggregation_data';
import {
LogBoundsKey,
LogEntriesKey,
@@ -175,8 +175,8 @@
this.redraw();
}
- publishAggregateCpuData(args: AggregateCpuData) {
- globals.aggregateCpuData = args;
+ publishAggregateData(args: {data: AggregateData, kind: string}) {
+ globals.setAggregateData(args.kind, args.data);
this.redraw();
}
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 9ba8637..3a252fb 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -110,10 +110,10 @@
this.prevAreaSelection.lastUpdate >= selection.lastUpdate) ||
area === undefined ||
globals.frontendLocalState.areaY.start === undefined ||
- globals.frontendLocalState.areaY.end === undefined) {
+ globals.frontendLocalState.areaY.end === undefined ||
+ this.panelPositions.length === 0) {
return;
}
-
// Only get panels from the current panel container if the selection began
// in this container.
const panelContainerTop = this.panelPositions[0].y;