Merge "trace writer: Reset last_packet_size_field_ on flush."
diff --git a/Android.bp b/Android.bp
index 4c7c85c..f89a890 100644
--- a/Android.bp
+++ b/Android.bp
@@ -828,6 +828,7 @@
     "test/cts/end_to_end_integrationtest_cts.cc",
     "test/cts/heapprofd_java_test_cts.cc",
     "test/cts/heapprofd_test_cts.cc",
+    "test/cts/traced_perf_test_cts.cc",
     "test/cts/utils.cc",
   ],
   static_libs: [
@@ -835,6 +836,9 @@
     "libgtest",
     "libperfetto_client_experimental",
   ],
+  whole_static_libs: [
+    "perfetto_gtest_logcat_printer",
+  ],
   export_include_dirs: [
     "include",
     "include/perfetto/base/build_configs/android_tree",
@@ -1209,6 +1213,25 @@
   },
 }
 
+// GN: //test:perfetto_gtest_logcat_printer
+cc_library_static {
+  name: "perfetto_gtest_logcat_printer",
+  srcs: [
+    "test/gtest_logcat_printer.cc",
+  ],
+  static_libs: [
+    "libgmock",
+    "libgtest",
+  ],
+  export_include_dirs: [
+    "include",
+    "include/perfetto/base/build_configs/android_tree",
+  ],
+  defaults: [
+    "perfetto_defaults",
+  ],
+}
+
 // GN: //include/perfetto/base:base
 filegroup {
   name: "perfetto_include_perfetto_base_base",
@@ -1429,6 +1452,9 @@
     "libgtest",
     "libperfetto_client_experimental",
   ],
