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