Merge pull request #134 from qperret/gem5stats-instru

instrument: Add power monitoring support on Gem5 platforms
diff --git a/devlib/__init__.py b/devlib/__init__.py
index 911c50d..2f50632 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -17,6 +17,7 @@
 from devlib.instrument.hwmon import HwmonInstrument
 from devlib.instrument.monsoon import MonsoonInstrument
 from devlib.instrument.netstats import NetstatsInstrument
+from devlib.instrument.gem5power import Gem5PowerInstrument
 
 from devlib.trace.ftrace import FtraceCollector
 
diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py
new file mode 100644
index 0000000..6411b3f
--- /dev/null
+++ b/devlib/instrument/gem5power.py
@@ -0,0 +1,74 @@
+#    Copyright 2017 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import division
+import csv
+import re
+
+from devlib.platform.gem5 import Gem5SimulationPlatform
+from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
+from devlib.exception import TargetError, HostError
+
+
+class Gem5PowerInstrument(Instrument):
+    '''
+    Instrument enabling power monitoring in gem5
+    '''
+
+    mode = CONTINUOUS
+    roi_label = 'power_instrument'
+    
+    def __init__(self, target, power_sites):
+        '''
+        Parameter power_sites is a list of gem5 identifiers for power values. 
+        One example of such a field:
+            system.cluster0.cores0.power_model.static_power
+        '''
+        if not isinstance(target.platform, Gem5SimulationPlatform):
+            raise TargetError('Gem5PowerInstrument requires a gem5 platform')
+        if not target.has('gem5stats'):
+            raise TargetError('Gem5StatsModule is not loaded')
+        super(Gem5PowerInstrument, self).__init__(target)
+
+        # power_sites is assumed to be a list later
+        if isinstance(power_sites, list):
+            self.power_sites = power_sites
+        else:
+            self.power_sites = [power_sites]
+        self.add_channel('sim_seconds', 'time')
+        for field in self.power_sites:
+            self.add_channel(field, 'power')
+        self.target.gem5stats.book_roi(self.roi_label)
+        self.sample_period_ns = 10000000
+        self.target.gem5stats.start_periodic_dump(0, self.sample_period_ns)
+
+    def start(self):
+        self.target.gem5stats.roi_start(self.roi_label)
+
+    def stop(self):
+        self.target.gem5stats.roi_end(self.roi_label)
+       
+    def get_data(self, outfile):
+        active_sites = [c.site for c in self.active_channels]
+        with open(outfile, 'wb') as wfh:
+            writer = csv.writer(wfh)
+            writer.writerow([c.label for c in self.active_channels]) # headers
+            for rec, rois in self.target.gem5stats.match_iter(active_sites, [self.roi_label]):
+                writer.writerow([float(rec[s]) for s in active_sites])
+        return MeasurementsCsv(outfile, self.active_channels)
+    
+    def reset(self, sites=None, kinds=None, channels=None):
+        super(Gem5PowerInstrument, self).reset(sites, kinds, channels)
+        self.target.gem5stats.reset_origin()
+
diff --git a/devlib/module/gem5stats.py b/devlib/module/gem5stats.py
index 3d109aa..ba24a43 100644
--- a/devlib/module/gem5stats.py
+++ b/devlib/module/gem5stats.py
@@ -28,6 +28,7 @@
         self.target = target
         self.number = number
         self.running = False
+        self.field = 'ROI::{}'.format(number)
 
     def start(self):
         if self.running:
@@ -42,7 +43,7 @@
         self.target.execute('m5 roiend {}'.format(self.number))
         self.running = False
         return True
-    
+
 class Gem5StatsModule(Module):
     '''
     Module controlling Region of Interest (ROIs) markers, satistics dump 
@@ -56,7 +57,7 @@
 
     @staticmethod
     def probe(target):
-       return isinstance(target.platform, Gem5SimulationPlatform)
+        return isinstance(target.platform, Gem5SimulationPlatform)
 
     def __init__(self, target):
         super(Gem5StatsModule, self).__init__(target)
@@ -112,26 +113,41 @@
         Keys must match fields in gem5's statistics log file. Key example:
             system.cluster0.cores0.power_model.static_power
         '''
+        records = defaultdict(lambda : defaultdict(list))
+        for record, active_rois in self.match_iter(keys, rois_labels):
+            for key in record:
+                for roi_label in active_rois:
+                    records[key][roi_label].append(record[key])
+        return records
+
+    def match_iter(self, keys, rois_labels):
+        '''
+        Yields for each dump since origin a pair containing:
+        1. a dict storing the values corresponding to each of the specified keys
+        2. the list of currently active ROIs among those passed as parameters.
+
+        Keys must match fields in gem5's statistics log file. Key example:
+            system.cluster0.cores0.power_model.static_power
+        '''
         for label in rois_labels:
             if label not in self.rois:
                 raise KeyError('Impossible to match ROI label {}'.format(label))
             if self.rois[label].running:
                 self.logger.warning('Trying to match records in statistics file'
                         ' while ROI {} is running'.format(label))
+        
+        def roi_active(roi_label, dump):
+            roi = self.rois[roi_label]
+            return (roi.field in dump) and (int(dump[roi.field]) == 1)
 
-        records = {}
-        for key in keys:
-            records[key] = defaultdict(list)
         with open(self._stats_file_path, 'r') as stats_file:
             stats_file.seek(self._current_origin)
             for dump in iter_statistics_dump(stats_file):
-                for label in rois_labels:
-                    # Save records only when ROIs are ON
-                    roi_field = 'ROI::{}'.format(self.rois[label].number)
-                    if (roi_field in dump) and (int(dump[roi_field]) == 1):
-                        for key in keys:
-                            records[key][label].append(dump[key])
-        return records
+                active_rois = [l for l in rois_labels if roi_active(l, dump)]
+                if active_rois:
+                    record = {k: dump[k] for k in keys}
+                    yield (record, active_rois)
+
 
     def reset_origin(self):
         '''