+  whole_static_libs: [
+    "perfetto_gtest_logcat_printer",
+  ],
   generated_headers: [
     "perfetto_protos_perfetto_common_cpp_gen_headers",
     "perfetto_protos_perfetto_common_zero_gen_headers",
@@ -6193,10 +6219,10 @@
 filegroup {
   name: "perfetto_src_trace_processor_lib",
   srcs: [
+    "src/trace_processor/experimental_counter_dur_generator.cc",
+    "src/trace_processor/experimental_flamegraph_generator.cc",
     "src/trace_processor/read_trace.cc",
     "src/trace_processor/sql_stats_table.cc",
-    "src/trace_processor/sqlite_experimental_counter_dur_table.cc",
-    "src/trace_processor/sqlite_experimental_flamegraph_table.cc",
     "src/trace_processor/sqlite_raw_table.cc",
     "src/trace_processor/stats_table.cc",
     "src/trace_processor/trace_processor.cc",
@@ -6360,6 +6386,7 @@
   srcs: [
     "src/trace_processor/clock_tracker_unittest.cc",
     "src/trace_processor/event_tracker_unittest.cc",
+    "src/trace_processor/experimental_counter_dur_generator_unittest.cc",
     "src/trace_processor/forwarding_trace_parser_unittest.cc",
     "src/trace_processor/ftrace_utils_unittest.cc",
     "src/trace_processor/heap_profile_tracker_unittest.cc",
@@ -6372,7 +6399,6 @@
     "src/trace_processor/process_tracker_unittest.cc",
     "src/trace_processor/protozero_to_text_unittests.cc",
     "src/trace_processor/slice_tracker_unittest.cc",
-    "src/trace_processor/sqlite_experimental_counter_dur_table_unittest.cc",
     "src/trace_processor/syscall_tracker_unittest.cc",
     "src/trace_processor/trace_sorter_unittest.cc",
   ],
@@ -7257,6 +7283,9 @@
     "libgmock",
     "libgtest",
   ],
+  whole_static_libs: [
+    "perfetto_gtest_logcat_printer",
+  ],
   generated_headers: [
     "gen_merged_sql_metrics",
     "perfetto_protos_perfetto_common_cpp_gen_headers",
@@ -7685,6 +7714,7 @@
     ":perfetto_src_protozero_protozero",
     ":perfetto_src_tracing_common",
     ":perfetto_src_tracing_core_core",
+    ":perfetto_src_tracing_core_service",
     ":perfetto_src_tracing_ipc_common",
     ":perfetto_src_tracing_ipc_producer_producer",
     "src/profiling/perf/main.cc",
diff --git a/BUILD b/BUILD
index 427b2c1..0f6e5b9 100644
--- a/BUILD
+++ b/BUILD
@@ -845,13 +845,13 @@
 filegroup(
     name = "src_trace_processor_lib",
     srcs = [
+        "src/trace_processor/experimental_counter_dur_generator.cc",
+        "src/trace_processor/experimental_counter_dur_generator.h",
+        "src/trace_processor/experimental_flamegraph_generator.cc",
+        "src/trace_processor/experimental_flamegraph_generator.h",
         "src/trace_processor/read_trace.cc",
         "src/trace_processor/sql_stats_table.cc",
         "src/trace_processor/sql_stats_table.h",
-        "src/trace_processor/sqlite_experimental_counter_dur_table.cc",
-        "src/trace_processor/sqlite_experimental_counter_dur_table.h",
-        "src/trace_processor/sqlite_experimental_flamegraph_table.cc",
-        "src/trace_processor/sqlite_experimental_flamegraph_table.h",
         "src/trace_processor/sqlite_raw_table.cc",
         "src/trace_processor/sqlite_raw_table.h",
         "src/trace_processor/stats_table.cc",
diff --git a/gn/perfetto_benchmarks.gni b/gn/perfetto_benchmarks.gni
index 2abd944..b70999a 100644
--- a/gn/perfetto_benchmarks.gni
+++ b/gn/perfetto_benchmarks.gni
@@ -17,11 +17,12 @@
 perfetto_benchmarks_targets = [
   "gn:default_deps",
   "src/base:benchmarks",
-  "src/traced/probes/ftrace:benchmarks",
   "src/trace_processor/containers:benchmarks",
   "src/trace_processor/tables:benchmarks",
-  "src/tracing/core:benchmarks",
   "src/traced/probes/ftrace/kallsyms:benchmarks",
+  "src/traced/probes/ftrace:benchmarks",
+  "src/tracing/core:benchmarks",
+  "src/tracing:benchmarks",
   "test:benchmark_main",
   "test:end_to_end_benchmarks",
 ]
diff --git a/include/perfetto/base/compiler.h b/include/perfetto/base/compiler.h
index d40fb5b..8b4ccda 100644
--- a/include/perfetto/base/compiler.h
+++ b/include/perfetto/base/compiler.h
@@ -51,7 +51,7 @@
 #define PERFETTO_PRINTF_FORMAT(x, y) \
   __attribute__((__format__(__printf__, x, y)))
 #else
-#defien PERFETTO_PRINTF_FORMAT(x, y)
+#define PERFETTO_PRINTF_FORMAT(x, y)
 #endif
 
 namespace perfetto {
diff --git a/include/perfetto/ext/base/metatrace_events.h b/include/perfetto/ext/base/metatrace_events.h
index e3cfb77..fd6a5dd 100644
--- a/include/perfetto/ext/base/metatrace_events.h
+++ b/include/perfetto/ext/base/metatrace_events.h
@@ -29,6 +29,7 @@
   TAG_PROC_POLLERS = 1 << 1,
   TAG_TRACE_WRITER = 1 << 2,
   TAG_TRACE_SERVICE = 1 << 3,
+  TAG_PRODUCER = 1 << 4,
 };
 
 // The macros below generate matching enums and arrays of string literals.
@@ -60,7 +61,11 @@
   F(FTRACE_READ_TICK), \
   F(FTRACE_CPU_READ_CYCLE), \
   F(FTRACE_CPU_READ_BATCH), \
-  F(KALLSYMS_PARSE)
+  F(KALLSYMS_PARSE), \
+  F(PROFILER_READ_TICK), \
+  F(PROFILER_READ_CPU), \
+  F(PROFILER_UNWIND_TICK), \
+  F(PROFILER_UNWIND_SAMPLE)
 
 // Append only, see above.
 //
@@ -71,7 +76,8 @@
   F(COUNTER_ZERO_UNUSED),\
   F(FTRACE_PAGES_DRAINED), \
   F(PS_PIDS_SCANNED), \
-  F(TRACE_SERVICE_COMMIT_DATA)
+  F(TRACE_SERVICE_COMMIT_DATA), \
+  F(PROFILER_UNWIND_QUEUE_SZ)
 
 // clang-format on
 
diff --git a/include/perfetto/ext/base/weak_ptr.h b/include/perfetto/ext/base/weak_ptr.h
index 7ba4ca3..778c4dd 100644
--- a/include/perfetto/ext/base/weak_ptr.h
+++ b/include/perfetto/ext/base/weak_ptr.h
@@ -90,9 +90,23 @@
 
   // Can be safely called on any thread, since it simply copies |weak_ptr_|.
   // Note that any accesses to the returned pointer need to be made on the
-  // thread that created the factory.
+  // thread that created/reset the factory.
   WeakPtr<T> GetWeakPtr() const { return weak_ptr_; }
 
+  // Reset the factory to a new owner & thread. May only be called before any
+  // weak pointers were passed out. Future weak pointers will be valid on the
+  // calling thread.
+  void Reset(T* owner) {
+    // Reset thread checker to current thread.
+    PERFETTO_DETACH_FROM_THREAD(thread_checker);
+    PERFETTO_DCHECK_THREAD(thread_checker);
+
+    // We should not have passed out any weak pointers yet at this point.
+    PERFETTO_DCHECK(weak_ptr_.handle_.use_count() == 1);
+
+    weak_ptr_ = WeakPtr<T>(std::shared_ptr<T*>(new T* {owner}));
+  }
+
  private:
   WeakPtrFactory(const WeakPtrFactory&) = delete;
   WeakPtrFactory& operator=(const WeakPtrFactory&) = delete;
diff --git a/include/perfetto/ext/tracing/core/trace_writer.h b/include/perfetto/ext/tracing/core/trace_writer.h
index 99662b2..9be44ff 100644
--- a/include/perfetto/ext/tracing/core/trace_writer.h
+++ b/include/perfetto/ext/tracing/core/trace_writer.h
@@ -79,7 +79,7 @@
   virtual WriterID writer_id() const = 0;
 
   // Bytes written since creation. Is not reset when new chunks are acquired.
-  virtual uint64_t written() const = 0;
+  virtual uint64_t written() const override = 0;
 
   // Set the id of the first chunk the writer will emit. Returns |false| if not
   // implemented or if the first chunk was already emitted by the writer.
diff --git a/include/perfetto/ext/tracing/ipc/producer_ipc_client.h b/include/perfetto/ext/tracing/ipc/producer_ipc_client.h
index 79b6ce6..5469ded 100644
--- a/include/perfetto/ext/tracing/ipc/producer_ipc_client.h
+++ b/include/perfetto/ext/tracing/ipc/producer_ipc_client.h
@@ -21,12 +21,13 @@
 #include <string>
 
 #include "perfetto/base/export.h"
+#include "perfetto/ext/tracing/core/shared_memory.h"
+#include "perfetto/ext/tracing/core/shared_memory_arbiter.h"
 #include "perfetto/ext/tracing/core/tracing_service.h"
 
 namespace perfetto {
 
 class Producer;
-class SharedMemory;
 
 // Allows to connect to a remote Service through a UNIX domain socket.
 // Exposed to:
@@ -42,9 +43,12 @@
   // ProducerEndpoint serves also to delimit the scope of the callbacks invoked
   // on the Producer interface: no more Producer callbacks are invoked
   // immediately after its destruction and any pending callback will be dropped.
-  // If |shm| is provided by the producer, the service will attempt to adopt the
-  // provided SMB. If it fails, the ProducerEndpoint will disconnect, but |shm|
-  // will remain valid until the client is destroyed.
+  // To provide a producer-allocated shared memory buffer, both |shm| and
+  // |shm_arbiter| should be set. |shm_arbiter| should be an unbound
+  // SharedMemoryArbiter instance. When |shm| and |shm_arbiter| are provided,
+  // the service will attempt to adopt the provided SMB. If this fails, the
+  // ProducerEndpoint will disconnect, but the SMB and arbiter will remain valid
+  // until the client is destroyed.
   //
   // TODO(eseckler): Support adoption failure more gracefully.
   static std::unique_ptr<TracingService::ProducerEndpoint> Connect(
@@ -56,7 +60,8 @@
           TracingService::ProducerSMBScrapingMode::kDefault,
       size_t shared_memory_size_hint_bytes = 0,
       size_t shared_memory_page_size_hint_bytes = 0,
-      std::unique_ptr<SharedMemory> shm = nullptr);
+      std::unique_ptr<SharedMemory> shm = nullptr,
+      std::unique_ptr<SharedMemoryArbiter> shm_arbiter = nullptr);
 
  protected:
   ProducerIPCClient() = delete;
diff --git a/include/perfetto/protozero/proto_decoder.h b/include/perfetto/protozero/proto_decoder.h
index b58e64e..8f98a62 100644
--- a/include/perfetto/protozero/proto_decoder.h
+++ b/include/perfetto/protozero/proto_decoder.h
@@ -40,7 +40,7 @@
 // (see proto_decoder_fuzzer.cc).
 // This class serves also as a building block for TypedProtoDecoder, used when
 // the schema is known at compile time.
-class ProtoDecoder {
+class PERFETTO_EXPORT ProtoDecoder {
  public:
   // Creates a ProtoDecoder using the given |buffer| with size |length| bytes.
   ProtoDecoder(const void* buffer, size_t length)
@@ -273,7 +273,7 @@
 // [ field 0 (invalid) ] [ fields 1 .. N ] [ repeated fields ]
 //                                        ^                  ^
 //                                        num_fields_        size_
-class TypedProtoDecoderBase : public ProtoDecoder {
+class PERFETTO_EXPORT TypedProtoDecoderBase : public ProtoDecoder {
  public:
   // If the field |id| is known at compile time, prefer the templated
   // specialization at<kFieldNumber>().
diff --git a/include/perfetto/tracing/data_source.h b/include/perfetto/tracing/data_source.h
index 1a2f57e..2e8dc4e 100644
--- a/include/perfetto/tracing/data_source.h
+++ b/include/perfetto/tracing/data_source.h
@@ -188,6 +188,11 @@
       tls_inst_->trace_writer->Flush(cb);
     }
 
+    // Returns the number of bytes written on the current thread by the current
+    // data-source since its creation.
+    // This can be useful for splitting protos that might grow very large.
+    uint64_t written() { return tls_inst_->trace_writer->written(); }
+
     // Returns a RAII handle to access the data source instance, guaranteeing
     // that it won't be deleted on another thread (because of trace stopping)
     // while accessing it from within the Trace() lambda.
diff --git a/include/perfetto/tracing/internal/track_event_data_source.h b/include/perfetto/tracing/internal/track_event_data_source.h
index 05ebd9d..bef0382 100644
--- a/include/perfetto/tracing/internal/track_event_data_source.h
+++ b/include/perfetto/tracing/internal/track_event_data_source.h
@@ -402,23 +402,6 @@
     }
   };
 
-  // Given either a static or dynamic category, extract a raw pointer to its
-  // underlying name. Note that the pointer is only valid as long as the
-  // parameter that was passed in.
-  static const char* GetCategoryName(size_t category_index,
-                                     const char* dynamic_category)
-      PERFETTO_ALWAYS_INLINE {
-    if (category_index == TrackEventCategoryRegistry::kDynamicCategoryIndex)
-      return dynamic_category;
-    return Registry->GetCategory(category_index)->name;
-  }
-
-  static const char* GetCategoryName(size_t,
-                                     const DynamicCategory& dynamic_category)
-      PERFETTO_ALWAYS_INLINE {
-    return dynamic_category.name.c_str();
-  }
-
   // TODO(skyostil): Make |CategoryIndex| a regular parameter to reuse trace
   // point code across different categories.
   template <size_t CategoryIndex,
@@ -441,22 +424,35 @@
     TraceWithInstances<CategoryIndex>(
         instances, [&](typename Base::TraceContext ctx) {
           // If this category is dynamic, first check whether it's enabled.
-          if (CategoryIndex ==
-                  TrackEventCategoryRegistry::kDynamicCategoryIndex &&
-              !IsDynamicCategoryEnabled(&ctx,
-                                        DynamicCategory{dynamic_category})) {
+          constexpr bool kIsDynamic =
+              CategoryIndex ==
+              TrackEventCategoryRegistry::kDynamicCategoryIndex;
+          if (kIsDynamic && !IsDynamicCategoryEnabled(
+                                &ctx, DynamicCategory{dynamic_category})) {
             return;
           }
+
           {
             // TODO(skyostil): Intern categories at compile time.
+            const Category* static_category =
+                kIsDynamic ? nullptr : Registry->GetCategory(CategoryIndex);
             auto event_ctx = TrackEventInternal::WriteEvent(
                 ctx.tls_inst_->trace_writer.get(), ctx.GetIncrementalState(),
-                GetCategoryName(CategoryIndex, dynamic_category), event_name,
-                type, timestamp);
+                static_category, event_name, type, timestamp);
+            if (kIsDynamic) {
+              Category category{
+                  Category::FromDynamicCategory(dynamic_category)};
+              category.ForEachGroupMember(
+                  [&](const char* member_name, size_t name_size) {
+                    event_ctx.event()->add_categories(member_name, name_size);
+                    return true;
+                  });
+            }
             if (track)
               event_ctx.event()->set_track_uuid(track.uuid);
             arg_function(std::move(event_ctx));
-          }
+          }  // event_ctx
+
           if (track) {
             TrackEventInternal::WriteTrackDescriptorIfNeeded(
                 track, ctx.tls_inst_->trace_writer.get(),
@@ -503,8 +499,9 @@
       // We haven't seen this category before. Let's figure out if it's enabled.
       // This requires grabbing a lock to read the session's trace config.
       auto ds = ctx->GetDataSourceLocked();
+      Category category{Category::FromDynamicCategory(dynamic_category)};
       bool enabled = TrackEventInternal::IsCategoryEnabled(
-          ds->config_, TrackEventCategory{dynamic_category.name.c_str()});
+          *Registry, ds->config_, category);
       // TODO(skyostil): Cap the size of |dynamic_categories|.
       incr_state->dynamic_categories[dynamic_category.name] = enabled;
       return enabled;
diff --git a/include/perfetto/tracing/internal/track_event_internal.h b/include/perfetto/tracing/internal/track_event_internal.h
index 09511d8..ed3d7cb 100644
--- a/include/perfetto/tracing/internal/track_event_internal.h
+++ b/include/perfetto/tracing/internal/track_event_internal.h
@@ -30,6 +30,7 @@
 
 namespace perfetto {
 class EventContext;
+struct Category;
 namespace protos {
 namespace gen {
 class TrackEventConfig;
@@ -40,7 +41,6 @@
 }  // namespace protos
 
 namespace internal {
-struct TrackEventCategory;
 class TrackEventCategoryRegistry;
 
 class BaseTrackEventInternedDataIndex {
@@ -102,13 +102,14 @@
                             uint32_t instance_index);
   static void DisableTracing(const TrackEventCategoryRegistry& registry,
                              uint32_t instance_index);
-  static bool IsCategoryEnabled(const protos::gen::TrackEventConfig& config,
-                                const TrackEventCategory& category);
+  static bool IsCategoryEnabled(const TrackEventCategoryRegistry& registry,
+                                const protos::gen::TrackEventConfig& config,
+                                const Category& category);
 
   static perfetto::EventContext WriteEvent(
       TraceWriterBase*,
       TrackEventIncrementalState*,
-      const char* category,
+      const Category* category,
       const char* name,
       perfetto::protos::pbzero::TrackEvent::Type,
       uint64_t timestamp = GetTimeNs());
diff --git a/include/perfetto/tracing/internal/track_event_macros.h b/include/perfetto/tracing/internal/track_event_macros.h
index 4d35f73..e0ea03c 100644
--- a/include/perfetto/tracing/internal/track_event_macros.h
+++ b/include/perfetto/tracing/internal/track_event_macros.h
@@ -40,8 +40,7 @@
 //
 #define PERFETTO_INTERNAL_DECLARE_CATEGORIES(...)                             \
   namespace internal {                                                        \
-  constexpr ::perfetto::internal::TrackEventCategory kCategories[] = {        \
-      __VA_ARGS__};                                                           \
+  constexpr ::perfetto::Category kCategories[] = {__VA_ARGS__};               \
   constexpr size_t kCategoryCount =                                           \
       sizeof(kCategories) / sizeof(kCategories[0]);                           \
   /* The per-instance enable/disable state per category */                    \
@@ -103,22 +102,21 @@
 // if so, emits one trace event with the given arguments.
 #define PERFETTO_INTERNAL_TRACK_EVENT(category, ...)                      \
   do {                                                                    \
-    using namespace ::PERFETTO_TRACK_EVENT_NAMESPACE;                     \
-    using namespace ::perfetto::internal;                                 \
+    namespace tns = ::PERFETTO_TRACK_EVENT_NAMESPACE;                     \
     /* Compute the category index outside the lambda to work around a */  \
     /* GCC 7 bug */                                                       \
     constexpr auto PERFETTO_UID(kCatIndex) =                              \
         PERFETTO_GET_CATEGORY_INDEX(category);                            \
-    if (internal::IsDynamicCategory(category)) {                          \
-      TrackEvent::CallIfEnabled([&](uint32_t instances) {                 \
-        TrackEvent::TraceForCategory<PERFETTO_UID(kCatIndex)>(            \
+    if (tns::internal::IsDynamicCategory(category)) {                     \
+      tns::TrackEvent::CallIfEnabled([&](uint32_t instances) {            \
+        tns::TrackEvent::TraceForCategory<PERFETTO_UID(kCatIndex)>(       \
             instances, category, ##__VA_ARGS__);                          \
       });                                                                 \
     } else {                                                              \
-      TrackEvent::CallIfCategoryEnabled<PERFETTO_UID(kCatIndex)>(         \
+      tns::TrackEvent::CallIfCategoryEnabled<PERFETTO_UID(kCatIndex)>(    \
           [&](uint32_t instances) {                                       \
             /* TODO(skyostil): Get rid of the category name parameter. */ \
-            TrackEvent::TraceForCategory<PERFETTO_UID(kCatIndex)>(        \
+            tns::TrackEvent::TraceForCategory<PERFETTO_UID(kCatIndex)>(   \
                 instances, nullptr, ##__VA_ARGS__);                       \
           });                                                             \
     }                                                                     \
diff --git a/include/perfetto/tracing/trace_writer_base.h b/include/perfetto/tracing/trace_writer_base.h
index 2d95668..824be1a 100644
--- a/include/perfetto/tracing/trace_writer_base.h
+++ b/include/perfetto/tracing/trace_writer_base.h
@@ -38,6 +38,7 @@
   NewTracePacket() = 0;
 
   virtual void Flush(std::function<void()> callback = {}) = 0;
+  virtual uint64_t written() const = 0;
 };
 
 }  // namespace perfetto
diff --git a/include/perfetto/tracing/track_event.h b/include/perfetto/tracing/track_event.h
index 7d5979e..be20e0b 100644
--- a/include/perfetto/tracing/track_event.h
+++ b/include/perfetto/tracing/track_event.h
@@ -39,9 +39,9 @@
 //   e.g., my_tracing.h:
 //
 //       PERFETTO_DEFINE_CATEGORIES(
-//           PERFETTO_CATEGORY(base),
-//           PERFETTO_CATEGORY(v8),
-//           PERFETTO_CATEGORY(cc));
+//           perfetto::Category("base"),
+//           perfetto::Category("v8"),
+//           perfetto::Category("cc"));
 //
 //   Then in a single .cc file, e.g., my_tracing.cc:
 //
@@ -125,10 +125,9 @@
 #define PERFETTO_TRACK_EVENT_NAMESPACE perfetto
 #endif
 
-// A name for a single category. Wrapped in a macro in case we need to introduce
-// more fields in the future.
+// Deprecated; see perfetto::Category().
 #define PERFETTO_CATEGORY(name) \
-  { #name }
+  ::perfetto::Category { #name }
 
 // Internal helpers for determining if a given category is defined at build or
 // runtime.
diff --git a/include/perfetto/tracing/track_event_category_registry.h b/include/perfetto/tracing/track_event_category_registry.h
index c769aef..ae58a31 100644
--- a/include/perfetto/tracing/track_event_category_registry.h
+++ b/include/perfetto/tracing/track_event_category_registry.h
@@ -19,10 +19,132 @@
 
 #include "perfetto/tracing/data_source.h"
 
+#include <stddef.h>
+
 #include <atomic>
 #include <utility>
 
 namespace perfetto {
+class DynamicCategory;
+
+// A compile-time representation of a track event category. See
+// PERFETTO_DEFINE_CATEGORIES for registering your own categories.
+struct Category {
+  using Tags = std::array<const char*, 4>;
+
+  const char* const name = nullptr;
+  const char* const description = nullptr;
+  const Tags tags = {};
+
+  constexpr Category(const Category&) = default;
+  constexpr explicit Category(const char* name_)
+      : name(CheckIsValidCategory(name_)),
+        name_sizes_(ComputeNameSizes(name_)) {}
+
+  constexpr Category SetDescription(const char* description_) const {
+    return Category(name, description_, tags, name_sizes_);
+  }
+
+  template <typename... Args>
+  constexpr Category SetTags(Args&&... args) const {
+    return Category(name, description, {std::forward<Args>(args)...},
+                    name_sizes_);
+  }
+
+  // A comma separated list of multiple categories to be used in a single trace
+  // point.
+  static constexpr Category Group(const char* names) {
+    return Category(names, AllowGroup{});
+  }
+
+  // Used for parsing dynamic category groups. Note that |name| and
+  // |DynamicCategory| must outlive the returned object because the category
+  // name isn't copied.
+  static Category FromDynamicCategory(const char* name);
+  static Category FromDynamicCategory(const DynamicCategory&);
+
+  constexpr bool IsGroup() const { return GetNameSize(1) > 0; }
+
+  // Returns the number of character in the category name. Not valid for
+  // category groups.
+  size_t name_size() const {
+    PERFETTO_DCHECK(!IsGroup());
+    return GetNameSize(0);
+  }
+
+  // Iterates over all the members of this category group, or just the name of
+  // the category itself if this isn't a category group. Return false from
+  // |callback| to stop iteration.
+  template <typename T>
+  void ForEachGroupMember(T callback) const {
+    const char* name_ptr = name;
+    size_t i = 0;
+    while (size_t name_size = GetNameSize(i++)) {
+      if (!callback(name_ptr, name_size))
+        break;
+      name_ptr += name_size + 1;
+    }
+  }
+
+ private:
+  static constexpr size_t kMaxGroupSize = 4;
+  using NameSizes = std::array<uint8_t, kMaxGroupSize>;
+
+  constexpr Category(const char* name_,
+                     const char* description_,
+                     Tags tags_,
+                     NameSizes name_sizes)
+      : name(name_),
+        description(description_),
+        tags(tags_),
+        name_sizes_(name_sizes) {}
+
+  enum AllowGroup {};
+  constexpr Category(const char* name_, AllowGroup)
+      : name(CheckIsValidCategoryGroup(name_)),
+        name_sizes_(ComputeNameSizes(name_)) {}
+
+  constexpr size_t GetNameSize(size_t i) const {
+    return i < name_sizes_.size() ? name_sizes_[i] : 0;
+  }
+
+  static constexpr NameSizes ComputeNameSizes(const char* s) {
+    static_assert(kMaxGroupSize == 4, "Unexpected maximum category group size");
+    return NameSizes{{static_cast<uint8_t>(GetNthNameSize(0, s, s)),
+                      static_cast<uint8_t>(GetNthNameSize(1, s, s)),
+                      static_cast<uint8_t>(GetNthNameSize(2, s, s)),
+                      static_cast<uint8_t>(GetNthNameSize(3, s, s))}};
+  }
+
+  static constexpr ptrdiff_t GetNthNameSize(int n,
+                                            const char* start,
+                                            const char* end,
+                                            int counter = 0) {
+    return (!*end || *end == ',')
+               ? ((!*end || counter == n)
+                      ? (counter == n ? end - start : 0)
+                      : GetNthNameSize(n, end + 1, end + 1, counter + 1))
+               : GetNthNameSize(n, start, end + 1, counter);
+  }
+
+  static constexpr const char* CheckIsValidCategory(const char* n) {
+    // We just replace invalid input with a nullptr here; it will trigger a
+    // static assert in TrackEventCategoryRegistry::ValidateCategories().
+    return GetNthNameSize(1, n, n) ? nullptr : n;
+  }
+
+  static constexpr const char* CheckIsValidCategoryGroup(const char* n) {
+    // Same as above: replace invalid input with nullptr.
+    return !GetNthNameSize(1, n, n) || GetNthNameSize(kMaxGroupSize, n, n)
+               ? nullptr
+               : n;
+  }
+
+  // An array of lengths of the different names associated with this category.
+  // If this category doesn't represent a group of multiple categories, only the
+  // first element is non-zero.
+  const NameSizes name_sizes_ = {};
+};
 
 // Dynamically constructed category names should marked as such through this
 // container type to make it less likely for trace points to accidentally start
@@ -66,18 +188,12 @@
          IsStringInPrefixList(str, std::forward<Args>(args)...);
 }
 
-// A compile-time representation of a track event category. See
-// PERFETTO_DEFINE_CATEGORIES for registering your own categories.
-struct TrackEventCategory {
-  const char* const name;
-};
-
 // Holds all the registered categories for one category namespace. See
 // PERFETTO_DEFINE_CATEGORIES for building the registry.
 class TrackEventCategoryRegistry {
  public:
   constexpr TrackEventCategoryRegistry(size_t category_count,
-                                       const TrackEventCategory* categories,
+                                       const Category* categories,
                                        std::atomic<uint8_t>* state_storage)
       : categories_(categories),
         category_count_(category_count),
@@ -91,7 +207,7 @@
   size_t category_count() const { return category_count_; }
 
   // Returns a category based on its index.
-  const TrackEventCategory* GetCategory(size_t index) const;
+  const Category* GetCategory(size_t index) const;
 
   // Turn tracing on or off for the given category in a track event data source
   // instance.
@@ -154,7 +270,7 @@
  private:
   // TODO(skyostil): Make the compile-time routines nicer with C++14.
   static constexpr bool IsValidCategoryName(const char* name) {
-    return (*name == '\"' || *name == '*')
+    return (!name || *name == '\"' || *name == '*' || *name == ' ')
                ? false
                : *name ? IsValidCategoryName(name + 1) : true;
   }
@@ -164,7 +280,7 @@
                     : (!*a || !*b) ? (*a == *b) : StringEq(a + 1, b + 1);
   }
 
-  const TrackEventCategory* const categories_;
+  const Category* const categories_;
   const size_t category_count_;
   std::atomic<uint8_t>* const state_storage_;
 };
diff --git a/include/perfetto/tracing/track_event_interned_data_index.h b/include/perfetto/tracing/track_event_interned_data_index.h
index be4c128..cce2665 100644
--- a/include/perfetto/tracing/track_event_interned_data_index.h
+++ b/include/perfetto/tracing/track_event_interned_data_index.h
@@ -170,8 +170,12 @@
     : public internal::BaseTrackEventInternedDataIndex {
  public:
   // Return an interning id for |value|. The returned id can be immediately
-  // written to the trace.
-  static size_t Get(EventContext* ctx, const ValueType& value) {
+  // written to the trace. The optional |add_args| are passed to the Add()
+  // function.
+  template <typename... Args>
+  static size_t Get(EventContext* ctx,
+                    const ValueType& value,
+                    Args&&... add_args) {
     // First check if the value exists in the dictionary.
     auto index_for_field = GetOrCreateIndexForField(ctx->incremental_state_);
     size_t iid;
@@ -186,7 +190,7 @@
     PERFETTO_DCHECK(iid);
     InternedDataType::Add(
         ctx->incremental_state_->serialized_interned_data.get(), iid,
-        std::move(value));
+        std::move(value), std::forward<Args>(add_args)...);
     return iid;
   }
 
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 2acc000..102bac2 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -1154,13 +1154,35 @@
   // The following fields define the set of enabled trace categories. Each list
   // item is a glob.
   //
-  // To determine if category X is enabled:
+  // To determine if category is enabled, it is checked against the filters in
+  // the following order:
   //
-  //   if (X in disabled_categories):
-  //     return (X in enabled_categories)
-  //   else if (X in disabled_tags):
-  //     return (X in enabled_categories or X in enabled_tags)
-  //   else return true
+  //   1. Exact matches in enabled categories.
+  //   2. Exact matches in enabled tags.
+  //   3. Exact matches in disabled categories.
+  //   4. Exact matches in disabled tags.
+  //   5. Pattern matches in enabled categories.
+  //   6. Pattern matches in enabled tags.
+  //   7. Pattern matches in disabled categories.
+  //   8. Pattern matches in disabled tags.
+  //
+  // If none of the steps produced a match, the category is enabled by default.
+  //
+  // Examples:
+  //
+  //  - To enable all non-slow/debug categories:
+  //
+  //       No configuration needed, happens by default.
+  //
+  //  - To enable a specific category:
+  //
+  //       disabled_categories = ["*"]
+  //       enabled_categories = ["my_category"]
+  //
+  //  - To enable only categories with a specific tag:
+  //
+  //       disabled_tags = ["*"]
+  //       enabled_tags = ["my_tag"]
   //
   repeated string disabled_categories = 1;  // Default: []
   repeated string enabled_categories = 2;   // Default: []
diff --git a/protos/perfetto/config/track_event/track_event_config.proto b/protos/perfetto/config/track_event/track_event_config.proto
index 8fab47c..6ae1f8f 100644
--- a/protos/perfetto/config/track_event/track_event_config.proto
+++ b/protos/perfetto/config/track_event/track_event_config.proto
@@ -22,13 +22,35 @@
   // The following fields define the set of enabled trace categories. Each list
   // item is a glob.
   //
-  // To determine if category X is enabled:
+  // To determine if category is enabled, it is checked against the filters in
+  // the following order:
   //
-  //   if (X in disabled_categories):
-  //     return (X in enabled_categories)
-  //   else if (X in disabled_tags):
-  //     return (X in enabled_categories or X in enabled_tags)
-  //   else return true
+  //   1. Exact matches in enabled categories.
+  //   2. Exact matches in enabled tags.
+  //   3. Exact matches in disabled categories.
+  //   4. Exact matches in disabled tags.
+  //   5. Pattern matches in enabled categories.
+  //   6. Pattern matches in enabled tags.
+  //   7. Pattern matches in disabled categories.
+  //   8. Pattern matches in disabled tags.
+  //
+  // If none of the steps produced a match, the category is enabled by default.
+  //
+  // Examples:
+  //
+  //  - To enable all non-slow/debug categories:
+  //
+  //       No configuration needed, happens by default.
+  //
+  //  - To enable a specific category:
+  //
+  //       disabled_categories = ["*"]
+  //       enabled_categories = ["my_category"]
+  //
+  //  - To enable only categories with a specific tag:
+  //
+  //       disabled_tags = ["*"]
+  //       enabled_tags = ["my_tag"]
   //
   repeated string disabled_categories = 1;  // Default: []
   repeated string enabled_categories = 2;   // Default: []
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 16ab8d0..a2ea1d1 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -5591,13 +5591,35 @@
   // The following fields define the set of enabled trace categories. Each list
   // item is a glob.
   //
-  // To determine if category X is enabled:
+  // To determine if category is enabled, it is checked against the filters in
+  // the following order:
   //
-  //   if (X in disabled_categories):
-  //     return (X in enabled_categories)
-  //   else if (X in disabled_tags):
-  //     return (X in enabled_categories or X in enabled_tags)
-  //   else return true
+  //   1. Exact matches in enabled categories.
+  //   2. Exact matches in enabled tags.
+  //   3. Exact matches in disabled categories.
+  //   4. Exact matches in disabled tags.
+  //   5. Pattern matches in enabled categories.
+  //   6. Pattern matches in enabled tags.
+  //   7. Pattern matches in disabled categories.
+  //   8. Pattern matches in disabled tags.
+  //
+  // If none of the steps produced a match, the category is enabled by default.
+  //
+  // Examples:
+  //
+  //  - To enable all non-slow/debug categories:
+  //
+  //       No configuration needed, happens by default.
+  //
+  //  - To enable a specific category:
+  //
+  //       disabled_categories = ["*"]
+  //       enabled_categories = ["my_category"]
+  //
+  //  - To enable only categories with a specific tag:
+  //
+  //       disabled_tags = ["*"]
+  //       enabled_tags = ["my_tag"]
   //
   repeated string disabled_categories = 1;  // Default: []
   repeated string enabled_categories = 2;   // Default: []
diff --git a/src/perfetto_cmd/perfetto_config.descriptor.h b/src/perfetto_cmd/perfetto_config.descriptor.h
index 8a77cfd..fb22c36 100644
--- a/src/perfetto_cmd/perfetto_config.descriptor.h
+++ b/src/perfetto_cmd/perfetto_config.descriptor.h
@@ -27,7 +27,7 @@
 // SHA1(tools/gen_binary_descriptors)
 // d6628b15181dba5287e35b56b966b39ea93d42b1
 // SHA1(protos/perfetto/config/perfetto_config.proto)
-// 0cf480593a4dbbfdf5aa88924418e6973523489d
+// a5fa2ae0a3cc1fc0f9e5ab400145cf0a3d086fa3
 
 // This is the proto PerfettoConfig encoded as a ProtoFileDescriptor to allow
 // for reflection without libprotobuf full/non-lite protos.
diff --git a/src/profiling/common/unwind_support.cc b/src/profiling/common/unwind_support.cc
index b597368..cb791bc 100644
--- a/src/profiling/common/unwind_support.cc
+++ b/src/profiling/common/unwind_support.cc
@@ -130,5 +130,26 @@
   return FrameData{std::move(frame), std::move(build_id)};
 }
 
+std::string StringifyLibUnwindstackError(unwindstack::ErrorCode e) {
+  switch (e) {
+    case unwindstack::ERROR_NONE:
+      return "NONE";
+    case unwindstack::ERROR_MEMORY_INVALID:
+      return "MEMORY_INVALID";
+    case unwindstack::ERROR_UNWIND_INFO:
+      return "UNWIND_INFO";
+    case unwindstack::ERROR_UNSUPPORTED:
+      return "UNSUPPORTED";
+    case unwindstack::ERROR_INVALID_MAP:
+      return "INVALID_MAP";
+    case unwindstack::ERROR_MAX_FRAMES_EXCEEDED:
+      return "MAX_FRAME_EXCEEDED";
+    case unwindstack::ERROR_REPEATED_FRAME:
+      return "REPEATED_FRAME";
+    case unwindstack::ERROR_INVALID_ELF:
+      return "INVALID_ELF";
+  }
+}
+
 }  // namespace profiling
 }  // namespace perfetto
diff --git a/src/profiling/common/unwind_support.h b/src/profiling/common/unwind_support.h
index 061ed61..732531c 100644
--- a/src/profiling/common/unwind_support.h
+++ b/src/profiling/common/unwind_support.h
@@ -125,6 +125,8 @@
 #endif
 };
 
+std::string StringifyLibUnwindstackError(unwindstack::ErrorCode);
+
 }  // namespace profiling
 }  // namespace perfetto
 
diff --git a/src/profiling/memory/client.cc b/src/profiling/memory/client.cc
index a0db2de..74cdafa 100644
--- a/src/profiling/memory/client.cc
+++ b/src/profiling/memory/client.cc
@@ -174,7 +174,7 @@
 
   base::ScopedFile page_idle(base::OpenFile("/proc/self/page_idle", O_RDWR));
   if (!page_idle) {
-    PERFETTO_LOG("Failed to open /proc/self/page_idle. Continuing.");
+    PERFETTO_DLOG("Failed to open /proc/self/page_idle. Continuing.");
     num_send_fds = kHandshakeSize - 1;
   }
 
diff --git a/src/profiling/memory/unwinding.cc b/src/profiling/memory/unwinding.cc
index 26a393a..c9e43fe 100644
--- a/src/profiling/memory/unwinding.cc
+++ b/src/profiling/memory/unwinding.cc
@@ -144,7 +144,7 @@
 #endif
   // Suppress incorrect "variable may be uninitialized" error for if condition
   // after this loop. error_code = LastErrorCode gets run at least once.
-  uint8_t error_code = 0;
+  unwindstack::ErrorCode error_code = unwindstack::ERROR_NONE;
   for (int attempt = 0; attempt < 2; ++attempt) {
     if (attempt > 0) {
       if (metadata->last_maps_reparse_time + kMapsReparseInterval >
@@ -177,7 +177,8 @@
   if (error_code != unwindstack::ERROR_NONE) {
     PERFETTO_DLOG("Unwinding error %" PRIu8, error_code);
     unwindstack::FrameData frame_data{};
-    frame_data.function_name = "ERROR " + std::to_string(error_code);
+    frame_data.function_name =
+        "ERROR " + StringifyLibUnwindstackError(error_code);
     frame_data.map_name = "ERROR";
 
     out->frames.emplace_back(std::move(frame_data), "");
diff --git a/src/profiling/perf/BUILD.gn b/src/profiling/perf/BUILD.gn
index 9b4e2d1..5f36377 100644
--- a/src/profiling/perf/BUILD.gn
+++ b/src/profiling/perf/BUILD.gn
@@ -44,6 +44,7 @@
   public_deps = [
     ":regs_parsing",
     "../../../include/perfetto/tracing/core",
+    "../../../src/tracing/core:service",  # for metatrace
   ]
   deps = [
     ":proc_descriptors",
diff --git a/src/profiling/perf/perf_producer.cc b/src/profiling/perf/perf_producer.cc
index 0437254..c6fd0db 100644
--- a/src/profiling/perf/perf_producer.cc
+++ b/src/profiling/perf/perf_producer.cc
@@ -25,6 +25,7 @@
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/task_runner.h"
+#include "perfetto/ext/base/metatrace.h"
 #include "perfetto/ext/base/weak_ptr.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/producer.h"
@@ -128,6 +129,13 @@
   PERFETTO_LOG("StartDataSource(%zu, %s)", static_cast<size_t>(instance_id),
                config.name().c_str());
 
+  if (config.name() == MetatraceWriter::kDataSourceName) {
+    StartMetatraceSource(instance_id,
+                         static_cast<BufferID>(config.target_buffer()));
+    return;
+  }
+
+  // linux.perf data source
   if (config.name() != kDataSourceName)
     return;
 
@@ -192,6 +200,16 @@
 
 void PerfProducer::StopDataSource(DataSourceInstanceID instance_id) {
   PERFETTO_LOG("StopDataSource(%zu)", static_cast<size_t>(instance_id));
+
+  // Metatrace: stop immediately (will miss the events from the
+  // asynchronous shutdown of the primary data source).
+  auto meta_it = metatrace_writers_.find(instance_id);
+  if (meta_it != metatrace_writers_.end()) {
+    meta_it->second.WriteAllAndFlushTraceWriter([] {});
+    metatrace_writers_.erase(meta_it);
+    return;
+  }
+
   auto ds_it = data_sources_.find(instance_id);
   if (ds_it == data_sources_.end())
     return;
@@ -209,14 +227,22 @@
 void PerfProducer::Flush(FlushRequestID flush_id,
                          const DataSourceInstanceID* data_source_ids,
                          size_t num_data_sources) {
+  bool should_ack_flush = false;
   for (size_t i = 0; i < num_data_sources; i++) {
     auto ds_id = data_source_ids[i];
     PERFETTO_DLOG("Flush(%zu)", static_cast<size_t>(ds_id));
-    auto ds_it = data_sources_.find(ds_id);
-    if (ds_it != data_sources_.end()) {
-      endpoint_->NotifyFlushComplete(flush_id);
+
+    auto meta_it = metatrace_writers_.find(ds_id);
+    if (meta_it != metatrace_writers_.end()) {
+      meta_it->second.WriteAllAndFlushTraceWriter([] {});
+      should_ack_flush = true;
+    }
+    if (data_sources_.find(ds_id) != data_sources_.end()) {
+      should_ack_flush = true;
     }
   }
+  if (should_ack_flush)
+    endpoint_->NotifyFlushComplete(flush_id);
 }
 
 void PerfProducer::TickDataSourceRead(DataSourceInstanceID ds_id) {
@@ -228,6 +254,8 @@
   }
   DataSource& ds = it->second;
 
+  PERFETTO_METATRACE_SCOPED(TAG_PRODUCER, PROFILER_READ_TICK);
+
   // Make a pass over all per-cpu readers.
   bool more_records_available = false;
   for (EventReader& reader : ds.per_cpu_readers) {
@@ -256,6 +284,7 @@
                                             DataSourceInstanceID ds_id,
                                             DataSource* ds) {
   using Status = DataSource::ProcDescriptors::Status;
+  PERFETTO_METATRACE_SCOPED(TAG_PRODUCER, PROFILER_READ_CPU);
 
   // If the kernel ring buffer dropped data, record it in the trace.
   size_t cpu = reader->cpu();
@@ -279,7 +308,7 @@
       continue;
     }
 
-    // Request proc-fds for the process if this is the first time we see it yet.
+    // Request proc-fds for the process if this is the first time we see it.
     pid_t pid = sample->pid;
     auto& fd_entry = ds->proc_fds[pid];  // created if absent
 
@@ -299,10 +328,14 @@
 
     // Push the sample into a dedicated unwinding queue.
     unwind_queues_[ds_id].emplace_back(std::move(sample.value()));
+
+    // Metatrace: counter sensible only when there's a single active source.
+    PERFETTO_METATRACE_COUNTER(TAG_PRODUCER, PROFILER_UNWIND_QUEUE_SZ,
+                               unwind_queues_[ds_id].size());
   }
 
-  // Most likely more events in the buffer - technically, max_samples can stop
-  // us right at the boundary.
+  // Most likely more events in the buffer. Though we might be exactly on the
+  // boundary due to |max_samples|.
   return true;
 }
 
@@ -371,6 +404,8 @@
   auto unwind_it = unwind_queues_.find(ds_id);
   PERFETTO_CHECK(unwind_it != unwind_queues_.end());
 
+  PERFETTO_METATRACE_SCOPED(TAG_PRODUCER, PROFILER_UNWIND_TICK);
+
   bool queue_active =
       ProcessUnwindQueue(ds_id, &unwind_it->second, &ds_it->second);
 
@@ -439,11 +474,12 @@
 
     // Sample ready - process it.
     if (fd_status == Status::kResolved) {
+      PERFETTO_METATRACE_SCOPED(TAG_PRODUCER, PROFILER_UNWIND_SAMPLE);
+
       PerfProducer::CompletedSample unwound_sample =
           UnwindSample(std::move(sample), &proc_fd_it->second);
 
       PostEmitSample(ds_id, std::move(unwound_sample));
-
       entry.valid = false;
       continue;
     }
@@ -457,6 +493,9 @@
     queue.pop_front();
   }
 
+  // Metatrace: counter sensible only when there's a single active source.
+  PERFETTO_METATRACE_COUNTER(TAG_PRODUCER, PROFILER_UNWIND_QUEUE_SZ,
+                             queue.size());
   PERFETTO_DLOG("Unwind queue drain: [%zu]->[%zu]", num_samples, queue.size());
 
   // Return whether we're done with unwindings for this source.
@@ -638,6 +677,18 @@
   }
 }
 
+void PerfProducer::StartMetatraceSource(DataSourceInstanceID ds_id,
+                                        BufferID target_buffer) {
+  auto writer = endpoint_->CreateTraceWriter(target_buffer);
+
+  auto it_and_inserted = metatrace_writers_.emplace(
+      std::piecewise_construct, std::make_tuple(ds_id), std::make_tuple());
+  PERFETTO_DCHECK(it_and_inserted.second);
+  // Note: only the first concurrent writer will actually be active.
+  metatrace_writers_[ds_id].Enable(task_runner_, std::move(writer),
+                                   metatrace::TAG_ANY);
+}
+
 void PerfProducer::ConnectWithRetries(const char* socket_name) {
   PERFETTO_DCHECK(state_ == kNotStarted);
   state_ = kNotConnected;
@@ -670,10 +721,19 @@
   ResetConnectionBackoff();
   PERFETTO_LOG("Connected to the service");
 
-  DataSourceDescriptor desc;
-  desc.set_name(kDataSourceName);
-  desc.set_will_notify_on_stop(true);
-  endpoint_->RegisterDataSource(desc);
+  {
+    // linux.perf
+    DataSourceDescriptor desc;
+    desc.set_name(kDataSourceName);
+    desc.set_will_notify_on_stop(true);
+    endpoint_->RegisterDataSource(desc);
+  }
+  {
+    // metatrace
+    DataSourceDescriptor desc;
+    desc.set_name(MetatraceWriter::kDataSourceName);
+    endpoint_->RegisterDataSource(desc);
+  }
 }
 
 void PerfProducer::OnDisconnect() {
diff --git a/src/profiling/perf/perf_producer.h b/src/profiling/perf/perf_producer.h
index ee2a661..1b127c7 100644
--- a/src/profiling/perf/perf_producer.h
+++ b/src/profiling/perf/perf_producer.h
@@ -41,6 +41,7 @@
 #include "src/profiling/perf/event_config.h"
 #include "src/profiling/perf/event_reader.h"
 #include "src/profiling/perf/proc_descriptors.h"
+#include "src/tracing/core/metatrace_writer.h"
 
 namespace perfetto {
 namespace profiling {
@@ -197,6 +198,8 @@
   // service of the stop.
   void FinishDataSourceStop(DataSourceInstanceID ds_id);
 
+  void StartMetatraceSource(DataSourceInstanceID ds_id, BufferID target_buffer);
+
   // Task runner owned by the main thread.
   base::TaskRunner* const task_runner_;
   State state_ = kNotStarted;
@@ -209,6 +212,10 @@
   // Owns shared memory, must outlive trace writing.
   std::unique_ptr<TracingService::ProducerEndpoint> endpoint_;
 
+  // If multiple metatrace sources are enabled concurrently,
+  // only the first one becomes active.
+  std::map<DataSourceInstanceID, MetatraceWriter> metatrace_writers_;
+
   // Interns callstacks across all data sources.
   // TODO(rsavitski): for long profiling sessions, consider purging trie when it
   // grows too large (at the moment purged only when no sources are active).
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index e57d41b..2aec4e3 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -298,13 +298,13 @@
 if (enable_perfetto_trace_processor_sqlite) {
   source_set("lib") {
     sources = [
+      "experimental_counter_dur_generator.cc",
+      "experimental_counter_dur_generator.h",
+      "experimental_flamegraph_generator.cc",
+      "experimental_flamegraph_generator.h",
       "read_trace.cc",
       "sql_stats_table.cc",
       "sql_stats_table.h",
-      "sqlite_experimental_counter_dur_table.cc",
-      "sqlite_experimental_counter_dur_table.h",
-      "sqlite_experimental_flamegraph_table.cc",
-      "sqlite_experimental_flamegraph_table.h",
       "sqlite_raw_table.cc",
       "sqlite_raw_table.h",
       "stats_table.cc",
@@ -409,7 +409,7 @@
   ]
 
   if (enable_perfetto_trace_processor_sqlite) {
-    sources += [ "sqlite_experimental_counter_dur_table_unittest.cc" ]
+    sources += [ "experimental_counter_dur_generator_unittest.cc" ]
     deps += [
       ":lib",
       "../../gn:sqlite",
diff --git a/src/trace_processor/db/column.h b/src/trace_processor/db/column.h
index e6e5387..b477e93 100644
--- a/src/trace_processor/db/column.h
+++ b/src/trace_processor/db/column.h
@@ -96,6 +96,12 @@
     // This is used to speed up filters as we can safely index SparseVector
     // directly if this flag is set.
     kNonNull = 1 << 1,
+
+    // Indicates that the data in the column is "hidden". This can by used to
+    // hint to users of Table and Column that this column should not be
+    // displayed to the user as it is part of the internal implementation
+    // details of the table.
+    kHidden = 1 << 2,
   };
 
   // Flags specified for an id column.
diff --git a/src/trace_processor/db/table.h b/src/trace_processor/db/table.h
index b283ab0..f25aec5 100644
--- a/src/trace_processor/db/table.h
+++ b/src/trace_processor/db/table.h
@@ -82,6 +82,7 @@
       SqlValue::Type type;
       bool is_id;
       bool is_sorted;
+      bool is_hidden;
     };
     std::vector<Column> columns;
   };
diff --git a/src/trace_processor/experimental_counter_dur_generator.cc b/src/trace_processor/experimental_counter_dur_generator.cc
new file mode 100644
index 0000000..6026fd0
--- /dev/null
+++ b/src/trace_processor/experimental_counter_dur_generator.cc
@@ -0,0 +1,92 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/experimental_counter_dur_generator.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+ExperimentalCounterDurGenerator::ExperimentalCounterDurGenerator(
+    const tables::CounterTable& table)
+    : counter_table_(&table) {}
+ExperimentalCounterDurGenerator::~ExperimentalCounterDurGenerator() = default;
+
+Table::Schema ExperimentalCounterDurGenerator::CreateSchema() {
+  Table::Schema schema = tables::CounterTable::Schema();
+  schema.columns.emplace_back(
+      Table::Schema::Column{"dur", SqlValue::Type::kLong, false /* is_id */,
+                            false /* is_sorted */, false /* is_hidden */});
+  return schema;
+}
+
+std::string ExperimentalCounterDurGenerator::TableName() {
+  return "experimental_counter_dur";
+}
+
+uint32_t ExperimentalCounterDurGenerator::EstimateRowCount() {
+  return counter_table_->row_count();
+}
+
+util::Status ExperimentalCounterDurGenerator::ValidateConstraints(
+    const QueryConstraints&) {
+  return util::OkStatus();
+}
+
+Table* ExperimentalCounterDurGenerator::ComputeTable(
+    const std::vector<Constraint>&,
+    const std::vector<Order>&) {
+  // We structure the code the following way (instead of just resetting the
+  // unique_ptr fields) to ensure that we don't hold onto a pointer to the
+  // sparsevector in the table after freeing the sparsevector.
+  std::unique_ptr<SparseVector<int64_t>> dur_column(
+      new SparseVector<int64_t>(ComputeDurColumn(*counter_table_)));
+  counter_with_dur_table_.reset(
+      new Table(counter_with_dur_table_->ExtendWithColumn(
+          "dur", dur_column.get(), TypedColumn<int64_t>::default_flags())));
+  dur_column_ = std::move(dur_column);
+  return counter_with_dur_table_.get();
+}
+
+// static
+SparseVector<int64_t> ExperimentalCounterDurGenerator::ComputeDurColumn(
+    const tables::CounterTable& table) {
+  // Keep track of the last seen row for each track id.
+  std::unordered_map<TrackId, uint32_t> last_row_for_track_id;
+  SparseVector<int64_t> dur;
+  for (uint32_t i = 0; i < table.row_count(); ++i) {
+    // Check if we already have a previous row for the current track id.
+    TrackId track_id = table.track_id()[i];
+    auto it = last_row_for_track_id.find(track_id);
+    if (it == last_row_for_track_id.end()) {
+      // This means we don't have any row - start tracking this row for the
+      // future.
+      last_row_for_track_id.emplace(track_id, i);
+    } else {
+      // This means we have an previous row for the current track id. Update
+      // the duration of the previous row to be up to the current ts.
+      uint32_t old_row = it->second;
+      it->second = i;
+      dur.Set(old_row, table.ts()[i] - table.ts()[old_row]);
+    }
+    // Append -1 to mark this event as not having been finished. On a later
+    // row, we may set this to have the correct value.
+    dur.Append(-1);
+  }
+  return dur;
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/experimental_counter_dur_generator.h b/src/trace_processor/experimental_counter_dur_generator.h
new file mode 100644
index 0000000..4be870f
--- /dev/null
+++ b/src/trace_processor/experimental_counter_dur_generator.h
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_EXPERIMENTAL_COUNTER_DUR_GENERATOR_H_
+#define SRC_TRACE_PROCESSOR_EXPERIMENTAL_COUNTER_DUR_GENERATOR_H_
+
+#include "src/trace_processor/sqlite/db_sqlite_table.h"
+
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class ExperimentalCounterDurGenerator
+    : public DbSqliteTable::DynamicTableGenerator {
+ public:
+  ExperimentalCounterDurGenerator(const tables::CounterTable& table);
+  virtual ~ExperimentalCounterDurGenerator() override;
+
+  Table::Schema CreateSchema() override;
+  std::string TableName() override;
+  uint32_t EstimateRowCount() override;
+  util::Status ValidateConstraints(const QueryConstraints&) override;
+  Table* ComputeTable(const std::vector<Constraint>&,
+                      const std::vector<Order>&) override;
+
+  // public + static for testing
+  static SparseVector<int64_t> ComputeDurColumn(
+      const tables::CounterTable& table);
+
+ private:
+  const tables::CounterTable* counter_table_ = nullptr;
+  std::unique_ptr<Table> counter_with_dur_table_;
+  std::unique_ptr<SparseVector<int64_t>> dur_column_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_EXPERIMENTAL_COUNTER_DUR_GENERATOR_H_
diff --git a/src/trace_processor/sqlite_experimental_counter_dur_table_unittest.cc b/src/trace_processor/experimental_counter_dur_generator_unittest.cc
similarity index 86%
rename from src/trace_processor/sqlite_experimental_counter_dur_table_unittest.cc
rename to src/trace_processor/experimental_counter_dur_generator_unittest.cc
index 81aa416..8538f1a 100644
--- a/src/trace_processor/sqlite_experimental_counter_dur_table_unittest.cc
+++ b/src/trace_processor/experimental_counter_dur_generator_unittest.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "src/trace_processor/sqlite_experimental_counter_dur_table.h"
+#include "src/trace_processor/experimental_counter_dur_generator.h"
 
 #include "test/gtest_and_gmock.h"
 
@@ -25,11 +25,11 @@
 tables::CounterTable::Row CounterRow(int64_t ts, uint32_t track_id) {
   tables::CounterTable::Row row;
   row.ts = ts;
-  row.track_id = TrackId{track_id};
+  row.track_id = tables::TrackTable::Id{track_id};
   return row;
 }
 
-TEST(SqliteExperimentalCounterDurTable, SmokeDur) {
+TEST(ExperimentalCounterDurGenerator, SmokeDur) {
   StringPool pool;
   tables::CounterTable table(&pool, nullptr);
 
@@ -40,7 +40,7 @@
   table.Insert(CounterRow(105 /* ts */, 2 /* track_id */));
   table.Insert(CounterRow(110 /* ts */, 2 /* track_id */));
 
-  auto dur = SqliteExperimentalCounterDurTable::ComputeDurColumn(table);
+  auto dur = ExperimentalCounterDurGenerator::ComputeDurColumn(table);
   ASSERT_EQ(dur.size(), table.row_count());
 
   ASSERT_EQ(dur.GetNonNull(0), 5);
diff --git a/src/trace_processor/experimental_flamegraph_generator.cc b/src/trace_processor/experimental_flamegraph_generator.cc
new file mode 100644
index 0000000..ccad480
--- /dev/null
+++ b/src/trace_processor/experimental_flamegraph_generator.cc
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+
+#include "src/trace_processor/experimental_flamegraph_generator.h"
+
+#include "src/trace_processor/heap_profile_tracker.h"
+#include "src/trace_processor/importers/proto/heap_graph_tracker.h"
+#include "src/trace_processor/trace_processor_context.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+namespace {
+
+ExperimentalFlamegraphGenerator::InputValues GetInputValues(
+    const std::vector<Constraint>& cs) {
+  using T = tables::ExperimentalFlamegraphNodesTable;
+
+  auto ts_fn = [](const Constraint& c) {
+    return c.col_idx == static_cast<uint32_t>(T::ColumnIndex::ts) &&
+           c.op == FilterOp::kEq;
+  };
+  auto upid_fn = [](const Constraint& c) {
+    return c.col_idx == static_cast<uint32_t>(T::ColumnIndex::upid) &&
+           c.op == FilterOp::kEq;
+  };
+  auto profile_type_fn = [](const Constraint& c) {
+    return c.col_idx == static_cast<uint32_t>(T::ColumnIndex::profile_type) &&
+           c.op == FilterOp::kEq;
+  };
+
+  auto ts_it = std::find_if(cs.begin(), cs.end(), ts_fn);
+  auto upid_it = std::find_if(cs.begin(), cs.end(), upid_fn);
+  auto profile_type_it = std::find_if(cs.begin(), cs.end(), profile_type_fn);
+
+  // We should always have valid iterators here because BestIndex should only
+  // allow the constraint set to be chosen when we have an equality constraint
+  // on both ts and upid.
+  PERFETTO_CHECK(ts_it != cs.end());
+  PERFETTO_CHECK(upid_it != cs.end());
+  PERFETTO_CHECK(profile_type_it != cs.end());
+
+  int64_t ts = ts_it->value.AsLong();
+  UniquePid upid = static_cast<UniquePid>(upid_it->value.AsLong());
+  std::string profile_type = profile_type_it->value.AsString();
+
+  return ExperimentalFlamegraphGenerator::InputValues{ts, upid, profile_type};
+}
+
+}  // namespace
+
+ExperimentalFlamegraphGenerator::ExperimentalFlamegraphGenerator(
+    TraceProcessorContext* context)
+    : context_(context) {}
+
+ExperimentalFlamegraphGenerator::~ExperimentalFlamegraphGenerator() = default;
+
+util::Status ExperimentalFlamegraphGenerator::ValidateConstraints(
+    const QueryConstraints& qc) {
+  using T = tables::ExperimentalFlamegraphNodesTable;
+
+  const auto& cs = qc.constraints();
+
+  auto ts_fn = [](const QueryConstraints::Constraint& c) {
+    return c.column == static_cast<int>(T::ColumnIndex::ts) &&
+           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
+  };
+  bool has_ts_cs = std::find_if(cs.begin(), cs.end(), ts_fn) != cs.end();
+
+  auto upid_fn = [](const QueryConstraints::Constraint& c) {
+    return c.column == static_cast<int>(T::ColumnIndex::upid) &&
+           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
+  };
+  bool has_upid_cs = std::find_if(cs.begin(), cs.end(), upid_fn) != cs.end();
+
+  auto profile_type_fn = [](const QueryConstraints::Constraint& c) {
+    return c.column == static_cast<int>(T::ColumnIndex::profile_type) &&
+           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
+  };
+  bool has_profile_type_cs =
+      std::find_if(cs.begin(), cs.end(), profile_type_fn) != cs.end();
+
+  return has_ts_cs && has_upid_cs && has_profile_type_cs
+             ? util::OkStatus()
+             : util::ErrStatus("Failed to find required constraints");
+}
+
+Table* ExperimentalFlamegraphGenerator::ComputeTable(
+    const std::vector<Constraint>& cs,
+    const std::vector<Order>&) {
+  // Get the input column values and compute the flamegraph using them.
+  auto values = GetInputValues(cs);
+
+  if (values.profile_type == "graph") {
+    auto* tracker = HeapGraphTracker::GetOrCreate(context_);
+    table_ = tracker->BuildFlamegraph(values.ts, values.upid);
+  }
+  if (values.profile_type == "native") {
+    table_ =
+        BuildNativeFlamegraph(context_->storage.get(), values.upid, values.ts);
+  }
+  return table_.get();
+}
+
+Table::Schema ExperimentalFlamegraphGenerator::CreateSchema() {
+  return tables::ExperimentalFlamegraphNodesTable::Schema();
+}
+
+std::string ExperimentalFlamegraphGenerator::TableName() {
+  return "experimental_flamegraph";
+}
+
+uint32_t ExperimentalFlamegraphGenerator::EstimateRowCount() {
+  // TODO(lalitm): return a better estimate here when possible.
+  return 1024;
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/experimental_flamegraph_generator.h b/src/trace_processor/experimental_flamegraph_generator.h
new file mode 100644
index 0000000..b74fa6d
--- /dev/null
+++ b/src/trace_processor/experimental_flamegraph_generator.h
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_EXPERIMENTAL_FLAMEGRAPH_GENERATOR_H_
+#define SRC_TRACE_PROCESSOR_EXPERIMENTAL_FLAMEGRAPH_GENERATOR_H_
+
+#include "src/trace_processor/sqlite/db_sqlite_table.h"
+
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+class ExperimentalFlamegraphGenerator
+    : public DbSqliteTable::DynamicTableGenerator {
+ public:
+  struct InputValues {
+    int64_t ts;
+    UniquePid upid;
+    std::string profile_type;
+  };
+
+  ExperimentalFlamegraphGenerator(TraceProcessorContext* context);
+  virtual ~ExperimentalFlamegraphGenerator() override;
+
+  Table::Schema CreateSchema() override;
+  std::string TableName() override;
+  uint32_t EstimateRowCount() override;
+  util::Status ValidateConstraints(const QueryConstraints&) override;
+  Table* ComputeTable(const std::vector<Constraint>& cs,
+                      const std::vector<Order>& ob) override;
+
+ private:
+  std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> table_;
+  TraceProcessorContext* context_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_EXPERIMENTAL_FLAMEGRAPH_GENERATOR_H_
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.cc b/src/trace_processor/importers/proto/heap_graph_tracker.cc
index 2a88b57..d84faeb 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.cc
@@ -19,6 +19,16 @@
 namespace perfetto {
 namespace trace_processor {
 
+base::Optional<base::StringView> GetStaticClassTypeName(base::StringView type) {
+  static const base::StringView kJavaClassTemplate("java.lang.Class<");
+  if (!type.empty() && type.at(type.size() - 1) == '>' &&
+      type.substr(0, kJavaClassTemplate.size()) == kJavaClassTemplate) {
+    return type.substr(kJavaClassTemplate.size(),
+                       type.size() - kJavaClassTemplate.size() - 1);
+  }
+  return {};
+}
+
 size_t NumberOfArrays(base::StringView type) {
   if (type.size() < 2)
     return 0;
@@ -32,8 +42,30 @@
   return arrays;
 }
 
+NormalizedType GetNormalizedType(base::StringView type) {
+  auto static_class_type_name = GetStaticClassTypeName(type);
+  if (static_class_type_name.has_value()) {
+    type = static_class_type_name.value();
+  }
+  size_t number_of_arrays = NumberOfArrays(type);
+  return {base::StringView(type.data(), type.size() - number_of_arrays * 2),
+          static_class_type_name.has_value(), number_of_arrays};
+}
+
 base::StringView NormalizeTypeName(base::StringView type) {
-  return base::StringView(type.data(), type.size() - NumberOfArrays(type) * 2);
+  return GetNormalizedType(type).name;
+}
+
+std::string DenormalizeTypeName(NormalizedType normalized,
+                                base::StringView deobfuscated_type_name) {
+  std::string result = deobfuscated_type_name.ToStdString();
+  for (size_t i = 0; i < normalized.number_of_arrays; ++i) {
+    result += "[]";
+  }
+  if (normalized.is_static_class) {
+    result = "java.lang.Class<" + result + ">";
+  }
+  return result;
 }
 
 HeapGraphTracker::HeapGraphTracker(TraceProcessorContext* context)
@@ -317,26 +349,18 @@
 }
 
 StringPool::Id HeapGraphTracker::MaybeDeobfuscate(StringPool::Id id) {
-  StringPool::Id normalized_type_id = id;
   base::StringView type_name = context_->storage->GetString(id);
-  size_t array_count = NumberOfArrays(type_name);
-  if (array_count > 0) {
-    base::StringView normalized_type = NormalizeTypeName(type_name);
-    normalized_type_id = context_->storage->InternString(normalized_type);
-  }
-  auto it = deobfuscation_mapping_.find(normalized_type_id);
+  auto normalized_type = GetNormalizedType(type_name);
+  auto it = deobfuscation_mapping_.find(
+      context_->storage->InternString(normalized_type.name));
   if (it == deobfuscation_mapping_.end())
     return id;
 
-  StringPool::Id deobfuscated_name_id = it->second;
-  if (array_count == 0)
-    return deobfuscated_name_id;
-
-  std::string deobfuscated_name =
-      context_->storage->GetString(deobfuscated_name_id).ToStdString();
-  for (size_t i = 0; i < array_count; ++i)
-    deobfuscated_name += "[]";
-  return context_->storage->InternString(base::StringView(deobfuscated_name));
+  base::StringView normalized_deobfuscated_name =
+      context_->storage->GetString(it->second);
+  std::string result =
+      DenormalizeTypeName(normalized_type, normalized_deobfuscated_name);
+  return context_->storage->InternString(base::StringView(result));
 }
 
 void HeapGraphTracker::AddDeobfuscationMapping(
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker.h b/src/trace_processor/importers/proto/heap_graph_tracker.h
index 0da715e..74b2867 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker.h
+++ b/src/trace_processor/importers/proto/heap_graph_tracker.h
@@ -32,8 +32,17 @@
 
 class TraceProcessorContext;
 
+struct NormalizedType {
+  base::StringView name;
+  bool is_static_class;
+  size_t number_of_arrays;
+};
+base::Optional<base::StringView> GetStaticClassTypeName(base::StringView type);
 size_t NumberOfArrays(base::StringView type);
+NormalizedType GetNormalizedType(base::StringView type);
 base::StringView NormalizeTypeName(base::StringView type);
+std::string DenormalizeTypeName(NormalizedType normalized,
+                                base::StringView deobfuscated_type_name);
 
 class HeapGraphTracker : public HeapGraphWalker::Delegate, public Destructible {
  public:
diff --git a/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc b/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
index d00462c..7472818 100644
--- a/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
+++ b/src/trace_processor/importers/proto/heap_graph_tracker_unittest.cc
@@ -146,6 +146,8 @@
 static const char kDoubleArray[] = "X[][]";
 static const char kNoArray[] = "X";
 static const char kLongNoArray[] = "ABCDE";
+static const char kStaticClassNoArray[] = "java.lang.Class<abc>";
+static const char kStaticClassArray[] = "java.lang.Class<abc[]>";
 
 TEST(HeapGraphTrackerTest, NormalizeTypeName) {
   // sizeof(...) - 1 below to get rid of the null-byte.
@@ -163,6 +165,14 @@
                 base::StringView(kLongNoArray, sizeof(kLongNoArray) - 1))
                 .ToStdString(),
             "ABCDE");
+  EXPECT_EQ(NormalizeTypeName(base::StringView(kStaticClassNoArray,
+                                               sizeof(kStaticClassNoArray) - 1))
+                .ToStdString(),
+            "abc");
+  EXPECT_EQ(NormalizeTypeName(base::StringView(kStaticClassArray,
+                                               sizeof(kStaticClassArray) - 1))
+                .ToStdString(),
+            "abc");
 }
 
 TEST(HeapGraphTrackerTest, NumberOfArray) {
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 98abb9b..20c2603 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -161,6 +161,8 @@
       flow_direction_value_inout_id_(context->storage->InternString("inout")),
       chrome_user_event_action_args_key_id_(
           context->storage->InternString("user_event.action")),
+      chrome_user_event_action_hash_args_key_id_(
+          context->storage->InternString("user_event.action_hash")),
       chrome_legacy_ipc_class_args_key_id_(
           context->storage->InternString("legacy_ipc.class")),
       chrome_legacy_ipc_line_args_key_id_(
@@ -175,6 +177,20 @@
           context->storage->InternString("histogram_sample.sample")),
       chrome_latency_info_trace_id_key_id_(
           context->storage->InternString("latency_info.trace_id")),
+      chrome_latency_info_step_key_id_(
+          context->storage->InternString("latency_info.step")),
+      chrome_latency_info_frame_tree_node_id_key_id_(
+          context->storage->InternString("latency_info.frame_tree_node_id")),
+      chrome_latency_info_step_ids_{
+          {context->storage->InternString("STEP_UNSPECIFIED"),
+           context->storage->InternString(
+               "STEP_HANDLE_INPUT_EVENT_MAIN_COMMIT"),
+           context->storage->InternString("STEP_MAIN_THREAD_SCROLL_UPDATE"),
+           context->storage->InternString("STEP_SEND_INPUT_EVENT_UI"),
+           context->storage->InternString("STEP_HANDLE_INPUT_EVENT_MAIN"),
+           context->storage->InternString("STEP_HANDLE_INPUT_EVENT_IMPL"),
+           context->storage->InternString("STEP_SWAP_BUFFERS"),
+           context->storage->InternString("STEP_DRAW_AND_SWAP")}},
       chrome_legacy_ipc_class_ids_{
           {context->storage->InternString("UNSPECIFIED"),
            context->storage->InternString("AUTOMATION"),
@@ -1314,6 +1330,10 @@
     inserter->AddArg(chrome_user_event_action_args_key_id_,
                      Variadic::String(action_id));
   }
+  if (event.has_action_hash()) {
+    inserter->AddArg(chrome_user_event_action_hash_args_key_id_,
+                     Variadic::UnsignedInteger(event.action_hash()));
+  }
 }
 
 void TrackEventParser::ParseChromeLegacyIpc(
@@ -1359,6 +1379,18 @@
     inserter->AddArg(chrome_latency_info_trace_id_key_id_,
                      Variadic::Integer(event.trace_id()));
   }
+  if (event.has_step()) {
+    size_t step_index = static_cast<size_t>(event.step());
+    if (step_index >= chrome_latency_info_step_ids_.size())
+      step_index = 0;
+    inserter->AddArg(
+        chrome_latency_info_step_key_id_,
+        Variadic::String(chrome_latency_info_step_ids_[step_index]));
+  }
+  if (event.has_frame_tree_node_id()) {
+    inserter->AddArg(chrome_latency_info_frame_tree_node_id_key_id_,
+                     Variadic::Integer(event.frame_tree_node_id()));
+  }
 }
 
 void TrackEventParser::ParseChromeHistogramSample(
diff --git a/src/trace_processor/importers/proto/track_event_parser.h b/src/trace_processor/importers/proto/track_event_parser.h
index 8cca814..ae9c3af 100644
--- a/src/trace_processor/importers/proto/track_event_parser.h
+++ b/src/trace_processor/importers/proto/track_event_parser.h
@@ -131,6 +131,7 @@
   const StringId flow_direction_value_out_id_;
   const StringId flow_direction_value_inout_id_;
   const StringId chrome_user_event_action_args_key_id_;
+  const StringId chrome_user_event_action_hash_args_key_id_;
   const StringId chrome_legacy_ipc_class_args_key_id_;
   const StringId chrome_legacy_ipc_line_args_key_id_;
   const StringId chrome_keyed_service_name_args_key_id_;
@@ -138,7 +139,10 @@
   const StringId chrome_histogram_sample_name_args_key_id_;
   const StringId chrome_histogram_sample_sample_args_key_id_;
   const StringId chrome_latency_info_trace_id_key_id_;
+  const StringId chrome_latency_info_step_key_id_;
+  const StringId chrome_latency_info_frame_tree_node_id_key_id_;
 
+  std::array<StringId, 8> chrome_latency_info_step_ids_;
   std::array<StringId, 38> chrome_legacy_ipc_class_ids_;
   std::array<StringId, 9> chrome_process_name_ids_;
   std::array<StringId, 14> chrome_thread_name_ids_;
diff --git a/src/trace_processor/importers/systrace/systrace_parser.h b/src/trace_processor/importers/systrace/systrace_parser.h
index c0133a5..31edbf9 100644
--- a/src/trace_processor/importers/systrace/systrace_parser.h
+++ b/src/trace_processor/importers/systrace/systrace_parser.h
@@ -48,8 +48,10 @@
 
   static SystraceTracePoint C(uint32_t tgid,
                               base::StringView name,
-                              double value) {
-    return SystraceTracePoint('C', tgid, std::move(name), value);
+                              double value,
+                              base::StringView category_group = "") {
+    return SystraceTracePoint('C', tgid, std::move(name), value,
+                              category_group);
   }
 
   static SystraceTracePoint S(uint32_t tgid,
@@ -64,8 +66,12 @@
     return SystraceTracePoint('F', tgid, std::move(name), value);
   }
 
-  SystraceTracePoint(char p, uint32_t tg, base::StringView n, double v)
-      : phase(p), tgid(tg), name(std::move(n)), value(v) {}
+  SystraceTracePoint(char p,
+                     uint32_t tg,
+                     base::StringView n,
+                     double v,
+                     base::StringView c = "")
+      : phase(p), tgid(tg), name(std::move(n)), value(v), category_group(c) {}
 
   // Phase can be one of B, E, C, S, F.
   char phase = '\0';
@@ -78,6 +84,9 @@
   // For phase = 'C' only.
   double value = 0;
 
+  // For phase = 'C' only (from Chrome).
+  base::StringView category_group;
+
   // Visible for unittesting.
   friend std::ostream& operator<<(std::ostream& os,
                                   const SystraceTracePoint& point) {
@@ -94,6 +103,7 @@
 // 4. C|1636|wq:monitor|0
 // 5. S|1636|frame_capture|123
 // 6. F|1636|frame_capture|456
+// 7. C|3209|TransfersBytesPendingOnDisk-value|0|Blob
 // Visible for unittesting.
 inline SystraceParseResult ParseSystraceTracePoint(base::StringView str,
                                                    SystraceTracePoint* out) {
@@ -160,10 +170,13 @@
       out->name = base::StringView(s + name_index, name_length.value());
 
       size_t value_index = name_index + name_length.value() + 1;
-      size_t value_len = len - value_index;
+      size_t value_pipe = str.find('|', value_index);
+      size_t value_len = value_pipe == base::StringView::npos
+                             ? len - value_index
+                             : value_pipe - value_index;
       if (value_len == 0)
         return SystraceParseResult::kFailure;
-      if (*(s + value_index + value_len - 1) == '\n')
+      if (s[value_index + value_len - 1] == '\n')
         value_len--;
       std::string value_str(s + value_index, value_len);
       base::Optional<double> maybe_value = base::StringToDouble(value_str);
@@ -171,6 +184,16 @@
         return SystraceParseResult::kFailure;
       }
       out->value = maybe_value.value();
+
+      if (value_pipe != base::StringView::npos) {
+        size_t group_len = len - value_pipe - 1;
+        if (group_len == 0)
+          return SystraceParseResult::kFailure;
+        if (s[len - 1] == '\n')
+          group_len--;
+        out->category_group = base::StringView(s + value_pipe + 1, group_len);
+      }
+
       return SystraceParseResult::kSuccess;
     }
     default:
diff --git a/src/trace_processor/importers/systrace/systrace_parser_unittest.cc b/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
index 55d42cc..2c51906 100644
--- a/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
+++ b/src/trace_processor/importers/systrace/systrace_parser_unittest.cc
@@ -60,6 +60,11 @@
   ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8", &result), Result::kSuccess);
   EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8));
 
+  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8|", &result), Result::kFailure);
+  ASSERT_EQ(ParseSystraceTracePoint("C|543|foo|8|group", &result),
+            Result::kSuccess);
+  EXPECT_EQ(result, SystraceTracePoint::C(543, "foo", 8, "group"));
+
   ASSERT_EQ(ParseSystraceTracePoint("S|", &result), Result::kFailure);
 
   ASSERT_EQ(ParseSystraceTracePoint("S|123|foo|456", &result),
diff --git a/src/trace_processor/metrics/android/unmapped_java_symbols.sql b/src/trace_processor/metrics/android/unmapped_java_symbols.sql
index b7d4560..0fc8443 100644
--- a/src/trace_processor/metrics/android/unmapped_java_symbols.sql
+++ b/src/trace_processor/metrics/android/unmapped_java_symbols.sql
@@ -20,7 +20,9 @@
 WITH distinct_unmapped_type_names AS (
   SELECT DISTINCT upid, type_name
   FROM (
-    SELECT upid, RTRIM(type_name, '[]') AS type_name
+    SELECT
+      upid,
+      RTRIM(REPLACE(type_name, 'java.lang.Class<', ''), '[]>') AS type_name
     FROM heap_graph_object
     WHERE deobfuscated_type_name IS NULL
   )
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index a98e85d..d4094a9 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -86,7 +86,9 @@
 DbSqliteTable::DbSqliteTable(sqlite3*, Context context)
     : cache_(context.cache),
       schema_(std::move(context.schema)),
-      table_(context.table) {}
+      computation_(context.computation),
+      static_table_(context.static_table),
+      generator_(std::move(context.generator)) {}
 DbSqliteTable::~DbSqliteTable() = default;
 
 void DbSqliteTable::RegisterTable(sqlite3* db,
@@ -94,8 +96,19 @@
                                   Table::Schema schema,
                                   const Table* table,
                                   const std::string& name) {
-  SqliteTable::Register<DbSqliteTable, Context>(
-      db, Context{cache, schema, table}, name);
+  Context context{cache, schema, TableComputation::kStatic, table, nullptr};
+  SqliteTable::Register<DbSqliteTable, Context>(db, std::move(context), name);
+}
+
+void DbSqliteTable::RegisterTable(
+    sqlite3* db,
+    QueryCache* cache,
+    std::unique_ptr<DynamicTableGenerator> generator) {
+  Table::Schema schema = generator->CreateSchema();
+  std::string name = generator->TableName();
+  Context context{cache, std::move(schema), TableComputation::kDynamic, nullptr,
+                  std::move(generator)};
+  SqliteTable::Register<DbSqliteTable, Context>(db, std::move(context), name);
 }
 
 util::Status DbSqliteTable::Init(int, const char* const*, Schema* schema) {
@@ -108,7 +121,7 @@
   std::vector<SqliteTable::Column> schema_cols;
   for (uint32_t i = 0; i < schema.columns.size(); ++i) {
     const auto& col = schema.columns[i];
-    schema_cols.emplace_back(i, col.name, col.type);
+    schema_cols.emplace_back(i, col.name, col.type, col.is_hidden);
   }
 
   // TODO(lalitm): this is hardcoded to be the id column but change this to be
@@ -129,7 +142,17 @@
 }
 
 int DbSqliteTable::BestIndex(const QueryConstraints& qc, BestIndexInfo* info) {
-  BestIndex(schema_, table_->row_count(), qc, info);
+  switch (computation_) {
+    case TableComputation::kStatic:
+      BestIndex(schema_, static_table_->row_count(), qc, info);
+      break;
+    case TableComputation::kDynamic:
+      util::Status status = generator_->ValidateConstraints(qc);
+      if (!status.ok())
+        return SQLITE_CONSTRAINT;
+      BestIndex(schema_, generator_->EstimateRowCount(), qc, info);
+      break;
+  }
   return SQLITE_OK;
 }
 
@@ -291,15 +314,13 @@
 }
 
 std::unique_ptr<SqliteTable::Cursor> DbSqliteTable::CreateCursor() {
-  return std::unique_ptr<Cursor>(new Cursor(this, cache_, table_));
+  return std::unique_ptr<Cursor>(new Cursor(this, cache_));
 }
 
-DbSqliteTable::Cursor::Cursor(SqliteTable* sqlite_table,
-                              QueryCache* cache,
-                              const Table* table)
+DbSqliteTable::Cursor::Cursor(DbSqliteTable* sqlite_table, QueryCache* cache)
     : SqliteTable::Cursor(sqlite_table),
-      cache_(cache),
-      initial_db_table_(table) {}
+      db_sqlite_table_(sqlite_table),
+      cache_(cache) {}
 
 void DbSqliteTable::Cursor::TryCacheCreateSortedTable(
     const QueryConstraints& qc,
@@ -314,7 +335,7 @@
 
     // Check if the new constraint set is cached by another cursor.
     sorted_cache_table_ =
-        cache_->GetIfCached(initial_db_table_, qc.constraints());
+        cache_->GetIfCached(upstream_table_, qc.constraints());
     return;
   }
 
@@ -343,13 +364,13 @@
 
   // If the column is already sorted, we don't need to cache at all.
   uint32_t col = static_cast<uint32_t>(c.column);
-  if (initial_db_table_->GetColumn(col).IsSorted())
+  if (upstream_table_->GetColumn(col).IsSorted())
     return;
 
   // Try again to get the result or start caching it.
   sorted_cache_table_ =
-      cache_->GetOrCache(initial_db_table_, qc.constraints(), [this, col]() {
-        return initial_db_table_->Sort({Order{col, false}});
+      cache_->GetOrCache(upstream_table_, qc.constraints(), [this, col]() {
+        return upstream_table_->Sort({Order{col, false}});
       });
 }
 
@@ -386,6 +407,23 @@
     orders_[i] = Order{col, static_cast<bool>(ob.desc)};
   }
 
+  // Setup the upstream table based on the computation state.
+  switch (db_sqlite_table_->computation_) {
+    case TableComputation::kStatic:
+      // If we have a static table, just set the upstream table to be the static
+      // table.
+      upstream_table_ = db_sqlite_table_->static_table_;
+      break;
+    case TableComputation::kDynamic:
+      // If we have a dynamically created table, regenerate the table based on
+      // the new constraints.
+      upstream_table_ =
+          db_sqlite_table_->generator_->ComputeTable(constraints_, orders_);
+      if (!upstream_table_)
+        return SQLITE_CONSTRAINT;
+      break;
+  }
+
   // Tries to create a sorted cached table which can be used to speed up
   // filters below.
   TryCacheCreateSortedTable(qc, history);
@@ -482,5 +520,7 @@
   return SQLITE_OK;
 }
 
+DbSqliteTable::DynamicTableGenerator::~DynamicTableGenerator() = default;
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/sqlite/db_sqlite_table.h b/src/trace_processor/sqlite/db_sqlite_table.h
index a020008..cfb5003 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.h
+++ b/src/trace_processor/sqlite/db_sqlite_table.h
@@ -27,9 +27,54 @@
 // Implements the SQLite table interface for db tables.
 class DbSqliteTable : public SqliteTable {
  public:
+  enum class TableComputation {
+    // Mode when the table is static (i.e. passed in at construction
+    // time).
+    kStatic,
+
+    // Mode when table is dynamically computed at filter time.
+    kDynamic,
+  };
+
+  // Interface which can be subclassed to allow generation of tables dynamically
+  // at filter time.
+  // This class is used to implement table-valued functions and other similar
+  // tables.
+  class DynamicTableGenerator {
+   public:
+    virtual ~DynamicTableGenerator();
+
+    // Returns the schema of the table that will be returned by ComputeTable.
+    virtual Table::Schema CreateSchema() = 0;
+
+    // Returns the name of the dynamic table.
+    // This will be used to register the table with SQLite.
+    virtual std::string TableName() = 0;
+
+    // Returns the estimated number of rows the table would generate.
+    virtual uint32_t EstimateRowCount() = 0;
+
+    // Checks that the constraint set is valid.
+    //
+    // Returning util::OkStatus means that the required constraints are present
+    // in |qc| for dynamically computing the table (e.g. any required
+    // constraints on hidden columns for table-valued functions are present).
+    virtual util::Status ValidateConstraints(const QueryConstraints& qc) = 0;
+
+    // Dynamically computes the table given the constraints and order by
+    // vectors.
+    //
+    // Implementations should store the generated table inside a stable pointer
+    // (e.g. unique_ptr, shared_ptr or similar) and return the pointer to that
+    // object. The pointer should not be freed until a successive call to
+    // |ComputeTable|.
+    virtual Table* ComputeTable(const std::vector<Constraint>& cs,
+                                const std::vector<Order>& ob) = 0;
+  };
+
   class Cursor : public SqliteTable::Cursor {
    public:
-    Cursor(SqliteTable*, QueryCache*, const Table*);
+    Cursor(DbSqliteTable*, QueryCache*);
 
     Cursor(Cursor&&) noexcept = default;
     Cursor& operator=(Cursor&&) = default;
@@ -42,12 +87,6 @@
     int Eof() override;
     int Column(sqlite3_context*, int N) override;
 
-   protected:
-    // Sets the table this class uses as the reference for all filter
-    // operations. Should be immediately followed by a call to Filter with
-    // |FilterHistory::kDifferent|.
-    void set_table(const Table* table) { initial_db_table_ = table; }
-
    private:
     enum class Mode {
       kSingleRow,
@@ -61,14 +100,16 @@
     const Table* SourceTable() const {
       // Try and use the sorted cache table (if it exists) to speed up the
       // sorting. Otherwise, just use the original table.
-      return sorted_cache_table_ ? &*sorted_cache_table_ : initial_db_table_;
+      return sorted_cache_table_ ? &*sorted_cache_table_ : upstream_table_;
     }
 
     Cursor(const Cursor&) = delete;
     Cursor& operator=(const Cursor&) = delete;
 
+    DbSqliteTable* db_sqlite_table_ = nullptr;
     QueryCache* cache_ = nullptr;
-    const Table* initial_db_table_ = nullptr;
+
+    const Table* upstream_table_ = nullptr;
 
     // Only valid for Mode::kSingleRow.
     base::Optional<uint32_t> single_row_;
@@ -100,7 +141,13 @@
   struct Context {
     QueryCache* cache;
     Table::Schema schema;
-    const Table* table;
+    TableComputation computation;
+
+    // Only valid when computation == TableComputation::kStatic.
+    const Table* static_table;
+
+    // Only valid when computation == TableComputation::kDynamic.
+    std::unique_ptr<DynamicTableGenerator> generator;
   };
 
   static void RegisterTable(sqlite3* db,
@@ -109,6 +156,10 @@
                             const Table* table,
                             const std::string& name);
 
+  static void RegisterTable(sqlite3* db,
+                            QueryCache* cache,
+                            std::unique_ptr<DynamicTableGenerator> generator);
+
   DbSqliteTable(sqlite3*, Context context);
   virtual ~DbSqliteTable() override;
 
@@ -138,7 +189,14 @@
  private:
   QueryCache* cache_ = nullptr;
   Table::Schema schema_;
-  const Table* table_ = nullptr;
+
+  TableComputation computation_ = TableComputation::kStatic;
+
+  // Only valid when computation_ == TableComputation::kStatic.
+  const Table* static_table_ = nullptr;
+
+  // Only valid when computation_ == TableComputation::kDynamic.
+  std::unique_ptr<DynamicTableGenerator> generator_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/sqlite/db_sqlite_table_unittest.cc b/src/trace_processor/sqlite/db_sqlite_table_unittest.cc
index ace779f..78b6961 100644
--- a/src/trace_processor/sqlite/db_sqlite_table_unittest.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table_unittest.cc
@@ -1,3 +1,4 @@
+
 /*
  * Copyright (C) 2019 The Android Open Source Project
  *
@@ -24,16 +25,16 @@
 
 Table::Schema CreateSchema() {
   Table::Schema schema;
-  schema.columns.push_back(
-      {"id", SqlValue::Type::kLong, true /* is_id */, true /* is_sorted */});
+  schema.columns.push_back({"id", SqlValue::Type::kLong, true /* is_id */,
+                            true /* is_sorted */, false /* is_hidden */});
   schema.columns.push_back({"type", SqlValue::Type::kLong, false /* is_id */,
-                            false /* is_sorted */});
+                            false /* is_sorted */, false /* is_hidden */});
   schema.columns.push_back({"test1", SqlValue::Type::kLong, false /* is_id */,
-                            true /* is_sorted */});
+                            true /* is_sorted */, false /* is_hidden */});
   schema.columns.push_back({"test2", SqlValue::Type::kLong, false /* is_id */,
-                            false /* is_sorted */});
+                            false /* is_sorted */, false /* is_hidden */});
   schema.columns.push_back({"test3", SqlValue::Type::kLong, false /* is_id */,
-                            false /* is_sorted */});
+                            false /* is_sorted */, false /* is_hidden */});
   return schema;
 }
 
diff --git a/src/trace_processor/sqlite/sqlite_table.h b/src/trace_processor/sqlite/sqlite_table.h
index 2cb7a0f..a6a038f 100644
--- a/src/trace_processor/sqlite/sqlite_table.h
+++ b/src/trace_processor/sqlite/sqlite_table.h
@@ -20,6 +20,7 @@
 #include <sqlite3.h>
 
 #include <functional>
+#include <limits>
 #include <memory>
 #include <string>
 #include <vector>
@@ -212,7 +213,7 @@
     auto create_fn = [](sqlite3* xdb, void* arg, int argc,
                         const char* const* argv, sqlite3_vtab** tab,
                         char** pzErr) {
-      const auto* xdesc = static_cast<const TableDescriptor<Context>*>(arg);
+      auto* xdesc = static_cast<TableDescriptor<Context>*>(arg);
       auto table = xdesc->factory(xdb, std::move(xdesc->context));
       table->name_ = xdesc->name;
 
diff --git a/src/trace_processor/sqlite_experimental_counter_dur_table.cc b/src/trace_processor/sqlite_experimental_counter_dur_table.cc
deleted file mode 100644
index bf5cf7b..0000000
--- a/src/trace_processor/sqlite_experimental_counter_dur_table.cc
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.
- */
-
-#include "src/trace_processor/sqlite_experimental_counter_dur_table.h"
-
-#include "src/trace_processor/trace_processor_context.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-SqliteExperimentalCounterDurTable::SqliteExperimentalCounterDurTable(
-    sqlite3* db,
-    Context context)
-    : DbSqliteTable(db,
-                    {context.cache, std::move(context.schema), context.table}),
-      cache_(context.cache),
-      counter_table_(context.table) {}
-
-SqliteExperimentalCounterDurTable::~SqliteExperimentalCounterDurTable() =
-    default;
-
-void SqliteExperimentalCounterDurTable::RegisterTable(
-    sqlite3* db,
-    QueryCache* cache,
-    const tables::CounterTable& table) {
-  // Add the dur column to the counter schema and use that as the base schema.
-  auto schema = tables::CounterTable::Schema();
-  schema.columns.push_back(
-      {"dur", SqlValue::Type::kLong, false /* is_id */, false /* is_sorted */});
-
-  SqliteTable::Register<SqliteExperimentalCounterDurTable>(
-      db, Context{cache, std::move(schema), &table},
-      "experimental_counter_dur");
-}
-
-std::unique_ptr<SqliteTable::Cursor>
-SqliteExperimentalCounterDurTable::CreateCursor() {
-  std::unique_ptr<TableAndColumn> table_and_column(new TableAndColumn());
-  table_and_column->dur = ComputeDurColumn(*counter_table_);
-  table_and_column->table = counter_table_->ExtendWithColumn(
-      "dur", &table_and_column->dur, TypedColumn<int64_t>::default_flags());
-  return std::unique_ptr<Cursor>(
-      new Cursor(this, cache_, std::move(table_and_column)));
-}
-
-// static
-SparseVector<int64_t> SqliteExperimentalCounterDurTable::ComputeDurColumn(
-    const tables::CounterTable& table) {
-  // Keep track of the last seen row for each track id.
-  std::unordered_map<TrackId, uint32_t> last_row_for_track_id;
-  SparseVector<int64_t> dur;
-  for (uint32_t i = 0; i < table.row_count(); ++i) {
-    // Check if we already have a previous row for the current track id.
-    TrackId track_id = table.track_id()[i];
-    auto it = last_row_for_track_id.find(track_id);
-    if (it == last_row_for_track_id.end()) {
-      // This means we don't have any row - start tracking this row for the
-      // future.
-      last_row_for_track_id.emplace(track_id, i);
-    } else {
-      // This means we have an previous row for the current track id. Update
-      // the duration of the previous row to be up to the current ts.
-      uint32_t old_row = it->second;
-      it->second = i;
-      dur.Set(old_row, table.ts()[i] - table.ts()[old_row]);
-    }
-    // Append -1 to make this event as not having been finished. On a later
-    // row, we may set this to have the correct value.
-    dur.Append(-1);
-  }
-  return dur;
-}
-
-SqliteExperimentalCounterDurTable::Cursor::Cursor(
-    SqliteTable* sqlite_table,
-    QueryCache* cache,
-    std::unique_ptr<TableAndColumn> table_and_column)
-    : DbSqliteTable::Cursor(sqlite_table, cache, &table_and_column->table),
-      table_and_column_(std::move(table_and_column)) {}
-
-SqliteExperimentalCounterDurTable::Cursor::~Cursor() = default;
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/sqlite_experimental_counter_dur_table.h b/src/trace_processor/sqlite_experimental_counter_dur_table.h
deleted file mode 100644
index b38201b..0000000
--- a/src/trace_processor/sqlite_experimental_counter_dur_table.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_SQLITE_EXPERIMENTAL_COUNTER_DUR_TABLE_H_
-#define SRC_TRACE_PROCESSOR_SQLITE_EXPERIMENTAL_COUNTER_DUR_TABLE_H_
-
-#include "src/trace_processor/sqlite/db_sqlite_table.h"
-
-#include "src/trace_processor/storage/trace_storage.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-class SqliteExperimentalCounterDurTable : public DbSqliteTable {
- public:
-  struct Context {
-    QueryCache* cache;
-    Table::Schema schema;
-    const tables::CounterTable* table;
-  };
-
-  struct TableAndColumn {
-    Table table;
-    SparseVector<int64_t> dur;
-  };
-
-  class Cursor : public DbSqliteTable::Cursor {
-   public:
-    Cursor(SqliteTable*,
-           QueryCache* cache,
-           std::unique_ptr<TableAndColumn> table_and_column);
-    ~Cursor() override;
-
-   private:
-    std::unique_ptr<TableAndColumn> table_and_column_;
-  };
-
-  SqliteExperimentalCounterDurTable(sqlite3*, Context context);
-  ~SqliteExperimentalCounterDurTable() override;
-
-  static void RegisterTable(sqlite3* db,
-                            QueryCache* cache,
-                            const tables::CounterTable& table);
-
-  // SqliteTable implementation.
-  std::unique_ptr<SqliteTable::Cursor> CreateCursor() override;
-
-  static SparseVector<int64_t> ComputeDurColumn(
-      const tables::CounterTable& table);
-
- private:
-  QueryCache* cache_ = nullptr;
-  const tables::CounterTable* counter_table_ = nullptr;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_SQLITE_EXPERIMENTAL_COUNTER_DUR_TABLE_H_
diff --git a/src/trace_processor/sqlite_experimental_flamegraph_table.cc b/src/trace_processor/sqlite_experimental_flamegraph_table.cc
deleted file mode 100644
index 29364ce..0000000
--- a/src/trace_processor/sqlite_experimental_flamegraph_table.cc
+++ /dev/null
@@ -1,179 +0,0 @@
-
-/*
- * 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.
- */
-
-#include "src/trace_processor/sqlite_experimental_flamegraph_table.h"
-
-#include "src/trace_processor/heap_profile_tracker.h"
-#include "src/trace_processor/importers/proto/heap_graph_tracker.h"
-#include "src/trace_processor/trace_processor_context.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-namespace {
-
-SqliteExperimentalFlamegraphTable::InputValues GetInputValues(
-    const QueryConstraints& qc,
-    sqlite3_value** argv) {
-  using T = tables::ExperimentalFlamegraphNodesTable;
-
-  const auto& cs = qc.constraints();
-
-  auto ts_fn = [](const QueryConstraints::Constraint& c) {
-    return c.column == static_cast<int>(T::ColumnIndex::ts) &&
-           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
-  };
-  auto upid_fn = [](const QueryConstraints::Constraint& c) {
-    return c.column == static_cast<int>(T::ColumnIndex::upid) &&
-           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
-  };
-  auto profile_type_fn = [](const QueryConstraints::Constraint& c) {
-    return c.column == static_cast<int>(T::ColumnIndex::profile_type) &&
-           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
-  };
-
-  auto ts_idx = static_cast<uint32_t>(
-      std::distance(cs.begin(), std::find_if(cs.begin(), cs.end(), ts_fn)));
-  auto upid_idx = static_cast<uint32_t>(
-      std::distance(cs.begin(), std::find_if(cs.begin(), cs.end(), upid_fn)));
-  auto profile_type_idx = static_cast<uint32_t>(std::distance(
-      cs.begin(), std::find_if(cs.begin(), cs.end(), profile_type_fn)));
-
-  // We should always have valid indices here because BestIndex should only
-  // allow the constraint set to be chosen when we have an equality constraint
-  // on both ts and upid.
-  PERFETTO_CHECK(ts_idx < cs.size());
-  PERFETTO_CHECK(upid_idx < cs.size());
-  PERFETTO_CHECK(profile_type_idx < cs.size());
-
-  int64_t ts = sqlite3_value_int64(argv[ts_idx]);
-  UniquePid upid = static_cast<UniquePid>(sqlite3_value_int64(argv[upid_idx]));
-  std::string profile_type =
-      reinterpret_cast<const char*>(sqlite3_value_text(argv[profile_type_idx]));
-
-  return SqliteExperimentalFlamegraphTable::InputValues{ts, upid, profile_type};
-}
-
-}  // namespace
-
-SqliteExperimentalFlamegraphTable::SqliteExperimentalFlamegraphTable(
-    sqlite3*,
-    TraceProcessorContext* context)
-    : context_(context) {}
-
-SqliteExperimentalFlamegraphTable::~SqliteExperimentalFlamegraphTable() =
-    default;
-
-void SqliteExperimentalFlamegraphTable::RegisterTable(
-    sqlite3* db,
-    TraceProcessorContext* context) {
-  SqliteTable::Register<SqliteExperimentalFlamegraphTable>(
-      db, context, "experimental_flamegraph");
-}
-
-util::Status SqliteExperimentalFlamegraphTable::Init(
-    int,
-    const char* const*,
-    SqliteTable::Schema* schema) {
-  // Create an empty table for the sake of getting the schema.
-  *schema = DbSqliteTable::ComputeSchema(
-      tables::ExperimentalFlamegraphNodesTable::Schema(), name().c_str());
-
-  using T = tables::ExperimentalFlamegraphNodesTable;
-
-  // TODO(lalitm): make it so that this happens on the macro table itself.
-  auto& cols = *schema->mutable_columns();
-  cols[static_cast<uint32_t>(T::ColumnIndex::ts)].set_hidden(true);
-  cols[static_cast<uint32_t>(T::ColumnIndex::upid)].set_hidden(true);
-  cols[static_cast<uint32_t>(T::ColumnIndex::profile_type)].set_hidden(true);
-
-  return util::OkStatus();
-}
-
-int SqliteExperimentalFlamegraphTable::BestIndex(const QueryConstraints& qc,
-                                                 BestIndexInfo*) {
-  using T = tables::ExperimentalFlamegraphNodesTable;
-
-  const auto& cs = qc.constraints();
-
-  auto ts_fn = [](const QueryConstraints::Constraint& c) {
-    return c.column == static_cast<int>(T::ColumnIndex::ts) &&
-           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
-  };
-  bool has_ts_cs = std::find_if(cs.begin(), cs.end(), ts_fn) != cs.end();
-
-  auto upid_fn = [](const QueryConstraints::Constraint& c) {
-    return c.column == static_cast<int>(T::ColumnIndex::upid) &&
-           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
-  };
-  bool has_upid_cs = std::find_if(cs.begin(), cs.end(), upid_fn) != cs.end();
-
-  auto profile_type_fn = [](const QueryConstraints::Constraint& c) {
-    return c.column == static_cast<int>(T::ColumnIndex::profile_type) &&
-           c.op == SQLITE_INDEX_CONSTRAINT_EQ;
-  };
-  bool has_profile_type_cs =
-      std::find_if(cs.begin(), cs.end(), profile_type_fn) != cs.end();
-
-  return has_ts_cs && has_upid_cs && has_profile_type_cs ? SQLITE_OK
-                                                         : SQLITE_CONSTRAINT;
-}
-
-std::unique_ptr<SqliteTable::Cursor>
-SqliteExperimentalFlamegraphTable::CreateCursor() {
-  return std::unique_ptr<Cursor>(new Cursor(this, context_));
-}
-
-SqliteExperimentalFlamegraphTable::Cursor::Cursor(
-    SqliteTable* sqlite_table,
-    TraceProcessorContext* context)
-    : DbSqliteTable::Cursor(sqlite_table, nullptr, nullptr),
-      context_(context) {}
-
-int SqliteExperimentalFlamegraphTable::Cursor::Filter(
-    const QueryConstraints& qc,
-    sqlite3_value** argv,
-    FilterHistory) {
-  // Extract the old table to free after we call the parent Filter function.
-  // We need to do this to make sure that we don't get a use-after-free for
-  // any pointers the parent is holding onto in this table.
-  auto old_table = std::move(table_);
-
-  // Get the input column values and compute the flamegraph using them.
-  values_ = GetInputValues(qc, argv);
-
-  if (values_.profile_type == "graph") {
-    auto* tracker = HeapGraphTracker::GetOrCreate(context_);
-    table_ = tracker->BuildFlamegraph(values_.ts, values_.upid);
-  }
-  if (values_.profile_type == "native") {
-    table_ = BuildNativeFlamegraph(context_->storage.get(),
-                                   values_.upid, values_.ts);
-  }
-
-  // table_ can be nullptr precisely where the constraints passed to us don't
-  // make sense. Therefore, we can just return this to SQLite.
-  if (!table_)
-    return SQLITE_CONSTRAINT;
-
-  // Set the table in the parent to the correct value and then filter.
-  DbSqliteTable::Cursor::set_table(table_.get());
-  return DbSqliteTable::Cursor::Filter(qc, argv, FilterHistory::kDifferent);
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/sqlite_experimental_flamegraph_table.h b/src/trace_processor/sqlite_experimental_flamegraph_table.h
deleted file mode 100644
index 5bd9731..0000000
--- a/src/trace_processor/sqlite_experimental_flamegraph_table.h
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_SQLITE_EXPERIMENTAL_FLAMEGRAPH_TABLE_H_
-#define SRC_TRACE_PROCESSOR_SQLITE_EXPERIMENTAL_FLAMEGRAPH_TABLE_H_
-
-#include "src/trace_processor/sqlite/db_sqlite_table.h"
-
-#include "src/trace_processor/storage/trace_storage.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-class TraceProcessorContext;
-
-class SqliteExperimentalFlamegraphTable : public SqliteTable {
- public:
-  struct InputValues {
-    int64_t ts;
-    UniquePid upid;
-    std::string profile_type;
-  };
-
-  class Cursor : public DbSqliteTable::Cursor {
-   public:
-    Cursor(SqliteTable*, TraceProcessorContext*);
-
-    int Filter(const QueryConstraints& qc,
-               sqlite3_value** argv,
-               FilterHistory) override;
-
-   private:
-    TraceProcessorContext* context_ = nullptr;
-
-    std::unique_ptr<Table> table_;
-    InputValues values_ = {};
-  };
-
-  SqliteExperimentalFlamegraphTable(sqlite3*, TraceProcessorContext*);
-  ~SqliteExperimentalFlamegraphTable() override;
-
-  static void RegisterTable(sqlite3* db, TraceProcessorContext* storage);
-
-  // SqliteTable implementation.
-  util::Status Init(int,
-                    const char* const*,
-                    SqliteTable::Schema*) override final;
-  std::unique_ptr<SqliteTable::Cursor> CreateCursor() override;
-  int BestIndex(const QueryConstraints&, BestIndexInfo*) override;
-
- private:
-  friend class Cursor;
-
-  TraceProcessorContext* context_;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_SQLITE_EXPERIMENTAL_FLAMEGRAPH_TABLE_H_
diff --git a/src/trace_processor/sqlite_raw_table.cc b/src/trace_processor/sqlite_raw_table.cc
index 1bf2ead..0997657 100644
--- a/src/trace_processor/sqlite_raw_table.cc
+++ b/src/trace_processor/sqlite_raw_table.cc
@@ -53,9 +53,10 @@
 }  // namespace
 
 SqliteRawTable::SqliteRawTable(sqlite3* db, Context context)
-    : DbSqliteTable(db,
-                    {context.cache, tables::RawTable::Schema(),
-                     &context.storage->raw_table()}),
+    : DbSqliteTable(
+          db,
+          {context.cache, tables::RawTable::Schema(), TableComputation::kStatic,
+           &context.storage->raw_table(), nullptr}),
       storage_(context.storage) {
   auto fn = [](sqlite3_context* ctx, int argc, sqlite3_value** argv) {
     auto* thiz = static_cast<SqliteRawTable*>(sqlite3_user_data(ctx));
diff --git a/src/trace_processor/tables/macros_internal.h b/src/trace_processor/tables/macros_internal.h
index e521e4a..60b41c7 100644
--- a/src/trace_processor/tables/macros_internal.h
+++ b/src/trace_processor/tables/macros_internal.h
@@ -234,7 +234,9 @@
   schema.columns.emplace_back(Table::Schema::Column{        \
       #name, TypedColumn<type>::SqlValueType(), false,      \
       static_cast<bool>(FlagsForColumn(ColumnIndex::name) & \
-                        Column::Flag::kSorted)});
+                        Column::Flag::kSorted),             \
+      static_cast<bool>(FlagsForColumn(ColumnIndex::name) & \
+                        Column::Flag::kHidden)});
 
 // Defines the accessors for a column.
 #define PERFETTO_TP_TABLE_COL_ACCESSOR(type, name, ...)       \
@@ -374,10 +376,10 @@
                                                                               \
     static Table::Schema Schema() {                                           \
       Table::Schema schema;                                                   \
-      schema.columns.emplace_back(                                            \
-          Table::Schema::Column{"id", SqlValue::Type::kLong, true, true});    \
       schema.columns.emplace_back(Table::Schema::Column{                      \
-          "type", SqlValue::Type::kString, false, false});                    \
+          "id", SqlValue::Type::kLong, true, true, false});                   \
+      schema.columns.emplace_back(Table::Schema::Column{                      \
+          "type", SqlValue::Type::kString, false, false, false});             \
       PERFETTO_TP_ALL_COLUMNS(DEF, PERFETTO_TP_COLUMN_SCHEMA);                \
       return schema;                                                          \
     }                                                                         \
diff --git a/src/trace_processor/tables/profiler_tables.h b/src/trace_processor/tables/profiler_tables.h
index 0ac47a9..1fe5d9a 100644
--- a/src/trace_processor/tables/profiler_tables.h
+++ b/src/trace_processor/tables/profiler_tables.h
@@ -88,9 +88,9 @@
 #define PERFETTO_TP_EXPERIMENTAL_FLAMEGRAPH_NODES(NAME, PARENT, C)        \
   NAME(ExperimentalFlamegraphNodesTable, "experimental_flamegraph_nodes") \
   PERFETTO_TP_ROOT_TABLE(PARENT, C)                                       \
-  C(int64_t, ts, Column::Flag::kSorted)                                   \
-  C(uint32_t, upid)                                                       \
-  C(StringPool::Id, profile_type)                                         \
+  C(int64_t, ts, Column::Flag::kSorted | Column::Flag::kHidden)           \
+  C(uint32_t, upid, Column::Flag::kHidden)                                \
+  C(StringPool::Id, profile_type, Column::Flag::kHidden)                  \
   C(uint32_t, depth)                                                      \
   C(StringPool::Id, name)                                                 \
   C(StringPool::Id, map_name)                                             \
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 4a43cb9..7b6164a 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -24,6 +24,8 @@
 #include "perfetto/ext/base/string_splitter.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "src/trace_processor/additional_modules.h"
+#include "src/trace_processor/experimental_counter_dur_generator.h"
+#include "src/trace_processor/experimental_flamegraph_generator.h"
 #include "src/trace_processor/importers/ftrace/sched_event_tracker.h"
 #include "src/trace_processor/metadata_tracker.h"
 #include "src/trace_processor/sql_stats_table.h"
@@ -32,8 +34,6 @@
 #include "src/trace_processor/sqlite/sqlite_table.h"
 #include "src/trace_processor/sqlite/sqlite_utils.h"
 #include "src/trace_processor/sqlite/window_operator_table.h"
-#include "src/trace_processor/sqlite_experimental_counter_dur_table.h"
-#include "src/trace_processor/sqlite_experimental_flamegraph_table.h"
 #include "src/trace_processor/sqlite_raw_table.h"
 #include "src/trace_processor/stats_table.h"
 #include "src/trace_processor/types/variadic.h"
@@ -417,11 +417,14 @@
   WindowOperatorTable::RegisterTable(*db_, storage);
 
   // New style tables but with some custom logic.
-  SqliteExperimentalFlamegraphTable::RegisterTable(*db_, &context_);
   SqliteRawTable::RegisterTable(*db_, query_cache_.get(),
                                 context_.storage.get());
-  SqliteExperimentalCounterDurTable::RegisterTable(*db_, query_cache_.get(),
-                                                   storage->counter_table());
+
+  // Tables dynamically generated at query time.
+  RegisterDynamicTable(std::unique_ptr<ExperimentalFlamegraphGenerator>(
+      new ExperimentalFlamegraphGenerator(&context_)));
+  RegisterDynamicTable(std::unique_ptr<ExperimentalCounterDurGenerator>(
+      new ExperimentalCounterDurGenerator(storage->counter_table())));
 
   // New style db-backed tables.
   RegisterDbTable(storage->arg_table());
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index afacd55..6d0967d 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -81,6 +81,12 @@
                                  &table, table.table_name());
   }
 
+  void RegisterDynamicTable(
+      std::unique_ptr<DbSqliteTable::DynamicTableGenerator> generator) {
+    DbSqliteTable::RegisterTable(*db_, query_cache_.get(),
+                                 std::move(generator));
+  }
+
   ScopedDb db_;
   std::unique_ptr<QueryCache> query_cache_;
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.cc b/src/traced/probes/ftrace/ftrace_config_muxer.cc
index 5a93e5f..f3917dc 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.cc
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.cc
@@ -428,11 +428,11 @@
   if (ds_configs_.empty()) {
     PERFETTO_DCHECK(active_configs_.empty());
 
-    PERFETTO_DCHECK(!current_state_.tracing_on);
-
     // If someone outside of perfetto is using ftrace give up now.
-    if (is_ftrace_enabled)
+    if (is_ftrace_enabled) {
+      PERFETTO_ELOG("ftrace in use by non-Perfetto.");
       return 0;
+    }
 
     // Setup ftrace, without starting it. Setting buffers can be quite slow
     // (up to hundreds of ms).
@@ -440,8 +440,10 @@
     SetupBufferSize(request);
   } else {
     // Did someone turn ftrace off behind our back? If so give up.
-    if (!active_configs_.empty() && !is_ftrace_enabled)
+    if (!active_configs_.empty() && !is_ftrace_enabled) {
+      PERFETTO_ELOG("ftrace disabled by non-Perfetto.");
       return 0;
+    }
   }
 
   std::set<GroupAndName> events = GetFtraceEvents(request, table_);
@@ -493,20 +495,19 @@
     return false;
   }
 
+  if (active_configs_.empty()) {
+    if (ftrace_->IsTracingEnabled()) {
+      // If someone outside of perfetto is using ftrace give up now.
+      PERFETTO_ELOG("ftrace in use by non-Perfetto.");
+      return false;
+    }
+    if (!ftrace_->EnableTracing()) {
+      PERFETTO_ELOG("Failed to enable ftrace.");
+      return false;
+    }
+  }
+
   active_configs_.insert(id);
-  if (active_configs_.size() > 1) {
-    PERFETTO_DCHECK(current_state_.tracing_on);
-    return true;  // We are not the first, ftrace is already enabled. All done.
-  }
-
-  PERFETTO_DCHECK(!current_state_.tracing_on);
-  if (ftrace_->IsTracingEnabled()) {
-    // If someone outside of perfetto is using ftrace give up now.
-    return false;
-  }
-
-  ftrace_->EnableTracing();
-  current_state_.tracing_on = true;
   return true;
 }
 
@@ -554,12 +555,11 @@
   // If there aren't any more active configs, disable ftrace.
   auto active_it = active_configs_.find(config_id);
   if (active_it != active_configs_.end()) {
-    PERFETTO_DCHECK(current_state_.tracing_on);
     active_configs_.erase(active_it);
     if (active_configs_.empty()) {
       // This was the last active config, disable ftrace.
-      ftrace_->DisableTracing();
-      current_state_.tracing_on = false;
+      if (!ftrace_->DisableTracing())
+        PERFETTO_ELOG("Failed to disable ftrace.");
     }
   }
 
diff --git a/src/traced/probes/ftrace/ftrace_config_muxer.h b/src/traced/probes/ftrace/ftrace_config_muxer.h
index 16883c4..71100e9 100644
--- a/src/traced/probes/ftrace/ftrace_config_muxer.h
+++ b/src/traced/probes/ftrace/ftrace_config_muxer.h
@@ -121,7 +121,6 @@
     std::vector<std::string> atrace_apps;
     std::vector<std::string> atrace_categories;
     size_t cpu_buffer_size_pages = 0;
-    bool tracing_on = false;
     bool atrace_on = false;
   };
 
diff --git a/src/traced/probes/ftrace/ftrace_controller.cc b/src/traced/probes/ftrace/ftrace_controller.cc
index a66ed05..ac960e7 100644
--- a/src/traced/probes/ftrace/ftrace_controller.cc
+++ b/src/traced/probes/ftrace/ftrace_controller.cc
@@ -105,6 +105,8 @@
 // We don't know what state the rest of the system and process is so as far
 // as possible avoid allocations.
 void HardResetFtraceState() {
+  PERFETTO_LOG("Hard resetting ftrace state.");
+
   WriteToFile("/sys/kernel/debug/tracing/tracing_on", "0");
   WriteToFile("/sys/kernel/debug/tracing/buffer_size_kb", "4");
   WriteToFile("/sys/kernel/debug/tracing/events/enable", "0");
diff --git a/src/traced/probes/ftrace/ftrace_procfs.cc b/src/traced/probes/ftrace/ftrace_procfs.cc
index 060f4cd..ddccbe1 100644
--- a/src/traced/probes/ftrace/ftrace_procfs.cc
+++ b/src/traced/probes/ftrace/ftrace_procfs.cc
@@ -157,12 +157,14 @@
 
 bool FtraceProcfs::EnableTracing() {
   KernelLogWrite("perfetto: enabled ftrace\n");
+  PERFETTO_LOG("enabled ftrace");
   std::string path = root_ + "tracing_on";
   return WriteToFile(path, "1");
 }
 
 bool FtraceProcfs::DisableTracing() {
   KernelLogWrite("perfetto: disabled ftrace\n");
+  PERFETTO_LOG("disabled ftrace");
   std::string path = root_ + "tracing_on";
   return WriteToFile(path, "0");
 }
@@ -173,7 +175,10 @@
 
 bool FtraceProcfs::IsTracingEnabled() {
   std::string path = root_ + "tracing_on";
-  return ReadOneCharFromFile(path) == '1';
+  char tracing_on = ReadOneCharFromFile(path);
+  if (tracing_on == '\0')
+    PERFETTO_PLOG("Failed to read %s", path.c_str());
+  return tracing_on == '1';
 }
 
 bool FtraceProcfs::SetClock(const std::string& clock_name) {
diff --git a/src/traced/probes/probes_producer.cc b/src/traced/probes/probes_producer.cc
index 036fb0c..1d79eae 100644
--- a/src/traced/probes/probes_producer.cc
+++ b/src/traced/probes/probes_producer.cc
@@ -261,9 +261,7 @@
       ftrace_->GetWeakPtr(), session_id, std::move(ftrace_config),
       endpoint_->CreateTraceWriter(buffer_id)));
   if (!ftrace_->AddDataSource(data_source.get())) {
-    PERFETTO_ELOG(
-        "Failed to setup tracing (too many concurrent sessions or ftrace is "
-        "already in use)");
+    PERFETTO_ELOG("Failed to setup ftrace");
     return nullptr;
   }
   return std::unique_ptr<ProbesDataSource>(std::move(data_source));
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index 7f14ed3..a6ea494 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -151,3 +151,16 @@
   ]
   sources = [ "internal/in_process_tracing_backend.cc" ]
 }
+
+if (enable_perfetto_benchmarks) {
+  source_set("benchmarks") {
+    testonly = true
+    deps = [
+      ":platform_posix",
+      "../..:libperfetto_client_experimental",
+      "../../../../../gn:benchmark",
+      "../../../../../gn:default_deps",
+    ]
+    sources = [ "api_benchmark.cc" ]
+  }
+}
diff --git a/src/tracing/api_benchmark.cc b/src/tracing/api_benchmark.cc
new file mode 100644
index 0000000..88d725e
--- /dev/null
+++ b/src/tracing/api_benchmark.cc
@@ -0,0 +1,132 @@
+// 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.
+
+#include <benchmark/benchmark.h>
+
+#include "perfetto/tracing.h"
+#include "protos/perfetto/trace/test_event.pbzero.h"
+#include "protos/perfetto/trace/track_event/log_message.pbzero.h"
+
+PERFETTO_DEFINE_CATEGORIES(perfetto::Category("benchmark"));
+PERFETTO_TRACK_EVENT_STATIC_STORAGE();
+
+namespace {
+
+class BenchmarkDataSource : public perfetto::DataSource<BenchmarkDataSource> {
+ public:
+  void OnSetup(const SetupArgs&) override {}
+  void OnStart(const StartArgs&) override {}
+  void OnStop(const StopArgs&) override {}
+};
+
+static void BM_TracingDataSourceDisabled(benchmark::State& state) {
+  while (state.KeepRunning()) {
+    BenchmarkDataSource::Trace([&](BenchmarkDataSource::TraceContext) {});
+    benchmark::ClobberMemory();
+  }
+}
+
+std::unique_ptr<perfetto::TracingSession> StartTracing(
+    const std::string& data_source_name) {
+  perfetto::TracingInitArgs args;
+  args.backends = perfetto::kInProcessBackend;
+  perfetto::Tracing::Initialize(args);
+
+  perfetto::DataSourceDescriptor dsd;
+  dsd.set_name("benchmark");
+  BenchmarkDataSource::Register(dsd);
+  perfetto::TrackEvent::Register();
+
+  perfetto::TraceConfig cfg;
+  cfg.add_buffers()->set_size_kb(1024);
+  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
+  ds_cfg->set_name(data_source_name);
+  auto tracing_session =
+      perfetto::Tracing::NewTrace(perfetto::kInProcessBackend);
+  tracing_session->Setup(cfg);
+  tracing_session->StartBlocking();
+  return tracing_session;
+}
+
+static void BM_TracingDataSourceLambda(benchmark::State& state) {
+  auto tracing_session = StartTracing("benchmark");
+
+  while (state.KeepRunning()) {
+    BenchmarkDataSource::Trace([&](BenchmarkDataSource::TraceContext ctx) {
+      auto packet = ctx.NewTracePacket();
+      packet->set_timestamp(42);
+      packet->set_for_testing()->set_str("benchmark");
+    });
+    benchmark::ClobberMemory();
+  }
+
+  tracing_session->StopBlocking();
+  PERFETTO_CHECK(!tracing_session->ReadTraceBlocking().empty());
+}
+
+static void BM_TracingTrackEventDisabled(benchmark::State& state) {
+  while (state.KeepRunning()) {
+    TRACE_EVENT_BEGIN("benchmark", "DisabledEvent");
+    benchmark::ClobberMemory();
+  }
+}
+
+static void BM_TracingTrackEventBasic(benchmark::State& state) {
+  auto tracing_session = StartTracing("track_event");
+
+  while (state.KeepRunning()) {
+    TRACE_EVENT_BEGIN("benchmark", "Event");
+    benchmark::ClobberMemory();
+  }
+
+  tracing_session->StopBlocking();
+  PERFETTO_CHECK(!tracing_session->ReadTraceBlocking().empty());
+}
+
+static void BM_TracingTrackEventDebugAnnotations(benchmark::State& state) {
+  auto tracing_session = StartTracing("track_event");
+
+  while (state.KeepRunning()) {
+    TRACE_EVENT_BEGIN("benchmark", "Event", "value", 42);
+    benchmark::ClobberMemory();
+  }
+
+  tracing_session->StopBlocking();
+  PERFETTO_CHECK(!tracing_session->ReadTraceBlocking().empty());
+}
+
+static void BM_TracingTrackEventLambda(benchmark::State& state) {
+  auto tracing_session = StartTracing("track_event");
+
+  while (state.KeepRunning()) {
+    TRACE_EVENT_BEGIN("benchmark", "Event", [&](perfetto::EventContext ctx) {
+      auto* log = ctx.event()->set_log_message();
+      log->set_source_location_iid(42);
+      log->set_body_iid(1234);
+    });
+    benchmark::ClobberMemory();
+  }
+
+  tracing_session->StopBlocking();
+  PERFETTO_CHECK(!tracing_session->ReadTraceBlocking().empty());
+}
+
+}  // namespace
+
+BENCHMARK(BM_TracingDataSourceDisabled);
+BENCHMARK(BM_TracingDataSourceLambda);
+BENCHMARK(BM_TracingTrackEventBasic);
+BENCHMARK(BM_TracingTrackEventDebugAnnotations);
+BENCHMARK(BM_TracingTrackEventDisabled);
+BENCHMARK(BM_TracingTrackEventLambda);
diff --git a/src/tracing/core/shared_memory_arbiter_impl.cc b/src/tracing/core/shared_memory_arbiter_impl.cc
index 9ea626b..f4ee8b3 100644
--- a/src/tracing/core/shared_memory_arbiter_impl.cc
+++ b/src/tracing/core/shared_memory_arbiter_impl.cc
@@ -257,12 +257,13 @@
 
     if (!commit_data_req_) {
       commit_data_req_.reset(new CommitDataRequest());
-      weak_this = weak_ptr_factory_.GetWeakPtr();
 
       // Flushing the commit is only supported while we're |fully_bound_|. If we
       // aren't, we'll flush when |fully_bound_| is updated.
-      if (fully_bound_)
+      if (fully_bound_) {
+        weak_this = weak_ptr_factory_.GetWeakPtr();
         task_runner_to_post_callback_on = task_runner_;
+      }
     }
 
     // If a valid chunk is specified, return it and attach it to the request.
@@ -418,6 +419,11 @@
     producer_endpoint_ = producer_endpoint;
     task_runner_ = task_runner;
 
+    // Now that we're bound to a task runner, also reset the WeakPtrFactory to
+    // it. Because this code runs on the task runner, the factory's weak
+    // pointers will be valid on it.
+    weak_ptr_factory_.Reset(this);
+
     // All writers registered so far should be startup trace writers, since
     // the producer cannot feasibly know the target buffer for any future
     // session yet.
diff --git a/src/tracing/core/trace_buffer.cc b/src/tracing/core/trace_buffer.cc
index fe7ff48..2227870 100644
--- a/src/tracing/core/trace_buffer.cc
+++ b/src/tracing/core/trace_buffer.cc
@@ -880,6 +880,12 @@
                         chunk_meta->is_complete())) {
     stats_.set_chunks_read(stats_.chunks_read() + 1);
     stats_.set_bytes_read(stats_.bytes_read() + chunk_meta->chunk_record->size);
+  } else {
+    // We have at least one more packet to parse. It should be within the chunk.
+    if (chunk_meta->cur_fragment_offset + sizeof(ChunkRecord) >=
+        chunk_meta->chunk_record->size) {
+      PERFETTO_DCHECK(suppress_sanity_dchecks_for_testing_);
+    }
   }
 
   chunk_meta->set_last_read_packet_skipped(false);
diff --git a/src/tracing/core/trace_writer_impl.cc b/src/tracing/core/trace_writer_impl.cc
index d34470f..6c3fd06 100644
--- a/src/tracing/core/trace_writer_impl.cc
+++ b/src/tracing/core/trace_writer_impl.cc
@@ -48,7 +48,8 @@
       id_(id),
       target_buffer_(target_buffer),
       buffer_exhausted_policy_(buffer_exhausted_policy),
-      protobuf_stream_writer_(this) {
+      protobuf_stream_writer_(this),
+      process_id_(base::GetProcessId()) {
   // TODO(primiano): we could handle the case of running out of TraceWriterID(s)
   // more gracefully and always return a no-op TracePacket in NewTracePacket().
   PERFETTO_CHECK(id_ != 0);
@@ -88,6 +89,11 @@
   // If we hit this, the caller is calling NewTracePacket() without having
   // finalized the previous packet.
   PERFETTO_CHECK(cur_packet_->is_finalized());
+  // If we hit this, this trace writer was created in a different process. This
+  // likely means that the process forked while tracing was active, and the
+  // forked child process tried to emit a trace event. This is not supported, as
+  // it would lead to two processes writing to the same tracing SMB.
+  PERFETTO_DCHECK(process_id_ == base::GetProcessId());
 
   fragmenting_packet_ = false;
 
diff --git a/src/tracing/core/trace_writer_impl.h b/src/tracing/core/trace_writer_impl.h
index 500f3c9..f3a53be 100644
--- a/src/tracing/core/trace_writer_impl.h
+++ b/src/tracing/core/trace_writer_impl.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACING_CORE_TRACE_WRITER_IMPL_H_
 #define SRC_TRACING_CORE_TRACE_WRITER_IMPL_H_
 
+#include "perfetto/base/proc_utils.h"
 #include "perfetto/ext/tracing/core/basic_types.h"
 #include "perfetto/ext/tracing/core/shared_memory_abi.h"
 #include "perfetto/ext/tracing/core/shared_memory_arbiter.h"
@@ -129,6 +130,10 @@
   // later sent out-of-band to the tracing service, who will patch the required
   // chunks, if they are still around.
   PatchList patch_list_;
+
+  // PID of the process that created the trace writer. Used for a DCHECK that
+  // aims to detect unsupported process forks while tracing.
+  const base::PlatformProcessId process_id_;
 };
 
 }  // namespace perfetto
diff --git a/src/tracing/core/tracing_service_impl.cc b/src/tracing/core/tracing_service_impl.cc
index 0f249a9..2451baa 100644
--- a/src/tracing/core/tracing_service_impl.cc
+++ b/src/tracing/core/tracing_service_impl.cc
@@ -683,10 +683,12 @@
 
   tracing_session->state = TracingSession::CONFIGURED;
   PERFETTO_LOG(
-      "Configured tracing, #sources:%zu, duration:%d ms, #buffers:%d, total "
-      "buffer size:%zu KB, total sessions:%zu session name: %s",
-      cfg.data_sources().size(), tracing_session->config.duration_ms(),
+      "Configured tracing session %" PRIu64
+      ", #sources:%zu, duration:%d ms, #buffers:%d, total "
+      "buffer size:%zu KB, total sessions:%zu, uid:%d session name: \"%s\"",
+      tsid, cfg.data_sources().size(), tracing_session->config.duration_ms(),
       cfg.buffers_size(), total_buf_size_kb, tracing_sessions_.size(),
+      static_cast<unsigned int>(consumer->uid_),
       cfg.unique_session_name().c_str());
 
   // Start the data sources, unless this is a case of early setup + fast
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index d27b9ef..eb62498 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -39,6 +39,9 @@
 namespace {
 
 std::atomic<perfetto::base::PlatformThreadId> g_main_thread;
+static constexpr const char kLegacySlowPrefix[] = "disabled-by-default-";
+static constexpr const char kSlowTag[] = "slow";
+static constexpr const char kDebugTag[] = "debug";
 
 struct InternedEventCategory
     : public TrackEventInternedDataIndex<
@@ -48,10 +51,11 @@
           SmallInternedDataTraits> {
   static void Add(protos::pbzero::InternedData* interned_data,
                   size_t iid,
-                  const char* value) {
+                  const char* value,
+                  size_t length) {
     auto category = interned_data->add_event_categories();
     category->set_iid(iid);
-    category->set_name(value);
+    category->set_name(value, length);
   }
 };
 
@@ -95,22 +99,28 @@
 #endif
 }
 
-bool NameMatchesPattern(const std::string& pattern, const std::string& name) {
+enum class MatchType { kExact, kPattern };
+
+bool NameMatchesPattern(const std::string& pattern,
+                        const std::string& name,
+                        MatchType match_type) {
   // To avoid pulling in all of std::regex, for now we only support a single "*"
   // wildcard at the end of the pattern.
-  // TODO(skyostil): Support comma-separated categories.
   size_t i = pattern.find('*');
   if (i != std::string::npos) {
     PERFETTO_DCHECK(i == pattern.size() - 1);
+    if (match_type != MatchType::kPattern)
+      return false;
     return name.substr(0, i) == pattern.substr(0, i);
   }
   return name == pattern;
 }
 
 bool NameMatchesPatternList(const std::vector<std::string>& patterns,
-                            const std::string& name) {
+                            const std::string& name,
+                            MatchType match_type) {
   for (const auto& pattern : patterns) {
-    if (NameMatchesPattern(pattern, name))
+    if (NameMatchesPattern(pattern, name, match_type))
       return true;
   }
   return false;
@@ -131,9 +141,20 @@
   protozero::HeapBuffered<protos::pbzero::TrackEventDescriptor> ted;
   for (size_t i = 0; i < registry.category_count(); i++) {
     auto category = registry.GetCategory(i);
+    // Don't register group categories.
+    if (category->IsGroup())
+      continue;
     auto cat = ted->add_available_categories();
     cat->set_name(category->name);
-    // TODO(skyostil): Advertise category tags and descriptions.
+    if (category->description)
+      cat->set_description(category->description);
+    for (const auto& tag : category->tags) {
+      if (tag)
+        cat->add_tags(tag);
+    }
+    // Disabled-by-default categories get a "slow" tag.
+    if (!strncmp(category->name, kLegacySlowPrefix, strlen(kLegacySlowPrefix)))
+      cat->add_tags(kSlowTag);
   }
   dsd.set_track_event_descriptor_raw(ted.SerializeAsString());
 
@@ -146,7 +167,7 @@
     const protos::gen::TrackEventConfig& config,
     uint32_t instance_index) {
   for (size_t i = 0; i < registry.category_count(); i++) {
-    if (IsCategoryEnabled(config, *registry.GetCategory(i)))
+    if (IsCategoryEnabled(registry, config, *registry.GetCategory(i)))
       registry.EnableCategoryForInstance(i, instance_index);
   }
 }
@@ -161,11 +182,91 @@
 
 // static
 bool TrackEventInternal::IsCategoryEnabled(
+    const TrackEventCategoryRegistry& registry,
     const protos::gen::TrackEventConfig& config,
-    const TrackEventCategory& category) {
-  if (NameMatchesPatternList(config.disabled_categories(), category.name))
-    return NameMatchesPatternList(config.enabled_categories(), category.name);
-  // TODO(skyostil): Support tag-based category configs.
+    const Category& category) {
+  // If this is a group category, check if any of its constituent categories are
+  // enabled. If so, then this one is enabled too.
+  if (category.IsGroup()) {
+    bool result = false;
+    category.ForEachGroupMember([&](const char* member_name, size_t name_size) {
+      for (size_t i = 0; i < registry.category_count(); i++) {
+        const auto ref_category = registry.GetCategory(i);
+        // Groups can't refer to other groups.
+        if (ref_category->IsGroup())
+          continue;
+        // Require an exact match.
+        if (ref_category->name_size() != name_size ||
+            strncmp(ref_category->name, member_name, name_size)) {
+          continue;
+        }
+        if (IsCategoryEnabled(registry, config, *ref_category)) {
+          result = true;
+          // Break ForEachGroupMember() loop.
+          return false;
+        }
+        break;
+      }
+      // No match found => keep iterating.
+      return true;
+    });
+    return result;
+  }
+
+  auto has_matching_tag = [&](std::function<bool(const char*)> matcher) {
+    for (const auto& tag : category.tags) {
+      if (!tag)
+        break;
+      if (matcher(tag))
+        return true;
+    }
+    // Legacy "disabled-by-default" categories automatically get the "slow" tag.
+    if (!strncmp(category.name, kLegacySlowPrefix, strlen(kLegacySlowPrefix)) &&
+        matcher(kSlowTag)) {
+      return true;
+    }
+    return false;
+  };
+
+  // First try exact matches, then pattern matches.
+  const std::array<MatchType, 2> match_types = {
+      {MatchType::kExact, MatchType::kPattern}};
+  for (auto match_type : match_types) {
+    // 1. Enabled categories.
+    if (NameMatchesPatternList(config.enabled_categories(), category.name,
+                               match_type)) {
+      return true;
+    }
+
+    // 2. Enabled tags.
+    if (has_matching_tag([&](const char* tag) {
+          return NameMatchesPatternList(config.enabled_tags(), tag, match_type);
+        })) {
+      return true;
+    }
+
+    // 3. Disabled categories.
+    if (NameMatchesPatternList(config.disabled_categories(), category.name,
+                               match_type)) {
+      return false;
+    }
+
+    // 4. Disabled tags.
+    if (has_matching_tag([&](const char* tag) {
+          if (config.disabled_tags_size()) {
+            return NameMatchesPatternList(config.disabled_tags(), tag,
+                                          match_type);
+          } else {
+            // The "slow" and "debug" tags are disabled by default.
+            return NameMatchesPattern(kSlowTag, tag, match_type) ||
+                   NameMatchesPattern(kDebugTag, tag, match_type);
+          }
+        })) {
+      return false;
+    }
+  }
+
+  // If nothing matched, enable the category by default.
   return true;
 }
 
@@ -224,11 +325,10 @@
 EventContext TrackEventInternal::WriteEvent(
     TraceWriterBase* trace_writer,
     TrackEventIncrementalState* incr_state,
-    const char* category,
+    const Category* category,
     const char* name,
     perfetto::protos::pbzero::TrackEvent::Type type,
     uint64_t timestamp) {
-  PERFETTO_DCHECK(category);
   PERFETTO_DCHECK(g_main_thread);
 
   if (incr_state->was_cleared) {
@@ -244,10 +344,14 @@
 
   // We assume that |category| and |name| point to strings with static lifetime.
   // This means we can use their addresses as interning keys.
-  if (type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
-    // TODO(skyostil): Handle multiple categories.
-    size_t category_iid = InternedEventCategory::Get(&ctx, category);
-    track_event->add_category_iids(category_iid);
+  if (category && type != protos::pbzero::TrackEvent::TYPE_SLICE_END) {
+    category->ForEachGroupMember(
+        [&](const char* member_name, size_t name_size) {
+          size_t category_iid =
+              InternedEventCategory::Get(&ctx, member_name, name_size);
+          track_event->add_category_iids(category_iid);
+          return true;
+        });
   }
   if (name) {
     size_t name_iid = InternedEventName::Get(&ctx, name);
diff --git a/src/tracing/ipc/producer/producer_ipc_client_impl.cc b/src/tracing/ipc/producer/producer_ipc_client_impl.cc
index a5d94c1..0730ed5 100644
--- a/src/tracing/ipc/producer/producer_ipc_client_impl.cc
+++ b/src/tracing/ipc/producer/producer_ipc_client_impl.cc
@@ -46,12 +46,14 @@
     TracingService::ProducerSMBScrapingMode smb_scraping_mode,
     size_t shared_memory_size_hint_bytes,
     size_t shared_memory_page_size_hint_bytes,
-    std::unique_ptr<SharedMemory> shm) {
+    std::unique_ptr<SharedMemory> shm,
+    std::unique_ptr<SharedMemoryArbiter> shm_arbiter) {
   return std::unique_ptr<TracingService::ProducerEndpoint>(
-      new ProducerIPCClientImpl(
-          service_sock_name, producer, producer_name, task_runner,
-          smb_scraping_mode, shared_memory_size_hint_bytes,
-          shared_memory_page_size_hint_bytes, std::move(shm)));
+      new ProducerIPCClientImpl(service_sock_name, producer, producer_name,
+                                task_runner, smb_scraping_mode,
+                                shared_memory_size_hint_bytes,
+                                shared_memory_page_size_hint_bytes,
+                                std::move(shm), std::move(shm_arbiter)));
 }
 
 ProducerIPCClientImpl::ProducerIPCClientImpl(
@@ -62,16 +64,30 @@
     TracingService::ProducerSMBScrapingMode smb_scraping_mode,
     size_t shared_memory_size_hint_bytes,
     size_t shared_memory_page_size_hint_bytes,
-    std::unique_ptr<SharedMemory> shm)
+    std::unique_ptr<SharedMemory> shm,
+    std::unique_ptr<SharedMemoryArbiter> shm_arbiter)
     : producer_(producer),
       task_runner_(task_runner),
       ipc_channel_(ipc::Client::CreateInstance(service_sock_name, task_runner)),
       producer_port_(this /* event_listener */),
       shared_memory_(std::move(shm)),
+      shared_memory_arbiter_(std::move(shm_arbiter)),
       name_(producer_name),
       shared_memory_page_size_hint_bytes_(shared_memory_page_size_hint_bytes),
       shared_memory_size_hint_bytes_(shared_memory_size_hint_bytes),
       smb_scraping_mode_(smb_scraping_mode) {
+  // Check for producer-provided SMB (used by Chrome for startup tracing).
+  if (shared_memory_) {
+    // We also expect a valid (unbound) arbiter. Bind it to this endpoint now.
+    PERFETTO_CHECK(shared_memory_arbiter_);
+    shared_memory_arbiter_->BindToProducerEndpoint(this, task_runner_);
+
+    // If the service accepts our SMB, then it must match our requested page
+    // layout. The protocol doesn't allow the service to change the size and
+    // layout when the SMB is provided by the producer.
+    shared_buffer_page_size_kb_ = shared_memory_page_size_hint_bytes_ / 1024;
+  }
+
   ipc_channel_->BindService(producer_port_.GetWeakPtr());
   PERFETTO_DCHECK_THREAD(thread_checker_);
 }
@@ -215,19 +231,14 @@
                                         /*require_seals_if_supported=*/false);
       shared_buffer_page_size_kb_ =
           cmd.setup_tracing().shared_buffer_page_size_kb();
+      shared_memory_arbiter_ = SharedMemoryArbiter::CreateInstance(
+          shared_memory_.get(), shared_buffer_page_size_kb_ * 1024, this,
+          task_runner_);
     } else {
       // Producer-provided SMB (used by Chrome for startup tracing).
-      PERFETTO_CHECK(is_shmem_provided_by_producer_ && shared_memory_);
-      // If the service accepted our SMB, then it must match our requested page
-      // layout. The protocol doesn't allow the sservice to change the size and
-      // layout when the SMB is provided by the producer.
-      shared_buffer_page_size_kb_ = shared_memory_page_size_hint_bytes_ / 1024;
-      // TODO(eseckler): In case of a procucer-provided SMB, create the
-      // SharedMemoryArbiter even earlier to support startup tracing.
+      PERFETTO_CHECK(is_shmem_provided_by_producer_ && shared_memory_ &&
+                     shared_memory_arbiter_);
     }
-    shared_memory_arbiter_ = SharedMemoryArbiter::CreateInstance(
-        shared_memory_.get(), shared_buffer_page_size_kb_ * 1024, this,
-        task_runner_);
     producer_->OnTracingSetup();
     return;
   }
diff --git a/src/tracing/ipc/producer/producer_ipc_client_impl.h b/src/tracing/ipc/producer/producer_ipc_client_impl.h
index 816a82bc..244a4f9 100644
--- a/src/tracing/ipc/producer/producer_ipc_client_impl.h
+++ b/src/tracing/ipc/producer/producer_ipc_client_impl.h
@@ -51,14 +51,16 @@
 class ProducerIPCClientImpl : public TracingService::ProducerEndpoint,
                               public ipc::ServiceProxy::EventListener {
  public:
-  ProducerIPCClientImpl(const char* service_sock_name,
-                        Producer*,
-                        const std::string& producer_name,
-                        base::TaskRunner*,
-                        TracingService::ProducerSMBScrapingMode,
-                        size_t shared_memory_size_hint_bytes = 0,
-                        size_t shared_memory_page_size_hint_bytes = 0,
-                        std::unique_ptr<SharedMemory> shm = nullptr);
+  ProducerIPCClientImpl(
+      const char* service_sock_name,
+      Producer*,
+      const std::string& producer_name,
+      base::TaskRunner*,
+      TracingService::ProducerSMBScrapingMode,
+      size_t shared_memory_size_hint_bytes = 0,
+      size_t shared_memory_page_size_hint_bytes = 0,
+      std::unique_ptr<SharedMemory> shm = nullptr,
+      std::unique_ptr<SharedMemoryArbiter> shm_arbiter = nullptr);
   ~ProducerIPCClientImpl() override;
 
   // TracingService::ProducerEndpoint implementation.
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 3605e4e..76ce6ab 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -76,13 +76,19 @@
 PERFETTO_DEFINE_TEST_CATEGORY_PREFIXES("dynamic");
 
 // Trace categories used in the tests.
-PERFETTO_DEFINE_CATEGORIES(PERFETTO_CATEGORY(test),
-                           PERFETTO_CATEGORY(foo),
-                           PERFETTO_CATEGORY(bar),
-                           PERFETTO_CATEGORY(cat),
-                           // TODO(skyostil): Figure out how to represent
-                           // disabled-by-default categories
-                           {TRACE_DISABLED_BY_DEFAULT("cat")});
+PERFETTO_DEFINE_CATEGORIES(
+    perfetto::Category("test")
+        .SetDescription("This is a test category")
+        .SetTags("tag"),
+    perfetto::Category("foo"),
+    perfetto::Category("bar"),
+    perfetto::Category("cat").SetTags("slow"),
+    perfetto::Category("cat.verbose").SetTags("debug"),
+    perfetto::Category::Group("foo,bar"),
+    perfetto::Category::Group("baz,bar,quux"),
+    perfetto::Category::Group("red,green,blue,foo"),
+    perfetto::Category::Group("red,green,blue,yellow"),
+    perfetto::Category(TRACE_DISABLED_BY_DEFAULT("cat")));
 PERFETTO_TRACK_EVENT_STATIC_STORAGE();
 
 // For testing interning of complex objects.
@@ -520,8 +526,11 @@
           id << "(tid_override=" << legacy_event.tid_override() << ")";
         slice += id.str();
       }
-      if (!track_event.category_iids().empty())
-        slice += ":" + categories[track_event.category_iids()[0]];
+      size_t category_count = 0;
+      for (const auto& it : track_event.category_iids())
+        slice += (category_count++ ? "," : ":") + categories[it];
+      for (const auto& it : track_event.categories())
+        slice += (category_count++ ? ",$" : ":$") + it;
       if (track_event.has_name_iid())
         slice += "." + event_names[track_event.name_iid()];
 
@@ -808,12 +817,19 @@
 
   // Check that the advertised categories match PERFETTO_DEFINE_CATEGORIES (see
   // above).
-  EXPECT_EQ(5, desc.available_categories_size());
+  EXPECT_EQ(6, desc.available_categories_size());
   EXPECT_EQ("test", desc.available_categories()[0].name());
+  EXPECT_EQ("This is a test category",
+            desc.available_categories()[0].description());
+  EXPECT_EQ("tag", desc.available_categories()[0].tags()[0]);
   EXPECT_EQ("foo", desc.available_categories()[1].name());
   EXPECT_EQ("bar", desc.available_categories()[2].name());
   EXPECT_EQ("cat", desc.available_categories()[3].name());
-  EXPECT_EQ("disabled-by-default-cat", desc.available_categories()[4].name());
+  EXPECT_EQ("slow", desc.available_categories()[3].tags()[0]);
+  EXPECT_EQ("cat.verbose", desc.available_categories()[4].name());
+  EXPECT_EQ("debug", desc.available_categories()[4].tags()[0]);
+  EXPECT_EQ("disabled-by-default-cat", desc.available_categories()[5].name());
+  EXPECT_EQ("slow", desc.available_categories()[5].tags()[0]);
 }
 
 TEST_F(PerfettoApiTest, TrackEventSharedIncrementalState) {
@@ -1653,17 +1669,35 @@
 
     TRACE_EVENT_BEGIN("foo", "FooEvent");
     TRACE_EVENT_BEGIN("bar", "BarEvent");
+    TRACE_EVENT_BEGIN("foo,bar", "MultiFooBar");
+    TRACE_EVENT_BEGIN("baz,bar,quux", "MultiBar");
+    TRACE_EVENT_BEGIN("red,green,blue,foo", "MultiFoo");
+    TRACE_EVENT_BEGIN("red,green,blue,yellow", "MultiNone");
+    TRACE_EVENT_BEGIN("cat", "SlowEvent");
+    TRACE_EVENT_BEGIN("cat.verbose", "DebugEvent");
+    TRACE_EVENT_BEGIN("test", "TagEvent");
+    TRACE_EVENT_BEGIN(TRACE_DISABLED_BY_DEFAULT("cat"), "SlowDisabledEvent");
+    TRACE_EVENT_BEGIN("dynamic,foo", "DynamicGroupFooEvent");
+    perfetto::DynamicCategory dyn{"dynamic,bar"};
+    TRACE_EVENT_BEGIN(dyn, "DynamicGroupBarEvent");
 
     perfetto::TrackEvent::Flush();
     tracing_session->get()->StopBlocking();
-    return ReadSlicesFromTrace(tracing_session->get());
+    auto slices = ReadSlicesFromTrace(tracing_session->get());
+    tracing_session->session.reset();
+    return slices;
   };
 
-  // Empty config should enable all categories.
+  // Empty config should enable all categories except slow ones.
   {
     perfetto::protos::gen::TrackEventConfig te_cfg;
     auto slices = check_config(te_cfg);
-    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent", "B:bar.BarEvent"));
+    EXPECT_THAT(
+        slices,
+        ElementsAre("B:foo.FooEvent", "B:bar.BarEvent", "B:foo,bar.MultiFooBar",
+                    "B:baz,bar,quux.MultiBar", "B:red,green,blue,foo.MultiFoo",
+                    "B:test.TagEvent", "B:$dynamic,$foo.DynamicGroupFooEvent",
+                    "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 
   // Enable exactly one category.
@@ -1672,7 +1706,9 @@
     te_cfg.add_disabled_categories("*");
     te_cfg.add_enabled_categories("foo");
     auto slices = check_config(te_cfg);
-    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent"));
+    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent", "B:foo,bar.MultiFooBar",
+                                    "B:red,green,blue,foo.MultiFoo",
+                                    "B:$dynamic,$foo.DynamicGroupFooEvent"));
   }
 
   // Enable two categories.
@@ -1683,17 +1719,25 @@
     te_cfg.add_enabled_categories("baz");
     te_cfg.add_enabled_categories("bar");
     auto slices = check_config(te_cfg);
-    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent", "B:bar.BarEvent"));
+    EXPECT_THAT(
+        slices,
+        ElementsAre("B:foo.FooEvent", "B:bar.BarEvent", "B:foo,bar.MultiFooBar",
+                    "B:baz,bar,quux.MultiBar", "B:red,green,blue,foo.MultiFoo",
+                    "B:$dynamic,$foo.DynamicGroupFooEvent",
+                    "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 
-  // Enable overrides disable.
+  // Enabling all categories with a pattern doesn't enable slow ones.
   {
     perfetto::protos::gen::TrackEventConfig te_cfg;
-    te_cfg.add_disabled_categories("foo");
-    te_cfg.add_disabled_categories("bar");
     te_cfg.add_enabled_categories("*");
     auto slices = check_config(te_cfg);
-    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent", "B:bar.BarEvent"));
+    EXPECT_THAT(
+        slices,
+        ElementsAre("B:foo.FooEvent", "B:bar.BarEvent", "B:foo,bar.MultiFooBar",
+                    "B:baz,bar,quux.MultiBar", "B:red,green,blue,foo.MultiFoo",
+                    "B:test.TagEvent", "B:$dynamic,$foo.DynamicGroupFooEvent",
+                    "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 
   // Enable with a pattern.
@@ -1702,7 +1746,46 @@
     te_cfg.add_disabled_categories("*");
     te_cfg.add_enabled_categories("fo*");
     auto slices = check_config(te_cfg);
-    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent"));
+    EXPECT_THAT(slices, ElementsAre("B:foo.FooEvent", "B:foo,bar.MultiFooBar",
+                                    "B:red,green,blue,foo.MultiFoo",
+                                    "B:$dynamic,$foo.DynamicGroupFooEvent"));
+  }
+
+  // Enable with a tag.
+  {
+    perfetto::protos::gen::TrackEventConfig te_cfg;
+    te_cfg.add_disabled_categories("*");
+    te_cfg.add_enabled_tags("tag");
+    auto slices = check_config(te_cfg);
+    EXPECT_THAT(slices, ElementsAre("B:test.TagEvent"));
+  }
+
+  // Enable just slow categories.
+  {
+    perfetto::protos::gen::TrackEventConfig te_cfg;
+    te_cfg.add_disabled_categories("*");
+    te_cfg.add_enabled_tags("slow");
+    auto slices = check_config(te_cfg);
+    EXPECT_THAT(slices,
+                ElementsAre("B:cat.SlowEvent",
+                            "B:disabled-by-default-cat.SlowDisabledEvent"));
+  }
+
+  // Enable everything including slow/debug categories.
+  {
+    perfetto::protos::gen::TrackEventConfig te_cfg;
+    te_cfg.add_enabled_categories("*");
+    te_cfg.add_enabled_tags("slow");
+    te_cfg.add_enabled_tags("debug");
+    auto slices = check_config(te_cfg);
+    EXPECT_THAT(slices,
+                ElementsAre("B:foo.FooEvent", "B:bar.BarEvent",
+                            "B:foo,bar.MultiFooBar", "B:baz,bar,quux.MultiBar",
+                            "B:red,green,blue,foo.MultiFoo", "B:cat.SlowEvent",
+                            "B:cat.verbose.DebugEvent", "B:test.TagEvent",
+                            "B:disabled-by-default-cat.SlowDisabledEvent",
+                            "B:$dynamic,$foo.DynamicGroupFooEvent",
+                            "B:$dynamic,$bar.DynamicGroupBarEvent"));
   }
 }
 
@@ -2071,15 +2154,9 @@
 }
 
 TEST_F(PerfettoApiTest, LegacyTraceEvents) {
-  // Setup the trace config.
-  perfetto::TraceConfig cfg;
-  cfg.set_duration_ms(500);
-  cfg.add_buffers()->set_size_kb(1024);
-  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
-  ds_cfg->set_name("track_event");
-
   // Create a new trace session.
-  auto* tracing_session = NewTrace(cfg);
+  auto* tracing_session =
+      NewTraceWithCategories({"cat", TRACE_DISABLED_BY_DEFAULT("cat")});
   tracing_session->get()->StartBlocking();
 
   // Basic events.
@@ -2131,15 +2208,8 @@
 }
 
 TEST_F(PerfettoApiTest, LegacyTraceEventsWithCustomAnnotation) {
-  // Setup the trace config.
-  perfetto::TraceConfig cfg;
-  cfg.set_duration_ms(500);
-  cfg.add_buffers()->set_size_kb(1024);
-  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
-  ds_cfg->set_name("track_event");
-
   // Create a new trace session.
-  auto* tracing_session = NewTrace(cfg);
+  auto* tracing_session = NewTraceWithCategories({"cat"});
   tracing_session->get()->StartBlocking();
 
   MyDebugAnnotation annotation;
@@ -2160,17 +2230,10 @@
   // Make sure that a uniquely owned debug annotation can be written into
   // multiple concurrent tracing sessions.
 
-  // Setup the trace config.
-  perfetto::TraceConfig cfg;
-  cfg.set_duration_ms(500);
-  cfg.add_buffers()->set_size_kb(1024);
-  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
-  ds_cfg->set_name("track_event");
-
-  auto* tracing_session = NewTrace(cfg);
+  auto* tracing_session = NewTraceWithCategories({"cat"});
   tracing_session->get()->StartBlocking();
 
-  auto* tracing_session2 = NewTrace(cfg);
+  auto* tracing_session2 = NewTraceWithCategories({"cat"});
   tracing_session2->get()->StartBlocking();
 
   std::unique_ptr<MyDebugAnnotation> owned_annotation(new MyDebugAnnotation());
@@ -2189,14 +2252,7 @@
 }
 
 TEST_F(PerfettoApiTest, LegacyTraceEventsWithId) {
-  // Setup the trace config.
-  perfetto::TraceConfig cfg;
-  cfg.set_duration_ms(500);
-  cfg.add_buffers()->set_size_kb(1024);
-  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
-  ds_cfg->set_name("track_event");
-
-  auto* tracing_session = NewTrace(cfg);
+  auto* tracing_session = NewTraceWithCategories({"cat"});
   tracing_session->get()->StartBlocking();
 
   TRACE_EVENT_ASYNC_BEGIN0("cat", "UnscopedId", 0x1000);
@@ -2217,14 +2273,7 @@
 }
 
 TEST_F(PerfettoApiTest, LegacyTraceEventsWithFlow) {
-  // Setup the trace config.
-  perfetto::TraceConfig cfg;
-  cfg.set_duration_ms(500);
-  cfg.add_buffers()->set_size_kb(1024);
-  auto* ds_cfg = cfg.add_data_sources()->mutable_config();
-  ds_cfg->set_name("track_event");
-
-  auto* tracing_session = NewTrace(cfg);
+  auto* tracing_session = NewTraceWithCategories({"cat"});
   tracing_session->get()->StartBlocking();
 
   const uint64_t flow_id = 1234;
diff --git a/src/tracing/test/tracing_module_categories.h b/src/tracing/test/tracing_module_categories.h
index b492db8..03c0425 100644
--- a/src/tracing/test/tracing_module_categories.h
+++ b/src/tracing/test/tracing_module_categories.h
@@ -27,6 +27,7 @@
 
 #include "perfetto/tracing.h"
 
+// Note: Using the old syntax here to ensure backwards compatibility.
 PERFETTO_DEFINE_CATEGORIES(PERFETTO_CATEGORY(cat1),
                            PERFETTO_CATEGORY(cat2),
                            PERFETTO_CATEGORY(cat3),
diff --git a/src/tracing/track_event_category_registry.cc b/src/tracing/track_event_category_registry.cc
index 5bab490..6d9a649 100644
--- a/src/tracing/track_event_category_registry.cc
+++ b/src/tracing/track_event_category_registry.cc
@@ -17,14 +17,31 @@
 #include "perfetto/tracing/track_event_category_registry.h"
 
 namespace perfetto {
+
+// static
+Category Category::FromDynamicCategory(const char* name) {
+  if (GetNthNameSize(1, name, name)) {
+    Category group(Group(name));
+    PERFETTO_DCHECK(group.name);
+    return group;
+  }
+  Category category(name);
+  PERFETTO_DCHECK(category.name);
+  return category;
+}
+
+Category Category::FromDynamicCategory(
+    const DynamicCategory& dynamic_category) {
+  return FromDynamicCategory(dynamic_category.name.c_str());
+}
+
 namespace internal {
 
 perfetto::DynamicCategory NullCategory(const perfetto::DynamicCategory&) {
   return perfetto::DynamicCategory{};
 }
 
-const TrackEventCategory* TrackEventCategoryRegistry::GetCategory(
-    size_t index) const {
+const Category* TrackEventCategoryRegistry::GetCategory(size_t index) const {
   PERFETTO_DCHECK(index < category_count_);
   return &categories_[index];
 }
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 09b10b7..f9629b3 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -144,3 +144,17 @@
     sources = [ "benchmark_main.cc" ]
   }
 }  # if (enable_perfetto_benchmarks)
+
+if (perfetto_build_with_android || (is_android && perfetto_build_standalone)) {
+  # This is used only in-tree builds. It's built in standalone builds just to
+  # get build coverage.
+  static_library("perfetto_gtest_logcat_printer") {
+    testonly = true
+    complete_static_lib = true
+    sources = [ "gtest_logcat_printer.cc" ]
+    deps = [
+      "../gn:default_deps",
+      "../gn:gtest_and_gmock",
+    ]
+  }
+}
diff --git a/test/cts/Android.bp b/test/cts/Android.bp
index 4871ebb..7628fb3 100644
--- a/test/cts/Android.bp
+++ b/test/cts/Android.bp
@@ -3,8 +3,9 @@
   srcs: [
     "device_feature_test_cts.cc",
     "end_to_end_integrationtest_cts.cc",
-    "heapprofd_test_cts.cc",
     "heapprofd_java_test_cts.cc",
+    "heapprofd_test_cts.cc",
+    "traced_perf_test_cts.cc",
     "utils.cc",
     ":perfetto_protos_perfetto_config_cpp_gen",
   ],
@@ -18,6 +19,9 @@
     "perfetto_cts_deps",
     "perfetto_trace_protos",
   ],
+  whole_static_libs: [
+    "perfetto_gtest_logcat_printer",
+  ],
   shared_libs: [
     "libandroid",
     "liblog",
diff --git a/test/cts/AndroidTest.xml b/test/cts/AndroidTest.xml
index 6dfeac1..3d40593 100644
--- a/test/cts/AndroidTest.xml
+++ b/test/cts/AndroidTest.xml
@@ -34,6 +34,7 @@
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="setprop persist.heapprofd.enable 1" />
+        <option name="run-command" value="setprop persist.traced_perf.enable 1" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.GTest" >
         <option name="native-test-device-path" value="/data/local/tmp" />
diff --git a/test/cts/BUILD.gn b/test/cts/BUILD.gn
index 84aba0c..84a7860 100644
--- a/test/cts/BUILD.gn
+++ b/test/cts/BUILD.gn
@@ -37,6 +37,7 @@
     "end_to_end_integrationtest_cts.cc",
     "heapprofd_java_test_cts.cc",
     "heapprofd_test_cts.cc",
+    "traced_perf_test_cts.cc",
     "utils.cc",
   ]
 }
diff --git a/test/cts/producer/jni/fake_producer_jni.cc b/test/cts/producer/jni/fake_producer_jni.cc
index ab587a8..7eb470e 100644
--- a/test/cts/producer/jni/fake_producer_jni.cc
+++ b/test/cts/producer/jni/fake_producer_jni.cc
@@ -56,8 +56,9 @@
     lock.unlock();
   });
 
-  FakeProducer producer(name);
-  producer.Connect(GetProducerSocket(), &task_runner, [] {}, [] {});
+  FakeProducer producer(name, &task_runner);
+  producer.Connect(
+      GetProducerSocket(), [] {}, [] {});
   task_runner.Run();
 
   // Cleanup the task runner again to remove outside visibilty so we can
diff --git a/test/cts/heapprofd_test_apps/Android.bp b/test/cts/test_apps/Android.bp
similarity index 93%
rename from test/cts/heapprofd_test_apps/Android.bp
rename to test/cts/test_apps/Android.bp
index bc2b323..543c1f6 100644
--- a/test/cts/heapprofd_test_apps/Android.bp
+++ b/test/cts/test_apps/Android.bp
@@ -27,7 +27,7 @@
     srcs: ["src/**/*.java"],
     sdk_version: "current",
     jni_libs: [
-        "libperfettocts_heapprofdtarget",
+        "libperfettocts_native",
         "libnativehelper_compat_libc++",
     ],
 }
@@ -47,7 +47,7 @@
     srcs: ["src/**/*.java"],
     sdk_version: "current",
     jni_libs: [
-        "libperfettocts_heapprofdtarget",
+        "libperfettocts_native",
         "libnativehelper_compat_libc++",
     ],
 }
@@ -67,7 +67,7 @@
     srcs: ["src/**/*.java"],
     sdk_version: "current",
     jni_libs: [
-        "libperfettocts_heapprofdtarget",
+        "libperfettocts_native",
         "libnativehelper_compat_libc++",
     ],
 }
diff --git a/test/cts/heapprofd_test_apps/AndroidManifest_debuggable.xml b/test/cts/test_apps/AndroidManifest_debuggable.xml
similarity index 71%
rename from test/cts/heapprofd_test_apps/AndroidManifest_debuggable.xml
rename to test/cts/test_apps/AndroidManifest_debuggable.xml
index 999541e..c85ab05 100755
--- a/test/cts/heapprofd_test_apps/AndroidManifest_debuggable.xml
+++ b/test/cts/test_apps/AndroidManifest_debuggable.xml
@@ -31,6 +31,18 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.BusyWaitActivity"
+          android:exported="false">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.debuggable.BusyWaitActivity"
+          android:targetActivity="android.perfetto.cts.app.BusyWaitActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
     </application>
 </manifest>
 
diff --git a/test/cts/heapprofd_test_apps/AndroidManifest_profileable.xml b/test/cts/test_apps/AndroidManifest_profileable.xml
similarity index 71%
rename from test/cts/heapprofd_test_apps/AndroidManifest_profileable.xml
rename to test/cts/test_apps/AndroidManifest_profileable.xml
index 99ceda2..129e922 100755
--- a/test/cts/heapprofd_test_apps/AndroidManifest_profileable.xml
+++ b/test/cts/test_apps/AndroidManifest_profileable.xml
@@ -32,6 +32,18 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.BusyWaitActivity"
+          android:exported="false">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.profileable.BusyWaitActivity"
+          android:targetActivity="android.perfetto.cts.app.BusyWaitActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
     </application>
 </manifest>
 
diff --git a/test/cts/heapprofd_test_apps/AndroidManifest_release.xml b/test/cts/test_apps/AndroidManifest_release.xml
similarity index 71%
rename from test/cts/heapprofd_test_apps/AndroidManifest_release.xml
rename to test/cts/test_apps/AndroidManifest_release.xml
index b2dfd25..5b64a94 100755
--- a/test/cts/heapprofd_test_apps/AndroidManifest_release.xml
+++ b/test/cts/test_apps/AndroidManifest_release.xml
@@ -31,6 +31,18 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity-alias>
+        <activity
+          android:name="android.perfetto.cts.app.BusyWaitActivity"
+          android:exported="false">
+        </activity>
+        <activity-alias
+          android:name="android.perfetto.cts.app.release.BusyWaitActivity"
+          android:targetActivity="android.perfetto.cts.app.BusyWaitActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
     </application>
 </manifest>
 
diff --git a/test/cts/heapprofd_test_apps/jni/Android.bp b/test/cts/test_apps/jni/Android.bp
similarity index 95%
rename from test/cts/heapprofd_test_apps/jni/Android.bp
rename to test/cts/test_apps/jni/Android.bp
index de090d2..3f50e72 100644
--- a/test/cts/heapprofd_test_apps/jni/Android.bp
+++ b/test/cts/test_apps/jni/Android.bp
@@ -14,7 +14,7 @@
 // limitations under the License.
 
 cc_library_shared {
-  name: "libperfettocts_heapprofdtarget",
+  name: "libperfettocts_native",
   srcs: [
     "target.cc",
   ],
diff --git a/test/cts/heapprofd_test_apps/jni/target.cc b/test/cts/test_apps/jni/target.cc
similarity index 77%
rename from test/cts/heapprofd_test_apps/jni/target.cc
rename to test/cts/test_apps/jni/target.cc
index 6bedf84..8f847c4 100644
--- a/test/cts/heapprofd_test_apps/jni/target.cc
+++ b/test/cts/test_apps/jni/target.cc
@@ -36,9 +36,21 @@
   }
 }
 
+// Runs continuously as a target for the sampling perf profiler tests.
+__attribute__((noreturn)) void perfetto_busy_wait() {
+  for (volatile unsigned i = 0;; i++) {
+  }
+}
+
 }  // namespace
 
 extern "C" JNIEXPORT void JNICALL
 Java_android_perfetto_cts_app_MainActivity_runNative(JNIEnv*, jclass) {
   perfetto_test_allocations();
 }
+
+extern "C" JNIEXPORT void JNICALL
+Java_android_perfetto_cts_app_BusyWaitActivity_runNativeBusyWait(JNIEnv*,
+                                                                 jclass) {
+  perfetto_busy_wait();
+}
diff --git a/test/cts/heapprofd_test_apps/src/android/perfetto/cts/app/MainActivity.java b/test/cts/test_apps/src/android/perfetto/cts/app/BusyWaitActivity.java
similarity index 82%
copy from test/cts/heapprofd_test_apps/src/android/perfetto/cts/app/MainActivity.java
copy to test/cts/test_apps/src/android/perfetto/cts/app/BusyWaitActivity.java
index 0e26bfc..689c98a 100644
--- a/test/cts/heapprofd_test_apps/src/android/perfetto/cts/app/MainActivity.java
+++ b/test/cts/test_apps/src/android/perfetto/cts/app/BusyWaitActivity.java
@@ -19,9 +19,9 @@
 import android.app.Activity;
 import android.os.Bundle;
 
-public class MainActivity extends Activity {
+public class BusyWaitActivity extends Activity {
     static {
-        System.loadLibrary("perfettocts_heapprofdtarget");
+        System.loadLibrary("perfettocts_native");
     }
 
     @Override
@@ -31,14 +31,13 @@
         new Thread(new Runnable() {
             public void run() {
                 try {
-                    runNative();
+                    runNativeBusyWait();
                 } catch (Exception ex) {
                     ex.printStackTrace();
                 }
             }
-        })
-                .start();
+        }).start();
     }
 
