diff --git a/devlib/__init__.py b/devlib/__init__.py index 2f50632..b1b4fa3 100644 --- a/devlib/__init__.py +++ b/devlib/__init__.py @@ -19,6 +19,9 @@ from devlib.instrument.monsoon import MonsoonInstrument from devlib.instrument.netstats import NetstatsInstrument from devlib.instrument.gem5power import Gem5PowerInstrument +from devlib.derived import DerivedMeasurements +from devlib.derived.derived_measurements import DerivedEnergyMeasurements + from devlib.trace.ftrace import FtraceCollector from devlib.host import LocalConnection diff --git a/devlib/derived/__init__.py b/devlib/derived/__init__.py new file mode 100644 index 0000000..5689a58 --- /dev/null +++ b/devlib/derived/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2015 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. +# +class DerivedMeasurements(object): + + @staticmethod + def process(measurements_csv): + raise NotImplementedError() diff --git a/devlib/derived/derived_measurements.py b/devlib/derived/derived_measurements.py new file mode 100644 index 0000000..770db88 --- /dev/null +++ b/devlib/derived/derived_measurements.py @@ -0,0 +1,97 @@ +# Copyright 2013-2015 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 +from collections import defaultdict + +from devlib import DerivedMeasurements +from devlib.instrument import Measurement, MEASUREMENT_TYPES, InstrumentChannel + + +class DerivedEnergyMeasurements(DerivedMeasurements): + + @staticmethod + def process(measurements_csv): + + should_calculate_energy = [] + use_timestamp = False + + # Determine sites to calculate energy for + channel_map = defaultdict(list) + for channel in measurements_csv.channels: + channel_map[channel].append(channel.kind) + for channel, kinds in channel_map.iteritems(): + if 'power' in kinds and not 'energy' in kinds: + should_calculate_energy.append(channel.site) + if channel.site == 'timestamp': + use_timestamp = True + time_measurment = channel.measurement_type + + if measurements_csv.sample_rate_hz is None and not use_timestamp: + msg = 'Timestamp data is unavailable, please provide a sample rate' + raise ValueError(msg) + + if use_timestamp: + # Find index of timestamp column + ts_index = [i for i, chan in enumerate(measurements_csv.channels) + if chan.site == 'timestamp'] + if len(ts_index) > 1: + raise ValueError('Multiple timestamps detected') + ts_index = ts_index[0] + + row_ts = 0 + last_ts = 0 + energy_results = defaultdict(dict) + power_results = defaultdict(float) + + # Process data + for count, row in enumerate(measurements_csv.itermeasurements()): + if use_timestamp: + last_ts = row_ts + row_ts = time_measurment.convert(float(row[ts_index].value), 'time') + for entry in row: + channel = entry.channel + site = channel.site + if channel.kind == 'energy': + if count == 0: + energy_results[site]['start'] = entry.value + else: + energy_results[site]['end'] = entry.value + + if channel.kind == 'power': + power_results[site] += entry.value + + if site in should_calculate_energy: + if count == 0: + energy_results[site]['start'] = 0 + energy_results[site]['end'] = 0 + elif use_timestamp: + energy_results[site]['end'] += entry.value * (row_ts - last_ts) + else: + energy_results[site]['end'] += entry.value * (1 / + measurements_csv.sample_rate_hz) + + # Calculate final measurements + derived_measurements = [] + for site in energy_results: + total_energy = energy_results[site]['end'] - energy_results[site]['start'] + instChannel = InstrumentChannel('cum_energy', site, MEASUREMENT_TYPES['energy']) + derived_measurements.append(Measurement(total_energy, instChannel)) + + for site in power_results: + power = power_results[site] / (count + 1) #pylint: disable=undefined-loop-variable + instChannel = InstrumentChannel('avg_power', site, MEASUREMENT_TYPES['power']) + derived_measurements.append(Measurement(power, instChannel)) + + return derived_measurements diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py index 044c7d4..9f8ac00 100644 --- a/devlib/instrument/__init__.py +++ b/devlib/instrument/__init__.py @@ -48,6 +48,8 @@ class MeasurementType(object): if not isinstance(to, MeasurementType): msg = 'Unexpected conversion target: "{}"' raise ValueError(msg.format(to)) + if to.name == self.name: + return value if not to.name in self.conversions: msg = 'No conversion from {} to {} available' raise ValueError(msg.format(self.name, to.name)) @@ -75,12 +77,20 @@ _measurement_types = [ MeasurementType('unknown', None), MeasurementType('time', 'seconds', conversions={ - 'time_us': lambda x: x * 1000, + 'time_us': lambda x: x * 1000000, + 'time_ms': lambda x: x * 1000, } ), MeasurementType('time_us', 'microseconds', + conversions={ + 'time': lambda x: x / 1000000, + 'time_ms': lambda x: x / 1000, + } + ), + MeasurementType('time_ms', 'milliseconds', conversions={ 'time': lambda x: x / 1000, + 'time_us': lambda x: x * 1000, } ), MeasurementType('temperature', 'degrees'), @@ -133,9 +143,10 @@ class Measurement(object): class MeasurementsCsv(object): - def __init__(self, path, channels=None): + def __init__(self, path, channels=None, sample_rate_hz=None): self.path = path self.channels = channels + self.sample_rate_hz = sample_rate_hz self._fh = open(path, 'rb') if self.channels is None: self._load_channels() diff --git a/devlib/instrument/acmecape.py b/devlib/instrument/acmecape.py new file mode 100644 index 0000000..e1bb6c1 --- /dev/null +++ b/devlib/instrument/acmecape.py @@ -0,0 +1,123 @@ +#pylint: disable=attribute-defined-outside-init +from __future__ import division +import csv +import os +import time +import tempfile +from fcntl import fcntl, F_GETFL, F_SETFL +from string import Template +from subprocess import Popen, PIPE, STDOUT + +from devlib import Instrument, CONTINUOUS, MeasurementsCsv +from devlib.exception import HostError +from devlib.utils.misc import which + +OUTPUT_CAPTURE_FILE = 'acme-cape.csv' +IIOCAP_CMD_TEMPLATE = Template(""" +${iio_capture} -n ${host} -b ${buffer_size} -c -f ${outfile} ${iio_device} +""") + +def _read_nonblock(pipe, size=1024): + fd = pipe.fileno() + flags = fcntl(fd, F_GETFL) + flags |= os.O_NONBLOCK + fcntl(fd, F_SETFL, flags) + + output = '' + try: + while True: + output += pipe.read(size) + except IOError: + pass + return output + + +class AcmeCapeInstrument(Instrument): + + mode = CONTINUOUS + + def __init__(self, target, + iio_capture=which('iio_capture'), + host='baylibre-acme.local', + iio_device='iio:device0', + buffer_size=256): + super(AcmeCapeInstrument, self).__init__(target) + self.iio_capture = iio_capture + self.host = host + self.iio_device = iio_device + self.buffer_size = buffer_size + self.sample_rate_hz = 100 + if self.iio_capture is None: + raise HostError('Missing iio-capture binary') + self.command = None + self.process = None + + self.add_channel('shunt', 'voltage') + self.add_channel('bus', 'voltage') + self.add_channel('device', 'power') + self.add_channel('device', 'current') + self.add_channel('timestamp', 'time_ms') + + def reset(self, sites=None, kinds=None, channels=None): + super(AcmeCapeInstrument, self).reset(sites, kinds, channels) + self.raw_data_file = tempfile.mkstemp('.csv')[1] + params = dict( + iio_capture=self.iio_capture, + host=self.host, + buffer_size=self.buffer_size, + iio_device=self.iio_device, + outfile=self.raw_data_file + ) + self.command = IIOCAP_CMD_TEMPLATE.substitute(**params) + self.logger.debug('ACME cape command: {}'.format(self.command)) + + def start(self): + self.process = Popen(self.command.split(), stdout=PIPE, stderr=STDOUT) + + def stop(self): + self.process.terminate() + timeout_secs = 10 + for _ in xrange(timeout_secs): + if self.process.poll() is not None: + break + time.sleep(1) + else: + output = _read_nonblock(self.process.stdout) + self.process.kill() + self.logger.error('iio-capture did not terminate gracefully') + if self.process.poll() is None: + msg = 'Could not terminate iio-capture:\n{}' + raise HostError(msg.format(output)) + if not os.path.isfile(self.raw_data_file): + raise HostError('Output CSV not generated.') + + def get_data(self, outfile): + if os.stat(self.raw_data_file).st_size == 0: + self.logger.warning('"{}" appears to be empty'.format(self.raw_data_file)) + return + + all_channels = [c.label for c in self.list_channels()] + active_channels = [c.label for c in self.active_channels] + active_indexes = [all_channels.index(ac) for ac in active_channels] + + with open(self.raw_data_file, 'rb') as fh: + with open(outfile, 'wb') as wfh: + writer = csv.writer(wfh) + writer.writerow(active_channels) + + reader = csv.reader(fh, skipinitialspace=True) + header = reader.next() + ts_index = header.index('timestamp ms') + + + for row in reader: + output_row = [] + for i in active_indexes: + if i == ts_index: + # Leave time in ms + output_row.append(float(row[i])) + else: + # Convert rest into standard units. + output_row.append(float(row[i])/1000) + writer.writerow(output_row) + return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) diff --git a/devlib/instrument/daq.py b/devlib/instrument/daq.py index 58d2f3e..75e854d 100644 --- a/devlib/instrument/daq.py +++ b/devlib/instrument/daq.py @@ -126,7 +126,7 @@ class DaqInstrument(Instrument): writer.writerow(row) raw_row = _read_next_rows() - return MeasurementsCsv(outfile, self.active_channels) + return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) finally: for fh in file_handles: fh.close() diff --git a/devlib/instrument/energy_probe.py b/devlib/instrument/energy_probe.py index ed502f5..5f47430 100644 --- a/devlib/instrument/energy_probe.py +++ b/devlib/instrument/energy_probe.py @@ -113,4 +113,4 @@ class EnergyProbeInstrument(Instrument): continue else: not_a_full_row_seen = True - return MeasurementsCsv(outfile, self.active_channels) + return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) diff --git a/devlib/instrument/frames.py b/devlib/instrument/frames.py index d5a2147..d1899fb 100644 --- a/devlib/instrument/frames.py +++ b/devlib/instrument/frames.py @@ -16,6 +16,7 @@ class FramesInstrument(Instrument): self.collector_target = collector_target self.period = period self.keep_raw = keep_raw + self.sample_rate_hz = 1 / self.period self.collector = None self.header = None self._need_reset = True @@ -43,7 +44,7 @@ class FramesInstrument(Instrument): self.collector.process_frames(raw_outfile) active_sites = [chan.label for chan in self.active_channels] self.collector.write_frames(outfile, columns=active_sites) - return MeasurementsCsv(outfile, self.active_channels) + return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) def _init_channels(self): raise NotImplementedError() diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py index 7715ec1..d265440 100644 --- a/devlib/instrument/gem5power.py +++ b/devlib/instrument/gem5power.py @@ -28,10 +28,11 @@ class Gem5PowerInstrument(Instrument): mode = CONTINUOUS roi_label = 'power_instrument' - + site_mapping = {'timestamp': 'sim_seconds'} + def __init__(self, target, power_sites): ''' - Parameter power_sites is a list of gem5 identifiers for power values. + 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 ''' @@ -46,11 +47,14 @@ class Gem5PowerInstrument(Instrument): self.power_sites = power_sites else: self.power_sites = [power_sites] - self.add_channel('sim_seconds', 'time') + self.add_channel('timestamp', '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 + # Sample rate must remain unset as gem5 does not provide samples + # at regular intervals therefore the reported timestamp should be used. + self.sample_rate_hz = None self.target.gem5stats.start_periodic_dump(0, self.sample_period_ns) self._base_stats_dump = 0 @@ -59,17 +63,18 @@ class Gem5PowerInstrument(Instrument): 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, + sites_to_match = [self.site_mapping.get(s, s) for s in active_sites] + for rec, rois in self.target.gem5stats.match_iter(sites_to_match, [self.roi_label], self._base_stats_dump): writer.writerow([float(rec[s]) for s in active_sites]) - return MeasurementsCsv(outfile, self.active_channels) - + return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) + def reset(self, sites=None, kinds=None, channels=None): super(Gem5PowerInstrument, self).reset(sites, kinds, channels) self._base_stats_dump = self.target.gem5stats.next_dump_no() diff --git a/devlib/instrument/monsoon.py b/devlib/instrument/monsoon.py index e373d68..3103618 100644 --- a/devlib/instrument/monsoon.py +++ b/devlib/instrument/monsoon.py @@ -129,4 +129,4 @@ class MonsoonInstrument(Instrument): row.append(usb) writer.writerow(row) - return MeasurementsCsv(outfile, self.active_channels) + return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) diff --git a/doc/derived_measurements.rst b/doc/derived_measurements.rst new file mode 100644 index 0000000..fcd497c --- /dev/null +++ b/doc/derived_measurements.rst @@ -0,0 +1,69 @@ +Derived Measurements +===================== + + +The ``DerivedMeasurements`` API provides a consistent way of performing post +processing on a provided :class:`MeasurementCsv` file. + +Example +------- + +The following example shows how to use an implementation of a +:class:`DerivedMeasurement` to obtain a list of calculated ``Measurements``. + +.. code-block:: ipython + + # Import the relevant derived measurement module + # in this example the derived energy module is used. + In [1]: from devlib import DerivedEnergyMeasurements + + # Obtain a MeasurementCsv file from an instrument or create from + # existing .csv file. In this example an existing csv file is used which was + # created with a sampling rate of 100Hz + In [2]: from devlib import MeasurementsCsv + In [3]: measurement_csv = MeasurementsCsv('/example/measurements.csv', sample_rate_hz=100) + + # Process the file and obtain a list of the derived measurements + In [4]: derived_measurements = DerivedEnergyMeasurements.process(measurement_csv) + + In [5]: derived_measurements + Out[5]: [device_energy: 239.1854075 joules, device_power: 5.5494089227 watts] + +API +--- + +Derived Measurements +~~~~~~~~~~~~~~~~~~~~ + +.. class:: DerivedMeasurements() + + The ``DerivedMeasurements`` class is an abstract base for implementing + additional classes to calculate various metrics. + +.. method:: DerivedMeasurements.process(measurement_csv) + + Returns a list of :class:`Measurement` objects that have been calculated. + + + +Available Derived Measurements +------------------------------- +.. class:: DerivedEnergyMeasurements() + + The ``DerivedEnergyMeasurements`` class is used to calculate average power and + cumulative energy for each site if the required data is present. + + The calculation of cumulative energy can occur in 3 ways. If a + ``site`` contains ``energy`` results, the first and last measurements are extracted + and the delta calculated. If not, a ``timestamp`` channel will be used to calculate + the energy from the power channel, failing back to using the sample rate attribute + of the :class:`MeasurementCsv` file if timestamps are not available. If neither + timestamps or a sample rate are available then an error will be raised. + + +.. method:: DerivedEnergyMeasurements.process(measurement_csv) + + Returns a list of :class:`Measurement` objects that have been calculated for + the average power and cumulative energy for each site. + + diff --git a/doc/index.rst b/doc/index.rst index 2c6d72f..5f4dda5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -19,6 +19,7 @@ Contents: target modules instrumentation + derived_measurements platform connection diff --git a/doc/instrumentation.rst b/doc/instrumentation.rst index 3c777ac..76adf39 100644 --- a/doc/instrumentation.rst +++ b/doc/instrumentation.rst @@ -139,6 +139,14 @@ Instrument ``_`` (see :class:`InstrumentChannel`). The order of the columns will be the same as the order of channels in ``Instrument.active_channels``. + If reporting timestamps, one channel must have a ``site`` named ``"timestamp"`` + and a ``kind`` of a :class:`MeasurmentType` of an appropriate time unit which will + be used, if appropriate, during any post processing. + + .. note:: Currently supported time units are seconds, milliseconds and + microseconds, other units can also be used if an appropriate + conversion is provided. + This returns a :class:`MeasurementCsv` instance associated with the outfile that can be used to stream :class:`Measurement`\ s lists (similar to what is returned by ``take_measurement()``. @@ -151,7 +159,7 @@ Instrument Sample rate of the instrument in Hz. Assumed to be the same for all channels. .. note:: This attribute is only provided by :class:`Instrument`\ s that - support ``CONTINUOUS`` measurment. + support ``CONTINUOUS`` measurement. Instrument Channel ~~~~~~~~~~~~~~~~~~ @@ -211,27 +219,31 @@ be reported as "power" in Watts, and never as "pwr" in milliWatts. Currently defined measurement types are -+-------------+---------+---------------+ -| name | units | category | -+=============+=========+===============+ -| time | seconds | | -+-------------+---------+---------------+ -| temperature | degrees | | -+-------------+---------+---------------+ -| power | watts | power/energy | -+-------------+---------+---------------+ -| voltage | volts | power/energy | -+-------------+---------+---------------+ -| current | amps | power/energy | -+-------------+---------+---------------+ -| energy | joules | power/energy | -+-------------+---------+---------------+ -| tx | bytes | data transfer | -+-------------+---------+---------------+ -| rx | bytes | data transfer | -+-------------+---------+---------------+ -| tx/rx | bytes | data transfer | -+-------------+---------+---------------+ ++-------------+-------------+---------------+ +| name | units | category | ++=============+=============+===============+ +| time | seconds | | ++-------------+-------------+---------------+ +| time | microseconds| | ++-------------+-------------+---------------+ +| time | milliseconds| | ++-------------+-------------+---------------+ +| temperature | degrees | | ++-------------+-------------+---------------+ +| power | watts | power/energy | ++-------------+-------------+---------------+ +| voltage | volts | power/energy | ++-------------+-------------+---------------+ +| current | amps | power/energy | ++-------------+-------------+---------------+ +| energy | joules | power/energy | ++-------------+-------------+---------------+ +| tx | bytes | data transfer | ++-------------+-------------+---------------+ +| rx | bytes | data transfer | ++-------------+-------------+---------------+ +| tx/rx | bytes | data transfer | ++-------------+-------------+---------------+ .. instruments: