[autotest][cfm] Add metric for amount of data written to storage

This is an experimental metric intended for tracking storage writes. In
particular, for tracking trends in logs from Chrome extensions.For now,
it lives in its separate control file with the possibility of being
integrated into control.meet_app later on.

BUG=b:36513774
TEST=local device

Change-Id: I3bfdeb293fc59acb10233ab90500606ca3d8c3ca
Reviewed-on: https://chromium-review.googlesource.com/1402735
Commit-Ready: Emil Lundmark <lndmrk@chromium.org>
Tested-by: Emil Lundmark <lndmrk@chromium.org>
Reviewed-by: Denis Tosic <dtosic@google.com>
diff --git a/client/bin/utils.py b/client/bin/utils.py
index eb378ae..ebeff6a 100644
--- a/client/bin/utils.py
+++ b/client/bin/utils.py
@@ -967,6 +967,34 @@
     return msg
 
 
+_IOSTAT_FIELDS = ('transfers_per_s', 'read_kb_per_s', 'written_kb_per_s',
+                  'read_kb', 'written_kb')
+_IOSTAT_RE = re.compile('ALL' + len(_IOSTAT_FIELDS) * r'\s+([\d\.]+)')
+
+def get_storage_statistics(device=None):
+    """
+    Fetches statistics for a storage device.
+
+    Using iostat(1) it retrieves statistics for a device since last boot.  See
+    the man page for iostat(1) for details on the different fields.
+
+    @param device: Path to a block device. Defaults to the device where root
+            is mounted.
+
+    @returns a dict mapping each field to its statistic.
+
+    @raises ValueError: If the output from iostat(1) can not be parsed.
+    """
+    if device is None:
+        device = get_root_device()
+    cmd = 'iostat -d -k -g ALL -H %s' % device
+    output = utils.system_output(cmd, ignore_status=True)
+    match = _IOSTAT_RE.search(output)
+    if not match:
+        raise ValueError('Unable to get iostat for %s' % device)
+    return dict(zip(_IOSTAT_FIELDS, map(float, match.groups())))
+
+
 def load_module(module_name, params=None):
     # Checks if a module has already been loaded
     if module_is_loaded(module_name):
diff --git a/client/cros/multimedia/system_facade_native.py b/client/cros/multimedia/system_facade_native.py
index e27fc15..6d80f23 100644
--- a/client/cros/multimedia/system_facade_native.py
+++ b/client/cros/multimedia/system_facade_native.py
@@ -129,3 +129,9 @@
         Returns the number of currently allocated file handles.
         """
         return utils.get_num_allocated_file_handles()
+
+    def get_storage_statistics(self, device=None):
+        """
+        Fetches statistics for a storage device.
+        """
+        return utils.get_storage_statistics(device)
diff --git a/server/cros/multimedia/system_facade_adapter.py b/server/cros/multimedia/system_facade_adapter.py
index d4d873e..5e3e4a4 100644
--- a/server/cros/multimedia/system_facade_adapter.py
+++ b/server/cros/multimedia/system_facade_adapter.py
@@ -122,3 +122,8 @@
         """
         return self._system_proxy.get_num_allocated_file_handles()
 
+    def get_storage_statistics(self, device=None):
+        """
+        Fetches statistics for a storage device.
+        """
+        return self._system_proxy.get_storage_statistics(device)
diff --git a/server/site_tests/enterprise_CFM_Perf/control.meet_app_storage b/server/site_tests/enterprise_CFM_Perf/control.meet_app_storage
new file mode 100644
index 0000000..724879c
--- /dev/null
+++ b/server/site_tests/enterprise_CFM_Perf/control.meet_app_storage
@@ -0,0 +1,29 @@
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+
+AUTHOR = "lndmrk@chromium.org"
+NAME = "enterprise_CFM_Perf.meet_app_storage"
+TIME = "MEDIUM"
+TEST_CATEGORY = "Performance"
+TEST_CLASS = "enterprise"
+ATTRIBUTES = "suite:hotrod"
+DEPENDENCIES = "meet_app"
+TEST_TYPE = "server"
+JOB_RETRIES = 3
+
+DOC = """
+This test extends enterprise_CFM_Perf.meet_app by also logging metrics for
+persistent storage. This is experimental and should be integrated into the
+existing meet_app control once we establish that collecting storage metrics does
+not have negative impact and that the data is useful.
+"""
+
+def run_test(machine):
+    host = hosts.create_host(machine)
+    job.run_test('enterprise_CFM_Perf', tag='meet_app_storage', host=host,
+                 include_storage_metrics=True)
+
+parallel_simple(run_test, machines)
diff --git a/server/site_tests/enterprise_CFM_Perf/enterprise_CFM_Perf.py b/server/site_tests/enterprise_CFM_Perf/enterprise_CFM_Perf.py
index b862151..417c8b9 100644
--- a/server/site_tests/enterprise_CFM_Perf/enterprise_CFM_Perf.py
+++ b/server/site_tests/enterprise_CFM_Perf/enterprise_CFM_Perf.py
@@ -45,6 +45,31 @@
         self.values.append(self.cfm_facade.get_participant_count())
 
 
+class StorageWrittenMetric(system_metrics_collector.Metric):
+    """
+    Metric that collects amount of data written to persistent storage.
+    """
+    def __init__(self, system_facade):
+        super(StorageWrittenMetric, self).__init__(
+                'storage_written', units='kB')
+        self.last_written_kb = None
+        self.system_facade = system_facade
+
+    def collect_metric(self):
+        """
+        Collects total amount of data written to persistent storage in kB.
+
+        This is a cumulative metric, the first call does not append a value. It
+        instead saves it and uses it for calculating subsequent deltas.
+        """
+        statistics = self.system_facade.get_storage_statistics()
+        written_kb = statistics['written_kb']
+        if self.last_written_kb is not None:
+            written_period = written_kb - self.last_written_kb
+            self.values.append(written_period)
+        self.last_written_kb = written_kb
+
+
 class enterprise_CFM_Perf(cfm_base_test.CfmBaseTest):
     """This is a server test which clears device TPM and runs
     enterprise_RemoraRequisition client test to enroll the device in to hotrod
@@ -74,7 +99,8 @@
         os.remove(local_path)
         return remote_path
 
-    def initialize(self, host, run_test_only=False, use_bond=True):
+    def initialize(self, host, run_test_only=False, use_bond=True,
+                   include_storage_metrics=False):
         """
         Initializes common test properties.
 
@@ -83,19 +109,21 @@
             deprovisioning, enrollment and system reboot. See cfm_base_test.
         @param use_bond: Whether to use BonD to add bots to the meeting. Useful
             for local testing.
+        @param include_storage_metrics: Also log metrics for persistent storage.
         """
         super(enterprise_CFM_Perf, self).initialize(host, run_test_only)
         self._host = host
         self._use_bond = use_bond
         system_facade = self._facade_factory.create_system_facade()
+        additional_metrics = [ParticipantCountMetric(self.cfm_facade)]
+        if include_storage_metrics:
+            additional_metrics.append(StorageWrittenMetric(system_facade))
         self._perf_metrics_collector = (
             perf_metrics_collector.PerfMetricsCollector(
                 system_facade,
                 self.cfm_facade,
                 self.output_perf_value,
-                additional_system_metrics=[
-                    ParticipantCountMetric(self.cfm_facade),
-                ]))
+                additional_system_metrics=additional_metrics))
 
     def setup(self):
         """