-    private static native void runNative();
+    private static native void runNativeBusyWait();
 }
diff --git a/test/cts/heapprofd_test_apps/src/android/perfetto/cts/app/MainActivity.java b/test/cts/test_apps/src/android/perfetto/cts/app/MainActivity.java
similarity index 92%
rename from test/cts/heapprofd_test_apps/src/android/perfetto/cts/app/MainActivity.java
rename to test/cts/test_apps/src/android/perfetto/cts/app/MainActivity.java
index 0e26bfc..0cabd16 100644
--- a/test/cts/heapprofd_test_apps/src/android/perfetto/cts/app/MainActivity.java
+++ b/test/cts/test_apps/src/android/perfetto/cts/app/MainActivity.java
@@ -21,7 +21,7 @@
 
 public class MainActivity extends Activity {
     static {
-        System.loadLibrary("perfettocts_heapprofdtarget");
+        System.loadLibrary("perfettocts_native");
     }
 
     @Override
@@ -36,8 +36,7 @@
                     ex.printStackTrace();
                 }
             }
-        })
-                .start();
+        }).start();
     }
 
     private static native void runNative();
diff --git a/test/cts/traced_perf_test_cts.cc b/test/cts/traced_perf_test_cts.cc
new file mode 100644
index 0000000..d6745fc
--- /dev/null
+++ b/test/cts/traced_perf_test_cts.cc
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+#include <stdlib.h>
+#include <sys/types.h>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/tracing/core/data_source_config.h"
+#include "src/base/test/test_task_runner.h"
+#include "test/cts/utils.h"
+#include "test/gtest_and_gmock.h"
+#include "test/test_helper.h"
+
+#include "protos/perfetto/config/profiling/perf_event_config.gen.h"
+#include "protos/perfetto/trace/profiling/profile_common.gen.h"
+#include "protos/perfetto/trace/profiling/profile_packet.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+
+namespace perfetto {
+namespace {
+
+std::vector<protos::gen::TracePacket> ProfileSystemWide(std::string app_name) {
+  base::TestTaskRunner task_runner;
+
+  // (re)start the target app's main activity
+  if (IsAppRunning(app_name)) {
+    StopApp(app_name, "old.app.stopped", &task_runner);
+    task_runner.RunUntilCheckpoint("old.app.stopped", 1000 /*ms*/);
+  }
+  StartAppActivity(app_name, "BusyWaitActivity", "target.app.running",
+                   &task_runner,
+                   /*delay_ms=*/100);
+  task_runner.RunUntilCheckpoint("target.app.running", 1000 /*ms*/);
+
+  // set up tracing
+  TestHelper helper(&task_runner);
+  helper.ConnectConsumer();
+  helper.WaitForConsumerConnect();
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(10 * 1024);
+  trace_config.set_duration_ms(2000);
+
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("linux.perf");
+  ds_config->set_target_buffer(0);
+
+  protos::gen::PerfEventConfig perf_config;
+
+  perf_config.set_all_cpus(true);
+  perf_config.set_sampling_frequency(10);  // Hz
+  ds_config->set_perf_event_config_raw(perf_config.SerializeAsString());
+
+  // start tracing
+  helper.StartTracing(trace_config);
+  helper.WaitForTracingDisabled(10000 /*ms*/);
+  helper.ReadData();
+  helper.WaitForReadData();
+
+  return helper.trace();
+}
+
+void AssertHasSampledStacksForPid(std::vector<protos::gen::TracePacket> packets,
+                                  int target_pid) {
+  ASSERT_GT(packets.size(), 0u);
+
+  int samples_found = 0;
+  for (const auto& packet : packets) {
+    if (!packet.has_perf_sample())
+      continue;
+
+    EXPECT_GT(packet.timestamp(), 0u) << "all samples 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;
+
+    // A full sample
+    EXPECT_GT(sample.tid(), 0u);
+    EXPECT_GT(sample.callstack_iid(), 0u);
+    samples_found += 1;
+  }
+  EXPECT_GT(samples_found, 0);
+}
+
+void AssertNoStacksForPid(std::vector<protos::gen::TracePacket> packets,
+                          int target_pid) {
+  for (const auto& packet : packets) {
+    if (packet.perf_sample().pid() == static_cast<uint32_t>(target_pid)) {
+      EXPECT_EQ(packet.perf_sample().callstack_iid(), 0u);
+    }
+  }
+}
+
+TEST(TracedPerfCtsTest, SystemWideDebuggableApp) {
+  std::string app_name = "android.perfetto.cts.app.debuggable";
+  const auto& packets = ProfileSystemWide(app_name);
+  int app_pid = PidForProcessName(app_name);
+  ASSERT_GT(app_pid, 0) << "failed to find pid for target process";
+
+  AssertHasSampledStacksForPid(packets, app_pid);
+  StopApp(app_name);
+}
+
+TEST(TracedPerfCtsTest, SystemWideProfileableApp) {
+  std::string app_name = "android.perfetto.cts.app.profileable";
+  const auto& packets = ProfileSystemWide(app_name);
+  int app_pid = PidForProcessName(app_name);
+  ASSERT_GT(app_pid, 0) << "failed to find pid for target process";
+
+  AssertHasSampledStacksForPid(packets, app_pid);
+  StopApp(app_name);
+}
+
+TEST(TracedPerfCtsTest, SystemWideReleaseApp) {
+  std::string app_name = "android.perfetto.cts.app.release";
+  const auto& packets = ProfileSystemWide(app_name);
+  int app_pid = PidForProcessName(app_name);
+  ASSERT_GT(app_pid, 0) << "failed to find pid for target process";
+
+  if (IsDebuggableBuild())
+    AssertHasSampledStacksForPid(packets, app_pid);
+  else
+    AssertNoStacksForPid(packets, app_pid);
+
+  StopApp(app_name);
+}
+
+}  // namespace
+}  // namespace perfetto
diff --git a/test/cts/utils.cc b/test/cts/utils.cc
index 4ea9309..b11cddf 100644
--- a/test/cts/utils.cc
+++ b/test/cts/utils.cc
@@ -20,6 +20,7 @@
 #include <sys/system_properties.h>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/file_utils.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
@@ -48,10 +49,7 @@
   char buf[PROP_VALUE_MAX + 1] = {};
   int ret = __system_property_get("ro.debuggable", buf);
   PERFETTO_CHECK(ret >= 0);
-  std::string debuggable(buf);
-  if (debuggable == "1")
-    return true;
-  return false;
+  return std::string(buf) == "1";
 }
 
 // note: cannot use gtest macros due to return type
@@ -67,6 +65,25 @@
   PERFETTO_FATAL("unexpected exit status from system(pgrep): %d", exit_status);
 }
 
+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 $$";
+  FILE* fp = popen(cmd.c_str(), "re");
+  if (!fp)
+    return -1;
+
+  std::string out;
+  base::ReadFileStream(fp, &out);
+  pclose(fp);
+
+  char* endptr = nullptr;
+  int pid = static_cast<int>(strtol(out.c_str(), &endptr, 10));
+  if (*endptr != '\0' && *endptr != '\n')
+    return -1;
+  return pid;
+}
+
 void WaitForProcess(const std::string& process,
                     const std::string& checkpoint_name,
                     base::TestTaskRunner* task_runner,
diff --git a/test/cts/utils.h b/test/cts/utils.h
index e2796ef..3fd5a3e 100644
--- a/test/cts/utils.h
+++ b/test/cts/utils.h
@@ -27,6 +27,9 @@
 
 bool IsAppRunning(const std::string& name);
 
+// returns -1 if the process wasn't found
+int PidForProcessName(const std::string& name);
+
 void WaitForProcess(const std::string& process,
                     const std::string& checkpoint_name,
                     base::TestTaskRunner* task_runner,
diff --git a/test/end_to_end_integrationtest.cc b/test/end_to_end_integrationtest.cc
index 12cfa9c..7dc37b1 100644
--- a/test/end_to_end_integrationtest.cc
+++ b/test/end_to_end_integrationtest.cc
@@ -268,14 +268,6 @@
     while (!ftrace_procfs_ && kTracingPaths[index]) {
       ftrace_procfs_ = FtraceProcfs::Create(kTracingPaths[index++]);
     }
-    if (!ftrace_procfs_)
-      return;
-    ftrace_procfs_->SetTracingOn(false);
-  }
-
-  void TearDown() override {
-    if (ftrace_procfs_)
-      ftrace_procfs_->SetTracingOn(false);
   }
 
   std::unique_ptr<FtraceProcfs> ftrace_procfs_;
@@ -348,10 +340,8 @@
 
 #if PERFETTO_BUILDFLAG(PERFETTO_ANDROID_BUILD)
 #define TreeHuggerOnly(x) x
-#define ExcludeTreeHugger(x) DISABLED_##x
 #else
 #define TreeHuggerOnly(x) DISABLED_##x
-#define ExcludeTreeHugger(x) x
 #endif
 
 // TODO(b/73453011): reenable on more platforms (including standalone Android).
@@ -689,13 +679,21 @@
   EXPECT_FALSE(helper.AttachConsumer("key"));
 }
 
