Collect some disk statistics.

Change-Id: Id30f4b7e5d121f2632592ebacf47a18ea1d89fec

BUG=chromium-os:12171
TEST=ran on target and observed that stats are generated

Review URL: http://codereview.chromium.org/6486021
diff --git a/metrics/metrics_daemon.cc b/metrics/metrics_daemon.cc
index a5059d9..1e15e1d 100644
--- a/metrics/metrics_daemon.cc
+++ b/metrics/metrics_daemon.cc
@@ -4,6 +4,7 @@
 
 #include "metrics_daemon.h"
 
+#include <fcntl.h>
 #include <string.h>
 
 #include <base/file_util.h>
@@ -84,6 +85,28 @@
 const char MetricsDaemon::kMetricCrashFrequencyMax = 100;
 const char MetricsDaemon::kMetricCrashFrequencyBuckets = 50;
 
+// disk stats metrics
+
+// The {Read,Write}Sectors numbers are in sectors/second.
+// A sector is usually 512 bytes.
+
+const char MetricsDaemon::kMetricReadSectorsLongName[] =
+    "Platform.ReadSectorsLong";
+const char MetricsDaemon::kMetricWriteSectorsLongName[] =
+    "Platform.WriteSectorsLong";
+const char MetricsDaemon::kMetricReadSectorsShortName[] =
+    "Platform.ReadSectorsShort";
+const char MetricsDaemon::kMetricWriteSectorsShortName[] =
+    "Platform.WriteSectorsShort";
+
+const int MetricsDaemon::kMetricDiskStatsShortInterval = 1;  // seconds
+const int MetricsDaemon::kMetricDiskStatsLongInterval = 30;  // seconds
+
+// Assume a max rate of 250Mb/s for reads (worse for writes) and 512 byte
+// sectors.
+const int MetricsDaemon::kMetricSectorsIOMax = 500000;  // sectors/second
+const int MetricsDaemon::kMetricSectorsBuckets = 50;    // buckets
+
 // persistent metrics path
 const char MetricsDaemon::kMetricsPath[] = "/var/log/metrics";
 
@@ -123,7 +146,8 @@
       session_state_(kUnknownSessionState),
       user_active_(false),
       usemon_interval_(0),
-      usemon_source_(NULL) {}
+      usemon_source_(NULL),
+      diskstats_path_(NULL) {}
 
 MetricsDaemon::~MetricsDaemon() {
   DeleteFrequencyCounters();
@@ -190,7 +214,8 @@
   frequency_counters_[histogram_name] = new_counter.release();
 }
 
