Merge "tools: add utilities to record traces with reporter API" into main
diff --git a/Android.bp b/Android.bp
index dac6686..d4db032 100644
--- a/Android.bp
+++ b/Android.bp
@@ -13295,7 +13295,6 @@
         ":perfetto_src_trace_processor_util_zip_reader",
         ":perfetto_src_trace_processor_views_views",
         "src/trace_processor/trace_processor_shell.cc",
-        "src/trace_processor/util/proto_to_json.cc",
     ],
     static_libs: [
         "perfetto_src_trace_processor_demangle",
diff --git a/BUILD b/BUILD
index afbb6b8..8cc2689 100644
--- a/BUILD
+++ b/BUILD
@@ -5436,8 +5436,6 @@
         ":src_trace_processor_util_zip_reader",
         ":src_trace_processor_views_views",
         "src/trace_processor/trace_processor_shell.cc",
-        "src/trace_processor/util/proto_to_json.cc",
-        "src/trace_processor/util/proto_to_json.h",
     ],
     visibility = [
         "//visibility:public",
diff --git a/include/perfetto/public/pb_utils.h b/include/perfetto/public/pb_utils.h
index ef4ed97..32ea759 100644
--- a/include/perfetto/public/pb_utils.h
+++ b/include/perfetto/public/pb_utils.h
@@ -123,4 +123,16 @@
          PERFETTO_STATIC_CAST(uint64_t, value >> 63);
 }
 
+static inline int32_t PerfettoPbZigZagDecode32(uint32_t value) {
+  uint32_t mask =
+      PERFETTO_STATIC_CAST(uint32_t, -PERFETTO_STATIC_CAST(int32_t, value & 1));
+  return PERFETTO_STATIC_CAST(int32_t, ((value >> 1) ^ mask));
+}
+
+static inline int64_t PerfettoPbZigZagDecode64(uint64_t value) {
+  uint64_t mask =
+      PERFETTO_STATIC_CAST(uint64_t, -PERFETTO_STATIC_CAST(int64_t, value & 1));
+  return PERFETTO_STATIC_CAST(int64_t, ((value >> 1) ^ mask));
+}
+
 #endif  // INCLUDE_PERFETTO_PUBLIC_PB_UTILS_H_
diff --git a/include/perfetto/trace_processor/read_trace.h b/include/perfetto/trace_processor/read_trace.h
index 98b3ddd..415d7d2 100644
--- a/include/perfetto/trace_processor/read_trace.h
+++ b/include/perfetto/trace_processor/read_trace.h
@@ -17,6 +17,7 @@
 #ifndef INCLUDE_PERFETTO_TRACE_PROCESSOR_READ_TRACE_H_
 #define INCLUDE_PERFETTO_TRACE_PROCESSOR_READ_TRACE_H_
 
+#include <cstdint>
 #include <functional>
 #include <vector>
 
diff --git a/perfetto.rc b/perfetto.rc
index 2118172..8a5ee38 100644
--- a/perfetto.rc
+++ b/perfetto.rc
@@ -19,7 +19,7 @@
     socket traced_producer stream 0666 root root
     user nobody
     group nobody
-    task_profiles ServiceCapacityLow
+    task_profiles ProcessCapacityHigh
 
 service traced_probes /system/bin/traced_probes
     class late_start
@@ -28,7 +28,7 @@
     # Despite the "log" group below, traced_probes is allowed to read log
     # only on userdebug/eng via selinux (see traced_probes.te).
     group nobody readproc log readtracefs
-    task_profiles ServiceCapacityLow
+    task_profiles ProcessCapacityHigh
     # Clean up procfs configuration even if traced_probes crashes
     # unexpectedly.
     onrestart exec_background - nobody shell -- /system/bin/traced_probes --cleanup-after-crash
diff --git a/protos/perfetto/common/descriptor.proto b/protos/perfetto/common/descriptor.proto
index 012401f..6684a70 100644
--- a/protos/perfetto/common/descriptor.proto
+++ b/protos/perfetto/common/descriptor.proto
@@ -82,6 +82,34 @@
   repeated string reserved_name = 10;
 }
 
+// A message representing a option the parser does not recognize. This only
+// appears in options protos created by the compiler::Parser class.
+// DescriptorPool resolves these when building Descriptor objects. Therefore,
+// options protos in descriptor objects (e.g. returned by Descriptor::options(),
+// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions
+// in them.
+message UninterpretedOption {
+  // The name of the uninterpreted option.  Each string represents a segment in
+  // a dot-separated name.  is_extension is true iff a segment represents an
+  // extension (denoted with parentheses in options specs in .proto files).
+  // E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents
+  // "foo.(bar.baz).moo".
+  message NamePart {
+    optional string name_part = 1;
+    optional bool is_extension = 2;
+  }
+  repeated NamePart name = 2;
+
+  // The value of the uninterpreted option, in whatever type the tokenizer
+  // identified it as during parsing. Exactly one of these should be set.
+  optional string identifier_value = 3;
+  optional uint64 positive_int_value = 4;
+  optional int64 negative_int_value = 5;
+  optional double double_value = 6;
+  optional bytes string_value = 7;
+  optional string aggregate_value = 8;
+}
+
 message FieldOptions {
   // The packed option can be enabled for repeated primitive fields to enable
   // a more efficient representation on the wire. Rather than repeatedly
@@ -89,6 +117,9 @@
   // a single length-delimited blob. In proto3, only explicit setting it to
   // false will avoid using packed encoding.
   optional bool packed = 2;
+
+  // The parser stores options it doesn't recognize here. See above.
+  repeated UninterpretedOption uninterpreted_option = 999;
 }
 
 // Describes a field within a message.
diff --git a/protos/perfetto/metrics/BUILD.gn b/protos/perfetto/metrics/BUILD.gn
index 751ed94..e09200c 100644
--- a/protos/perfetto/metrics/BUILD.gn
+++ b/protos/perfetto/metrics/BUILD.gn
@@ -25,7 +25,10 @@
 }
 
 perfetto_proto_library("custom_options_@TYPE@") {
-  proto_generators = [ "source_set" ]
+  proto_generators = [
+    "lite",
+    "source_set",
+  ]
   sources = [ "custom_options.proto" ]
   import_dirs = [ "${perfetto_protobuf_src_dir}" ]
 }
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index 60c60a7..a8b6bc2 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -15,7 +15,11 @@
 import("../../../../gn/proto_library.gni")
 
 perfetto_proto_library("@TYPE@") {
-  proto_generators = [ "source_set" ]
+  proto_generators = [
+    "lite",
+    "source_set",
+  ]
+  import_dirs = [ "${perfetto_protobuf_src_dir}" ]
   deps = [
     "..:@TYPE@",
     "..:custom_options_@TYPE@",
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 70d9546..4479846 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -5231,6 +5231,34 @@
   repeated string reserved_name = 10;
 }
 
+// A message representing a option the parser does not recognize. This only
+// appears in options protos created by the compiler::Parser class.
+// DescriptorPool resolves these when building Descriptor objects. Therefore,
+// options protos in descriptor objects (e.g. returned by Descriptor::options(),
+// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions
+// in them.
+message UninterpretedOption {
+  // The name of the uninterpreted option.  Each string represents a segment in
+  // a dot-separated name.  is_extension is true iff a segment represents an
+  // extension (denoted with parentheses in options specs in .proto files).
+  // E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents
+  // "foo.(bar.baz).moo".
+  message NamePart {
+    optional string name_part = 1;
+    optional bool is_extension = 2;
+  }
+  repeated NamePart name = 2;
+
+  // The value of the uninterpreted option, in whatever type the tokenizer
+  // identified it as during parsing. Exactly one of these should be set.
+  optional string identifier_value = 3;
+  optional uint64 positive_int_value = 4;
+  optional int64 negative_int_value = 5;
+  optional double double_value = 6;
+  optional bytes string_value = 7;
+  optional string aggregate_value = 8;
+}
+
 message FieldOptions {
   // The packed option can be enabled for repeated primitive fields to enable
   // a more efficient representation on the wire. Rather than repeatedly
@@ -5238,6 +5266,9 @@
   // a single length-delimited blob. In proto3, only explicit setting it to
   // false will avoid using packed encoding.
   optional bool packed = 2;
+
+  // The parser stores options it doesn't recognize here. See above.
+  repeated UninterpretedOption uninterpreted_option = 999;
 }
 
 // Describes a field within a message.
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index cc5813f..eca1b1a 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index bac9122..ef4ff29 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -56,6 +56,24 @@
     ])
 
 
+class AllowList(object):
+
+  def __init__(self, allowed, dst, reasoning):
+    self.allowed = allowed
+    self.dst = dst
+    self.reasoning = reasoning
+
+  def check(self, graph):
+    for node, edges in graph.items():
+      for edge in edges:
+        if re.match(self.dst, edge):
+          if not any(re.match(a, node) for a in self.allowed):
+            yield Failure([node, edge], self)
+
+  def __str__(self):
+    return f'Only items in the allowlist ({self.allowed}) may directly depend on "{self.dst}" ' + self.reasoning
+
+
 class NoDirectDep(object):
 
   def __init__(self, src, dst, reasoning):
@@ -114,6 +132,11 @@
 # NoDep(a, b) = as above but 'a' may not even transitively import 'b'.
 # NoCircularDeps = forbid introduction of circular dependencies
 RULES = [
+    AllowList(
+        ['/core/protos'],
+        r'/gen/protos',
+        'protos should be re-exported from /core/protos without the nesting.',
+    ),
     NoDirectDep(
         r'/plugins/.*',
         r'/core/.*',
diff --git a/src/protozero/test/fake_scattered_buffer.cc b/src/protozero/test/fake_scattered_buffer.cc
index 7b65b29..1c36063 100644
--- a/src/protozero/test/fake_scattered_buffer.cc
+++ b/src/protozero/test/fake_scattered_buffer.cc
@@ -16,6 +16,7 @@
 
 #include "src/protozero/test/fake_scattered_buffer.h"
 
+#include <iomanip>
 #include <sstream>
 #include <utility>
 
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index 24b2ec1..7a2889d 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -249,13 +249,13 @@
       return thiz->ds2_callbacks_.OnStart(ds_impl, inst_id, thiz->ds2_user_arg_,
                                           inst_ctx, args);
     };
-    params.on_stop_cb =
-        [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
-           void* user_arg, void* inst_ctx, struct PerfettoDsOnStopArgs* args) {
-          auto* thiz = static_cast<SharedLibDataSourceTest*>(user_arg);
-          return thiz->ds2_callbacks_.OnStop(
-              ds_impl, inst_id, thiz->ds2_user_arg_, inst_ctx, args);
-        };
+    params.on_stop_cb = [](struct PerfettoDsImpl* ds_impl,
+                           PerfettoDsInstanceIndex inst_id, void* user_arg,
+                           void* inst_ctx, struct PerfettoDsOnStopArgs* args) {
+      auto* thiz = static_cast<SharedLibDataSourceTest*>(user_arg);
+      return thiz->ds2_callbacks_.OnStop(ds_impl, inst_id, thiz->ds2_user_arg_,
+                                         inst_ctx, args);
+    };
     params.on_destroy_cb = [](struct PerfettoDsImpl* ds_impl, void* user_arg,
                               void* inst_ctx) -> void {
       auto* thiz = static_cast<SharedLibDataSourceTest*>(user_arg);
@@ -792,6 +792,71 @@
   EXPECT_TRUE(found);
 }
 
+TEST_F(SharedLibTrackEventTest, TrackEventHlDynamicCategoryMultipleSessions) {
+  TracingSession tracing_session1 = TracingSession::Builder()
+                                        .set_data_source_name("track_event")
+                                        .add_enabled_category("cat1")
+                                        .add_enabled_category("dyn1")
+                                        .add_disabled_category("dyn2")
+                                        .add_disabled_category("*")
+                                        .Build();
+
+  TracingSession tracing_session2 = TracingSession::Builder()
+                                        .set_data_source_name("track_event")
+                                        .add_enabled_category("cat1")
+                                        .add_enabled_category("dyn2")
+                                        .add_disabled_category("dyn1")
+                                        .add_disabled_category("*")
+                                        .Build();
+
+  PERFETTO_TE(PERFETTO_TE_DYNAMIC_CATEGORY,
+              PERFETTO_TE_INSTANT("interned_string"),
+              PERFETTO_TE_DYNAMIC_CATEGORY_STRING("dyn1"));
+  PERFETTO_TE(PERFETTO_TE_DYNAMIC_CATEGORY,
+              PERFETTO_TE_INSTANT("interned_string"),
+              PERFETTO_TE_DYNAMIC_CATEGORY_STRING("dyn2"));
+  PERFETTO_TE(cat1, PERFETTO_TE_INSTANT(""));
+
+  tracing_session1.StopBlocking();
+  std::vector<uint8_t> data1 = tracing_session1.ReadBlocking();
+  EXPECT_THAT(
+      FieldView(data1),
+      Contains(PbField(
+          perfetto_protos_Trace_packet_field_number,
+          MsgField(AllOf(
+              Contains(PbField(
+                  perfetto_protos_TracePacket_track_event_field_number,
+                  MsgField(Contains(PbField(
+                      perfetto_protos_TrackEvent_categories_field_number,
+                      StringField("dyn1")))))),
+              Contains(PbField(
+                  perfetto_protos_TracePacket_interned_data_field_number,
+                  MsgField(Contains(PbField(
+                      perfetto_protos_InternedData_event_names_field_number,
+                      MsgField(Contains(
+                          PbField(perfetto_protos_EventName_name_field_number,
+                                  StringField("interned_string"))))))))))))));
+  tracing_session2.StopBlocking();
+  std::vector<uint8_t> data2 = tracing_session2.ReadBlocking();
+  EXPECT_THAT(
+      FieldView(data2),
+      Contains(PbField(
+          perfetto_protos_Trace_packet_field_number,
+          MsgField(AllOf(
+              Contains(PbField(
+                  perfetto_protos_TracePacket_track_event_field_number,
+                  MsgField(Contains(PbField(
+                      perfetto_protos_TrackEvent_categories_field_number,
+                      StringField("dyn2")))))),
+              Contains(PbField(
+                  perfetto_protos_TracePacket_interned_data_field_number,
+                  MsgField(Contains(PbField(
+                      perfetto_protos_InternedData_event_names_field_number,
+                      MsgField(Contains(
+                          PbField(perfetto_protos_EventName_name_field_number,
+                                  StringField("interned_string"))))))))))))));
+}
+
 TEST_F(SharedLibTrackEventTest, TrackEventHlInstant) {
   TracingSession tracing_session =
       TracingSession::Builder().set_data_source_name("track_event").Build();
diff --git a/src/shared_lib/track_event.cc b/src/shared_lib/track_event.cc
index 46dda54..29d9a8c 100644
--- a/src/shared_lib/track_event.cc
+++ b/src/shared_lib/track_event.cc
@@ -856,10 +856,13 @@
   return res;
 }
 
+// If the category `dyn_cat` is enabled on the data source instance pointed by
+// `ii`, returns immediately. Otherwise, advances `ii` to a data source instance
+// where `dyn_cat` is enabled. If there's no data source instance where
+// `dyn_cat` is enabled, `ii->instance` will be nullptr.
 static void AdvanceToFirstEnabledDynamicCategory(
     perfetto::internal::DataSourceType::InstancesIterator* ii,
     perfetto::internal::DataSourceThreadLocalState* tls_state,
-    perfetto::shlib::TrackEventIncrementalState* incr_state,
     struct PerfettoTeCategoryImpl* cat,
     const PerfettoTeCategoryDescriptor& dyn_cat) {
   perfetto::internal::DataSourceType* ds =
@@ -867,6 +870,9 @@
   for (; ii->instance;
        ds->NextIteration</*Traits=*/perfetto::shlib::TracePointTraits>(
            ii, tls_state, {cat})) {
+    auto* incr_state =
+        static_cast<perfetto::shlib::TrackEventIncrementalState*>(
+            ds->GetIncrementalState(ii->instance, ii->i));
     if (IsDynamicCategoryEnabled(ii->i, incr_state, dyn_cat)) {
       break;
     }
@@ -933,6 +939,13 @@
     ts = TrackEventInternal::GetTraceTime();
   }
 
+  if (PERFETTO_UNLIKELY(dynamic_cat)) {
+    AdvanceToFirstEnabledDynamicCategory(ii, tls_state, cat, *dynamic_cat);
+    if (!ii->instance) {
+      return;
+    }
+  }
+
   const auto& track_event_tls =
       *static_cast<perfetto::shlib::TrackEventTlsState*>(
           ii->instance->data_source_custom_tls.get());
@@ -942,14 +955,6 @@
   ResetIncrementalStateIfRequired(ii->instance->trace_writer.get(), incr_state,
                                   track_event_tls, ts);
 
-  if (PERFETTO_UNLIKELY(dynamic_cat)) {
-    AdvanceToFirstEnabledDynamicCategory(ii, tls_state, incr_state, cat,
-                                         *dynamic_cat);
-    if (!ii->instance) {
-      return;
-    }
-  }
-
   if (registered_track) {
     if (incr_state->seen_track_uuids.insert(registered_track->uuid).second) {
       auto packet = ii->instance->trace_writer->NewTracePacket();
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 13fcc5e..38c5cf0 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -214,11 +214,7 @@
     if (enable_perfetto_trace_processor_httpd) {
       deps += [ "rpc:httpd" ]
     }
-    sources = [
-      "trace_processor_shell.cc",
-      "util/proto_to_json.cc",
-      "util/proto_to_json.h",
-    ]
+    sources = [ "trace_processor_shell.cc" ]
     if ((perfetto_build_standalone || build_with_chromium) &&
         !is_perfetto_build_generator) {
       data_deps = [
diff --git a/src/trace_processor/metrics/metrics_unittest.cc b/src/trace_processor/metrics/metrics_unittest.cc
index 7d90e81..7b2d814 100644
--- a/src/trace_processor/metrics/metrics_unittest.cc
+++ b/src/trace_processor/metrics/metrics_unittest.cc
@@ -19,6 +19,7 @@
 #include <vector>
 
 #include "protos/perfetto/common/descriptor.pbzero.h"
+#include "src/trace_processor/util/descriptors.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
@@ -78,8 +79,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor(
-      "int_value", 1, FieldDescriptorProto::TYPE_INT64, "", false, false));
+  descriptor.AddField(FieldDescriptor("int_value", 1,
+                                      FieldDescriptorProto::TYPE_INT64, "",
+                                      std::vector<uint8_t>(), false, false));
 
   ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(builder.AppendLong("int_value", 12345).ok());
@@ -101,8 +103,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor(
-      "double_value", 1, FieldDescriptorProto::TYPE_DOUBLE, "", false, false));
+  descriptor.AddField(FieldDescriptor("double_value", 1,
+                                      FieldDescriptorProto::TYPE_DOUBLE, "",
+                                      std::vector<uint8_t>(), false, false));
 
   ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(builder.AppendDouble("double_value", 1.2345).ok());
@@ -124,8 +127,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor(
-      "string_value", 1, FieldDescriptorProto::TYPE_STRING, "", false, false));
+  descriptor.AddField(FieldDescriptor("string_value", 1,
+                                      FieldDescriptorProto::TYPE_STRING, "",
+                                      std::vector<uint8_t>(), false, false));
 
   ProtoBuilder builder(&pool, &descriptor);
   ASSERT_TRUE(builder.AppendString("string_value", "hello world!").ok());
@@ -151,15 +155,16 @@
                          ".perfetto.protos.TestProto.NestedProto",
                          ProtoDescriptor::Type::kMessage, std::nullopt);
   nested.AddField(FieldDescriptor("nested_int_value", 1,
-                                  FieldDescriptorProto::TYPE_INT64, "", false,
-                                  false));
+                                  FieldDescriptorProto::TYPE_INT64, "",
+                                  std::vector<uint8_t>(), false, false));
 
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
   auto field =
       FieldDescriptor("nested_value", 1, FieldDescriptorProto::TYPE_MESSAGE,
-                      ".perfetto.protos.TestProto.NestedProto", false, false);
+                      ".perfetto.protos.TestProto.NestedProto",
+                      std::vector<uint8_t>(), false, false);
   field.set_resolved_type_name(".perfetto.protos.TestProto.NestedProto");
   descriptor.AddField(field);
 
@@ -199,8 +204,9 @@
   ProtoDescriptor descriptor("file.proto", ".perfetto.protos",
                              ".perfetto.protos.TestProto",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
-  descriptor.AddField(FieldDescriptor(
-      "rep_int_value", 1, FieldDescriptorProto::TYPE_INT64, "", true, false));
+  descriptor.AddField(FieldDescriptor("rep_int_value", 1,
+                                      FieldDescriptorProto::TYPE_INT64, "",
+                                      std::vector<uint8_t>(), true, false));
 
   RepeatedFieldBuilder rep_builder;
   rep_builder.AddLong(1234);
@@ -246,7 +252,8 @@
                              ".perfetto.protos.TestMessage",
                              ProtoDescriptor::Type::kMessage, std::nullopt);
   FieldDescriptor enum_field("enum_value", 1, FieldDescriptorProto::TYPE_ENUM,
-                             ".perfetto.protos.TestEnum", false, false);
+                             ".perfetto.protos.TestEnum",
+                             std::vector<uint8_t>(), false, false);
   enum_field.set_resolved_type_name(".perfetto.protos.TestEnum");
   descriptor.AddField(enum_field);
   pool.AddProtoDescriptorForTesting(descriptor);
diff --git a/src/trace_processor/perfetto_sql/engine/created_function.cc b/src/trace_processor/perfetto_sql/engine/created_function.cc
index a4f8678..66be287 100644
--- a/src/trace_processor/perfetto_sql/engine/created_function.cc
+++ b/src/trace_processor/perfetto_sql/engine/created_function.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/perfetto_sql/engine/created_function.h"
 
+#include <cstddef>
 #include <queue>
 #include <stack>
 
diff --git a/src/trace_processor/read_trace_internal.h b/src/trace_processor/read_trace_internal.h
index f4bb56e..c946ad7 100644
--- a/src/trace_processor/read_trace_internal.h
+++ b/src/trace_processor/read_trace_internal.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_READ_TRACE_INTERNAL_H_
 #define SRC_TRACE_PROCESSOR_READ_TRACE_INTERNAL_H_
 
+#include <cstdint>
 #include <functional>
 #include <vector>
 
diff --git a/src/trace_processor/sqlite/query_constraints.h b/src/trace_processor/sqlite/query_constraints.h
index a5c299d..6ff7e5b 100644
--- a/src/trace_processor/sqlite/query_constraints.h
+++ b/src/trace_processor/sqlite/query_constraints.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_SQLITE_QUERY_CONSTRAINTS_H_
 #define SRC_TRACE_PROCESSOR_SQLITE_QUERY_CONSTRAINTS_H_
 
+#include <cstdint>
 #include <limits>
 #include <vector>
 
diff --git a/src/trace_processor/sqlite/sql_source.h b/src/trace_processor/sqlite/sql_source.h
index 805105e..7d3220b 100644
--- a/src/trace_processor/sqlite/sql_source.h
+++ b/src/trace_processor/sqlite/sql_source.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_SQLITE_SQL_SOURCE_H_
 #define SRC_TRACE_PROCESSOR_SQLITE_SQL_SOURCE_H_
 
+#include <cstdint>
 #include <optional>
 #include <string>
 #include <string_view>
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 56fb013..21f4709 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -894,7 +894,8 @@
       *metrics_string = protozero_to_json::ProtozeroToJson(
           pool_, ".perfetto.protos.TraceMetrics",
           protozero::ConstBytes{metrics_proto.data(), metrics_proto.size()},
-          protozero_to_json::kPretty | protozero_to_json::kInlineErrors);
+          protozero_to_json::kPretty | protozero_to_json::kInlineErrors |
+              protozero_to_json::kInlineAnnotations);
       break;
   }
   return status;
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index f4b4e0c..0f0543f 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -53,7 +53,6 @@
 #include "src/trace_processor/metrics/metrics.descriptor.h"
 #include "src/trace_processor/metrics/metrics.h"
 #include "src/trace_processor/read_trace_internal.h"
-#include "src/trace_processor/util/proto_to_json.h"
 #include "src/trace_processor/util/sql_modules.h"
 #include "src/trace_processor/util/status_macros.h"
 
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index 12f812b..c717d3e 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -281,6 +281,12 @@
     "../storage",
     "../types",
   ]
+  if (perfetto_build_standalone) {
+    deps += [
+      "../../../protos/perfetto/metrics/chrome:lite",
+      "../metrics:gen_cc_all_chrome_metrics_descriptor",
+    ]
+  }
   if (enable_perfetto_zlib) {
     sources += [ "gzip_utils_unittest.cc" ]
     deps += [ "../../../gn:zlib" ]
diff --git a/src/trace_processor/util/descriptors.cc b/src/trace_processor/util/descriptors.cc
index c47d4bc..bd365f8 100644
--- a/src/trace_processor/util/descriptors.cc
+++ b/src/trace_processor/util/descriptors.cc
@@ -16,9 +16,16 @@
 
 #include "src/trace_processor/util/descriptors.h"
 
+#include <cstdint>
+#include <optional>
+#include <vector>
+
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/field.h"
+#include "perfetto/protozero/message.h"
+#include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
 #include "protos/perfetto/common/descriptor.pbzero.h"
 #include "protos/perfetto/trace_processor/trace_processor.pbzero.h"
@@ -26,7 +33,7 @@
 
 namespace perfetto {
 namespace trace_processor {
-
+namespace {
 FieldDescriptor CreateFieldFromDecoder(
     const protos::pbzero::FieldDescriptorProto::Decoder& f_decoder,
     bool is_extension) {
@@ -44,10 +51,35 @@
   return FieldDescriptor(
       base::StringView(f_decoder.name()).ToStdString(),
       static_cast<uint32_t>(f_decoder.number()), type, std::move(type_name),
+      std::vector<uint8_t>(f_decoder.options().data,
+                           f_decoder.options().data + f_decoder.options().size),
       f_decoder.label() == FieldDescriptorProto::LABEL_REPEATED, opt.packed(),
       is_extension);
 }
 
+base::Status CheckExtensionField(const ProtoDescriptor& proto_descriptor,
+                                 const FieldDescriptor& field) {
+  using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
+  auto existing_field = proto_descriptor.FindFieldByTag(field.number());
+  if (existing_field) {
+    if (field.type() != existing_field->type()) {
+      return base::ErrStatus("Field %s is re-introduced with different type",
+                             field.name().c_str());
+    }
+    if ((field.type() == FieldDescriptorProto::TYPE_MESSAGE ||
+         field.type() == FieldDescriptorProto::TYPE_ENUM) &&
+        field.raw_type_name() != existing_field->raw_type_name()) {
+      return base::ErrStatus(
+          "Field %s is re-introduced with different type %s (was %s)",
+          field.name().c_str(), field.raw_type_name().c_str(),
+          existing_field->raw_type_name().c_str());
+    }
+  }
+  return base::OkStatus();
+}
+
+}  // namespace
+
 std::optional<uint32_t> DescriptorPool::ResolveShortType(
     const std::string& parent_path,
     const std::string& short_type) {
@@ -70,31 +102,33 @@
   return ResolveShortType(parent_substr, short_type);
 }
 
-util::Status DescriptorPool::AddExtensionField(
+base::Status DescriptorPool::AddExtensionField(
     const std::string& package_name,
     protozero::ConstBytes field_desc_proto) {
   using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
   FieldDescriptorProto::Decoder f_decoder(field_desc_proto);
   auto field = CreateFieldFromDecoder(f_decoder, true);
 
-  auto extendee_name = base::StringView(f_decoder.extendee()).ToStdString();
+  std::string extendee_name = f_decoder.extendee().ToStdString();
   if (extendee_name.empty()) {
-    return util::ErrStatus("Extendee name is empty");
+    return base::ErrStatus("Extendee name is empty");
   }
 
   if (extendee_name[0] != '.') {
     // Only prepend if the extendee is not fully qualified
     extendee_name = package_name + "." + extendee_name;
   }
-  auto extendee = FindDescriptorIdx(extendee_name);
+  std::optional<uint32_t> extendee = FindDescriptorIdx(extendee_name);
   if (!extendee.has_value()) {
-    return util::ErrStatus("Extendee does not exist %s", extendee_name.c_str());
+    return base::ErrStatus("Extendee does not exist %s", extendee_name.c_str());
   }
-  descriptors_[extendee.value()].AddField(field);
-  return util::OkStatus();
+  ProtoDescriptor& extendee_desc = descriptors_[extendee.value()];
+  RETURN_IF_ERROR(CheckExtensionField(extendee_desc, field));
+  extendee_desc.AddField(field);
+  return base::OkStatus();
 }
 
-util::Status DescriptorPool::AddNestedProtoDescriptors(
+base::Status DescriptorPool::AddNestedProtoDescriptors(
     const std::string& file_name,
     const std::string& package_name,
     std::optional<uint32_t> parent_idx,
@@ -111,7 +145,7 @@
   auto idx = FindDescriptorIdx(full_name);
   if (idx.has_value() && !merge_existing_messages) {
     const auto& existing_descriptor = descriptors_[*idx];
-    return util::ErrStatus("%s: %s was already defined in file %s",
+    return base::ErrStatus("%s: %s was already defined in file %s",
                            file_name.c_str(), full_name.c_str(),
                            existing_descriptor.file_name().c_str());
   }
@@ -123,7 +157,7 @@
   }
   ProtoDescriptor& proto_descriptor = descriptors_[*idx];
   if (proto_descriptor.type() != ProtoDescriptor::Type::kMessage) {
-    return util::ErrStatus("%s was enum, redefined as message",
+    return base::ErrStatus("%s was enum, redefined as message",
                            full_name.c_str());
   }
 
@@ -131,23 +165,8 @@
   for (auto it = decoder.field(); it; ++it) {
     FieldDescriptorProto::Decoder f_decoder(*it);
     auto field = CreateFieldFromDecoder(f_decoder, /*is_extension=*/false);
-    auto existing_field = proto_descriptor.FindFieldByTag(field.number());
-    if (!existing_field) {
-      proto_descriptor.AddField(std::move(field));
-    } else {
-      if (field.type() != existing_field->type()) {
-        return util::ErrStatus("Field %s is re-introduced with different type",
-                               field.name().c_str());
-      }
-      if ((field.type() == FieldDescriptorProto::TYPE_MESSAGE ||
-           field.type() == FieldDescriptorProto::TYPE_ENUM) &&
-          field.raw_type_name() != existing_field->raw_type_name()) {
-        return util::ErrStatus(
-            "Field %s is re-introduced with different type %s (was %s)",
-            field.name().c_str(), field.raw_type_name().c_str(),
-            existing_field->raw_type_name().c_str());
-      }
-    }
+    RETURN_IF_ERROR(CheckExtensionField(proto_descriptor, field));
+    proto_descriptor.AddField(std::move(field));
   }
 
   for (auto it = decoder.enum_type(); it; ++it) {
@@ -162,10 +181,10 @@
   for (auto ext_it = decoder.extension(); ext_it; ++ext_it) {
     extensions->emplace_back(package_name, *ext_it);
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
-util::Status DescriptorPool::AddEnumProtoDescriptors(
+base::Status DescriptorPool::AddEnumProtoDescriptors(
     const std::string& file_name,
     const std::string& package_name,
     std::optional<uint32_t> parent_idx,
@@ -181,7 +200,7 @@
   auto prev_idx = FindDescriptorIdx(full_name);
   if (prev_idx.has_value() && !merge_existing_messages) {
     const auto& existing_descriptor = descriptors_[*prev_idx];
-    return util::ErrStatus("%s: %s was already defined in file %s",
+    return base::ErrStatus("%s: %s was already defined in file %s",
                            file_name.c_str(), full_name.c_str(),
                            existing_descriptor.file_name().c_str());
   }
@@ -193,7 +212,7 @@
   }
   ProtoDescriptor& proto_descriptor = descriptors_[*prev_idx];
   if (proto_descriptor.type() != ProtoDescriptor::Type::kEnum) {
-    return util::ErrStatus("%s was message, redefined as enum",
+    return base::ErrStatus("%s was message, redefined as enum",
                            full_name.c_str());
   }
 
@@ -204,10 +223,10 @@
                                   enum_value.name().ToStdString());
   }
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
-util::Status DescriptorPool::AddFromFileDescriptorSet(
+base::Status DescriptorPool::AddFromFileDescriptorSet(
     const uint8_t* file_descriptor_set_proto,
     size_t size,
     const std::vector<std::string>& skip_prefixes,
@@ -242,25 +261,23 @@
 
   // Second pass: Add extension fields to the real protos.
   for (const auto& extension : extensions) {
-    auto status = AddExtensionField(extension.first, extension.second);
-    if (!status.ok())
-      return status;
+    RETURN_IF_ERROR(AddExtensionField(extension.first, extension.second));
   }
 
-  // Third pass: resolve the types of all the fields to the correct indiices.
+  // Third pass: resolve the types of all the fields.
   using FieldDescriptorProto = protos::pbzero::FieldDescriptorProto;
-  for (auto& descriptor : descriptors_) {
+  for (ProtoDescriptor& descriptor : descriptors_) {
     for (auto& entry : *descriptor.mutable_fields()) {
-      auto& field = entry.second;
-      if (!field.resolved_type_name().empty())
-        continue;
-
-      if (field.type() == FieldDescriptorProto::TYPE_MESSAGE ||
-          field.type() == FieldDescriptorProto::TYPE_ENUM) {
+      FieldDescriptor& field = entry.second;
+      bool needs_resolution =
+          field.resolved_type_name().empty() &&
+          (field.type() == FieldDescriptorProto::TYPE_MESSAGE ||
+           field.type() == FieldDescriptorProto::TYPE_ENUM);
+      if (needs_resolution) {
         auto opt_desc =
             ResolveShortType(descriptor.full_name(), field.raw_type_name());
         if (!opt_desc.has_value()) {
-          return util::ErrStatus(
+          return base::ErrStatus(
               "Unable to find short type %s in field inside message %s",
               field.raw_type_name().c_str(), descriptor.full_name().c_str());
         }
@@ -269,7 +286,102 @@
       }
     }
   }
-  return util::OkStatus();
+
+  // Fourth pass: resolve all "uninterpreted" options to real options.
+  for (ProtoDescriptor& descriptor : descriptors_) {
+    for (auto& entry : *descriptor.mutable_fields()) {
+      FieldDescriptor& field = entry.second;
+      if (field.options().empty()) {
+        continue;
+      }
+      ResolveUninterpretedOption(descriptor, field, *field.mutable_options());
+    }
+  }
+  return base::OkStatus();
+}
+
+base::Status DescriptorPool::ResolveUninterpretedOption(
+    const ProtoDescriptor& proto_desc,
+    const FieldDescriptor& field_desc,
+    std::vector<uint8_t>& options) {
+  auto opt_idx = FindDescriptorIdx(".google.protobuf.FieldOptions");
+  if (!opt_idx) {
+    return base::ErrStatus("Unable to find field options for field %s in %s",
+                           field_desc.name().c_str(),
+                           proto_desc.full_name().c_str());
+  }
+  ProtoDescriptor& field_options_desc = descriptors_[*opt_idx];
+
+  protozero::ProtoDecoder decoder(field_desc.options().data(),
+                                  field_desc.options().size());
+  protozero::HeapBuffered<protozero::Message> field_options;
+  for (;;) {
+    const uint8_t* start = decoder.begin() + decoder.read_offset();
+    auto field = decoder.ReadField();
+    if (!field.valid()) {
+      break;
+    }
+    const uint8_t* end = decoder.begin() + decoder.read_offset();
+
+    if (field.id() !=
+        protos::pbzero::FieldOptions::kUninterpretedOptionFieldNumber) {
+      field_options->AppendRawProtoBytes(start,
+                                         static_cast<size_t>(end - start));
+      continue;
+    }
+
+    protos::pbzero::UninterpretedOption::Decoder unint(field.as_bytes());
+    auto it = unint.name();
+    if (!it) {
+      return base::ErrStatus(
+          "Option for field %s in message %s does not have a name",
+          field_desc.name().c_str(), proto_desc.full_name().c_str());
+    }
+    protos::pbzero::UninterpretedOption::NamePart::Decoder name_part(*it);
+    auto option_field_desc =
+        field_options_desc.FindFieldByName(name_part.name_part().ToStdString());
+
+    // It's not immediately clear how options with multiple names should
+    // be parsed. This likely requires digging into protobuf compiler
+    // source; given we don't have any examples of this in the codebase
+    // today, defer handling of this to when we may need it.
+    if (++it) {
+      return base::ErrStatus(
+          "Option for field %s in message %s has multiple name segments",
+          field_desc.name().c_str(), proto_desc.full_name().c_str());
+    }
+    if (unint.has_identifier_value()) {
+      field_options->AppendString(option_field_desc->number(),
+                                  unint.identifier_value().ToStdString());
+    } else if (unint.has_positive_int_value()) {
+      field_options->AppendVarInt(option_field_desc->number(),
+                                  unint.positive_int_value());
+    } else if (unint.has_negative_int_value()) {
+      field_options->AppendVarInt(option_field_desc->number(),
+                                  unint.negative_int_value());
+    } else if (unint.has_double_value()) {
+      field_options->AppendFixed(option_field_desc->number(),
+                                 unint.double_value());
+    } else if (unint.has_string_value()) {
+      field_options->AppendString(option_field_desc->number(),
+                                  unint.string_value().ToStdString());
+    } else if (unint.has_aggregate_value()) {
+      field_options->AppendString(option_field_desc->number(),
+                                  unint.aggregate_value().ToStdString());
+    } else {
+      return base::ErrStatus(
+          "Unknown field set in UninterpretedOption %s for field %s in message "
+          "%s",
+          option_field_desc->name().c_str(), field_desc.name().c_str(),
+          proto_desc.full_name().c_str());
+    }
+  }
+  if (decoder.bytes_left() > 0) {
+    return base::ErrStatus("Unexpected extra bytes when parsing option %zu",
+                           decoder.bytes_left());
+  }
+  options = field_options.SerializeAsArray();
+  return base::OkStatus();
 }
 
 std::optional<uint32_t> DescriptorPool::FindDescriptorIdx(
@@ -293,8 +405,8 @@
           proto_descriptor->add_field();
       field_descriptor->set_name(field.name());
       field_descriptor->set_number(static_cast<int32_t>(field.number()));
-      // We do not support required fields. They will show up as optional
-      // after serialization.
+      // We do not support required fields. They will show up as
+      // optional after serialization.
       field_descriptor->set_label(
           field.is_repeated()
               ? protos::pbzero::FieldDescriptorProto::LABEL_REPEATED
@@ -329,6 +441,7 @@
                                  uint32_t number,
                                  uint32_t type,
                                  std::string raw_type_name,
+                                 std::vector<uint8_t> options,
                                  bool is_repeated,
                                  bool is_packed,
                                  bool is_extension)
@@ -336,6 +449,7 @@
       number_(number),
       type_(type),
       raw_type_name_(std::move(raw_type_name)),
+      options_(std::move(options)),
       is_repeated_(is_repeated),
       is_packed_(is_packed),
       is_extension_(is_extension) {}
diff --git a/src/trace_processor/util/descriptors.h b/src/trace_processor/util/descriptors.h
index f876a09..c99a72b 100644
--- a/src/trace_processor/util/descriptors.h
+++ b/src/trace_processor/util/descriptors.h
@@ -18,6 +18,7 @@
 #define SRC_TRACE_PROCESSOR_UTIL_DESCRIPTORS_H_
 
 #include <algorithm>
+#include <cstdint>
 #include <optional>
 #include <set>
 #include <string>
@@ -40,6 +41,7 @@
                   uint32_t number,
                   uint32_t type,
                   std::string raw_type_name,
+                  std::vector<uint8_t>,
                   bool is_repeated,
                   bool is_packed,
                   bool is_extension = false);
@@ -53,6 +55,9 @@
   bool is_packed() const { return is_packed_; }
   bool is_extension() const { return is_extension_; }
 
+  const std::vector<uint8_t>& options() const { return options_; }
+  std::vector<uint8_t>* mutable_options() { return &options_; }
+
   void set_resolved_type_name(const std::string& resolved_type_name) {
     resolved_type_name_ = resolved_type_name;
   }
@@ -63,15 +68,12 @@
   uint32_t type_;
   std::string raw_type_name_;
   std::string resolved_type_name_;
+  std::vector<uint8_t> options_;
   bool is_repeated_;
   bool is_packed_;
   bool is_extension_;
 };
 
-FieldDescriptor CreateFieldFromDecoder(
-    const protos::pbzero::FieldDescriptorProto::Decoder& f_decoder,
-    bool is_extension);
-
 class ProtoDescriptor {
  public:
   enum class Type { kEnum = 0, kMessage = 1 };
@@ -202,6 +204,10 @@
   std::optional<uint32_t> ResolveShortType(const std::string& parent_path,
                                            const std::string& short_type);
 
+  base::Status ResolveUninterpretedOption(const ProtoDescriptor&,
+                                          const FieldDescriptor&,
+                                          std::vector<uint8_t>&);
+
   // Adds a new descriptor to the pool and returns its index. There must not be
   // already a descriptor with the same full_name in the pool.
   uint32_t AddProtoDescriptor(ProtoDescriptor descriptor);
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
index 633eb59..e09359c 100644
--- a/src/trace_processor/util/proto_to_args_parser.cc
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -54,7 +54,7 @@
       old_key_length_(key.key.length()) {}
 
 ProtoToArgsParser::ScopedNestedKeyContext::ScopedNestedKeyContext(
-    ProtoToArgsParser::ScopedNestedKeyContext&& other)
+    ProtoToArgsParser::ScopedNestedKeyContext&& other) noexcept
     : key_(other.key_),
       old_flat_key_length_(other.old_flat_key_length_),
       old_key_length_(other.old_key_length_) {
@@ -366,14 +366,14 @@
 
 ProtoToArgsParser::ScopedNestedKeyContext ProtoToArgsParser::EnterArray(
     size_t index) {
-  auto context = ScopedNestedKeyContext(key_prefix_);
+  ScopedNestedKeyContext context(key_prefix_);
   key_prefix_.key += "[" + std::to_string(index) + "]";
   return context;
 }
 
 ProtoToArgsParser::ScopedNestedKeyContext ProtoToArgsParser::EnterDictionary(
     const std::string& name) {
-  auto context = ScopedNestedKeyContext(key_prefix_);
+  ScopedNestedKeyContext context(key_prefix_);
   AppendProtoType(key_prefix_.key, name);
   AppendProtoType(key_prefix_.flat_key, name);
   return context;
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index e797efd..b709a40 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -67,7 +67,7 @@
 
   struct Key {
     Key(const std::string& flat_key, const std::string& key);
-    Key(const std::string& key);
+    explicit Key(const std::string& key);
     Key();
     ~Key();
 
@@ -154,7 +154,7 @@
   struct ScopedNestedKeyContext {
    public:
     ~ScopedNestedKeyContext();
-    ScopedNestedKeyContext(ScopedNestedKeyContext&&);
+    ScopedNestedKeyContext(ScopedNestedKeyContext&&) noexcept;
     ScopedNestedKeyContext(const ScopedNestedKeyContext&) = delete;
     ScopedNestedKeyContext& operator=(const ScopedNestedKeyContext&) = delete;
 
@@ -167,7 +167,7 @@
    private:
     friend class ProtoToArgsParser;
 
-    ScopedNestedKeyContext(Key& old_value);
+    explicit ScopedNestedKeyContext(Key& old_value);
 
     struct ScopedStringAppender;
 
diff --git a/src/trace_processor/util/proto_to_json.cc b/src/trace_processor/util/proto_to_json.cc
deleted file mode 100644
index 700357f..0000000
--- a/src/trace_processor/util/proto_to_json.cc
+++ /dev/null
@@ -1,325 +0,0 @@
-/*
- * Copyright (C) 2018 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 <vector>
-
-#include <google/protobuf/descriptor.h>
-#include <google/protobuf/descriptor.pb.h>
-#include <google/protobuf/message.h>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "src/trace_processor/util/proto_to_json.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace proto_to_json {
-
-namespace {
-
-std::string QuoteAndEscapeJsonString(const std::string& raw) {
-  std::string ret;
-  for (auto it = raw.cbegin(); it != raw.cend(); it++) {
-    char c = *it;
-    switch (c) {
-      case '"':
-        // Double quote needs to be escaped.
-        ret += "\\\"";
-        break;
-      case '\n':
-        // Escape new line specially because it appears often and so is worth
-        // treating specially.
-        ret += "\\n";
-        break;
-      default:
-        if (c < 0x20) {
-          // All 32 ASCII control codes need to be escaped. Instead of using the
-          // short forms, we just always use \u escape sequences instead to make
-          // things simpler.
-          ret += "\\u00";
-
-          // Print |c| as a hex character. We reserve 3 bytes of space: 2 for
-          // the hex code and one for the null terminator.
-          base::StackString<3> buf("%02X", c);
-          ret += buf.c_str();
-        } else {
-          // Everything else can be passed through directly.
-          ret += c;
-        }
-        break;
-    }
-  }
-  return '"' + ret + '"';
-}
-
-std::string FieldToJson(const google::protobuf::Message& message,
-                        const google::protobuf::FieldDescriptor* field_desc,
-                        int idx,
-                        uint32_t indent) {
-  using google::protobuf::FieldDescriptor;
-
-  const google::protobuf::Reflection* ref = message.GetReflection();
-  bool is_repeated = field_desc->is_repeated();
-  switch (field_desc->cpp_type()) {
-    case FieldDescriptor::CppType::CPPTYPE_BOOL:
-      return std::to_string(is_repeated
-                                ? ref->GetRepeatedBool(message, field_desc, idx)
-                                : ref->GetBool(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_ENUM:
-      return QuoteAndEscapeJsonString(
-          is_repeated ? ref->GetRepeatedEnum(message, field_desc, idx)->name()
-                      : ref->GetEnum(message, field_desc)->name());
-    case FieldDescriptor::CppType::CPPTYPE_FLOAT:
-      return std::to_string(
-          is_repeated
-              ? static_cast<double>(
-                    ref->GetRepeatedFloat(message, field_desc, idx))
-              : static_cast<double>(ref->GetFloat(message, field_desc)));
-    case FieldDescriptor::CppType::CPPTYPE_INT32:
-      return std::to_string(
-          is_repeated ? ref->GetRepeatedInt32(message, field_desc, idx)
-                      : ref->GetInt32(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_INT64:
-      return std::to_string(
-          is_repeated ? ref->GetRepeatedInt64(message, field_desc, idx)
-                      : ref->GetInt64(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_DOUBLE:
-      return std::to_string(
-          is_repeated ? ref->GetRepeatedDouble(message, field_desc, idx)
-                      : ref->GetDouble(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_STRING:
-      return QuoteAndEscapeJsonString(
-          is_repeated ? ref->GetRepeatedString(message, field_desc, idx)
-                      : ref->GetString(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_UINT32:
-      return std::to_string(
-          is_repeated ? ref->GetRepeatedUInt32(message, field_desc, idx)
-                      : ref->GetUInt32(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_UINT64:
-      return std::to_string(
-          is_repeated ? ref->GetRepeatedInt64(message, field_desc, idx)
-                      : ref->GetInt64(message, field_desc));
-    case FieldDescriptor::CppType::CPPTYPE_MESSAGE:
-      return MessageToJson(
-          is_repeated ? ref->GetRepeatedMessage(message, field_desc, idx)
-                      : ref->GetMessage(message, field_desc),
-          indent);
-  }
-  PERFETTO_FATAL("For GCC");
-}
-
-std::string RepeatedFieldValuesToJson(
-    const google::protobuf::Message& message,
-    const google::protobuf::FieldDescriptor* field_desc,
-    uint32_t indent) {
-  const google::protobuf::Reflection* ref = message.GetReflection();
-  std::string ret;
-  for (int i = 0; i < ref->FieldSize(message, field_desc); ++i) {
-    if (i != 0) {
-      ret += ",";
-    }
-    ret += "\n" + std::string(indent, ' ') +
-           FieldToJson(message, field_desc, i, indent);
-  }
-  return ret;
-}
-
-std::string MessageFieldsToJson(const google::protobuf::Message& message,
-                                uint32_t indent) {
-  const google::protobuf::Reflection* ref = message.GetReflection();
-  std::vector<const google::protobuf::FieldDescriptor*> field_descs;
-  ref->ListFields(message, &field_descs);
-
-  std::string ret;
-  uint32_t next_field_idx = 0;
-  for (const google::protobuf::FieldDescriptor* field_desc : field_descs) {
-    if (next_field_idx++ != 0) {
-      ret += ",";
-    }
-    std::string value;
-    if (field_desc->is_repeated()) {
-      value = "[" + RepeatedFieldValuesToJson(message, field_desc, indent + 2) +
-              "\n" + std::string(indent, ' ') + "]";
-    } else {
-      value = FieldToJson(message, field_desc, 0, indent);
-    }
-    const std::string& name = field_desc->is_extension()
-                                  ? field_desc->full_name()
-                                  : field_desc->name();
-    ret += "\n" + std::string(indent, ' ') + "\"" + name + "\": " + value;
-  }
-  return ret;
-}
-
-// This is a class helps avoid the situation where every function has to take
-// field_options_prototype as an argument, which becomes distracting.
-class OptionsConverter {
- public:
-  explicit OptionsConverter(
-      const google::protobuf::Message* field_options_prototype)
-      : field_options_prototype_(field_options_prototype) {}
-
-  // Prints all field options for non-empty fields of a message. Example:
-  // --- Message definitions ---
-  // FooMessage {
-  //   repeated int64 foo = 1 [op1 = val1, op2 = val2];
-  //   optional BarMessage bar = 2 [op3 = val3];
-  // }
-  //
-  // BarMessage {
-  //   optional int64 baz = 1 [op4 = val4];
-  // }
-  // --- MessageInstance ---
-  // foo_msg = {  // (As JSON)
-  //   foo: [23, 24, 25],
-  //   bar: {
-  //     baz: 42
-  //   }
-  // }
-  // --- Output of MessageFieldOptionsToJson(foo_msg) ---
-  //   foo: {
-  //     __field_options: {
-  //       op1: val1,
-  //       op2: val2,
-  //     },
-  //     __repeated: true
-  //   }
-  //   bar: {
-  //     __field_options: {
-  //       op3 = val3,
-  //     },
-  //     baz: {
-  //       __field_options: {
-  //         op4 = val4
-  //       },
-  //     }
-  //   }
-  // --- Notes ---
-  // This function does not produce the surrounding braces for easier use in
-  // recursive use cases. The caller needs to surround the output with braces.
-  std::string MessageFieldOptionsToJson(
-      const google::protobuf::Message& message,
-      uint32_t indent) {
-    using google::protobuf::FieldDescriptor;
-    std::vector<const FieldDescriptor*> field_descs;
-    message.GetReflection()->ListFields(message, &field_descs);
-    std::vector<std::string> field_outputs;
-    for (auto* field_desc : field_descs) {
-      std::vector<std::string> field_entries;
-      if (HasFieldOptions(field_desc)) {
-        std::string options_entry;
-        options_entry +=
-            std::string(indent + 2, ' ') + R"("__field_options": )";
-        options_entry += FieldOptionsToJson(field_desc, indent + 4);
-        field_entries.push_back(std::move(options_entry));
-      }
-      std::string nested_fields =
-          NestedMessageFieldOptionsToJson(message, field_desc, indent + 2);
-      if (!nested_fields.empty()) {
-        field_entries.push_back(std::move(nested_fields));
-      }
-      // We don't output annotations for a field if that field and all its
-      // descendants have no field options.
-      if (!field_entries.empty()) {
-        if (field_desc->is_repeated()) {
-          field_entries.push_back(std::string(indent, ' ') +
-                                  R"("__repeated": true)");
-        }
-        std::string field_output;
-        const std::string& name = field_desc->is_extension()
-                                      ? field_desc->full_name()
-                                      : field_desc->name();
-        field_output += std::string(indent, ' ') + "\"" + name + "\": {\n";
-        field_output += base::Join(field_entries, ",\n") + "\n";
-        field_output += std::string(indent, ' ') + "}";
-        field_outputs.push_back(std::move(field_output));
-      }
-    }
-    return base::Join(field_outputs, ",\n");
-  }
-
- private:
-  static bool HasFieldOptions(
-      const google::protobuf::FieldDescriptor* field_desc) {
-    return field_desc->options().ByteSizeLong() > 0;
-  }
-
-  std::string NestedMessageFieldOptionsToJson(
-      const google::protobuf::Message& message,
-      const google::protobuf::FieldDescriptor* field_desc,
-      uint32_t indent) {
-    using google::protobuf::FieldDescriptor;
-    if (field_desc->cpp_type() != FieldDescriptor::CppType::CPPTYPE_MESSAGE)
-      return "";
-    const auto* reflection = message.GetReflection();
-    const google::protobuf::Message& nested_message =
-        field_desc->is_repeated()
-            ? reflection->GetRepeatedMessage(message, field_desc, 0)
-            : reflection->GetMessage(message, field_desc);
-    return MessageFieldOptionsToJson(nested_message, indent);
-  }
-
-  std::string FieldOptionsToJson(
-      const google::protobuf::FieldDescriptor* field_desc,
-      uint32_t indent) {
-    PERFETTO_DCHECK(HasFieldOptions(field_desc));
-    std::unique_ptr<google::protobuf::Message> options(
-        field_options_prototype_->New());
-    // Field option extensions are compiled at runtime as opposed to being
-    // compiled in and being part of the generated pool, so the field option
-    // must be re-parsed as a dynamic message for the extensions to show up. If
-    // we do not do this, the extension fields remain "unknown fields" to the
-    // reflection API.
-    options->ParseFromString(field_desc->options().SerializeAsString());
-    return MessageToJson(*options, indent);
-  }
-
-  const google::protobuf::Message* field_options_prototype_;
-};
-
-}  // namespace
-
-std::string MessageToJson(const google::protobuf::Message& message,
-                          uint32_t indent) {
-  return "{" + MessageFieldsToJson(message, indent + 2) + '\n' +
-         std::string(indent, ' ') + "}";
-}
-
-std::string MessageToJsonWithAnnotations(
-    const google::protobuf::Message& message,
-    const google::protobuf::Message* field_options_prototype,
-    uint32_t indent) {
-  std::string ret;
-  OptionsConverter options_converter(field_options_prototype);
-
-  ret = "{" + MessageFieldsToJson(message, indent + 2);
-  std::string annotation_fields =
-      options_converter.MessageFieldOptionsToJson(message, indent + 4);
-  if (annotation_fields.empty()) {
-    ret += "\n";
-  } else {
-    ret += ",\n";
-    ret += std::string(indent + 2, ' ') + "\"__annotations\": {\n";
-    ret += annotation_fields + "\n";
-    ret += std::string(indent + 2, ' ') + "}\n";
-  }
-  ret += std::string(indent, ' ') + "}\n";
-  return ret;
-}
-
-}  // namespace proto_to_json
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/util/proto_to_json.h b/src/trace_processor/util/proto_to_json.h
deleted file mode 100644
index cfe7a11..0000000
--- a/src/trace_processor/util/proto_to_json.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_UTIL_PROTO_TO_JSON_H_
-#define SRC_TRACE_PROCESSOR_UTIL_PROTO_TO_JSON_H_
-
-#include <google/protobuf/message.h>
-
-namespace perfetto {
-namespace trace_processor {
-namespace proto_to_json {
-
-std::string MessageToJson(const google::protobuf::Message& message,
-                          uint32_t indent = 0);
-
-std::string MessageToJsonWithAnnotations(
-    const google::protobuf::Message& message,
-    const google::protobuf::Message* field_options_prototype,
-    uint32_t indent = 0);
-
-}  // namespace proto_to_json
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_UTIL_PROTO_TO_JSON_H_
diff --git a/src/trace_processor/util/protozero_to_json.cc b/src/trace_processor/util/protozero_to_json.cc
index 7c52ad0..fa08f21 100644
--- a/src/trace_processor/util/protozero_to_json.cc
+++ b/src/trace_processor/util/protozero_to_json.cc
@@ -18,9 +18,12 @@
 
 #include <optional>
 #include <unordered_set>
+#include <utility>
+#include <vector>
 
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/protozero/proto_utils.h"
 #include "protos/perfetto/common/descriptor.pbzero.h"
@@ -38,11 +41,13 @@
 
 class JsonBuilder {
  public:
-  JsonBuilder(int flags) : flags_(flags) {}
+  explicit JsonBuilder(int flags) : flags_(flags) {}
 
   void OpenObject() {
-    if (is_array_scope() && !is_empty_scope()) {
-      Append(",");
+    if (is_array_scope()) {
+      if (!is_empty_scope()) {
+        Append(",");
+      }
       MaybeAppendNewline();
       MaybeAppendIndent();
     }
@@ -51,12 +56,6 @@
   }
 
   void CloseObject() {
-    // If we're closing the root object add errors if requested:
-    if (is_root_scope() && is_inline_errors() && !errors_.empty()) {
-      Key("__error");
-      StringValue(base::StringView(base::Join(errors_, "\n")));
-    }
-
     bool needs_newline = !is_empty_scope();
     stack_.pop_back();
     if (needs_newline) {
@@ -115,9 +114,13 @@
 
   std::string ToString() { return base::Join(parts_, ""); }
 
-  bool is_pretty() { return flags_ & Flags::kPretty; }
+  bool is_empty_scope() { return !stack_.empty() && stack_.back().is_empty; }
 
-  bool is_inline_errors() { return flags_ & Flags::kInlineErrors; }
+  bool is_pretty() const { return flags_ & Flags::kPretty; }
+
+  bool is_inline_errors() const { return flags_ & Flags::kInlineErrors; }
+
+  const std::vector<std::string>& errors() const { return errors_; }
 
  private:
   enum class ScopeContext {
@@ -136,19 +139,15 @@
   std::vector<std::string> errors_;
 
   bool is_object_scope() {
-    return stack_.size() > 0 && stack_.back().ctx == ScopeContext::kObject;
+    return !stack_.empty() && stack_.back().ctx == ScopeContext::kObject;
   }
 
   bool is_array_scope() {
-    return stack_.size() > 0 && stack_.back().ctx == ScopeContext::kArray;
+    return !stack_.empty() && stack_.back().ctx == ScopeContext::kArray;
   }
 
-  bool is_empty_scope() { return stack_.size() > 0 && stack_.back().is_empty; }
-
-  bool is_root_scope() { return stack_.size() == 1; }
-
   void MarkScopeAsNonEmpty() {
-    if (stack_.size() > 0) {
+    if (!stack_.empty()) {
       stack_.back().is_empty = false;
     }
   }
@@ -204,7 +203,7 @@
           result += R"(\b)";
           break;
         case '\f':
-          result += R"(\b)";
+          result += R"(\f)";
           break;
         case '\r':
           result += R"(\r)";
@@ -274,6 +273,15 @@
   }
 };
 
+bool HasFieldOptions(const FieldDescriptor& field_desc) {
+  return !field_desc.options().empty();
+}
+
+std::string FulllyQualifiedFieldName(const ProtoDescriptor& desc,
+                                     const FieldDescriptor& field_desc) {
+  return desc.package_name().substr(1) + "." + field_desc.name();
+}
+
 bool IsTypeMatch(ProtoWireType wire, uint32_t type) {
   switch (wire) {
     case ProtoWireType::kVarInt:
@@ -366,6 +374,7 @@
 void MessageField(const DescriptorPool& pool,
                   const std::string& type,
                   protozero::ConstBytes protobytes,
+                  bool fully_qualify_extensions,
                   JsonBuilder* out);
 void EnumField(const DescriptorPool& pool,
                const FieldDescriptor& fd,
@@ -418,6 +427,7 @@
 void LengthField(const DescriptorPool& pool,
                  const FieldDescriptor* fd,
                  const protozero::Field& field,
+                 bool fully_qualify_extensions,
                  JsonBuilder* out) {
   uint32_t type = fd ? fd->type() : 0;
   switch (type) {
@@ -428,7 +438,8 @@
       out->StringValue(field.as_string());
       return;
     case FieldDescriptorProto::TYPE_MESSAGE:
-      MessageField(pool, fd->resolved_type_name(), field.as_bytes(), out);
+      MessageField(pool, fd->resolved_type_name(), field.as_bytes(),
+                   fully_qualify_extensions, out);
       return;
     case FieldDescriptorProto::TYPE_DOUBLE:
       PackedField<ProtoWireType::kFixed64, double>(pool, *fd, field, out);
@@ -600,13 +611,14 @@
                          protozero::ConstBytes protobytes,
                          const FieldDescriptor* fd,
                          uint32_t id,
+                         bool fully_qualify_extensions,
                          JsonBuilder* out) {
   out->OpenArray();
   protozero::ProtoDecoder decoder(protobytes.data, protobytes.size);
   for (auto field = decoder.ReadField(); field.valid();
        field = decoder.ReadField()) {
     if (field.id() == id) {
-      LengthField(pool, fd, field, out);
+      LengthField(pool, fd, field, fully_qualify_extensions, out);
     }
   }
   out->CloseArray();
@@ -642,12 +654,11 @@
   out->CloseArray();
 }
 
-void MessageField(const DescriptorPool& pool,
-                  const std::string& type,
-                  protozero::ConstBytes protobytes,
-                  JsonBuilder* out) {
-  out->OpenObject();
-
+void InnerMessageField(const DescriptorPool& pool,
+                       const std::string& type,
+                       protozero::ConstBytes protobytes,
+                       bool fully_qualify_extensions,
+                       JsonBuilder* out) {
   std::optional<uint32_t> opt_proto_desc_idx = pool.FindDescriptorIdx(type);
   const ProtoDescriptor* opt_proto_descriptor =
       opt_proto_desc_idx ? &pool.descriptors()[*opt_proto_desc_idx] : nullptr;
@@ -668,7 +679,12 @@
       if (fields_seen.count(field.id())) {
         continue;
       }
-      out->Key(opt_field_descriptor->name());
+      if (opt_field_descriptor->is_extension() && fully_qualify_extensions) {
+        out->Key(FulllyQualifiedFieldName(*opt_proto_descriptor,
+                                          *opt_field_descriptor));
+      } else {
+        out->Key(opt_field_descriptor->name());
+      }
     } else {
       out->Key(std::to_string(field.id()));
     }
@@ -686,10 +702,11 @@
             // wire_type = length + field_type in
             // {u,s,}int{32,64}, float, double etc means this is the
             // packed case:
-            LengthField(pool, opt_field_descriptor, field, out);
+            LengthField(pool, opt_field_descriptor, field,
+                        fully_qualify_extensions, out);
           } else {
             RepeatedLengthField(pool, protobytes, opt_field_descriptor,
-                                field.id(), out);
+                                field.id(), fully_qualify_extensions, out);
           }
           break;
         case ProtoWireType::kFixed32:
@@ -705,7 +722,8 @@
           VarIntField(pool, opt_field_descriptor, field, out);
           break;
         case ProtoWireType::kLengthDelimited:
-          LengthField(pool, opt_field_descriptor, field, out);
+          LengthField(pool, opt_field_descriptor, field,
+                      fully_qualify_extensions, out);
           break;
         case ProtoWireType::kFixed32:
           Fixed32Field(opt_field_descriptor, field, out);
@@ -720,10 +738,130 @@
   if (decoder.bytes_left() != 0) {
     out->AddError(std::to_string(decoder.bytes_left()) + " extra bytes");
   }
+}
 
+void MessageField(const DescriptorPool& pool,
+                  const std::string& type,
+                  protozero::ConstBytes protobytes,
+                  bool fully_qualify_extensions,
+                  JsonBuilder* out) {
+  out->OpenObject();
+  InnerMessageField(pool, type, protobytes, fully_qualify_extensions, out);
   out->CloseObject();
 }
 
+// Prints all field options for non-empty fields of a message. Example:
+// --- Message definitions ---
+// FooMessage {
+//   repeated int64 foo = 1 [op1 = val1, op2 = val2];
+//   optional BarMessage bar = 2 [op3 = val3];
+// }
+//
+// BarMessage {
+//   optional int64 baz = 1 [op4 = val4];
+// }
+// --- MessageInstance ---
+// foo_msg = {  // (As JSON)
+//   foo: [23, 24, 25],
+//   bar: {
+//     baz: 42
+//   }
+// }
+// --- Output of MessageFieldOptionsToJson(foo_msg) ---
+//   foo: {
+//     __field_options: {
+//       op1: val1,
+//       op2: val2,
+//     },
+//     __repeated: true
+//   }
+//   bar: {
+//     __field_options: {
+//       op3 = val3,
+//     },
+//     baz: {
+//       __field_options: {
+//         op4 = val4
+//       },
+//     }
+//   }
+void MessageFieldOptionsToJson(
+    const DescriptorPool& pool,
+    const std::string& type,
+    const std::string& field_prefix,
+    const std::unordered_set<std::string>& allowed_fields,
+    JsonBuilder* out) {
+  std::optional<uint32_t> opt_proto_desc_idx = pool.FindDescriptorIdx(type);
+  if (!opt_proto_desc_idx) {
+    return;
+  }
+  const ProtoDescriptor& desc = pool.descriptors()[*opt_proto_desc_idx];
+  for (const auto& id_and_field : desc.fields()) {
+    const FieldDescriptor& field_desc = id_and_field.second;
+    std::string full_field_name = field_prefix + field_desc.name();
+    if (allowed_fields.find(full_field_name) == allowed_fields.end()) {
+      continue;
+    }
+    if (field_desc.is_extension()) {
+      out->Key(FulllyQualifiedFieldName(desc, field_desc));
+    } else {
+      out->Key(field_desc.name());
+    }
+    out->OpenObject();
+    if (HasFieldOptions(field_desc)) {
+      out->Key("__field_options");
+      MessageField(pool, ".google.protobuf.FieldOptions",
+                   protozero::ConstBytes{field_desc.options().data(),
+                                         field_desc.options().size()},
+                   false, out);
+    }
+    if (field_desc.type() == FieldDescriptorProto::Type::TYPE_MESSAGE) {
+      MessageFieldOptionsToJson(pool, field_desc.resolved_type_name(),
+                                full_field_name + ".", allowed_fields, out);
+    }
+    if (field_desc.is_repeated()) {
+      out->Key("__repeated");
+      out->BoolValue(true);
+    }
+    out->CloseObject();
+  }
+}
+
+bool PopulateAllowedFieldOptionsSet(
+    const DescriptorPool& pool,
+    const std::string& type,
+    const std::string& field_prefix,
+    protozero::ConstBytes protobytes,
+    std::unordered_set<std::string>& allowed_fields) {
+  std::optional<uint32_t> opt_proto_desc_idx = pool.FindDescriptorIdx(type);
+  if (!opt_proto_desc_idx) {
+    return false;
+  }
+  const ProtoDescriptor& desc = pool.descriptors()[*opt_proto_desc_idx];
+  protozero::ProtoDecoder decoder(protobytes);
+  bool allowed = false;
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    auto* opt_field_descriptor = desc.FindFieldByTag(field.id());
+    if (!opt_field_descriptor) {
+      continue;
+    }
+    std::string full_field_name = field_prefix + opt_field_descriptor->name();
+    bool nested = false;
+    if (opt_field_descriptor->type() ==
+        protos::pbzero::FieldDescriptorProto::TYPE_MESSAGE) {
+      nested = PopulateAllowedFieldOptionsSet(
+          pool, opt_field_descriptor->resolved_type_name(),
+          full_field_name + ".", field.as_bytes(), allowed_fields);
+    }
+    if (nested || HasFieldOptions(*opt_field_descriptor)) {
+      allowed_fields.emplace(full_field_name);
+      allowed = true;
+    }
+  }
+  return allowed;
+}
+
 }  // namespace
 
 std::string ProtozeroToJson(const DescriptorPool& pool,
@@ -731,7 +869,23 @@
                             protozero::ConstBytes protobytes,
                             int flags) {
   JsonBuilder builder(flags);
-  MessageField(pool, type, protobytes, &builder);
+  builder.OpenObject();
+  InnerMessageField(pool, type, protobytes, true, &builder);
+  if (builder.is_inline_errors() && !builder.errors().empty()) {
+    builder.Key("__error");
+    builder.StringValue(base::StringView(base::Join(builder.errors(), "\n")));
+  }
+  if (flags & kInlineAnnotations) {
+    std::unordered_set<std::string> allowed_fields;
+    PopulateAllowedFieldOptionsSet(pool, type, "", protobytes, allowed_fields);
+    if (!allowed_fields.empty()) {
+      builder.Key("__annotations");
+      builder.OpenObject();
+      MessageFieldOptionsToJson(pool, type, "", allowed_fields, &builder);
+      builder.CloseObject();
+    }
+  }
+  builder.CloseObject();
   return builder.ToString();
 }
 
diff --git a/src/trace_processor/util/protozero_to_json.h b/src/trace_processor/util/protozero_to_json.h
index c571cb1..afc8d9d 100644
--- a/src/trace_processor/util/protozero_to_json.h
+++ b/src/trace_processor/util/protozero_to_json.h
@@ -32,16 +32,29 @@
   kNone = 0,
 
   // Produce nice json (newlines, 1 space post :, 2 space indents)
-  kPretty = 1,
+  kPretty = 1 << 0,
 
   // Report errors as an extra key on the root json object. For example
   // the output with this flag might look like:
   // {
   //    "foo": { ... },
   //    "baz": { ... },
-  //    __error: "Failed to decode key 'bar' due to <some error>"
+  //    "__error": "Failed to decode key 'bar' due to <some error>"
   // }
-  kInlineErrors = 2,
+  kInlineErrors = 1 << 1,
+
+  // Report annotations as an extra key on the root json object. For example
+  // the output with this flag might look like:
+  // {
+  //    "foo": { ... },
+  //    "baz": { ... },
+  //    "__annotations": {
+  //      "foo": {
+  //        "__field_options": { "unit": "ms_smallerIsBetter" }
+  //      }
+  //    }
+  // }
+  kInlineAnnotations = 1 << 2,
 };
 
 // Given a protozero message |protobytes| which is of fully qualified name
diff --git a/src/trace_processor/util/protozero_to_json_unittests.cc b/src/trace_processor/util/protozero_to_json_unittests.cc
index 3dcdb24..b8e01a7 100644
--- a/src/trace_processor/util/protozero_to_json_unittests.cc
+++ b/src/trace_processor/util/protozero_to_json_unittests.cc
@@ -27,6 +27,11 @@
 #include "protos/perfetto/trace/track_event/chrome_compositor_scheduler_state.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_event.pbzero.h"
 
+#if PERFETTO_BUILDFLAG(PERFETTO_STANDALONE_BUILD)
+#include "protos/perfetto/metrics/chrome/all_chrome_metrics.pb.h"  // nogncheck
+#include "src/trace_processor/metrics/all_chrome_metrics.descriptor.h"  // nogncheck
+#endif
+
 namespace perfetto {
 namespace trace_processor {
 namespace protozero_to_json {
@@ -117,6 +122,38 @@
                       kNone));
 }
 
+// This test depends on the CustomOptions message in descriptor.proto which
+// is very tricky to point to on the non-standalone build.
+#if PERFETTO_BUILDFLAG(PERFETTO_STANDALONE_BUILD)
+TEST(ProtozeroToJsonTest, CustomDescriptorPoolAnnotations) {
+  using perfetto::protos::TestChromeMetric;
+  TestChromeMetric msg;
+  msg.set_test_value(1);
+  auto binary_proto = msg.SerializeAsString();
+  protozero::ConstBytes binary_proto_bytes{
+      reinterpret_cast<const uint8_t*>(binary_proto.data()),
+      binary_proto.size()};
+
+  DescriptorPool pool;
+  auto status = pool.AddFromFileDescriptorSet(
+      kAllChromeMetricsDescriptor.data(), kAllChromeMetricsDescriptor.size());
+  ASSERT_TRUE(status.ok());
+
+  EXPECT_EQ(R"({
+  "test_value": 1,
+  "__annotations": {
+    "test_value": {
+      "__field_options": {
+        "unit": "count_smallerIsBetter"
+      }
+    }
+  }
+})",
+            ProtozeroToJson(pool, ".perfetto.protos.TestChromeMetric",
+                            binary_proto_bytes, kPretty | kInlineAnnotations));
+}
+#endif
+
 // Sets up a descriptor pool with all the messages from
 // "src/protozero/test/example_proto/test_messages.proto"
 class ProtozeroToJsonTestMessageTest : public testing::Test {
diff --git a/ui/src/chrome_extension/chrome_tracing_controller.ts b/ui/src/chrome_extension/chrome_tracing_controller.ts
index 9ed3930..a40d22f 100644
--- a/ui/src/chrome_extension/chrome_tracing_controller.ts
+++ b/ui/src/chrome_extension/chrome_tracing_controller.ts
@@ -23,13 +23,12 @@
   ReadBuffersResponse,
 } from '../controller/consumer_port_types';
 import {RpcConsumerPort} from '../controller/record_controller_interfaces';
-import {TraceConfig} from '../core/protos';
+import {ITraceStats, TraceConfig} from '../core/protos';
 import {
   browserSupportsPerfettoConfig,
   extractTraceConfig,
   hasSystemDataSourceConfig,
 } from '../core/trace_config_utils';
-import {perfetto} from '../gen/protos';
 
 import {DevToolsSocket} from './devtools_socket';
 
@@ -201,7 +200,7 @@
     if (this.lastBufferUsageEvent && this.lastBufferUsageEvent.percentFull) {
       percentFull = this.lastBufferUsageEvent.percentFull;
     }
-    const stats: perfetto.protos.ITraceStats = {
+    const stats: ITraceStats = {
       bufferStats:
           [{bufferSize: 1000, bytesWritten: Math.round(percentFull * 1000)}],
     };
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index fe05e04..43c5f16 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -20,10 +20,14 @@
   ComputeMetricArgs,
   ComputeMetricResult,
   DisableAndReadMetatraceResult,
+  EnableMetatraceArgs,
+  MetatraceCategories,
   QueryArgs,
+  QueryResult as ProtoQueryResult,
   ResetTraceProcessorArgs,
+  TraceProcessorRpc,
+  TraceProcessorRpcStream,
 } from '../core/protos';
-import {perfetto} from '../gen/protos';
 
 import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
@@ -37,9 +41,7 @@
   WritableQueryResult,
 } from './query_result';
 
-import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
-import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
-import TPM = perfetto.protos.TraceProcessorRpc.TraceProcessorMethod;
+import TPM = TraceProcessorRpc.TraceProcessorMethod;
 
 export interface LoadingTracker {
   beginLoading(): void;
@@ -133,23 +135,20 @@
     // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field.
     // 2. We stash (a view of) the original buffer into the |rawQueryResult| so
     //    the `case TPM_QUERY_STREAMING` below can take it.
-    perfetto.protos.QueryResult.decode =
-        (reader: protobuf.Reader, length: number) => {
-          const res =
-              perfetto.protos.QueryResult.create() as {} as QueryResultBypass;
-          res.rawQueryResult =
-              reader.buf.subarray(reader.pos, reader.pos + length);
-          // All this works only if protobufjs returns the original ArrayBuffer
-          // from |rpcMsgEncoded|. It should be always the case given the
-          // current implementation. This check mainly guards against future
-          // behavioral changes of protobufjs. We don't want to accidentally
-          // hold onto some internal protobufjs buffer. We are fine holding
-          // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
-          // is buffer-retention-friendly.
-          assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
-          reader.pos += length;
-          return res as {} as perfetto.protos.QueryResult;
-        };
+    ProtoQueryResult.decode = (reader: protobuf.Reader, length: number) => {
+      const res = ProtoQueryResult.create() as {} as QueryResultBypass;
+      res.rawQueryResult = reader.buf.subarray(reader.pos, reader.pos + length);
+      // All this works only if protobufjs returns the original ArrayBuffer
+      // from |rpcMsgEncoded|. It should be always the case given the
+      // current implementation. This check mainly guards against future
+      // behavioral changes of protobufjs. We don't want to accidentally
+      // hold onto some internal protobufjs buffer. We are fine holding
+      // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
+      // is buffer-retention-friendly.
+      assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
+      reader.pos += length;
+      return res as {} as ProtoQueryResult;
+    };
 
     const rpc = TraceProcessorRpc.decode(rpcMsgEncoded);
 
@@ -352,11 +351,11 @@
     return this._isMetatracingEnabled;
   }
 
-  enableMetatrace(categories?: perfetto.protos.MetatraceCategories) {
+  enableMetatrace(categories?: MetatraceCategories) {
     const rpc = TraceProcessorRpc.create();
     rpc.request = TPM.TPM_ENABLE_METATRACE;
     if (categories) {
-      rpc.enableMetatraceArgs = new perfetto.protos.EnableMetatraceArgs();
+      rpc.enableMetatraceArgs = new EnableMetatraceArgs();
       rpc.enableMetatraceArgs.categories = categories;
     }
     this._isMetatracingEnabled = true;
diff --git a/ui/src/common/metatracing.ts b/ui/src/common/metatracing.ts
index 99c5ea9..09d080f 100644
--- a/ui/src/common/metatracing.ts
+++ b/ui/src/common/metatracing.ts
@@ -12,8 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {PerfettoMetatrace, Trace, TracePacket} from '../core/protos';
-import {perfetto} from '../gen/protos';
+import {
+  MetatraceCategories,
+  PerfettoMetatrace,
+  Trace,
+  TracePacket,
+} from '../core/protos';
 
 import {featureFlags} from './feature_flags';
 
@@ -28,8 +32,6 @@
   kOmniboxStatus = 3,
 }
 
-import MetatraceCategories = perfetto.protos.MetatraceCategories;
-
 const AOMT_FLAG = featureFlags.register({
   id: 'alwaysOnMetatracing',
   name: 'Enable always-on-metatracing',
diff --git a/ui/src/common/query_result_unittest.ts b/ui/src/common/query_result_unittest.ts
index 50641fe..090eb54 100644
--- a/ui/src/common/query_result_unittest.ts
+++ b/ui/src/common/query_result_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import protoNamespace from '../gen/protos';
+import {QueryResult as QueryResultProto} from '../core/protos';
 
 import {
   createQueryResult,
@@ -23,8 +23,7 @@
   STR_NULL,
 } from './query_result';
 
-const T = protoNamespace.perfetto.protos.QueryResult.CellsBatch.CellType;
-const QueryResultProto = protoNamespace.perfetto.protos.QueryResult;
+const T = QueryResultProto.CellsBatch.CellType;
 
 test('QueryResult.SimpleOneRow', () => {
   const batch = QueryResultProto.CellsBatch.create({
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/common/recordingV2/recording_config_utils.ts
index 9524670..dd33e36 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/common/recordingV2/recording_config_utils.ts
@@ -30,19 +30,20 @@
   NativeContinuousDumpConfig,
   NetworkPacketTraceConfig,
   PerfEventConfig,
+  PerfEvents,
   ProcessStatsConfig,
   SysStatsConfig,
   TraceConfig,
   TrackEventConfig,
   VmstatCounters,
 } from '../../core/protos';
-import {perfetto} from '../../gen/protos';
 
 import {TargetInfo} from './recording_interfaces_v2';
 
-import Timebase = perfetto.protos.PerfEvents.Timebase;
-import CallstackSampling = perfetto.protos.PerfEventConfig.CallstackSampling;
-import Scope = perfetto.protos.PerfEventConfig.Scope;
+import PerfClock = PerfEvents.PerfClock;
+import Timebase = PerfEvents.Timebase;
+import CallstackSampling = PerfEventConfig.CallstackSampling;
+import Scope = PerfEventConfig.Scope;
 
 export interface ConfigProtoEncoded {
   configProtoText?: string;
@@ -477,8 +478,7 @@
     // TODO: The timestampClock needs to be changed to MONOTONIC once we start
     // offering a choice of counter to record on through the recording UI, as
     // not all clocks are compatible with hardware counters).
-    perfEventConfig.timebase.timestampClock =
-        perfetto.protos.PerfEvents.PerfClock.PERF_CLOCK_BOOTTIME;
+    perfEventConfig.timebase.timestampClock = PerfClock.PERF_CLOCK_BOOTTIME;
 
     const callstackSampling = new CallstackSampling();
     if (uiCfg.targetCmdLine.length > 0) {
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/controller/adb_record_controller_jsdomtest.ts
index 21b2898..5d506a6 100644
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ b/ui/src/controller/adb_record_controller_jsdomtest.ts
@@ -15,7 +15,7 @@
 import {dingus} from 'dingusjs';
 
 import {utf8Encode} from '../base/string_utils';
-import {perfetto} from '../gen/protos';
+import {EnableTracingRequest, TraceConfig} from '../core/protos';
 
 import {AdbStream, MockAdb, MockAdbStream} from './adb_interfaces';
 import {AdbConsumerPort} from './adb_shell_controller';
@@ -33,10 +33,10 @@
 const adbController = new AdbConsumerPort(adbMock, mainCallback);
 const mockIntArray = new Uint8Array();
 
-const enableTracingRequest = new perfetto.protos.EnableTracingRequest();
-enableTracingRequest.traceConfig = new perfetto.protos.TraceConfig();
+const enableTracingRequest = new EnableTracingRequest();
+enableTracingRequest.traceConfig = new TraceConfig();
 const enableTracingRequestProto =
-    perfetto.protos.EnableTracingRequest.encode(enableTracingRequest).finish();
+    EnableTracingRequest.encode(enableTracingRequest).finish();
 
 
 test('handleCommand', async () => {
diff --git a/ui/src/controller/adb_socket_controller.ts b/ui/src/controller/adb_socket_controller.ts
index 87af792..ac761f0 100644
--- a/ui/src/controller/adb_socket_controller.ts
+++ b/ui/src/controller/adb_socket_controller.ts
@@ -14,7 +14,14 @@
 
 import protobuf from 'protobufjs/minimal';
 
-import {perfetto} from '../gen/protos';
+import {
+  DisableTracingResponse,
+  EnableTracingResponse,
+  FreeBuffersResponse,
+  GetTraceStatsResponse,
+  IPCFrame,
+  ReadBuffersResponse,
+} from '../core/protos';
 
 import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller';
 import {Adb, AdbStream} from './adb_interfaces';
@@ -38,10 +45,9 @@
 const TRACE_PACKET_PROTO_TAG =
     (TRACE_PACKET_PROTO_ID << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
 
-declare type Frame = perfetto.protos.IPCFrame;
-declare type IMethodInfo =
-    perfetto.protos.IPCFrame.BindServiceReply.IMethodInfo;
-declare type ISlice = perfetto.protos.ReadBuffersResponse.ISlice;
+declare type Frame = IPCFrame;
+declare type IMethodInfo = IPCFrame.BindServiceReply.IMethodInfo;
+declare type ISlice = ReadBuffersResponse.ISlice;
 
 interface Command {
   method: string;
@@ -115,9 +121,9 @@
       console.error(`Method ${method} not supported by the target`);
       return;
     }
-    const frame = new perfetto.protos.IPCFrame({
+    const frame = new IPCFrame({
       requestId,
-      msgInvokeMethod: new perfetto.protos.IPCFrame.InvokeMethod(
+      msgInvokeMethod: new IPCFrame.InvokeMethod(
           {serviceId: this.serviceId, methodId, argsProto}),
     });
     this.requestMethods.set(requestId, method);
@@ -127,8 +133,7 @@
   }
 
   static generateFrameBufferToSend(frame: Frame): Uint8Array {
-    const frameProto: Uint8Array =
-        perfetto.protos.IPCFrame.encode(frame).finish();
+    const frameProto: Uint8Array = IPCFrame.encode(frame).finish();
     const frameLen = frameProto.length;
     const buf = new Uint8Array(WIRE_PROTOCOL_HEADER_SIZE + frameLen);
     const dv = new DataView(buf.buffer);
@@ -165,7 +170,7 @@
     const buf = new ArrayBuffer(frameBuffer.byteLength);
     const arr = new Uint8Array(buf);
     arr.set(frameBuffer);
-    const frame = perfetto.protos.IPCFrame.decode(arr);
+    const frame = IPCFrame.decode(arr);
     this.handleIncomingFrame(frame);
   }
 
@@ -297,10 +302,9 @@
   bind() {
     console.assert(this.socket !== undefined);
     const requestId = this.requestId++;
-    const frame = new perfetto.protos.IPCFrame({
+    const frame = new IPCFrame({
       requestId,
-      msgBindService: new perfetto.protos.IPCFrame.BindService(
-          {serviceName: 'ConsumerPort'}),
+      msgBindService: new IPCFrame.BindService({serviceName: 'ConsumerPort'}),
     });
     return new Promise<void>((resolve, _) => {
       this.resolveBindingPromise = resolve;
@@ -325,7 +329,7 @@
     }
   }
 
-  handleIncomingFrame(frame: perfetto.protos.IPCFrame) {
+  handleIncomingFrame(frame: IPCFrame) {
     const requestId = frame.requestId;
     switch (frame.msg) {
       case 'msgBindServiceReply': {
@@ -361,10 +365,9 @@
   }
 }
 
-const decoders =
-    new Map<string, Function>()
-        .set('EnableTracing', perfetto.protos.EnableTracingResponse.decode)
-        .set('FreeBuffers', perfetto.protos.FreeBuffersResponse.decode)
-        .set('ReadBuffers', perfetto.protos.ReadBuffersResponse.decode)
-        .set('DisableTracing', perfetto.protos.DisableTracingResponse.decode)
-        .set('GetTraceStats', perfetto.protos.GetTraceStatsResponse.decode);
+const decoders = new Map<string, Function>()
+                     .set('EnableTracing', EnableTracingResponse.decode)
+                     .set('FreeBuffers', FreeBuffersResponse.decode)
+                     .set('ReadBuffers', ReadBuffersResponse.decode)
+                     .set('DisableTracing', DisableTracingResponse.decode)
+                     .set('GetTraceStats', GetTraceStatsResponse.decode);
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 6ff6ec4..46f0f47 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -17,6 +17,7 @@
 import {pluginManager} from '../../common/plugins';
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
+import {CPU_SLICE_TRACK_KIND} from '../../tracks/cpu_slices';
 
 import {AggregationController} from './aggregation_controller';
 
@@ -25,12 +26,12 @@
   async createAggregateView(engine: Engine, area: Area) {
     await engine.query(`drop view if exists ${this.kind};`);
 
-    const selectedCpus: (string|number)[] = [];
+    const selectedCpus: number[] = [];
     for (const trackId of area.tracks) {
       const track = globals.state.tracks[trackId];
       if (track?.uri) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.type === 'cpu_sched') {
+        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
           const cpu = trackInfo?.tags?.cpu;
           cpu && selectedCpus.push(cpu);
         }
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
index 3745da3..aebaf94 100644
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
@@ -17,6 +17,7 @@
 import {pluginManager} from '../../common/plugins';
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
+import {CPU_SLICE_TRACK_KIND} from '../../tracks/cpu_slices';
 
 import {AggregationController} from './aggregation_controller';
 
@@ -24,12 +25,12 @@
   async createAggregateView(engine: Engine, area: Area) {
     await engine.query(`drop view if exists ${this.kind};`);
 
-    const selectedCpus: string[] = [];
+    const selectedCpus: number[] = [];
     for (const trackId of area.tracks) {
       const track = globals.state.tracks[trackId];
       if (track?.uri) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.type === 'cpu_sched') {
+        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
           const cpu = trackInfo?.tags?.cpu;
           cpu && selectedCpus.push(cpu);
         }
diff --git a/ui/src/controller/consumer_port_types.ts b/ui/src/controller/consumer_port_types.ts
index 8b3cd45..c221516 100644
--- a/ui/src/controller/consumer_port_types.ts
+++ b/ui/src/controller/consumer_port_types.ts
@@ -12,7 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {perfetto} from '../gen/protos';
+import {
+  IDisableTracingResponse,
+  IEnableTracingResponse,
+  IFreeBuffersResponse,
+  IGetTraceStatsResponse,
+  IReadBuffersResponse,
+} from '../core/protos';
 
 export interface Typed {
   type: string;
@@ -29,17 +35,13 @@
   return obj.hasOwnProperty('type');
 }
 
-export interface ReadBuffersResponse extends
-    Typed, perfetto.protos.IReadBuffersResponse {}
-export interface EnableTracingResponse extends
-    Typed, perfetto.protos.IEnableTracingResponse {}
-export interface GetTraceStatsResponse extends
-    Typed, perfetto.protos.IGetTraceStatsResponse {}
-export interface FreeBuffersResponse extends
-    Typed, perfetto.protos.IFreeBuffersResponse {}
+export interface ReadBuffersResponse extends Typed, IReadBuffersResponse {}
+export interface EnableTracingResponse extends Typed, IEnableTracingResponse {}
+export interface GetTraceStatsResponse extends Typed, IGetTraceStatsResponse {}
+export interface FreeBuffersResponse extends Typed, IFreeBuffersResponse {}
 export interface GetCategoriesResponse extends Typed {}
-export interface DisableTracingResponse extends
-    Typed, perfetto.protos.IDisableTracingResponse {}
+export interface DisableTracingResponse extends Typed,
+                                                IDisableTracingResponse {}
 
 export type ConsumerPortResponse =
     EnableTracingResponse|ReadBuffersResponse|GetTraceStatsResponse|
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index 1d1de5e..b8ee0d3 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -21,13 +21,16 @@
   Time,
   TimeSpan,
 } from '../base/time';
+import {exists} from '../base/utils';
 import {Engine} from '../common/engine';
+import {pluginManager} from '../common/plugins';
 import {LONG, NUM, STR} from '../common/query_result';
 import {escapeSearchQuery} from '../common/query_utils';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {OmniboxState} from '../common/state';
 import {globals} from '../frontend/globals';
 import {publishSearch, publishSearchResult} from '../frontend/publish';
+import {CPU_SLICE_TRACK_KIND} from '../tracks/cpu_slices';
 
 import {Controller} from './controller';
 
@@ -199,9 +202,12 @@
     // easier once the track table has entries for all the tracks.
     const cpuToTrackId = new Map();
     for (const track of Object.values(globals.state.tracks)) {
-      if (track.kind === 'CpuSliceTrack') {
-        cpuToTrackId.set((track.config as {cpu: number}).cpu, track.id);
-        continue;
+      if (exists(track?.uri)) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
+          const cpu = trackInfo?.tags?.cpu;
+          cpu && cpuToTrackId.set(cpu, track.id);
+        }
       }
     }
 
diff --git a/ui/src/core/protos.ts b/ui/src/core/protos.ts
index c3fdaca..0e55a5f 100644
--- a/ui/src/core/protos.ts
+++ b/ui/src/core/protos.ts
@@ -17,67 +17,80 @@
 // Aliases protos to avoid the super nested namespaces.
 // See https://www.typescriptlang.org/docs/handbook/namespaces.html#aliases
 import AndroidLogConfig = protos.perfetto.protos.AndroidLogConfig;
-import AndroidPowerConfig = protos.perfetto.protos.AndroidPowerConfig;
 import AndroidLogId = protos.perfetto.protos.AndroidLogId;
+import AndroidPowerConfig = protos.perfetto.protos.AndroidPowerConfig;
 import BatteryCounters =
     protos.perfetto.protos.AndroidPowerConfig.BatteryCounters;
 import BufferConfig = protos.perfetto.protos.TraceConfig.BufferConfig;
 import ChromeConfig = protos.perfetto.protos.ChromeConfig;
-import TrackEventConfig = protos.perfetto.protos.TrackEventConfig;
-import ConsumerPort = protos.perfetto.protos.ConsumerPort;
-import NetworkPacketTraceConfig =
-    protos.perfetto.protos.NetworkPacketTraceConfig;
-import NativeContinuousDumpConfig =
-    protos.perfetto.protos.HeapprofdConfig.ContinuousDumpConfig;
-import JavaContinuousDumpConfig =
-    protos.perfetto.protos.JavaHprofConfig.ContinuousDumpConfig;
-import DataSourceConfig = protos.perfetto.protos.DataSourceConfig;
-import DataSourceDescriptor = protos.perfetto.protos.DataSourceDescriptor;
-import FtraceConfig = protos.perfetto.protos.FtraceConfig;
-import HeapprofdConfig = protos.perfetto.protos.HeapprofdConfig;
-import JavaHprofConfig = protos.perfetto.protos.JavaHprofConfig;
-import PerfEventConfig = protos.perfetto.protos.PerfEventConfig;
-import IAndroidPowerConfig = protos.perfetto.protos.IAndroidPowerConfig;
-import IBufferConfig = protos.perfetto.protos.TraceConfig.IBufferConfig;
-import IProcessStatsConfig = protos.perfetto.protos.IProcessStatsConfig;
-import ISysStatsConfig = protos.perfetto.protos.ISysStatsConfig;
-import ITraceConfig = protos.perfetto.protos.ITraceConfig;
-import MeminfoCounters = protos.perfetto.protos.MeminfoCounters;
-import ProcessStatsConfig = protos.perfetto.protos.ProcessStatsConfig;
-import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
-import SysStatsConfig = protos.perfetto.protos.SysStatsConfig;
-import TraceConfig = protos.perfetto.protos.TraceConfig;
-import VmstatCounters = protos.perfetto.protos.VmstatCounters;
-import IPCFrame = protos.perfetto.protos.IPCFrame;
-import IMethodInfo =
-    protos.perfetto.protos.IPCFrame.BindServiceReply.IMethodInfo;
-import IBufferStats = protos.perfetto.protos.TraceStats.IBufferStats;
-import ISlice = protos.perfetto.protos.ReadBuffersResponse.ISlice;
-import EnableTracingRequest = protos.perfetto.protos.EnableTracingRequest;
-import DisableTracingRequest = protos.perfetto.protos.DisableTracingRequest;
-import GetTraceStatsRequest = protos.perfetto.protos.GetTraceStatsRequest;
-import FreeBuffersRequest = protos.perfetto.protos.FreeBuffersRequest;
-import ReadBuffersRequest = protos.perfetto.protos.ReadBuffersRequest;
-import QueryServiceStateRequest =
-    protos.perfetto.protos.QueryServiceStateRequest;
-import EnableTracingResponse = protos.perfetto.protos.EnableTracingResponse;
-import DisableTracingResponse = protos.perfetto.protos.DisableTracingResponse;
-import GetTraceStatsResponse = protos.perfetto.protos.GetTraceStatsResponse;
-import FreeBuffersResponse = protos.perfetto.protos.FreeBuffersResponse;
-import ReadBuffersResponse = protos.perfetto.protos.ReadBuffersResponse;
-import QueryServiceStateResponse =
-    protos.perfetto.protos.QueryServiceStateResponse;
-// Trace Processor protos.
-import QueryArgs = protos.perfetto.protos.QueryArgs;
-import ResetTraceProcessorArgs = protos.perfetto.protos.ResetTraceProcessorArgs;
-import StatusResult = protos.perfetto.protos.StatusResult;
 import ComputeMetricArgs = protos.perfetto.protos.ComputeMetricArgs;
 import ComputeMetricResult = protos.perfetto.protos.ComputeMetricResult;
+import ConsumerPort = protos.perfetto.protos.ConsumerPort;
+import DataSourceConfig = protos.perfetto.protos.DataSourceConfig;
+import DataSourceDescriptor = protos.perfetto.protos.DataSourceDescriptor;
 import DisableAndReadMetatraceResult =
     protos.perfetto.protos.DisableAndReadMetatraceResult;
-import Trace = protos.perfetto.protos.Trace;
-import TracePacket = protos.perfetto.protos.TracePacket;
+import DisableTracingRequest = protos.perfetto.protos.DisableTracingRequest;
+import DisableTracingResponse = protos.perfetto.protos.DisableTracingResponse;
+import EnableMetatraceArgs = protos.perfetto.protos.EnableMetatraceArgs;
+import EnableTracingRequest = protos.perfetto.protos.EnableTracingRequest;
+import EnableTracingResponse = protos.perfetto.protos.EnableTracingResponse;
+import FreeBuffersRequest = protos.perfetto.protos.FreeBuffersRequest;
+import FreeBuffersResponse = protos.perfetto.protos.FreeBuffersResponse;
+import FtraceConfig = protos.perfetto.protos.FtraceConfig;
+import GetTraceStatsRequest = protos.perfetto.protos.GetTraceStatsRequest;
+import GetTraceStatsResponse = protos.perfetto.protos.GetTraceStatsResponse;
+import HeapprofdConfig = protos.perfetto.protos.HeapprofdConfig;
+import IAndroidPowerConfig = protos.perfetto.protos.IAndroidPowerConfig;
+import IBufferConfig = protos.perfetto.protos.TraceConfig.IBufferConfig;
+import IBufferStats = protos.perfetto.protos.TraceStats.IBufferStats;
+import IDisableTracingResponse = protos.perfetto.protos.IDisableTracingResponse;
+import IEnableTracingResponse = protos.perfetto.protos.IEnableTracingResponse;
+import IFreeBuffersResponse = protos.perfetto.protos.IFreeBuffersResponse;
+import IGetTraceStatsResponse = protos.perfetto.protos.IGetTraceStatsResponse;
+import IMethodInfo =
+    protos.perfetto.protos.IPCFrame.BindServiceReply.IMethodInfo;
+import IPCFrame = protos.perfetto.protos.IPCFrame;
+import IProcessStatsConfig = protos.perfetto.protos.IProcessStatsConfig;
+import IReadBuffersResponse = protos.perfetto.protos.IReadBuffersResponse;
+import ISlice = protos.perfetto.protos.ReadBuffersResponse.ISlice;
+import ISysStatsConfig = protos.perfetto.protos.ISysStatsConfig;
+import ITraceConfig = protos.perfetto.protos.ITraceConfig;
+import ITraceStats = protos.perfetto.protos.ITraceStats;
+import JavaContinuousDumpConfig =
+    protos.perfetto.protos.JavaHprofConfig.ContinuousDumpConfig;
+import JavaHprofConfig = protos.perfetto.protos.JavaHprofConfig;
+import MeminfoCounters = protos.perfetto.protos.MeminfoCounters;
+import MetatraceCategories = protos.perfetto.protos.MetatraceCategories;
+import NativeContinuousDumpConfig =
+    protos.perfetto.protos.HeapprofdConfig.ContinuousDumpConfig;
+import NetworkPacketTraceConfig =
+    protos.perfetto.protos.NetworkPacketTraceConfig;
+import PerfEventConfig = protos.perfetto.protos.PerfEventConfig;
+import PerfEvents = protos.perfetto.protos.PerfEvents;
 import PerfettoMetatrace = protos.perfetto.protos.PerfettoMetatrace;
+import ProcessStatsConfig = protos.perfetto.protos.ProcessStatsConfig;
+import QueryArgs = protos.perfetto.protos.QueryArgs;
+import QueryResult = protos.perfetto.protos.QueryResult;
+import QueryServiceStateRequest =
+    protos.perfetto.protos.QueryServiceStateRequest;
+import QueryServiceStateResponse =
+    protos.perfetto.protos.QueryServiceStateResponse;
+import ReadBuffersRequest = protos.perfetto.protos.ReadBuffersRequest;
+import ReadBuffersResponse = protos.perfetto.protos.ReadBuffersResponse;
+import ResetTraceProcessorArgs = protos.perfetto.protos.ResetTraceProcessorArgs;
+import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
+import StatusResult = protos.perfetto.protos.StatusResult;
+import SysStatsConfig = protos.perfetto.protos.SysStatsConfig;
+import Trace = protos.perfetto.protos.Trace;
+import TraceConfig = protos.perfetto.protos.TraceConfig;
+import TracePacket = protos.perfetto.protos.TracePacket;
+import TraceProcessorApiVersion =
+    protos.perfetto.protos.TraceProcessorApiVersion;
+import TraceProcessorRpc = protos.perfetto.protos.TraceProcessorRpc;
+import TraceProcessorRpcStream = protos.perfetto.protos.TraceProcessorRpcStream;
+import TrackEventConfig = protos.perfetto.protos.TrackEventConfig;
+import VmstatCounters = protos.perfetto.protos.VmstatCounters;
 
 export {
   AndroidLogConfig,
@@ -94,6 +107,7 @@
   DisableAndReadMetatraceResult,
   DisableTracingRequest,
   DisableTracingResponse,
+  EnableMetatraceArgs,
   EnableTracingRequest,
   EnableTracingResponse,
   FreeBuffersRequest,
@@ -105,21 +119,30 @@
   IAndroidPowerConfig,
   IBufferConfig,
   IBufferStats,
+  IDisableTracingResponse,
+  IEnableTracingResponse,
+  IFreeBuffersResponse,
+  IGetTraceStatsResponse,
   IMethodInfo,
   IPCFrame,
   IProcessStatsConfig,
+  IReadBuffersResponse,
   ISlice,
   ISysStatsConfig,
   ITraceConfig,
+  ITraceStats,
   JavaContinuousDumpConfig,
   JavaHprofConfig,
   MeminfoCounters,
+  MetatraceCategories,
   NativeContinuousDumpConfig,
   NetworkPacketTraceConfig,
   PerfettoMetatrace,
   PerfEventConfig,
+  PerfEvents,
   ProcessStatsConfig,
   QueryArgs,
+  QueryResult,
   QueryServiceStateRequest,
   QueryServiceStateResponse,
   ReadBuffersRequest,
@@ -131,6 +154,10 @@
   Trace,
   TraceConfig,
   TracePacket,
+  TraceProcessorApiVersion,
+  TraceProcessorRpc,
+  TraceProcessorRpcStream,
   TrackEventConfig,
   VmstatCounters,
+
 };
diff --git a/ui/src/core/trace_config_utils.ts b/ui/src/core/trace_config_utils.ts
index e7e6daf..3f91edc 100644
--- a/ui/src/core/trace_config_utils.ts
+++ b/ui/src/core/trace_config_utils.ts
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TraceConfig} from '../core/protos';
-import {perfetto} from '../gen/protos';
+import {EnableTracingRequest, TraceConfig} from '../core/protos';
 
 // In this file are contained a few functions to simplify the proto parsing.
 
@@ -21,10 +20,9 @@
     Uint8Array|undefined {
   try {
     const enableTracingObject =
-        perfetto.protos.EnableTracingRequest.decode(enableTracingRequest);
+        EnableTracingRequest.decode(enableTracingRequest);
     if (!enableTracingObject.traceConfig) return undefined;
-    return perfetto.protos.TraceConfig.encode(enableTracingObject.traceConfig)
-        .finish();
+    return TraceConfig.encode(enableTracingObject.traceConfig).finish();
   } catch (e) {  // This catch is for possible proto encoding/decoding issues.
     console.error('Error extracting the config: ', e.message);
     return undefined;
@@ -33,7 +31,7 @@
 
 export function extractDurationFromTraceConfig(traceConfigProto: Uint8Array) {
   try {
-    return perfetto.protos.TraceConfig.decode(traceConfigProto).durationMs;
+    return TraceConfig.decode(traceConfigProto).durationMs;
   } catch (e) {  // This catch is for possible proto encoding/decoding issues.
     return undefined;
   }
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index a7e168d..209583c 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -85,7 +85,7 @@
   private height = 0;
   private previousHeight = this.height;
   private resize: (height: number) => void = () => {};
-  private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
+  private isClosed = this.height <= 0;
   private isFullscreen = false;
   // We can't get real fullscreen height until the pan_and_zoom_handler exists.
   private fullscreenHeight = getDetailsHeight();
@@ -94,7 +94,7 @@
   oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) {
     this.resize = attrs.resize;
     this.height = attrs.height;
-    this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
+    this.isClosed = this.height <= 0;
     this.fullscreenHeight = getFullScreenHeight();
     const elem = dom as HTMLElement;
     this.trash.add(new DragGestureHandler(
@@ -107,7 +107,7 @@
   onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) {
     this.resize = attrs.resize;
     this.height = attrs.height;
-    this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX;
+    this.isClosed = this.height <= 0;
   }
 
   onremove(_: m.CVnodeDOM<DragHandleAttrs>) {
@@ -117,7 +117,7 @@
   onDrag(_x: number, y: number) {
     const newHeight =
         Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y);
-    this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX;
+    this.isClosed = newHeight <= 0;
     this.isFullscreen = newHeight >= this.fullscreenHeight;
     this.resize(newHeight);
     raf.scheduleFullRedraw();
@@ -154,7 +154,7 @@
               onclick: () => {
                 this.isClosed = false;
                 this.isFullscreen = true;
-                this.resize(this.fullscreenHeight - DRAG_HANDLE_HEIGHT_PX);
+                this.resize(this.fullscreenHeight);
                 raf.scheduleFullRedraw();
               },
               title: 'Open fullscreen',
@@ -164,7 +164,7 @@
           m('i.material-icons',
             {
               onclick: () => {
-                if (this.height === DRAG_HANDLE_HEIGHT_PX) {
+                if (this.height === 0) {
                   this.isClosed = false;
                   if (this.previousHeight === 0) {
                     this.previousHeight = getDetailsHeight();
diff --git a/ui/src/frontend/rpc_http_dialog.ts b/ui/src/frontend/rpc_http_dialog.ts
index 024a113..7684fc5 100644
--- a/ui/src/frontend/rpc_http_dialog.ts
+++ b/ui/src/frontend/rpc_http_dialog.ts
@@ -17,15 +17,14 @@
 import {assertExists} from '../base/logging';
 import {Actions} from '../common/actions';
 import {HttpRpcEngine, RPC_URL} from '../common/http_rpc_engine';
-import {StatusResult} from '../core/protos';
+import {StatusResult, TraceProcessorApiVersion} from '../core/protos';
 import {VERSION} from '../gen/perfetto_version';
-import {perfetto} from '../gen/protos';
 
 import {globals} from './globals';
 import {showModal} from './modal';
 
-const CURRENT_API_VERSION = perfetto.protos.TraceProcessorApiVersion
-                                .TRACE_PROCESSOR_CURRENT_API_VERSION;
+const CURRENT_API_VERSION =
+    TraceProcessorApiVersion.TRACE_PROCESSOR_CURRENT_API_VERSION;
 
 const PROMPT = `Trace Processor Native Accelerator detected on ${RPC_URL} with:
 $loadedTraceName
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index 687c15a..bb1b2c7 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -20,10 +20,13 @@
   Time,
   time,
 } from '../base/time';
+import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {EngineProxy} from '../common/engine';
+import {pluginManager} from '../common/plugins';
 import {LONG, NUM, NUM_NULL, STR_NULL} from '../common/query_result';
 import {translateState} from '../common/thread_state';
+import {CPU_SLICE_TRACK_KIND} from '../tracks/cpu_slices';
 import {Anchor} from '../widgets/anchor';
 
 import {globals} from './globals';
@@ -141,9 +144,14 @@
 export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: time) {
   let trackId: string|undefined;
   for (const track of Object.values(globals.state.tracks)) {
-    if (track.kind === 'CpuSliceTrack' &&
-        (track.config as {cpu: number}).cpu === cpu) {
-      trackId = track.id;
+    if (exists(track?.uri)) {
+      const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
+        if (trackInfo?.tags?.cpu === cpu) {
+          trackId = track.id;
+          break;
+        }
+      }
     }
   }
   if (trackId === undefined) {
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index b8020fc..8dffff1 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -345,8 +345,7 @@
   private track: TrackLike|undefined;
   private trackState: TrackState|undefined;
 
-  constructor(vnode: m.CVnode<TrackPanelAttrs>) {
-    super();
+  private tryLoadTrack(vnode: m.CVnode<TrackPanelAttrs>) {
     const trackId = vnode.attrs.id;
     const trackState = globals.state.tracks[trackId];
 
@@ -359,7 +358,11 @@
     this.trackState = trackState;
   }
 
-  view() {
+  view(vnode: m.CVnode<TrackPanelAttrs>) {
+    if (!this.track) {
+      this.tryLoadTrack(vnode);
+    }
+
     if (this.track === undefined || this.trackState === undefined) {
       return m('div', 'No such track');
     }
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index adafeb6..b939428 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -280,15 +280,27 @@
 // A predicate for selecting a groups of tracks.
 export type TrackPredicate = (info: TrackTags) => boolean;
 
-// An set of key/value pairs describing a given track. These
-// are used for selecting tracks to pin/unpin and (in future) the
-// sorting and grouping of tracks. The values are always strings.
-export interface TrackTags {
+interface WellKnownTrackTags {
   // A human readable name for this specific track.
-  name?: string;
+  name: string;
 
+  // This is where "XXX_TRACK_KIND" values should be placed.
+  kind: string;
+
+  // The CPU number associated with this track.
+  cpu: number;
+}
+
+// An set of key/value pairs describing a given track. These are used for
+// selecting tracks to pin/unpin and (in future) the sorting and grouping of
+// tracks.
+// These are also (ab)used for communicating information about tracks for the
+// purposes of locating tracks by their properties e.g. aggregation & search.
+// We define a handful of well known fields, and the rest are arbitrary key-
+// value pairs.
+export type TrackTags = Partial<WellKnownTrackTags>&{
   // There may be arbitrary other key/value pairs.
-  [key: string]: string|undefined;
+  [key: string]: string|number|undefined;
 }
 
 // Plugins can be passed as class refs, factory functions, or concrete plugin
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index 73a89dc..948f82d 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -155,7 +155,6 @@
       ctx.addTrack({
         uri: 'perfetto.AndroidLog',
         displayName: 'Android logs',
-        tags: {type: 'counter'},
         trackFactory: ({trackInstanceId}) => {
           return new TrackWithControllerAdapter<Config, Data>(
               ctx.engine,
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index b2d49b6..40526a4 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -43,6 +43,8 @@
   TracePluginContext,
 } from '../../public';
 
+export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
+
 export interface Data extends TrackData {
   // Slices are stored in a columnar fashion. All fields have the same length.
   ids: Float64Array;
@@ -483,8 +485,8 @@
         uri,
         displayName: name,
         tags: {
-          cpu: `${cpu}`,
-          type: 'cpu_sched',
+          cpu,
+          kind: CPU_SLICE_TRACK_KIND,
         },
         trackFactory: ({trackInstanceId}) => {
           return new TrackWithControllerAdapter<Config, Data>(