-// TODO(b/148841422): Reenable on treehugger once selinux rules are in place.
-TEST_F(PerfettoTest, ExcludeTreeHugger(TestProducerProvidedSMB)) {
+TEST_F(PerfettoTest, TestProducerProvidedSMB) {
   base::TestTaskRunner task_runner;
 
   TestHelper helper(&task_runner);
   helper.CreateProducerProvidedSmb();
 
+  protos::gen::TestConfig test_config;
+  test_config.set_seed(42);
+  test_config.set_message_count(1);
+  test_config.set_message_size(1024);
+  test_config.set_send_batch_on_register(true);
+
+  // Write a first batch before connection.
+  helper.ProduceStartupEventBatch(test_config);
+
   helper.StartServiceIfRequired();
   helper.ConnectFakeProducer();
   helper.ConnectConsumer();
@@ -708,12 +706,10 @@
   auto* ds_config = trace_config.add_data_sources()->mutable_config();
   ds_config->set_name("android.perfetto.FakeProducer");
   ds_config->set_target_buffer(0);
+  *ds_config->mutable_for_testing() = test_config;
 
-  ds_config->mutable_for_testing()->set_seed(42);
-  ds_config->mutable_for_testing()->set_message_count(1);
-  ds_config->mutable_for_testing()->set_message_size(1024);
-  ds_config->mutable_for_testing()->set_send_batch_on_register(true);
-
+  // The data source is configured to emit another batch when it is started via
+  // send_batch_on_register in the TestConfig.
   helper.StartTracing(trace_config);
   helper.WaitForTracingDisabled();
 
@@ -723,8 +719,11 @@
   helper.WaitForReadData();
 
   const auto& packets = helper.trace();
-  ASSERT_EQ(packets.size(), 1u);
+  // We should have produced two batches, one before the producer connected and
+  // another one when the data source was started.
+  ASSERT_EQ(packets.size(), 2u);
   ASSERT_TRUE(packets[0].has_for_testing());
+  ASSERT_TRUE(packets[1].has_for_testing());
 }
 
 // Disable cmdline tests on sanitizets because they use fork() and that messes
diff --git a/test/fake_producer.cc b/test/fake_producer.cc
index dd5c0e7..784b711 100644
--- a/test/fake_producer.cc
+++ b/test/fake_producer.cc
@@ -22,6 +22,7 @@
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/traced/traced.h"
+#include "perfetto/ext/tracing/core/shared_memory_arbiter.h"
 #include "perfetto/ext/tracing/core/trace_packet.h"
 #include "perfetto/ext/tracing/core/trace_writer.h"
 #include "perfetto/tracing/core/data_source_config.h"
@@ -32,21 +33,27 @@
 
 namespace perfetto {
 
-FakeProducer::FakeProducer(const std::string& name) : name_(name) {}
+namespace {
+const MaybeUnboundBufferID kStartupTargetBufferReservationId = 1;
+}  // namespace
+
+FakeProducer::FakeProducer(const std::string& name,
+                           base::TaskRunner* task_runner)
+    : name_(name), task_runner_(task_runner) {}
 FakeProducer::~FakeProducer() = default;
 
 void FakeProducer::Connect(const char* socket_name,
-                           base::TaskRunner* task_runner,
                            std::function<void()> on_setup_data_source_instance,
                            std::function<void()> on_create_data_source_instance,
-                           std::unique_ptr<SharedMemory> shm) {
+                           std::unique_ptr<SharedMemory> shm,
+                           std::unique_ptr<SharedMemoryArbiter> shm_arbiter) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
-  task_runner_ = task_runner;
   endpoint_ = ProducerIPCClient::Connect(
-      socket_name, this, "android.perfetto.FakeProducer", task_runner,
+      socket_name, this, "android.perfetto.FakeProducer", task_runner_,
       TracingService::ProducerSMBScrapingMode::kDefault,
       /*shared_memory_size_hint_bytes=*/0,
-      /*shared_memory_page_size_hint_bytes=*/base::kPageSize, std::move(shm));
+      /*shared_memory_page_size_hint_bytes=*/base::kPageSize, std::move(shm),
+      std::move(shm_arbiter));
   on_setup_data_source_instance_ = std::move(on_setup_data_source_instance);
   on_create_data_source_instance_ = std::move(on_create_data_source_instance);
 }
@@ -71,13 +78,17 @@
 void FakeProducer::StartDataSource(DataSourceInstanceID,
                                    const DataSourceConfig& source_config) {
   PERFETTO_DCHECK_THREAD(thread_checker_);
-  trace_writer_ = endpoint_->CreateTraceWriter(
-      static_cast<BufferID>(source_config.target_buffer()));
-  rnd_engine_ = std::minstd_rand0(source_config.for_testing().seed());
-  message_count_ = source_config.for_testing().message_count();
-  message_size_ = source_config.for_testing().message_size();
-  max_messages_per_second_ =
-      source_config.for_testing().max_messages_per_second();
+  if (trace_writer_) {
+    // Startup tracing was already active, just bind the target buffer.
+    endpoint_->MaybeSharedMemoryArbiter()->BindStartupTargetBuffer(
+        kStartupTargetBufferReservationId,
+        static_cast<BufferID>(source_config.target_buffer()));
+  } else {
+    // Common case: Start tracing now.
+    trace_writer_ = endpoint_->CreateTraceWriter(
+        static_cast<BufferID>(source_config.target_buffer()));
+    SetupFromConfig(source_config.for_testing());
+  }
   if (source_config.for_testing().send_batch_on_register()) {
     ProduceEventBatch(on_create_data_source_instance_);
   } else {
@@ -91,49 +102,29 @@
 }
 
 // Note: this can be called on a different thread.
-void FakeProducer::ProduceEventBatch(std::function<void()> callback) {
-  task_runner_->PostTask([this, callback] {
-    PERFETTO_CHECK(trace_writer_);
-    PERFETTO_CHECK(message_size_ > 1);
-    std::unique_ptr<char, base::FreeDeleter> payload(
-        static_cast<char*>(malloc(message_size_)));
-    memset(payload.get(), '.', message_size_);
-    payload.get()[message_size_ - 1] = 0;
+void FakeProducer::ProduceStartupEventBatch(
+    const protos::gen::TestConfig& config,
+    SharedMemoryArbiter* arbiter,
+    std::function<void()> callback) {
+  task_runner_->PostTask([this, config, arbiter, callback] {
+    SetupFromConfig(config);
 
-    base::TimeMillis start = base::GetWallTimeMs();
-    int64_t iterations = 0;
-    uint32_t messages_to_emit = message_count_;
-    while (messages_to_emit > 0) {
-      uint32_t messages_in_minibatch =
-          max_messages_per_second_ == 0
-              ? messages_to_emit
-              : std::min(max_messages_per_second_, messages_to_emit);
-      PERFETTO_DCHECK(messages_to_emit >= messages_in_minibatch);
+    PERFETTO_CHECK(!trace_writer_);
+    trace_writer_ =
+        arbiter->CreateStartupTraceWriter(kStartupTargetBufferReservationId);
 
-      for (uint32_t i = 0; i < messages_in_minibatch; i++) {
-        auto handle = trace_writer_->NewTracePacket();
-        handle->set_for_testing()->set_seq_value(
-            static_cast<uint32_t>(rnd_engine_()));
-        handle->set_for_testing()->set_str(payload.get(), message_size_);
-      }
-      messages_to_emit -= messages_in_minibatch;
-      iterations++;
+    EmitEventBatchOnTaskRunner({});
 
-      // Pause until the second boundary to make sure that we are adhering to
-      // the speed limitation.
-      if (max_messages_per_second_ > 0) {
-        int64_t expected_time_taken = iterations * 1000;
-        base::TimeMillis time_taken = base::GetWallTimeMs() - start;
-        while (time_taken.count() < expected_time_taken) {
-          usleep(static_cast<useconds_t>(
-              (expected_time_taken - time_taken.count()) * 1000));
-          time_taken = base::GetWallTimeMs() - start;
-        }
-      }
-      trace_writer_->Flush(messages_to_emit > 0 ? [] {} : callback);
-    }
+    // Issue callback right after writing - cannot wait for flush yet because
+    // we're not connected yet.
+    callback();
   });
 }
+// Note: this can be called on a different thread.
+void FakeProducer::ProduceEventBatch(std::function<void()> callback) {
+  task_runner_->PostTask(
+      [this, callback] { EmitEventBatchOnTaskRunner(callback); });
+}
 
 void FakeProducer::OnTracingSetup() {}
 
@@ -146,4 +137,53 @@
   endpoint_->NotifyFlushComplete(flush_request_id);
 }
 
+void FakeProducer::SetupFromConfig(const protos::gen::TestConfig& config) {
+  rnd_engine_ = std::minstd_rand0(config.seed());
+  message_count_ = config.message_count();
+  message_size_ = config.message_size();
+  max_messages_per_second_ = config.max_messages_per_second();
+}
+
+void FakeProducer::EmitEventBatchOnTaskRunner(std::function<void()> callback) {
+  PERFETTO_CHECK(trace_writer_);
+  PERFETTO_CHECK(message_size_ > 1);
+  std::unique_ptr<char, base::FreeDeleter> payload(
+      static_cast<char*>(malloc(message_size_)));
+  memset(payload.get(), '.', message_size_);
+  payload.get()[message_size_ - 1] = 0;
+
+  base::TimeMillis start = base::GetWallTimeMs();
+  int64_t iterations = 0;
+  uint32_t messages_to_emit = message_count_;
+  while (messages_to_emit > 0) {
+    uint32_t messages_in_minibatch =
+        max_messages_per_second_ == 0
+            ? messages_to_emit
+            : std::min(max_messages_per_second_, messages_to_emit);
+    PERFETTO_DCHECK(messages_to_emit >= messages_in_minibatch);
+
+    for (uint32_t i = 0; i < messages_in_minibatch; i++) {
+      auto handle = trace_writer_->NewTracePacket();
+      handle->set_for_testing()->set_seq_value(
+          static_cast<uint32_t>(rnd_engine_()));
+      handle->set_for_testing()->set_str(payload.get(), message_size_);
+    }
+    messages_to_emit -= messages_in_minibatch;
+    iterations++;
+
+    // Pause until the second boundary to make sure that we are adhering to
+    // the speed limitation.
+    if (max_messages_per_second_ > 0) {
+      int64_t expected_time_taken = iterations * 1000;
+      base::TimeMillis time_taken = base::GetWallTimeMs() - start;
+      while (time_taken.count() < expected_time_taken) {
+        usleep(static_cast<useconds_t>(
+            (expected_time_taken - time_taken.count()) * 1000));
+        time_taken = base::GetWallTimeMs() - start;
+      }
+    }
+    trace_writer_->Flush(messages_to_emit > 0 ? [] {} : callback);
+  }
+}
+
 }  // namespace perfetto
diff --git a/test/fake_producer.h b/test/fake_producer.h
index 46af80e..f71d90e 100644
--- a/test/fake_producer.h
+++ b/test/fake_producer.h
@@ -30,16 +30,30 @@
 
 namespace perfetto {
 
+namespace protos {
+namespace gen {
+class TestConfig;
+}  // namespace gen
+}  // namespace protos
+
 class FakeProducer : public Producer {
  public:
-  explicit FakeProducer(const std::string& name);
+  explicit FakeProducer(const std::string& name, base::TaskRunner* task_runner);
   ~FakeProducer() override;
 
   void Connect(const char* socket_name,
-               base::TaskRunner* task_runner,
                std::function<void()> on_setup_data_source_instance,
                std::function<void()> on_create_data_source_instance,
-               std::unique_ptr<SharedMemory> shm = nullptr);
+               std::unique_ptr<SharedMemory> shm = nullptr,
+               std::unique_ptr<SharedMemoryArbiter> shm_arbiter = nullptr);
+
+  // Produces a batch of events (as configured by the passed config) before the
+  // producer is connected to the service using the provided unbound arbiter.
+  // Posts |callback| once the data was written. May only be called once.
+  void ProduceStartupEventBatch(
+      const protos::gen::TestConfig& config,
+      SharedMemoryArbiter* arbiter,
+      std::function<void()> callback = [] {});
 
   // Produces a batch of events (as configured in the DataSourceConfig) and
   // posts a callback when the service acknowledges the commit.
@@ -63,11 +77,12 @@
                              size_t /*num_data_sources*/) override {}
 
  private:
-  void Shutdown();
+  void SetupFromConfig(const protos::gen::TestConfig& config);
+  void EmitEventBatchOnTaskRunner(std::function<void()> callback);
 
   base::ThreadChecker thread_checker_;
-  base::TaskRunner* task_runner_ = nullptr;
   std::string name_;
+  base::TaskRunner* task_runner_ = nullptr;
   std::minstd_rand0 rnd_engine_;
   uint32_t message_size_ = 0;
   uint32_t message_count_ = 0;
diff --git a/test/gtest_logcat_printer.cc b/test/gtest_logcat_printer.cc
new file mode 100644
index 0000000..f88e01b
--- /dev/null
+++ b/test/gtest_logcat_printer.cc
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+// This file registers a GTest listener that logs test begin/end in logcat.
+// This is to avoid problems like b/149852934 where the test output cannot be
+// correlated with the prouction code's logcat logs because they use different
+// clock sources.
+
+#include <android/log.h>
+
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace test {
+
+namespace {
+
+#define PERFETTO_TEST_LOG(...) \
+  __android_log_print(ANDROID_LOG_DEBUG, "perfetto", ##__VA_ARGS__)
+
+class LogcatPrinter : public testing::EmptyTestEventListener {
+ public:
+  LogcatPrinter();
+  ~LogcatPrinter() override;
+
+  // testing::EmptyTestEventListener:
+  void OnTestCaseStart(const testing::TestCase& test_case) override;
+  void OnTestStart(const testing::TestInfo& test_info) override;
+  void OnTestEnd(const testing::TestInfo& test_info) override;
+  void OnTestCaseEnd(const testing::TestCase& test_case) override;
+};
+
+LogcatPrinter::LogcatPrinter() = default;
+LogcatPrinter::~LogcatPrinter() = default;
+
+void LogcatPrinter::OnTestCaseStart(const testing::TestCase& test_case) {
+  PERFETTO_TEST_LOG("Test case start: %s", test_case.name());
+}
+
+void LogcatPrinter::OnTestStart(const testing::TestInfo& test_info) {
+  PERFETTO_TEST_LOG("Test start: %s.%s", test_info.test_case_name(),
+                    test_info.name());
+}
+
+void LogcatPrinter::OnTestEnd(const testing::TestInfo& test_info) {
+  const auto* result = test_info.result();
+  const char* state = "N/A";
+  if (result)
+    state = result->Passed() ? "PASS" : "FAIL";
+  PERFETTO_TEST_LOG("Test end: %s.%s [%s]", test_info.test_case_name(),
+                    test_info.name(), state);
+}
+
+void LogcatPrinter::OnTestCaseEnd(const testing::TestCase& test_case) {
+  PERFETTO_TEST_LOG("Test case end: %s. succeeded=%d, failed=%d",
+                    test_case.name(), test_case.successful_test_count(),
+                    test_case.failed_test_count());
+}
+
+// This static initializer
+void __attribute__((constructor)) __attribute__((visibility("default")))
+SetupGtestLogcatPrinter() {
+  static LogcatPrinter* instance = new LogcatPrinter();
+  auto& listeners = testing::UnitTest::GetInstance()->listeners();
+  listeners.Append(instance);
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace perfetto
diff --git a/test/metrics/java_heap_stats.out b/test/metrics/java_heap_stats.out
index 3314b44..d5421f7 100644
--- a/test/metrics/java_heap_stats.out
+++ b/test/metrics/java_heap_stats.out
@@ -7,7 +7,7 @@
     }
     samples {
       ts: 10
-      heap_size: 736
+      heap_size: 992
       reachable_heap_size: 352
     }
   }
diff --git a/test/metrics/obfuscated_heap_graph.textproto b/test/metrics/obfuscated_heap_graph.textproto
index b6654fe..4eb9ecf 100644
--- a/test/metrics/obfuscated_heap_graph.textproto
+++ b/test/metrics/obfuscated_heap_graph.textproto
@@ -37,6 +37,11 @@
       type_id: 2
       self_size: 32
     }
+    objects {
+      id: 0x03
+      type_id: 3
+      self_size: 32
+    }
     continued: true
     index: 1
   }
@@ -53,6 +58,10 @@
       iid: 2
       str: "a"
     }
+    type_names {
+      iid: 3
+      str: "java.lang.Class<abc[]>"
+    }
     field_names {
       iid: 1
       str: "FactoryProducerDelegateImplActor.a"
diff --git a/test/metrics/unmapped_java_symbols.out b/test/metrics/unmapped_java_symbols.out
index 5888642..4628dd9 100644
--- a/test/metrics/unmapped_java_symbols.out
+++ b/test/metrics/unmapped_java_symbols.out
@@ -6,6 +6,7 @@
     }
     type_name: "FactoryProducerDelegateImplActor"
     type_name: "a"
+    type_name: "abc"
     field {
       field_name: "FactoryProducerDelegateImplActor.a"
     }
diff --git a/test/test_helper.cc b/test/test_helper.cc
index ced1f96..0e16645 100644
--- a/test/test_helper.cc
+++ b/test/test_helper.cc
@@ -122,6 +122,14 @@
   return fake_producer_thread_.producer()->IsShmemProvidedByProducer();
 }
 
+void TestHelper::ProduceStartupEventBatch(
+    const protos::gen::TestConfig& config) {
+  auto on_data_written = CreateCheckpoint("startup_data_written");
+  fake_producer_thread_.ProduceStartupEventBatch(config,
+                                                 WrapTask(on_data_written));
+  RunUntilCheckpoint("startup_data_written");
+}
+
 void TestHelper::StartTracing(const TraceConfig& config,
                               base::ScopedFile file) {
   trace_.clear();
diff --git a/test/test_helper.h b/test/test_helper.h
index 46ae6de..8617466 100644
--- a/test/test_helper.h
+++ b/test/test_helper.h
@@ -20,6 +20,7 @@
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/thread_task_runner.h"
 #include "perfetto/ext/tracing/core/consumer.h"
+#include "perfetto/ext/tracing/core/shared_memory_arbiter.h"
 #include "perfetto/ext/tracing/core/trace_packet.h"
 #include "perfetto/ext/tracing/ipc/consumer_ipc_client.h"
 #include "perfetto/ext/tracing/ipc/service_ipc_host.h"
@@ -103,21 +104,23 @@
                      std::function<void()> start_callback)
       : producer_socket_(producer_socket),
         setup_callback_(std::move(setup_callback)),
-        start_callback_(std::move(start_callback)) {}
+        start_callback_(std::move(start_callback)) {
+    runner_ = base::ThreadTaskRunner::CreateAndStart("perfetto.prd.fake");
+    runner_->PostTaskAndWaitForTesting([this]() {
+      producer_.reset(
+          new FakeProducer("android.perfetto.FakeProducer", runner_->get()));
+    });
+  }
 
   ~FakeProducerThread() {
-    if (!runner_)
-      return;
     runner_->PostTaskAndWaitForTesting([this]() { producer_.reset(); });
   }
 
   void Connect() {
-    runner_ = base::ThreadTaskRunner::CreateAndStart("perfetto.prd.fake");
     runner_->PostTaskAndWaitForTesting([this]() {
-      producer_.reset(new FakeProducer("android.perfetto.FakeProducer"));
-      producer_->Connect(producer_socket_.c_str(), runner_->get(),
-                         std::move(setup_callback_), std::move(start_callback_),
-                         std::move(shm_));
+      producer_->Connect(producer_socket_.c_str(), std::move(setup_callback_),
+                         std::move(start_callback_), std::move(shm_),
+                         std::move(shm_arbiter_));
     });
   }
 
@@ -128,6 +131,14 @@
   void CreateProducerProvidedSmb() {
     PosixSharedMemory::Factory factory;
     shm_ = factory.CreateSharedMemory(1024 * 1024);
+    shm_arbiter_ =
+        SharedMemoryArbiter::CreateUnboundInstance(shm_.get(), base::kPageSize);
+  }
+
+  void ProduceStartupEventBatch(const protos::gen::TestConfig& config,
+                                std::function<void()> callback) {
+    PERFETTO_CHECK(shm_arbiter_);
+    producer_->ProduceStartupEventBatch(config, shm_arbiter_.get(), callback);
   }
 
  private:
@@ -138,6 +149,7 @@
   std::function<void()> setup_callback_;
   std::function<void()> start_callback_;
   std::unique_ptr<SharedMemory> shm_;
+  std::unique_ptr<SharedMemoryArbiter> shm_arbiter_;
 };
 
 class TestHelper : public Consumer {
@@ -169,6 +181,7 @@
   bool AttachConsumer(const std::string& key);
   void CreateProducerProvidedSmb();
   bool IsShmemProvidedByProducer();
+  void ProduceStartupEventBatch(const protos::gen::TestConfig& config);
 
   void WaitForConsumerConnect();
   void WaitForProducerSetup();
diff --git a/test/trace_processor/heap_graph.textproto b/test/trace_processor/heap_graph.textproto
index 44b99f6..de4d5b4 100644
--- a/test/trace_processor/heap_graph.textproto
+++ b/test/trace_processor/heap_graph.textproto
@@ -55,6 +55,11 @@
       type_id: 4
       self_size: 256
     }
+    objects {
+      id: 0x06
+      type_id: 5
+      self_size: 256
+    }
     continued: true
     index: 0
   }
@@ -79,6 +84,10 @@
       iid: 4
       str: "a[]"
     }
+    type_names {
+      iid: 5
+      str: "java.lang.Class<a[]>"
+    }
     field_names {
       iid: 1
       str: "FactoryProducerDelegateImplActor.foo"
diff --git a/test/trace_processor/heap_graph_object.out b/test/trace_processor/heap_graph_object.out
index b2cf862..a5cf29e 100644
--- a/test/trace_processor/heap_graph_object.out
+++ b/test/trace_processor/heap_graph_object.out
@@ -4,3 +4,4 @@
 2,"heap_graph_object",2,10,3,128,-1,-1,2,0,"Foo","[NULL]","[NULL]"
 3,"heap_graph_object",2,10,4,256,-1,-1,2,0,"a","DeobfuscatedA","[NULL]"
 4,"heap_graph_object",2,10,5,256,-1,-1,3,1,"a[]","DeobfuscatedA[]","ROOT_JAVA_FRAME"
+5,"heap_graph_object",2,10,6,256,-1,-1,3,0,"java.lang.Class<a[]>","java.lang.Class<DeobfuscatedA[]>","[NULL]"
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 59d5a11..95a4c39 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -63,6 +63,7 @@
     '//src/traced/service:traced',
     '//test/cts:perfetto_cts_deps',
     '//test/cts:perfetto_cts_jni_deps',
+    '//test:perfetto_gtest_logcat_printer',
 ]
 
 # Host targets
@@ -184,6 +185,8 @@
 def enable_gtest_and_gmock(module):
   module.static_libs.add('libgmock')
   module.static_libs.add('libgtest')
+  if module.name != 'perfetto_gtest_logcat_printer':
+    module.whole_static_libs.add('perfetto_gtest_logcat_printer')
 
 
 def enable_protobuf_full(module):
@@ -310,12 +313,14 @@
     self.name = name
     self.shared_libs = set()
     self.static_libs = set()
+    self.whole_static_libs = set()
     self.cflags = set()
 
   def to_string(self, output):
     nested_out = []
     self._output_field(nested_out, 'shared_libs')
     self._output_field(nested_out, 'static_libs')
+    self._output_field(nested_out, 'whole_static_libs')
     self._output_field(nested_out, 'cflags')
 
     if nested_out:
@@ -340,6 +345,7 @@
     self.comment = 'GN: ' + gn_utils.label_without_toolchain(gn_target)
     self.shared_libs = set()
     self.static_libs = set()
+    self.whole_static_libs = set()
     self.tools = set()
     self.cmd = None
     self.host_supported = False
@@ -375,6 +381,7 @@
     self._output_field(output, 'srcs')
     self._output_field(output, 'shared_libs')
     self._output_field(output, 'static_libs')
+    self._output_field(output, 'whole_static_libs')
     self._output_field(output, 'tools')
     self._output_field(output, 'cmd', sort=False)
     self._output_field(output, 'host_supported')
diff --git a/traced_perf.rc b/traced_perf.rc
index b38b7c4..b4edcfa 100644
--- a/traced_perf.rc
+++ b/traced_perf.rc
@@ -14,7 +14,6 @@
 
 service traced_perf /system/bin/traced_perf
     class late_start
-    disabled
     # socket for receiving /proc/pid/{maps,mem} file descriptors
     socket traced_perf stream 0666 root root
     user nobody
diff --git a/ui/index.html b/ui/index.html
index 5a9190f..a893d30 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -2,6 +2,8 @@
 <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/actions.ts b/ui/src/common/actions.ts
index 13d1e39..ffd33eb 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,9 +15,14 @@
 import {Draft} from 'immer';
 
 import {assertExists} from '../base/logging';
-import {Area, CallsiteInfo} from '../common/state';
+import {
+  Area,
+  CallsiteInfo,
+  HeapProfileFlamegraphViewingOption
+} from '../common/state';
 import {ConvertTrace, ConvertTraceToPprof} from '../controller/trace_converter';
 
+import {DEFAULT_VIEWING_OPTION} from './flamegraph_util';
 import {
   AdbRecordingTarget,
   createEmptyState,
@@ -461,6 +466,7 @@
       upid: args.upid,
       ts: args.ts,
       type: args.type,
+      viewingOption: DEFAULT_VIEWING_OPTION,
     };
   },
 
@@ -471,7 +477,8 @@
   },
 
   changeViewHeapProfileFlamegraph(
-      state: StateDraft, args: {viewingOption: string}): void {
+      state: StateDraft,
+      args: {viewingOption: HeapProfileFlamegraphViewingOption}): void {
     if (state.currentHeapProfileFlamegraph === null) return;
     state.currentHeapProfileFlamegraph.viewingOption = args.viewingOption;
   },
diff --git a/ui/src/common/flamegraph_util.ts b/ui/src/common/flamegraph_util.ts
index 4f134c5..a94eddd 100644
--- a/ui/src/common/flamegraph_util.ts
+++ b/ui/src/common/flamegraph_util.ts
@@ -14,10 +14,10 @@
 
 import {CallsiteInfo} from '../common/state';
 
-export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'space';
-export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'alloc_space';
-export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'objects';
-export const OBJECTS_ALLOCATED_KEY = 'alloc_objects';
+export const SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE';
+export const ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE';
+export const OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS';
+export const OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS';
 
 export const DEFAULT_VIEWING_OPTION = SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY;
 
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index a761961..5bd331a 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -48,6 +48,9 @@
 
 export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM';
 
+export type HeapProfileFlamegraphViewingOption =
+    'SPACE'|'ALLOC_SPACE'|'OBJECTS'|'ALLOC_OBJECTS';
+
 export interface CallsiteInfo {
   id: number;
   parentId: number;
@@ -182,8 +185,8 @@
   upid: number;
   ts: number;
   type: string;
+  viewingOption: HeapProfileFlamegraphViewingOption;
   expandedCallsite?: CallsiteInfo;
-  viewingOption?: string;
 }
 
 export interface ChromeSliceSelection {
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 4607a32..327a411 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -664,15 +664,33 @@
 
         const name =
             this.getTrackName(utid, processName, pid, threadName, tid, upid);
-        addTrackGroupActions.push(Actions.addTrackGroup({
+        const addTrackGroup = Actions.addTrackGroup({
           engineId: this.engineId,
           summaryTrackId,
           name,
           id: pUuid,
           collapsed: !(upid !== null && heapUpids.has(upid)),
-        }));
+        });
+
+        // If the track group contains a heap profile, it should be before all
+        // other processes.
+        if (upid !== null && heapUpids.has(upid)) {
+          addTrackGroupActions.unshift(addTrackGroup);
+        } else {
+          addTrackGroupActions.push(addTrackGroup);
+        }
 
         if (upid !== null) {
+          if (heapUpids.has(upid)) {
+            tracksToAdd.push({
+              engineId: this.engineId,
+              kind: HEAP_PROFILE_TRACK_KIND,
+              name: `Heap Profile`,
+              trackGroup: pUuid,
+              config: {upid}
+            });
+          }
+
           const counterNames = counterUpids.get(upid);
           if (counterNames !== undefined) {
             counterNames.forEach(element => {
@@ -691,16 +709,6 @@
             });
           }
 
-          if (heapUpids.has(upid)) {
-            tracksToAdd.push({
-              engineId: this.engineId,
-              kind: HEAP_PROFILE_TRACK_KIND,
-              name: `Heap Profile`,
-              trackGroup: pUuid,
-              config: {upid}
-            });
-          }
-
           if (upidToProcessTracks.has(upid)) {
             for (const track of upidToProcessTracks.get(upid)) {
               tracksToAdd.push(Object.assign(track, {trackGroup: pUuid}));
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index 57f95c9..a966a45 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -328,6 +328,7 @@
     const endSec = Math.min(ts.end, globals.state.traceTime.endSec);
     this.visibleWindowTime = new TimeSpan(startSec, endSec);
     this.timeScale.setTimeBounds(this.visibleWindowTime);
+    this.updateResolution(this.timeScale.startPx, this.timeScale.endPx);
   }
 
   // We lock an area selection by adding an area note. When we select the note
@@ -361,6 +362,7 @@
 
   updateResolution(pxStart: number, pxEnd: number) {
     this.timeScale.setLimitsPx(pxStart, pxEnd);
+    this._visibleState.lastUpdate = Date.now() / 1000;
     this._visibleState.resolution = globals.getCurResolution();
     this.ratelimitedUpdateVisible();
   }
diff --git a/ui/src/frontend/heap_profile_panel.ts b/ui/src/frontend/heap_profile_panel.ts
index a210a9e..85b8b7a 100644
--- a/ui/src/frontend/heap_profile_panel.ts
+++ b/ui/src/frontend/heap_profile_panel.ts
@@ -17,11 +17,11 @@
 import {Actions} from '../common/actions';
 import {
   ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-  DEFAULT_VIEWING_OPTION,
   OBJECTS_ALLOCATED_KEY,
   OBJECTS_ALLOCATED_NOT_FREED_KEY,
-  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
+  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
 } from '../common/flamegraph_util';
+import {HeapProfileFlamegraphViewingOption} from '../common/state';
 import {timeToCode} from '../common/time';
 
 import {Flamegraph} from './flamegraph';
@@ -37,7 +37,6 @@
   private ts = 0;
   private pid = 0;
   private flamegraph: Flamegraph = new Flamegraph([]);
-  private currentViewingOption = DEFAULT_VIEWING_OPTION;
 
   view() {
     const heapDumpInfo = globals.heapProfileDetails;
@@ -106,8 +105,11 @@
     }
   }
 
-  getButtonsClass(viewingOption = DEFAULT_VIEWING_OPTION): string {
-    return this.currentViewingOption === viewingOption ? '.chosen' : '';
+  getButtonsClass(button: HeapProfileFlamegraphViewingOption): string {
+    if (globals.state.currentHeapProfileFlamegraph === null) return '';
+    return globals.state.currentHeapProfileFlamegraph.viewingOption === button ?
+        '.chosen' :
+        '';
   }
 
   getViewingOptionButtons(): m.Children {
@@ -126,7 +128,7 @@
               this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY);
             }
           },
-          'alloc_space'),
+          'alloc space'),
         m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`,
           {
             onclick: () => {
@@ -140,11 +142,10 @@
               this.changeViewingOption(OBJECTS_ALLOCATED_KEY);
             }
           },
-          'alloc_objects'));
+          'alloc objects'));
   }
 
-  changeViewingOption(viewingOption: string) {
-    this.currentViewingOption = viewingOption;
+  changeViewingOption(viewingOption: HeapProfileFlamegraphViewingOption) {
     globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption}));
   }
 
@@ -164,9 +165,11 @@
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     this.changeFlamegraphData();
+    const current = globals.state.currentHeapProfileFlamegraph;
+    if (current === null) return;
     const unit =
-        this.currentViewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
-            this.currentViewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
+        current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
+            current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
         'B' :
         '';
     this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit);