1
0
mirror of https://github.com/ARM-software/workload-automation.git synced 2025-02-20 20:09:11 +00:00

Merge pull request #447 from marcbonnici/energy_measurement

Energy measurement
This commit is contained in:
setrofim 2017-07-25 16:22:18 +01:00 committed by GitHub
commit 531dcfbb1b
4 changed files with 301 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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