Add MetricsSetProtoFileExporter

Bug: b/246095034
Change-Id: I002d0d5b132e61887b4bc87542fbf70dd81e488b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/275881
Commit-Queue: Artem Titov <titovartem@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38125}
diff --git a/api/test/metrics/BUILD.gn b/api/test/metrics/BUILD.gn
index 544808f..8bfcf39 100644
--- a/api/test/metrics/BUILD.gn
+++ b/api/test/metrics/BUILD.gn
@@ -7,6 +7,9 @@
 # be found in the AUTHORS file in the root of the source tree.
 
 import("../../../webrtc.gni")
+if (rtc_enable_protobuf) {
+  import("//third_party/protobuf/proto_library.gni")
+}
 
 group("metrics") {
   deps = [
@@ -25,6 +28,10 @@
       ":metrics_logger_and_exporter_test",
       ":stdout_metrics_exporter_test",
     ]
+
+    if (rtc_enable_protobuf) {
+      deps += [ ":metrics_set_proto_file_exporter_test" ]
+    }
   }
 }
 
@@ -84,6 +91,35 @@
   ]
 }
 
+if (rtc_enable_protobuf) {
+  proto_library("metric_proto") {
+    visibility = [ "*" ]
+    sources = [ "proto/metric.proto" ]
+    proto_out_dir = "api/test/metrics/proto"
+    cc_generator_options = "lite"
+  }
+}
+
+rtc_library("metrics_set_proto_file_exporter") {
+  visibility = [ "*" ]
+  testonly = true
+  sources = [
+    "metrics_set_proto_file_exporter.cc",
+    "metrics_set_proto_file_exporter.h",
+  ]
+  deps = [
+    ":metric",
+    ":metrics_exporter",
+    "../..:array_view",
+    "../../../rtc_base:logging",
+    "../../../test:fileutils",
+  ]
+
+  if (rtc_enable_protobuf) {
+    deps += [ ":metric_proto" ]
+  }
+}
+
 if (rtc_include_tests) {
   rtc_library("stdout_metrics_exporter_test") {
     testonly = true
@@ -109,4 +145,20 @@
     ]
     absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
   }
+
+  if (rtc_enable_protobuf) {
+    rtc_library("metrics_set_proto_file_exporter_test") {
+      testonly = true
+      sources = [ "metrics_set_proto_file_exporter_test.cc" ]
+      deps = [
+        ":metric",
+        ":metric_proto",
+        ":metrics_set_proto_file_exporter",
+        "../../../rtc_base:protobuf_utils",
+        "../../../test:fileutils",
+        "../../../test:test_support",
+        "../../units:timestamp",
+      ]
+    }
+  }
 }
