blob: 0d2c1edcb59e5d3cee9559954f1826c06ea6f076 [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001# Copyright 2015 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
Sergei Trofimovdf817422017-06-06 14:15:40 +010015from __future__ import division
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010016import csv
17import logging
Leo Yan3229bb12016-01-05 15:01:18 +080018import collections
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010019
20from devlib.utils.types import numeric
21
22
23# Channel modes describe what sort of measurement the instrument supports.
24# Values must be powers of 2
25INSTANTANEOUS = 1
26CONTINUOUS = 2
27
Sergei Trofimovdf817422017-06-06 14:15:40 +010028MEASUREMENT_TYPES = {} # populated further down
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010029
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010030
Sergei Trofimovdf817422017-06-06 14:15:40 +010031class MeasurementType(object):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010032
Sergei Trofimovdf817422017-06-06 14:15:40 +010033 def __init__(self, name, units, category=None, conversions=None):
34 self.name = name
35 self.units = units
36 self.category = category
37 self.conversions = {}
38 if conversions is not None:
39 for key, value in conversions.iteritems():
40 if not callable(value):
41 msg = 'Converter must be callable; got {} "{}"'
42 raise ValueError(msg.format(type(value), value))
43 self.conversions[key] = value
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010044
Sergei Trofimovdf817422017-06-06 14:15:40 +010045 def convert(self, value, to):
46 if isinstance(to, basestring) and to in MEASUREMENT_TYPES:
47 to = MEASUREMENT_TYPES[to]
48 if not isinstance(to, MeasurementType):
49 msg = 'Unexpected conversion target: "{}"'
50 raise ValueError(msg.format(to))
Marc Bonnici5ef99f22017-08-02 16:59:10 +010051 if to.name == self.name:
52 return value
Sergei Trofimovdf817422017-06-06 14:15:40 +010053 if not to.name in self.conversions:
54 msg = 'No conversion from {} to {} available'
55 raise ValueError(msg.format(self.name, to.name))
56 return self.conversions[to.name](value)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010057
58 def __cmp__(self, other):
59 if isinstance(other, MeasurementType):
60 other = other.name
61 return cmp(self.name, other)
62
63 def __str__(self):
64 return self.name
65
Sergei Trofimovdf817422017-06-06 14:15:40 +010066 def __repr__(self):
67 if self.category:
68 text = 'MeasurementType({}, {}, {})'
69 return text.format(self.name, self.units, self.category)
70 else:
71 text = 'MeasurementType({}, {})'
72 return text.format(self.name, self.units)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010073
74
Sergei Trofimov01b5cff2017-09-07 14:26:04 +010075# Standard measures. In order to make sure that downstream data processing is not tied
76# to particular insturments (e.g. a particular method of mearuing power), instruments
77# must, where possible, resport their measurments formatted as on of the standard types
78# defined here.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010079_measurement_types = [
Sergei Trofimov01b5cff2017-09-07 14:26:04 +010080 # For whatever reason, the type of measurement could not be established.
Sergei Trofimov59f36fc2017-06-07 10:07:22 +010081 MeasurementType('unknown', None),
Sergei Trofimov01b5cff2017-09-07 14:26:04 +010082
83 # Generic measurements
84 MeasurementType('count', 'count'),
85 MeasurementType('percent', 'percent'),
86
87 # Time measurement. While there is typically a single "canonical" unit
88 # used for each type of measurmenent, time may be measured to a wide variety
89 # of events occuring at a wide range of scales. Forcing everying into a
90 # single scale will lead to inefficient and awkward to work with result tables.
91 # Coversion functions between the formats are specified, so that downstream
92 # processors that expect all times time be at a particular scale can automatically
93 # covert without being familar with individual instruments.
Sergei Trofimov2afa8f82017-08-30 14:27:03 +010094 MeasurementType('time', 'seconds', 'time',
Sergei Trofimov871c59a2017-06-06 14:17:03 +010095 conversions={
Marc Bonnici2de2b362017-07-25 16:19:08 +010096 'time_us': lambda x: x * 1000000,
Marc Bonnici9b465c22017-08-18 09:59:23 +010097 'time_ms': lambda x: x * 1000,
Sergei Trofimov871c59a2017-06-06 14:17:03 +010098 }
99 ),
Sergei Trofimov2afa8f82017-08-30 14:27:03 +0100100 MeasurementType('time_us', 'microseconds', 'time',
Sergei Trofimov871c59a2017-06-06 14:17:03 +0100101 conversions={
Marc Bonnici2de2b362017-07-25 16:19:08 +0100102 'time': lambda x: x / 1000000,
Marc Bonnici9b465c22017-08-18 09:59:23 +0100103 'time_ms': lambda x: x / 1000,
104 }
105 ),
Sergei Trofimov2afa8f82017-08-30 14:27:03 +0100106 MeasurementType('time_ms', 'milliseconds', 'time',
Marc Bonnici9b465c22017-08-18 09:59:23 +0100107 conversions={
108 'time': lambda x: x / 1000,
109 'time_us': lambda x: x * 1000,
Sergei Trofimov871c59a2017-06-06 14:17:03 +0100110 }
111 ),
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100112
Sergei Trofimov01b5cff2017-09-07 14:26:04 +0100113 # Measurements related to thermals.
114 MeasurementType('temperature', 'degrees', 'thermal'),
115
116 # Measurements related to power end energy consumption.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100117 MeasurementType('power', 'watts', 'power/energy'),
118 MeasurementType('voltage', 'volts', 'power/energy'),
119 MeasurementType('current', 'amps', 'power/energy'),
120 MeasurementType('energy', 'joules', 'power/energy'),
121
Sergei Trofimov01b5cff2017-09-07 14:26:04 +0100122 # Measurments realted to data transfer, e.g. neworking,
123 # memory, or backing storage.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100124 MeasurementType('tx', 'bytes', 'data transfer'),
125 MeasurementType('rx', 'bytes', 'data transfer'),
126 MeasurementType('tx/rx', 'bytes', 'data transfer'),
Sergei Trofimov871c59a2017-06-06 14:17:03 +0100127
Sergei Trofimov01b5cff2017-09-07 14:26:04 +0100128 MeasurementType('fps', 'fps', 'ui render'),
Sergei Trofimov871c59a2017-06-06 14:17:03 +0100129 MeasurementType('frames', 'frames', 'ui render'),
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100130]
Sergei Trofimovdf817422017-06-06 14:15:40 +0100131for m in _measurement_types:
132 MEASUREMENT_TYPES[m.name] = m
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100133
134
135class Measurement(object):
136
137 __slots__ = ['value', 'channel']
138
139 @property
140 def name(self):
141 return '{}_{}'.format(self.channel.site, self.channel.kind)
142
143 @property
144 def units(self):
145 return self.channel.units
146
147 def __init__(self, value, channel):
148 self.value = value
149 self.channel = channel
150
151 def __cmp__(self, other):
Sergei Trofimov8479af42017-09-07 10:13:59 +0100152 if hasattr(other, 'value'):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100153 return cmp(self.value, other.value)
154 else:
155 return cmp(self.value, other)
156
157 def __str__(self):
158 if self.units:
159 return '{}: {} {}'.format(self.name, self.value, self.units)
160 else:
161 return '{}: {}'.format(self.name, self.value)
162
163 __repr__ = __str__
164
165
166class MeasurementsCsv(object):
167
Marc Bonnicid3c30152017-08-03 12:13:48 +0100168 def __init__(self, path, channels=None, sample_rate_hz=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100169 self.path = path
170 self.channels = channels
Marc Bonnicid3c30152017-08-03 12:13:48 +0100171 self.sample_rate_hz = sample_rate_hz
Sergei Trofimov59f36fc2017-06-07 10:07:22 +0100172 if self.channels is None:
173 self._load_channels()
Sergei Trofimov8479af42017-09-07 10:13:59 +0100174 headings = [chan.label for chan in self.channels]
175 self.data_tuple = collections.namedtuple('csv_entry', headings)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100176
177 def measurements(self):
Sergei Trofimovdfd0b8e2017-08-21 11:01:42 +0100178 return list(self.iter_measurements())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100179
Sergei Trofimovdfd0b8e2017-08-21 11:01:42 +0100180 def iter_measurements(self):
Sergei Trofimov8479af42017-09-07 10:13:59 +0100181 for row in self._iter_rows():
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100182 values = map(numeric, row)
183 yield [Measurement(v, c) for (v, c) in zip(values, self.channels)]
184
Sergei Trofimov8479af42017-09-07 10:13:59 +0100185 def values(self):
186 return list(self.iter_values())
187
188 def iter_values(self):
189 for row in self._iter_rows():
190 values = map(numeric, row)
191 yield self.data_tuple(*values)
192
Sergei Trofimov59f36fc2017-06-07 10:07:22 +0100193 def _load_channels(self):
Sergei Trofimov8479af42017-09-07 10:13:59 +0100194 header = []
195 with open(self.path, 'rb') as fh:
196 reader = csv.reader(fh)
197 header = reader.next()
Sergei Trofimov59f36fc2017-06-07 10:07:22 +0100198
199 self.channels = []
200 for entry in header:
201 for mt in MEASUREMENT_TYPES:
202 suffix = '_{}'.format(mt)
203 if entry.endswith(suffix):
204 site = entry[:-len(suffix)]
205 measure = mt
Sergei Trofimov59f36fc2017-06-07 10:07:22 +0100206 break
207 else:
Sergei Trofimov8479af42017-09-07 10:13:59 +0100208 if entry in MEASUREMENT_TYPES:
209 site = None
210 measure = entry
211 else:
212 site = entry
213 measure = 'unknown'
Sergei Trofimov59f36fc2017-06-07 10:07:22 +0100214
Sergei Trofimov9192deb2017-08-31 17:38:33 +0100215 chan = InstrumentChannel(site, measure)
Sergei Trofimov59f36fc2017-06-07 10:07:22 +0100216 self.channels.append(chan)
217
Sergei Trofimov8479af42017-09-07 10:13:59 +0100218 def _iter_rows(self):
219 with open(self.path, 'rb') as fh:
220 reader = csv.reader(fh)
221 reader.next() # headings
222 for row in reader:
223 yield row
224
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100225
226class InstrumentChannel(object):
227
228 @property
229 def label(self):
Sergei Trofimov07ba1772017-09-07 09:52:20 +0100230 if self.site is not None:
231 return '{}_{}'.format(self.site, self.kind)
232 return self.kind
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100233
Sergei Trofimov9192deb2017-08-31 17:38:33 +0100234 name = label
235
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100236 @property
237 def kind(self):
238 return self.measurement_type.name
239
240 @property
241 def units(self):
242 return self.measurement_type.units
243
Sergei Trofimov9192deb2017-08-31 17:38:33 +0100244 def __init__(self, site, measurement_type, **attrs):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100245 self.site = site
246 if isinstance(measurement_type, MeasurementType):
247 self.measurement_type = measurement_type
248 else:
249 try:
250 self.measurement_type = MEASUREMENT_TYPES[measurement_type]
251 except KeyError:
252 raise ValueError('Unknown measurement type: {}'.format(measurement_type))
253 for atname, atvalue in attrs.iteritems():
254 setattr(self, atname, atvalue)
255
256 def __str__(self):
257 if self.name == self.label:
258 return 'CHAN({})'.format(self.label)
259 else:
260 return 'CHAN({}, {})'.format(self.name, self.label)
261
262 __repr__ = __str__
263
264
265class Instrument(object):
266
267 mode = 0
268
269 def __init__(self, target):
270 self.target = target
271 self.logger = logging.getLogger(self.__class__.__name__)
Leo Yan3229bb12016-01-05 15:01:18 +0800272 self.channels = collections.OrderedDict()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100273 self.active_channels = []
Brendan Jackman49b547a2017-04-26 15:07:05 +0100274 self.sample_rate_hz = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100275
276 # channel management
277
278 def list_channels(self):
279 return self.channels.values()
280
281 def get_channels(self, measure):
282 if hasattr(measure, 'name'):
283 measure = measure.name
Brendan Jackman1bc29d72017-04-25 15:56:14 +0100284 return [c for c in self.list_channels() if c.kind == measure]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100285
Sergei Trofimov9192deb2017-08-31 17:38:33 +0100286 def add_channel(self, site, measure, **attrs):
287 chan = InstrumentChannel(site, measure, **attrs)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100288 self.channels[chan.label] = chan
289
290 # initialization and teardown
291
292 def setup(self, *args, **kwargs):
293 pass
294
295 def teardown(self):
296 pass
297
Sergei Trofimov390a5442016-09-02 14:03:33 +0100298 def reset(self, sites=None, kinds=None, channels=None):
Brendan Jackman1513db02017-05-10 11:18:21 +0100299 self.active_channels = []
Sergei Trofimov390a5442016-09-02 14:03:33 +0100300 if kinds is None and sites is None and channels is None:
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100301 self.active_channels = sorted(self.channels.values(), key=lambda x: x.label)
Brendan Jackman1513db02017-05-10 11:18:21 +0100302 elif channels is not None:
303 if sites is not None or kinds is not None:
304 raise ValueError(
305 'sites and kinds should not be set if channels is set')
306 for chan_name in channels:
Sergei Trofimov390a5442016-09-02 14:03:33 +0100307 try:
308 self.active_channels.append(self.channels[chan_name])
309 except KeyError:
310 msg = 'Unexpected channel "{}"; must be in {}'
311 raise ValueError(msg.format(chan_name, self.channels.keys()))
Brendan Jackman1513db02017-05-10 11:18:21 +0100312 else:
313 if isinstance(sites, basestring):
314 sites = [sites]
315 if isinstance(kinds, basestring):
316 kinds = [kinds]
317 else:
318 for chan in self.channels.values():
319 if (kinds is None or chan.kind in kinds) and \
320 (sites is None or chan.site in sites):
321 self.active_channels.append(chan)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100322
323 # instantaneous
324
325 def take_measurement(self):
326 pass
327
328 # continuous
329
330 def start(self):
331 pass
332
333 def stop(self):
334 pass
335
336 def get_data(self, outfile):
337 pass
Sergei Trofimov823ce712017-08-30 14:55:42 +0100338
339 def get_raw(self):
340 return []