metrics: Implement loading metric extension directory

You can now pass in --metric-extension flag to trace_processor_shell.
The flag is in the format disk_path:virtual_path.

Any internal metric proto/sql files matching the virtual_path location
is skipped. All protos in the disk_path are loaded with
"protos/perfetto/metrics/virtual_path/" location, and all SQL files are
loaded in the "virtual_path/" location.

Design doc: go/perfetto-runtime-metrics-import

Bug: 182165266
Change-Id: I6ce3d5881219e5099b6eb9c6f653b1dfded51297
diff --git a/include/perfetto/ext/base/file_utils.h b/include/perfetto/ext/base/file_utils.h
index bf33d70..0acd537 100644
--- a/include/perfetto/ext/base/file_utils.h
+++ b/include/perfetto/ext/base/file_utils.h
@@ -21,9 +21,11 @@
 #include <stddef.h>
 
 #include <string>
+#include <vector>
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/export.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/utils.h"
 
@@ -78,6 +80,17 @@
 // Wrapper around access(path, F_OK).
 bool FileExists(const std::string& path);
 
+// Gets the extension for a filename. If the file has two extensions, returns
+// only the last one (foo.pb.gz => .gz). Returns empty string if there is no
+// extension.
+std::string GetFileExtension(const std::string& filename);
+
+// Puts the path to all files under |dir_path| in |output|, recursively walking
+// subdirectories. File paths are relative to |dir_path|. Only files are
+// included, not directories.
+base::Status ListFilesRecursive(const std::string& dir_path,
+                                std::vector<std::string>& output);
+
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/include/perfetto/ext/base/string_utils.h b/include/perfetto/ext/base/string_utils.h
index 13b8f16..a44ea73 100644
--- a/include/perfetto/ext/base/string_utils.h
+++ b/include/perfetto/ext/base/string_utils.h
@@ -97,6 +97,8 @@
 
 bool StartsWith(const std::string& str, const std::string& prefix);
 bool EndsWith(const std::string& str, const std::string& suffix);
+bool StartsWithAny(const std::string& str,
+                   const std::vector<std::string>& prefixes);
 bool Contains(const std::string& haystack, const std::string& needle);
 bool Contains(const std::string& haystack, char needle);
 size_t Find(const StringView& needle, const StringView& haystack);
diff --git a/include/perfetto/trace_processor/basic_types.h b/include/perfetto/trace_processor/basic_types.h
index 2b38b6d..05735b9 100644
--- a/include/perfetto/trace_processor/basic_types.h
+++ b/include/perfetto/trace_processor/basic_types.h
@@ -23,6 +23,7 @@
 #include <stdint.h>
 #include <functional>
 #include <string>
+#include <vector>
 
 #include "perfetto/base/export.h"
 #include "perfetto/base/logging.h"
@@ -34,6 +35,10 @@
 // simpler (e.g. use arrays instead of vectors).
 constexpr size_t kMaxCpus = 128;
 
+// All metrics protos are in this directory. When loading metric extensions, the
+// protos are mounted onto a virtual path inside this directory.
+constexpr char kMetricProtoRoot[] = "protos/perfetto/metrics/";
+
 // Enum which encodes how trace processor should try to sort the ingested data.
 enum class SortingMode {
   // This option allows trace processor to use built-in heuristics about how to
@@ -106,6 +111,10 @@
   // the trace before that event. See the ennu documenetation for more details.
   DropFtraceDataBefore drop_ftrace_data_before =
       DropFtraceDataBefore::kTracingStarted;
+
+  // Any built-in metric proto or sql files matching these paths are skipped
+  // during trace processor metric initialization.
+  std::vector<std::string> skip_builtin_metric_paths;
 };
 
 // Represents a dynamically typed value returned by SQL.
diff --git a/include/perfetto/trace_processor/trace_processor.h b/include/perfetto/trace_processor/trace_processor.h
index c949695..77a7889 100644
--- a/include/perfetto/trace_processor/trace_processor.h
+++ b/include/perfetto/trace_processor/trace_processor.h
@@ -57,6 +57,13 @@
   // proto builder functions when computing metrics.
   virtual util::Status ExtendMetricsProto(const uint8_t* data, size_t size) = 0;
 
+  // Behaves exactly as ExtendMetricsProto, except any FileDescriptor with
+  // filename matching a prefix in |skip_prefixes| is skipped.
+  virtual util::Status ExtendMetricsProto(
+      const uint8_t* data,
+      size_t size,
+      const std::vector<std::string>& skip_prefixes) = 0;
+
   // Computes the given metrics on the loded portion of the trace. If
   // successful, the output argument |metrics_proto| will be filled with the
   // proto-encoded bytes for the message TraceMetrics in
diff --git a/src/base/file_utils.cc b/src/base/file_utils.cc
index 9b92af2..baa9b2e 100644
--- a/src/base/file_utils.cc
+++ b/src/base/file_utils.cc
@@ -20,10 +20,13 @@
 #include <sys/types.h>
 
 #include <algorithm>
+#include <string>
+#include <vector>
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/platform_handle.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/scoped_file.h"
 #include "perfetto/ext/base/utils.h"
 
@@ -208,5 +211,39 @@
 #endif
 }
 
+base::Status ListFilesRecursive(const std::string& dir_path,
+                                std::vector<std::string>& output) {
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  // TODO(b/182165266): Write the windows equivalent of this function.
+  return base::ErrStatus("ListFilesRecursive not supported in windows yet");
+#else
+  DIR* dir = opendir(dir_path.c_str());
+  if (dir == nullptr) {
+    return base::ErrStatus("Failed to open directory %s", dir_path.c_str());
+  }
+  for (auto* dirent = readdir(dir); dirent != nullptr; dirent = readdir(dir)) {
+    if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) {
+      continue;
+    }
+    if (dirent->d_type == DT_DIR) {
+      std::string full_path = dir_path + '/' + dirent->d_name;
+      auto status = ListFilesRecursive(full_path, output);
+      if (!status.ok())
+        return status;
+    } else if (dirent->d_type == DT_REG) {
+      output.push_back(dirent->d_name);
+    }
+  }
+  return base::OkStatus();
+#endif
+}
+
+std::string GetFileExtension(const std::string& filename) {
+  auto ext_idx = filename.rfind('.');
+  if (ext_idx == std::string::npos)
+    return std::string();
+  return filename.substr(ext_idx);
+}
+
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/string_utils.cc b/src/base/string_utils.cc
index 975e42c..a6ee833 100644
--- a/src/base/string_utils.cc
+++ b/src/base/string_utils.cc
@@ -18,12 +18,12 @@
 
 #include <locale.h>
 #include <string.h>
+#include <algorithm>
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
 #include <xlocale.h>
 #endif
 
-#include <algorithm>
 #include <cinttypes>
 
 #include "perfetto/base/logging.h"
@@ -88,6 +88,13 @@
   return str.compare(0, prefix.length(), prefix) == 0;
 }
 
+bool StartsWithAny(const std::string& str,
+                   const std::vector<std::string>& prefixes) {
+  return std::any_of(
+      prefixes.begin(), prefixes.end(),
+      [&str](const std::string& prefix) { return StartsWith(str, prefix); });
+}
+
 bool EndsWith(const std::string& str, const std::string& suffix) {
   if (suffix.size() > str.size())
     return false;
diff --git a/src/base/string_utils_unittest.cc b/src/base/string_utils_unittest.cc
index 5c41f16..2c7108d 100644
--- a/src/base/string_utils_unittest.cc
+++ b/src/base/string_utils_unittest.cc
@@ -156,6 +156,14 @@
   EXPECT_FALSE(StartsWith("", "ab"));
 }
 
+TEST(StringUtilsTest, StartsWithAny) {
+  EXPECT_FALSE(StartsWithAny("", {"a", "b"}));
+  EXPECT_FALSE(StartsWithAny("abcd", {}));
+  EXPECT_FALSE(StartsWithAny("", {}));
+  EXPECT_TRUE(StartsWithAny("abcd", {"ac", "ab"}));
+  EXPECT_FALSE(StartsWithAny("abcd", {"bc", "ac"}));
+}
+
 TEST(StringUtilsTest, EndsWith) {
   EXPECT_TRUE(EndsWith("", ""));
   EXPECT_TRUE(EndsWith("abc", ""));
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index be5f1ff..4b133a8 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -69,6 +69,7 @@
   auto extension = decoder.extension_set();
   return context_->descriptor_pool_->AddFromFileDescriptorSet(
       extension.data, extension.size,
+      /*skip_prefixes*/ {},
       /*merge_existing_messages=*/true);
 }
 
diff --git a/src/trace_processor/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index c6a8c62..e427f93 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -35,6 +35,44 @@
 
 constexpr size_t kMaxChunkSize = 4 * 1024 * 1024;
 
+TEST(TraceProcessorCustomConfigTest, SkipInternalMetricsMatchingMountPath) {
+  auto config = Config();
+  config.skip_builtin_metric_paths = {"android/"};
+  auto processor = TraceProcessor::CreateInstance(config);
+  processor->NotifyEndOfFile();
+
+  // Check that andorid metrics have not been loaded.
+  auto it = processor->ExecuteQuery(
+      "select count(*) from trace_metrics "
+      "where name = 'android_cpu';");
+  ASSERT_TRUE(it.Next());
+  ASSERT_EQ(it.Get(0).type, SqlValue::kLong);
+  ASSERT_EQ(it.Get(0).long_value, 0);
+
+  // Check that other metrics have been loaded.
+  it = processor->ExecuteQuery(
+      "select count(*) from trace_metrics "
+      "where name = 'trace_metadata';");
+  ASSERT_TRUE(it.Next());
+  ASSERT_EQ(it.Get(0).type, SqlValue::kLong);
+  ASSERT_EQ(it.Get(0).long_value, 1);
+}
+
+TEST(TraceProcessorCustomConfigTest, HandlesMalformedMountPath) {
+  auto config = Config();
+  config.skip_builtin_metric_paths = {"", "androi"};
+  auto processor = TraceProcessor::CreateInstance(config);
+  processor->NotifyEndOfFile();
+
+  // Check that andorid metrics have been loaded.
+  auto it = processor->ExecuteQuery(
+      "select count(*) from trace_metrics "
+      "where name = 'android_cpu';");
+  ASSERT_TRUE(it.Next());
+  ASSERT_EQ(it.Get(0).type, SqlValue::kLong);
+  ASSERT_EQ(it.Get(0).long_value, 1);
+}
+
 class TraceProcessorIntegrationTest : public ::testing::Test {
  public:
   TraceProcessorIntegrationTest()
@@ -397,6 +435,7 @@
   ASSERT_TRUE(it.Next());
   EXPECT_STREQ(it.Get(0).string_value, "123e4567-e89b-12d3-a456-426655443322");
 }
+
 }  // namespace
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 97a1ba9..0d7b5ea 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -634,14 +634,37 @@
   }
 }
 
+std::vector<std::string> SanitizeMetricMountPaths(
+    const std::vector<std::string>& mount_paths) {
+  std::vector<std::string> sanitized;
+  for (const auto& path : mount_paths) {
+    if (path.length() == 0)
+      continue;
+    sanitized.push_back(path);
+    if (path.back() != '/')
+      sanitized.back().append("/");
+  }
+  return sanitized;
+}
+
 void SetupMetrics(TraceProcessor* tp,
                   sqlite3* db,
-                  std::vector<metrics::SqlMetricFile>* sql_metrics) {
-  tp->ExtendMetricsProto(kMetricsDescriptor.data(), kMetricsDescriptor.size());
+                  std::vector<metrics::SqlMetricFile>* sql_metrics,
+                  const std::vector<std::string>& extension_paths) {
+  const std::vector<std::string> sanitized_extension_paths =
+      SanitizeMetricMountPaths(extension_paths);
+  std::vector<std::string> skip_prefixes;
+  for (const auto& path : sanitized_extension_paths) {
+    skip_prefixes.push_back(kMetricProtoRoot + path);
+  }
+  tp->ExtendMetricsProto(kMetricsDescriptor.data(), kMetricsDescriptor.size(),
+                         skip_prefixes);
   tp->ExtendMetricsProto(kAllChromeMetricsDescriptor.data(),
-                         kAllChromeMetricsDescriptor.size());
+                         kAllChromeMetricsDescriptor.size(), skip_prefixes);
 
   for (const auto& file_to_sql : metrics::sql_metrics::kFileToSql) {
+    if (base::StartsWithAny(file_to_sql.path, sanitized_extension_paths))
+      continue;
     tp->RegisterMetric(file_to_sql.path, file_to_sql.sql);
   }
 
@@ -731,7 +754,7 @@
   CreateValueAtMaxTsFunction(db);
   CreateUnwrapMetricProtoFunction(db);
 
-  SetupMetrics(this, *db_, &sql_metrics_);
+  SetupMetrics(this, *db_, &sql_metrics_, cfg.skip_builtin_metric_paths);
 
   // Setup the query cache.
   query_cache_.reset(new QueryCache());
@@ -1012,7 +1035,15 @@
 
 util::Status TraceProcessorImpl::ExtendMetricsProto(const uint8_t* data,
                                                     size_t size) {
-  util::Status status = pool_.AddFromFileDescriptorSet(data, size);
+  return ExtendMetricsProto(data, size, /*skip_prefixes*/ {});
+}
+
+util::Status TraceProcessorImpl::ExtendMetricsProto(
+    const uint8_t* data,
+    size_t size,
+    const std::vector<std::string>& skip_prefixes) {
+  util::Status status =
+      pool_.AddFromFileDescriptorSet(data, size, skip_prefixes);
   if (!status.ok())
     return status;
 
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index cb9ef9f..5db351c 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -61,6 +61,11 @@
 
   util::Status ExtendMetricsProto(const uint8_t* data, size_t size) override;
 
+  util::Status ExtendMetricsProto(
+      const uint8_t* data,
+      size_t size,
+      const std::vector<std::string>& skip_prefixes) override;
+
   util::Status ComputeMetric(const std::vector<std::string>& metric_names,
                              std::vector<uint8_t>* metrics) override;
 
@@ -118,7 +123,6 @@
   uint64_t bytes_parsed_ = 0;
 };
 
-
 }  // namespace trace_processor
 }  // namespace perfetto
 
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 9b2d671..b50c172 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -21,6 +21,7 @@
 #include <cinttypes>
 #include <functional>
 #include <iostream>
+#include <unordered_set>
 #include <vector>
 
 #include <google/protobuf/compiler/parser.h>
@@ -30,6 +31,7 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
 #include "perfetto/base/time.h"
 #include "perfetto/ext/base/file_utils.h"
 #include "perfetto/ext/base/getopt.h"
@@ -42,6 +44,7 @@
 #include "perfetto/trace_processor/trace_processor.h"
 #include "src/trace_processor/metrics/chrome/all_chrome_metrics.descriptor.h"
 #include "src/trace_processor/metrics/metrics.descriptor.h"
+#include "src/trace_processor/metrics/metrics.h"
 #include "src/trace_processor/util/proto_to_json.h"
 #include "src/trace_processor/util/status_macros.h"
 
@@ -343,23 +346,28 @@
   return g_tp->RegisterMetric(path, sql);
 }
 
-util::Status ExtendMetricsProto(const std::string& extend_metrics_proto,
-                                google::protobuf::DescriptorPool* pool) {
-  google::protobuf::FileDescriptorSet desc_set;
-
-  base::ScopedFile file(base::OpenFile(extend_metrics_proto, O_RDONLY));
+base::Status ParseToFileDescriptorProto(
+    const std::string& filename,
+    google::protobuf::FileDescriptorProto* file_desc) {
+  base::ScopedFile file(base::OpenFile(filename, O_RDONLY));
   if (file.get() == -1) {
-    return util::ErrStatus("Failed to open proto file %s",
-                           extend_metrics_proto.c_str());
+    return base::ErrStatus("Failed to open proto file %s", filename.c_str());
   }
 
   google::protobuf::io::FileInputStream stream(file.get());
   ErrorPrinter printer;
   google::protobuf::io::Tokenizer tokenizer(&stream, &printer);
 
-  auto* file_desc = desc_set.add_file();
   google::protobuf::compiler::Parser parser;
   parser.Parse(&tokenizer, file_desc);
+  return base::OkStatus();
+}
+
+util::Status ExtendMetricsProto(const std::string& extend_metrics_proto,
+                                google::protobuf::DescriptorPool* pool) {
+  google::protobuf::FileDescriptorSet desc_set;
+  auto* file_desc = desc_set.add_file();
+  RETURN_IF_ERROR(ParseToFileDescriptorProto(extend_metrics_proto, file_desc));
 
   file_desc->set_name(BaseName(extend_metrics_proto));
   pool->BuildFile(*file_desc);
@@ -656,6 +664,33 @@
   return util::OkStatus();
 }
 
+class MetricExtension {
+ public:
+  void SetDiskPath(std::string path) {
+    AddTrailingSlashIfNeeded(path);
+    disk_path_ = std::move(path);
+  }
+  void SetVirtualPath(std::string path) {
+    AddTrailingSlashIfNeeded(path);
+    virtual_path_ = std::move(path);
+  }
+
+  // Disk location. Ends with a trailing slash.
+  const std::string& disk_path() const { return disk_path_; }
+  // Virtual location. Ends with a trailing slash.
+  const std::string& virtual_path() const { return virtual_path_; }
+
+ private:
+  std::string disk_path_;
+  std::string virtual_path_;
+
+  static void AddTrailingSlashIfNeeded(std::string& path) {
+    if (path.length() > 0 && path[path.length() - 1] != '/') {
+      path.push_back('/');
+    }
+  }
+};
+
 struct CommandLineOptions {
   std::string perf_file_path;
   std::string query_file_path;
@@ -665,6 +700,7 @@
   std::string metric_output;
   std::string trace_file_path;
   std::string port_number;
+  std::vector<std::string> raw_metric_extensions;
   bool launch_shell = false;
   bool enable_httpd = false;
   bool wide = false;
@@ -716,7 +752,12 @@
                                       writing the resulting trace into FILE.
  --full-sort                          Forces the trace processor into performing
                                       a full sort ignoring any windowing
-                                      logic.)",
+                                      logic.
+ --metric-extension DISK_PATH:VIRTUAL_PATH
+                                      Loads metric proto and sql files from
+                                      DISK_PATH/protos and DISK_PATH/sql
+                                      respectively, and mounts them onto
+                                      VIRTUAL_PATH.)",
                 argv[0]);
 }
 
@@ -728,6 +769,7 @@
     OPT_METRICS_OUTPUT,
     OPT_FORCE_FULL_SORT,
     OPT_HTTP_PORT,
+    OPT_METRIC_EXTENSION,
   };
 
   static const option long_options[] = {
@@ -746,6 +788,7 @@
       {"metrics-output", required_argument, nullptr, OPT_METRICS_OUTPUT},
       {"full-sort", no_argument, nullptr, OPT_FORCE_FULL_SORT},
       {"http-port", required_argument, nullptr, OPT_HTTP_PORT},
+      {"metric-extension", required_argument, nullptr, OPT_METRIC_EXTENSION},
       {nullptr, 0, nullptr, 0}};
 
   bool explicit_interactive = false;
@@ -832,6 +875,11 @@
       continue;
     }
 
+    if (option == OPT_METRIC_EXTENSION) {
+      command_line_options.raw_metric_extensions.push_back(optarg);
+      continue;
+    }
+
     PrintUsage(argv);
     exit(option == 'h' ? 0 : 1);
   }
@@ -858,16 +906,20 @@
     PrintUsage(argv);
     exit(1);
   }
+
   return command_line_options;
 }
 
 void ExtendPoolWithBinaryDescriptor(google::protobuf::DescriptorPool& pool,
                                     const void* data,
-                                    int size) {
+                                    int size,
+                                    std::vector<std::string>& skip_prefixes) {
   google::protobuf::FileDescriptorSet desc_set;
   desc_set.ParseFromArray(data, size);
-  for (const auto& desc : desc_set.file()) {
-    pool.BuildFile(desc);
+  for (const auto& file_desc : desc_set.file()) {
+    if (base::StartsWithAny(file_desc.name(), skip_prefixes))
+      continue;
+    pool.BuildFile(file_desc);
   }
 }
 
@@ -941,16 +993,151 @@
   return util::OkStatus();
 }
 
-util::Status RunMetrics(const CommandLineOptions& options) {
-  // Descriptor pool used for printing output as textproto.
-  // Building on top of generated pool so default protos in
-  // google.protobuf.descriptor.proto are available.
+base::Status ParseSingleMetricExtensionPath(const std::string& raw_extension,
+                                            MetricExtension& parsed_extension) {
+  std::vector<std::string> parts = base::SplitString(raw_extension, ":");
+  if (parts.size() != 2 || parts[0].length() == 0 || parts[1].length() == 0) {
+    return base::ErrStatus(
+        "--metric-extension-dir must be of format disk_path:virtual_path");
+  }
+
+  parsed_extension.SetDiskPath(std::move(parts[0]));
+  parsed_extension.SetVirtualPath(std::move(parts[1]));
+
+  if (parsed_extension.virtual_path() == "shell/") {
+    return base::Status(
+        "Cannot have 'shell/' as metric extension virtual path.");
+  }
+  return util::OkStatus();
+}
+
+base::Status CheckForDuplicateMetricExtension(
+    const std::vector<MetricExtension>& metric_extensions) {
+  std::unordered_set<std::string> disk_paths;
+  std::unordered_set<std::string> virtual_paths;
+  for (const auto& extension : metric_extensions) {
+    auto ret = disk_paths.insert(extension.disk_path());
+    if (!ret.second) {
+      return base::ErrStatus(
+          "Another metric extension is already using disk path %s",
+          extension.disk_path().c_str());
+    }
+    ret = virtual_paths.insert(extension.virtual_path());
+    if (!ret.second) {
+      return base::ErrStatus(
+          "Another metric extension is already using virtual path %s",
+          extension.virtual_path().c_str());
+    }
+  }
+  return base::OkStatus();
+}
+
+base::Status ParseMetricExtensionPaths(
+    const std::vector<std::string>& raw_metric_extensions,
+    std::vector<MetricExtension>& metric_extensions) {
+  for (const auto& raw_extension : raw_metric_extensions) {
+    metric_extensions.push_back({});
+    RETURN_IF_ERROR(ParseSingleMetricExtensionPath(raw_extension,
+                                                   metric_extensions.back()));
+  }
+  return CheckForDuplicateMetricExtension(metric_extensions);
+}
+
+base::Status LoadMetricExtensionProtos(const std::string& proto_root,
+                                       const std::string& mount_path) {
+  if (!base::FileExists(proto_root)) {
+    return base::ErrStatus(
+        "Directory %s does not exist. Metric extension directory must contain "
+        "a 'sql/' and 'protos/' subdirectory.",
+        proto_root.c_str());
+  }
+  std::vector<std::string> proto_files;
+  RETURN_IF_ERROR(base::ListFilesRecursive(proto_root, proto_files));
+
+  google::protobuf::FileDescriptorSet parsed_protos;
+  for (const auto& file_path : proto_files) {
+    if (base::GetFileExtension(file_path) != ".proto")
+      continue;
+    auto* file_desc = parsed_protos.add_file();
+    ParseToFileDescriptorProto(proto_root + file_path, file_desc);
+    file_desc->set_name(mount_path + file_path);
+  }
+
+  std::vector<uint8_t> serialized_filedescset;
+  serialized_filedescset.resize(parsed_protos.ByteSizeLong());
+  parsed_protos.SerializeToArray(
+      serialized_filedescset.data(),
+      static_cast<int>(serialized_filedescset.size()));
+
+  RETURN_IF_ERROR(g_tp->ExtendMetricsProto(serialized_filedescset.data(),
+                                           serialized_filedescset.size()));
+
+  return base::OkStatus();
+}
+
+base::Status LoadMetricExtensionSql(const std::string& sql_root,
+                                    const std::string& mount_path) {
+  if (!base::FileExists(sql_root)) {
+    return base::ErrStatus(
+        "Directory %s does not exist. Metric extension directory must contain "
+        "a 'sql/' and 'protos/' subdirectory.",
+        sql_root.c_str());
+  }
+
+  std::vector<std::string> sql_files;
+  RETURN_IF_ERROR(base::ListFilesRecursive(sql_root, sql_files));
+  for (const auto& file_path : sql_files) {
+    if (base::GetFileExtension(file_path) != ".sql")
+      continue;
+    std::string file_contents;
+    if (!base::ReadFile(sql_root + file_path, &file_contents)) {
+      return base::ErrStatus("Cannot read file %s", file_path.c_str());
+    }
+    RETURN_IF_ERROR(
+        g_tp->RegisterMetric(mount_path + file_path, file_contents));
+  }
+
+  return base::OkStatus();
+}
+
+base::Status LoadMetricExtension(const MetricExtension& extension) {
+  const std::string& disk_path = extension.disk_path();
+  const std::string& virtual_path = extension.virtual_path();
+
+  if (!base::FileExists(disk_path)) {
+    return base::ErrStatus("Metric extension directory %s does not exist",
+                           disk_path.c_str());
+  }
+
+  // Note: Proto files must be loaded first, because we determine whether an SQL
+  // file is a metric or not by checking if the name matches a field of the root
+  // TraceMetrics proto.
+  RETURN_IF_ERROR(LoadMetricExtensionProtos(disk_path + "protos/",
+                                            kMetricProtoRoot + virtual_path));
+  RETURN_IF_ERROR(LoadMetricExtensionSql(disk_path + "sql/", virtual_path));
+
+  return base::OkStatus();
+}
+
+util::Status RunMetrics(const CommandLineOptions& options,
+                        std::vector<MetricExtension>& metric_extensions) {
+  // Descriptor pool used for printing output as textproto. Building on top of
+  // generated pool so default protos in google.protobuf.descriptor.proto are
+  // available.
   google::protobuf::DescriptorPool pool(
       google::protobuf::DescriptorPool::generated_pool());
+  // TODO(b/182165266): There is code duplication here with trace_processor_impl
+  // SetupMetrics. This will be removed when we switch the output formatter to
+  // use internal DescriptorPool.
+  std::vector<std::string> skip_prefixes;
+  for (const auto& ext : metric_extensions) {
+    skip_prefixes.push_back(kMetricProtoRoot + ext.virtual_path());
+  }
   ExtendPoolWithBinaryDescriptor(pool, kMetricsDescriptor.data(),
-                                 kMetricsDescriptor.size());
+                                 kMetricsDescriptor.size(), skip_prefixes);
   ExtendPoolWithBinaryDescriptor(pool, kAllChromeMetricsDescriptor.data(),
-                                 kAllChromeMetricsDescriptor.size());
+                                 kAllChromeMetricsDescriptor.size(),
+                                 skip_prefixes);
 
   std::vector<std::string> metrics;
   for (base::StringSplitter ss(options.metric_names, ','); ss.Next();) {
@@ -995,6 +1182,7 @@
   } else {
     format = OutputFormat::kTextProto;
   }
+
   return RunMetrics(std::move(metrics), format, pool);
 }
 
@@ -1058,6 +1246,14 @@
                             ? SortingMode::kForceFullSort
                             : SortingMode::kDefaultHeuristics;
 
+  std::vector<MetricExtension> metric_extensions;
+  RETURN_IF_ERROR(ParseMetricExtensionPaths(options.raw_metric_extensions,
+                                            metric_extensions));
+
+  for (const auto& extension : metric_extensions) {
+    config.skip_builtin_metric_paths.push_back(extension.virtual_path());
+  }
+
   std::unique_ptr<TraceProcessor> tp = TraceProcessor::CreateInstance(config);
   g_tp = tp.get();
 
@@ -1066,6 +1262,13 @@
     tp->EnableMetatrace();
   }
 
+  // We load all the metric extensions even when --run-metrics arg is not there,
+  // because we want the metrics to be available in interactive mode or when
+  // used in UI using httpd.
+  for (const auto& extension : metric_extensions) {
+    RETURN_IF_ERROR(LoadMetricExtension(extension));
+  }
+
   base::TimeNanos t_load{};
   if (!options.trace_file_path.empty()) {
     base::TimeNanos t_load_start = base::GetWallTimeNs();
@@ -1097,7 +1300,7 @@
   }
 
   if (!options.metric_names.empty()) {
-    RETURN_IF_ERROR(RunMetrics(options));
+    RETURN_IF_ERROR(RunMetrics(options, metric_extensions));
   }
 
   if (!options.query_file_path.empty()) {
diff --git a/src/trace_processor/util/descriptors.cc b/src/trace_processor/util/descriptors.cc
index 068da6f..4990b38 100644
--- a/src/trace_processor/util/descriptors.cc
+++ b/src/trace_processor/util/descriptors.cc
@@ -15,6 +15,8 @@
  */
 
 #include "src/trace_processor/util/descriptors.h"
+
+#include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/field.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
@@ -208,15 +210,16 @@
 util::Status DescriptorPool::AddFromFileDescriptorSet(
     const uint8_t* file_descriptor_set_proto,
     size_t size,
+    const std::vector<std::string>& skip_prefixes,
     bool merge_existing_messages) {
-  // First pass: extract all the message descriptors from the file and add them
-  // to the pool.
   protos::pbzero::FileDescriptorSet::Decoder proto(file_descriptor_set_proto,
                                                    size);
   std::vector<ExtensionInfo> extensions;
   for (auto it = proto.file(); it; ++it) {
     protos::pbzero::FileDescriptorProto::Decoder file(*it);
-    std::string file_name = file.name().ToStdString();
+    const std::string file_name = file.name().ToStdString();
+    if (base::StartsWithAny(file_name, skip_prefixes))
+      continue;
     if (processed_files_.find(file_name) != processed_files_.end()) {
       // This file has been loaded once already. Skip.
       continue;
diff --git a/src/trace_processor/util/descriptors.h b/src/trace_processor/util/descriptors.h
index a5c7cde..e4da7a0 100644
--- a/src/trace_processor/util/descriptors.h
+++ b/src/trace_processor/util/descriptors.h
@@ -158,9 +158,12 @@
 
 class DescriptorPool {
  public:
+  // Adds Descriptors from file_descriptor_set_proto. Ignores any FileDescriptor
+  // with name matching a prefix in |skip_prefixes|.
   base::Status AddFromFileDescriptorSet(
       const uint8_t* file_descriptor_set_proto,
       size_t size,
+      const std::vector<std::string>& skip_prefixes = {},
       bool merge_existing_messages = false);
 
   base::Optional<uint32_t> FindDescriptorIdx(