diff --git a/wa/framework/configuration/parsers.py b/wa/framework/configuration/parsers.py index b537abc6..34c3f362 100644 --- a/wa/framework/configuration/parsers.py +++ b/wa/framework/configuration/parsers.py @@ -186,7 +186,7 @@ def pop_aliased_param(cfg_point, d, default=None): aliases = [cfg_point.name] + cfg_point.aliases alias_map = [a for a in aliases if a in d] if len(alias_map) > 1: - raise ConfigError(DUPLICATE_ENTRY_ERROR.format(aliases)) + raise ConfigError('Duplicate entry: {}'.format(aliases)) elif alias_map: return d.pop(alias_map[0]) else: diff --git a/wa/instrumentation/energy_measurement.py b/wa/instrumentation/energy_measurement.py new file mode 100644 index 00000000..5c63b7b2 --- /dev/null +++ b/wa/instrumentation/energy_measurement.py @@ -0,0 +1,249 @@ +# Copyright 2013-2017 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. +# + + +# pylint: disable=W0613,E1101 +from __future__ import division +import os +from collections import defaultdict + +from devlib.instrument import CONTINUOUS +from devlib.instrument.energy_probe import EnergyProbeInstrument +from devlib.instrument.daq import DaqInstrument + +from wa import Instrument, Parameter +from wa.framework import pluginloader +from wa.framework.plugin import Plugin +from wa.framework.exception import ConfigError +from wa.utils.types import list_of_strings, list_of_ints, list_or_string + + +class EnergyInstrumentBackend(Plugin): + + name = None + kind = 'energy_instrument_backend' + parameters = [] + + instrument = None + + def get_parameters(self): + return {p.name : p for p in self.parameters} + + def validate_parameters(self, params): + pass + + +class DAQBackend(EnergyInstrumentBackend): + + name = 'daq' + + parameters = [ + Parameter('resistor_values', kind=list_of_ints, + description=""" + The values of resistors (in Ohms) across which the voltages + are measured on. + """), + Parameter('labels', kind=list_of_strings, + description=""" + 'List of port labels. If specified, the length of the list + must match the length of ``resistor_values``. + """), + Parameter('host', kind=str, default='localhost', + description=""" + The host address of the machine that runs the daq Server which + the instrument communicates with. + """), + Parameter('port', kind=int, default=45677, + description=""" + The port number for daq Server in which daq instrument + communicates with. + """), + Parameter('device_id', kind=str, default='Dev1', + description=""" + The ID under which the DAQ is registered with the driver. + """), + Parameter('v_range', kind=str, default=2.5, + description=""" + Specifies the voltage range for the SOC voltage channel on the + DAQ (please refer to :ref:`daq_setup` for details). + """), + Parameter('dv_range', kind=str, default=0.2, + description=""" + Specifies the voltage range for the resistor voltage channel + on the DAQ (please refer to :ref:`daq_setup` for details). + """), + Parameter('sample_rate_hz', kind=str, default=10000, + description=""" + Specify the sample rate in Hz. + """), + Parameter('channel_map', kind=list_of_ints, + default=(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23), + description=""" + Represents mapping from logical AI channel number to physical + connector on the DAQ (varies between DAQ models). The default + assumes DAQ 6363 and similar with AI channels on connectors + 0-7 and 16-23. + """) + ] + + instrument = DaqInstrument + + def validate_parameters(self, params): + if not params.get('resistor_values'): + raise ConfigError('Mandatory parameter "resistor_values" is not set.') + if params.get('labels'): + if len(params.get('labels')) != len(params.get('resistor_values')): + msg = 'Number of DAQ port labels does not match the number of resistor values.' + raise ConfigError(msg) + + +class EnergyProbeBackend(EnergyInstrumentBackend): + + name = 'energy_probe' + + parameters = [ + Parameter('resistor_values', kind=list_of_ints, + description=""" + The values of resistors (in Ohms) across which the voltages + are measured on. + """), + Parameter('labels', kind=list_of_strings, + description=""" + 'List of port labels. If specified, the length of the list + must match the length of ``resistor_values``. + """), + Parameter('device_entry', kind=str, default='/dev/ttyACM0', + description=""" + Path to /dev entry for the energy probe (it should be /dev/ttyACMx) + """), + ] + + instrument = EnergyProbeInstrument + + def validate_parameters(self, params): + if not params.get('resistor_values'): + raise ConfigError('Mandatory parameter "resistor_values" is not set.') + if params.get('labels'): + if len(params.get('labels')) != len(params.get('resistor_values')): + msg = 'Number of Energy Probe port labels does not match the number of resistor values.' + raise ConfigError(msg) + + +class EnergyMeasurement(Instrument): + + name = 'energy_measurement' + + description = """ + This instrument is designed to be used as an interface to the various + energy measurement instruments located in devlib. + """ + + parameters = [ + Parameter('instrument', kind=str, mandatory=True, + allowed_values=['daq', 'energy_probe'], + description=""" + Specify the energy instrumentation to be enabled. + """), + Parameter('instrument_parameters', kind=dict, default={}, + description=""" + Specify the parameters used to initialize the desired + instrumentation. + """), + Parameter('sites', kind=list_or_string, default=[], + description=""" + Specify which sites measurements should be collected + from, if not specified the measurements will be + collected for all available sites. + """), + Parameter('kinds', kind=list_or_string, default=[], + description=""" + Specify the kinds of measurements should be collected, + if not specified measurements will be + collected for all available kinds. + """), + Parameter('channels', kind=list_or_string, default=[], + description=""" + Specify the channels to be collected, + if not specified the measurements will be + collected for all available channels. + """), + ] + + def __init__(self, target, loader=pluginloader, **kwargs): + super(EnergyMeasurement, self).__init__(target, **kwargs) + self.instrumentation = None + self.measurement_csv = None + self.loader = loader + self.backend = self.loader.get_plugin(self.instrument) + self.params = {} + + if self.backend.instrument.mode != CONTINUOUS: + msg = '{} instrument does not support continuous measurement collection' + raise ConfigError(msg.format(self.instrument)) + + supported_params = self.backend.get_parameters() + for name, value in supported_params.iteritems(): + if name in self.instrument_parameters: + self.params[name] = self.instrument_parameters[name] + elif value.default: + self.params[name] = value.default + self.backend.validate_parameters(self.params) + + def initialize(self, context): + self.instrumentation = self.backend.instrument(self.target, **self.params) + + for channel in self.channels: + if not self.instrumentation.get_channels(channel): + raise ConfigError('No channels found for "{}"'.format(channel)) + + def setup(self, context): + self.instrumentation.reset(sites=self.sites, + kinds=self.kinds, + channels=self.channels) + + def start(self, context): + self.instrumentation.start() + + def stop(self, context): + self.instrumentation.stop() + + def update_result(self, context): + outfile = os.path.join(context.output_directory, 'energy_instrument_output.csv') + self.measurement_csv = self.instrumentation.get_data(outfile) + context.add_artifact('energy_instrument_output', outfile, 'data') + self.extract_metrics(context) + + def extract_metrics(self, context): + measurements = self.measurement_csv.itermeasurements() + energy_results = defaultdict(dict) + power_results = defaultdict(int) + + for count, row in enumerate(measurements): + for entry in row: + channel = entry.channel + if channel.kind == 'energy': + if count == 0: + energy_results[channel.site]['start'] = entry.value + else: + energy_results[channel.site]['end'] = entry.value + elif channel.kind == 'power': + power_results[channel.site] += entry.value + + for site in energy_results: + total_energy = energy_results[site]['end'] - energy_results[site]['start'] + context.add_metric('{}_energy'.format(site), total_energy, 'joules') + for site in power_results: + power = power_results[site] / count + 1 #pylint: disable=undefined-loop-variable + context.add_metric('{}_power'.format(site), power, 'watts') diff --git a/wa/instrumentation/misc.py b/wa/instrumentation/misc.py index ea6fda39..a5e455b9 100644 --- a/wa/instrumentation/misc.py +++ b/wa/instrumentation/misc.py @@ -85,18 +85,18 @@ class SysfsExtractor(Instrument): ] def initialize(self, context): - if not self.device.is_rooted and self.use_tmpfs: # pylint: disable=access-member-before-definition + if not self.target.is_rooted and self.use_tmpfs: # pylint: disable=access-member-before-definition raise ConfigError('use_tempfs must be False for an unrooted device.') elif self.use_tmpfs is None: # pylint: disable=access-member-before-definition - self.use_tmpfs = self.device.is_rooted + self.use_tmpfs = self.target.is_rooted if self.use_tmpfs: - self.on_device_before = self.device.path.join(self.tmpfs_mount_point, 'before') - self.on_device_after = self.device.path.join(self.tmpfs_mount_point, 'after') + self.on_device_before = self.target.path.join(self.tmpfs_mount_point, 'before') + self.on_device_after = self.target.path.join(self.tmpfs_mount_point, 'after') - if not self.device.file_exists(self.tmpfs_mount_point): - self.device.execute('mkdir -p {}'.format(self.tmpfs_mount_point), as_root=True) - self.device.execute(self.mount_command.format(self.tmpfs_size, self.tmpfs_mount_point), + if not self.target.file_exists(self.tmpfs_mount_point): + self.target.execute('mkdir -p {}'.format(self.tmpfs_mount_point), as_root=True) + self.target.execute(self.mount_command.format(self.tmpfs_size, self.tmpfs_mount_point), as_root=True) def setup(self, context): @@ -116,62 +116,62 @@ class SysfsExtractor(Instrument): if self.use_tmpfs: for d in self.paths: - before_dir = self.device.path.join(self.on_device_before, - self.device.path.dirname(as_relative(d))) - after_dir = self.device.path.join(self.on_device_after, - self.device.path.dirname(as_relative(d))) - if self.device.file_exists(before_dir): - self.device.execute('rm -rf {}'.format(before_dir), as_root=True) - self.device.execute('mkdir -p {}'.format(before_dir), as_root=True) - if self.device.file_exists(after_dir): - self.device.execute('rm -rf {}'.format(after_dir), as_root=True) - self.device.execute('mkdir -p {}'.format(after_dir), as_root=True) + before_dir = self.target.path.join(self.on_device_before, + self.target.path.dirname(as_relative(d))) + after_dir = self.target.path.join(self.on_device_after, + self.target.path.dirname(as_relative(d))) + if self.target.file_exists(before_dir): + self.target.execute('rm -rf {}'.format(before_dir), as_root=True) + self.target.execute('mkdir -p {}'.format(before_dir), as_root=True) + if self.target.file_exists(after_dir): + self.target.execute('rm -rf {}'.format(after_dir), as_root=True) + self.target.execute('mkdir -p {}'.format(after_dir), as_root=True) def slow_start(self, context): if self.use_tmpfs: for d in self.paths: - dest_dir = self.device.path.join(self.on_device_before, as_relative(d)) + dest_dir = self.target.path.join(self.on_device_before, as_relative(d)) if '*' in dest_dir: - dest_dir = self.device.path.dirname(dest_dir) - self.device.execute('{} cp -Hr {} {}'.format(self.device.busybox, d, dest_dir), + dest_dir = self.target.path.dirname(dest_dir) + self.target.execute('{} cp -Hr {} {}'.format(self.target.busybox, d, dest_dir), as_root=True, check_exit_code=False) else: # not rooted for dev_dir, before_dir, _, _ in self.device_and_host_paths: - self.device.pull(dev_dir, before_dir) + self.target.pull(dev_dir, before_dir) def slow_stop(self, context): if self.use_tmpfs: for d in self.paths: - dest_dir = self.device.path.join(self.on_device_after, as_relative(d)) + dest_dir = self.target.path.join(self.on_device_after, as_relative(d)) if '*' in dest_dir: - dest_dir = self.device.path.dirname(dest_dir) - self.device.execute('{} cp -Hr {} {}'.format(self.device.busybox, d, dest_dir), + dest_dir = self.target.path.dirname(dest_dir) + self.target.execute('{} cp -Hr {} {}'.format(self.target.busybox, d, dest_dir), as_root=True, check_exit_code=False) else: # not using tmpfs for dev_dir, _, after_dir, _ in self.device_and_host_paths: - self.device.pull(dev_dir, after_dir) + self.target.pull(dev_dir, after_dir) def update_result(self, context): if self.use_tmpfs: - on_device_tarball = self.device.path.join(self.device.working_directory, self.tarname) - on_host_tarball = self.device.path.join(context.output_directory, self.tarname) - self.device.execute('{} tar czf {} -C {} .'.format(self.device.busybox, + on_device_tarball = self.target.path.join(self.target.working_directory, self.tarname) + on_host_tarball = self.target.path.join(context.output_directory, self.tarname) + self.target.execute('{} tar czf {} -C {} .'.format(self.target.busybox, on_device_tarball, self.tmpfs_mount_point), as_root=True) - self.device.execute('chmod 0777 {}'.format(on_device_tarball), as_root=True) - self.device.pull(on_device_tarball, on_host_tarball) + self.target.execute('chmod 0777 {}'.format(on_device_tarball), as_root=True) + self.target.pull(on_device_tarball, on_host_tarball) with tarfile.open(on_host_tarball, 'r:gz') as tf: tf.extractall(context.output_directory) - self.device.remove(on_device_tarball) + self.target.remove(on_device_tarball) os.remove(on_host_tarball) for paths in self.device_and_host_paths: after_dir = paths[self.AFTER_PATH] dev_dir = paths[self.DEVICE_PATH].strip('*') # remove potential trailing '*' if (not os.listdir(after_dir) and - self.device.file_exists(dev_dir) and - self.device.list_directory(dev_dir)): + self.target.file_exists(dev_dir) and + self.target.list_directory(dev_dir)): self.logger.error('sysfs files were not pulled from the device.') self.device_and_host_paths.remove(paths) # Path is removed to skip diffing it for _, before_dir, after_dir, diff_dir in self.device_and_host_paths: @@ -183,19 +183,19 @@ class SysfsExtractor(Instrument): def finalize(self, context): if self.use_tmpfs: try: - self.device.execute('umount {}'.format(self.tmpfs_mount_point), as_root=True) + self.target.execute('umount {}'.format(self.tmpfs_mount_point), as_root=True) except (TargetError, CalledProcessError): # assume a directory but not mount point pass - self.device.execute('rm -rf {}'.format(self.tmpfs_mount_point), + self.target.execute('rm -rf {}'.format(self.tmpfs_mount_point), as_root=True, check_exit_code=False) def validate(self): if not self.tmpfs_mount_point: # pylint: disable=access-member-before-definition - self.tmpfs_mount_point = self.device.path.join(self.device.working_directory, 'temp-fs') + self.tmpfs_mount_point = self.target.path.join(self.target.working_directory, 'temp-fs') def _local_dir(self, directory): - return os.path.dirname(as_relative(directory).replace(self.device.path.sep, os.sep)) + return os.path.dirname(as_relative(directory).replace(self.target.path.sep, os.sep)) class ExecutionTimeInstrument(Instrument): diff --git a/wa/instrumentation/trace-cmd.py b/wa/instrumentation/trace-cmd.py index 2f44db41..06e883ca 100644 --- a/wa/instrumentation/trace-cmd.py +++ b/wa/instrumentation/trace-cmd.py @@ -40,13 +40,13 @@ class TraceCmdInstrument(Instrument): name = 'trace-cmd' description = """ - trace-cmd is an instrument which interacts with Ftrace Linux kernel internal + trace-cmd is an instrument which interacts with ftrace Linux kernel internal tracer From trace-cmd man page: - trace-cmd command interacts with the Ftrace tracer that is built inside the - Linux kernel. It interfaces with the Ftrace specific files found in the + trace-cmd command interacts with the ftrace tracer that is built inside the + Linux kernel. It interfaces with the ftrace specific files found in the debugfs file system under the tracing directory. trace-cmd reads a list of events it will trace, which can be specified in @@ -54,13 +54,8 @@ class TraceCmdInstrument(Instrument): trace_events = ['irq*', 'power*'] - If no event is specified in the config file, trace-cmd traces the following - events: - - - sched* - - irq* - - power* - - cpufreq_interactive* + If no event is specified, a default set of events that are generally considered useful + for debugging/profiling purposes will be enabled. The list of available events can be obtained by rooting and running the following command line on the device :: @@ -93,13 +88,17 @@ class TraceCmdInstrument(Instrument): is happening in each case from trace-cmd documentation: https://lwn.net/Articles/341902/. - This instrument comes with an Android trace-cmd binary that will be copied - and used on the device, however post-processing will be done on-host and - you must have trace-cmd installed and in your path. On Ubuntu systems, this - may be done with:: + This instrument comes with an trace-cmd binary that will be copied and used + on the device, however post-processing will be, by default, done on-host and you must + have trace-cmd installed and in your path. On Ubuntu systems, this may be + done with:: sudo apt-get install trace-cmd + Alternatively, you may set ``report_on_target`` parameter to ``True`` to enable on-target + processing (this is useful when running on non-Linux hosts, but is likely to take longer + and may fail on particularly resource-constrained targets). + """ parameters = [ @@ -114,7 +113,7 @@ class TraceCmdInstrument(Instrument): Parameter('functions', kind=list_of_strings, global_alias='trace_functions', description=""" - Specifies the list of functions to be traced. + Specifies the list of functions to be traced. """), Parameter('buffer_size', kind=int, default=None, global_alias='trace_buffer_size',