mirror of
https://github.com/ARM-software/devlib.git
synced 2025-02-26 06:27:50 +00:00
Add a 'nanosecond' measurement type and the appropriate conversions. Also update notion of existing conversions to make things clearer.
354 lines
11 KiB
Python
354 lines
11 KiB
Python
# Copyright 2018 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 logging
|
|
import collections
|
|
|
|
from past.builtins import basestring
|
|
|
|
from devlib.utils.csvutil import csvreader
|
|
from devlib.utils.types import numeric
|
|
from devlib.utils.types import identifier
|
|
|
|
|
|
# Channel modes describe what sort of measurement the instrument supports.
|
|
# Values must be powers of 2
|
|
INSTANTANEOUS = 1
|
|
CONTINUOUS = 2
|
|
|
|
MEASUREMENT_TYPES = {} # populated further down
|
|
|
|
|
|
class MeasurementType(object):
|
|
|
|
def __init__(self, name, units, category=None, conversions=None):
|
|
self.name = name
|
|
self.units = units
|
|
self.category = category
|
|
self.conversions = {}
|
|
if conversions is not None:
|
|
for key, value in conversions.items():
|
|
if not callable(value):
|
|
msg = 'Converter must be callable; got {} "{}"'
|
|
raise ValueError(msg.format(type(value), value))
|
|
self.conversions[key] = value
|
|
|
|
def convert(self, value, to):
|
|
if isinstance(to, basestring) and to in MEASUREMENT_TYPES:
|
|
to = MEASUREMENT_TYPES[to]
|
|
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))
|
|
return self.conversions[to.name](value)
|
|
|
|
# pylint: disable=undefined-variable
|
|
def __cmp__(self, other):
|
|
if isinstance(other, MeasurementType):
|
|
other = other.name
|
|
return cmp(self.name, other)
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
def __repr__(self):
|
|
if self.category:
|
|
text = 'MeasurementType({}, {}, {})'
|
|
return text.format(self.name, self.units, self.category)
|
|
else:
|
|
text = 'MeasurementType({}, {})'
|
|
return text.format(self.name, self.units)
|
|
|
|
|
|
# Standard measures. In order to make sure that downstream data processing is not tied
|
|
# to particular insturments (e.g. a particular method of mearuing power), instruments
|
|
# must, where possible, resport their measurments formatted as on of the standard types
|
|
# defined here.
|
|
_measurement_types = [
|
|
# For whatever reason, the type of measurement could not be established.
|
|
MeasurementType('unknown', None),
|
|
|
|
# Generic measurements
|
|
MeasurementType('count', 'count'),
|
|
MeasurementType('percent', 'percent'),
|
|
|
|
# Time measurement. While there is typically a single "canonical" unit
|
|
# used for each type of measurmenent, time may be measured to a wide variety
|
|
# of events occuring at a wide range of scales. Forcing everying into a
|
|
# single scale will lead to inefficient and awkward to work with result tables.
|
|
# Coversion functions between the formats are specified, so that downstream
|
|
# processors that expect all times time be at a particular scale can automatically
|
|
# covert without being familar with individual instruments.
|
|
MeasurementType('time', 'seconds', 'time',
|
|
conversions={
|
|
'time_us': lambda x: x * 1e6,
|
|
'time_ms': lambda x: x * 1e3,
|
|
'time_ns': lambda x: x * 1e9,
|
|
}
|
|
),
|
|
MeasurementType('time_us', 'microseconds', 'time',
|
|
conversions={
|
|
'time': lambda x: x / 1e6,
|
|
'time_ms': lambda x: x / 1e3,
|
|
'time_ns': lambda x: x * 1e3,
|
|
}
|
|
),
|
|
MeasurementType('time_ms', 'milliseconds', 'time',
|
|
conversions={
|
|
'time': lambda x: x / 1e3,
|
|
'time_us': lambda x: x * 1e3,
|
|
'time_ns': lambda x: x * 1e6,
|
|
}
|
|
),
|
|
MeasurementType('time_ns', 'nanoseconds', 'time',
|
|
conversions={
|
|
'time': lambda x: x / 1e9,
|
|
'time_ms': lambda x: x / 1e6,
|
|
'time_us': lambda x: x / 1e3,
|
|
}
|
|
),
|
|
|
|
# Measurements related to thermals.
|
|
MeasurementType('temperature', 'degrees', 'thermal'),
|
|
|
|
# Measurements related to power end energy consumption.
|
|
MeasurementType('power', 'watts', 'power/energy'),
|
|
MeasurementType('voltage', 'volts', 'power/energy'),
|
|
MeasurementType('current', 'amps', 'power/energy'),
|
|
MeasurementType('energy', 'joules', 'power/energy'),
|
|
|
|
# Measurments realted to data transfer, e.g. neworking,
|
|
# memory, or backing storage.
|
|
MeasurementType('tx', 'bytes', 'data transfer'),
|
|
MeasurementType('rx', 'bytes', 'data transfer'),
|
|
MeasurementType('tx/rx', 'bytes', 'data transfer'),
|
|
|
|
MeasurementType('fps', 'fps', 'ui render'),
|
|
MeasurementType('frames', 'frames', 'ui render'),
|
|
]
|
|
for m in _measurement_types:
|
|
MEASUREMENT_TYPES[m.name] = m
|
|
|
|
|
|
class Measurement(object):
|
|
|
|
__slots__ = ['value', 'channel']
|
|
|
|
@property
|
|
def name(self):
|
|
return '{}_{}'.format(self.channel.site, self.channel.kind)
|
|
|
|
@property
|
|
def units(self):
|
|
return self.channel.units
|
|
|
|
def __init__(self, value, channel):
|
|
self.value = value
|
|
self.channel = channel
|
|
|
|
# pylint: disable=undefined-variable
|
|
def __cmp__(self, other):
|
|
if hasattr(other, 'value'):
|
|
return cmp(self.value, other.value)
|
|
else:
|
|
return cmp(self.value, other)
|
|
|
|
def __str__(self):
|
|
if self.units:
|
|
return '{}: {} {}'.format(self.name, self.value, self.units)
|
|
else:
|
|
return '{}: {}'.format(self.name, self.value)
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class MeasurementsCsv(object):
|
|
|
|
def __init__(self, path, channels=None, sample_rate_hz=None):
|
|
self.path = path
|
|
self.channels = channels
|
|
self.sample_rate_hz = sample_rate_hz
|
|
if self.channels is None:
|
|
self._load_channels()
|
|
headings = [chan.label for chan in self.channels]
|
|
self.data_tuple = collections.namedtuple('csv_entry',
|
|
map(identifier, headings))
|
|
|
|
def measurements(self):
|
|
return list(self.iter_measurements())
|
|
|
|
def iter_measurements(self):
|
|
for row in self._iter_rows():
|
|
values = map(numeric, row)
|
|
yield [Measurement(v, c) for (v, c) in zip(values, self.channels)]
|
|
|
|
def values(self):
|
|
return list(self.iter_values())
|
|
|
|
def iter_values(self):
|
|
for row in self._iter_rows():
|
|
values = list(map(numeric, row))
|
|
yield self.data_tuple(*values)
|
|
|
|
def _load_channels(self):
|
|
header = []
|
|
with csvreader(self.path) as reader:
|
|
header = next(reader)
|
|
|
|
self.channels = []
|
|
for entry in header:
|
|
for mt in MEASUREMENT_TYPES:
|
|
suffix = '_{}'.format(mt)
|
|
if entry.endswith(suffix):
|
|
site = entry[:-len(suffix)]
|
|
measure = mt
|
|
break
|
|
else:
|
|
if entry in MEASUREMENT_TYPES:
|
|
site = None
|
|
measure = entry
|
|
else:
|
|
site = entry
|
|
measure = 'unknown'
|
|
|
|
chan = InstrumentChannel(site, measure)
|
|
self.channels.append(chan)
|
|
|
|
# pylint: disable=stop-iteration-return
|
|
def _iter_rows(self):
|
|
with csvreader(self.path) as reader:
|
|
next(reader) # headings
|
|
for row in reader:
|
|
yield row
|
|
|
|
|
|
class InstrumentChannel(object):
|
|
|
|
@property
|
|
def label(self):
|
|
if self.site is not None:
|
|
return '{}_{}'.format(self.site, self.kind)
|
|
return self.kind
|
|
|
|
name = label
|
|
|
|
@property
|
|
def kind(self):
|
|
return self.measurement_type.name
|
|
|
|
@property
|
|
def units(self):
|
|
return self.measurement_type.units
|
|
|
|
def __init__(self, site, measurement_type, **attrs):
|
|
self.site = site
|
|
if isinstance(measurement_type, MeasurementType):
|
|
self.measurement_type = measurement_type
|
|
else:
|
|
try:
|
|
self.measurement_type = MEASUREMENT_TYPES[measurement_type]
|
|
except KeyError:
|
|
raise ValueError('Unknown measurement type: {}'.format(measurement_type))
|
|
for atname, atvalue in attrs.items():
|
|
setattr(self, atname, atvalue)
|
|
|
|
def __str__(self):
|
|
if self.name == self.label:
|
|
return 'CHAN({})'.format(self.label)
|
|
else:
|
|
return 'CHAN({}, {})'.format(self.name, self.label)
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
class Instrument(object):
|
|
|
|
mode = 0
|
|
|
|
def __init__(self, target):
|
|
self.target = target
|
|
self.logger = logging.getLogger(self.__class__.__name__)
|
|
self.channels = collections.OrderedDict()
|
|
self.active_channels = []
|
|
self.sample_rate_hz = None
|
|
|
|
# channel management
|
|
|
|
def list_channels(self):
|
|
return list(self.channels.values())
|
|
|
|
def get_channels(self, measure):
|
|
if hasattr(measure, 'name'):
|
|
measure = measure.name
|
|
return [c for c in self.list_channels() if c.kind == measure]
|
|
|
|
def add_channel(self, site, measure, **attrs):
|
|
chan = InstrumentChannel(site, measure, **attrs)
|
|
self.channels[chan.label] = chan
|
|
|
|
# initialization and teardown
|
|
|
|
def setup(self, *args, **kwargs):
|
|
pass
|
|
|
|
def teardown(self):
|
|
pass
|
|
|
|
def reset(self, sites=None, kinds=None, channels=None):
|
|
if channels is not None:
|
|
if sites is not None or kinds is not None:
|
|
raise ValueError('sites and kinds should not be set if channels is set')
|
|
|
|
try:
|
|
self.active_channels = [self.channels[ch] for ch in channels]
|
|
except KeyError as e:
|
|
msg = 'Unexpected channel "{}"; must be in {}'
|
|
raise ValueError(msg.format(e, self.channels.keys()))
|
|
elif sites is None and kinds is None:
|
|
self.active_channels = sorted(self.channels.values(), key=lambda x: x.label)
|
|
else:
|
|
if isinstance(sites, basestring):
|
|
sites = [sites]
|
|
if isinstance(kinds, basestring):
|
|
kinds = [kinds]
|
|
|
|
wanted = lambda ch: ((kinds is None or ch.kind in kinds) and
|
|
(sites is None or ch.site in sites))
|
|
self.active_channels = list(filter(wanted, self.channels.values()))
|
|
|
|
# instantaneous
|
|
|
|
def take_measurement(self):
|
|
pass
|
|
|
|
# continuous
|
|
|
|
def start(self):
|
|
pass
|
|
|
|
def stop(self):
|
|
pass
|
|
|
|
# pylint: disable=no-self-use
|
|
def get_data(self, outfile):
|
|
pass
|
|
|
|
def get_raw(self):
|
|
return []
|