TrackEvent: Add category tags and groups

This patch makes it possible to classify a single trace point to
multiple categories:

  TRACE_EVENT("benchmark,input", "Name);

Each category group must be registered at build time:

  PERFETTO_DEFINE_CATEGORIES(
    perfetto::Category::Group("benchmark,input")
  );

Additionally, we introduce a builder pattern for adding properties
such as tags and descriptions to categories:

  PERFETTO_DEFINE_CATEGORIES(
    perfetto::Category("moon_lander")
        .SetDescription("Events from the moon lander module"),
    perfetto::Category("moon_lander.verbose")
        .SetDescription("Verbose events from the moon lander module")
        .SetTags("slow")
  );

Design doc: https://docs.google.com/document/d/1YSmRh1g8QuyxHQxN26m_uUNN-F-RzwHidARbWvPeApY/

Bug: 148779455
Change-Id: I453a7b1539894361d4a72200154cc8bf8f43823a
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..1de69aa 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 */                    \
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..8346dc4 100644
--- a/include/perfetto/tracing/track_event_category_registry.h
+++ b/include/perfetto/tracing/track_event_category_registry.h
@@ -23,6 +23,126 @@
 #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 ssize_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 +186,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 +205,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 +268,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 +278,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/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index d27b9ef..1b740c2 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/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];
 }