diff --git a/api/test/metrics/metrics_set_proto_file_exporter.cc b/api/test/metrics/metrics_set_proto_file_exporter.cc
new file mode 100644
index 0000000..6a41098
--- /dev/null
+++ b/api/test/metrics/metrics_set_proto_file_exporter.cc
@@ -0,0 +1,157 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "api/test/metrics/metrics_set_proto_file_exporter.h"
+
+#include <stdio.h>
+
+#include <string>
+
+#include "api/test/metrics/metric.h"
+#include "rtc_base/logging.h"
+#include "test/testsupport/file_utils.h"
+
+#if WEBRTC_ENABLE_PROTOBUF
+#include "api/test/metrics/proto/metric.pb.h"
+#endif
+
+namespace webrtc {
+namespace test {
+namespace {
+
+#if WEBRTC_ENABLE_PROTOBUF
+webrtc::test_metrics::Unit ToProtoUnit(Unit unit) {
+  switch (unit) {
+    case Unit::kTimeMs:
+      return webrtc::test_metrics::Unit::MILLISECONDS;
+    case Unit::kPercent:
+      return webrtc::test_metrics::Unit::PERCENT;
+    case Unit::kSizeInBytes:
+      return webrtc::test_metrics::Unit::BYTES;
+    case Unit::kKilobitsPerSecond:
+      return webrtc::test_metrics::Unit::KILOBITS_PER_SECOND;
+    case Unit::kHertz:
+      return webrtc::test_metrics::Unit::HERTZ;
+    case Unit::kUnitless:
+      return webrtc::test_metrics::Unit::UNITLESS;
+    case Unit::kCount:
+      return webrtc::test_metrics::Unit::COUNT;
+  }
+}
+
+webrtc::test_metrics::ImprovementDirection ToProtoImprovementDirection(
+    ImprovementDirection direction) {
+  switch (direction) {
+    case ImprovementDirection::kBiggerIsBetter:
+      return webrtc::test_metrics::ImprovementDirection::BIGGER_IS_BETTER;
+    case ImprovementDirection::kNeitherIsBetter:
+      return webrtc::test_metrics::ImprovementDirection::NEITHER_IS_BETTER;
+    case ImprovementDirection::kSmallerIsBetter:
+      return webrtc::test_metrics::ImprovementDirection::SMALLER_IS_BETTER;
+  }
+}
+
+void SetTimeSeries(
+    const Metric::TimeSeries& time_series,
+    webrtc::test_metrics::Metric::TimeSeries* proto_time_series) {
+  for (const Metric::TimeSeries::Sample& sample : time_series.samples) {
+    webrtc::test_metrics::Metric::TimeSeries::Sample* proto_sample =
+        proto_time_series->add_samples();
+    proto_sample->set_value(sample.value);
+    proto_sample->set_timestamp_us(sample.timestamp.us());
+    for (const auto& [key, value] : sample.sample_metadata) {
+      proto_sample->mutable_sample_metadata()->insert({key, value});
+    }
+  }
+}
+
+void SetStats(const Metric::Stats& stats,
+              webrtc::test_metrics::Metric::Stats* proto_stats) {
+  if (stats.mean.has_value()) {
+    proto_stats->set_mean(*stats.mean);
+  }
+  if (stats.stddev.has_value()) {
+    proto_stats->set_stddev(*stats.stddev);
+  }
+  if (stats.min.has_value()) {
+    proto_stats->set_min(*stats.min);
+  }
+  if (stats.max.has_value()) {
+    proto_stats->set_max(*stats.max);
+  }
+}
+
+bool WriteMetricsToFile(const std::string& path,
+                        const webrtc::test_metrics::MetricsSet& metrics_set) {
+  std::string data;
+  bool ok = metrics_set.SerializeToString(&data);
+  if (!ok) {
+    RTC_LOG(LS_ERROR) << "Failed to serialize histogram set to string";
+    return false;
+  }
+
+  CreateDir(DirName(path));
+  FILE* output = fopen(path.c_str(), "wb");
+  if (output == NULL) {
+    RTC_LOG(LS_ERROR) << "Failed to write to " << path;
+    return false;
+  }
+  size_t written = fwrite(data.c_str(), sizeof(char), data.size(), output);
+  fclose(output);
+
+  if (written != data.size()) {
+    size_t expected = data.size();
+    RTC_LOG(LS_ERROR) << "Wrote " << written << ", tried to write " << expected;
+    return false;
+  }
+  return true;
+}
+#endif  // WEBRTC_ENABLE_PROTOBUF
+
+}  // namespace
+
+MetricsSetProtoFileExporter::Options::Options(
+    absl::string_view export_file_path)
+    : export_file_path(export_file_path) {}
+MetricsSetProtoFileExporter::Options::Options(
+    absl::string_view export_file_path,
+    bool export_whole_time_series)
+    : export_file_path(export_file_path),
+      export_whole_time_series(export_whole_time_series) {}
+
+bool MetricsSetProtoFileExporter::Export(rtc::ArrayView<const Metric> metrics) {
+#if WEBRTC_ENABLE_PROTOBUF
+  webrtc::test_metrics::MetricsSet metrics_set;
+  for (const Metric& metric : metrics) {
+    webrtc::test_metrics::Metric* metric_proto = metrics_set.add_metrics();
+    metric_proto->set_name(metric.name);
+    metric_proto->set_unit(ToProtoUnit(metric.unit));
+    metric_proto->set_improvement_direction(
+        ToProtoImprovementDirection(metric.improvement_direction));
+    metric_proto->set_test_case(metric.test_case);
+    for (const auto& [key, value] : metric.metric_metadata) {
+      metric_proto->mutable_metric_metadata()->insert({key, value});
+    }
+
+    if (options_.export_whole_time_series) {
+      SetTimeSeries(metric.time_series, metric_proto->mutable_time_series());
+    }
+    SetStats(metric.stats, metric_proto->mutable_stats());
+  }
+
+  return WriteMetricsToFile(options_.export_file_path, metrics_set);
+#else
+  RTC_LOG(LS_ERROR)
+      << "Compile with protobuf support to properly use this class";
+  return false;
+#endif
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/api/test/metrics/metrics_set_proto_file_exporter.h b/api/test/metrics/metrics_set_proto_file_exporter.h
new file mode 100644
index 0000000..f996e9e7
--- /dev/null
+++ b/api/test/metrics/metrics_set_proto_file_exporter.h
@@ -0,0 +1,54 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef API_TEST_METRICS_METRICS_SET_PROTO_FILE_EXPORTER_H_
+#define API_TEST_METRICS_METRICS_SET_PROTO_FILE_EXPORTER_H_
+
+#include <string>
+
+#include "api/array_view.h"
+#include "api/test/metrics/metric.h"
+#include "api/test/metrics/metrics_exporter.h"
+
+namespace webrtc {
+namespace test {
+
+// Exports all collected metrics to the proto file using
+// `webrtc::test_metrics::MetricsSet` format.
+class MetricsSetProtoFileExporter : public MetricsExporter {
+ public:
+  struct Options {
+    explicit Options(absl::string_view export_file_path);
+    Options(absl::string_view export_file_path, bool export_whole_time_series);
+
+    // File to export proto.
+    std::string export_file_path;
+    // If true will write all time series values to the output proto file,
+    // otherwise will write stats only.
+    bool export_whole_time_series = true;
+  };
+
+  explicit MetricsSetProtoFileExporter(const Options& options)
+      : options_(options) {}
+
+  MetricsSetProtoFileExporter(const MetricsSetProtoFileExporter&) = delete;
+  MetricsSetProtoFileExporter& operator=(const MetricsSetProtoFileExporter&) =
+      delete;
+
+  bool Export(rtc::ArrayView<const Metric> metrics) override;
+
+ private:
+  const Options options_;
+};
+
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // API_TEST_METRICS_METRICS_SET_PROTO_FILE_EXPORTER_H_
diff --git a/api/test/metrics/metrics_set_proto_file_exporter_test.cc b/api/test/metrics/metrics_set_proto_file_exporter_test.cc
new file mode 100644
index 0000000..97987d2
--- /dev/null
+++ b/api/test/metrics/metrics_set_proto_file_exporter_test.cc
@@ -0,0 +1,151 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "api/test/metrics/metrics_set_proto_file_exporter.h"
+
+#include <fstream>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "api/test/metrics/metric.h"
+#include "api/test/metrics/proto/metric.pb.h"
+#include "api/units/timestamp.h"
+#include "rtc_base/protobuf_utils.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+#include "test/testsupport/file_utils.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+using ::testing::Eq;
+using ::testing::Test;
+
+namespace proto = ::webrtc::test_metrics;
+
+std::string ReadFileAsString(const std::string& filename) {
+  std::ifstream infile(filename, std::ios_base::binary);
+  auto buffer = std::vector<char>(std::istreambuf_iterator<char>(infile),
+                                  std::istreambuf_iterator<char>());
+  return std::string(buffer.begin(), buffer.end());
+}
+
+std::map<std::string, std::string> DefaultMetadata() {
+  return std::map<std::string, std::string>{{"key", "value"}};
+}
+
+Metric::TimeSeries::Sample Sample(double value) {
+  return Metric::TimeSeries::Sample{.timestamp = Timestamp::Seconds(1),
+                                    .value = value,
+                                    .sample_metadata = DefaultMetadata()};
+}
+
+void AssertSamplesEqual(const proto::Metric::TimeSeries::Sample& actual_sample,
+                        const Metric::TimeSeries::Sample& expected_sample) {
+  EXPECT_THAT(actual_sample.value(), Eq(expected_sample.value));
+  EXPECT_THAT(actual_sample.timestamp_us(), Eq(expected_sample.timestamp.us()));
+  EXPECT_THAT(actual_sample.sample_metadata().size(),
+              Eq(expected_sample.sample_metadata.size()));
+  for (const auto& [key, value] : expected_sample.sample_metadata) {
+    EXPECT_THAT(actual_sample.sample_metadata().at(key), Eq(value));
+  }
+}
+
+class MetricsSetProtoFileExporterTest : public Test {
+ protected:
+  ~MetricsSetProtoFileExporterTest() override = default;
+
+  void SetUp() override {
+    temp_filename_ = webrtc::test::TempFilename(
+        webrtc::test::OutputPath(), "metrics_set_proto_file_exporter_test");
+  }
+
+  void TearDown() override {
+    ASSERT_TRUE(webrtc::test::RemoveFile(temp_filename_));
+  }
+
+  std::string temp_filename_;
+};
+
+TEST_F(MetricsSetProtoFileExporterTest, MetricsAreExportedCorrectly) {
+  MetricsSetProtoFileExporter::Options options(temp_filename_);
+  MetricsSetProtoFileExporter exporter(options);
+
+  Metric metric1{
+      .name = "test_metric1",
+      .unit = Unit::kTimeMs,
+      .improvement_direction = ImprovementDirection::kBiggerIsBetter,
+      .test_case = "test_case_name1",
+      .metric_metadata = DefaultMetadata(),
+      .time_series =
+          Metric::TimeSeries{.samples = std::vector{Sample(10), Sample(20)}},
+      .stats =
+          Metric::Stats{.mean = 15.0, .stddev = 5.0, .min = 10.0, .max = 20.0}};
+  Metric metric2{
+      .name = "test_metric2",
+      .unit = Unit::kKilobitsPerSecond,
+      .improvement_direction = ImprovementDirection::kSmallerIsBetter,
+      .test_case = "test_case_name2",
+      .metric_metadata = DefaultMetadata(),
+      .time_series =
+          Metric::TimeSeries{.samples = std::vector{Sample(20), Sample(40)}},
+      .stats = Metric::Stats{
+          .mean = 30.0, .stddev = 10.0, .min = 20.0, .max = 40.0}};
+
+  ASSERT_TRUE(exporter.Export(std::vector<Metric>{metric1, metric2}));
+  webrtc::test_metrics::MetricsSet actual_metrics_set;
+  actual_metrics_set.ParseFromString(ReadFileAsString(temp_filename_));
+  EXPECT_THAT(actual_metrics_set.metrics().size(), Eq(2));
+
+  EXPECT_THAT(actual_metrics_set.metrics(0).name(), Eq("test_metric1"));
+  EXPECT_THAT(actual_metrics_set.metrics(0).test_case(), Eq("test_case_name1"));
+  EXPECT_THAT(actual_metrics_set.metrics(0).unit(),
+              Eq(proto::Unit::MILLISECONDS));
+  EXPECT_THAT(actual_metrics_set.metrics(0).improvement_direction(),
+              Eq(proto::ImprovementDirection::BIGGER_IS_BETTER));
+  EXPECT_THAT(actual_metrics_set.metrics(0).metric_metadata().size(), Eq(1lu));
+  EXPECT_THAT(actual_metrics_set.metrics(0).metric_metadata().at("key"),
+              Eq("value"));
+  EXPECT_THAT(actual_metrics_set.metrics(0).time_series().samples().size(),
+              Eq(2));
+  AssertSamplesEqual(actual_metrics_set.metrics(0).time_series().samples(0),
+                     Sample(10.0));
+  AssertSamplesEqual(actual_metrics_set.metrics(0).time_series().samples(1),
+                     Sample(20.0));
+  EXPECT_THAT(actual_metrics_set.metrics(0).stats().mean(), Eq(15.0));
+  EXPECT_THAT(actual_metrics_set.metrics(0).stats().stddev(), Eq(5.0));
+  EXPECT_THAT(actual_metrics_set.metrics(0).stats().min(), Eq(10.0));
+  EXPECT_THAT(actual_metrics_set.metrics(0).stats().max(), Eq(20.0));
+
+  EXPECT_THAT(actual_metrics_set.metrics(1).name(), Eq("test_metric2"));
+  EXPECT_THAT(actual_metrics_set.metrics(1).test_case(), Eq("test_case_name2"));
+  EXPECT_THAT(actual_metrics_set.metrics(1).unit(),
+              Eq(proto::Unit::KILOBITS_PER_SECOND));
+  EXPECT_THAT(actual_metrics_set.metrics(1).improvement_direction(),
+              Eq(proto::ImprovementDirection::SMALLER_IS_BETTER));
+  EXPECT_THAT(actual_metrics_set.metrics(1).metric_metadata().size(), Eq(1lu));
+  EXPECT_THAT(actual_metrics_set.metrics(1).metric_metadata().at("key"),
+              Eq("value"));
+  EXPECT_THAT(actual_metrics_set.metrics(1).time_series().samples().size(),
+              Eq(2));
+  AssertSamplesEqual(actual_metrics_set.metrics(1).time_series().samples(0),
+                     Sample(20.0));
+  AssertSamplesEqual(actual_metrics_set.metrics(1).time_series().samples(1),
+                     Sample(40.0));
+  EXPECT_THAT(actual_metrics_set.metrics(1).stats().mean(), Eq(30.0));
+  EXPECT_THAT(actual_metrics_set.metrics(1).stats().stddev(), Eq(10.0));
+  EXPECT_THAT(actual_metrics_set.metrics(1).stats().min(), Eq(20.0));
+  EXPECT_THAT(actual_metrics_set.metrics(1).stats().max(), Eq(40.0));
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace webrtc
diff --git a/api/test/metrics/proto/metric.proto b/api/test/metrics/proto/metric.proto
new file mode 100644
index 0000000..cdd3125
--- /dev/null
+++ b/api/test/metrics/proto/metric.proto
@@ -0,0 +1,87 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+syntax = "proto3";
+
+package webrtc.test_metrics;
+
+// Root message of the proto file. Contains collection of all the metrics.
+message MetricsSet {
+  repeated Metric metrics = 1;
+}
+
+enum Unit {
+  // Default value that has to be defined.
+  UNDEFINED_UNIT = 0;
+  // General unitless value. Can be used either for dimensionless quantities
+  // (ex ratio) or for units not presented in this enum and too specific to add
+  // to this enum.
+  UNITLESS = 1;
+  MILLISECONDS = 2;
+  PERCENT = 3;
+  BYTES = 4;
+  KILOBITS_PER_SECOND = 5;
+  HERTZ = 6;
+  COUNT = 7;
+}
+
+enum ImprovementDirection {
+  // Default value that has to be defined.
+  UNDEFINED_IMPROVEMENT_DIRECTION = 0;
+  BIGGER_IS_BETTER = 1;
+  NEITHER_IS_BETTER = 2;
+  SMALLER_IS_BETTER = 3;
+}
+
+// Single performance metric with all related metadata.
+message Metric {
+  // Metric name, for example PSNR, SSIM, decode_time, etc.
+  string name = 1;
+  Unit unit = 2;
+  ImprovementDirection improvement_direction = 3;
+  // If the metric is generated by a test, this field can be used to specify
+  // this information.
+  string test_case = 4;
+  // Metadata associated with the whole metric.
+  map<string, string> metric_metadata = 5;
+
+  message TimeSeries {
+    message Sample {
+      // Timestamp in microseconds associated with a sample. For example,
+      // the timestamp when the sample was collected.
+      int64 timestamp_us = 1;
+      double value = 2;
+      // Metadata associated with this particular sample.
+      map<string, string> sample_metadata = 3;
+    }
+    // All samples collected for this metric. It can be empty if the Metric
+    // object only contains `stats`.
+    repeated Sample samples = 1;
+  }
+  // Contains all samples of the metric collected during test execution.
+  // It can be empty if the user only stores precomputed statistics into
+  // `stats`.
+  TimeSeries time_series = 6;
+
+  // Contains metric's precomputed statistics based on the `time_series` or if
+  // `time_series` is omitted (has 0 samples) contains precomputed statistics
+  // provided by the metric's calculator.
+  message Stats {
+    // Sample mean of the metric
+    // (https://en.wikipedia.org/wiki/Sample_mean_and_covariance).
+    optional double mean = 1;
+    // Standard deviation (https://en.wikipedia.org/wiki/Standard_deviation).
+    // Is undefined if `time_series` contains only a single sample.
+    optional double stddev = 2;
+    optional double min = 3;
+    optional double max = 4;
+  }
+  Stats stats = 7;
+}