diff --git a/devlib/__init__.py b/devlib/__init__.py index b1b4fa3..42509f9 100644 --- a/devlib/__init__.py +++ b/devlib/__init__.py @@ -19,8 +19,8 @@ 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.derived import DerivedMeasurements, DerivedMetric +from devlib.derived.energy import DerivedEnergyMeasurements from devlib.trace.ftrace import FtraceCollector diff --git a/devlib/derived/__init__.py b/devlib/derived/__init__.py index 5689a58..24ac060 100644 --- a/devlib/derived/__init__.py +++ b/devlib/derived/__init__.py @@ -12,8 +12,49 @@ # See the License for the specific language governing permissions and # limitations under the License. # + +from devlib.instrument import MeasurementType, MEASUREMENT_TYPES + + +class DerivedMetric(object): + + __slots__ = ['name', 'value', 'measurement_type'] + + @property + def units(self): + return self.measurement_type.units + + def __init__(self, name, value, measurement_type): + self.name = name + self.value = value + if isinstance(measurement_type, MeasurementType): + self.measurement_type = measurement_type + else: + try: + self.measurement_type = MEASUREMENT_TYPES[measurement_type] + except KeyError: + msg = 'Unknown measurement type: {}' + raise ValueError(msg.format(measurement_type)) + + 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 DerivedMeasurements(object): - @staticmethod - def process(measurements_csv): - raise NotImplementedError() + def process(self, measurements_csv): + return [] + + def process_raw(self, *args): + return [] diff --git a/devlib/derived/derived_measurements.py b/devlib/derived/energy.py similarity index 86% rename from devlib/derived/derived_measurements.py rename to devlib/derived/energy.py index 770db88..84d3d7c 100644 --- a/devlib/derived/derived_measurements.py +++ b/devlib/derived/energy.py @@ -15,8 +15,8 @@ from __future__ import division from collections import defaultdict -from devlib import DerivedMeasurements -from devlib.instrument import Measurement, MEASUREMENT_TYPES, InstrumentChannel +from devlib import DerivedMeasurements, DerivedMetric +from devlib.instrument import MEASUREMENT_TYPES, InstrumentChannel class DerivedEnergyMeasurements(DerivedMeasurements): @@ -56,7 +56,7 @@ class DerivedEnergyMeasurements(DerivedMeasurements): power_results = defaultdict(float) # Process data - for count, row in enumerate(measurements_csv.itermeasurements()): + for count, row in enumerate(measurements_csv.iter_measurements()): if use_timestamp: last_ts = row_ts row_ts = time_measurment.convert(float(row[ts_index].value), 'time') @@ -86,12 +86,12 @@ class DerivedEnergyMeasurements(DerivedMeasurements): 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)) + name = '{}_total_energy'.format(site) + derived_measurements.append(DerivedMetric(name, total_energy, MEASUREMENT_TYPES['energy'])) 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)) + name = '{}_average_power'.format(site) + derived_measurements.append(DerivedMetric(name, power, MEASUREMENT_TYPES['power'])) return derived_measurements diff --git a/devlib/instrument/__init__.py b/devlib/instrument/__init__.py index 77ba1d3..0d2c1ed 100644 --- a/devlib/instrument/__init__.py +++ b/devlib/instrument/__init__.py @@ -72,38 +72,60 @@ class MeasurementType(object): return text.format(self.name, self.units) -# Standard measures +# 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), - MeasurementType('time', 'seconds', + + # 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 * 1000000, 'time_ms': lambda x: x * 1000, } ), - MeasurementType('time_us', 'microseconds', + MeasurementType('time_us', 'microseconds', 'time', conversions={ 'time': lambda x: x / 1000000, 'time_ms': lambda x: x / 1000, } ), - MeasurementType('time_ms', 'milliseconds', + MeasurementType('time_ms', 'milliseconds', 'time', conversions={ 'time': lambda x: x / 1000, 'time_us': lambda x: x * 1000, } ), - MeasurementType('temperature', 'degrees'), + # 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: @@ -127,7 +149,7 @@ class Measurement(object): self.channel = channel def __cmp__(self, other): - if isinstance(other, Measurement): + if hasattr(other, 'value'): return cmp(self.value, other.value) else: return cmp(self.value, other) @@ -147,26 +169,32 @@ class MeasurementsCsv(object): 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() + headings = [chan.label for chan in self.channels] + self.data_tuple = collections.namedtuple('csv_entry', headings) def measurements(self): - return list(self.itermeasurements()) + return list(self.iter_measurements()) - def itermeasurements(self): - self._fh.seek(0) - reader = csv.reader(self._fh) - reader.next() # headings - for row in reader: + 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 = map(numeric, row) + yield self.data_tuple(*values) + def _load_channels(self): - self._fh.seek(0) - reader = csv.reader(self._fh) - header = reader.next() - self._fh.seek(0) + header = [] + with open(self.path, 'rb') as fh: + reader = csv.reader(fh) + header = reader.next() self.channels = [] for entry in header: @@ -175,22 +203,35 @@ class MeasurementsCsv(object): if entry.endswith(suffix): site = entry[:-len(suffix)] measure = mt - name = '{}_{}'.format(site, measure) break else: - site = entry - measure = 'unknown' - name = entry + if entry in MEASUREMENT_TYPES: + site = None + measure = entry + else: + site = entry + measure = 'unknown' - chan = InstrumentChannel(name, site, measure) + chan = InstrumentChannel(site, measure) self.channels.append(chan) + def _iter_rows(self): + with open(self.path, 'rb') as fh: + reader = csv.reader(fh) + reader.next() # headings + for row in reader: + yield row + class InstrumentChannel(object): @property def label(self): - return '{}_{}'.format(self.site, self.kind) + if self.site is not None: + return '{}_{}'.format(self.site, self.kind) + return self.kind + + name = label @property def kind(self): @@ -200,8 +241,7 @@ class InstrumentChannel(object): def units(self): return self.measurement_type.units - def __init__(self, name, site, measurement_type, **attrs): - self.name = name + def __init__(self, site, measurement_type, **attrs): self.site = site if isinstance(measurement_type, MeasurementType): self.measurement_type = measurement_type @@ -243,10 +283,8 @@ class Instrument(object): measure = measure.name return [c for c in self.list_channels() if c.kind == measure] - def add_channel(self, site, measure, name=None, **attrs): - if name is None: - name = '{}_{}'.format(site, measure) - chan = InstrumentChannel(name, site, measure, **attrs) + def add_channel(self, site, measure, **attrs): + chan = InstrumentChannel(site, measure, **attrs) self.channels[chan.label] = chan # initialization and teardown @@ -297,3 +335,6 @@ class Instrument(object): def get_data(self, outfile): pass + + def get_raw(self): + return [] diff --git a/devlib/instrument/acmecape.py b/devlib/instrument/acmecape.py index e1bb6c1..1053c9d 100644 --- a/devlib/instrument/acmecape.py +++ b/devlib/instrument/acmecape.py @@ -121,3 +121,6 @@ class AcmeCapeInstrument(Instrument): output_row.append(float(row[i])/1000) writer.writerow(output_row) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) + + def get_raw(self): + return [self.raw_data_file] diff --git a/devlib/instrument/daq.py b/devlib/instrument/daq.py index 75e854d..d497151 100644 --- a/devlib/instrument/daq.py +++ b/devlib/instrument/daq.py @@ -33,6 +33,7 @@ class DaqInstrument(Instrument): # pylint: disable=no-member super(DaqInstrument, self).__init__(target) self._need_reset = True + self._raw_files = [] if execute_command is None: raise HostError('Could not import "daqpower": {}'.format(import_error_mesg)) if labels is None: @@ -68,6 +69,7 @@ class DaqInstrument(Instrument): if not result.status == Status.OK: # pylint: disable=no-member raise HostError(result.message) self._need_reset = False + self._raw_files = [] def start(self): if self._need_reset: @@ -86,6 +88,7 @@ class DaqInstrument(Instrument): site = os.path.splitext(entry)[0] path = os.path.join(tempdir, entry) raw_file_map[site] = path + self._raw_files.append(path) active_sites = unique([c.site for c in self.active_channels]) file_handles = [] @@ -131,6 +134,9 @@ class DaqInstrument(Instrument): for fh in file_handles: fh.close() + def get_raw(self): + return self._raw_files + def teardown(self): self.execute('close') diff --git a/devlib/instrument/energy_probe.py b/devlib/instrument/energy_probe.py index 5f47430..c8f179e 100644 --- a/devlib/instrument/energy_probe.py +++ b/devlib/instrument/energy_probe.py @@ -52,6 +52,7 @@ class EnergyProbeInstrument(Instrument): self.raw_output_directory = None self.process = None self.sample_rate_hz = 10000 # Determined empirically + self.raw_data_file = None for label in self.labels: for kind in self.attributes: @@ -64,6 +65,7 @@ class EnergyProbeInstrument(Instrument): for i, rval in enumerate(self.resistor_values)] rstring = ''.join(parts) self.command = '{} -d {} -l {} {}'.format(self.caiman, self.device_entry, rstring, self.raw_output_directory) + self.raw_data_file = None def start(self): self.logger.debug(self.command) @@ -92,10 +94,10 @@ class EnergyProbeInstrument(Instrument): num_of_ports = len(self.resistor_values) struct_format = '{}I'.format(num_of_ports * self.attributes_per_sample) not_a_full_row_seen = False - raw_data_file = os.path.join(self.raw_output_directory, '0000000000') + self.raw_data_file = os.path.join(self.raw_output_directory, '0000000000') - self.logger.debug('Parsing raw data file: {}'.format(raw_data_file)) - with open(raw_data_file, 'rb') as bfile: + self.logger.debug('Parsing raw data file: {}'.format(self.raw_data_file)) + with open(self.raw_data_file, 'rb') as bfile: with open(outfile, 'wb') as wfh: writer = csv.writer(wfh) writer.writerow(active_channels) @@ -114,3 +116,6 @@ class EnergyProbeInstrument(Instrument): else: not_a_full_row_seen = True return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) + + def get_raw(self): + return [self.raw_data_file] diff --git a/devlib/instrument/frames.py b/devlib/instrument/frames.py index d1899fb..54869c1 100644 --- a/devlib/instrument/frames.py +++ b/devlib/instrument/frames.py @@ -20,6 +20,7 @@ class FramesInstrument(Instrument): self.collector = None self.header = None self._need_reset = True + self._raw_file = None self._init_channels() def reset(self, sites=None, kinds=None, channels=None): @@ -27,6 +28,7 @@ class FramesInstrument(Instrument): self.collector = self.collector_cls(self.target, self.period, self.collector_target, self.header) self._need_reset = False + self._raw_file = None def start(self): if self._need_reset: @@ -38,14 +40,16 @@ class FramesInstrument(Instrument): self._need_reset = True def get_data(self, outfile): - raw_outfile = None if self.keep_raw: - raw_outfile = outfile + '.raw' - self.collector.process_frames(raw_outfile) + self._raw_file = outfile + '.raw' + self.collector.process_frames(self._raw_file) active_sites = [chan.label for chan in self.active_channels] self.collector.write_frames(outfile, columns=active_sites) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz) + def get_raw(self): + return [self._raw_file] if self._raw_file else [] + def _init_channels(self): raise NotImplementedError() diff --git a/devlib/instrument/hwmon.py b/devlib/instrument/hwmon.py index ae49f40..5a9d8af 100644 --- a/devlib/instrument/hwmon.py +++ b/devlib/instrument/hwmon.py @@ -45,7 +45,7 @@ class HwmonInstrument(Instrument): measure = self.measure_map.get(ts.kind)[0] if measure: self.logger.debug('\tAdding sensor {}'.format(ts.name)) - self.add_channel(_guess_site(ts), measure, name=ts.name, sensor=ts) + self.add_channel(_guess_site(ts), measure, sensor=ts) else: self.logger.debug('\tSkipping sensor {} (unknown kind "{}")'.format(ts.name, ts.kind)) except ValueError: diff --git a/devlib/platform/arm.py b/devlib/platform/arm.py index e760eaf..76b58a4 100644 --- a/devlib/platform/arm.py +++ b/devlib/platform/arm.py @@ -210,22 +210,22 @@ class JunoEnergyInstrument(Instrument): mode = CONTINUOUS | INSTANTANEOUS _channels = [ - InstrumentChannel('sys_curr', 'sys', 'current'), - InstrumentChannel('a57_curr', 'a57', 'current'), - InstrumentChannel('a53_curr', 'a53', 'current'), - InstrumentChannel('gpu_curr', 'gpu', 'current'), - InstrumentChannel('sys_volt', 'sys', 'voltage'), - InstrumentChannel('a57_volt', 'a57', 'voltage'), - InstrumentChannel('a53_volt', 'a53', 'voltage'), - InstrumentChannel('gpu_volt', 'gpu', 'voltage'), - InstrumentChannel('sys_pow', 'sys', 'power'), - InstrumentChannel('a57_pow', 'a57', 'power'), - InstrumentChannel('a53_pow', 'a53', 'power'), - InstrumentChannel('gpu_pow', 'gpu', 'power'), - InstrumentChannel('sys_cenr', 'sys', 'energy'), - InstrumentChannel('a57_cenr', 'a57', 'energy'), - InstrumentChannel('a53_cenr', 'a53', 'energy'), - InstrumentChannel('gpu_cenr', 'gpu', 'energy'), + InstrumentChannel('sys', 'current'), + InstrumentChannel('a57', 'current'), + InstrumentChannel('a53', 'current'), + InstrumentChannel('gpu', 'current'), + InstrumentChannel('sys', 'voltage'), + InstrumentChannel('a57', 'voltage'), + InstrumentChannel('a53', 'voltage'), + InstrumentChannel('gpu', 'voltage'), + InstrumentChannel('sys', 'power'), + InstrumentChannel('a57', 'power'), + InstrumentChannel('a53', 'power'), + InstrumentChannel('gpu', 'power'), + InstrumentChannel('sys', 'energy'), + InstrumentChannel('a57', 'energy'), + InstrumentChannel('a53', 'energy'), + InstrumentChannel('gpu', 'energy'), ] def __init__(self, target): diff --git a/devlib/target.py b/devlib/target.py index 51826fb..4b2da42 100644 --- a/devlib/target.py +++ b/devlib/target.py @@ -1011,11 +1011,12 @@ class AndroidTarget(Target): self.uninstall_executable(name) def get_pids_of(self, process_name): - result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip() - if result and 'not found' not in result: - return [int(x.split()[1]) for x in result.split('\n')[1:]] - else: - return [] + result = [] + search_term = process_name[-15:] + for entry in self.ps(): + if search_term in entry.name: + result.append(entry.pid) + return result def ps(self, **kwargs): lines = iter(convert_new_lines(self.execute('ps')).split('\n')) @@ -1023,8 +1024,12 @@ class AndroidTarget(Target): result = [] for line in lines: parts = line.split(None, 8) - if parts: - result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:]))) + if not parts: + continue + if len(parts) == 8: + # wchan was blank; insert an empty field where it should be. + parts.insert(5, '') + result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:]))) if not kwargs: return result else: diff --git a/devlib/utils/misc.py b/devlib/utils/misc.py index f601686..8cfd59f 100644 --- a/devlib/utils/misc.py +++ b/devlib/utils/misc.py @@ -103,6 +103,9 @@ CPU_PART_MAP = { 0x211: {0x1: 'KryoGold'}, 0x800: {None: 'Falkor'}, }, + 0x53: { # Samsung LSI + 0x001: {0x1: 'MongooseM1'}, + }, 0x56: { # Marvell 0x131: { 0x2: 'Feroceon 88F6281', diff --git a/devlib/utils/rendering.py b/devlib/utils/rendering.py index 3b7b6c4..665135a 100644 --- a/devlib/utils/rendering.py +++ b/devlib/utils/rendering.py @@ -83,9 +83,14 @@ class FrameCollector(threading.Thread): header = self.header frames = self.frames else: - header = [c for c in self.header if c in columns] - indexes = [self.header.index(c) for c in header] + indexes = [] + for c in columns: + if c not in self.header: + msg = 'Invalid column "{}"; must be in {}' + raise ValueError(msg.format(c, self.header)) + indexes.append(self.header.index(c)) frames = [[f[i] for i in indexes] for f in self.frames] + header = columns with open(outfile, 'w') as wfh: writer = csv.writer(wfh) if header: @@ -122,7 +127,8 @@ class SurfaceFlingerFrameCollector(FrameCollector): return self.target.execute(cmd.format(activity)) def list(self): - return self.target.execute('dumpsys SurfaceFlinger --list').split('\r\n') + text = self.target.execute('dumpsys SurfaceFlinger --list') + return text.replace('\r\n', '\n').replace('\r', '\n').split('\n') def _process_raw_file(self, fh): text = fh.read().replace('\r\n', '\n').replace('\r', '\n') @@ -203,3 +209,43 @@ class GfxinfoFrameCollector(FrameCollector): if not found: logger.warning('Could not find frames data in gfxinfo output') return + + +def _file_reverse_iter(fh, buf_size=1024): + fh.seek(0, os.SEEK_END) + offset = 0 + file_size = remaining_size = fh.tell() + while remaining_size > 0: + offset = min(file_size, offset + buf_size) + fh.seek(file_size - offset) + buf = fh.read(min(remaining_size, buf_size)) + remaining_size -= buf_size + yield buf + + +def gfxinfo_get_last_dump(filepath): + """ + Return the last gfxinfo dump from the frame collector's raw output. + + """ + record = '' + with open(filepath, 'r') as fh: + fh_iter = _file_reverse_iter(fh) + try: + while True: + buf = fh_iter.next() + ix = buf.find('** Graphics') + if ix >= 0: + return buf[ix:] + record + + ix = buf.find(' **\n') + if ix >= 0: + buf = fh_iter.next() + buf + ix = buf.find('** Graphics') + if ix < 0: + msg = '"{}" appears to be corrupted' + raise RuntimeError(msg.format(filepath)) + return buf[ix:] + record + record = buf + record + except StopIteration: + pass diff --git a/doc/derived_measurements.rst b/doc/derived_measurements.rst index fcd497c..285bce6 100644 --- a/doc/derived_measurements.rst +++ b/doc/derived_measurements.rst @@ -9,7 +9,7 @@ Example ------- The following example shows how to use an implementation of a -:class:`DerivedMeasurement` to obtain a list of calculated ``Measurements``. +:class:`DerivedMeasurement` to obtain a list of calculated ``DerivedMetric``'s. .. code-block:: ipython @@ -35,35 +35,92 @@ API Derived Measurements ~~~~~~~~~~~~~~~~~~~~ -.. class:: DerivedMeasurements() +.. class:: DerivedMeasurements - The ``DerivedMeasurements`` class is an abstract base for implementing - additional classes to calculate various metrics. + The ``DerivedMeasurements`` class provides an API for post-processing + instrument output offline (i.e. without a connection to the target device) to + generate additional metrics. .. method:: DerivedMeasurements.process(measurement_csv) - Returns a list of :class:`Measurement` objects that have been calculated. + Process a :class:`MeasurementsCsv`, returning a list of + :class:`DerivedMetric` and/or :class:`MeasurementsCsv` objects that have been + derived from the input. The exact nature and ordering of the list memebers + is specific to indivial 'class'`DerivedMeasurements` implementations. +.. method:: DerivedMeasurements.process_raw(\*args) + + Process raw output from an instrument, returnin a list :class:`DerivedMetric` + and/or :class:`MeasurementsCsv` objects that have been derived from the + input. The exact nature and ordering of the list memebers is specific to + indivial 'class'`DerivedMeasurements` implewmentations. + + The arguents to this method should be paths to raw output files generated by + an instrument. The number and order of expected arguments is specific to + particular implmentations. + + +Derived Metric +~~~~~~~~~~~~~~ + +.. class:: DerivedMetric + + Represents a metric derived from previously collected ``Measurement``s. + Unlike, a ``Measurement``, this was not measured directly from the target. + + +.. attribute:: DerivedMetric.name + + The name of the derived metric. This uniquely defines a metric -- two + ``DerivedMetric`` objects with the same ``name`` represent to instances of + the same metric (e.g. computed from two different inputs). + +.. attribute:: DerivedMetric.value + + The ``numeric`` value of the metric that has been computed for a particular + input. + +.. attribute:: DerivedMetric.measurement_type + + The ``MeasurementType`` of the metric. This indicates which conceptual + category the metric falls into, its units, and conversions to other + measurement types. + +.. attribute:: DerivedMetric.units + + The units in which the metric's value is expressed. 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. +.. note:: If a method of the API is not documented for a particular + implementation, that means that it s not overriden by that + implementation. It is still safe to call it -- an empty list will be + returned. - 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. +Energy +~~~~~~ + +.. 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. - + This will return total cumulative energy for each energy channel, and the + average power for each power channel in the input CSV. The output will contain + all energy metrics followed by power metrics. The ordering of both will match + the ordering of channels in the input. The metrics will by named based on the + sites of the coresponding channels according to the following patters: + ``"_total_energy"`` and ``"_average_power"``. diff --git a/doc/instrumentation.rst b/doc/instrumentation.rst index 8aee1ce..0d4a6ce 100644 --- a/doc/instrumentation.rst +++ b/doc/instrumentation.rst @@ -65,8 +65,8 @@ Instrument :INSTANTANEOUS: The instrument supports taking a single sample via ``take_measurement()``. :CONTINUOUS: The instrument supports collecting measurements over a - period of time via ``start()``, ``stop()``, and - ``get_data()`` methods. + period of time via ``start()``, ``stop()``, ``get_data()``, + and (optionally) ``get_raw`` methods. .. note:: It's possible for one instrument to support more than a single mode. @@ -161,6 +161,13 @@ Instrument .. note:: This method is only implemented by :class:`Instrument`\ s that support ``CONTINUOUS`` measurement. +.. method:: Instrument.get_raw() + + Returns a list of paths to files containing raw output from the underlying + source(s) that is used to produce the data CSV. If now raw output is + generated or saved, an empty list will be returned. The format of the + contents of the raw files is entirely source-dependent. + .. attribute:: Instrument.sample_rate_hz Sample rate of the instrument in Hz. Assumed to be the same for all channels. @@ -229,13 +236,15 @@ defined measurement types are +-------------+-------------+---------------+ | name | units | category | +=============+=============+===============+ -| time | seconds | | +| count | count | | +-------------+-------------+---------------+ -| time | microseconds| | +| percent | percent | | +-------------+-------------+---------------+ -| time | milliseconds| | +| time_us | microseconds| time | +-------------+-------------+---------------+ -| temperature | degrees | | +| time_ms | milliseconds| time | ++-------------+-------------+---------------+ +| temperature | degrees | thermal | +-------------+-------------+---------------+ | power | watts | power/energy | +-------------+-------------+---------------+