-void MetricsDaemon::Init(bool testing, MetricsLibraryInterface* metrics_lib) {
+void MetricsDaemon::Init(bool testing, MetricsLibraryInterface* metrics_lib,
+                         const char* diskstats_path) {
   testing_ = testing;
   DCHECK(metrics_lib != NULL);
   metrics_lib_ = metrics_lib;
@@ -218,6 +243,9 @@
   ConfigureCrashFrequencyReporter(kMetricUserCrashesDailyName);
   ConfigureCrashFrequencyReporter(kMetricUserCrashesWeeklyName);
 
+  diskstats_path_ = diskstats_path;
+  DiskStatsReporterInit();
+
   // Don't setup D-Bus and GLib in test mode.
   if (testing)
     return;
@@ -494,6 +522,99 @@
   usemon_interval_ = 0;
 }
 
+void MetricsDaemon::DiskStatsReporterInit() {
+  DiskStatsReadStats(&read_sectors_, &write_sectors_);
+  // The first time around just run the long stat, so we don't delay boot.
+  diskstats_state_ = kDiskStatsLong;
+  ScheduleDiskStatsCallback(kMetricDiskStatsLongInterval);
+}
+
+void MetricsDaemon::ScheduleDiskStatsCallback(int wait) {
+  if (testing_) {
+    return;
+  }
+  g_timeout_add_seconds(wait, DiskStatsCallbackStatic, this);
+}
+
+void MetricsDaemon::DiskStatsReadStats(long int* read_sectors,
+                                       long int* write_sectors) {
+  int nchars;
+  int nitems;
+  char line[200];
+  int file = HANDLE_EINTR(open(diskstats_path_, O_RDONLY));
+  if (file < 0) {
+    PLOG(WARNING) << "cannot open " << diskstats_path_;
+    return;
+  }
+  nchars = HANDLE_EINTR(read(file, line, sizeof(line)));
+  if (nchars < 0) {
+    PLOG(WARNING) << "cannot read from " << diskstats_path_;
+  } else {
+    LOG_IF(WARNING, nchars == sizeof(line)) << "line too long in "
+                                            << diskstats_path_;
+    line[nchars] = '\0';
+    nitems = sscanf(line, "%*d %*d %ld %*d %*d %*d %ld",
+                    read_sectors, write_sectors);
+    LOG_IF(WARNING, nitems != 2) << "found " << nitems << " items in "
+                                 << diskstats_path_ << ", expected 2";
+  }
+  HANDLE_EINTR(close(file));
+}
+
+// static
+gboolean MetricsDaemon::DiskStatsCallbackStatic(void* handle) {
+  (static_cast<MetricsDaemon*>(handle))->DiskStatsCallback();
+  return false;  // one-time callback
+}
+
+void MetricsDaemon::DiskStatsCallback() {
+  long int read_sectors_now, write_sectors_now;
+  DiskStatsReadStats(&read_sectors_now, &write_sectors_now);
+
+  switch (diskstats_state_) {
+    case kDiskStatsShort:
+      SendMetric(kMetricReadSectorsShortName,
+                 (int) (read_sectors_now - read_sectors_) /
+                 kMetricDiskStatsShortInterval,
+                 1,
+                 kMetricSectorsIOMax,
+                 kMetricSectorsBuckets);
+      SendMetric(kMetricWriteSectorsShortName,
+                 (int) (write_sectors_now - write_sectors_) /
+                 kMetricDiskStatsShortInterval,
+                 1,
+                 kMetricSectorsIOMax,
+                 kMetricSectorsBuckets);
+      // Schedule long callback.
+      diskstats_state_ = kDiskStatsLong;
+      ScheduleDiskStatsCallback(kMetricDiskStatsLongInterval -
+                                kMetricDiskStatsShortInterval);
+      break;
+    case kDiskStatsLong:
+      SendMetric(kMetricReadSectorsLongName,
+                 (int) (read_sectors_now - read_sectors_) /
+                 kMetricDiskStatsLongInterval,
+                 1,
+                 kMetricSectorsIOMax,
+                 kMetricSectorsBuckets);
+      SendMetric(kMetricWriteSectorsLongName,
+                 (int) (write_sectors_now - write_sectors_) /
+                 kMetricDiskStatsLongInterval,
+                 1,
+                 kMetricSectorsIOMax,
+                 kMetricSectorsBuckets);
+      // Reset sector counters
+      read_sectors_ = read_sectors_now;
+      write_sectors_ = write_sectors_now;
+      // Schedule short callback.
+      diskstats_state_ = kDiskStatsShort;
+      ScheduleDiskStatsCallback(kMetricDiskStatsShortInterval);
+      break;
+    default:
+      LOG(FATAL) << "Invalid disk stats state";
+  }
+}
+
 // static
 void MetricsDaemon::ReportDailyUse(void* handle, int tag, int count) {
   if (count <= 0)
diff --git a/metrics/metrics_daemon.h b/metrics/metrics_daemon.h
index 5f2e786..dd61322 100644
--- a/metrics/metrics_daemon.h
+++ b/metrics/metrics_daemon.h
@@ -29,7 +29,8 @@
   ~MetricsDaemon();
 
   // Initializes.
-  void Init(bool testing, MetricsLibraryInterface* metrics_lib);
+  void Init(bool testing, MetricsLibraryInterface* metrics_lib,
+            const char* diskstats_path);
 
   // Does all the work. If |run_as_daemon| is true, daemonizes by
   // forking.
@@ -52,6 +53,7 @@
   FRIEND_TEST(MetricsDaemonTest, ProcessUserCrash);
   FRIEND_TEST(MetricsDaemonTest, ReportCrashesDailyFrequency);
   FRIEND_TEST(MetricsDaemonTest, ReportDailyUse);
+  FRIEND_TEST(MetricsDaemonTest, ReportDiskStats);
   FRIEND_TEST(MetricsDaemonTest, ReportKernelCrashInterval);
   FRIEND_TEST(MetricsDaemonTest, ReportUncleanShutdownInterval);
   FRIEND_TEST(MetricsDaemonTest, ReportUserCrashInterval);
@@ -77,6 +79,12 @@
     kNumberSessionStates
   };
 
+  // State for disk stats collector callback.
+  enum DiskStatsState {
+    kDiskStatsShort,    // short wait before short interval collection
+    kDiskStatsLong,     // final wait before new collection
+  };
+
   // Data record for aggregating daily usage.
   class UseRecord {
    public:
@@ -111,6 +119,15 @@
   static const char kMetricUserCrashesDailyName[];
   static const char kMetricUserCrashesWeeklyName[];
   static const char kMetricUserCrashIntervalName[];
+  static const char kMetricReadSectorsLongName[];
+  static const char kMetricReadSectorsShortName[];
+  static const char kMetricWriteSectorsLongName[];
+  static const char kMetricWriteSectorsShortName[];
+  static const int kMetricDiskStatsShortInterval;
+  static const int kMetricDiskStatsLongInterval;
+  static const int kMetricSectorsIOMax;
+  static const int kMetricSectorsBuckets;
+  static const char kMetricsDiskStatsPath[];
 
   // D-Bus message match strings.
   static const char* kDBusMatches_[];
@@ -217,6 +234,22 @@
   void SendMetric(const std::string& name, int sample,
                   int min, int max, int nbuckets);
 
+  // Initializes disk stats reporting.
+  void DiskStatsReporterInit();
+
+  // Schedules a callback for the next disk stats collection.
+  void ScheduleDiskStatsCallback(int wait);
+
+  // Reads cumulative disk statistics from sysfs.
+  void DiskStatsReadStats(long int* read_sectors, long int* write_sectors);
+
+  // Reports disk statistics (static version for glib).  Arguments are a glib
+  // artifact.
+  static gboolean DiskStatsCallbackStatic(void* handle);
+
+  // Reports disk statistics.
+  void DiskStatsCallback();
+
   // Test mode.
   bool testing_;
 
@@ -265,6 +298,13 @@
 
   // Scheduled daily use monitor source (see ScheduleUseMonitor).
   GSource* usemon_source_;
+
+  // Contains the most recent disk stats.
+  long int read_sectors_;
+  long int write_sectors_;
+
+  DiskStatsState diskstats_state_;
+  const char* diskstats_path_;
 };
 
 #endif  // METRICS_DAEMON_H_
diff --git a/metrics/metrics_daemon_main.cc b/metrics/metrics_daemon_main.cc
index f3f0714..899256c 100644
--- a/metrics/metrics_daemon_main.cc
+++ b/metrics/metrics_daemon_main.cc
@@ -9,11 +9,14 @@
 
 DEFINE_bool(daemon, true, "run as daemon (use -nodaemon for debugging)");
 
+// Path to disk stats.  This may be system dependent.
+const char kMetricsMainDiskStatsPath[] = "/sys/class/block/sda/stat";
+
 int main(int argc, char** argv) {
   google::ParseCommandLineFlags(&argc, &argv, true);
   MetricsLibrary metrics_lib;
   metrics_lib.Init();
   MetricsDaemon daemon;
-  daemon.Init(false, &metrics_lib);
+  daemon.Init(false, &metrics_lib, kMetricsMainDiskStatsPath);
   daemon.Run(FLAGS_daemon);
 }
diff --git a/metrics/metrics_daemon_test.cc b/metrics/metrics_daemon_test.cc
index 530f357..208d8e5 100644
--- a/metrics/metrics_daemon_test.cc
+++ b/metrics/metrics_daemon_test.cc
@@ -8,6 +8,7 @@
 #include <vector>
 
 #include <base/file_util.h>
+#include <base/stringprintf.h>
 #include <gtest/gtest.h>
 
 #include "counter_mock.h"
@@ -32,6 +33,14 @@
 static const char kTestDir[] = "test";
 static const char kLastFile[] = "test/last";
 static const char kCurrentFile[] = "test/current";
+static const char kFakeDiskStatsPath[] = "fake-disk-stats";
+static const char kFakeDiskStatsFormat[] =
+    "    1793     1788    %d   105580    "
+    "    196      175     %d    30290    "
+    "    0    44060   135850\n";
+static string kFakeDiskStats[2];
+static const int kFakeReadSectors[] = {80000, 100000};
+static const int kFakeWriteSectors[] = {3000, 4000};
 
 // This class allows a TimeTicks object to be initialized with seconds
 // (rather than microseconds) through the protected TimeTicks(int64)
@@ -54,7 +63,12 @@
     EXPECT_EQ(NULL, daemon_.daily_use_.get());
     EXPECT_EQ(NULL, daemon_.kernel_crash_interval_.get());
     EXPECT_EQ(NULL, daemon_.user_crash_interval_.get());
-    daemon_.Init(true, &metrics_lib_);
+    kFakeDiskStats[0] = StringPrintf(kFakeDiskStatsFormat,
+                                     kFakeReadSectors[0], kFakeWriteSectors[0]);
+    kFakeDiskStats[1] = StringPrintf(kFakeDiskStatsFormat,
+                                     kFakeReadSectors[1], kFakeWriteSectors[1]);
+    CreateFakeDiskStatsFile(kFakeDiskStats[0].c_str());
+    daemon_.Init(true, &metrics_lib_, kFakeDiskStatsPath);
 
     // Check configuration of a few histograms.
     FrequencyCounter* frequency_counter =
@@ -120,7 +134,9 @@
     file_util::CreateDirectory(FilePath(kTestDir));
   }
 
-  virtual void TearDown() {}
+  virtual void TearDown() {
+    EXPECT_EQ(unlink(kFakeDiskStatsPath), 0);
+  }
 
   const TaggedCounterReporter*
   GetReporter(FrequencyCounter* frequency_counter) const {
@@ -222,12 +238,22 @@
     dbus_message_unref(msg);
   }
 
-  // Get the frequency counter for the given name.
+  // Gets the frequency counter for the given name.
   FrequencyCounterMock& GetFrequencyMock(const char* histogram_name) {
     return *static_cast<FrequencyCounterMock*>(
         daemon_.frequency_counters_[histogram_name]);
   }
 
+  // Creates or overwrites an input file containing fake disk stats.
+  void CreateFakeDiskStatsFile(const char* fake_stats) {
+    if (unlink(kFakeDiskStatsPath) < 0) {
+      EXPECT_EQ(errno, ENOENT);
+    }
+    FILE* f = fopen(kFakeDiskStatsPath, "w");
+    EXPECT_EQ(1, fwrite(fake_stats, strlen(fake_stats), 1, f));
+    EXPECT_EQ(0, fclose(f));
+  }
+
   // The MetricsDaemon under test.
   MetricsDaemon daemon_;
 
@@ -533,6 +559,25 @@
                 MetricsDaemon::kMetricAnyCrashesDailyName).value());
 }
 
+TEST_F(MetricsDaemonTest, ReportDiskStats) {
+  long int read_sectors_now, write_sectors_now;
+
+  CreateFakeDiskStatsFile(kFakeDiskStats[1].c_str());
+  daemon_.DiskStatsReadStats(&read_sectors_now, &write_sectors_now);
+  EXPECT_EQ(read_sectors_now, kFakeReadSectors[1]);
+  EXPECT_EQ(write_sectors_now, kFakeWriteSectors[1]);
+
+  MetricsDaemon::DiskStatsState ds_state = daemon_.diskstats_state_;
+  EXPECT_CALL(metrics_lib_,
+              SendToUMA(_, (kFakeReadSectors[1] - kFakeReadSectors[0]) / 30,
+                        _, _, _));
+  EXPECT_CALL(metrics_lib_,
+              SendToUMA(_, (kFakeWriteSectors[1] - kFakeWriteSectors[0]) / 30,
+                        _, _, _));
+  daemon_.DiskStatsCallback();
+  EXPECT_TRUE(ds_state != daemon_.diskstats_state_);
+}
+
 int main(int argc, char** argv) {
   testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();