1
0
mirror of https://github.com/ARM-software/devlib.git synced 2025-01-31 02:00:45 +00:00

Merge pull request #163 from marcbonnici/Derived_Measurements

Add support for AcmeCape and Derived Measurements
This commit is contained in:
setrofim 2017-08-21 08:46:43 +01:00 committed by GitHub
commit 66a50a2f49
13 changed files with 376 additions and 35 deletions

View File

@ -19,6 +19,9 @@ from devlib.instrument.monsoon import MonsoonInstrument
from devlib.instrument.netstats import NetstatsInstrument from devlib.instrument.netstats import NetstatsInstrument
from devlib.instrument.gem5power import Gem5PowerInstrument 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.trace.ftrace import FtraceCollector
from devlib.host import LocalConnection from devlib.host import LocalConnection

View File

@ -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()

View File

@ -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

View File

@ -48,6 +48,8 @@ class MeasurementType(object):
if not isinstance(to, MeasurementType): if not isinstance(to, MeasurementType):
msg = 'Unexpected conversion target: "{}"' msg = 'Unexpected conversion target: "{}"'
raise ValueError(msg.format(to)) raise ValueError(msg.format(to))
if to.name == self.name:
return value
if not to.name in self.conversions: if not to.name in self.conversions:
msg = 'No conversion from {} to {} available' msg = 'No conversion from {} to {} available'
raise ValueError(msg.format(self.name, to.name)) raise ValueError(msg.format(self.name, to.name))
@ -75,12 +77,20 @@ _measurement_types = [
MeasurementType('unknown', None), MeasurementType('unknown', None),
MeasurementType('time', 'seconds', MeasurementType('time', 'seconds',
conversions={ conversions={
'time_us': lambda x: x * 1000, 'time_us': lambda x: x * 1000000,
'time_ms': lambda x: x * 1000,
} }
), ),
MeasurementType('time_us', 'microseconds', MeasurementType('time_us', 'microseconds',
conversions={
'time': lambda x: x / 1000000,
'time_ms': lambda x: x / 1000,
}
),
MeasurementType('time_ms', 'milliseconds',
conversions={ conversions={
'time': lambda x: x / 1000, 'time': lambda x: x / 1000,
'time_us': lambda x: x * 1000,
} }
), ),
MeasurementType('temperature', 'degrees'), MeasurementType('temperature', 'degrees'),
@ -133,9 +143,10 @@ class Measurement(object):
class MeasurementsCsv(object): class MeasurementsCsv(object):
def __init__(self, path, channels=None): def __init__(self, path, channels=None, sample_rate_hz=None):
self.path = path self.path = path
self.channels = channels self.channels = channels
self.sample_rate_hz = sample_rate_hz
self._fh = open(path, 'rb') self._fh = open(path, 'rb')
if self.channels is None: if self.channels is None:
self._load_channels() self._load_channels()

View File

@ -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)

View File

@ -126,7 +126,7 @@ class DaqInstrument(Instrument):
writer.writerow(row) writer.writerow(row)
raw_row = _read_next_rows() raw_row = _read_next_rows()
return MeasurementsCsv(outfile, self.active_channels) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
finally: finally:
for fh in file_handles: for fh in file_handles:
fh.close() fh.close()

View File

@ -113,4 +113,4 @@ class EnergyProbeInstrument(Instrument):
continue continue
else: else:
not_a_full_row_seen = True not_a_full_row_seen = True
return MeasurementsCsv(outfile, self.active_channels) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)

View File

@ -16,6 +16,7 @@ class FramesInstrument(Instrument):
self.collector_target = collector_target self.collector_target = collector_target
self.period = period self.period = period
self.keep_raw = keep_raw self.keep_raw = keep_raw
self.sample_rate_hz = 1 / self.period
self.collector = None self.collector = None
self.header = None self.header = None
self._need_reset = True self._need_reset = True
@ -43,7 +44,7 @@ class FramesInstrument(Instrument):
self.collector.process_frames(raw_outfile) self.collector.process_frames(raw_outfile)
active_sites = [chan.label for chan in self.active_channels] active_sites = [chan.label for chan in self.active_channels]
self.collector.write_frames(outfile, columns=active_sites) 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): def _init_channels(self):
raise NotImplementedError() raise NotImplementedError()

View File

