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;