metrics_daemon: add zram stats collection

Memory compression stats are being collected by Chrome, but it
is more natural to do it here since they are system-wide rather than
Chrome-specific.

In addition, this provides better granularity for the compression ratio
(percents, from 100% to 600%) since we're especially interested in the
distribution of values between 1 and 2, and currently these all fall
in the same bucket.

Finally, we collect more interesting stats on zero pages.

BUG=chromium:315113
TEST=unit testing, checked about:histograms

Change-Id: I09c974989661d42f45d44afd428e8114e4ee1dbd
Reviewed-on: https://chromium-review.googlesource.com/202587
Reviewed-by: Luigi Semenzato <semenzato@chromium.org>
Commit-Queue: Luigi Semenzato <semenzato@chromium.org>
Tested-by: Luigi Semenzato <semenzato@chromium.org>
diff --git a/metrics/metrics_daemon.cc b/metrics/metrics_daemon.cc
index e3f6bf7..9b66877 100644
--- a/metrics/metrics_daemon.cc
+++ b/metrics/metrics_daemon.cc
@@ -111,6 +111,12 @@
 const char MetricsDaemon::kMetricScaledCpuFrequencyName[] =
     "Platform.CpuFrequencyThermalScaling";
 
+// Zram sysfs entries.
+
+const char MetricsDaemon::kComprDataSizeName[] = "compr_data_size";
+const char MetricsDaemon::kOrigDataSizeName[] = "orig_data_size";
+const char MetricsDaemon::kZeroPagesName[] = "zero_pages";
+
 // Memory use stats collection intervals.  We collect some memory use interval
 // at these intervals after boot, and we stop collecting after the last one,
 // with the assumption that in most cases the memory use won't change much
@@ -737,7 +743,62 @@
     LOG(WARNING) << "cannot read " << meminfo_path.value().c_str();
     return false;
   }
-  return ProcessMeminfo(meminfo_raw);
+  // Make both calls even if the first one fails.
+  bool success = ProcessMeminfo(meminfo_raw);
+  return ReportZram(base::FilePath(FILE_PATH_LITERAL("/sys/block/zram0"))) &&
+      success;
+}
+
+// static
+bool MetricsDaemon::ReadFileToUint64(const base::FilePath& path,
+                                     uint64* value) {
+  std::string content;
+  if (!base::ReadFileToString(path, &content)) {
+    PLOG(WARNING) << "cannot read " << path.MaybeAsASCII();
+    return false;
+  }
+  if (!base::StringToUint64(content, value)) {
+    LOG(WARNING) << "invalid integer: " << content;
+    return false;
+  }
+  return true;
+}
+
+bool MetricsDaemon::ReportZram(const base::FilePath& zram_dir) {
+  // Data sizes are in bytes.  |zero_pages| is in number of pages.
+  uint64 compr_data_size, orig_data_size, zero_pages;
+  const size_t page_size = 4096;
+
+  if (!ReadFileToUint64(zram_dir.Append(kComprDataSizeName),
+                        &compr_data_size) ||
+      !ReadFileToUint64(zram_dir.Append(kOrigDataSizeName), &orig_data_size) ||
+      !ReadFileToUint64(zram_dir.Append(kZeroPagesName), &zero_pages)) {
+    return false;
+  }
+
+  // |orig_data_size| does not include zero-filled pages.
+  orig_data_size += zero_pages * page_size;
+
+  const int compr_data_size_mb = compr_data_size >> 20;
+  const int savings_mb = (orig_data_size - compr_data_size) >> 20;
+  const int zero_ratio_percent = zero_pages * page_size * 100 / orig_data_size;
+
+  // Report compressed size in megabytes.  100 MB or less has little impact.
+  SendSample("Platform.ZramCompressedSize", compr_data_size_mb, 100, 4000, 50);
+  SendSample("Platform.ZramSavings", savings_mb, 100, 4000, 50);
+  // The compression ratio is multiplied by 100 for better resolution.  The
+  // ratios of interest are between 1 and 6 (100% and 600% as reported).  We
+  // don't want samples when very little memory is being compressed.
+  if (compr_data_size_mb >= 1) {
+    SendSample("Platform.ZramCompressionRatioPercent",
+               orig_data_size * 100 / compr_data_size, 100, 600, 50);
+  }
+  // The values of interest for zero_pages are between 1MB and 1GB.  The units
+  // are number of pages.
+  SendSample("Platform.ZramZeroPages", zero_pages, 256, 256 * 1024, 50);
+  SendSample("Platform.ZramZeroRatioPercent", zero_ratio_percent, 1, 50, 50);
+
+  return true;
 }
 
 bool MetricsDaemon::ProcessMeminfo(const string& meminfo_raw) {
diff --git a/metrics/metrics_daemon.h b/metrics/metrics_daemon.h
index 81e5386..df85a2f 100644
--- a/metrics/metrics_daemon.h
+++ b/metrics/metrics_daemon.h
@@ -36,6 +36,12 @@
   // forking.
   void Run(bool run_as_daemon);
 
+ protected:
+  // Used also by the unit tests.
+  static const char kComprDataSizeName[];
+  static const char kOrigDataSizeName[];
+  static const char kZeroPagesName[];
+
  private:
   friend class MetricsDaemonTest;
   FRIEND_TEST(MetricsDaemonTest, CheckSystemCrash);
@@ -59,6 +65,7 @@
   FRIEND_TEST(MetricsDaemonTest, ReportUserCrashInterval);
   FRIEND_TEST(MetricsDaemonTest, SendSample);
   FRIEND_TEST(MetricsDaemonTest, SendCpuThrottleMetrics);
+  FRIEND_TEST(MetricsDaemonTest, SendZramMetrics);
 
   // State for disk stats collector callback.
   enum StatsState {
@@ -270,6 +277,14 @@
   // Invoked periodically by |update_stats_timeout_id_| to call UpdateStats().
   static gboolean HandleUpdateStatsTimeout(gpointer data);
 
+  // Reports zram statistics.
+  bool ReportZram(const base::FilePath& zram_dir);
+
+  // Reads a string from a file and converts it to uint64.
+  static bool ReadFileToUint64(const base::FilePath& path, uint64* value);
+
+  // VARIABLES
+
   // Test mode.
   bool testing_;
 
diff --git a/metrics/metrics_daemon_test.cc b/metrics/metrics_daemon_test.cc
index 33d0d11..3d5f5d7 100644
--- a/metrics/metrics_daemon_test.cc
+++ b/metrics/metrics_daemon_test.cc
@@ -9,6 +9,7 @@
 
 #include <base/at_exit.h>
 #include <base/file_util.h>
+#include <base/strings/string_number_conversions.h>
 #include <base/strings/stringprintf.h>
 #include <chromeos/dbus/service_constants.h>
 #include <gtest/gtest.h>
@@ -30,8 +31,7 @@
 using ::testing::StrictMock;
 using chromeos_metrics::PersistentIntegerMock;
 
-static const char kTestDir[] = "test";
-static const char kFakeDiskStatsPath[] = "fake-disk-stats";
+static const char kFakeDiskStatsName[] = "fake-disk-stats";
 static const char kFakeDiskStatsFormat[] =
     "    1793     1788    %d   105580    "
     "    196      175     %d    30290    "
@@ -40,7 +40,7 @@
 static const int kFakeReadSectors[] = {80000, 100000};
 static const int kFakeWriteSectors[] = {3000, 4000};
 
-static const char kFakeVmStatsPath[] = "fake-vm-stats";
+static const char kFakeVmStatsName[] = "fake-vm-stats";
 static const char kFakeScalingMaxFreqPath[] = "fake-scaling-max-freq";
 static const char kFakeCpuinfoMaxFreqPath[] = "fake-cpuinfo-max-freq";
 
@@ -54,16 +54,13 @@
                                            kFakeReadSectors[1],
                                            kFakeWriteSectors[1]);
     CreateFakeDiskStatsFile(kFakeDiskStats[0].c_str());
-    CreateFakeCpuFrequencyFile(kFakeCpuinfoMaxFreqPath, 10000000);
-    CreateFakeCpuFrequencyFile(kFakeScalingMaxFreqPath, 10000000);
+    CreateUint64ValueFile(base::FilePath(kFakeCpuinfoMaxFreqPath), 10000000);
+    CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), 10000000);
 
     chromeos_metrics::PersistentInteger::SetTestingMode(true);
-    daemon_.Init(true, &metrics_lib_, kFakeDiskStatsPath, kFakeVmStatsPath,
+    daemon_.Init(true, &metrics_lib_, kFakeDiskStatsName, kFakeVmStatsName,
         kFakeScalingMaxFreqPath, kFakeCpuinfoMaxFreqPath);
 
-    base::DeleteFile(FilePath(kTestDir), true);
-    base::CreateDirectory(FilePath(kTestDir));
-
     // Replace original persistent values with mock ones.
     daily_active_use_mock_ =
         new StrictMock<PersistentIntegerMock>("1.mock");
@@ -84,7 +81,7 @@
   }
 
   virtual void TearDown() {
-    EXPECT_EQ(0, unlink(kFakeDiskStatsPath));
+    EXPECT_EQ(0, unlink(kFakeDiskStatsName));
     EXPECT_EQ(0, unlink(kFakeScalingMaxFreqPath));
     EXPECT_EQ(0, unlink(kFakeCpuinfoMaxFreqPath));
   }
@@ -157,23 +154,22 @@
 
   // Creates or overwrites an input file containing fake disk stats.
   void CreateFakeDiskStatsFile(const char* fake_stats) {
-    if (unlink(kFakeDiskStatsPath) < 0) {
+    if (unlink(kFakeDiskStatsName) < 0) {
       EXPECT_EQ(errno, ENOENT);
     }
-    FILE* f = fopen(kFakeDiskStatsPath, "w");
+    FILE* f = fopen(kFakeDiskStatsName, "w");
     EXPECT_EQ(1, fwrite(fake_stats, strlen(fake_stats), 1, f));
     EXPECT_EQ(0, fclose(f));
   }
 
-  // Creates or overwrites an input file containing a fake CPU frequency.
-  void CreateFakeCpuFrequencyFile(const char* filename, int frequency) {
-    FilePath path(filename);
+  // Creates or overwrites the file in |path| so that it contains the printable
+  // representation of |value|.
+  void CreateUint64ValueFile(const base::FilePath& path, uint64 value) {
     base::DeleteFile(path, false);
-    std::string frequency_string = StringPrintf("%d\n", frequency);
-    int frequency_string_length = frequency_string.length();
-    EXPECT_EQ(frequency_string.length(),
-              base::WriteFile(path, frequency_string.c_str(),
-                              frequency_string_length));
+    std::string value_string = base::Uint64ToString(value);
+    ASSERT_EQ(value_string.length(),
+              base::WriteFile(path, value_string.c_str(),
+                              value_string.length()));
   }
 
   // The MetricsDaemon under test.
@@ -351,8 +347,9 @@
   const int fake_max_freq = 2000000;
   int scaled_freq = 0;
   int max_freq = 0;
-  CreateFakeCpuFrequencyFile(kFakeScalingMaxFreqPath, fake_scaled_freq);
-  CreateFakeCpuFrequencyFile(kFakeCpuinfoMaxFreqPath, fake_max_freq);
+  CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath),
+                        fake_scaled_freq);
+  CreateUint64ValueFile(base::FilePath(kFakeCpuinfoMaxFreqPath), fake_max_freq);
   EXPECT_TRUE(daemon_.testing_);
   EXPECT_TRUE(daemon_.ReadFreqToInt(kFakeScalingMaxFreqPath, &scaled_freq));
   EXPECT_TRUE(daemon_.ReadFreqToInt(kFakeCpuinfoMaxFreqPath, &max_freq));
@@ -361,17 +358,51 @@
 }
 
 TEST_F(MetricsDaemonTest, SendCpuThrottleMetrics) {
-  CreateFakeCpuFrequencyFile(kFakeCpuinfoMaxFreqPath, 2001000);
+  CreateUint64ValueFile(base::FilePath(kFakeCpuinfoMaxFreqPath), 2001000);
   // Test the 101% and 100% cases.
-  CreateFakeCpuFrequencyFile(kFakeScalingMaxFreqPath, 2001000);
+  CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), 2001000);
   EXPECT_TRUE(daemon_.testing_);
   EXPECT_CALL(metrics_lib_, SendEnumToUMA(_, 101, 101));
   daemon_.SendCpuThrottleMetrics();
-  CreateFakeCpuFrequencyFile(kFakeScalingMaxFreqPath, 2000000);
+  CreateUint64ValueFile(base::FilePath(kFakeScalingMaxFreqPath), 2000000);
   EXPECT_CALL(metrics_lib_, SendEnumToUMA(_, 100, 101));
   daemon_.SendCpuThrottleMetrics();
 }
 
+TEST_F(MetricsDaemonTest, SendZramMetrics) {
+  EXPECT_TRUE(daemon_.testing_);
+
+  // |compr_data_size| is the size in bytes of compressed data.
+  const uint64 compr_data_size = 50 * 1000 * 1000;
+  // The constant '3' is a realistic but random choice.
+  // |orig_data_size| does not include zero pages.
+  const uint64 orig_data_size = compr_data_size * 3;
+  const uint64 page_size = 4096;
+  const uint64 zero_pages = 10 * 1000 * 1000 / page_size;
+
+  CreateUint64ValueFile(base::FilePath(MetricsDaemon::kComprDataSizeName),
+                        compr_data_size);
+  CreateUint64ValueFile(base::FilePath(MetricsDaemon::kOrigDataSizeName),
+                        orig_data_size);
+  CreateUint64ValueFile(base::FilePath(MetricsDaemon::kZeroPagesName),
+                        zero_pages);
+
+  const uint64 real_orig_size = orig_data_size + zero_pages * page_size;
+  const uint64 zero_ratio_percent =
+      zero_pages * page_size * 100 / real_orig_size;
+  // Ratio samples are in percents.
+  const uint64 actual_ratio_sample = real_orig_size * 100 / compr_data_size;
+
+  EXPECT_CALL(metrics_lib_, SendToUMA(_, compr_data_size >> 20, _, _, _));
+  EXPECT_CALL(metrics_lib_,
+              SendToUMA(_, (real_orig_size - compr_data_size) >> 20, _, _, _));
+  EXPECT_CALL(metrics_lib_, SendToUMA(_, actual_ratio_sample, _, _, _));
+  EXPECT_CALL(metrics_lib_, SendToUMA(_, zero_pages, _, _, _));
+  EXPECT_CALL(metrics_lib_, SendToUMA(_, zero_ratio_percent, _, _, _));
+
+  EXPECT_TRUE(daemon_.ReportZram(base::FilePath(".")));
+}
+
 int main(int argc, char** argv) {
   testing::InitGoogleTest(&argc, argv);
   // Some libchrome calls need this.