@ -28,10 +28,11 @@ class Gem5PowerInstrument(Instrument):
mode = CONTINUOUS mode = CONTINUOUS
roi_label = 'power_instrument' roi_label = 'power_instrument'
site_mapping = {'timestamp': 'sim_seconds'}
def __init__(self, target, power_sites): 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: One example of such a field:
system.cluster0.cores0.power_model.static_power system.cluster0.cores0.power_model.static_power
''' '''
@ -46,11 +47,14 @@ class Gem5PowerInstrument(Instrument):
self.power_sites = power_sites self.power_sites = power_sites
else: else:
self.power_sites = [power_sites] self.power_sites = [power_sites]
self.add_channel('sim_seconds', 'time') self.add_channel('timestamp', 'time')
for field in self.power_sites: for field in self.power_sites:
self.add_channel(field, 'power') self.add_channel(field, 'power')
self.target.gem5stats.book_roi(self.roi_label) self.target.gem5stats.book_roi(self.roi_label)
self.sample_period_ns = 10000000 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.target.gem5stats.start_periodic_dump(0, self.sample_period_ns)
self._base_stats_dump = 0 self._base_stats_dump = 0
@ -59,17 +63,18 @@ class Gem5PowerInstrument(Instrument):
def stop(self): def stop(self):
self.target.gem5stats.roi_end(self.roi_label) self.target.gem5stats.roi_end(self.roi_label)
def get_data(self, outfile): def get_data(self, outfile):
active_sites = [c.site for c in self.active_channels] active_sites = [c.site for c in self.active_channels]
with open(outfile, 'wb') as wfh: with open(outfile, 'wb') as wfh:
writer = csv.writer(wfh) writer = csv.writer(wfh)
writer.writerow([c.label for c in self.active_channels]) # headers 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): [self.roi_label], self._base_stats_dump):
writer.writerow([float(rec[s]) for s in active_sites]) 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): def reset(self, sites=None, kinds=None, channels=None):
super(Gem5PowerInstrument, self).reset(sites, kinds, channels) super(Gem5PowerInstrument, self).reset(sites, kinds, channels)
self._base_stats_dump = self.target.gem5stats.next_dump_no() self._base_stats_dump = self.target.gem5stats.next_dump_no()

View File

@ -129,4 +129,4 @@ class MonsoonInstrument(Instrument):
row.append(usb) row.append(usb)
writer.writerow(row) writer.writerow(row)
return MeasurementsCsv(outfile, self.active_channels) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)

View File

@ -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.

View File

@ -19,6 +19,7 @@ Contents:
target target
modules modules
instrumentation instrumentation
derived_measurements
platform platform
connection connection

View File

@ -139,6 +139,14 @@ Instrument
``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the columns ``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the columns
will be the same as the order of channels in ``Instrument.active_channels``. 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 This returns a :class:`MeasurementCsv` instance associated with the outfile
that can be used to stream :class:`Measurement`\ s lists (similar to what is that can be used to stream :class:`Measurement`\ s lists (similar to what is
returned by ``take_measurement()``. returned by ``take_measurement()``.
@ -151,7 +159,7 @@ Instrument
Sample rate of the instrument in Hz. Assumed to be the same for all channels. 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 .. note:: This attribute is only provided by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment. support ``CONTINUOUS`` measurement.
Instrument Channel Instrument Channel
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
@ -211,27 +219,31 @@ be reported as "power" in Watts, and never as "pwr" in milliWatts. Currently
defined measurement types are defined measurement types are
+-------------+---------+---------------+ +-------------+-------------+---------------+
| name | units | category | | name | units | category |
+=============+=========+===============+ +=============+=============+===============+
| time | seconds | | | time | seconds | |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| temperature | degrees | | | time | microseconds| |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| power | watts | power/energy | | time | milliseconds| |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| voltage | volts | power/energy | | temperature | degrees | |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| current | amps | power/energy | | power | watts | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| energy | joules | power/energy | | voltage | volts | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| tx | bytes | data transfer | | current | amps | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| rx | bytes | data transfer | | energy | joules | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| tx/rx | bytes | data transfer | | tx | bytes | data transfer |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| rx | bytes | data transfer |
+-------------+-------------+---------------+
| tx/rx | bytes | data transfer |
+-------------+-------------+---------------+
.. instruments: .. instruments: