mirror of
https://github.com/ARM-software/workload-automation.git
synced 2025-01-18 20:11:20 +00:00
removing old files
Removing old and unsused files: - wa/framework/old_output.py: superseded by output.py in the same dir - the entire wlauto tree: replaced by wa/ tree; it's stale by now anyway. - log.py and actor.py from framework/ as neither is used.
This commit is contained in:
parent
0e5f7eb724
commit
867972f742
@ -1,31 +0,0 @@
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
from wa.framework import pluginloader
|
||||
from wa.framework.plugin import Plugin
|
||||
|
||||
|
||||
class JobActor(Plugin):
|
||||
|
||||
kind = 'job_actor'
|
||||
|
||||
def initialize(self, context):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
def restart(self):
|
||||
pass
|
||||
|
||||
def complete(self):
|
||||
pass
|
||||
|
||||
def finalize(self):
|
||||
pass
|
||||
|
||||
|
||||
class NullJobActor(JobActor):
|
||||
|
||||
name = 'null-job-actor'
|
||||
|
@ -1,306 +0,0 @@
|
||||
# Copyright 2013-2015 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=E1101
|
||||
import logging
|
||||
import string
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
import colorama
|
||||
|
||||
from wa.framework import signal
|
||||
from wa.framework.exception import WAError
|
||||
from wa.utils.misc import get_traceback
|
||||
|
||||
|
||||
COLOR_MAP = {
|
||||
logging.DEBUG: colorama.Fore.BLUE,
|
||||
logging.INFO: colorama.Fore.GREEN,
|
||||
logging.WARNING: colorama.Fore.YELLOW,
|
||||
logging.ERROR: colorama.Fore.RED,
|
||||
logging.CRITICAL: colorama.Style.BRIGHT + colorama.Fore.RED,
|
||||
}
|
||||
|
||||
RESET_COLOR = colorama.Style.RESET_ALL
|
||||
|
||||
_indent_level = 0
|
||||
_indent_width = 4
|
||||
_console_handler = None
|
||||
|
||||
|
||||
def init(verbosity=logging.INFO, color=True, indent_with=4,
|
||||
regular_fmt='%(levelname)-8s %(message)s',
|
||||
verbose_fmt='%(asctime)s %(levelname)-8s %(name)-10.10s: %(message)s',
|
||||
debug=False):
|
||||
global _indent_width, _console_handler
|
||||
_indent_width = indent_with
|
||||
signal.log_error_func = lambda m: log_error(m, signal.logger)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
error_handler = ErrorSignalHandler(logging.DEBUG)
|
||||
root_logger.addHandler(error_handler)
|
||||
|
||||
_console_handler = logging.StreamHandler()
|
||||
if color:
|
||||
formatter = ColorFormatter
|
||||
else:
|
||||
formatter = LineFormatter
|
||||
if verbosity:
|
||||
_console_handler.setLevel(logging.DEBUG)
|
||||
_console_handler.setFormatter(formatter(verbose_fmt))
|
||||
else:
|
||||
_console_handler.setLevel(logging.INFO)
|
||||
_console_handler.setFormatter(formatter(regular_fmt))
|
||||
root_logger.addHandler(_console_handler)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
if not debug:
|
||||
logging.raiseExceptions = False
|
||||
|
||||
|
||||
def set_level(level):
|
||||
_console_handler.setLevel(level)
|
||||
|
||||
|
||||
def add_file(filepath, level=logging.DEBUG,
|
||||
fmt='%(asctime)s %(levelname)-8s %(name)s: %(message)-10.10s'):
|
||||
root_logger = logging.getLogger()
|
||||
file_handler = logging.FileHandler(filepath)
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setFormatter(LineFormatter(fmt))
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
|
||||
def enable(logs):
|
||||
if isinstance(logs, list):
|
||||
for log in logs:
|
||||
__enable_logger(log)
|
||||
else:
|
||||
__enable_logger(logs)
|
||||
|
||||
|
||||
def disable(logs):
|
||||
if isinstance(logs, list):
|
||||
for log in logs:
|
||||
__disable_logger(log)
|
||||
else:
|
||||
__disable_logger(logs)
|
||||
|
||||
|
||||
def __enable_logger(logger):
|
||||
if isinstance(logger, basestring):
|
||||
logger = logging.getLogger(logger)
|
||||
logger.propagate = True
|
||||
|
||||
|
||||
def __disable_logger(logger):
|
||||
if isinstance(logger, basestring):
|
||||
logger = logging.getLogger(logger)
|
||||
logger.propagate = False
|
||||
|
||||
|
||||
def indent():
|
||||
global _indent_level
|
||||
_indent_level += 1
|
||||
|
||||
|
||||
def dedent():
|
||||
global _indent_level
|
||||
_indent_level -= 1
|
||||
|
||||
|
||||
def log_error(e, logger, critical=False):
|
||||
"""
|
||||
Log the specified Exception as an error. The Error message will be formatted
|
||||
differently depending on the nature of the exception.
|
||||
|
||||
:e: the error to log. should be an instance of ``Exception``
|
||||
:logger: logger to be used.
|
||||
:critical: if ``True``, this error will be logged at ``logging.CRITICAL``
|
||||
level, otherwise it will be logged as ``logging.ERROR``.
|
||||
|
||||
"""
|
||||
if critical:
|
||||
log_func = logger.critical
|
||||
else:
|
||||
log_func = logger.error
|
||||
|
||||
if isinstance(e, KeyboardInterrupt):
|
||||
log_func('Got CTRL-C. Aborting.')
|
||||
elif isinstance(e, WAError):
|
||||
log_func(e)
|
||||
elif isinstance(e, subprocess.CalledProcessError):
|
||||
tb = get_traceback()
|
||||
log_func(tb)
|
||||
command = e.cmd
|
||||
if e.args:
|
||||
command = '{} {}'.format(command, ' '.join(e.args))
|
||||
message = 'Command \'{}\' returned non-zero exit status {}\nOUTPUT:\n{}\n'
|
||||
log_func(message.format(command, e.returncode, e.output))
|
||||
elif isinstance(e, SyntaxError):
|
||||
tb = get_traceback()
|
||||
log_func(tb)
|
||||
message = 'Syntax Error in {}, line {}, offset {}:'
|
||||
log_func(message.format(e.filename, e.lineno, e.offset))
|
||||
log_func('\t{}'.format(e.msg))
|
||||
else:
|
||||
tb = get_traceback()
|
||||
log_func(tb)
|
||||
log_func('{}({})'.format(e.__class__.__name__, e))
|
||||
|
||||
|
||||
class ErrorSignalHandler(logging.Handler):
|
||||
"""
|
||||
Emits signals for ERROR and WARNING level traces.
|
||||
|
||||
"""
|
||||
|
||||
def emit(self, record):
|
||||
if record.levelno == logging.ERROR:
|
||||
signal.send(signal.ERROR_LOGGED, self)
|
||||
elif record.levelno == logging.WARNING:
|
||||
signal.send(signal.WARNING_LOGGED, self)
|
||||
|
||||
|
||||
class LineFormatter(logging.Formatter):
|
||||
"""
|
||||
Logs each line of the message separately.
|
||||
|
||||
"""
|
||||
|
||||
def format(self, record):
|
||||
record.message = record.getMessage()
|
||||
if self.usesTime():
|
||||
record.asctime = self.formatTime(record, self.datefmt)
|
||||
|
||||
indent = _indent_width * _indent_level
|
||||
d = record.__dict__
|
||||
parts = []
|
||||
for line in record.message.split('\n'):
|
||||
line = ' ' * indent + line
|
||||
d.update({'message': line.strip('\r')})
|
||||
parts.append(self._fmt % d)
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
class ColorFormatter(LineFormatter):
|
||||
"""
|
||||
Formats logging records with color and prepends record info
|
||||
to each line of the message.
|
||||
|
||||
BLUE for DEBUG logging level
|
||||
GREEN for INFO logging level
|
||||
YELLOW for WARNING logging level
|
||||
RED for ERROR logging level
|
||||
BOLD RED for CRITICAL logging level
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, fmt=None, datefmt=None):
|
||||
super(ColorFormatter, self).__init__(fmt, datefmt)
|
||||
template_text = self._fmt.replace('%(message)s', RESET_COLOR + '%(message)s${color}')
|
||||
template_text = '${color}' + template_text + RESET_COLOR
|
||||
self.fmt_template = string.Template(template_text)
|
||||
|
||||
def format(self, record):
|
||||
self._set_color(COLOR_MAP[record.levelno])
|
||||
return super(ColorFormatter, self).format(record)
|
||||
|
||||
def _set_color(self, color):
|
||||
self._fmt = self.fmt_template.substitute(color=color)
|
||||
|
||||
|
||||
class BaseLogWriter(object):
|
||||
|
||||
def __init__(self, name, level=logging.DEBUG):
|
||||
"""
|
||||
File-like object class designed to be used for logging from streams
|
||||
Each complete line (terminated by new line character) gets logged
|
||||
at DEBUG level. In complete lines are buffered until the next new line.
|
||||
|
||||
:param name: The name of the logger that will be used.
|
||||
|
||||
"""
|
||||
self.logger = logging.getLogger(name)
|
||||
self.buffer = ''
|
||||
if level == logging.DEBUG:
|
||||
self.do_write = self.logger.debug
|
||||
elif level == logging.INFO:
|
||||
self.do_write = self.logger.info
|
||||
elif level == logging.WARNING:
|
||||
self.do_write = self.logger.warning
|
||||
elif level == logging.ERROR:
|
||||
self.do_write = self.logger.error
|
||||
else:
|
||||
raise Exception('Unknown logging level: {}'.format(level))
|
||||
|
||||
def flush(self):
|
||||
# Defined to match the interface expected by pexpect.
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self.buffer:
|
||||
self.logger.debug(self.buffer)
|
||||
self.buffer = ''
|
||||
return self
|
||||
|
||||
def __del__(self):
|
||||
# Ensure we don't lose bufferd output
|
||||
self.close()
|
||||
|
||||
|
||||
class LogWriter(BaseLogWriter):
|
||||
|
||||
def write(self, data):
|
||||
data = data.replace('\r\n', '\n').replace('\r', '\n')
|
||||
if '\n' in data:
|
||||
parts = data.split('\n')
|
||||
parts[0] = self.buffer + parts[0]
|
||||
for part in parts[:-1]:
|
||||
self.do_write(part)
|
||||
self.buffer = parts[-1]
|
||||
else:
|
||||
self.buffer += data
|
||||
return self
|
||||
|
||||
|
||||
class LineLogWriter(BaseLogWriter):
|
||||
|
||||
def write(self, data):
|
||||
self.do_write(data)
|
||||
|
||||
|
||||
class StreamLogger(threading.Thread):
|
||||
"""
|
||||
Logs output from a stream in a thread.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, stream, level=logging.DEBUG, klass=LogWriter):
|
||||
super(StreamLogger, self).__init__()
|
||||
self.writer = klass(name, level)
|
||||
self.stream = stream
|
||||
self.daemon = True
|
||||
|
||||
def run(self):
|
||||
line = self.stream.readline()
|
||||
while line:
|
||||
self.writer.write(line.rstrip('\n'))
|
||||
line = self.stream.readline()
|
||||
self.writer.close()
|
@ -1,362 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
import uuid
|
||||
from copy import copy
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from wa.framework import signal, log
|
||||
from wa.framework.configuration.core import merge_config_values
|
||||
from wa.utils import serializer
|
||||
from wa.utils.misc import enum_metaclass, ensure_directory_exists as _d
|
||||
from wa.utils.types import numeric
|
||||
|
||||
|
||||
class Status(object):
|
||||
|
||||
__metaclass__ = enum_metaclass('values', return_name=True)
|
||||
|
||||
values = [
|
||||
'NEW',
|
||||
'PENDING',
|
||||
'RUNNING',
|
||||
'COMPLETE',
|
||||
'OK',
|
||||
'OKISH',
|
||||
'NONCRITICAL',
|
||||
'PARTIAL',
|
||||
'FAILED',
|
||||
'ABORTED',
|
||||
'SKIPPED',
|
||||
'UNKNOWN',
|
||||
]
|
||||
|
||||
|
||||
class WAOutput(object):
|
||||
|
||||
basename = '.wa-output'
|
||||
|
||||
@classmethod
|
||||
def load(cls, source):
|
||||
if os.path.isfile(source):
|
||||
pod = serializer.load(source)
|
||||
elif os.path.isdir(source):
|
||||
pod = serializer.load(os.path.join(source, cls.basename))
|
||||
else:
|
||||
message = 'Cannot load {} from {}'
|
||||
raise ValueError(message.format(cls.__name__, source))
|
||||
return cls.from_pod(pod)
|
||||
|
||||
@classmethod
|
||||
def from_pod(cls, pod):
|
||||
instance = cls(pod['output_directory'])
|
||||
instance.status = pod['status']
|
||||
instance.metrics = [Metric.from_pod(m) for m in pod['metrics']]
|
||||
instance.artifacts = [Artifact.from_pod(a) for a in pod['artifacts']]
|
||||
instance.events = [RunEvent.from_pod(e) for e in pod['events']]
|
||||
instance.classifiers = pod['classifiers']
|
||||
return instance
|
||||
|
||||
def __init__(self, output_directory):
|
||||
self.logger = logging.getLogger('output')
|
||||
self.output_directory = output_directory
|
||||
self.status = Status.UNKNOWN
|
||||
self.classifiers = {}
|
||||
self.metrics = []
|
||||
self.artifacts = []
|
||||
self.events = []
|
||||
|
||||
def initialize(self, overwrite=False):
|
||||
if os.path.exists(self.output_directory):
|
||||
if not overwrite:
|
||||
raise RuntimeError('"{}" already exists.'.format(self.output_directory))
|
||||
self.logger.info('Removing existing output directory.')
|
||||
shutil.rmtree(self.output_directory)
|
||||
self.logger.debug('Creating output directory {}'.format(self.output_directory))
|
||||
os.makedirs(self.output_directory)
|
||||
|
||||
def add_metric(self, name, value, units=None, lower_is_better=False, classifiers=None):
|
||||
classifiers = merge_config_values(self.classifiers, classifiers or {})
|
||||
self.metrics.append(Metric(name, value, units, lower_is_better, classifiers))
|
||||
|
||||
def add_artifact(self, name, path, kind, *args, **kwargs):
|
||||
path = _check_artifact_path(path, self.output_directory)
|
||||
self.artifacts.append(Artifact(name, path, kind, Artifact.RUN, *args, **kwargs))
|
||||
|
||||
def get_path(self, subpath):
|
||||
return os.path.join(self.output_directory, subpath)
|
||||
|
||||
def to_pod(self):
|
||||
return {
|
||||
'output_directory': self.output_directory,
|
||||
'status': self.status,
|
||||
'metrics': [m.to_pod() for m in self.metrics],
|
||||
'artifacts': [a.to_pod() for a in self.artifacts],
|
||||
'events': [e.to_pod() for e in self.events],
|
||||
'classifiers': copy(self.classifiers),
|
||||
}
|
||||
|
||||
def persist(self):
|
||||
statefile = os.path.join(self.output_directory, self.basename)
|
||||
with open(statefile, 'wb') as wfh:
|
||||
serializer.dump(self, wfh)
|
||||
|
||||
|
||||
class RunInfo(object):
|
||||
|
||||
default_name_format = 'wa-run-%y%m%d-%H%M%S'
|
||||
|
||||
def __init__(self, project=None, project_stage=None, name=None):
|
||||
self.uuid = uuid.uuid4()
|
||||
self.project = project
|
||||
self.project_stage = project_stage
|
||||
self.name = name or datetime.now().strftime(self.default_name_format)
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.duration = None
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
instance = RunInfo()
|
||||
instance.uuid = uuid.UUID(pod['uuid'])
|
||||
instance.project = pod['project']
|
||||
instance.project_stage = pod['project_stage']
|
||||
instance.name = pod['name']
|
||||
instance.start_time = pod['start_time']
|
||||
instance.end_time = pod['end_time']
|
||||
instance.duration = timedelta(seconds=pod['duration'])
|
||||
return instance
|
||||
|
||||
def to_pod(self):
|
||||
d = copy(self.__dict__)
|
||||
d['uuid'] = str(self.uuid)
|
||||
d['duration'] = self.duration.days * 3600 * 24 + self.duration.seconds
|
||||
return d
|
||||
|
||||
|
||||
class RunOutput(WAOutput):
|
||||
|
||||
@property
|
||||
def info_directory(self):
|
||||
return _d(os.path.join(self.output_directory, '_info'))
|
||||
|
||||
@property
|
||||
def config_directory(self):
|
||||
return _d(os.path.join(self.output_directory, '_config'))
|
||||
|
||||
@property
|
||||
def failed_directory(self):
|
||||
return _d(os.path.join(self.output_directory, '_failed'))
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
return os.path.join(self.output_directory, 'run.log')
|
||||
|
||||
@classmethod
|
||||
def from_pod(cls, pod):
|
||||
instance = WAOutput.from_pod(pod)
|
||||
instance.info = RunInfo.from_pod(pod['info'])
|
||||
instance.jobs = [JobOutput.from_pod(i) for i in pod['jobs']]
|
||||
instance.failed = [JobOutput.from_pod(i) for i in pod['failed']]
|
||||
return instance
|
||||
|
||||
def __init__(self, output_directory):
|
||||
super(RunOutput, self).__init__(output_directory)
|
||||
self.logger = logging.getLogger('output')
|
||||
self.info = RunInfo()
|
||||
self.jobs = []
|
||||
self.failed = []
|
||||
|
||||
def initialize(self, overwrite=False):
|
||||
super(RunOutput, self).initialize(overwrite)
|
||||
log.add_file(self.log_file)
|
||||
self.add_artifact('runlog', self.log_file, 'log')
|
||||
|
||||
def create_job_output(self, id):
|
||||
outdir = os.path.join(self.output_directory, id)
|
||||
job_output = JobOutput(outdir)
|
||||
self.jobs.append(job_output)
|
||||
return job_output
|
||||
|
||||
def move_failed(self, job_output):
|
||||
basename = os.path.basename(job_output.output_directory)
|
||||
i = 1
|
||||
dest = os.path.join(self.failed_directory, basename + '-{}'.format(i))
|
||||
while os.path.exists(dest):
|
||||
i += 1
|
||||
dest = '{}-{}'.format(dest[:-2], i)
|
||||
shutil.move(job_output.output_directory, dest)
|
||||
|
||||
def to_pod(self):
|
||||
pod = super(RunOutput, self).to_pod()
|
||||
pod['info'] = self.info.to_pod()
|
||||
pod['jobs'] = [i.to_pod() for i in self.jobs]
|
||||
pod['failed'] = [i.to_pod() for i in self.failed]
|
||||
return pod
|
||||
|
||||
|
||||
class JobOutput(WAOutput):
|
||||
|
||||
def add_artifact(self, name, path, kind, *args, **kwargs):
|
||||
path = _check_artifact_path(path, self.output_directory)
|
||||
self.artifacts.append(Artifact(name, path, kind, Artifact.ITERATION, *args, **kwargs))
|
||||
|
||||
|
||||
class Artifact(object):
|
||||
"""
|
||||
This is an artifact generated during execution/post-processing of a workload.
|
||||
Unlike metrics, this represents an actual artifact, such as a file, generated.
|
||||
This may be "result", such as trace, or it could be "meta data" such as logs.
|
||||
These are distinguished using the ``kind`` attribute, which also helps WA decide
|
||||
how it should be handled. Currently supported kinds are:
|
||||
|
||||
:log: A log file. Not part of "results" as such but contains information about the
|
||||
run/workload execution that be useful for diagnostics/meta analysis.
|
||||
:meta: A file containing metadata. This is not part of "results", but contains
|
||||
information that may be necessary to reproduce the results (contrast with
|
||||
``log`` artifacts which are *not* necessary).
|
||||
:data: This file contains new data, not available otherwise and should be considered
|
||||
part of the "results" generated by WA. Most traces would fall into this category.
|
||||
:export: Exported version of results or some other artifact. This signifies that
|
||||
this artifact does not contain any new data that is not available
|
||||
elsewhere and that it may be safely discarded without losing information.
|
||||
:raw: Signifies that this is a raw dump/log that is normally processed to extract
|
||||
useful information and is then discarded. In a sense, it is the opposite of
|
||||
``export``, but in general may also be discarded.
|
||||
|
||||
.. note:: whether a file is marked as ``log``/``data`` or ``raw`` depends on
|
||||
how important it is to preserve this file, e.g. when archiving, vs
|
||||
how much space it takes up. Unlike ``export`` artifacts which are
|
||||
(almost) always ignored by other exporters as that would never result
|
||||
in data loss, ``raw`` files *may* be processed by exporters if they
|
||||
decided that the risk of losing potentially (though unlikely) useful
|
||||
data is greater than the time/space cost of handling the artifact (e.g.
|
||||
a database uploader may choose to ignore ``raw`` artifacts, where as a
|
||||
network filer archiver may choose to archive them).
|
||||
|
||||
.. note: The kind parameter is intended to represent the logical function of a particular
|
||||
artifact, not it's intended means of processing -- this is left entirely up to the
|
||||
result processors.
|
||||
|
||||
"""
|
||||
|
||||
RUN = 'run'
|
||||
ITERATION = 'iteration'
|
||||
|
||||
valid_kinds = ['log', 'meta', 'data', 'export', 'raw']
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
return Artifact(**pod)
|
||||
|
||||
def __init__(self, name, path, kind, level=RUN, mandatory=False, description=None):
|
||||
""""
|
||||
:param name: Name that uniquely identifies this artifact.
|
||||
:param path: The *relative* path of the artifact. Depending on the ``level``
|
||||
must be either relative to the run or iteration output directory.
|
||||
Note: this path *must* be delimited using ``/`` irrespective of the
|
||||
operating system.
|
||||
:param kind: The type of the artifact this is (e.g. log file, result, etc.) this
|
||||
will be used a hit to result processors. This must be one of ``'log'``,
|
||||
``'meta'``, ``'data'``, ``'export'``, ``'raw'``.
|
||||
:param level: The level at which the artifact will be generated. Must be either
|
||||
``'iteration'`` or ``'run'``.
|
||||
:param mandatory: Boolean value indicating whether this artifact must be present
|
||||
at the end of result processing for its level.
|
||||
:param description: A free-form description of what this artifact is.
|
||||
|
||||
"""
|
||||
if kind not in self.valid_kinds:
|
||||
raise ValueError('Invalid Artifact kind: {}; must be in {}'.format(kind, self.valid_kinds))
|
||||
self.name = name
|
||||
self.path = path.replace('/', os.sep) if path is not None else path
|
||||
self.kind = kind
|
||||
self.level = level
|
||||
self.mandatory = mandatory
|
||||
self.description = description
|
||||
|
||||
def exists(self, context):
|
||||
"""Returns ``True`` if artifact exists within the specified context, and
|
||||
``False`` otherwise."""
|
||||
fullpath = os.path.join(context.output_directory, self.path)
|
||||
return os.path.exists(fullpath)
|
||||
|
||||
def to_pod(self):
|
||||
return copy(self.__dict__)
|
||||
|
||||
|
||||
class RunEvent(object):
|
||||
"""
|
||||
An event that occured during a run.
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
instance = RunEvent(pod['message'])
|
||||
instance.timestamp = pod['timestamp']
|
||||
return instance
|
||||
|
||||
def __init__(self, message):
|
||||
self.timestamp = datetime.utcnow()
|
||||
self.message = message
|
||||
|
||||
def to_pod(self):
|
||||
return copy(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.timestamp, self.message)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class Metric(object):
|
||||
"""
|
||||
This is a single metric collected from executing a workload.
|
||||
|
||||
:param name: the name of the metric. Uniquely identifies the metric
|
||||
within the results.
|
||||
:param value: The numerical value of the metric for this execution of
|
||||
a workload. This can be either an int or a float.
|
||||
:param units: Units for the collected value. Can be None if the value
|
||||
has no units (e.g. it's a count or a standardised score).
|
||||
:param lower_is_better: Boolean flag indicating where lower values are
|
||||
better than higher ones. Defaults to False.
|
||||
:param classifiers: A set of key-value pairs to further classify this metric
|
||||
beyond current iteration (e.g. this can be used to identify
|
||||
sub-tests).
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
return Metric(**pod)
|
||||
|
||||
def __init__(self, name, value, units=None, lower_is_better=False, classifiers=None):
|
||||
self.name = name
|
||||
self.value = numeric(value)
|
||||
self.units = units
|
||||
self.lower_is_better = lower_is_better
|
||||
self.classifiers = classifiers or {}
|
||||
|
||||
def to_pod(self):
|
||||
return copy(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
result = '{}: {}'.format(self.name, self.value)
|
||||
if self.units:
|
||||
result += ' ' + self.units
|
||||
result += ' ({})'.format('-' if self.lower_is_better else '+')
|
||||
return '<{}>'.format(result)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
def _check_artifact_path(path, rootpath):
|
||||
if path.startswith(rootpath):
|
||||
return os.path.abspath(path)
|
||||
rootpath = os.path.abspath(rootpath)
|
||||
full_path = os.path.join(rootpath, path)
|
||||
if not os.path.isfile(full_path):
|
||||
raise ValueError('Cannot add artifact because {} does not exist.'.format(full_path))
|
||||
return full_path
|
@ -1,80 +0,0 @@
|
||||
import string
|
||||
from copy import copy
|
||||
|
||||
from devlib import Platform, AndroidTarget
|
||||
|
||||
|
||||
class TargetInfo(object):
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
instance = TargetInfo()
|
||||
instance.target = pod['target']
|
||||
instance.abi = pod['abi']
|
||||
instance.cpuinfo = Cpuinfo(pod['cpuinfo'])
|
||||
instance.os = pod['os']
|
||||
instance.os_version = pod['os_version']
|
||||
instance.abi = pod['abi']
|
||||
instance.is_rooted = pod['is_rooted']
|
||||
instance.kernel_version = KernelVersion(pod['kernel_release'],
|
||||
pod['kernel_version'])
|
||||
instance.kernel_config = KernelConfig(pod['kernel_config'])
|
||||
|
||||
if pod["target"] == "AndroidTarget":
|
||||
instance.screen_resolution = pod['screen_resolution']
|
||||
instance.prop = pod['prop']
|
||||
instance.prop = pod['android_id']
|
||||
|
||||
return instance
|
||||
|
||||
def __init__(self, target=None):
|
||||
if target:
|
||||
self.target = target.__class__.__name__
|
||||
self.cpuinfo = target.cpuinfo
|
||||
self.os = target.os
|
||||
self.os_version = target.os_version
|
||||
self.abi = target.abi
|
||||
self.is_rooted = target.is_rooted
|
||||
self.kernel_version = target.kernel_version
|
||||
self.kernel_config = target.config
|
||||
|
||||
if isinstance(target, AndroidTarget):
|
||||
self.screen_resolution = target.screen_resolution
|
||||
self.prop = target.getprop()
|
||||
self.android_id = target.android_id
|
||||
|
||||
else:
|
||||
self.target = None
|
||||
self.cpuinfo = None
|
||||
self.os = None
|
||||
self.os_version = None
|
||||
self.abi = None
|
||||
self.is_rooted = None
|
||||
self.kernel_version = None
|
||||
self.kernel_config = None
|
||||
|
||||
if isinstance(target, AndroidTarget):
|
||||
self.screen_resolution = None
|
||||
self.prop = None
|
||||
self.android_id = None
|
||||
|
||||
def to_pod(self):
|
||||
pod = {}
|
||||
pod['target'] = self.target
|
||||
pod['abi'] = self.abi
|
||||
pod['cpuinfo'] = self.cpuinfo.sections
|
||||
pod['os'] = self.os
|
||||
pod['os_version'] = self.os_version
|
||||
pod['abi'] = self.abi
|
||||
pod['is_rooted'] = self.is_rooted
|
||||
pod['kernel_release'] = self.kernel_version.release
|
||||
pod['kernel_version'] = self.kernel_version.version
|
||||
pod['kernel_config'] = dict(self.kernel_config.iteritems())
|
||||
|
||||
if self.target == "AndroidTarget":
|
||||
pod['screen_resolution'] = self.screen_resolution
|
||||
pod['prop'] = self.prop
|
||||
pod['android_id'] = self.android_id
|
||||
|
||||
return pod
|
||||
|
@ -1,35 +0,0 @@
|
||||
# Copyright 2013-2015 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 wlauto.core.configuration import settings # NOQA
|
||||
from wlauto.core.device_manager import DeviceManager # NOQA
|
||||
from wlauto.core.command import Command # NOQA
|
||||
from wlauto.core.workload import Workload # NOQA
|
||||
from wlauto.core.plugin import Artifact, Alias # NOQA
|
||||
from wlauto.core.configuration.configuration import ConfigurationPoint as Parameter
|
||||
import wlauto.core.pluginloader as PluginLoader # NOQA
|
||||
from wlauto.core.instrumentation import Instrument # NOQA
|
||||
from wlauto.core.result import ResultProcessor, IterationResult # NOQA
|
||||
from wlauto.core.resource import ResourceGetter, Resource, GetterPriority, NO_ONE # NOQA
|
||||
from wlauto.core.exttype import get_plugin_type # NOQA Note: MUST be imported after other core imports.
|
||||
|
||||
from wlauto.common.resources import File, PluginAsset, Executable
|
||||
from wlauto.common.android.resources import ApkFile, JarFile
|
||||
from wlauto.common.android.workload import (UiAutomatorWorkload, ApkWorkload, AndroidBenchmark, # NOQA
|
||||
AndroidUiAutoBenchmark, GameWorkload) # NOQA
|
||||
|
||||
from wlauto.core.version import get_wa_version
|
||||
|
||||
__version__ = get_wa_version()
|
@ -1,79 +0,0 @@
|
||||
# This agenda specifies configuration that may be used for regression runs
|
||||
# on big.LITTLE systems. This agenda will with a TC2 device configured as
|
||||
# described in the documentation.
|
||||
config:
|
||||
device: tc2
|
||||
run_name: big.LITTLE_regression
|
||||
global:
|
||||
iterations: 5
|
||||
sections:
|
||||
- id: mp_a15only
|
||||
boot_parameters:
|
||||
os_mode: mp_a15_only
|
||||
runtime_parameters:
|
||||
a15_governor: interactive
|
||||
a15_governor_tunables:
|
||||
above_hispeed_delay: 20000
|
||||
- id: mp_a7bc
|
||||
boot_parameters:
|
||||
os_mode: mp_a7_bootcluster
|
||||
runtime_parameters:
|
||||
a7_governor: interactive
|
||||
a7_min_frequency: 500000
|
||||
a7_governor_tunables:
|
||||
above_hispeed_delay: 20000
|
||||
a15_governor: interactive
|
||||
a15_governor_tunables:
|
||||
above_hispeed_delay: 20000
|
||||
- id: mp_a15bc
|
||||
boot_parameters:
|
||||
os_mode: mp_a15_bootcluster
|
||||
runtime_parameters:
|
||||
a7_governor: interactive
|
||||
a7_min_frequency: 500000
|
||||
a7_governor_tunables:
|
||||
above_hispeed_delay: 20000
|
||||
a15_governor: interactive
|
||||
a15_governor_tunables:
|
||||
above_hispeed_delay: 20000
|
||||
workloads:
|
||||
- id: b01
|
||||
name: andebench
|
||||
workload_parameters:
|
||||
number_of_threads: 5
|
||||
- id: b02
|
||||
name: andebench
|
||||
label: andebenchst
|
||||
workload_parameters:
|
||||
number_of_threads: 1
|
||||
- id: b03
|
||||
name: antutu
|
||||
label: antutu4.0.3
|
||||
workload_parameters:
|
||||
version: 4.0.3
|
||||
- id: b04
|
||||
name: benchmarkpi
|
||||
- id: b05
|
||||
name: caffeinemark
|
||||
- id: b06
|
||||
name: cfbench
|
||||
- id: b07
|
||||
name: geekbench
|
||||
label: geekbench3
|
||||
workload_parameters:
|
||||
version: 3
|
||||
- id: b08
|
||||
name: linpack
|
||||
- id: b09
|
||||
name: quadrant
|
||||
- id: b10
|
||||
name: smartbench
|
||||
- id: b11
|
||||
name: sqlite
|
||||
- id: b12
|
||||
name: vellamo
|
||||
|
||||
- id: w01
|
||||
name: bbench_with_audio
|
||||
- id: w02
|
||||
name: audio
|
@ -1,43 +0,0 @@
|
||||
# This an agenda that is built-up during the explantion of the agenda features
|
||||
# in the documentation. This should work out-of-the box on most rooted Android
|
||||
# devices.
|
||||
config:
|
||||
project: governor_comparison
|
||||
run_name: performance_vs_interactive
|
||||
|
||||
device: generic_android
|
||||
reboot_policy: never
|
||||
|
||||
instrumentation: [coreutil, cpufreq]
|
||||
coreutil:
|
||||
threshold: 80
|
||||
sysfs_extractor:
|
||||
paths: [/proc/meminfo]
|
||||
result_processors: [sqlite]
|
||||
sqlite:
|
||||
database: ~/my_wa_results.sqlite
|
||||
global:
|
||||
iterations: 5
|
||||
sections:
|
||||
- id: perf
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
- id: inter
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: interactive
|
||||
workloads:
|
||||
- id: 01_dhry
|
||||
name: dhrystone
|
||||
label: dhrystone_15over6
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- id: 02_memc
|
||||
name: memcpy
|
||||
instrumentation: [sysfs_extractor]
|
||||
- id: 03_cycl
|
||||
name: cyclictest
|
||||
iterations: 10
|
||||
|
@ -1,16 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
@ -1,400 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import string
|
||||
import textwrap
|
||||
import argparse
|
||||
import shutil
|
||||
import getpass
|
||||
import subprocess
|
||||
from collections import OrderedDict
|
||||
|
||||
import yaml
|
||||
|
||||
from wlauto import PluginLoader, Command, settings
|
||||
from wlauto.exceptions import CommandError, ConfigError
|
||||
from wlauto.core.command import init_argument_parser
|
||||
from wlauto.utils.misc import (capitalize, check_output,
|
||||
ensure_file_directory_exists as _f, ensure_directory_exists as _d)
|
||||
from wlauto.utils.types import identifier
|
||||
from wlauto.utils.doc import format_body
|
||||
|
||||
|
||||
__all__ = ['create_workload']
|
||||
|
||||
|
||||
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), 'templates')
|
||||
|
||||
UIAUTO_BUILD_SCRIPT = """#!/bin/bash
|
||||
|
||||
class_dir=bin/classes/com/arm/wlauto/uiauto
|
||||
base_class=`python -c "import os, wlauto; print os.path.join(os.path.dirname(wlauto.__file__), 'common', 'android', 'BaseUiAutomation.class')"`
|
||||
mkdir -p $$class_dir
|
||||
cp $$base_class $$class_dir
|
||||
|
||||
ant build
|
||||
|
||||
if [[ -f bin/${package_name}.jar ]]; then
|
||||
cp bin/${package_name}.jar ..
|
||||
fi
|
||||
"""
|
||||
|
||||
|
||||
class CreateSubcommand(object):
|
||||
|
||||
name = None
|
||||
help = None
|
||||
usage = None
|
||||
description = None
|
||||
epilog = None
|
||||
formatter_class = None
|
||||
|
||||
def __init__(self, logger, subparsers):
|
||||
self.logger = logger
|
||||
self.group = subparsers
|
||||
parser_params = dict(help=(self.help or self.description), usage=self.usage,
|
||||
description=format_body(textwrap.dedent(self.description), 80),
|
||||
epilog=self.epilog)
|
||||
if self.formatter_class:
|
||||
parser_params['formatter_class'] = self.formatter_class
|
||||
self.parser = subparsers.add_parser(self.name, **parser_params)
|
||||
init_argument_parser(self.parser) # propagate top-level options
|
||||
self.initialize()
|
||||
|
||||
def initialize(self):
|
||||
pass
|
||||
|
||||
|
||||
class CreateWorkloadSubcommand(CreateSubcommand):
|
||||
|
||||
name = 'workload'
|
||||
description = '''Create a new workload. By default, a basic workload template will be
|
||||
used but you can use options to specify a different template.'''
|
||||
|
||||
def initialize(self):
|
||||
self.parser.add_argument('name', metavar='NAME',
|
||||
help='Name of the workload to be created')
|
||||
self.parser.add_argument('-p', '--path', metavar='PATH', default=None,
|
||||
help='The location at which the workload will be created. If not specified, ' +
|
||||
'this defaults to "~/.workload_automation/workloads".')
|
||||
self.parser.add_argument('-f', '--force', action='store_true',
|
||||
help='Create the new workload even if a workload with the specified ' +
|
||||
'name already exists.')
|
||||
|
||||
template_group = self.parser.add_mutually_exclusive_group()
|
||||
template_group.add_argument('-A', '--android-benchmark', action='store_true',
|
||||
help='Use android benchmark template. This template allows you to specify ' +
|
||||
' an APK file that will be installed and run on the device. You should ' +
|
||||
' place the APK file into the workload\'s directory at the same level ' +
|
||||
'as the __init__.py.')
|
||||
template_group.add_argument('-U', '--ui-automation', action='store_true',
|
||||
help='Use UI automation template. This template generates a UI automation ' +
|
||||
'Android project as well as the Python class. This a more general ' +
|
||||
'version of the android benchmark template that makes no assumptions ' +
|
||||
'about the nature of your workload, apart from the fact that you need ' +
|
||||
'UI automation. If you need to install an APK, start an app on device, ' +
|
||||
'etc., you will need to do that explicitly in your code.')
|
||||
template_group.add_argument('-B', '--android-uiauto-benchmark', action='store_true',
|
||||
help='Use android uiauto benchmark template. This generates a UI automation ' +
|
||||
'project as well as a Python class. This template should be used ' +
|
||||
'if you have a APK file that needs to be run on the device. You ' +
|
||||
'should place the APK file into the workload\'s directory at the ' +
|
||||
'same level as the __init__.py.')
|
||||
|
||||
def execute(self, state, args): # pylint: disable=R0201
|
||||
where = args.path or 'local'
|
||||
check_name = not args.force
|
||||
|
||||
if args.android_benchmark:
|
||||
kind = 'android'
|
||||
elif args.ui_automation:
|
||||
kind = 'uiauto'
|
||||
elif args.android_uiauto_benchmark:
|
||||
kind = 'android_uiauto'
|
||||
else:
|
||||
kind = 'basic'
|
||||
|
||||
try:
|
||||
create_workload(args.name, kind, where, check_name)
|
||||
except CommandError, e:
|
||||
print "ERROR:", e
|
||||
|
||||
|
||||
class CreatePackageSubcommand(CreateSubcommand):
|
||||
|
||||
name = 'package'
|
||||
description = '''Create a new empty Python package for WA plugins. On installation,
|
||||
this package will "advertise" itself to WA so that Plugins with in it will
|
||||
be loaded by WA when it runs.'''
|
||||
|
||||
def initialize(self):
|
||||
self.parser.add_argument('name', metavar='NAME',
|
||||
help='Name of the package to be created')
|
||||
self.parser.add_argument('-p', '--path', metavar='PATH', default=None,
|
||||
help='The location at which the new pacakge will be created. If not specified, ' +
|
||||
'current working directory will be used.')
|
||||
self.parser.add_argument('-f', '--force', action='store_true',
|
||||
help='Create the new package even if a file or directory with the same name '
|
||||
'already exists at the specified location.')
|
||||
|
||||
def execute(self, args): # pylint: disable=R0201
|
||||
package_dir = args.path or os.path.abspath('.')
|
||||
template_path = os.path.join(TEMPLATES_DIR, 'setup.template')
|
||||
self.create_plugins_package(package_dir, args.name, template_path, args.force)
|
||||
|
||||
def create_plugins_package(self, location, name, setup_template_path, overwrite=False):
|
||||
package_path = os.path.join(location, name)
|
||||
if os.path.exists(package_path):
|
||||
if overwrite:
|
||||
self.logger.info('overwriting existing "{}"'.format(package_path))
|
||||
shutil.rmtree(package_path)
|
||||
else:
|
||||
raise CommandError('Location "{}" already exists.'.format(package_path))
|
||||
actual_package_path = os.path.join(package_path, name)
|
||||
os.makedirs(actual_package_path)
|
||||
setup_text = render_template(setup_template_path, {'package_name': name, 'user': getpass.getuser()})
|
||||
with open(os.path.join(package_path, 'setup.py'), 'w') as wfh:
|
||||
wfh.write(setup_text)
|
||||
touch(os.path.join(actual_package_path, '__init__.py'))
|
||||
|
||||
|
||||
class CreateAgendaSubcommand(CreateSubcommand):
|
||||
|
||||
name = 'agenda'
|
||||
description = """
|
||||
Create an agenda whith the specified plugins enabled. And parameters set to their
|
||||
default values.
|
||||
"""
|
||||
|
||||
def initialize(self):
|
||||
self.parser.add_argument('plugins', nargs='+',
|
||||
help='Plugins to be added')
|
||||
self.parser.add_argument('-i', '--iterations', type=int, default=1,
|
||||
help='Sets the number of iterations for all workloads')
|
||||
self.parser.add_argument('-r', '--include-runtime-params', action='store_true',
|
||||
help="""
|
||||
Adds runtime parameters to the global section of the generated
|
||||
agenda. Note: these do not have default values, so only name
|
||||
will be added. Also, runtime parameters are devices-specific, so
|
||||
a device must be specified (either in the list of plugins,
|
||||
or in the existing config).
|
||||
""")
|
||||
self.parser.add_argument('-o', '--output', metavar='FILE',
|
||||
help='Output file. If not specfied, STDOUT will be used instead.')
|
||||
|
||||
def execute(self, args): # pylint: disable=no-self-use,too-many-branches,too-many-statements
|
||||
loader = PluginLoader(packages=settings.plugin_packages,
|
||||
paths=settings.plugin_paths)
|
||||
agenda = OrderedDict()
|
||||
agenda['config'] = OrderedDict(instrumentation=[], result_processors=[])
|
||||
agenda['global'] = OrderedDict(iterations=args.iterations)
|
||||
agenda['workloads'] = []
|
||||
device = None
|
||||
device_config = None
|
||||
for name in args.plugins:
|
||||
extcls = loader.get_plugin_class(name)
|
||||
config = loader.get_default_config(name)
|
||||
del config['modules']
|
||||
|
||||
if extcls.kind == 'workload':
|
||||
entry = OrderedDict()
|
||||
entry['name'] = extcls.name
|
||||
if name != extcls.name:
|
||||
entry['label'] = name
|
||||
entry['params'] = config
|
||||
agenda['workloads'].append(entry)
|
||||
elif extcls.kind == 'device':
|
||||
if device is not None:
|
||||
raise ConfigError('Specifying multiple devices: {} and {}'.format(device.name, name))
|
||||
device = extcls
|
||||
device_config = config
|
||||
agenda['config']['device'] = name
|
||||
agenda['config']['device_config'] = config
|
||||
else:
|
||||
if extcls.kind == 'instrument':
|
||||
agenda['config']['instrumentation'].append(name)
|
||||
if extcls.kind == 'result_processor':
|
||||
agenda['config']['result_processors'].append(name)
|
||||
agenda['config'][name] = config
|
||||
|
||||
if args.include_runtime_params:
|
||||
if not device:
|
||||
if settings.device:
|
||||
device = loader.get_plugin_class(settings.device)
|
||||
device_config = loader.get_default_config(settings.device)
|
||||
else:
|
||||
raise ConfigError('-r option requires for a device to be in the list of plugins')
|
||||
rps = OrderedDict()
|
||||
for rp in device.runtime_parameters:
|
||||
if hasattr(rp, 'get_runtime_parameters'):
|
||||
# a core parameter needs to be expanded for each of the
|
||||
# device's cores, if they're avialable
|
||||
for crp in rp.get_runtime_parameters(device_config.get('core_names', [])):
|
||||
rps[crp.name] = None
|
||||
else:
|
||||
rps[rp.name] = None
|
||||
agenda['global']['runtime_params'] = rps
|
||||
|
||||
if args.output:
|
||||
wfh = open(args.output, 'w')
|
||||
else:
|
||||
wfh = sys.stdout
|
||||
yaml.dump(agenda, wfh, indent=4, default_flow_style=False)
|
||||
if args.output:
|
||||
wfh.close()
|
||||
|
||||
|
||||
class CreateCommand(Command):
|
||||
|
||||
name = 'create'
|
||||
description = '''Used to create various WA-related objects (see positional arguments list for what
|
||||
objects may be created).\n\nUse "wa create <object> -h" for object-specific arguments.'''
|
||||
formatter_class = argparse.RawDescriptionHelpFormatter
|
||||
subcmd_classes = [
|
||||
CreateWorkloadSubcommand,
|
||||
CreatePackageSubcommand,
|
||||
CreateAgendaSubcommand,
|
||||
]
|
||||
|
||||
def initialize(self, context):
|
||||
subparsers = self.parser.add_subparsers(dest='what')
|
||||
self.subcommands = [] # pylint: disable=W0201
|
||||
for subcmd_cls in self.subcmd_classes:
|
||||
subcmd = subcmd_cls(self.logger, subparsers)
|
||||
self.subcommands.append(subcmd)
|
||||
|
||||
def execute(self, args):
|
||||
for subcmd in self.subcommands:
|
||||
if subcmd.name == args.what:
|
||||
subcmd.execute(args)
|
||||
break
|
||||
else:
|
||||
raise CommandError('Not a valid create parameter: {}'.format(args.name))
|
||||
|
||||
|
||||
def create_workload(name, kind='basic', where='local', check_name=True, **kwargs):
|
||||
if check_name:
|
||||
extloader = PluginLoader(packages=settings.plugin_packages, paths=settings.plugin_paths)
|
||||
if name in [wl.name for wl in extloader.list_workloads()]:
|
||||
raise CommandError('Workload with name "{}" already exists.'.format(name))
|
||||
|
||||
class_name = get_class_name(name)
|
||||
if where == 'local':
|
||||
workload_dir = _d(os.path.join(settings.user_directory, 'workloads', name))
|
||||
else:
|
||||
workload_dir = _d(os.path.join(where, name))
|
||||
|
||||
if kind == 'basic':
|
||||
create_basic_workload(workload_dir, name, class_name, **kwargs)
|
||||
elif kind == 'uiauto':
|
||||
create_uiautomator_workload(workload_dir, name, class_name, **kwargs)
|
||||
elif kind == 'android':
|
||||
create_android_benchmark(workload_dir, name, class_name, **kwargs)
|
||||
elif kind == 'android_uiauto':
|
||||
create_android_uiauto_benchmark(workload_dir, name, class_name, **kwargs)
|
||||
else:
|
||||
raise CommandError('Unknown workload type: {}'.format(kind))
|
||||
|
||||
print 'Workload created in {}'.format(workload_dir)
|
||||
|
||||
|
||||
def create_basic_workload(path, name, class_name):
|
||||
source_file = os.path.join(path, '__init__.py')
|
||||
with open(source_file, 'w') as wfh:
|
||||
wfh.write(render_template('basic_workload', {'name': name, 'class_name': class_name}))
|
||||
|
||||
|
||||
def create_uiautomator_workload(path, name, class_name):
|
||||
uiauto_path = _d(os.path.join(path, 'uiauto'))
|
||||
create_uiauto_project(uiauto_path, name)
|
||||
source_file = os.path.join(path, '__init__.py')
|
||||
with open(source_file, 'w') as wfh:
|
||||
wfh.write(render_template('uiauto_workload', {'name': name, 'class_name': class_name}))
|
||||
|
||||
|
||||
def create_android_benchmark(path, name, class_name):
|
||||
source_file = os.path.join(path, '__init__.py')
|
||||
with open(source_file, 'w') as wfh:
|
||||
wfh.write(render_template('android_benchmark', {'name': name, 'class_name': class_name}))
|
||||
|
||||
|
||||
def create_android_uiauto_benchmark(path, name, class_name):
|
||||
uiauto_path = _d(os.path.join(path, 'uiauto'))
|
||||
create_uiauto_project(uiauto_path, name)
|
||||
source_file = os.path.join(path, '__init__.py')
|
||||
with open(source_file, 'w') as wfh:
|
||||
wfh.write(render_template('android_uiauto_benchmark', {'name': name, 'class_name': class_name}))
|
||||
|
||||
|
||||
def create_uiauto_project(path, name, target='1'):
|
||||
sdk_path = get_sdk_path()
|
||||
android_path = os.path.join(sdk_path, 'tools', 'android')
|
||||
package_name = 'com.arm.wlauto.uiauto.' + name.lower()
|
||||
|
||||
# ${ANDROID_HOME}/tools/android create uitest-project -n com.arm.wlauto.uiauto.linpack -t 1 -p ../test2
|
||||
command = '{} create uitest-project --name {} --target {} --path {}'.format(android_path,
|
||||
package_name,
|
||||
target,
|
||||
path)
|
||||
try:
|
||||
check_output(command, shell=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if 'is is not valid' in e.output:
|
||||
message = 'No Android SDK target found; have you run "{} update sdk" and download a platform?'
|
||||
raise CommandError(message.format(android_path))
|
||||
|
||||
build_script = os.path.join(path, 'build.sh')
|
||||
with open(build_script, 'w') as wfh:
|
||||
template = string.Template(UIAUTO_BUILD_SCRIPT)
|
||||
wfh.write(template.substitute({'package_name': package_name}))
|
||||
os.chmod(build_script, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||||
|
||||
source_file = _f(os.path.join(path, 'src',
|
||||
os.sep.join(package_name.split('.')[:-1]),
|
||||
'UiAutomation.java'))
|
||||
with open(source_file, 'w') as wfh:
|
||||
wfh.write(render_template('UiAutomation.java', {'name': name, 'package_name': package_name}))
|
||||
|
||||
|
||||
# Utility functions
|
||||
|
||||
def get_sdk_path():
|
||||
sdk_path = os.getenv('ANDROID_HOME')
|
||||
if not sdk_path:
|
||||
raise CommandError('Please set ANDROID_HOME environment variable to point to ' +
|
||||
'the locaton of Android SDK')
|
||||
return sdk_path
|
||||
|
||||
|
||||
def get_class_name(name, postfix=''):
|
||||
name = identifier(name)
|
||||
return ''.join(map(capitalize, name.split('_'))) + postfix
|
||||
|
||||
|
||||
def render_template(name, params):
|
||||
filepath = os.path.join(TEMPLATES_DIR, name)
|
||||
with open(filepath) as fh:
|
||||
text = fh.read()
|
||||
template = string.Template(text)
|
||||
return template.substitute(params)
|
||||
|
||||
|
||||
def touch(path):
|
||||
with open(path, 'w') as _:
|
||||
pass
|
@ -1,74 +0,0 @@
|
||||
# Copyright 2014-2015 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 wlauto import PluginLoader, Command, settings
|
||||
from wlauto.utils.formatter import DescriptionListFormatter
|
||||
from wlauto.utils.doc import get_summary
|
||||
from wlauto.core import pluginloader
|
||||
|
||||
class ListCommand(Command):
|
||||
|
||||
name = 'list'
|
||||
description = 'List available WA plugins with a short description of each.'
|
||||
|
||||
def initialize(self, context):
|
||||
plugin_types = ['{}s'.format(name) for name in pluginloader.kinds]
|
||||
self.parser.add_argument('kind', metavar='KIND',
|
||||
help=('Specify the kind of plugin to list. Must be '
|
||||
'one of: {}'.format(', '.join(plugin_types))),
|
||||
choices=plugin_types)
|
||||
self.parser.add_argument('-n', '--name', help='Filter results by the name specified')
|
||||
self.parser.add_argument('-o', '--packaged-only', action='store_true',
|
||||
help='''
|
||||
Only list plugins packaged with WA itself. Do not list plugins
|
||||
installed locally or from other packages.
|
||||
''')
|
||||
self.parser.add_argument('-p', '--platform', help='Only list results that are supported by '
|
||||
'the specified platform')
|
||||
|
||||
def execute(self, state, args):
|
||||
filters = {}
|
||||
if args.name:
|
||||
filters['name'] = args.name
|
||||
|
||||
results = pluginloader.list_plugins(args.kind[:-1])
|
||||
if filters or args.platform:
|
||||
filtered_results = []
|
||||
for result in results:
|
||||
passed = True
|
||||
for k, v in filters.iteritems():
|
||||
if getattr(result, k) != v:
|
||||
passed = False
|
||||
break
|
||||
if passed and args.platform:
|
||||
passed = check_platform(result, args.platform)
|
||||
if passed:
|
||||
filtered_results.append(result)
|
||||
else: # no filters specified
|
||||
filtered_results = results
|
||||
|
||||
if filtered_results:
|
||||
output = DescriptionListFormatter()
|
||||
for result in sorted(filtered_results, key=lambda x: x.name):
|
||||
output.add_item(get_summary(result), result.name)
|
||||
print output.format_data()
|
||||
|
||||
|
||||
def check_platform(plugin, platform):
|
||||
supported_platforms = getattr(plugin, 'supported_platforms', [])
|
||||
if supported_platforms:
|
||||
return platform in supported_platforms
|
||||
return True
|
@ -1,217 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
from wlauto import Command, settings
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.common.resources import Executable
|
||||
from wlauto.core.resource import NO_ONE
|
||||
from wlauto.core.resolver import ResourceResolver
|
||||
from wlauto.core.configuration import RunConfiguration
|
||||
from wlauto.common.android.workload import ApkWorkload
|
||||
|
||||
|
||||
class RecordCommand(Command):
|
||||
|
||||
name = 'record'
|
||||
description = '''Performs a revent recording
|
||||
|
||||
This command helps making revent recordings. It will automatically
|
||||
deploy revent and even has the option of automatically opening apps.
|
||||
|
||||
Revent allows you to record raw inputs such as screen swipes or button presses.
|
||||
This can be useful for recording inputs for workloads such as games that don't
|
||||
have XML UI layouts that can be used with UIAutomator. As a drawback from this,
|
||||
revent recordings are specific to the device type they were recorded on.
|
||||
|
||||
WA uses two parts to the names of revent recordings in the format,
|
||||
{device_name}.{suffix}.revent.
|
||||
|
||||
- device_name can either be specified manually with the ``-d`` argument or
|
||||
it can be automatically determined. On Android device it will be obtained
|
||||
from ``build.prop``, on Linux devices it is obtained from ``/proc/device-tree/model``.
|
||||
- suffix is used by WA to determine which part of the app execution the
|
||||
recording is for, currently these are either ``setup`` or ``run``. This
|
||||
should be specified with the ``-s`` argument.
|
||||
'''
|
||||
|
||||
def initialize(self, context):
|
||||
self.context = context
|
||||
self.parser.add_argument('-d', '--device', help='The name of the device')
|
||||
self.parser.add_argument('-o', '--output', help='Directory to save the recording in')
|
||||
|
||||
# Need validation
|
||||
self.parser.add_argument('-s', '--suffix', help='The suffix of the revent file, e.g. ``setup``')
|
||||
self.parser.add_argument('-C', '--clear', help='Clear app cache before launching it',
|
||||
action="store_true")
|
||||
|
||||
group = self.parser.add_mutually_exclusive_group(required=False)
|
||||
group.add_argument('-p', '--package', help='Package to launch before recording')
|
||||
group.add_argument('-w', '--workload', help='Name of a revent workload (mostly games)')
|
||||
|
||||
# Validate command options
|
||||
def validate_args(self, args):
|
||||
if args.clear and not (args.package or args.workload):
|
||||
self.logger.error("Package/Workload must be specified if you want to clear cache")
|
||||
self.parser.print_help()
|
||||
sys.exit()
|
||||
if args.workload and args.suffix:
|
||||
self.logger.error("cannot specify manual suffixes for workloads")
|
||||
self.parser.print_help()
|
||||
sys.exit()
|
||||
if args.suffix:
|
||||
args.suffix += "."
|
||||
|
||||
# pylint: disable=W0201
|
||||
def execute(self, state, args):
|
||||
self.validate_args(args)
|
||||
self.logger.info("Connecting to device...")
|
||||
|
||||
# Setup config
|
||||
self.config = RunConfiguration(pluginloader)
|
||||
for filepath in settings.config_paths:
|
||||
self.config.load_config(filepath)
|
||||
self.config.set_agenda(Agenda())
|
||||
self.config.finalize()
|
||||
|
||||
# Setup device
|
||||
self.device_manager = pluginloader.get_manager(self.config.device)
|
||||
self.device_manager.validate()
|
||||
self.device_manager.connect()
|
||||
context = LightContext(self.config, self.device_manager)
|
||||
self.device_manager.initialize(context)
|
||||
self.device = self.device_manager.target
|
||||
if args.device:
|
||||
self.device_name = args.device
|
||||
else:
|
||||
self.device_name = self.device.model
|
||||
|
||||
# Install Revent
|
||||
host_binary = context.resolver.get(Executable(NO_ONE, self.device.abi, 'revent'))
|
||||
self.target_binary = self.device.install_if_needed(host_binary)
|
||||
|
||||
if args.workload:
|
||||
self.workload_record(args, context)
|
||||
elif args.package:
|
||||
self.package_record(args, context)
|
||||
else:
|
||||
self.manual_record(args, context)
|
||||
|
||||
def manual_record(self, args, context):
|
||||
revent_file = self.device.get_workpath('{}.{}revent'.format(self.device_name, args.suffix or ""))
|
||||
self._record(revent_file, "", args.output)
|
||||
|
||||
def package_record(self, args, context):
|
||||
revent_file = self.device.get_workpath('{}.{}revent'.format(self.device_name, args.suffix or ""))
|
||||
if args.clear:
|
||||
self.device.execute("pm clear {}".format(args.package))
|
||||
|
||||
self.logger.info("Starting {}".format(args.package))
|
||||
self.device.execute('monkey -p {} -c android.intent.category.LAUNCHER 1'.format(args.package))
|
||||
|
||||
self._record(revent_file, "", args.output)
|
||||
|
||||
def workload_record(self, args, context):
|
||||
setup_file = self.device.get_workpath('{}.setup.revent'.format(self.device_name))
|
||||
run_file = self.device.get_workpath('{}.run.revent'.format(self.device_name))
|
||||
|
||||
self.logger.info("Deploying {}".format(args.workload))
|
||||
workload = pluginloader.get_workload(args.workload, self.device)
|
||||
workload.apk_init_resources(context)
|
||||
workload.initialize_package(context)
|
||||
workload.do_post_install(context)
|
||||
workload.start_activity()
|
||||
|
||||
if args.clear:
|
||||
workload.reset(context)
|
||||
|
||||
self._record(setup_file, " SETUP",
|
||||
args.output or os.path.join(workload.dependencies_directory, 'revent_files'))
|
||||
self._record(run_file, " RUN",
|
||||
args.output or os.path.join(workload.dependencies_directory, 'revent_files'))
|
||||
|
||||
self.logger.info("Tearing down {}".format(args.workload))
|
||||
workload.apk_teardown(context)
|
||||
|
||||
def _record(self, revent_file, name, output_path):
|
||||
self.logger.info("Press Enter when you are ready to record{}...".format(name))
|
||||
raw_input("")
|
||||
command = "{} record -t 100000 -s {}".format(self.target_binary, revent_file)
|
||||
self.device.kick_off(command)
|
||||
|
||||
self.logger.info("Press Enter when you have finished recording {}...".format(name))
|
||||
raw_input("")
|
||||
self.device.killall("revent")
|
||||
|
||||
output_path = output_path or os.getcwdu()
|
||||
if not os.path.isdir(output_path):
|
||||
os.mkdirs(output_path)
|
||||
|
||||
revent_file_name = self.device.path.basename(revent_file)
|
||||
host_path = os.path.join(output_path, revent_file_name)
|
||||
if os.path.exists(host_path):
|
||||
self.logger.info("Revent file '{}' already exists, overwrite? [y/n]".format(revent_file_name))
|
||||
if raw_input("") == "y":
|
||||
os.remove(host_path)
|
||||
else:
|
||||
self.logger.warning("Did not pull and overwrite '{}'".format(revent_file_name))
|
||||
return
|
||||
self.logger.info("Pulling '{}' from device".format(self.device.path.basename(revent_file)))
|
||||
self.device.pull(revent_file, output_path)
|
||||
|
||||
class ReplayCommand(RecordCommand):
|
||||
|
||||
name = 'replay'
|
||||
description = '''Replay a revent recording
|
||||
|
||||
Revent allows you to record raw inputs such as screen swipes or button presses.
|
||||
See ``wa show record`` to see how to make an revent recording.
|
||||
'''
|
||||
|
||||
def initialize(self, context):
|
||||
self.context = context
|
||||
self.parser.add_argument('revent', help='The name of the file to replay')
|
||||
self.parser.add_argument('-p', '--package', help='Package to launch before recording')
|
||||
self.parser.add_argument('-C', '--clear', help='Clear app cache before launching it',
|
||||
action="store_true")
|
||||
|
||||
|
||||
# pylint: disable=W0201
|
||||
def run(self, args):
|
||||
self.logger.info("Pushing file to device")
|
||||
self.device.push(args.revent, self.device.working_directory)
|
||||
revent_file = self.device.path.join(self.device.working_directory, os.path.split(args.revent)[1])
|
||||
|
||||
if args.clear:
|
||||
self.device.execute("pm clear {}".format(args.package))
|
||||
|
||||
if args.package:
|
||||
self.logger.info("Starting {}".format(args.package))
|
||||
self.device.execute('monkey -p {} -c android.intent.category.LAUNCHER 1'.format(args.package))
|
||||
|
||||
command = "{} replay {}".format(self.target_binary, revent_file)
|
||||
self.device.execute(command)
|
||||
self.logger.info("Finished replay")
|
||||
|
||||
|
||||
# Used to satisfy the API
|
||||
class LightContext(object):
|
||||
def __init__(self, config, device_manager):
|
||||
self.resolver = ResourceResolver(config)
|
||||
self.resolver.load()
|
||||
self.device_manager = device_manager
|
@ -1,123 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
import wlauto
|
||||
from wlauto import Command, settings
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.core.configuration import RunConfiguration
|
||||
from wlauto.core.configuration.parsers import AgendaParser, ConfigParser
|
||||
from wlauto.core.execution import Executor
|
||||
from wlauto.core.output import init_wa_output
|
||||
from wlauto.core.version import get_wa_version
|
||||
from wlauto.exceptions import NotFoundError, ConfigError
|
||||
from wlauto.utils.log import add_log_file
|
||||
from wlauto.utils.types import toggle_set
|
||||
|
||||
|
||||
class RunCommand(Command):
|
||||
|
||||
name = 'run'
|
||||
description = 'Execute automated workloads on a remote device and process the resulting output.'
|
||||
|
||||
def initialize(self, context):
|
||||
self.parser.add_argument('agenda', metavar='AGENDA',
|
||||
help="""
|
||||
Agenda for this workload automation run. This defines which
|
||||
workloads will be executed, how many times, with which
|
||||
tunables, etc. See example agendas in {} for an example of
|
||||
how this file should be structured.
|
||||
""".format(os.path.dirname(wlauto.__file__)))
|
||||
self.parser.add_argument('-d', '--output-directory', metavar='DIR', default=None,
|
||||
help="""
|
||||
Specify a directory where the output will be generated. If
|
||||
the directory already exists, the script will abort unless -f
|
||||
option (see below) is used, in which case the contents of the
|
||||
directory will be overwritten. If this option is not specified,
|
||||
then {} will be used instead.
|
||||
""".format(settings.default_output_directory))
|
||||
self.parser.add_argument('-f', '--force', action='store_true',
|
||||
help="""
|
||||
Overwrite output directory if it exists. By default, the script
|
||||
will abort in this situation to prevent accidental data loss.
|
||||
""")
|
||||
self.parser.add_argument('-i', '--id', action='append', dest='only_run_ids', metavar='ID',
|
||||
help="""
|
||||
Specify a workload spec ID from an agenda to run. If this is
|
||||
specified, only that particular spec will be run, and other
|
||||
workloads in the agenda will be ignored. This option may be
|
||||
used to specify multiple IDs.
|
||||
""")
|
||||
self.parser.add_argument('--disable', action='append', dest='instruments_to_disable',
|
||||
default=[],
|
||||
metavar='INSTRUMENT', help="""
|
||||
Specify an instrument to disable from the command line. This
|
||||
equivalent to adding "~{metavar}" to the instrumentation list in
|
||||
the agenda. This can be used to temporarily disable a troublesome
|
||||
instrument for a particular run without introducing permanent
|
||||
change to the config (which one might then forget to revert).
|
||||
This option may be specified multiple times.
|
||||
""")
|
||||
|
||||
def execute(self, config, args):
|
||||
output = self.set_up_output_directory(config, args)
|
||||
add_log_file(output.logfile)
|
||||
|
||||
self.logger.debug('Version: {}'.format(get_wa_version()))
|
||||
self.logger.debug('Command Line: {}'.format(' '.join(sys.argv)))
|
||||
|
||||
disabled_instruments = toggle_set(["~{}".format(i)
|
||||
for i in args.instruments_to_disable])
|
||||
config.jobs_config.disable_instruments(disabled_instruments)
|
||||
config.jobs_config.only_run_ids(args.only_run_ids)
|
||||
|
||||
parser = AgendaParser()
|
||||
if os.path.isfile(args.agenda):
|
||||
parser.load_from_path(config, args.agenda)
|
||||
shutil.copy(args.agenda, output.raw_config_dir)
|
||||
else:
|
||||
try:
|
||||
pluginloader.get_plugin_class(args.agenda, kind='workload')
|
||||
agenda = {'workloads': [{'name': args.agenda}]}
|
||||
parser.load(config, agenda, 'CMDLINE_ARGS')
|
||||
except NotFoundError:
|
||||
msg = 'Agenda file "{}" does not exist, and there no workload '\
|
||||
'with that name.\nYou can get a list of available '\
|
||||
'by running "wa list workloads".'
|
||||
raise ConfigError(msg.format(args.agenda))
|
||||
|
||||
executor = Executor()
|
||||
executor.execute(config, output)
|
||||
|
||||
def set_up_output_directory(self, config, args):
|
||||
if args.output_directory:
|
||||
output_directory = args.output_directory
|
||||
else:
|
||||
output_directory = settings.default_output_directory
|
||||
self.logger.debug('Using output directory: {}'.format(output_directory))
|
||||
try:
|
||||
return init_wa_output(output_directory, config, args.force)
|
||||
except RuntimeError as e:
|
||||
if 'path exists' in str(e):
|
||||
msg = 'Output directory "{}" exists.\nPlease specify another '\
|
||||
'location, or use -f option to overwrite.'
|
||||
self.logger.critical(msg.format(output_directory))
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise e
|
@ -1,114 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
from cStringIO import StringIO
|
||||
|
||||
from wlauto import Command
|
||||
from wlauto.core.configuration import settings
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.utils.doc import (get_summary, get_description, get_type_name, format_column, format_body,
|
||||
format_paragraph, indent, strip_inlined_text)
|
||||
from wlauto.utils.misc import get_pager
|
||||
from wlauto.utils.terminalsize import get_terminal_size
|
||||
|
||||
|
||||
class ShowCommand(Command):
|
||||
|
||||
name = 'show'
|
||||
|
||||
description = """
|
||||
Display documentation for the specified plugin (workload, instrument, etc.).
|
||||
"""
|
||||
|
||||
def initialize(self, context):
|
||||
self.parser.add_argument('name', metavar='EXTENSION',
|
||||
help='''The name of the plugin for which information will
|
||||
be shown.''')
|
||||
|
||||
def execute(self, state, args):
|
||||
# pylint: disable=unpacking-non-sequence
|
||||
plugin = pluginloader.get_plugin_class(args.name)
|
||||
out = StringIO()
|
||||
term_width, term_height = get_terminal_size()
|
||||
format_plugin(plugin, out, term_width)
|
||||
text = out.getvalue()
|
||||
pager = get_pager()
|
||||
if len(text.split('\n')) > term_height and pager:
|
||||
try:
|
||||
sp = subprocess.Popen(pager, stdin=subprocess.PIPE)
|
||||
sp.communicate(text)
|
||||
except OSError:
|
||||
self.logger.warning('Could not use PAGER "{}"'.format(pager))
|
||||
sys.stdout.write(text)
|
||||
else:
|
||||
sys.stdout.write(text)
|
||||
|
||||
|
||||
def format_plugin(plugin, out, width):
|
||||
format_plugin_name(plugin, out)
|
||||
out.write('\n')
|
||||
format_plugin_summary(plugin, out, width)
|
||||
out.write('\n')
|
||||
if hasattr(plugin, 'supported_platforms'):
|
||||
format_supported_platforms(plugin, out, width)
|
||||
out.write('\n')
|
||||
if plugin.parameters:
|
||||
format_plugin_parameters(plugin, out, width)
|
||||
out.write('\n')
|
||||
format_plugin_description(plugin, out, width)
|
||||
|
||||
|
||||
def format_plugin_name(plugin, out):
|
||||
out.write('\n{}\n'.format(plugin.name))
|
||||
|
||||
|
||||
def format_plugin_summary(plugin, out, width):
|
||||
out.write('{}\n'.format(format_body(strip_inlined_text(get_summary(plugin)), width)))
|
||||
|
||||
|
||||
def format_supported_platforms(plugin, out, width):
|
||||
text = 'supported on: {}'.format(', '.join(plugin.supported_platforms))
|
||||
out.write('{}\n'.format(format_body(text, width)))
|
||||
|
||||
|
||||
def format_plugin_description(plugin, out, width):
|
||||
# skip the initial paragraph of multi-paragraph description, as already
|
||||
# listed above.
|
||||
description = get_description(plugin).split('\n\n', 1)[-1]
|
||||
out.write('{}\n'.format(format_body(strip_inlined_text(description), width)))
|
||||
|
||||
|
||||
def format_plugin_parameters(plugin, out, width, shift=4):
|
||||
out.write('parameters:\n\n')
|
||||
param_texts = []
|
||||
for param in plugin.parameters:
|
||||
description = format_paragraph(strip_inlined_text(param.description or ''), width - shift)
|
||||
param_text = '{}'.format(param.name)
|
||||
if param.mandatory:
|
||||
param_text += " (MANDATORY)"
|
||||
param_text += '\n{}\n'.format(description)
|
||||
param_text += indent('type: {}\n'.format(get_type_name(param.kind)))
|
||||
if param.allowed_values:
|
||||
param_text += indent('allowed values: {}\n'.format(', '.join(map(str, param.allowed_values))))
|
||||
elif param.constraint:
|
||||
param_text += indent('constraint: {}\n'.format(get_type_name(param.constraint)))
|
||||
if param.default is not None:
|
||||
param_text += indent('default: {}\n'.format(param.default))
|
||||
param_texts.append(indent(param_text, shift))
|
||||
|
||||
out.write(format_column('\n'.join(param_texts), width))
|
@ -1,25 +0,0 @@
|
||||
package ${package_name};
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
// Import the uiautomator libraries
|
||||
import com.android.uiautomator.core.UiObject;
|
||||
import com.android.uiautomator.core.UiObjectNotFoundException;
|
||||
import com.android.uiautomator.core.UiScrollable;
|
||||
import com.android.uiautomator.core.UiSelector;
|
||||
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
|
||||
|
||||
import com.arm.wlauto.uiauto.BaseUiAutomation;
|
||||
|
||||
public class UiAutomation extends BaseUiAutomation {
|
||||
|
||||
public static String TAG = "${name}";
|
||||
|
||||
public void runUiAutomation() throws Exception {
|
||||
// UI Automation code goes here
|
||||
}
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
from wlauto import AndroidBenchmark, Parameter
|
||||
|
||||
|
||||
class ${class_name}(AndroidBenchmark):
|
||||
|
||||
name = '${name}'
|
||||
# NOTE: Please do not leave these comments in the code.
|
||||
#
|
||||
# Replace with the package for the app in the APK file.
|
||||
package = 'com.foo.bar'
|
||||
# Replace with the full path to the activity to run.
|
||||
activity = '.RunBuzz'
|
||||
description = "This is an placeholder description"
|
||||
|
||||
parameters = [
|
||||
# Workload parameters go here e.g.
|
||||
Parameter('Example parameter', kind=int, allowed_values=[1,2,3], default=1, override=True, mandatory=False,
|
||||
description='This is an example parameter')
|
||||
]
|
||||
|
||||
def run(self, context):
|
||||
pass
|
||||
|
||||
def update_result(self, context):
|
||||
super(${class_name}, self).update_result(context)
|
||||
# process results and add them using
|
||||
# context.result.add_metric
|
@ -1,24 +0,0 @@
|
||||
from wlauto import AndroidUiAutoBenchmark, Parameter
|
||||
|
||||
|
||||
class ${class_name}(AndroidUiAutoBenchmark):
|
||||
|
||||
name = '${name}'
|
||||
# NOTE: Please do not leave these comments in the code.
|
||||
#
|
||||
# Replace with the package for the app in the APK file.
|
||||
package = 'com.foo.bar'
|
||||
# Replace with the full path to the activity to run.
|
||||
activity = '.RunBuzz'
|
||||
description = "This is an placeholder description"
|
||||
|
||||
parameters = [
|
||||
# Workload parameters go here e.g.
|
||||
Parameter('Example parameter', kind=int, allowed_values=[1,2,3], default=1, override=True, mandatory=False,
|
||||
description='This is an example parameter')
|
||||
]
|
||||
|
||||
def update_result(self, context):
|
||||
super(${class_name}, self).update_result(context)
|
||||
# process results and add them using
|
||||
# context.result.add_metric
|
@ -1,28 +0,0 @@
|
||||
from wlauto import Workload, Parameter
|
||||
|
||||
|
||||
class ${class_name}(Workload):
|
||||
|
||||
name = '${name}'
|
||||
description = "This is an placeholder description"
|
||||
|
||||
parameters = [
|
||||
# Workload parameters go here e.g.
|
||||
Parameter('Example parameter', kind=int, allowed_values=[1,2,3], default=1, override=True, mandatory=False,
|
||||
description='This is an example parameter')
|
||||
]
|
||||
|
||||
def setup(self, context):
|
||||
pass
|
||||
|
||||
def run(self, context):
|
||||
pass
|
||||
|
||||
def update_result(self, context):
|
||||
pass
|
||||
|
||||
def teardown(self, context):
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
pass
|
@ -1,102 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from multiprocessing import Process
|
||||
|
||||
try:
|
||||
from setuptools.command.install import install as orig_install
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils.command.install import install as orig_install
|
||||
from distutils.core import setup
|
||||
|
||||
try:
|
||||
import pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
|
||||
warnings.filterwarnings('ignore', "Unknown distribution option: 'install_requires'")
|
||||
|
||||
try:
|
||||
os.remove('MANIFEST')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
packages = []
|
||||
data_files = {}
|
||||
source_dir = os.path.dirname(__file__)
|
||||
for root, dirs, files in os.walk('$package_name'):
|
||||
rel_dir = os.path.relpath(root, source_dir)
|
||||
data = []
|
||||
if '__init__.py' in files:
|
||||
for f in files:
|
||||
if os.path.splitext(f)[1] not in ['.py', '.pyc', '.pyo']:
|
||||
data.append(f)
|
||||
package_name = rel_dir.replace(os.sep, '.')
|
||||
package_dir = root
|
||||
packages.append(package_name)
|
||||
data_files[package_name] = data
|
||||
else:
|
||||
# use previous package name
|
||||
filepaths = [os.path.join(root, f) for f in files]
|
||||
data_files[package_name].extend([os.path.relpath(f, package_dir) for f in filepaths])
|
||||
|
||||
params = dict(
|
||||
name='$package_name',
|
||||
version='0.0.1',
|
||||
packages=packages,
|
||||
package_data=data_files,
|
||||
url='N/A',
|
||||
maintainer='$user',
|
||||
maintainer_email='$user@example.com',
|
||||
install_requires=[
|
||||
'wlauto',
|
||||
],
|
||||
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'License :: Other/Proprietary License',
|
||||
'Operating System :: Unix',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def update_wa_packages():
|
||||
sudo_user = os.getenv('SUDO_USER')
|
||||
if sudo_user:
|
||||
user_entry = pwd.getpwnam(sudo_user)
|
||||
os.setgid(user_entry.pw_gid)
|
||||
os.setuid(user_entry.pw_uid)
|
||||
env_root = os.getenv('WA_USER_DIRECTORY', os.path.join(os.path.expanduser('~'), '.workload_automation'))
|
||||
if not os.path.isdir(env_root):
|
||||
os.makedirs(env_root)
|
||||
wa_packages_file = os.path.join(env_root, 'packages')
|
||||
if os.path.isfile(wa_packages_file):
|
||||
with open(wa_packages_file, 'r') as wfh:
|
||||
package_list = wfh.read().split()
|
||||
if params['name'] not in package_list:
|
||||
package_list.append(params['name'])
|
||||
else: # no existing package file
|
||||
package_list = [params['name']]
|
||||
with open(wa_packages_file, 'w') as wfh:
|
||||
wfh.write('\n'.join(package_list))
|
||||
|
||||
|
||||
class install(orig_install):
|
||||
|
||||
def run(self):
|
||||
orig_install.run(self)
|
||||
# Must be done in a separate process because will drop privileges if
|
||||
# sudo, and won't be able to reacquire them.
|
||||
p = Process(target=update_wa_packages)
|
||||
p.start()
|
||||
p.join()
|
||||
|
||||
|
||||
params['cmdclass'] = {'install': install}
|
||||
|
||||
|
||||
setup(**params)
|
@ -1,35 +0,0 @@
|
||||
from wlauto import UiAutomatorWorkload, Parameter
|
||||
|
||||
|
||||
class ${class_name}(UiAutomatorWorkload):
|
||||
|
||||
name = '${name}'
|
||||
description = "This is an placeholder description"
|
||||
|
||||
parameters = [
|
||||
# Workload parameters go here e.g.
|
||||
Parameter('Example parameter', kind=int, allowed_values=[1,2,3], default=1, override=True, mandatory=False,
|
||||
description='This is an example parameter')
|
||||
]
|
||||
|
||||
def setup(self, context):
|
||||
super(${class_name}, self).setup(context)
|
||||
# Perform any necessary setup before starting the UI automation
|
||||
# e.g. copy files to the device, start apps, reset logs, etc.
|
||||
|
||||
|
||||
def update_result(self, context):
|
||||
pass
|
||||
# Process workload execution artifacts to extract metrics
|
||||
# and add them to the run result using
|
||||
# context.result.add_metric()
|
||||
|
||||
def teardown(self, context):
|
||||
super(${class_name}, self).teardown(context)
|
||||
# Preform any necessary cleanup
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
# Validate inter-parameter assumptions etc
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
Binary file not shown.
@ -1,16 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
@ -1,40 +0,0 @@
|
||||
# Copyright 2014-2015 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 wlauto.common.resources import FileResource
|
||||
|
||||
|
||||
class ReventFile(FileResource):
|
||||
|
||||
name = 'revent'
|
||||
|
||||
def __init__(self, owner, stage):
|
||||
super(ReventFile, self).__init__(owner)
|
||||
self.stage = stage
|
||||
|
||||
|
||||
class JarFile(FileResource):
|
||||
|
||||
name = 'jar'
|
||||
|
||||
|
||||
class ApkFile(FileResource):
|
||||
|
||||
name = 'apk'
|
||||
|
||||
def __init__(self, owner, version):
|
||||
super(ApkFile, self).__init__(owner)
|
||||
self.version = version
|
@ -1,506 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from wlauto.core.plugin import Parameter
|
||||
from wlauto.core.workload import Workload
|
||||
from wlauto.core.resource import NO_ONE
|
||||
from wlauto.common.resources import PluginAsset, Executable
|
||||
from wlauto.exceptions import WorkloadError, ResourceError, ConfigError
|
||||
from wlauto.utils.android import ApkInfo, ANDROID_NORMAL_PERMISSIONS
|
||||
from wlauto.utils.types import boolean
|
||||
import wlauto.common.android.resources
|
||||
|
||||
|
||||
DELAY = 5
|
||||
|
||||
|
||||
class UiAutomatorWorkload(Workload):
|
||||
"""
|
||||
Base class for all workloads that rely on a UI Automator JAR file.
|
||||
|
||||
This class should be subclassed by workloads that rely on android UiAutomator
|
||||
to work. This class handles transferring the UI Automator JAR file to the device
|
||||
and invoking it to run the workload. By default, it will look for the JAR file in
|
||||
the same directory as the .py file for the workload (this can be changed by overriding
|
||||
the ``uiauto_file`` property in the subclassing workload).
|
||||
|
||||
To inintiate UI Automation, the fully-qualified name of the Java class and the
|
||||
corresponding method name are needed. By default, the package part of the class name
|
||||
is derived from the class file, and class and method names are ``UiAutomation``
|
||||
and ``runUiAutomaton`` respectively. If you have generated the boilder plate for the
|
||||
UiAutomatior code using ``create_workloads`` utility, then everything should be named
|
||||
correctly. If you're creating the Java project manually, you need to make sure the names
|
||||
match what is expected, or you could override ``uiauto_package``, ``uiauto_class`` and
|
||||
``uiauto_method`` class attributes with the value that match your Java code.
|
||||
|
||||
You can also pass parameters to the JAR file. To do this add the parameters to
|
||||
``self.uiauto_params`` dict inside your class's ``__init__`` or ``setup`` methods.
|
||||
|
||||
"""
|
||||
|
||||
supported_platforms = ['android']
|
||||
|
||||
uiauto_package = ''
|
||||
uiauto_class = 'UiAutomation'
|
||||
uiauto_method = 'runUiAutomation'
|
||||
|
||||
# Can be overidden by subclasses to adjust to run time of specific
|
||||
# benchmarks.
|
||||
run_timeout = 4 * 60 # seconds
|
||||
|
||||
def __init__(self, device, _call_super=True, **kwargs): # pylint: disable=W0613
|
||||
if _call_super:
|
||||
super(UiAutomatorWorkload, self).__init__(device, **kwargs)
|
||||
self.uiauto_file = None
|
||||
self.device_uiauto_file = None
|
||||
self.command = None
|
||||
self.uiauto_params = {}
|
||||
|
||||
def init_resources(self, context):
|
||||
self.uiauto_file = context.resolver.get(wlauto.common.android.resources.JarFile(self))
|
||||
if not self.uiauto_file:
|
||||
raise ResourceError('No UI automation JAR file found for workload {}.'.format(self.name))
|
||||
self.device_uiauto_file = self.device.path.join(self.device.working_directory,
|
||||
os.path.basename(self.uiauto_file))
|
||||
if not self.uiauto_package:
|
||||
self.uiauto_package = os.path.splitext(os.path.basename(self.uiauto_file))[0]
|
||||
|
||||
def setup(self, context):
|
||||
method_string = '{}.{}#{}'.format(self.uiauto_package, self.uiauto_class, self.uiauto_method)
|
||||
params_dict = self.uiauto_params
|
||||
params_dict['workdir'] = self.device.working_directory
|
||||
params = ''
|
||||
for k, v in self.uiauto_params.iteritems():
|
||||
params += ' -e {} {}'.format(k, v)
|
||||
self.command = 'uiautomator runtest {}{} -c {}'.format(self.device_uiauto_file, params, method_string)
|
||||
self.device.push(self.uiauto_file, self.device_uiauto_file)
|
||||
self.device.killall('uiautomator')
|
||||
|
||||
def run(self, context):
|
||||
result = self.device.execute(self.command, self.run_timeout)
|
||||
if 'FAILURE' in result:
|
||||
raise WorkloadError(result)
|
||||
else:
|
||||
self.logger.debug(result)
|
||||
time.sleep(DELAY)
|
||||
|
||||
def update_result(self, context):
|
||||
pass
|
||||
|
||||
def teardown(self, context):
|
||||
self.device.remove(self.device_uiauto_file)
|
||||
|
||||
def validate(self):
|
||||
if not self.uiauto_file:
|
||||
raise WorkloadError('No UI automation JAR file found for workload {}.'.format(self.name))
|
||||
if not self.uiauto_package:
|
||||
raise WorkloadError('No UI automation package specified for workload {}.'.format(self.name))
|
||||
|
||||
|
||||
class ApkWorkload(Workload):
|
||||
"""
|
||||
A workload based on an APK file.
|
||||
|
||||
Defines the following attributes:
|
||||
|
||||
:package: The package name of the app. This is usually a Java-style name of the form
|
||||
``com.companyname.appname``.
|
||||
:activity: This is the initial activity of the app. This will be used to launch the
|
||||
app during the setup.
|
||||
:view: The class of the main view pane of the app. This needs to be defined in order
|
||||
to collect SurfaceFlinger-derived statistics (such as FPS) for the app, but
|
||||
may otherwise be left as ``None``.
|
||||
:install_timeout: Timeout for the installation of the APK. This may vary wildly based on
|
||||
the size and nature of a specific APK, and so should be defined on
|
||||
per-workload basis.
|
||||
|
||||
.. note:: To a lesser extent, this will also vary based on the the
|
||||
device and the nature of adb connection (USB vs Ethernet),
|
||||
so, as with all timeouts, so leeway must be included in
|
||||
the specified value.
|
||||
|
||||
.. note:: Both package and activity for a workload may be obtained from the APK using
|
||||
the ``aapt`` tool that comes with the ADT (Android Developemnt Tools) bundle.
|
||||
|
||||
"""
|
||||
package = None
|
||||
activity = None
|
||||
view = None
|
||||
supported_platforms = ['android']
|
||||
|
||||
parameters = [
|
||||
Parameter('install_timeout', kind=int, default=300,
|
||||
description='Timeout for the installation of the apk.'),
|
||||
Parameter('check_apk', kind=boolean, default=True,
|
||||
description='''
|
||||
Discover the APK for this workload on the host, and check that
|
||||
the version matches the one on device (if already installed).
|
||||
'''),
|
||||
Parameter('force_install', kind=boolean, default=False,
|
||||
description='''
|
||||
Always re-install the APK, even if matching version is found
|
||||
on already installed on the device.
|
||||
'''),
|
||||
Parameter('uninstall_apk', kind=boolean, default=False,
|
||||
description='If ``True``, will uninstall workload\'s APK as part of teardown.'),
|
||||
]
|
||||
|
||||
def __init__(self, device, _call_super=True, **kwargs):
|
||||
if _call_super:
|
||||
super(ApkWorkload, self).__init__(device, **kwargs)
|
||||
self.apk_file = None
|
||||
self.apk_version = None
|
||||
self.logcat_log = None
|
||||
|
||||
def init_resources(self, context):
|
||||
self.apk_file = context.resolver.get(wlauto.common.android.resources.ApkFile(self),
|
||||
version=getattr(self, 'version', None),
|
||||
strict=self.check_apk)
|
||||
|
||||
def validate(self):
|
||||
if self.check_apk:
|
||||
if not self.apk_file:
|
||||
raise WorkloadError('No APK file found for workload {}.'.format(self.name))
|
||||
else:
|
||||
if self.force_install:
|
||||
raise ConfigError('force_install cannot be "True" when check_apk is set to "False".')
|
||||
|
||||
def setup(self, context):
|
||||
self.initialize_package(context)
|
||||
self.start_activity()
|
||||
self.device.execute('am kill-all') # kill all *background* activities
|
||||
self.device.clear_logcat()
|
||||
|
||||
def initialize_package(self, context):
|
||||
installed_version = self.device.get_package_version(self.package)
|
||||
if self.check_apk:
|
||||
self.initialize_with_host_apk(context, installed_version)
|
||||
else:
|
||||
if not installed_version:
|
||||
message = '''{} not found found on the device and check_apk is set to "False"
|
||||
so host version was not checked.'''
|
||||
raise WorkloadError(message.format(self.package))
|
||||
message = 'Version {} installed on device; skipping host APK check.'
|
||||
self.logger.debug(message.format(installed_version))
|
||||
self.reset(context)
|
||||
self.apk_version = installed_version
|
||||
|
||||
def initialize_with_host_apk(self, context, installed_version):
|
||||
host_version = ApkInfo(self.apk_file).version_name
|
||||
if installed_version != host_version:
|
||||
if installed_version:
|
||||
message = '{} host version: {}, device version: {}; re-installing...'
|
||||
self.logger.debug(message.format(os.path.basename(self.apk_file),
|
||||
host_version, installed_version))
|
||||
else:
|
||||
message = '{} host version: {}, not found on device; installing...'
|
||||
self.logger.debug(message.format(os.path.basename(self.apk_file),
|
||||
host_version))
|
||||
self.force_install = True # pylint: disable=attribute-defined-outside-init
|
||||
else:
|
||||
message = '{} version {} found on both device and host.'
|
||||
self.logger.debug(message.format(os.path.basename(self.apk_file),
|
||||
host_version))
|
||||
if self.force_install:
|
||||
if installed_version:
|
||||
self.device.uninstall(self.package)
|
||||
self.install_apk(context)
|
||||
else:
|
||||
self.reset(context)
|
||||
self.apk_version = host_version
|
||||
|
||||
def start_activity(self):
|
||||
output = self.device.execute('am start -W -n {}/{}'.format(self.package, self.activity))
|
||||
if 'Error:' in output:
|
||||
self.device.execute('am force-stop {}'.format(self.package)) # this will dismiss any erro dialogs
|
||||
raise WorkloadError(output)
|
||||
self.logger.debug(output)
|
||||
|
||||
def reset(self, context): # pylint: disable=W0613
|
||||
self.device.execute('am force-stop {}'.format(self.package))
|
||||
self.device.execute('pm clear {}'.format(self.package))
|
||||
|
||||
# As of android API level 23, apps can request permissions at runtime,
|
||||
# this will grant all of them so requests do not pop up when running the app
|
||||
if self.device.os_version['sdk'] >= 23:
|
||||
self._grant_requested_permissions()
|
||||
|
||||
def install_apk(self, context):
|
||||
output = self.device.install(self.apk_file, self.install_timeout)
|
||||
if 'Failure' in output:
|
||||
if 'ALREADY_EXISTS' in output:
|
||||
self.logger.warn('Using already installed APK (did not unistall properly?)')
|
||||
else:
|
||||
raise WorkloadError(output)
|
||||
else:
|
||||
self.logger.debug(output)
|
||||
self.do_post_install(context)
|
||||
|
||||
def _grant_requested_permissions(self):
|
||||
dumpsys_output = self.device.execute(command="dumpsys package {}".format(self.package))
|
||||
permissions = []
|
||||
lines = iter(dumpsys_output.splitlines())
|
||||
for line in lines:
|
||||
if "requested permissions:" in line:
|
||||
break
|
||||
|
||||
for line in lines:
|
||||
if "android.permission." in line:
|
||||
permissions.append(line.split(":")[0].strip())
|
||||
else:
|
||||
break
|
||||
|
||||
for permission in permissions:
|
||||
# "Normal" Permisions are automatically granted and cannot be changed
|
||||
permission_name = permission.rsplit('.', 1)[1]
|
||||
if permission_name not in ANDROID_NORMAL_PERMISSIONS:
|
||||
self.device.execute("pm grant {} {}".format(self.package, permission))
|
||||
|
||||
def do_post_install(self, context):
|
||||
""" May be overwritten by dervied classes."""
|
||||
pass
|
||||
|
||||
def run(self, context):
|
||||
pass
|
||||
|
||||
def update_result(self, context):
|
||||
self.logcat_log = os.path.join(context.output_directory, 'logcat.log')
|
||||
context.device_manager.dump_logcat(self.logcat_log)
|
||||
context.add_iteration_artifact(name='logcat',
|
||||
path='logcat.log',
|
||||
kind='log',
|
||||
description='Logact dump for the run.')
|
||||
|
||||
def teardown(self, context):
|
||||
self.device.execute('am force-stop {}'.format(self.package))
|
||||
if self.uninstall_apk:
|
||||
self.device.uninstall(self.package)
|
||||
|
||||
|
||||
AndroidBenchmark = ApkWorkload # backward compatibility
|
||||
|
||||
|
||||
class ReventWorkload(Workload):
|
||||
|
||||
default_setup_timeout = 5 * 60 # in seconds
|
||||
default_run_timeout = 10 * 60 # in seconds
|
||||
|
||||
@property
|
||||
def on_device_setup_revent(self):
|
||||
return self.device.get_workpath('{}.setup.revent'.format(self.device.model))
|
||||
|
||||
@property
|
||||
def on_device_run_revent(self):
|
||||
return self.device.get_workpath('{}.run.revent'.format(self.device.model))
|
||||
|
||||
def __init__(self, device, _call_super=True, **kwargs):
|
||||
if _call_super:
|
||||
super(ReventWorkload, self).__init__(device, **kwargs)
|
||||
self.on_device_revent_binary = None
|
||||
self.setup_timeout = kwargs.get('setup_timeout', self.default_setup_timeout)
|
||||
self.run_timeout = kwargs.get('run_timeout', self.default_run_timeout)
|
||||
self.revent_setup_file = None
|
||||
self.revent_run_file = None
|
||||
|
||||
def initialize(self, context):
|
||||
self.revent_setup_file = context.resolver.get(wlauto.common.android.resources.ReventFile(self, 'setup'))
|
||||
self.revent_run_file = context.resolver.get(wlauto.common.android.resources.ReventFile(self, 'run'))
|
||||
|
||||
def setup(self, context):
|
||||
self._check_revent_files(context)
|
||||
self.device.killall('revent')
|
||||
command = '{} replay {}'.format(self.on_device_revent_binary, self.on_device_setup_revent)
|
||||
self.device.execute(command, timeout=self.setup_timeout)
|
||||
|
||||
def run(self, context):
|
||||
command = '{} replay {}'.format(self.on_device_revent_binary, self.on_device_run_revent)
|
||||
self.logger.debug('Replaying {}'.format(os.path.basename(self.on_device_run_revent)))
|
||||
self.device.execute(command, timeout=self.run_timeout)
|
||||
self.logger.debug('Replay completed.')
|
||||
|
||||
def update_result(self, context):
|
||||
pass
|
||||
|
||||
def teardown(self, context):
|
||||
self.device.remove(self.on_device_setup_revent)
|
||||
self.device.remove(self.on_device_run_revent)
|
||||
|
||||
def _check_revent_files(self, context):
|
||||
# check the revent binary
|
||||
revent_binary = context.resolver.get(Executable(NO_ONE, self.device.abi, 'revent'))
|
||||
if not os.path.isfile(revent_binary):
|
||||
message = '{} does not exist. '.format(revent_binary)
|
||||
message += 'Please build revent for your system and place it in that location'
|
||||
raise WorkloadError(message)
|
||||
if not self.revent_setup_file:
|
||||
# pylint: disable=too-few-format-args
|
||||
message = '{0}.setup.revent file does not exist, Please provide one for your device, {0}'.format(self.device.name)
|
||||
raise WorkloadError(message)
|
||||
if not self.revent_run_file:
|
||||
# pylint: disable=too-few-format-args
|
||||
message = '{0}.run.revent file does not exist, Please provide one for your device, {0}'.format(self.device.name)
|
||||
raise WorkloadError(message)
|
||||
|
||||
self.on_device_revent_binary = self.device.install_executable(revent_binary)
|
||||
self.device.push(self.revent_run_file, self.on_device_run_revent)
|
||||
self.device.push(self.revent_setup_file, self.on_device_setup_revent)
|
||||
|
||||
|
||||
class AndroidUiAutoBenchmark(UiAutomatorWorkload, AndroidBenchmark):
|
||||
|
||||
supported_platforms = ['android']
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
UiAutomatorWorkload.__init__(self, device, **kwargs)
|
||||
AndroidBenchmark.__init__(self, device, _call_super=False, **kwargs)
|
||||
|
||||
def init_resources(self, context):
|
||||
UiAutomatorWorkload.init_resources(self, context)
|
||||
AndroidBenchmark.init_resources(self, context)
|
||||
|
||||
def setup(self, context):
|
||||
UiAutomatorWorkload.setup(self, context)
|
||||
AndroidBenchmark.setup(self, context)
|
||||
|
||||
def update_result(self, context):
|
||||
UiAutomatorWorkload.update_result(self, context)
|
||||
AndroidBenchmark.update_result(self, context)
|
||||
|
||||
def teardown(self, context):
|
||||
UiAutomatorWorkload.teardown(self, context)
|
||||
AndroidBenchmark.teardown(self, context)
|
||||
|
||||
|
||||
class GameWorkload(ApkWorkload, ReventWorkload):
|
||||
"""
|
||||
GameWorkload is the base class for all the workload that use revent files to
|
||||
run.
|
||||
|
||||
For more in depth details on how to record revent files, please see
|
||||
:ref:`revent_files_creation`. To subclass this class, please refer to
|
||||
:ref:`GameWorkload`.
|
||||
|
||||
Additionally, this class defines the following attributes:
|
||||
|
||||
:asset_file: A tarball containing additional assets for the workload. These are the assets
|
||||
that are not part of the APK but would need to be downloaded by the workload
|
||||
(usually, on first run of the app). Since the presence of a network connection
|
||||
cannot be assumed on some devices, this provides an alternative means of obtaining
|
||||
the assets.
|
||||
:saved_state_file: A tarball containing the saved state for a workload. This tarball gets
|
||||
deployed in the same way as the asset file. The only difference being that
|
||||
it is usually much slower and re-deploying the tarball should alone be
|
||||
enough to reset the workload to a known state (without having to reinstall
|
||||
the app or re-deploy the other assets).
|
||||
:loading_time: Time it takes for the workload to load after the initial activity has been
|
||||
started.
|
||||
|
||||
"""
|
||||
|
||||
# May be optionally overwritten by subclasses
|
||||
asset_file = None
|
||||
saved_state_file = None
|
||||
view = 'SurfaceView'
|
||||
loading_time = 10
|
||||
supported_platforms = ['android']
|
||||
|
||||
parameters = [
|
||||
Parameter('install_timeout', default=500, override=True),
|
||||
Parameter('assets_push_timeout', kind=int, default=500,
|
||||
description='Timeout used during deployment of the assets package (if there is one).'),
|
||||
Parameter('clear_data_on_reset', kind=bool, default=True,
|
||||
description="""
|
||||
If set to ``False``, this will prevent WA from clearing package
|
||||
data for this workload prior to running it.
|
||||
"""),
|
||||
]
|
||||
|
||||
def __init__(self, device, **kwargs): # pylint: disable=W0613
|
||||
ApkWorkload.__init__(self, device, **kwargs)
|
||||
ReventWorkload.__init__(self, device, _call_super=False, **kwargs)
|
||||
self.logcat_process = None
|
||||
self.module_dir = os.path.dirname(sys.modules[self.__module__].__file__)
|
||||
self.revent_dir = os.path.join(self.module_dir, 'revent_files')
|
||||
|
||||
def apk_init_resources(self, context):
|
||||
ApkWorkload.init_resources(self, context)
|
||||
|
||||
def init_resources(self, context):
|
||||
self.apk_init_resources(context)
|
||||
ReventWorkload.init_resources(self, context)
|
||||
|
||||
def setup(self, context):
|
||||
ApkWorkload.setup(self, context)
|
||||
self.logger.debug('Waiting for the game to load...')
|
||||
time.sleep(self.loading_time)
|
||||
ReventWorkload.setup(self, context)
|
||||
|
||||
def do_post_install(self, context):
|
||||
ApkWorkload.do_post_install(self, context)
|
||||
self._deploy_assets(context, self.assets_push_timeout)
|
||||
|
||||
def reset(self, context):
|
||||
# If saved state exists, restore it; if not, do full
|
||||
# uninstall/install cycle.
|
||||
self.device.execute('am force-stop {}'.format(self.package))
|
||||
if self.saved_state_file:
|
||||
self._deploy_resource_tarball(context, self.saved_state_file)
|
||||
else:
|
||||
if self.clear_data_on_reset:
|
||||
self.device.execute('pm clear {}'.format(self.package))
|
||||
self._deploy_assets(context)
|
||||
|
||||
def run(self, context):
|
||||
ReventWorkload.run(self, context)
|
||||
|
||||
def apk_teardown(self, context):
|
||||
if not self.saved_state_file:
|
||||
ApkWorkload.teardown(self, context)
|
||||
else:
|
||||
self.device.execute('am force-stop {}'.format(self.package))
|
||||
|
||||
def teardown(self, context):
|
||||
self.apk_teardown(context)
|
||||
ReventWorkload.teardown(self, context)
|
||||
|
||||
def _deploy_assets(self, context, timeout=300):
|
||||
if self.asset_file:
|
||||
self._deploy_resource_tarball(context, self.asset_file, timeout)
|
||||
if self.saved_state_file: # must be deployed *after* asset tarball!
|
||||
self._deploy_resource_tarball(context, self.saved_state_file, timeout)
|
||||
|
||||
def _deploy_resource_tarball(self, context, resource_file, timeout=300):
|
||||
kind = 'data'
|
||||
if ':' in resource_file:
|
||||
kind, resource_file = resource_file.split(':', 1)
|
||||
ondevice_cache = self.device.path.join(self.device.working_directory, '.cache', self.name, resource_file)
|
||||
if not self.device.file_exists(ondevice_cache):
|
||||
asset_tarball = context.resolver.get(PluginAsset(self, resource_file))
|
||||
if not asset_tarball:
|
||||
message = 'Could not find resource {} for workload {}.'
|
||||
raise WorkloadError(message.format(resource_file, self.name))
|
||||
# adb push will create intermediate directories if they don't
|
||||
# exist.
|
||||
self.device.push(asset_tarball, ondevice_cache, timeout=timeout)
|
||||
|
||||
device_asset_directory = self.device.path.join(context.device_manager.external_storage_directory, 'Android', kind)
|
||||
deploy_command = 'cd {} && {} tar -xzf {}'.format(device_asset_directory,
|
||||
self.device.busybox,
|
||||
ondevice_cache)
|
||||
self.device.execute(deploy_command, timeout=timeout, as_root=True)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +0,0 @@
|
||||
The gem5 simulator can be obtained from http://repo.gem5.org/gem5/ and the
|
||||
corresponding documentation can be found at http://www.gem5.org.
|
||||
|
||||
The source for the m5 binaries bundled with Workload Automation (found at
|
||||
wlauto/common/bin/arm64/m5 and wlauto/common/bin/armeabi/m5) can be found at
|
||||
util/m5 in the gem5 source at http://repo.gem5.org/gem5/.
|
@ -1,16 +0,0 @@
|
||||
# Copyright 2015 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.
|
||||
#
|
||||
|
||||
|
@ -1,64 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from wlauto.core.resource import Resource
|
||||
|
||||
|
||||
class FileResource(Resource):
|
||||
"""
|
||||
Base class for all resources that are a regular file in the
|
||||
file system.
|
||||
|
||||
"""
|
||||
|
||||
def delete(self, instance):
|
||||
os.remove(instance)
|
||||
|
||||
|
||||
class File(FileResource):
|
||||
|
||||
name = 'file'
|
||||
|
||||
def __init__(self, owner, path, url=None):
|
||||
super(File, self).__init__(owner)
|
||||
self.path = path
|
||||
self.url = url
|
||||
|
||||
def __str__(self):
|
||||
return '<{}\'s {} {}>'.format(self.owner, self.name, self.path or self.url)
|
||||
|
||||
|
||||
class PluginAsset(File):
|
||||
|
||||
name = 'plugin_asset'
|
||||
|
||||
def __init__(self, owner, path):
|
||||
super(PluginAsset, self).__init__(owner, os.path.join(owner.name, path))
|
||||
|
||||
|
||||
class Executable(FileResource):
|
||||
|
||||
name = 'executable'
|
||||
|
||||
def __init__(self, owner, platform, filename):
|
||||
super(Executable, self).__init__(owner)
|
||||
self.platform = platform
|
||||
self.filename = filename
|
||||
|
||||
def __str__(self):
|
||||
return '<{}\'s {} {}>'.format(self.owner, self.platform, self.filename)
|
@ -1,289 +0,0 @@
|
||||
"""
|
||||
Default config for Workload Automation. DO NOT MODIFY this file. This file
|
||||
gets copied to ~/.workload_automation/config.py on initial run of run_workloads.
|
||||
Add your configuration to that file instead.
|
||||
|
||||
"""
|
||||
# *** WARNING: ***
|
||||
# Configuration listed in this file is NOT COMPLETE. This file sets the default
|
||||
# configuration for WA and gives EXAMPLES of other configuration available. It
|
||||
# is not supposed to be an exhaustive list.
|
||||
# PLEASE REFER TO WA DOCUMENTATION FOR THE COMPLETE LIST OF AVAILABLE
|
||||
# EXTENSIONS AND THEIR configuration.
|
||||
|
||||
|
||||
# This defines when the device will be rebooted during Workload Automation execution. #
|
||||
# #
|
||||
# Valid policies are: #
|
||||
# never: The device will never be rebooted. #
|
||||
# as_needed: The device will only be rebooted if the need arises (e.g. if it #
|
||||
# becomes unresponsive #
|
||||
# initial: The device will be rebooted when the execution first starts, just before executing #
|
||||
# the first workload spec. #
|
||||
# each_spec: The device will be rebooted before running a new workload spec. #
|
||||
# each_iteration: The device will be rebooted before each new iteration. #
|
||||
# #
|
||||
reboot_policy = 'as_needed'
|
||||
|
||||
# Defines the order in which the agenda spec will be executed. At the moment, #
|
||||
# the following execution orders are supported: #
|
||||
# #
|
||||
# by_iteration: The first iteration of each workload spec is executed one ofter the other, #
|
||||
# so all workloads are executed before proceeding on to the second iteration. #
|
||||
# This is the default if no order is explicitly specified. #
|
||||
# If multiple sections were specified, this will also split them up, so that specs #
|
||||
# in the same section are further apart in the execution order. #
|
||||
# by_section: Same as "by_iteration", but runn specs from the same section one after the other #
|
||||
# by_spec: All iterations of the first spec are executed before moving on to the next #
|
||||
# spec. This may also be specified as ``"classic"``, as this was the way #
|
||||
# workloads were executed in earlier versions of WA. #
|
||||
# random: Randomisizes the order in which specs run. #
|
||||
execution_order = 'by_iteration'
|
||||
|
||||
|
||||
# This indicates when a job will be re-run.
|
||||
# Possible values:
|
||||
# OK: This iteration has completed and no errors have been detected
|
||||
# PARTIAL: One or more instruments have failed (the iteration may still be running).
|
||||
# FAILED: The workload itself has failed.
|
||||
# ABORTED: The user interupted the workload
|
||||
#
|
||||
# If set to an empty list, a job will not be re-run ever.
|
||||
retry_on_status = ['FAILED', 'PARTIAL']
|
||||
|
||||
# How many times a job will be re-run before giving up
|
||||
max_retries = 3
|
||||
|
||||
####################################################################################################
|
||||
######################################### Device Settings ##########################################
|
||||
####################################################################################################
|
||||
# Specify the device you want to run workload automation on. This must be a #
|
||||
# string with the ID of the device. At the moment, only 'TC2' is supported. #
|
||||
# #
|
||||
device = 'generic_android'
|
||||
|
||||
# Configuration options that will be passed onto the device. These are obviously device-specific, #
|
||||
# so check the documentation for the particular device to find out which options and values are #
|
||||
# valid. The settings listed below are common to all devices #
|
||||
# #
|
||||
device_config = dict(
|
||||
# The name used by adb to identify the device. Use "adb devices" in bash to list
|
||||
# the devices currently seen by adb.
|
||||
#adb_name='10.109.173.2:5555',
|
||||
|
||||
# The directory on the device that WA will use to push files to
|
||||
#working_directory='/sdcard/wa-working',
|
||||
|
||||
# This specifies the device's CPU cores. The order must match how they
|
||||
# appear in cpufreq. The example below is for TC2.
|
||||
# core_names = ['a7', 'a7', 'a7', 'a15', 'a15']
|
||||
|
||||
# Specifies cluster mapping for the device's cores.
|
||||
# core_clusters = [0, 0, 0, 1, 1]
|
||||
)
|
||||
|
||||
|
||||
####################################################################################################
|
||||
################################### Instrumention Configuration ####################################
|
||||
####################################################################################################
|
||||
# This defines the additionnal instrumentation that will be enabled during workload execution, #
|
||||
# which in turn determines what additional data (such as /proc/interrupts content or Streamline #
|
||||
# traces) will be available in the results directory. #
|
||||
# #
|
||||
instrumentation = [
|
||||
# Records the time it took to run the workload
|
||||
'execution_time',
|
||||
|
||||
# Collects /proc/interrupts before and after execution and does a diff.
|
||||
'interrupts',
|
||||
|
||||
# Collects the contents of/sys/devices/system/cpu before and after execution and does a diff.
|
||||
'cpufreq',
|
||||
|
||||
# Gets energy usage from the workload form HWMON devices
|
||||
# NOTE: the hardware needs to have the right sensors in order for this to work
|
||||
#'hwmon',
|
||||
|
||||
# Run perf in the background during workload execution and then collect the results. perf is a
|
||||
# standard Linux performance analysis tool.
|
||||
#'perf',
|
||||
|
||||
# Collect Streamline traces during workload execution. Streamline is part of DS-5
|
||||
#'streamline',
|
||||
|
||||
# Collects traces by interacting with Ftrace Linux kernel internal tracer
|
||||
#'trace-cmd',
|
||||
|
||||
# Obtains the power consumption of the target device's core measured by National Instruments
|
||||
# Data Acquisition(DAQ) device.
|
||||
#'daq',
|
||||
|
||||
# Collects CCI counter data.
|
||||
#'cci_pmu_logger',
|
||||
|
||||
# Collects FPS (Frames Per Second) and related metrics (such as jank) from
|
||||
# the View of the workload (Note: only a single View per workload is
|
||||
# supported at the moment, so this is mainly useful for games).
|
||||
#'fps',
|
||||
]
|
||||
|
||||
|
||||
####################################################################################################
|
||||
################################# Result Processors Configuration ##################################
|
||||
####################################################################################################
|
||||
# Specifies how results will be processed and presented. #
|
||||
# #
|
||||
result_processors = [
|
||||
# Creates a status.txt that provides a summary status for the run
|
||||
'status',
|
||||
|
||||
# Creates a results.txt file for each iteration that lists all collected metrics
|
||||
# in "name = value (units)" format
|
||||
'standard',
|
||||
|
||||
# Creates a results.csv that contains metrics for all iterations of all workloads
|
||||
# in the .csv format.
|
||||
'csv',
|
||||
|
||||
# Creates a summary.csv that contains summary metrics for all iterations of all
|
||||
# all in the .csv format. Summary metrics are defined on per-worklod basis
|
||||
# are typically things like overall scores. The contents of summary.csv are
|
||||
# always a subset of the contents of results.csv (if it is generated).
|
||||
#'summary_csv',
|
||||
|
||||
# Creates a results.csv that contains metrics for all iterations of all workloads
|
||||
# in the JSON format
|
||||
#'json',
|
||||
|
||||
# Write results to an sqlite3 database. By default, a new database will be
|
||||
# generated for each run, however it is possible to specify a path to an
|
||||
# existing DB file (see result processor configuration below), in which
|
||||
# case results from multiple runs may be stored in the one file.
|
||||
#'sqlite',
|
||||
]
|
||||
|
||||
|
||||
####################################################################################################
|
||||
################################### Logging output Configuration ###################################
|
||||
####################################################################################################
|
||||
# Specify the format of logging messages. The format uses the old formatting syntax: #
|
||||
# #
|
||||
# http://docs.python.org/2/library/stdtypes.html#string-formatting-operations #
|
||||
# #
|
||||
# The attributes that can be used in formats are listested here: #
|
||||
# #
|
||||
# http://docs.python.org/2/library/logging.html#logrecord-attributes #
|
||||
# #
|
||||
logging = {
|
||||
# Log file format
|
||||
'file format': '%(asctime)s %(levelname)-8s %(name)s: %(message)s',
|
||||
# Verbose console output format
|
||||
'verbose format': '%(asctime)s %(levelname)-8s %(name)s: %(message)s',
|
||||
# Regular console output format
|
||||
'regular format': '%(levelname)-8s %(message)s',
|
||||
# Colouring the console output
|
||||
'colour_enabled': True,
|
||||
}
|
||||
|
||||
|
||||
####################################################################################################
|
||||
#################################### Instruments Configuration #####################################
|
||||
####################################################################################################
|
||||
# Instrumention Configuration is related to specific insturment's settings. Some of the #
|
||||
# instrumentations require specific settings in order for them to work. These settings are #
|
||||
# specified here. #
|
||||
# Note that these settings only take effect if the corresponding instrument is
|
||||
# enabled above.
|
||||
|
||||
####################################################################################################
|
||||
######################################## perf configuration ########################################
|
||||
|
||||
# The hardware events such as instructions executed, cache-misses suffered, or branches
|
||||
# mispredicted to be reported by perf. Events can be obtained from the device by tpying
|
||||
# 'perf list'.
|
||||
#perf_events = ['migrations', 'cs']
|
||||
|
||||
# The perf options which can be obtained from man page for perf-record
|
||||
#perf_options = '-a -i'
|
||||
|
||||
####################################################################################################
|
||||
####################################### hwmon configuration ########################################
|
||||
|
||||
# The kinds of sensors hwmon instrument will look for
|
||||
#hwmon_sensors = ['energy', 'temp']
|
||||
|
||||
####################################################################################################
|
||||
###################################### trace-cmd configuration #####################################
|
||||
|
||||
# trace-cmd events to be traced. The events can be found by rooting on the device then type
|
||||
# 'trace-cmd list -e'
|
||||
#trace_events = ['power*']
|
||||
|
||||
####################################################################################################
|
||||
######################################### DAQ configuration ########################################
|
||||
|
||||
# The host address of the machine that runs the daq Server which the insturment communicates with
|
||||
#daq_server_host = '10.1.17.56'
|
||||
|
||||
# The port number for daq Server in which daq insturment communicates with
|
||||
#daq_server_port = 56788
|
||||
|
||||
# The values of resistors 1 and 2 (in Ohms) across which the voltages are measured
|
||||
#daq_resistor_values = [0.002, 0.002]
|
||||
|
||||
####################################################################################################
|
||||
################################### cci_pmu_logger configuration ###################################
|
||||
|
||||
# The events to be counted by PMU
|
||||
# NOTE: The number of events must not exceed the number of counters available (which is 4 for CCI-400)
|
||||
#cci_pmu_events = ['0x63', '0x83']
|
||||
|
||||
# The name of the events which will be used when reporting PMU counts
|
||||
#cci_pmu_event_labels = ['event_0x63', 'event_0x83']
|
||||
|
||||
# The period (in jiffies) between counter reads
|
||||
#cci_pmu_period = 15
|
||||
|
||||
####################################################################################################
|
||||
################################### fps configuration ##############################################
|
||||
|
||||
# Data points below this FPS will dropped as not constituting "real" gameplay. The assumption
|
||||
# being that while actually running, the FPS in the game will not drop below X frames per second,
|
||||
# except on loading screens, menus, etc, which should not contribute to FPS calculation.
|
||||
#fps_drop_threshold=5
|
||||
|
||||
# If set to True, this will keep the raw dumpsys output in the results directory (this is maily
|
||||
# used for debugging). Note: frames.csv with collected frames data will always be generated
|
||||
# regardless of this setting.
|
||||
#fps_keep_raw=False
|
||||
|
||||
####################################################################################################
|
||||
################################# Result Processor Configuration ###################################
|
||||
####################################################################################################
|
||||
|
||||
# Specifies an alternative database to store results in. If the file does not
|
||||
# exist, it will be created (the directiory of the file must exist however). If
|
||||
# the file does exist, the results will be added to the existing data set (each
|
||||
# run as a UUID, so results won't clash even if identical agendas were used).
|
||||
# Note that in order for this to work, the version of the schema used to generate
|
||||
# the DB file must match that of the schema used for the current run. Please
|
||||
# see "What's new" secition in WA docs to check if the schema has changed in
|
||||
# recent releases of WA.
|
||||
#sqlite_database = '/work/results/myresults.sqlite'
|
||||
|
||||
# If the file specified by sqlite_database exists, setting this to True will
|
||||
# cause that file to be overwritten rather than updated -- existing results in
|
||||
# the file will be lost.
|
||||
#sqlite_overwrite = False
|
||||
|
||||
# distribution: internal
|
||||
|
||||
####################################################################################################
|
||||
#################################### Resource Getter configuration #################################
|
||||
####################################################################################################
|
||||
|
||||
# The location on your system where /arm/scratch is mounted. Used by
|
||||
# Scratch resource getter.
|
||||
#scratch_mount_point = '/arm/scratch'
|
||||
|
||||
# end distribution
|
@ -1,16 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
@ -1,81 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
import textwrap
|
||||
|
||||
from wlauto.core.plugin import Plugin
|
||||
from wlauto.utils.doc import format_body
|
||||
from wlauto.core.version import get_wa_version
|
||||
|
||||
|
||||
def init_argument_parser(parser):
|
||||
parser.add_argument('-c', '--config', action='append', default=[],
|
||||
help='specify an additional config.py')
|
||||
parser.add_argument('-v', '--verbose', action='count',
|
||||
help='The scripts will produce verbose output.')
|
||||
parser.add_argument('--version', action='version',
|
||||
version='%(prog)s {}'.format(get_wa_version()))
|
||||
return parser
|
||||
|
||||
|
||||
class Command(Plugin):
|
||||
"""
|
||||
Defines a Workload Automation command. This will be executed from the
|
||||
command line as ``wa <command> [args ...]``. This defines the name to be
|
||||
used when invoking wa, the code that will actually be executed on
|
||||
invocation and the argument parser to be used to parse the reset of the
|
||||
command line arguments.
|
||||
|
||||
"""
|
||||
kind = "command"
|
||||
help = None
|
||||
usage = None
|
||||
description = None
|
||||
epilog = None
|
||||
formatter_class = None
|
||||
|
||||
def __init__(self, subparsers):
|
||||
super(Command, self).__init__()
|
||||
self.group = subparsers
|
||||
parser_params = dict(help=(self.help or self.description), usage=self.usage,
|
||||
description=format_body(textwrap.dedent(self.description), 80),
|
||||
epilog=self.epilog)
|
||||
if self.formatter_class:
|
||||
parser_params['formatter_class'] = self.formatter_class
|
||||
self.parser = subparsers.add_parser(self.name, **parser_params)
|
||||
init_argument_parser(self.parser) # propagate top-level options
|
||||
self.initialize(None)
|
||||
|
||||
def initialize(self, context):
|
||||
"""
|
||||
Perform command-specific initialisation (e.g. adding command-specific
|
||||
options to the command's parser). ``context`` is always ``None``.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute(self, state, args):
|
||||
"""
|
||||
Execute this command.
|
||||
|
||||
:state: An initialized ``ConfigManager`` that contains the current state of
|
||||
WA exeuction up to that point (processed configuraition, loaded
|
||||
plugins, etc).
|
||||
:args: An ``argparse.Namespace`` containing command line arguments (as returned by
|
||||
``argparse.ArgumentParser.parse_args()``. This would usually be the result of
|
||||
invoking ``self.parser``.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
@ -1,19 +0,0 @@
|
||||
# Copyright 2013-2016 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 wlauto.core.configuration.configuration import (settings,
|
||||
RunConfiguration,
|
||||
JobGenerator,
|
||||
ConfigurationPoint)
|
||||
from wlauto.core.configuration.plugin_cache import PluginCache
|
File diff suppressed because it is too large
Load Diff
@ -1,42 +0,0 @@
|
||||
from wlauto.core.configuration.configuration import MetaConfiguration, RunConfiguration
|
||||
from wlauto.core.configuration.plugin_cache import PluginCache
|
||||
from wlauto.utils.serializer import yaml
|
||||
from wlauto.utils.doc import strip_inlined_text
|
||||
|
||||
DEFAULT_INSTRUMENTS = ['execution_time',
|
||||
'interrupts',
|
||||
'cpufreq',
|
||||
'status',
|
||||
'standard',
|
||||
'csv']
|
||||
|
||||
|
||||
def _format_yaml_comment(param, short_description=False):
|
||||
comment = param.description
|
||||
comment = strip_inlined_text(comment)
|
||||
if short_description:
|
||||
comment = comment.split('\n\n')[0]
|
||||
comment = comment.replace('\n', '\n# ')
|
||||
comment = "# {}\n".format(comment)
|
||||
return comment
|
||||
|
||||
|
||||
def _format_instruments(output):
|
||||
plugin_cache = PluginCache()
|
||||
output.write("instrumentation:\n")
|
||||
for plugin in DEFAULT_INSTRUMENTS:
|
||||
plugin_cls = plugin_cache.loader.get_plugin_class(plugin)
|
||||
output.writelines(_format_yaml_comment(plugin_cls, short_description=True))
|
||||
output.write(" - {}\n".format(plugin))
|
||||
output.write("\n")
|
||||
|
||||
|
||||
def generate_default_config(path):
|
||||
with open(path, 'w') as output:
|
||||
for param in MetaConfiguration.config_points + RunConfiguration.config_points:
|
||||
entry = {param.name: param.default}
|
||||
comment = _format_yaml_comment(param)
|
||||
output.writelines(comment)
|
||||
yaml.dump(entry, output, default_flow_style=False)
|
||||
output.write("\n")
|
||||
_format_instruments(output)
|
@ -1,213 +0,0 @@
|
||||
import random
|
||||
from itertools import izip_longest, groupby, chain
|
||||
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.core.configuration.configuration import (MetaConfiguration,
|
||||
RunConfiguration,
|
||||
JobGenerator, settings)
|
||||
from wlauto.core.configuration.parsers import ConfigParser
|
||||
from wlauto.core.configuration.plugin_cache import PluginCache
|
||||
|
||||
|
||||
class CombinedConfig(object):
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
instance = CombinedConfig()
|
||||
instance.settings = MetaConfiguration.from_pod(pod.get('settings', {}))
|
||||
instance.run_config = RunConfiguration.from_pod(pod.get('run_config', {}))
|
||||
return instance
|
||||
|
||||
def __init__(self, settings=None, run_config=None):
|
||||
self.settings = settings
|
||||
self.run_config = run_config
|
||||
|
||||
def to_pod(self):
|
||||
return {'settings': self.settings.to_pod(),
|
||||
'run_config': self.run_config.to_pod()}
|
||||
|
||||
|
||||
class Job(object):
|
||||
|
||||
def __init__(self, spec, iteration, context):
|
||||
self.spec = spec
|
||||
self.iteration = iteration
|
||||
self.context = context
|
||||
self.status = 'new'
|
||||
self.workload = None
|
||||
self.output = None
|
||||
|
||||
def load(self, target, loader=pluginloader):
|
||||
self.workload = loader.get_workload(self.spec.workload_name,
|
||||
target,
|
||||
**self.spec.workload_parameters)
|
||||
self.workload.init_resources(self.context)
|
||||
self.workload.validate()
|
||||
|
||||
|
||||
class ConfigManager(object):
|
||||
"""
|
||||
Represents run-time state of WA. Mostly used as a container for loaded
|
||||
configuration and discovered plugins.
|
||||
|
||||
This exists outside of any command or run and is associated with the running
|
||||
instance of wA itself.
|
||||
"""
|
||||
|
||||
@property
|
||||
def enabled_instruments(self):
|
||||
return self.jobs_config.enabled_instruments
|
||||
|
||||
@property
|
||||
def job_specs(self):
|
||||
if not self._jobs_generated:
|
||||
msg = 'Attempting to access job specs before '\
|
||||
'jobs have been generated'
|
||||
raise RuntimeError(msg)
|
||||
return [j.spec for j in self._jobs]
|
||||
|
||||
@property
|
||||
def jobs(self):
|
||||
if not self._jobs_generated:
|
||||
msg = 'Attempting to access jobs before '\
|
||||
'they have been generated'
|
||||
raise RuntimeError(msg)
|
||||
return self._jobs
|
||||
|
||||
def __init__(self, settings=settings):
|
||||
self.settings = settings
|
||||
self.run_config = RunConfiguration()
|
||||
self.plugin_cache = PluginCache()
|
||||
self.jobs_config = JobGenerator(self.plugin_cache)
|
||||
self.loaded_config_sources = []
|
||||
self._config_parser = ConfigParser()
|
||||
self._jobs = []
|
||||
self._jobs_generated = False
|
||||
self.agenda = None
|
||||
|
||||
def load_config_file(self, filepath):
|
||||
self._config_parser.load_from_path(self, filepath)
|
||||
self.loaded_config_sources.append(filepath)
|
||||
|
||||
def load_config(self, values, source, wrap_exceptions=True):
|
||||
self._config_parser.load(self, values, source)
|
||||
self.loaded_config_sources.append(source)
|
||||
|
||||
def get_plugin(self, name=None, kind=None, *args, **kwargs):
|
||||
return self.plugin_cache.get_plugin(name, kind, *args, **kwargs)
|
||||
|
||||
def get_instruments(self, target):
|
||||
instruments = []
|
||||
for name in self.enabled_instruments:
|
||||
instruments.append(self.get_plugin(name, kind='instrument',
|
||||
target=target))
|
||||
return instruments
|
||||
|
||||
def finalize(self):
|
||||
if not self.agenda:
|
||||
msg = 'Attempting to finalize config before agenda has been set'
|
||||
raise RuntimeError(msg)
|
||||
self.run_config.merge_device_config(self.plugin_cache)
|
||||
return CombinedConfig(self.settings, self.run_config)
|
||||
|
||||
def generate_jobs(self, context):
|
||||
job_specs = self.jobs_config.generate_job_specs(context.tm)
|
||||
exec_order = self.run_config.execution_order
|
||||
for spec, i in permute_iterations(job_specs, exec_order):
|
||||
job = Job(spec, i, context)
|
||||
job.load(context.tm.target)
|
||||
self._jobs.append(job)
|
||||
self._jobs_generated = True
|
||||
|
||||
|
||||
def permute_by_job(specs):
|
||||
"""
|
||||
This is that "classic" implementation that executes all iterations of a
|
||||
workload spec before proceeding onto the next spec.
|
||||
|
||||
"""
|
||||
for spec in specs:
|
||||
for i in range(1, spec.iterations + 1):
|
||||
yield (spec, i)
|
||||
|
||||
|
||||
def permute_by_iteration(specs):
|
||||
"""
|
||||
Runs the first iteration for all benchmarks first, before proceeding to the
|
||||
next iteration, i.e. A1, B1, C1, A2, B2, C2... instead of A1, A1, B1, B2,
|
||||
C1, C2...
|
||||
|
||||
If multiple sections where specified in the agenda, this will run all
|
||||
sections for the first global spec first, followed by all sections for the
|
||||
second spec, etc.
|
||||
|
||||
e.g. given sections X and Y, and global specs A and B, with 2 iterations,
|
||||
this will run
|
||||
|
||||
X.A1, Y.A1, X.B1, Y.B1, X.A2, Y.A2, X.B2, Y.B2
|
||||
|
||||
"""
|
||||
groups = [list(g) for k, g in groupby(specs, lambda s: s.workload_id)]
|
||||
|
||||
all_tuples = []
|
||||
for spec in chain(*groups):
|
||||
all_tuples.append([(spec, i + 1)
|
||||
for i in xrange(spec.iterations)])
|
||||
for t in chain(*map(list, izip_longest(*all_tuples))):
|
||||
if t is not None:
|
||||
yield t
|
||||
|
||||
|
||||
def permute_by_section(specs):
|
||||
"""
|
||||
Runs the first iteration for all benchmarks first, before proceeding to the
|
||||
next iteration, i.e. A1, B1, C1, A2, B2, C2... instead of A1, A1, B1, B2,
|
||||
C1, C2...
|
||||
|
||||
If multiple sections where specified in the agenda, this will run all specs
|
||||
for the first section followed by all specs for the seciod section, etc.
|
||||
|
||||
e.g. given sections X and Y, and global specs A and B, with 2 iterations,
|
||||
this will run
|
||||
|
||||
X.A1, X.B1, Y.A1, Y.B1, X.A2, X.B2, Y.A2, Y.B2
|
||||
|
||||
"""
|
||||
groups = [list(g) for k, g in groupby(specs, lambda s: s.section_id)]
|
||||
|
||||
all_tuples = []
|
||||
for spec in chain(*groups):
|
||||
all_tuples.append([(spec, i + 1)
|
||||
for i in xrange(spec.iterations)])
|
||||
for t in chain(*map(list, izip_longest(*all_tuples))):
|
||||
if t is not None:
|
||||
yield t
|
||||
|
||||
|
||||
def permute_randomly(specs):
|
||||
"""
|
||||
This will generate a random permutation of specs/iteration tuples.
|
||||
|
||||
"""
|
||||
result = []
|
||||
for spec in specs:
|
||||
for i in xrange(1, spec.iterations + 1):
|
||||
result.append((spec, i))
|
||||
random.shuffle(result)
|
||||
for t in result:
|
||||
yield t
|
||||
|
||||
|
||||
permute_map = {
|
||||
'by_iteration': permute_by_iteration,
|
||||
'by_job': permute_by_job,
|
||||
'by_section': permute_by_section,
|
||||
'random': permute_randomly,
|
||||
}
|
||||
|
||||
|
||||
def permute_iterations(specs, exec_order):
|
||||
if exec_order not in permute_map:
|
||||
msg = 'Unknown execution order "{}"; must be in: {}'
|
||||
raise ValueError(msg.format(exec_order, permute_map.keys()))
|
||||
return permute_map[exec_order](specs)
|
@ -1,308 +0,0 @@
|
||||
# Copyright 2015 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.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from wlauto.exceptions import ConfigError
|
||||
from wlauto.utils.serializer import read_pod, SerializerSyntaxError
|
||||
from wlauto.utils.types import toggle_set, counter
|
||||
from wlauto.core.configuration.configuration import JobSpec
|
||||
|
||||
|
||||
###############
|
||||
### Parsers ###
|
||||
###############
|
||||
|
||||
class ConfigParser(object):
|
||||
|
||||
def load_from_path(self, state, filepath):
|
||||
self.load(state, _load_file(filepath, "Config"), filepath)
|
||||
|
||||
def load(self, state, raw, source, wrap_exceptions=True): # pylint: disable=too-many-branches
|
||||
try:
|
||||
if 'run_name' in raw:
|
||||
msg = '"run_name" can only be specified in the config '\
|
||||
'section of an agenda'
|
||||
raise ConfigError(msg)
|
||||
|
||||
if 'id' in raw:
|
||||
raise ConfigError('"id" cannot be set globally')
|
||||
|
||||
merge_result_processors_instruments(raw)
|
||||
|
||||
# Get WA core configuration
|
||||
for cfg_point in state.settings.configuration.itervalues():
|
||||
value = get_aliased_param(cfg_point, raw)
|
||||
if value is not None:
|
||||
state.settings.set(cfg_point.name, value)
|
||||
|
||||
# Get run specific configuration
|
||||
for cfg_point in state.run_config.configuration.itervalues():
|
||||
value = get_aliased_param(cfg_point, raw)
|
||||
if value is not None:
|
||||
state.run_config.set(cfg_point.name, value)
|
||||
|
||||
# Get global job spec configuration
|
||||
for cfg_point in JobSpec.configuration.itervalues():
|
||||
value = get_aliased_param(cfg_point, raw)
|
||||
if value is not None:
|
||||
state.jobs_config.set_global_value(cfg_point.name, value)
|
||||
|
||||
for name, values in raw.iteritems():
|
||||
# Assume that all leftover config is for a plug-in or a global
|
||||
# alias it is up to PluginCache to assert this assumption
|
||||
state.plugin_cache.add_configs(name, values, source)
|
||||
|
||||
except ConfigError as e:
|
||||
if wrap_exceptions:
|
||||
raise ConfigError('Error in "{}":\n{}'.format(source, str(e)))
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
class AgendaParser(object):
|
||||
|
||||
def load_from_path(self, state, filepath):
|
||||
raw = _load_file(filepath, 'Agenda')
|
||||
self.load(state, raw, filepath)
|
||||
|
||||
def load(self, state, raw, source):
|
||||
try:
|
||||
if not isinstance(raw, dict):
|
||||
raise ConfigError('Invalid agenda, top level entry must be a dict')
|
||||
|
||||
self._populate_and_validate_config(state, raw, source)
|
||||
sections = self._pop_sections(raw)
|
||||
global_workloads = self._pop_workloads(raw)
|
||||
|
||||
if raw:
|
||||
msg = 'Invalid top level agenda entry(ies): "{}"'
|
||||
raise ConfigError(msg.format('", "'.join(raw.keys())))
|
||||
|
||||
sect_ids, wkl_ids = self._collect_ids(sections, global_workloads)
|
||||
self._process_global_workloads(state, global_workloads, wkl_ids)
|
||||
self._process_sections(state, sections, sect_ids, wkl_ids)
|
||||
|
||||
state.agenda = source
|
||||
|
||||
except (ConfigError, SerializerSyntaxError) as e:
|
||||
raise ConfigError('Error in "{}":\n\t{}'.format(source, str(e)))
|
||||
|
||||
def _populate_and_validate_config(self, state, raw, source):
|
||||
for name in ['config', 'global']:
|
||||
entry = raw.pop(name, None)
|
||||
if entry is None:
|
||||
continue
|
||||
|
||||
if not isinstance(entry, dict):
|
||||
msg = 'Invalid entry "{}" - must be a dict'
|
||||
raise ConfigError(msg.format(name))
|
||||
|
||||
if 'run_name' in entry:
|
||||
state.run_config.set('run_name', entry.pop('run_name'))
|
||||
|
||||
state.load_config(entry, source, wrap_exceptions=False)
|
||||
|
||||
def _pop_sections(self, raw):
|
||||
sections = raw.pop("sections", [])
|
||||
if not isinstance(sections, list):
|
||||
raise ConfigError('Invalid entry "sections" - must be a list')
|
||||
return sections
|
||||
|
||||
def _pop_workloads(self, raw):
|
||||
workloads = raw.pop("workloads", [])
|
||||
if not isinstance(workloads, list):
|
||||
raise ConfigError('Invalid entry "workloads" - must be a list')
|
||||
return workloads
|
||||
|
||||
def _collect_ids(self, sections, global_workloads):
|
||||
seen_section_ids = set()
|
||||
seen_workload_ids = set()
|
||||
|
||||
for workload in global_workloads:
|
||||
workload = _get_workload_entry(workload)
|
||||
_collect_valid_id(workload.get("id"), seen_workload_ids, "workload")
|
||||
|
||||
for section in sections:
|
||||
_collect_valid_id(section.get("id"), seen_section_ids, "section")
|
||||
for workload in section["workloads"] if "workloads" in section else []:
|
||||
workload = _get_workload_entry(workload)
|
||||
_collect_valid_id(workload.get("id"), seen_workload_ids,
|
||||
"workload")
|
||||
|
||||
return seen_section_ids, seen_workload_ids
|
||||
|
||||
def _process_global_workloads(self, state, global_workloads, seen_wkl_ids):
|
||||
for workload_entry in global_workloads:
|
||||
workload = _process_workload_entry(workload_entry, seen_wkl_ids,
|
||||
state.jobs_config)
|
||||
state.jobs_config.add_workload(workload)
|
||||
|
||||
def _process_sections(self, state, sections, seen_sect_ids, seen_wkl_ids):
|
||||
for section in sections:
|
||||
workloads = []
|
||||
for workload_entry in section.pop("workloads", []):
|
||||
workload = _process_workload_entry(workload_entry, seen_workload_ids,
|
||||
state.jobs_config)
|
||||
workloads.append(workload)
|
||||
|
||||
section = _construct_valid_entry(section, seen_sect_ids,
|
||||
"s", state.jobs_config)
|
||||
state.jobs_config.add_section(section, workloads)
|
||||
|
||||
|
||||
########################
|
||||
### Helper functions ###
|
||||
########################
|
||||
|
||||
def get_aliased_param(cfg_point, d, default=None, pop=True):
|
||||
"""
|
||||
Given a ConfigurationPoint and a dict, this function will search the dict for
|
||||
the ConfigurationPoint's name/aliases. If more than one is found it will raise
|
||||
a ConfigError. If one (and only one) is found then it will return the value
|
||||
for the ConfigurationPoint. If the name or aliases are present in the dict it will
|
||||
return the "default" parameter of this function.
|
||||
"""
|
||||
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))
|
||||
elif alias_map:
|
||||
if pop:
|
||||
return d.pop(alias_map[0])
|
||||
else:
|
||||
return d[alias_map[0]]
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
def _load_file(filepath, error_name):
|
||||
if not os.path.isfile(filepath):
|
||||
raise ValueError("{} does not exist".format(filepath))
|
||||
try:
|
||||
raw = read_pod(filepath)
|
||||
except SerializerSyntaxError as e:
|
||||
raise ConfigError('Error parsing {} {}: {}'.format(error_name, filepath, e))
|
||||
if not isinstance(raw, dict):
|
||||
message = '{} does not contain a valid {} structure; top level must be a dict.'
|
||||
raise ConfigError(message.format(filepath, error_name))
|
||||
return raw
|
||||
|
||||
|
||||
def merge_result_processors_instruments(raw):
|
||||
instr_config = JobSpec.configuration['instrumentation']
|
||||
instruments = toggle_set(get_aliased_param(instr_config, raw, default=[]))
|
||||
result_processors = toggle_set(raw.pop('result_processors', []))
|
||||
if instruments and result_processors:
|
||||
conflicts = instruments.conflicts_with(result_processors)
|
||||
if conflicts:
|
||||
msg = '"instrumentation" and "result_processors" have '\
|
||||
'conflicting entries: {}'
|
||||
entires = ', '.join('"{}"'.format(c.strip("~")) for c in conflicts)
|
||||
raise ConfigError(msg.format(entires))
|
||||
raw['instrumentation'] = instruments.merge_with(result_processors)
|
||||
|
||||
|
||||
def _pop_aliased(d, names, entry_id):
|
||||
name_count = sum(1 for n in names if n in d)
|
||||
if name_count > 1:
|
||||
names_list = ', '.join(names)
|
||||
msg = 'Inivalid workload entry "{}": at moust one of ({}}) must be specified.'
|
||||
raise ConfigError(msg.format(workload_entry['id'], names_list))
|
||||
for name in names:
|
||||
if name in d:
|
||||
return d.pop(name)
|
||||
return None
|
||||
|
||||
|
||||
def _construct_valid_entry(raw, seen_ids, prefix, jobs_config):
|
||||
workload_entry = {}
|
||||
|
||||
# Generate an automatic ID if the entry doesn't already have one
|
||||
if 'id' not in raw:
|
||||
while True:
|
||||
new_id = '{}{}'.format(prefix, counter(name=prefix))
|
||||
if new_id not in seen_ids:
|
||||
break
|
||||
workload_entry['id'] = new_id
|
||||
seen_ids.add(new_id)
|
||||
else:
|
||||
workload_entry['id'] = raw.pop('id')
|
||||
|
||||
# Process instrumentation
|
||||
merge_result_processors_instruments(raw)
|
||||
|
||||
# Validate all workload_entry
|
||||
for name, cfg_point in JobSpec.configuration.iteritems():
|
||||
value = get_aliased_param(cfg_point, raw)
|
||||
if value is not None:
|
||||
value = cfg_point.kind(value)
|
||||
cfg_point.validate_value(name, value)
|
||||
workload_entry[name] = value
|
||||
|
||||
wk_id = workload_entry['id']
|
||||
param_names = ['workload_params', 'workload_parameters']
|
||||
if prefix == 'wk':
|
||||
param_names += ['params', 'parameters']
|
||||
workload_entry["workload_parameters"] = _pop_aliased(raw, param_names, wk_id)
|
||||
|
||||
param_names = ['runtime_parameters', 'runtime_params']
|
||||
if prefix == 's':
|
||||
param_names += ['params', 'parameters']
|
||||
workload_entry["runtime_parameters"] = _pop_aliased(raw, param_names, wk_id)
|
||||
|
||||
param_names = ['boot_parameters', 'boot_params']
|
||||
workload_entry["boot_parameters"] = _pop_aliased(raw, param_names, wk_id)
|
||||
|
||||
if "instrumentation" in workload_entry:
|
||||
jobs_config.update_enabled_instruments(workload_entry["instrumentation"])
|
||||
|
||||
# error if there are unknown workload_entry
|
||||
if raw:
|
||||
msg = 'Invalid entry(ies) in "{}": "{}"'
|
||||
raise ConfigError(msg.format(workload_entry['id'], ', '.join(raw.keys())))
|
||||
|
||||
return workload_entry
|
||||
|
||||
|
||||
def _collect_valid_id(entry_id, seen_ids, entry_type):
|
||||
if entry_id is None:
|
||||
return
|
||||
if entry_id in seen_ids:
|
||||
raise ConfigError('Duplicate {} ID "{}".'.format(entry_type, entry_id))
|
||||
# "-" is reserved for joining section and workload IDs
|
||||
if "-" in entry_id:
|
||||
msg = 'Invalid {} ID "{}"; IDs cannot contain a "-"'
|
||||
raise ConfigError(msg.format(entry_type, entry_id))
|
||||
if entry_id == "global":
|
||||
msg = 'Invalid {} ID "global"; is a reserved ID'
|
||||
raise ConfigError(msg.format(entry_type))
|
||||
seen_ids.add(entry_id)
|
||||
|
||||
|
||||
def _get_workload_entry(workload):
|
||||
if isinstance(workload, basestring):
|
||||
workload = {'name': workload}
|
||||
elif not isinstance(workload, dict):
|
||||
raise ConfigError('Invalid workload entry: "{}"')
|
||||
return workload
|
||||
|
||||
|
||||
def _process_workload_entry(workload, seen_workload_ids, jobs_config):
|
||||
workload = _get_workload_entry(workload)
|
||||
workload = _construct_valid_entry(workload, seen_workload_ids,
|
||||
"wk", jobs_config)
|
||||
return workload
|
||||
|
@ -1,210 +0,0 @@
|
||||
# Copyright 2016 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 copy import copy
|
||||
from collections import defaultdict
|
||||
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.exceptions import ConfigError
|
||||
from wlauto.utils.types import obj_dict
|
||||
from devlib.utils.misc import memoized
|
||||
|
||||
GENERIC_CONFIGS = ["device_config", "workload_parameters",
|
||||
"boot_parameters", "runtime_parameters"]
|
||||
|
||||
|
||||
class PluginCache(object):
|
||||
"""
|
||||
The plugin cache is used to store configuration that cannot be processed at
|
||||
this stage, whether thats because it is unknown if its needed
|
||||
(in the case of disabled plug-ins) or it is not know what it belongs to (in
|
||||
the case of "device-config" ect.). It also maintains where configuration came
|
||||
from, and the priority order of said sources.
|
||||
"""
|
||||
|
||||
def __init__(self, loader=pluginloader):
|
||||
self.loader = loader
|
||||
self.sources = []
|
||||
self.plugin_configs = defaultdict(lambda: defaultdict(dict))
|
||||
self.global_alias_values = defaultdict(dict)
|
||||
|
||||
# Generate a mapping of what global aliases belong to
|
||||
self._global_alias_map = defaultdict(dict)
|
||||
self._list_of_global_aliases = set()
|
||||
for plugin in self.loader.list_plugins():
|
||||
for param in plugin.parameters:
|
||||
if param.global_alias:
|
||||
self._global_alias_map[plugin.name][param.global_alias] = param
|
||||
self._list_of_global_aliases.add(param.global_alias)
|
||||
|
||||
def add_source(self, source):
|
||||
if source in self.sources:
|
||||
raise Exception("Source has already been added.")
|
||||
self.sources.append(source)
|
||||
|
||||
def add_global_alias(self, alias, value, source):
|
||||
if source not in self.sources:
|
||||
msg = "Source '{}' has not been added to the plugin cache."
|
||||
raise RuntimeError(msg.format(source))
|
||||
|
||||
if not self.is_global_alias(alias):
|
||||
msg = "'{} is not a valid global alias'"
|
||||
raise RuntimeError(msg.format(alias))
|
||||
|
||||
self.global_alias_values[alias][source] = value
|
||||
|
||||
def add_configs(self, plugin_name, values, source):
|
||||
if self.is_global_alias(plugin_name):
|
||||
self.add_global_alias(plugin_name, values, source)
|
||||
return
|
||||
for name, value in values.iteritems():
|
||||
self.add_config(plugin_name, name, value, source)
|
||||
|
||||
def add_config(self, plugin_name, name, value, source):
|
||||
if source not in self.sources:
|
||||
msg = "Source '{}' has not been added to the plugin cache."
|
||||
raise RuntimeError(msg.format(source))
|
||||
|
||||
if (not self.loader.has_plugin(plugin_name) and
|
||||
plugin_name not in GENERIC_CONFIGS):
|
||||
msg = 'configuration provided for unknown plugin "{}"'
|
||||
raise ConfigError(msg.format(plugin_name))
|
||||
|
||||
if (plugin_name not in GENERIC_CONFIGS and
|
||||
name not in self.get_plugin_parameters(plugin_name)):
|
||||
msg = "'{}' is not a valid parameter for '{}'"
|
||||
raise ConfigError(msg.format(name, plugin_name))
|
||||
|
||||
self.plugin_configs[plugin_name][source][name] = value
|
||||
|
||||
def is_global_alias(self, name):
|
||||
return name in self._list_of_global_aliases
|
||||
|
||||
def get_plugin_config(self, plugin_name, generic_name=None):
|
||||
config = obj_dict(not_in_dict=['name'])
|
||||
config.name = plugin_name
|
||||
|
||||
# Load plugin defaults
|
||||
cfg_points = self.get_plugin_parameters(plugin_name)
|
||||
for cfg_point in cfg_points.itervalues():
|
||||
cfg_point.set_value(config, check_mandatory=False)
|
||||
|
||||
# Merge global aliases
|
||||
for alias, param in self._global_alias_map[plugin_name].iteritems():
|
||||
if alias in self.global_alias_values:
|
||||
for source in self.sources:
|
||||
if source not in self.global_alias_values[alias]:
|
||||
continue
|
||||
val = self.global_alias_values[alias][source]
|
||||
param.set_value(config, value=val)
|
||||
|
||||
# Merge user config
|
||||
# Perform a simple merge with the order of sources representing priority
|
||||
if generic_name is None:
|
||||
plugin_config = self.plugin_configs[plugin_name]
|
||||
for source in self.sources:
|
||||
if source not in plugin_config:
|
||||
continue
|
||||
for name, value in plugin_config[source].iteritems():
|
||||
cfg_points[name].set_value(config, value=value)
|
||||
# A more complicated merge that involves priority of sources and specificity
|
||||
else:
|
||||
self._merge_using_priority_specificity(plugin_name, generic_name, config)
|
||||
|
||||
return config
|
||||
|
||||
def get_plugin(self, name, kind=None, *args, **kwargs):
|
||||
config = self.get_plugin_config(name)
|
||||
kwargs = dict(config.items() + kwargs.items())
|
||||
return self.loader.get_plugin(name, kind=kind, *args, **kwargs)
|
||||
|
||||
@memoized
|
||||
def get_plugin_parameters(self, name):
|
||||
params = self.loader.get_plugin_class(name).parameters
|
||||
return {param.name: param for param in params}
|
||||
|
||||
# pylint: disable=too-many-nested-blocks, too-many-branches
|
||||
def _merge_using_priority_specificity(self, specific_name,
|
||||
generic_name, final_config):
|
||||
"""
|
||||
WA configuration can come from various sources of increasing priority,
|
||||
as well as being specified in a generic and specific manner (e.g.
|
||||
``device_config`` and ``nexus10`` respectivly). WA has two rules for
|
||||
the priority of configuration:
|
||||
|
||||
- Configuration from higher priority sources overrides
|
||||
configuration from lower priority sources.
|
||||
- More specific configuration overrides less specific configuration.
|
||||
|
||||
There is a situation where these two rules come into conflict. When a
|
||||
generic configuration is given in config source of high priority and a
|
||||
specific configuration is given in a config source of lower priority.
|
||||
In this situation it is not possible to know the end users intention
|
||||
and WA will error.
|
||||
|
||||
:param generic_name: The name of the generic configuration
|
||||
e.g ``device_config``
|
||||
:param specific_name: The name of the specific configuration used
|
||||
e.g ``nexus10``
|
||||
:param cfg_point: A dict of ``ConfigurationPoint``s to be used when
|
||||
merging configuration. keys=config point name,
|
||||
values=config point
|
||||
|
||||
:rtype: A fully merged and validated configuration in the form of a
|
||||
obj_dict.
|
||||
"""
|
||||
generic_config = copy(self.plugin_configs[generic_name])
|
||||
specific_config = copy(self.plugin_configs[specific_name])
|
||||
cfg_points = self.get_plugin_parameters(specific_name)
|
||||
sources = self.sources
|
||||
seen_specific_config = defaultdict(list)
|
||||
|
||||
# set_value uses the 'name' attribute of the passed object in it error
|
||||
# messages, to ensure these messages make sense the name will have to be
|
||||
# changed several times during this function.
|
||||
final_config.name = specific_name
|
||||
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
for source in sources:
|
||||
try:
|
||||
if source in generic_config:
|
||||
final_config.name = generic_name
|
||||
for name, cfg_point in cfg_points.iteritems():
|
||||
if name in generic_config[source]:
|
||||
if name in seen_specific_config:
|
||||
msg = ('"{generic_name}" configuration "{config_name}" has already been '
|
||||
'specified more specifically for {specific_name} in:\n\t\t{sources}')
|
||||
msg = msg.format(generic_name=generic_name,
|
||||
config_name=name,
|
||||
specific_name=specific_name,
|
||||
sources=", ".join(seen_specific_config[name]))
|
||||
raise ConfigError(msg)
|
||||
value = generic_config[source][name]
|
||||
cfg_point.set_value(final_config, value, check_mandatory=False)
|
||||
|
||||
if source in specific_config:
|
||||
final_config.name = specific_name
|
||||
for name, cfg_point in cfg_points.iteritems():
|
||||
if name in specific_config[source]:
|
||||
seen_specific_config[name].append(str(source))
|
||||
value = specific_config[source][name]
|
||||
cfg_point.set_value(final_config, value, check_mandatory=False)
|
||||
|
||||
except ConfigError as e:
|
||||
raise ConfigError('Error in "{}":\n\t{}'.format(source, str(e)))
|
||||
|
||||
# Validate final configuration
|
||||
final_config.name = specific_name
|
||||
for cfg_point in cfg_points.itervalues():
|
||||
cfg_point.validate(final_config)
|
@ -1,89 +0,0 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
|
||||
class JobSpecSource(object):
|
||||
|
||||
kind = ""
|
||||
|
||||
def __init__(self, config, parent=None):
|
||||
self.config = config
|
||||
self.parent = parent
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.config['id']
|
||||
|
||||
def name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class WorkloadEntry(JobSpecSource):
|
||||
kind = "workload"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self.parent.id == "global":
|
||||
return 'workload "{}"'.format(self.id)
|
||||
else:
|
||||
return 'workload "{}" from section "{}"'.format(self.id, self.parent.id)
|
||||
|
||||
|
||||
class SectionNode(JobSpecSource):
|
||||
|
||||
kind = "section"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self.id == "global":
|
||||
return "globally specified configuration"
|
||||
else:
|
||||
return 'section "{}"'.format(self.id)
|
||||
|
||||
@property
|
||||
def is_leaf(self):
|
||||
return not bool(self.children)
|
||||
|
||||
def __init__(self, config, parent=None):
|
||||
super(SectionNode, self).__init__(config, parent=parent)
|
||||
self.workload_entries = []
|
||||
self.children = []
|
||||
|
||||
def add_section(self, section):
|
||||
new_node = SectionNode(section, parent=self)
|
||||
self.children.append(new_node)
|
||||
return new_node
|
||||
|
||||
def add_workload(self, workload_config):
|
||||
self.workload_entries.append(WorkloadEntry(workload_config, self))
|
||||
|
||||
def descendants(self):
|
||||
for child in self.children:
|
||||
for n in child.descendants():
|
||||
yield n
|
||||
yield child
|
||||
|
||||
def ancestors(self):
|
||||
if self.parent is not None:
|
||||
yield self.parent
|
||||
for ancestor in self.parent.ancestors():
|
||||
yield ancestor
|
||||
|
||||
def leaves(self):
|
||||
if self.is_leaf:
|
||||
yield self
|
||||
else:
|
||||
for n in self.descendants():
|
||||
if n.is_leaf:
|
||||
yield n
|
@ -1,198 +0,0 @@
|
||||
import string
|
||||
from copy import copy
|
||||
|
||||
from wlauto.core.plugin import Plugin, Parameter
|
||||
from wlauto.core.configuration.configuration import RuntimeParameter
|
||||
from wlauto.exceptions import ConfigError
|
||||
from wlauto.utils.types import list_of_integers, list_of, caseless_string
|
||||
|
||||
from devlib.platform import Platform
|
||||
from devlib.target import AndroidTarget, Cpuinfo, KernelVersion, KernelConfig
|
||||
|
||||
__all__ = ['RuntimeParameter', 'CoreParameter', 'DeviceManager', 'TargetInfo']
|
||||
|
||||
UNKOWN_RTP = 'Unknown runtime parameter "{}"'
|
||||
|
||||
|
||||
class TargetInfo(object):
|
||||
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
instance = TargetInfo()
|
||||
instance.target = pod['target']
|
||||
instance.abi = pod['abi']
|
||||
instance.cpuinfo = Cpuinfo(pod['cpuinfo'])
|
||||
instance.os = pod['os']
|
||||
instance.os_version = pod['os_version']
|
||||
instance.abi = pod['abi']
|
||||
instance.is_rooted = pod['is_rooted']
|
||||
instance.kernel_version = KernelVersion(pod['kernel_release'],
|
||||
pod['kernel_version'])
|
||||
instance.kernel_config = KernelConfig(pod['kernel_config'])
|
||||
|
||||
if pod["target"] == "AndroidTarget":
|
||||
instance.screen_resolution = pod['screen_resolution']
|
||||
instance.prop = pod['prop']
|
||||
instance.prop = pod['android_id']
|
||||
|
||||
return instance
|
||||
|
||||
def __init__(self, target=None):
|
||||
if target:
|
||||
self.target = target.__class__.__name__
|
||||
self.cpuinfo = target.cpuinfo
|
||||
self.os = target.os
|
||||
self.os_version = target.os_version
|
||||
self.abi = target.abi
|
||||
self.is_rooted = target.is_rooted
|
||||
self.kernel_version = target.kernel_version
|
||||
self.kernel_config = target.config
|
||||
|
||||
if isinstance(target, AndroidTarget):
|
||||
self.screen_resolution = target.screen_resolution
|
||||
self.prop = target.getprop()
|
||||
self.android_id = target.android_id
|
||||
|
||||
else:
|
||||
self.target = None
|
||||
self.cpuinfo = None
|
||||
self.os = None
|
||||
self.os_version = None
|
||||
self.abi = None
|
||||
self.is_rooted = None
|
||||
self.kernel_version = None
|
||||
self.kernel_config = None
|
||||
|
||||
if isinstance(target, AndroidTarget):
|
||||
self.screen_resolution = None
|
||||
self.prop = None
|
||||
self.android_id = None
|
||||
|
||||
def to_pod(self):
|
||||
pod = {}
|
||||
pod['target'] = self.target
|
||||
pod['abi'] = self.abi
|
||||
pod['cpuinfo'] = self.cpuinfo.sections
|
||||
pod['os'] = self.os
|
||||
pod['os_version'] = self.os_version
|
||||
pod['abi'] = self.abi
|
||||
pod['is_rooted'] = self.is_rooted
|
||||
pod['kernel_release'] = self.kernel_version.release
|
||||
pod['kernel_version'] = self.kernel_version.version
|
||||
pod['kernel_config'] = dict(self.kernel_config.iteritems())
|
||||
|
||||
if self.target == "AndroidTarget":
|
||||
pod['screen_resolution'] = self.screen_resolution
|
||||
pod['prop'] = self.prop
|
||||
pod['android_id'] = self.android_id
|
||||
|
||||
return pod
|
||||
|
||||
|
||||
class DeviceManager(Plugin):
|
||||
|
||||
kind = "manager"
|
||||
name = None
|
||||
target_type = None
|
||||
platform_type = Platform
|
||||
has_gpu = None
|
||||
path_module = None
|
||||
info = None
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', kind=list_of(caseless_string),
|
||||
description="""
|
||||
This is a list of all cpu cores on the device with each
|
||||
element being the core type, e.g. ``['a7', 'a7', 'a15']``. The
|
||||
order of the cores must match the order they are listed in
|
||||
``'/sys/devices/system/cpu'``. So in this case, ``'cpu0'`` must
|
||||
be an A7 core, and ``'cpu2'`` an A15.'
|
||||
"""),
|
||||
Parameter('core_clusters', kind=list_of_integers,
|
||||
description="""
|
||||
This is a list indicating the cluster affinity of the CPU cores,
|
||||
each element correponding to the cluster ID of the core coresponding
|
||||
to its index. E.g. ``[0, 0, 1]`` indicates that cpu0 and cpu1 are on
|
||||
cluster 0, while cpu2 is on cluster 1. If this is not specified, this
|
||||
will be inferred from ``core_names`` if possible (assuming all cores with
|
||||
the same name are on the same cluster).
|
||||
"""),
|
||||
Parameter('working_directory',
|
||||
description='''
|
||||
Working directory to be used by WA. This must be in a location where the specified user
|
||||
has write permissions. This will default to /home/<username>/wa (or to /root/wa, if
|
||||
username is 'root').
|
||||
'''),
|
||||
Parameter('binaries_directory',
|
||||
description='Location of executable binaries on this device (must be in PATH).'),
|
||||
]
|
||||
modules = []
|
||||
|
||||
runtime_parameter_managers = [
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(DeviceManager, self).__init__()
|
||||
self.runtime_parameter_values = None
|
||||
|
||||
# Framework
|
||||
|
||||
def connect(self):
|
||||
raise NotImplementedError("connect method must be implemented for device managers")
|
||||
|
||||
def initialize(self, context):
|
||||
super(DeviceManager, self).initialize(context)
|
||||
self.info = TargetInfo(self.target)
|
||||
self.target.setup()
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
|
||||
# Runtime Parameters
|
||||
|
||||
def merge_runtime_parameters(self, params):
|
||||
merged_values = {}
|
||||
for source, values in params.iteritems():
|
||||
for name, value in values:
|
||||
for rtpm in self.runtime_parameter_managers:
|
||||
if rtpm.match(name):
|
||||
rtpm.update_value(name, value, source, merged_values)
|
||||
break
|
||||
else:
|
||||
msg = 'Unknown runtime parameter "{}" in "{}"'
|
||||
raise ConfigError(msg.format(name, source))
|
||||
return merged_values
|
||||
|
||||
def static_runtime_parameter_validation(self, params):
|
||||
params = copy(params)
|
||||
for rtpm in self.runtime_parameters_managers:
|
||||
rtpm.static_validation(params)
|
||||
if params:
|
||||
msg = 'Unknown runtime_parameters for "{}": "{}"'
|
||||
raise ConfigError(msg.format(self.name, '", "'.join(params.iterkeys())))
|
||||
|
||||
def dynamic_runtime_parameter_validation(self, params):
|
||||
for rtpm in self.runtime_parameters_managers:
|
||||
rtpm.dynamic_validation(params)
|
||||
|
||||
def commit_runtime_parameters(self, params):
|
||||
params = copy(params)
|
||||
for rtpm in self.runtime_parameters_managers:
|
||||
rtpm.commit(params)
|
||||
|
||||
#Runtime parameter getters/setters
|
||||
def get_sysfile_values(self):
|
||||
return self._written_sysfiles
|
||||
|
||||
def set_sysfile_values(self, params):
|
||||
for sysfile, value in params.iteritems():
|
||||
verify = not sysfile.endswith('!')
|
||||
sysfile = sysfile.rstrip('!')
|
||||
self._written_sysfiles.append((sysfile, value))
|
||||
self.target.write_value(sysfile, value, verify=verify)
|
@ -1,108 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import warnings
|
||||
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.core.command import init_argument_parser
|
||||
from wlauto.core.configuration import settings
|
||||
from wlauto.core.configuration.manager import ConfigManager
|
||||
from wlauto.core.host import init_user_directory
|
||||
from wlauto.exceptions import WAError, DevlibError, ConfigError
|
||||
from wlauto.utils.doc import format_body
|
||||
from wlauto.utils.log import init_logging
|
||||
from wlauto.utils.misc import get_traceback
|
||||
|
||||
warnings.filterwarnings(action='ignore', category=UserWarning, module='zope')
|
||||
|
||||
|
||||
logger = logging.getLogger('command_line')
|
||||
|
||||
|
||||
def load_commands(subparsers):
|
||||
commands = {}
|
||||
for command in pluginloader.list_commands():
|
||||
commands[command.name] = pluginloader.get_command(command.name,
|
||||
subparsers=subparsers)
|
||||
return commands
|
||||
|
||||
|
||||
def main():
|
||||
config = ConfigManager()
|
||||
|
||||
if not os.path.exists(settings.user_directory):
|
||||
init_user_directory()
|
||||
|
||||
try:
|
||||
|
||||
description = ("Execute automated workloads on a remote device and process "
|
||||
"the resulting output.\n\nUse \"wa <subcommand> -h\" to see "
|
||||
"help for individual subcommands.")
|
||||
parser = argparse.ArgumentParser(description=format_body(description, 80),
|
||||
prog='wa',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
init_argument_parser(parser)
|
||||
# each command will add its own subparser
|
||||
commands = load_commands(parser.add_subparsers(dest='command'))
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
settings.set("verbosity", args.verbose)
|
||||
|
||||
config.load_config_file(settings.user_config_file)
|
||||
for config_file in args.config:
|
||||
if not os.path.exists(config_file):
|
||||
raise ConfigError("Config file {} not found".format(config_file))
|
||||
config.load_config_file(config_file)
|
||||
|
||||
init_logging(settings.verbosity)
|
||||
|
||||
command = commands[args.command]
|
||||
sys.exit(command.execute(config, args))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info('Got CTRL-C. Aborting.')
|
||||
sys.exit(3)
|
||||
except (WAError, DevlibError) as e:
|
||||
logging.critical(e)
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
tb = get_traceback()
|
||||
logging.critical(tb)
|
||||
command = e.cmd
|
||||
if e.args:
|
||||
command = '{} {}'.format(command, ' '.join(e.args))
|
||||
message = 'Command \'{}\' returned non-zero exit status {}\nOUTPUT:\n{}\n'
|
||||
logging.critical(message.format(command, e.returncode, e.output))
|
||||
sys.exit(2)
|
||||
except SyntaxError as e:
|
||||
tb = get_traceback()
|
||||
logging.critical(tb)
|
||||
message = 'Syntax Error in {}, line {}, offset {}:'
|
||||
logging.critical(message.format(e.filename, e.lineno, e.offset))
|
||||
logging.critical('\t{}'.format(e.msg))
|
||||
sys.exit(2)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
tb = get_traceback()
|
||||
logging.critical(tb)
|
||||
logging.critical('{}({})'.format(e.__class__.__name__, e))
|
||||
sys.exit(2)
|
@ -1,875 +0,0 @@
|
||||
# Copyright 2013-2015 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=no-member
|
||||
|
||||
"""
|
||||
This module contains the execution logic for Workload Automation. It defines the
|
||||
following actors:
|
||||
|
||||
WorkloadSpec: Identifies the workload to be run and defines parameters under
|
||||
which it should be executed.
|
||||
|
||||
Executor: Responsible for the overall execution process. It instantiates
|
||||
and/or intialises the other actors, does any necessary vaidation
|
||||
and kicks off the whole process.
|
||||
|
||||
Execution Context: Provides information about the current state of run
|
||||
execution to instrumentation.
|
||||
|
||||
RunInfo: Information about the current run.
|
||||
|
||||
Runner: This executes workload specs that are passed to it. It goes through
|
||||
stages of execution, emitting an appropriate signal at each step to
|
||||
allow instrumentation to do its stuff.
|
||||
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import uuid
|
||||
from collections import Counter, defaultdict, OrderedDict
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
from itertools import izip_longest
|
||||
|
||||
import wlauto.core.signal as signal
|
||||
from wlauto.core import instrumentation
|
||||
from wlauto.core import pluginloader
|
||||
from wlauto.core.configuration import settings
|
||||
from wlauto.core.device_manager import TargetInfo
|
||||
from wlauto.core.plugin import Artifact
|
||||
from wlauto.core.resolver import ResourceResolver
|
||||
from wlauto.core.result import ResultManager, IterationResult, RunResult
|
||||
from wlauto.exceptions import (WAError, ConfigError, TimeoutError, InstrumentError,
|
||||
DeviceError, DeviceNotRespondingError)
|
||||
from wlauto.utils.misc import (ensure_directory_exists as _d,
|
||||
get_traceback, format_duration)
|
||||
from wlauto.utils.serializer import json
|
||||
|
||||
|
||||
# The maximum number of reboot attempts for an iteration.
|
||||
MAX_REBOOT_ATTEMPTS = 3
|
||||
|
||||
# If something went wrong during device initialization, wait this
|
||||
# long (in seconds) before retrying. This is necessary, as retrying
|
||||
# immediately may not give the device enough time to recover to be able
|
||||
# to reboot.
|
||||
REBOOT_DELAY = 3
|
||||
|
||||
|
||||
class ExecutionContext(object):
|
||||
|
||||
|
||||
def __init__(self, cm, tm, output):
|
||||
self.logger = logging.getLogger('ExecContext')
|
||||
self.cm = cm
|
||||
self.tm = tm
|
||||
self.output = output
|
||||
self.logger.debug('Loading resource discoverers')
|
||||
self.resolver = ResourceResolver(cm)
|
||||
self.resolver.load()
|
||||
|
||||
|
||||
class OldExecutionContext(object):
|
||||
"""
|
||||
Provides a context for instrumentation. Keeps track of things like
|
||||
current workload and iteration.
|
||||
|
||||
This class also provides two status members that can be used by workloads
|
||||
and instrumentation to keep track of arbitrary state. ``result``
|
||||
is reset on each new iteration of a workload; run_status is maintained
|
||||
throughout a Workload Automation run.
|
||||
|
||||
"""
|
||||
|
||||
# These are the artifacts generated by the core framework.
|
||||
default_run_artifacts = [
|
||||
Artifact('runlog', 'run.log', 'log', mandatory=True,
|
||||
description='The log for the entire run.'),
|
||||
]
|
||||
|
||||
@property
|
||||
def current_iteration(self):
|
||||
if self.current_job:
|
||||
spec_id = self.current_job.spec.id
|
||||
return self.job_iteration_counts[spec_id]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def job_status(self):
|
||||
if not self.current_job:
|
||||
return None
|
||||
return self.current_job.result.status
|
||||
|
||||
@property
|
||||
def workload(self):
|
||||
return getattr(self.spec, 'workload', None)
|
||||
|
||||
@property
|
||||
def spec(self):
|
||||
return getattr(self.current_job, 'spec', None)
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
return getattr(self.current_job, 'result', self.run_result)
|
||||
|
||||
def __init__(self, device_manager, config):
|
||||
self.device_manager = device_manager
|
||||
self.device = self.device_manager.target
|
||||
self.config = config
|
||||
self.reboot_policy = config.reboot_policy
|
||||
self.output_directory = None
|
||||
self.current_job = None
|
||||
self.resolver = None
|
||||
self.last_error = None
|
||||
self.run_info = None
|
||||
self.run_result = None
|
||||
self.run_output_directory = self.config.output_directory
|
||||
self.host_working_directory = self.config.meta_directory
|
||||
self.iteration_artifacts = None
|
||||
self.run_artifacts = copy(self.default_run_artifacts)
|
||||
self.job_iteration_counts = defaultdict(int)
|
||||
self.aborted = False
|
||||
self.runner = None
|
||||
if config.agenda.filepath:
|
||||
self.run_artifacts.append(Artifact('agenda',
|
||||
os.path.join(self.host_working_directory,
|
||||
os.path.basename(config.agenda.filepath)),
|
||||
'meta',
|
||||
mandatory=True,
|
||||
description='Agenda for this run.'))
|
||||
for i, filepath in enumerate(settings.config_paths, 1):
|
||||
name = 'config_{}'.format(i)
|
||||
path = os.path.join(self.host_working_directory,
|
||||
name + os.path.splitext(filepath)[1])
|
||||
self.run_artifacts.append(Artifact(name,
|
||||
path,
|
||||
kind='meta',
|
||||
mandatory=True,
|
||||
description='Config file used for the run.'))
|
||||
|
||||
def initialize(self):
|
||||
if not os.path.isdir(self.run_output_directory):
|
||||
os.makedirs(self.run_output_directory)
|
||||
self.output_directory = self.run_output_directory
|
||||
self.resolver = ResourceResolver(self.config)
|
||||
self.run_info = RunInfo(self.config)
|
||||
self.run_result = RunResult(self.run_info, self.run_output_directory)
|
||||
|
||||
def next_job(self, job):
|
||||
"""Invoked by the runner when starting a new iteration of workload execution."""
|
||||
self.current_job = job
|
||||
self.job_iteration_counts[self.spec.id] += 1
|
||||
if not self.aborted:
|
||||
outdir_name = '_'.join(map(str, [self.spec.label, self.spec.id, self.current_iteration]))
|
||||
self.output_directory = _d(os.path.join(self.run_output_directory, outdir_name))
|
||||
self.iteration_artifacts = [wa for wa in self.workload.artifacts]
|
||||
self.current_job.result.iteration = self.current_iteration
|
||||
self.current_job.result.output_directory = self.output_directory
|
||||
|
||||
def end_job(self):
|
||||
if self.current_job.result.status == IterationResult.ABORTED:
|
||||
self.aborted = True
|
||||
self.current_job = None
|
||||
self.output_directory = self.run_output_directory
|
||||
|
||||
def add_metric(self, *args, **kwargs):
|
||||
self.result.add_metric(*args, **kwargs)
|
||||
|
||||
def add_artifact(self, name, path, kind, *args, **kwargs):
|
||||
if self.current_job is None:
|
||||
self.add_run_artifact(name, path, kind, *args, **kwargs)
|
||||
else:
|
||||
self.add_iteration_artifact(name, path, kind, *args, **kwargs)
|
||||
|
||||
def add_run_artifact(self, name, path, kind, *args, **kwargs):
|
||||
path = _check_artifact_path(path, self.run_output_directory)
|
||||
self.run_artifacts.append(Artifact(name, path, kind, Artifact.ITERATION, *args, **kwargs))
|
||||
|
||||
def add_iteration_artifact(self, name, path, kind, *args, **kwargs):
|
||||
path = _check_artifact_path(path, self.output_directory)
|
||||
self.iteration_artifacts.append(Artifact(name, path, kind, Artifact.RUN, *args, **kwargs))
|
||||
|
||||
def get_artifact(self, name):
|
||||
if self.iteration_artifacts:
|
||||
for art in self.iteration_artifacts:
|
||||
if art.name == name:
|
||||
return art
|
||||
for art in self.run_artifacts:
|
||||
if art.name == name:
|
||||
return art
|
||||
return None
|
||||
|
||||
|
||||
def _check_artifact_path(path, rootpath):
|
||||
if path.startswith(rootpath):
|
||||
return os.path.abspath(path)
|
||||
rootpath = os.path.abspath(rootpath)
|
||||
full_path = os.path.join(rootpath, path)
|
||||
if not os.path.isfile(full_path):
|
||||
raise ValueError('Cannot add artifact because {} does not exist.'.format(full_path))
|
||||
return full_path
|
||||
|
||||
|
||||
class FakeTargetManager(object):
|
||||
# TODO: this is a FAKE
|
||||
|
||||
def __init__(self, name, config):
|
||||
self.device_name = name
|
||||
self.device_config = config
|
||||
|
||||
from devlib import LocalLinuxTarget
|
||||
self.target = LocalLinuxTarget({'unrooted': True})
|
||||
|
||||
def get_target_info(self):
|
||||
return TargetInfo(self.target)
|
||||
|
||||
def validate_runtime_parameters(self, params):
|
||||
pass
|
||||
|
||||
def merge_runtime_parameters(self, params):
|
||||
pass
|
||||
|
||||
|
||||
def init_target_manager(config):
|
||||
return FakeTargetManager(config.device, config.device_config)
|
||||
|
||||
|
||||
class Executor(object):
|
||||
"""
|
||||
The ``Executor``'s job is to set up the execution context and pass to a
|
||||
``Runner`` along with a loaded run specification. Once the ``Runner`` has
|
||||
done its thing, the ``Executor`` performs some final reporint before
|
||||
returning.
|
||||
|
||||
The initial context set up involves combining configuration from various
|
||||
sources, loading of requided workloads, loading and installation of
|
||||
instruments and result processors, etc. Static validation of the combined
|
||||
configuration is also performed.
|
||||
|
||||
"""
|
||||
# pylint: disable=R0915
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('Executor')
|
||||
self.error_logged = False
|
||||
self.warning_logged = False
|
||||
pluginloader = None
|
||||
self.device_manager = None
|
||||
self.device = None
|
||||
self.context = None
|
||||
|
||||
def execute(self, config_manager, output):
|
||||
"""
|
||||
Execute the run specified by an agenda. Optionally, selectors may be
|
||||
used to only selecute a subset of the specified agenda.
|
||||
|
||||
Params::
|
||||
|
||||
:state: a ``ConfigManager`` containing processed configuraiton
|
||||
:output: an initialized ``RunOutput`` that will be used to
|
||||
store the results.
|
||||
|
||||
"""
|
||||
signal.connect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
||||
signal.connect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
||||
|
||||
self.logger.info('Initializing run')
|
||||
self.logger.debug('Finalizing run configuration.')
|
||||
config = config_manager.finalize()
|
||||
output.write_config(config)
|
||||
|
||||
self.logger.info('Connecting to target')
|
||||
target_manager = init_target_manager(config.run_config)
|
||||
output.write_target_info(target_manager.get_target_info())
|
||||
|
||||
self.logger.info('Initializing execution conetext')
|
||||
context = ExecutionContext(config_manager, target_manager, output)
|
||||
|
||||
self.logger.info('Generating jobs')
|
||||
config_manager.generate_jobs(context)
|
||||
output.write_job_specs(config_manager.job_specs)
|
||||
|
||||
self.logger.info('Installing instrumentation')
|
||||
for instrument in config_manager.get_instruments(target_manager.target):
|
||||
instrumentation.install(instrument)
|
||||
instrumentation.validate()
|
||||
|
||||
def old_exec(self, agenda, selectors={}):
|
||||
self.config.set_agenda(agenda, selectors)
|
||||
self.config.finalize()
|
||||
config_outfile = os.path.join(self.config.meta_directory, 'run_config.json')
|
||||
with open(config_outfile, 'w') as wfh:
|
||||
json.dump(self.config, wfh)
|
||||
|
||||
self.logger.debug('Initialising device configuration.')
|
||||
if not self.config.device:
|
||||
raise ConfigError('Make sure a device is specified in the config.')
|
||||
self.device_manager = pluginloader.get_manager(self.config.device,
|
||||
**self.config.device_config)
|
||||
self.device_manager.validate()
|
||||
self.device = self.device_manager.target
|
||||
|
||||
self.context = ExecutionContext(self.device_manager, self.config)
|
||||
|
||||
self.logger.debug('Loading resource discoverers.')
|
||||
self.context.initialize()
|
||||
self.context.resolver.load()
|
||||
self.context.add_artifact('run_config', config_outfile, 'meta')
|
||||
|
||||
self.logger.debug('Installing instrumentation')
|
||||
for name, params in self.config.instrumentation.iteritems():
|
||||
instrument = pluginloader.get_instrument(name, self.device, **params)
|
||||
instrumentation.install(instrument)
|
||||
instrumentation.validate()
|
||||
|
||||
self.logger.debug('Installing result processors')
|
||||
result_manager = ResultManager()
|
||||
for name, params in self.config.result_processors.iteritems():
|
||||
processor = pluginloader.get_result_processor(name, **params)
|
||||
result_manager.install(processor)
|
||||
result_manager.validate()
|
||||
|
||||
self.logger.debug('Loading workload specs')
|
||||
for workload_spec in self.config.workload_specs:
|
||||
workload_spec.load(self.device, pluginloader)
|
||||
workload_spec.workload.init_resources(self.context)
|
||||
workload_spec.workload.validate()
|
||||
|
||||
if self.config.flashing_config:
|
||||
if not self.device.flasher:
|
||||
msg = 'flashing_config specified for {} device that does not support flashing.'
|
||||
raise ConfigError(msg.format(self.device.name))
|
||||
self.logger.debug('Flashing the device')
|
||||
self.device.flasher.flash(self.device)
|
||||
|
||||
self.logger.info('Running workloads')
|
||||
runner = self._get_runner(result_manager)
|
||||
runner.init_queue(self.config.workload_specs)
|
||||
runner.run()
|
||||
self.execute_postamble()
|
||||
|
||||
def execute_postamble(self):
|
||||
"""
|
||||
This happens after the run has completed. The overall results of the run are
|
||||
summarised to the user.
|
||||
|
||||
"""
|
||||
result = self.context.run_result
|
||||
counter = Counter()
|
||||
for ir in result.iteration_results:
|
||||
counter[ir.status] += 1
|
||||
self.logger.info('Done.')
|
||||
self.logger.info('Run duration: {}'.format(format_duration(self.context.run_info.duration)))
|
||||
status_summary = 'Ran a total of {} iterations: '.format(sum(self.context.job_iteration_counts.values()))
|
||||
parts = []
|
||||
for status in IterationResult.values:
|
||||
if status in counter:
|
||||
parts.append('{} {}'.format(counter[status], status))
|
||||
self.logger.info(status_summary + ', '.join(parts))
|
||||
self.logger.info('Results can be found in {}'.format(self.config.output_directory))
|
||||
|
||||
if self.error_logged:
|
||||
self.logger.warn('There were errors during execution.')
|
||||
self.logger.warn('Please see {}'.format(self.config.log_file))
|
||||
elif self.warning_logged:
|
||||
self.logger.warn('There were warnings during execution.')
|
||||
self.logger.warn('Please see {}'.format(self.config.log_file))
|
||||
|
||||
def _get_runner(self, result_manager):
|
||||
if not self.config.execution_order or self.config.execution_order == 'by_iteration':
|
||||
if self.config.reboot_policy == 'each_spec':
|
||||
self.logger.info('each_spec reboot policy with the default by_iteration execution order is '
|
||||
'equivalent to each_iteration policy.')
|
||||
runnercls = ByIterationRunner
|
||||
elif self.config.execution_order in ['classic', 'by_spec']:
|
||||
runnercls = BySpecRunner
|
||||
elif self.config.execution_order == 'by_section':
|
||||
runnercls = BySectionRunner
|
||||
elif self.config.execution_order == 'random':
|
||||
runnercls = RandomRunner
|
||||
else:
|
||||
raise ConfigError('Unexpected execution order: {}'.format(self.config.execution_order))
|
||||
return runnercls(self.device_manager, self.context, result_manager)
|
||||
|
||||
def _error_signalled_callback(self):
|
||||
self.error_logged = True
|
||||
signal.disconnect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
||||
|
||||
def _warning_signalled_callback(self):
|
||||
self.warning_logged = True
|
||||
signal.disconnect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
||||
|
||||
|
||||
class Runner(object):
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class RunnerJob(object):
|
||||
"""
|
||||
Represents a single execution of a ``RunnerJobDescription``. There will be one created for each iteration
|
||||
specified by ``RunnerJobDescription.number_of_iterations``.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, spec, retry=0):
|
||||
self.spec = spec
|
||||
self.retry = retry
|
||||
self.iteration = None
|
||||
self.result = IterationResult(self.spec)
|
||||
|
||||
|
||||
class OldRunner(object):
|
||||
"""
|
||||
This class is responsible for actually performing a workload automation
|
||||
run. The main responsibility of this class is to emit appropriate signals
|
||||
at the various stages of the run to allow things like traces an other
|
||||
instrumentation to hook into the process.
|
||||
|
||||
This is an abstract base class that defines each step of the run, but not
|
||||
the order in which those steps are executed, which is left to the concrete
|
||||
derived classes.
|
||||
|
||||
"""
|
||||
class _RunnerError(Exception):
|
||||
"""Internal runner error."""
|
||||
pass
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.context.config
|
||||
|
||||
@property
|
||||
def current_job(self):
|
||||
if self.job_queue:
|
||||
return self.job_queue[0]
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_job(self):
|
||||
if self.completed_jobs:
|
||||
return self.completed_jobs[-1]
|
||||
return None
|
||||
|
||||
@property
|
||||
def next_job(self):
|
||||
if self.job_queue:
|
||||
if len(self.job_queue) > 1:
|
||||
return self.job_queue[1]
|
||||
return None
|
||||
|
||||
@property
|
||||
def spec_changed(self):
|
||||
if self.previous_job is None and self.current_job is not None: # Start of run
|
||||
return True
|
||||
if self.previous_job is not None and self.current_job is None: # End of run
|
||||
return True
|
||||
return self.current_job.spec.id != self.previous_job.spec.id
|
||||
|
||||
@property
|
||||
def spec_will_change(self):
|
||||
if self.current_job is None and self.next_job is not None: # Start of run
|
||||
return True
|
||||
if self.current_job is not None and self.next_job is None: # End of run
|
||||
return True
|
||||
return self.current_job.spec.id != self.next_job.spec.id
|
||||
|
||||
def __init__(self, device_manager, context, result_manager):
|
||||
self.device_manager = device_manager
|
||||
self.device = device_manager.target
|
||||
self.context = context
|
||||
self.result_manager = result_manager
|
||||
self.logger = logging.getLogger('Runner')
|
||||
self.job_queue = []
|
||||
self.completed_jobs = []
|
||||
self._initial_reset = True
|
||||
|
||||
def init_queue(self, specs):
|
||||
raise NotImplementedError()
|
||||
|
||||
def run(self): # pylint: disable=too-many-branches
|
||||
self._send(signal.RUN_START)
|
||||
self._initialize_run()
|
||||
|
||||
try:
|
||||
while self.job_queue:
|
||||
try:
|
||||
self._init_job()
|
||||
self._run_job()
|
||||
except KeyboardInterrupt:
|
||||
self.current_job.result.status = IterationResult.ABORTED
|
||||
raise
|
||||
except Exception, e: # pylint: disable=broad-except
|
||||
self.current_job.result.status = IterationResult.FAILED
|
||||
self.current_job.result.add_event(e.message)
|
||||
if isinstance(e, DeviceNotRespondingError):
|
||||
self.logger.info('Device appears to be unresponsive.')
|
||||
if self.context.reboot_policy.can_reboot and self.device.can('reset_power'):
|
||||
self.logger.info('Attempting to hard-reset the device...')
|
||||
try:
|
||||
self.device.boot(hard=True)
|
||||
self.device.connect()
|
||||
except DeviceError: # hard_boot not implemented for the device.
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
else: # not a DeviceNotRespondingError
|
||||
self.logger.error(e)
|
||||
finally:
|
||||
self._finalize_job()
|
||||
except KeyboardInterrupt:
|
||||
self.logger.info('Got CTRL-C. Finalizing run... (CTRL-C again to abort).')
|
||||
# Skip through the remaining jobs.
|
||||
while self.job_queue:
|
||||
self.context.next_job(self.current_job)
|
||||
self.current_job.result.status = IterationResult.ABORTED
|
||||
self._finalize_job()
|
||||
except DeviceNotRespondingError:
|
||||
self.logger.info('Device unresponsive and recovery not possible. Skipping the rest of the run.')
|
||||
self.context.aborted = True
|
||||
while self.job_queue:
|
||||
self.context.next_job(self.current_job)
|
||||
self.current_job.result.status = IterationResult.SKIPPED
|
||||
self._finalize_job()
|
||||
|
||||
instrumentation.enable_all()
|
||||
self._finalize_run()
|
||||
self._process_results()
|
||||
|
||||
self.result_manager.finalize(self.context)
|
||||
self._send(signal.RUN_END)
|
||||
|
||||
def _initialize_run(self):
|
||||
self.context.runner = self
|
||||
self.context.run_info.start_time = datetime.utcnow()
|
||||
self._connect_to_device()
|
||||
self.logger.info('Initializing device')
|
||||
self.device_manager.initialize(self.context)
|
||||
|
||||
self.logger.info('Initializing workloads')
|
||||
for workload_spec in self.context.config.workload_specs:
|
||||
workload_spec.workload.initialize(self.context)
|
||||
|
||||
self.context.run_info.device_properties = self.device_manager.info
|
||||
self.result_manager.initialize(self.context)
|
||||
self._send(signal.RUN_INIT)
|
||||
|
||||
if instrumentation.check_failures():
|
||||
raise InstrumentError('Detected failure(s) during instrumentation initialization.')
|
||||
|
||||
def _connect_to_device(self):
|
||||
if self.context.reboot_policy.perform_initial_boot:
|
||||
try:
|
||||
self.device_manager.connect()
|
||||
except DeviceError: # device may be offline
|
||||
if self.device.can('reset_power'):
|
||||
with self._signal_wrap('INITIAL_BOOT'):
|
||||
self.device.boot(hard=True)
|
||||
else:
|
||||
raise DeviceError('Cannot connect to device for initial reboot; '
|
||||
'and device does not support hard reset.')
|
||||
else: # successfully connected
|
||||
self.logger.info('\tBooting device')
|
||||
with self._signal_wrap('INITIAL_BOOT'):
|
||||
self._reboot_device()
|
||||
else:
|
||||
self.logger.info('Connecting to device')
|
||||
self.device_manager.connect()
|
||||
|
||||
def _init_job(self):
|
||||
self.current_job.result.status = IterationResult.RUNNING
|
||||
self.context.next_job(self.current_job)
|
||||
|
||||
def _run_job(self): # pylint: disable=too-many-branches
|
||||
spec = self.current_job.spec
|
||||
if not spec.enabled:
|
||||
self.logger.info('Skipping workload %s (iteration %s)', spec, self.context.current_iteration)
|
||||
self.current_job.result.status = IterationResult.SKIPPED
|
||||
return
|
||||
|
||||
self.logger.info('Running workload %s (iteration %s)', spec, self.context.current_iteration)
|
||||
if spec.flash:
|
||||
if not self.context.reboot_policy.can_reboot:
|
||||
raise ConfigError('Cannot flash as reboot_policy does not permit rebooting.')
|
||||
if not self.device.can('flash'):
|
||||
raise DeviceError('Device does not support flashing.')
|
||||
self._flash_device(spec.flash)
|
||||
elif not self.completed_jobs:
|
||||
# Never reboot on the very fist job of a run, as we would have done
|
||||
# the initial reboot if a reboot was needed.
|
||||
pass
|
||||
elif self.context.reboot_policy.reboot_on_each_spec and self.spec_changed:
|
||||
self.logger.debug('Rebooting on spec change.')
|
||||
self._reboot_device()
|
||||
elif self.context.reboot_policy.reboot_on_each_iteration:
|
||||
self.logger.debug('Rebooting on iteration.')
|
||||
self._reboot_device()
|
||||
|
||||
instrumentation.disable_all()
|
||||
instrumentation.enable(spec.instrumentation)
|
||||
self.device_manager.start()
|
||||
|
||||
if self.spec_changed:
|
||||
self._send(signal.WORKLOAD_SPEC_START)
|
||||
self._send(signal.ITERATION_START)
|
||||
|
||||
try:
|
||||
setup_ok = False
|
||||
with self._handle_errors('Setting up device parameters'):
|
||||
self.device_manager.set_runtime_parameters(spec.runtime_parameters)
|
||||
setup_ok = True
|
||||
|
||||
if setup_ok:
|
||||
with self._handle_errors('running {}'.format(spec.workload.name)):
|
||||
self.current_job.result.status = IterationResult.RUNNING
|
||||
self._run_workload_iteration(spec.workload)
|
||||
else:
|
||||
self.logger.info('\tSkipping the rest of the iterations for this spec.')
|
||||
spec.enabled = False
|
||||
except KeyboardInterrupt:
|
||||
self._send(signal.ITERATION_END)
|
||||
self._send(signal.WORKLOAD_SPEC_END)
|
||||
raise
|
||||
else:
|
||||
self._send(signal.ITERATION_END)
|
||||
if self.spec_will_change or not spec.enabled:
|
||||
self._send(signal.WORKLOAD_SPEC_END)
|
||||
finally:
|
||||
self.device_manager.stop()
|
||||
|
||||
def _finalize_job(self):
|
||||
self.context.run_result.iteration_results.append(self.current_job.result)
|
||||
job = self.job_queue.pop(0)
|
||||
job.iteration = self.context.current_iteration
|
||||
if job.result.status in self.config.retry_on_status:
|
||||
if job.retry >= self.config.max_retries:
|
||||
self.logger.error('Exceeded maxium number of retries. Abandoning job.')
|
||||
else:
|
||||
self.logger.info('Job status was {}. Retrying...'.format(job.result.status))
|
||||
retry_job = RunnerJob(job.spec, job.retry + 1)
|
||||
self.job_queue.insert(0, retry_job)
|
||||
self.completed_jobs.append(job)
|
||||
self.context.end_job()
|
||||
|
||||
def _finalize_run(self):
|
||||
self.logger.info('Finalizing workloads')
|
||||
for workload_spec in self.context.config.workload_specs:
|
||||
workload_spec.workload.finalize(self.context)
|
||||
|
||||
self.logger.info('Finalizing.')
|
||||
self._send(signal.RUN_FIN)
|
||||
|
||||
with self._handle_errors('Disconnecting from the device'):
|
||||
self.device.disconnect()
|
||||
|
||||
info = self.context.run_info
|
||||
info.end_time = datetime.utcnow()
|
||||
info.duration = info.end_time - info.start_time
|
||||
|
||||
def _process_results(self):
|
||||
self.logger.info('Processing overall results')
|
||||
with self._signal_wrap('OVERALL_RESULTS_PROCESSING'):
|
||||
if instrumentation.check_failures():
|
||||
self.context.run_result.non_iteration_errors = True
|
||||
self.result_manager.process_run_result(self.context.run_result, self.context)
|
||||
|
||||
def _run_workload_iteration(self, workload):
|
||||
self.logger.info('\tSetting up')
|
||||
with self._signal_wrap('WORKLOAD_SETUP'):
|
||||
try:
|
||||
workload.setup(self.context)
|
||||
except:
|
||||
self.logger.info('\tSkipping the rest of the iterations for this spec.')
|
||||
self.current_job.spec.enabled = False
|
||||
raise
|
||||
try:
|
||||
|
||||
self.logger.info('\tExecuting')
|
||||
with self._handle_errors('Running workload'):
|
||||
with self._signal_wrap('WORKLOAD_EXECUTION'):
|
||||
workload.run(self.context)
|
||||
|
||||
self.logger.info('\tProcessing result')
|
||||
self._send(signal.BEFORE_WORKLOAD_RESULT_UPDATE)
|
||||
try:
|
||||
if self.current_job.result.status != IterationResult.FAILED:
|
||||
with self._handle_errors('Processing workload result',
|
||||
on_error_status=IterationResult.PARTIAL):
|
||||
workload.update_result(self.context)
|
||||
self._send(signal.SUCCESSFUL_WORKLOAD_RESULT_UPDATE)
|
||||
|
||||
if self.current_job.result.status == IterationResult.RUNNING:
|
||||
self.current_job.result.status = IterationResult.OK
|
||||
finally:
|
||||
self._send(signal.AFTER_WORKLOAD_RESULT_UPDATE)
|
||||
|
||||
finally:
|
||||
self.logger.info('\tTearing down')
|
||||
with self._handle_errors('Tearing down workload',
|
||||
on_error_status=IterationResult.NONCRITICAL):
|
||||
with self._signal_wrap('WORKLOAD_TEARDOWN'):
|
||||
workload.teardown(self.context)
|
||||
self.result_manager.add_result(self.current_job.result, self.context)
|
||||
|
||||
def _flash_device(self, flashing_params):
|
||||
with self._signal_wrap('FLASHING'):
|
||||
self.device.flash(**flashing_params)
|
||||
self.device.connect()
|
||||
|
||||
def _reboot_device(self):
|
||||
with self._signal_wrap('BOOT'):
|
||||
for reboot_attempts in xrange(MAX_REBOOT_ATTEMPTS):
|
||||
if reboot_attempts:
|
||||
self.logger.info('\tRetrying...')
|
||||
with self._handle_errors('Rebooting device'):
|
||||
self.device.boot(**self.current_job.spec.boot_parameters)
|
||||
break
|
||||
else:
|
||||
raise DeviceError('Could not reboot device; max reboot attempts exceeded.')
|
||||
self.device.connect()
|
||||
|
||||
def _send(self, s):
|
||||
signal.send(s, self, self.context)
|
||||
|
||||
def _take_screenshot(self, filename):
|
||||
if self.context.output_directory:
|
||||
filepath = os.path.join(self.context.output_directory, filename)
|
||||
else:
|
||||
filepath = os.path.join(settings.output_directory, filename)
|
||||
self.device.capture_screen(filepath)
|
||||
|
||||
@contextmanager
|
||||
def _handle_errors(self, action, on_error_status=IterationResult.FAILED):
|
||||
try:
|
||||
if action is not None:
|
||||
self.logger.debug(action)
|
||||
yield
|
||||
except (KeyboardInterrupt, DeviceNotRespondingError):
|
||||
raise
|
||||
except (WAError, TimeoutError), we:
|
||||
self.device.check_responsive()
|
||||
if self.current_job:
|
||||
self.current_job.result.status = on_error_status
|
||||
self.current_job.result.add_event(str(we))
|
||||
try:
|
||||
self._take_screenshot('error.png')
|
||||
except Exception, e: # pylint: disable=W0703
|
||||
# We're already in error state, so the fact that taking a
|
||||
# screenshot failed is not surprising...
|
||||
pass
|
||||
if action:
|
||||
action = action[0].lower() + action[1:]
|
||||
self.logger.error('Error while {}:\n\t{}'.format(action, we))
|
||||
except Exception, e: # pylint: disable=W0703
|
||||
error_text = '{}("{}")'.format(e.__class__.__name__, e)
|
||||
if self.current_job:
|
||||
self.current_job.result.status = on_error_status
|
||||
self.current_job.result.add_event(error_text)
|
||||
self.logger.error('Error while {}'.format(action))
|
||||
self.logger.error(error_text)
|
||||
if isinstance(e, subprocess.CalledProcessError):
|
||||
self.logger.error('Got:')
|
||||
self.logger.error(e.output)
|
||||
tb = get_traceback()
|
||||
self.logger.error(tb)
|
||||
|
||||
@contextmanager
|
||||
def _signal_wrap(self, signal_name):
|
||||
"""Wraps the suite in before/after signals, ensuring
|
||||
that after signal is always sent."""
|
||||
before_signal = getattr(signal, 'BEFORE_' + signal_name)
|
||||
success_signal = getattr(signal, 'SUCCESSFUL_' + signal_name)
|
||||
after_signal = getattr(signal, 'AFTER_' + signal_name)
|
||||
try:
|
||||
self._send(before_signal)
|
||||
yield
|
||||
self._send(success_signal)
|
||||
finally:
|
||||
self._send(after_signal)
|
||||
|
||||
|
||||
class BySpecRunner(Runner):
|
||||
"""
|
||||
This is that "classic" implementation that executes all iterations of a workload
|
||||
spec before proceeding onto the next spec.
|
||||
|
||||
"""
|
||||
|
||||
def init_queue(self, specs):
|
||||
jobs = [[RunnerJob(s) for _ in xrange(s.number_of_iterations)] for s in specs] # pylint: disable=unused-variable
|
||||
self.job_queue = [j for spec_jobs in jobs for j in spec_jobs]
|
||||
|
||||
|
||||
class BySectionRunner(Runner):
|
||||
"""
|
||||
Runs the first iteration for all benchmarks first, before proceeding to the next iteration,
|
||||
i.e. A1, B1, C1, A2, B2, C2... instead of A1, A1, B1, B2, C1, C2...
|
||||
|
||||
If multiple sections where specified in the agenda, this will run all specs for the first section
|
||||
followed by all specs for the seciod section, etc.
|
||||
|
||||
e.g. given sections X and Y, and global specs A and B, with 2 iterations, this will run
|
||||
|
||||
X.A1, X.B1, Y.A1, Y.B1, X.A2, X.B2, Y.A2, Y.B2
|
||||
|
||||
"""
|
||||
|
||||
def init_queue(self, specs):
|
||||
jobs = [[RunnerJob(s) for _ in xrange(s.number_of_iterations)] for s in specs]
|
||||
self.job_queue = [j for spec_jobs in izip_longest(*jobs) for j in spec_jobs if j]
|
||||
|
||||
|
||||
class ByIterationRunner(Runner):
|
||||
"""
|
||||
Runs the first iteration for all benchmarks first, before proceeding to the next iteration,
|
||||
i.e. A1, B1, C1, A2, B2, C2... instead of A1, A1, B1, B2, C1, C2...
|
||||
|
||||
If multiple sections where specified in the agenda, this will run all sections for the first global
|
||||
spec first, followed by all sections for the second spec, etc.
|
||||
|
||||
e.g. given sections X and Y, and global specs A and B, with 2 iterations, this will run
|
||||
|
||||
X.A1, Y.A1, X.B1, Y.B1, X.A2, Y.A2, X.B2, Y.B2
|
||||
|
||||
"""
|
||||
|
||||
def init_queue(self, specs):
|
||||
sections = OrderedDict()
|
||||
for s in specs:
|
||||
if s.section_id not in sections:
|
||||
sections[s.section_id] = []
|
||||
sections[s.section_id].append(s)
|
||||
specs = [s for section_specs in izip_longest(*sections.values()) for s in section_specs if s]
|
||||
jobs = [[RunnerJob(s) for _ in xrange(s.number_of_iterations)] for s in specs]
|
||||
self.job_queue = [j for spec_jobs in izip_longest(*jobs) for j in spec_jobs if j]
|
||||
|
||||
|
||||
class RandomRunner(Runner):
|
||||
"""
|
||||
This will run specs in a random order.
|
||||
|
||||
"""
|
||||
|
||||
def init_queue(self, specs):
|
||||
jobs = [[RunnerJob(s) for _ in xrange(s.number_of_iterations)] for s in specs] # pylint: disable=unused-variable
|
||||
all_jobs = [j for spec_jobs in jobs for j in spec_jobs]
|
||||
random.shuffle(all_jobs)
|
||||
self.job_queue = all_jobs
|
@ -1,32 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
# Separate module to avoid circular dependencies
|
||||
from wlauto.core.configuration import settings
|
||||
from wlauto.core.plugin import Plugin
|
||||
from wlauto.utils.misc import load_class
|
||||
from wlauto.core import pluginloader
|
||||
|
||||
|
||||
def get_plugin_type(ext):
|
||||
"""Given an instance of ``wlauto.core.Plugin``, return a string representing
|
||||
the type of the plugin (e.g. ``'workload'`` for a Workload subclass instance)."""
|
||||
if not isinstance(ext, Plugin):
|
||||
raise ValueError('{} is not an instance of Plugin'.format(ext))
|
||||
for name, cls in pluginloaderkind_map.iteritems():
|
||||
if isinstance(ext, cls):
|
||||
return name
|
||||
raise ValueError('Unknown plugin type: {}'.format(ext.__class__.__name__))
|
@ -1,33 +0,0 @@
|
||||
import os
|
||||
|
||||
from wlauto.core.configuration import settings
|
||||
|
||||
def init_user_directory(overwrite_existing=False): # pylint: disable=R0914
|
||||
"""
|
||||
Initialise a fresh user directory.
|
||||
"""
|
||||
if os.path.exists(settings.user_directory):
|
||||
if not overwrite_existing:
|
||||
raise RuntimeError('Environment {} already exists.'.format(settings.user_directory))
|
||||
shutil.rmtree(settings.user_directory)
|
||||
|
||||
os.makedirs(settings.user_directory)
|
||||
os.makedirs(settings.dependencies_directory)
|
||||
os.makedirs(settings.plugins_directory)
|
||||
|
||||
# TODO: generate default config.yaml here
|
||||
|
||||
if os.getenv('USER') == 'root':
|
||||
# If running with sudo on POSIX, change the ownership to the real user.
|
||||
real_user = os.getenv('SUDO_USER')
|
||||
if real_user:
|
||||
import pwd # done here as module won't import on win32
|
||||
user_entry = pwd.getpwnam(real_user)
|
||||
uid, gid = user_entry.pw_uid, user_entry.pw_gid
|
||||
os.chown(settings.user_directory, uid, gid)
|
||||
# why, oh why isn't there a recusive=True option for os.chown?
|
||||
for root, dirs, files in os.walk(settings.user_directory):
|
||||
for d in dirs:
|
||||
os.chown(os.path.join(root, d), uid, gid)
|
||||
for f in files:
|
||||
os.chown(os.path.join(root, f), uid, gid)
|
@ -1,399 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
Adding New Instrument
|
||||
=====================
|
||||
|
||||
Any new instrument should be a subclass of Instrument and it must have a name.
|
||||
When a new instrument is added to Workload Automation, the methods of the new
|
||||
instrument will be found automatically and hooked up to the supported signals.
|
||||
Once a signal is broadcasted, the corresponding registered method is invoked.
|
||||
|
||||
Each method in Instrument must take two arguments, which are self and context.
|
||||
Supported signals can be found in [... link to signals ...] To make
|
||||
implementations easier and common, the basic steps to add new instrument is
|
||||
similar to the steps to add new workload.
|
||||
|
||||
Hence, the following methods are sufficient to implement to add new instrument:
|
||||
|
||||
- setup: This method is invoked after the workload is setup. All the
|
||||
necessary setups should go inside this method. Setup, includes operations
|
||||
like, pushing the files to the target device, install them, clear logs,
|
||||
etc.
|
||||
- start: It is invoked just before the workload start execution. Here is
|
||||
where instrument measures start being registered/taken.
|
||||
- stop: It is invoked just after the workload execution stops. The measures
|
||||
should stop being taken/registered.
|
||||
- update_result: It is invoked after the workload updated its result.
|
||||
update_result is where the taken measures are added to the result so it
|
||||
can be processed by Workload Automation.
|
||||
- teardown is invoked after the workload is teared down. It is a good place
|
||||
to clean any logs generated by the instrument.
|
||||
|
||||
For example, to add an instrument which will trace device errors, we subclass
|
||||
Instrument and overwrite the variable name.::
|
||||
|
||||
#BINARY_FILE = os.path.join(os.path.dirname(__file__), 'trace')
|
||||
class TraceErrorsInstrument(Instrument):
|
||||
|
||||
name = 'trace-errors'
|
||||
|
||||
def __init__(self, device):
|
||||
super(TraceErrorsInstrument, self).__init__(device)
|
||||
self.trace_on_device = os.path.join(self.device.working_directory, 'trace')
|
||||
|
||||
We then declare and implement the aforementioned methods. For the setup method,
|
||||
we want to push the file to the target device and then change the file mode to
|
||||
755 ::
|
||||
|
||||
def setup(self, context):
|
||||
self.device.push(BINARY_FILE, self.device.working_directory)
|
||||
self.device.execute('chmod 755 {}'.format(self.trace_on_device))
|
||||
|
||||
Then we implemented the start method, which will simply run the file to start
|
||||
tracing. ::
|
||||
|
||||
def start(self, context):
|
||||
self.device.execute('{} start'.format(self.trace_on_device))
|
||||
|
||||
Lastly, we need to stop tracing once the workload stops and this happens in the
|
||||
stop method::
|
||||
|
||||
def stop(self, context):
|
||||
self.device.execute('{} stop'.format(self.trace_on_device))
|
||||
|
||||
The generated result can be updated inside update_result, or if it is trace, we
|
||||
just pull the file to the host device. context has a result variable which
|
||||
has add_metric method. It can be used to add the instrumentation results metrics
|
||||
to the final result for the workload. The method can be passed 4 params, which
|
||||
are metric key, value, unit and lower_is_better, which is a boolean. ::
|
||||
|
||||
def update_result(self, context):
|
||||
# pull the trace file to the device
|
||||
result = os.path.join(self.device.working_directory, 'trace.txt')
|
||||
self.device.pull(result, context.working_directory)
|
||||
|
||||
# parse the file if needs to be parsed, or add result to
|
||||
# context.result
|
||||
|
||||
At the end, we might want to delete any files generated by the instrumentation
|
||||
and the code to clear these file goes in teardown method. ::
|
||||
|
||||
def teardown(self, context):
|
||||
self.device.remove(os.path.join(self.device.working_directory, 'trace.txt'))
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import inspect
|
||||
from collections import OrderedDict
|
||||
|
||||
import wlauto.core.signal as signal
|
||||
from wlauto.core.plugin import Plugin
|
||||
from wlauto.exceptions import WAError, DeviceNotRespondingError, TimeoutError
|
||||
from wlauto.utils.misc import get_traceback, isiterable
|
||||
from wlauto.utils.types import identifier
|
||||
|
||||
|
||||
logger = logging.getLogger('instrumentation')
|
||||
|
||||
|
||||
# Maps method names onto signals the should be registered to.
|
||||
# Note: the begin/end signals are paired -- if a begin_ signal is sent,
|
||||
# then the corresponding end_ signal is guaranteed to also be sent.
|
||||
# Note: using OrderedDict to preserve logical ordering for the table generated
|
||||
# in the documentation
|
||||
SIGNAL_MAP = OrderedDict([
|
||||
# Below are "aliases" for some of the more common signals to allow
|
||||
# instrumentation to have similar structure to workloads
|
||||
('initialize', signal.RUN_INIT),
|
||||
('setup', signal.SUCCESSFUL_WORKLOAD_SETUP),
|
||||
('start', signal.BEFORE_WORKLOAD_EXECUTION),
|
||||
('stop', signal.AFTER_WORKLOAD_EXECUTION),
|
||||
('process_workload_result', signal.SUCCESSFUL_WORKLOAD_RESULT_UPDATE),
|
||||
('update_result', signal.AFTER_WORKLOAD_RESULT_UPDATE),
|
||||
('teardown', signal.AFTER_WORKLOAD_TEARDOWN),
|
||||
('finalize', signal.RUN_FIN),
|
||||
|
||||
('on_run_start', signal.RUN_START),
|
||||
('on_run_end', signal.RUN_END),
|
||||
('on_workload_spec_start', signal.WORKLOAD_SPEC_START),
|
||||
('on_workload_spec_end', signal.WORKLOAD_SPEC_END),
|
||||
('on_iteration_start', signal.ITERATION_START),
|
||||
('on_iteration_end', signal.ITERATION_END),
|
||||
|
||||
('before_initial_boot', signal.BEFORE_INITIAL_BOOT),
|
||||
('on_successful_initial_boot', signal.SUCCESSFUL_INITIAL_BOOT),
|
||||
('after_initial_boot', signal.AFTER_INITIAL_BOOT),
|
||||
('before_first_iteration_boot', signal.BEFORE_FIRST_ITERATION_BOOT),
|
||||
('on_successful_first_iteration_boot', signal.SUCCESSFUL_FIRST_ITERATION_BOOT),
|
||||
('after_first_iteration_boot', signal.AFTER_FIRST_ITERATION_BOOT),
|
||||
('before_boot', signal.BEFORE_BOOT),
|
||||
('on_successful_boot', signal.SUCCESSFUL_BOOT),
|
||||
('after_boot', signal.AFTER_BOOT),
|
||||
|
||||
('on_spec_init', signal.SPEC_INIT),
|
||||
('on_run_init', signal.RUN_INIT),
|
||||
('on_iteration_init', signal.ITERATION_INIT),
|
||||
|
||||
('before_workload_setup', signal.BEFORE_WORKLOAD_SETUP),
|
||||
('on_successful_workload_setup', signal.SUCCESSFUL_WORKLOAD_SETUP),
|
||||
('after_workload_setup', signal.AFTER_WORKLOAD_SETUP),
|
||||
('before_workload_execution', signal.BEFORE_WORKLOAD_EXECUTION),
|
||||
('on_successful_workload_execution', signal.SUCCESSFUL_WORKLOAD_EXECUTION),
|
||||
('after_workload_execution', signal.AFTER_WORKLOAD_EXECUTION),
|
||||
('before_workload_result_update', signal.BEFORE_WORKLOAD_RESULT_UPDATE),
|
||||
('on_successful_workload_result_update', signal.SUCCESSFUL_WORKLOAD_RESULT_UPDATE),
|
||||
('after_workload_result_update', signal.AFTER_WORKLOAD_RESULT_UPDATE),
|
||||
('before_workload_teardown', signal.BEFORE_WORKLOAD_TEARDOWN),
|
||||
('on_successful_workload_teardown', signal.SUCCESSFUL_WORKLOAD_TEARDOWN),
|
||||
('after_workload_teardown', signal.AFTER_WORKLOAD_TEARDOWN),
|
||||
|
||||
('before_overall_results_processing', signal.BEFORE_OVERALL_RESULTS_PROCESSING),
|
||||
('on_successful_overall_results_processing', signal.SUCCESSFUL_OVERALL_RESULTS_PROCESSING),
|
||||
('after_overall_results_processing', signal.AFTER_OVERALL_RESULTS_PROCESSING),
|
||||
|
||||
('on_error', signal.ERROR_LOGGED),
|
||||
('on_warning', signal.WARNING_LOGGED),
|
||||
])
|
||||
|
||||
PRIORITY_MAP = OrderedDict([
|
||||
('very_fast_', 20),
|
||||
('fast_', 10),
|
||||
('normal_', 0),
|
||||
('slow_', -10),
|
||||
('very_slow_', -20),
|
||||
])
|
||||
|
||||
installed = []
|
||||
|
||||
|
||||
def is_installed(instrument):
|
||||
if isinstance(instrument, Instrument):
|
||||
if instrument in installed:
|
||||
return True
|
||||
if instrument.name in [i.name for i in installed]:
|
||||
return True
|
||||
elif isinstance(instrument, type):
|
||||
if instrument in [i.__class__ for i in installed]:
|
||||
return True
|
||||
else: # assume string
|
||||
if identifier(instrument) in [identifier(i.name) for i in installed]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_enabled(instrument):
|
||||
if isinstance(instrument, Instrument) or isinstance(instrument, type):
|
||||
name = instrument.name
|
||||
else: # assume string
|
||||
name = instrument
|
||||
try:
|
||||
installed_instrument = get_instrument(name)
|
||||
return installed_instrument.is_enabled
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
failures_detected = False
|
||||
|
||||
|
||||
def reset_failures():
|
||||
global failures_detected # pylint: disable=W0603
|
||||
failures_detected = False
|
||||
|
||||
|
||||
def check_failures():
|
||||
result = failures_detected
|
||||
reset_failures()
|
||||
return result
|
||||
|
||||
|
||||
class ManagedCallback(object):
|
||||
"""
|
||||
This wraps instruments' callbacks to ensure that errors do interfer
|
||||
with run execution.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, instrument, callback):
|
||||
self.instrument = instrument
|
||||
self.callback = callback
|
||||
|
||||
def __call__(self, context):
|
||||
if self.instrument.is_enabled:
|
||||
try:
|
||||
self.callback(context)
|
||||
except (KeyboardInterrupt, DeviceNotRespondingError, TimeoutError): # pylint: disable=W0703
|
||||
raise
|
||||
except Exception as e: # pylint: disable=W0703
|
||||
logger.error('Error in insturment {}'.format(self.instrument.name))
|
||||
global failures_detected # pylint: disable=W0603
|
||||
failures_detected = True
|
||||
if isinstance(e, WAError):
|
||||
logger.error(e)
|
||||
else:
|
||||
tb = get_traceback()
|
||||
logger.error(tb)
|
||||
logger.error('{}({})'.format(e.__class__.__name__, e))
|
||||
if not context.current_iteration:
|
||||
# Error occureed outside of an iteration (most likely
|
||||
# during intial setup or teardown). Since this would affect
|
||||
# the rest of the run, mark the instument as broken so that
|
||||
# it doesn't get re-enabled for subsequent iterations.
|
||||
self.instrument.is_broken = True
|
||||
disable(self.instrument)
|
||||
|
||||
|
||||
# Need this to keep track of callbacks, because the dispatcher only keeps
|
||||
# weak references, so if the callbacks aren't referenced elsewhere, they will
|
||||
# be deallocated before they've had a chance to be invoked.
|
||||
_callbacks = []
|
||||
|
||||
|
||||
def install(instrument):
|
||||
"""
|
||||
This will look for methods (or any callable members) with specific names
|
||||
in the instrument and hook them up to the corresponding signals.
|
||||
|
||||
:param instrument: Instrument instance to install.
|
||||
|
||||
"""
|
||||
logger.debug('Installing instrument %s.', instrument)
|
||||
if is_installed(instrument):
|
||||
raise ValueError('Instrument {} is already installed.'.format(instrument.name))
|
||||
for attr_name in dir(instrument):
|
||||
priority = 0
|
||||
stripped_attr_name = attr_name
|
||||
for key, value in PRIORITY_MAP.iteritems():
|
||||
if attr_name.startswith(key):
|
||||
stripped_attr_name = attr_name[len(key):]
|
||||
priority = value
|
||||
break
|
||||
if stripped_attr_name in SIGNAL_MAP:
|
||||
attr = getattr(instrument, attr_name)
|
||||
if not callable(attr):
|
||||
raise ValueError('Attribute {} not callable in {}.'.format(attr_name, instrument))
|
||||
argspec = inspect.getargspec(attr)
|
||||
arg_num = len(argspec.args)
|
||||
# Instrument callbacks will be passed exactly two arguments: self
|
||||
# (the instrument instance to which the callback is bound) and
|
||||
# context. However, we also allow callbacks to capture the context
|
||||
# in variable arguments (declared as "*args" in the definition).
|
||||
if arg_num > 2 or (arg_num < 2 and argspec.varargs is None):
|
||||
message = '{} must take exactly 2 positional arguments; {} given.'
|
||||
raise ValueError(message.format(attr_name, arg_num))
|
||||
|
||||
logger.debug('\tConnecting %s to %s', attr.__name__, SIGNAL_MAP[stripped_attr_name])
|
||||
mc = ManagedCallback(instrument, attr)
|
||||
_callbacks.append(mc)
|
||||
signal.connect(mc, SIGNAL_MAP[stripped_attr_name], priority=priority)
|
||||
installed.append(instrument)
|
||||
|
||||
|
||||
def uninstall(instrument):
|
||||
instrument = get_instrument(instrument)
|
||||
installed.remove(instrument)
|
||||
|
||||
|
||||
def validate():
|
||||
for instrument in installed:
|
||||
instrument.validate()
|
||||
|
||||
|
||||
def get_instrument(inst):
|
||||
if isinstance(inst, Instrument):
|
||||
return inst
|
||||
for installed_inst in installed:
|
||||
if identifier(installed_inst.name) == identifier(inst):
|
||||
return installed_inst
|
||||
raise ValueError('Instrument {} is not installed'.format(inst))
|
||||
|
||||
|
||||
def disable_all():
|
||||
for instrument in installed:
|
||||
_disable_instrument(instrument)
|
||||
|
||||
|
||||
def enable_all():
|
||||
for instrument in installed:
|
||||
_enable_instrument(instrument)
|
||||
|
||||
|
||||
def enable(to_enable):
|
||||
if isiterable(to_enable):
|
||||
for inst in to_enable:
|
||||
_enable_instrument(inst)
|
||||
else:
|
||||
_enable_instrument(to_enable)
|
||||
|
||||
|
||||
def disable(to_disable):
|
||||
if isiterable(to_disable):
|
||||
for inst in to_disable:
|
||||
_disable_instrument(inst)
|
||||
else:
|
||||
_disable_instrument(to_disable)
|
||||
|
||||
|
||||
def _enable_instrument(inst):
|
||||
inst = get_instrument(inst)
|
||||
if not inst.is_broken:
|
||||
logger.debug('Enabling instrument {}'.format(inst.name))
|
||||
inst.is_enabled = True
|
||||
else:
|
||||
logger.debug('Not enabling broken instrument {}'.format(inst.name))
|
||||
|
||||
|
||||
def _disable_instrument(inst):
|
||||
inst = get_instrument(inst)
|
||||
if inst.is_enabled:
|
||||
logger.debug('Disabling instrument {}'.format(inst.name))
|
||||
inst.is_enabled = False
|
||||
|
||||
|
||||
def get_enabled():
|
||||
return [i for i in installed if i.is_enabled]
|
||||
|
||||
|
||||
def get_disabled():
|
||||
return [i for i in installed if not i.is_enabled]
|
||||
|
||||
|
||||
class Instrument(Plugin):
|
||||
"""
|
||||
Base class for instrumentation implementations.
|
||||
"""
|
||||
kind = "instrument"
|
||||
|
||||
def __init__(self, target, **kwargs):
|
||||
super(Instrument, self).__init__(**kwargs)
|
||||
self.target = target
|
||||
self.is_enabled = True
|
||||
self.is_broken = False
|
||||
|
||||
def initialize(self, context):
|
||||
pass
|
||||
|
||||
def finalize(self, context):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return 'Instrument({})'.format(self.name)
|
@ -1,188 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import string
|
||||
import sys
|
||||
import uuid
|
||||
from copy import copy
|
||||
|
||||
from wlauto.core.configuration.configuration import JobSpec
|
||||
from wlauto.core.configuration.manager import ConfigManager
|
||||
from wlauto.core.device_manager import TargetInfo
|
||||
from wlauto.utils.misc import touch
|
||||
from wlauto.utils.serializer import write_pod, read_pod
|
||||
|
||||
|
||||
logger = logging.getLogger('output')
|
||||
|
||||
|
||||
class RunInfo(object):
|
||||
"""
|
||||
Information about the current run, such as its unique ID, run
|
||||
time, etc.
|
||||
|
||||
"""
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
uid = pod.pop('uuid')
|
||||
if uid is not None:
|
||||
uid = uuid.UUID(uid)
|
||||
instance = RunInfo(**pod)
|
||||
instance.uuid = uid
|
||||
return instance
|
||||
|
||||
def __init__(self, run_name=None, project=None, project_stage=None,
|
||||
start_time=None, end_time=None, duration=None):
|
||||
self.uuid = uuid.uuid4()
|
||||
self.run_name = None
|
||||
self.project = None
|
||||
self.project_stage = None
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.duration = None
|
||||
|
||||
def to_pod(self):
|
||||
d = copy(self.__dict__)
|
||||
d['uuid'] = str(self.uuid)
|
||||
return d
|
||||
|
||||
|
||||
class RunState(object):
|
||||
"""
|
||||
Represents the state of a WA run.
|
||||
|
||||
"""
|
||||
@staticmethod
|
||||
def from_pod(pod):
|
||||
return RunState()
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def to_pod(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RunOutput(object):
|
||||
|
||||
@property
|
||||
def logfile(self):
|
||||
return os.path.join(self.basepath, 'run.log')
|
||||
|
||||
@property
|
||||
def metadir(self):
|
||||
return os.path.join(self.basepath, '__meta')
|
||||
|
||||
@property
|
||||
def infofile(self):
|
||||
return os.path.join(self.metadir, 'run_info.json')
|
||||
|
||||
@property
|
||||
def statefile(self):
|
||||
return os.path.join(self.basepath, '.run_state.json')
|
||||
|
||||
@property
|
||||
def configfile(self):
|
||||
return os.path.join(self.metadir, 'config.json')
|
||||
|
||||
@property
|
||||
def targetfile(self):
|
||||
return os.path.join(self.metadir, 'target_info.json')
|
||||
|
||||
@property
|
||||
def jobsfile(self):
|
||||
return os.path.join(self.metadir, 'jobs.json')
|
||||
|
||||
@property
|
||||
def raw_config_dir(self):
|
||||
return os.path.join(self.metadir, 'raw_config')
|
||||
|
||||
def __init__(self, path):
|
||||
self.basepath = path
|
||||
self.info = None
|
||||
self.state = None
|
||||
if (not os.path.isfile(self.statefile) or
|
||||
not os.path.isfile(self.infofile)):
|
||||
msg = '"{}" does not exist or is not a valid WA output directory.'
|
||||
raise ValueError(msg.format(self.basepath))
|
||||
self.reload()
|
||||
|
||||
def reload(self):
|
||||
self.info = RunInfo.from_pod(read_pod(self.infofile))
|
||||
self.state = RunState.from_pod(read_pod(self.statefile))
|
||||
|
||||
def write_info(self):
|
||||
write_pod(self.info.to_pod(), self.infofile)
|
||||
|
||||
def write_state(self):
|
||||
write_pod(self.state.to_pod(), self.statefile)
|
||||
|
||||
def write_config(self, config):
|
||||
write_pod(config.to_pod(), self.configfile)
|
||||
|
||||
def read_config(self):
|
||||
if not os.path.isfile(self.configfile):
|
||||
return None
|
||||
return ConfigManager.from_pod(read_pod(self.configfile))
|
||||
|
||||
def write_target_info(self, ti):
|
||||
write_pod(ti.to_pod(), self.targetfile)
|
||||
|
||||
def read_config(self):
|
||||
if not os.path.isfile(self.targetfile):
|
||||
return None
|
||||
return TargetInfo.from_pod(read_pod(self.targetfile))
|
||||
|
||||
def write_job_specs(self, job_specs):
|
||||
job_specs[0].to_pod()
|
||||
js_pod = {'jobs': [js.to_pod() for js in job_specs]}
|
||||
write_pod(js_pod, self.jobsfile)
|
||||
|
||||
def read_job_specs(self):
|
||||
if not os.path.isfile(self.jobsfile):
|
||||
return None
|
||||
pod = read_pod(self.jobsfile)
|
||||
return [JobSpec.from_pod(jp) for jp in pod['jobs']]
|
||||
|
||||
|
||||
def init_wa_output(path, wa_state, force=False):
|
||||
if os.path.exists(path):
|
||||
if force:
|
||||
logger.info('Removing existing output directory.')
|
||||
shutil.rmtree(os.path.abspath(path))
|
||||
else:
|
||||
raise RuntimeError('path exists: {}'.format(path))
|
||||
|
||||
logger.info('Creating output directory.')
|
||||
os.makedirs(path)
|
||||
meta_dir = os.path.join(path, '__meta')
|
||||
os.makedirs(meta_dir)
|
||||
_save_raw_config(meta_dir, wa_state)
|
||||
touch(os.path.join(path, 'run.log'))
|
||||
|
||||
info = RunInfo(
|
||||
run_name=wa_state.run_config.run_name,
|
||||
project=wa_state.run_config.project,
|
||||
project_stage=wa_state.run_config.project_stage,
|
||||
)
|
||||
write_pod(info.to_pod(), os.path.join(meta_dir, 'run_info.json'))
|
||||
|
||||
with open(os.path.join(path, '.run_state.json'), 'w') as wfh:
|
||||
wfh.write('{}')
|
||||
|
||||
return RunOutput(path)
|
||||
|
||||
|
||||
def _save_raw_config(meta_dir, state):
|
||||
raw_config_dir = os.path.join(meta_dir, 'raw_config')
|
||||
os.makedirs(raw_config_dir)
|
||||
|
||||
for i, source in enumerate(state.loaded_config_sources):
|
||||
if not os.path.isfile(source):
|
||||
continue
|
||||
basename = os.path.basename(source)
|
||||
dest_path = os.path.join(raw_config_dir, 'cfg{}-{}'.format(i, basename))
|
||||
shutil.copy(source, dest_path)
|
||||
|
||||
|
||||
|
@ -1,793 +0,0 @@
|
||||
# Copyright 2013-2015 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=E1101
|
||||
import os
|
||||
import sys
|
||||
import inspect
|
||||
import imp
|
||||
import string
|
||||
import logging
|
||||
from collections import OrderedDict, defaultdict
|
||||
from itertools import chain
|
||||
from copy import copy
|
||||
|
||||
from wlauto.exceptions import NotFoundError, LoaderError, ValidationError, ConfigError, HostError
|
||||
from wlauto.utils.misc import (ensure_directory_exists as _d,
|
||||
walk_modules, load_class, merge_dicts_simple, get_article)
|
||||
from wlauto.core.configuration import settings
|
||||
from wlauto.utils.types import identifier, boolean
|
||||
from wlauto.core.configuration.configuration import ConfigurationPoint as Parameter
|
||||
|
||||
|
||||
MODNAME_TRANS = string.maketrans(':/\\.', '____')
|
||||
|
||||
|
||||
class AttributeCollection(object):
|
||||
"""
|
||||
Accumulator for plugin attribute objects (such as Parameters or Artifacts). This will
|
||||
replace any class member list accumulating such attributes through the magic of
|
||||
metaprogramming\ [*]_.
|
||||
|
||||
.. [*] which is totally safe and not going backfire in any way...
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
return self._attrs.values()
|
||||
|
||||
def __init__(self, attrcls):
|
||||
self._attrcls = attrcls
|
||||
self._attrs = OrderedDict()
|
||||
|
||||
def add(self, p):
|
||||
p = self._to_attrcls(p)
|
||||
if p.name in self._attrs:
|
||||
if p.override:
|
||||
newp = copy(self._attrs[p.name])
|
||||
for a, v in p.__dict__.iteritems():
|
||||
if v is not None:
|
||||
setattr(newp, a, v)
|
||||
if not hasattr(newp, "_overridden"):
|
||||
newp._overridden = p._owner
|
||||
self._attrs[p.name] = newp
|
||||
else:
|
||||
# Duplicate attribute condition is check elsewhere.
|
||||
pass
|
||||
else:
|
||||
self._attrs[p.name] = p
|
||||
|
||||
append = add
|
||||
|
||||
def __str__(self):
|
||||
return 'AC({})'.format(map(str, self._attrs.values()))
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def _to_attrcls(self, p):
|
||||
old_owner = getattr(p, "_owner", None)
|
||||
if isinstance(p, basestring):
|
||||
p = self._attrcls(p)
|
||||
elif isinstance(p, tuple) or isinstance(p, list):
|
||||
p = self._attrcls(*p)
|
||||
elif isinstance(p, dict):
|
||||
p = self._attrcls(**p)
|
||||
elif not isinstance(p, self._attrcls):
|
||||
raise ValueError('Invalid parameter value: {}'.format(p))
|
||||
if (p.name in self._attrs and not p.override and
|
||||
p.name != 'modules'): # TODO: HACK due to "diamond dependecy" in workloads...
|
||||
raise ValueError('Attribute {} has already been defined.'.format(p.name))
|
||||
p._owner = old_owner
|
||||
return p
|
||||
|
||||
def __iadd__(self, other):
|
||||
for p in other:
|
||||
self.add(p)
|
||||
return self
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.values)
|
||||
|
||||
def __contains__(self, p):
|
||||
return p in self._attrs
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self._attrs[i]
|
||||
|
||||
def __len__(self):
|
||||
return len(self._attrs)
|
||||
|
||||
|
||||
class AliasCollection(AttributeCollection):
|
||||
|
||||
def __init__(self):
|
||||
super(AliasCollection, self).__init__(Alias)
|
||||
|
||||
def _to_attrcls(self, p):
|
||||
if isinstance(p, tuple) or isinstance(p, list):
|
||||
# must be in the form (name, {param: value, ...})
|
||||
p = self._attrcls(p[1], **p[1])
|
||||
elif not isinstance(p, self._attrcls):
|
||||
raise ValueError('Invalid parameter value: {}'.format(p))
|
||||
if p.name in self._attrs:
|
||||
raise ValueError('Attribute {} has already been defined.'.format(p.name))
|
||||
return p
|
||||
|
||||
|
||||
class ListCollection(list):
|
||||
|
||||
def __init__(self, attrcls): # pylint: disable=unused-argument
|
||||
super(ListCollection, self).__init__()
|
||||
|
||||
|
||||
class Artifact(object):
|
||||
"""
|
||||
This is an artifact generated during execution/post-processing of a workload.
|
||||
Unlike metrics, this represents an actual artifact, such as a file, generated.
|
||||
This may be "result", such as trace, or it could be "meta data" such as logs.
|
||||
These are distinguished using the ``kind`` attribute, which also helps WA decide
|
||||
how it should be handled. Currently supported kinds are:
|
||||
|
||||
:log: A log file. Not part of "results" as such but contains information about the
|
||||
run/workload execution that be useful for diagnostics/meta analysis.
|
||||
:meta: A file containing metadata. This is not part of "results", but contains
|
||||
information that may be necessary to reproduce the results (contrast with
|
||||
``log`` artifacts which are *not* necessary).
|
||||
:data: This file contains new data, not available otherwise and should be considered
|
||||
part of the "results" generated by WA. Most traces would fall into this category.
|
||||
:export: Exported version of results or some other artifact. This signifies that
|
||||
this artifact does not contain any new data that is not available
|
||||
elsewhere and that it may be safely discarded without losing information.
|
||||
:raw: Signifies that this is a raw dump/log that is normally processed to extract
|
||||
useful information and is then discarded. In a sense, it is the opposite of
|
||||
``export``, but in general may also be discarded.
|
||||
|
||||
.. note:: whether a file is marked as ``log``/``data`` or ``raw`` depends on
|
||||
how important it is to preserve this file, e.g. when archiving, vs
|
||||
how much space it takes up. Unlike ``export`` artifacts which are
|
||||
(almost) always ignored by other exporters as that would never result
|
||||
in data loss, ``raw`` files *may* be processed by exporters if they
|
||||
decided that the risk of losing potentially (though unlikely) useful
|
||||
data is greater than the time/space cost of handling the artifact (e.g.
|
||||
a database uploader may choose to ignore ``raw`` artifacts, where as a
|
||||
network filer archiver may choose to archive them).
|
||||
|
||||
.. note: The kind parameter is intended to represent the logical function of a particular
|
||||
artifact, not its intended means of processing -- this is left entirely up to the
|
||||
result processors.
|
||||
|
||||
"""
|
||||
|
||||
RUN = 'run'
|
||||
ITERATION = 'iteration'
|
||||
|
||||
valid_kinds = ['log', 'meta', 'data', 'export', 'raw']
|
||||
|
||||
def __init__(self, name, path, kind, level=RUN, mandatory=False, description=None):
|
||||
""""
|
||||
:param name: Name that uniquely identifies this artifact.
|
||||
:param path: The *relative* path of the artifact. Depending on the ``level``
|
||||
must be either relative to the run or iteration output directory.
|
||||
Note: this path *must* be delimited using ``/`` irrespective of the
|
||||
operating system.
|
||||
:param kind: The type of the artifact this is (e.g. log file, result, etc.) this
|
||||
will be used a hit to result processors. This must be one of ``'log'``,
|
||||
``'meta'``, ``'data'``, ``'export'``, ``'raw'``.
|
||||
:param level: The level at which the artifact will be generated. Must be either
|
||||
``'iteration'`` or ``'run'``.
|
||||
:param mandatory: Boolean value indicating whether this artifact must be present
|
||||
at the end of result processing for its level.
|
||||
:param description: A free-form description of what this artifact is.
|
||||
|
||||
"""
|
||||
if kind not in self.valid_kinds:
|
||||
raise ValueError('Invalid Artifact kind: {}; must be in {}'.format(kind, self.valid_kinds))
|
||||
self.name = name
|
||||
self.path = path.replace('/', os.sep) if path is not None else path
|
||||
self.kind = kind
|
||||
self.level = level
|
||||
self.mandatory = mandatory
|
||||
self.description = description
|
||||
|
||||
def exists(self, context):
|
||||
"""Returns ``True`` if artifact exists within the specified context, and
|
||||
``False`` otherwise."""
|
||||
fullpath = os.path.join(context.output_directory, self.path)
|
||||
return os.path.exists(fullpath)
|
||||
|
||||
def to_dict(self):
|
||||
return copy(self.__dict__)
|
||||
|
||||
|
||||
class Alias(object):
|
||||
"""
|
||||
This represents a configuration alias for an plugin, mapping an alternative name to
|
||||
a set of parameter values, effectively providing an alternative set of default values.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, **kwargs):
|
||||
self.name = name
|
||||
self.params = kwargs
|
||||
self.plugin_name = None # gets set by the MetaClass
|
||||
|
||||
def validate(self, ext):
|
||||
ext_params = set(p.name for p in ext.parameters)
|
||||
for param in self.params:
|
||||
if param not in ext_params:
|
||||
# Raising config error because aliases might have come through
|
||||
# the config.
|
||||
msg = 'Parameter {} (defined in alias {}) is invalid for {}'
|
||||
raise ConfigError(msg.format(param, self.name, ext.name))
|
||||
|
||||
|
||||
class PluginMeta(type):
|
||||
"""
|
||||
This basically adds some magic to plugins to make implementing new plugins, such as
|
||||
workloads less complicated.
|
||||
|
||||
It ensures that certain class attributes (specified by the ``to_propagate``
|
||||
attribute of the metaclass) get propagated down the inheritance hierarchy. The assumption
|
||||
is that the values of the attributes specified in the class are iterable; if that is not met,
|
||||
Bad Things (tm) will happen.
|
||||
|
||||
This also provides virtual method implementation, similar to those in C-derived OO languages,
|
||||
and alias specifications.
|
||||
|
||||
"""
|
||||
|
||||
to_propagate = [
|
||||
('parameters', Parameter, AttributeCollection),
|
||||
('artifacts', Artifact, AttributeCollection),
|
||||
('core_modules', str, ListCollection),
|
||||
]
|
||||
|
||||
virtual_methods = ['validate', 'initialize', 'finalize']
|
||||
global_virtuals = ['initialize', 'finalize']
|
||||
|
||||
def __new__(mcs, clsname, bases, attrs):
|
||||
mcs._propagate_attributes(bases, attrs, clsname)
|
||||
cls = type.__new__(mcs, clsname, bases, attrs)
|
||||
mcs._setup_aliases(cls)
|
||||
mcs._implement_virtual(cls, bases)
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
def _propagate_attributes(mcs, bases, attrs, clsname):
|
||||
"""
|
||||
For attributes specified by to_propagate, their values will be a union of
|
||||
that specified for cls and its bases (cls values overriding those of bases
|
||||
in case of conflicts).
|
||||
|
||||
"""
|
||||
for prop_attr, attr_cls, attr_collector_cls in mcs.to_propagate:
|
||||
should_propagate = False
|
||||
propagated = attr_collector_cls(attr_cls)
|
||||
for base in bases:
|
||||
if hasattr(base, prop_attr):
|
||||
propagated += getattr(base, prop_attr) or []
|
||||
should_propagate = True
|
||||
if prop_attr in attrs:
|
||||
pattrs = attrs[prop_attr] or []
|
||||
for pa in pattrs:
|
||||
if not isinstance(pa, basestring):
|
||||
pa._owner = clsname
|
||||
propagated += pattrs
|
||||
should_propagate = True
|
||||
if should_propagate:
|
||||
for p in propagated:
|
||||
override = bool(getattr(p, "override", None))
|
||||
overridden = bool(getattr(p, "_overridden", None))
|
||||
if override != overridden:
|
||||
msg = "Overriding non existing parameter '{}' inside '{}'"
|
||||
raise ValueError(msg.format(p.name, p._owner))
|
||||
attrs[prop_attr] = propagated
|
||||
|
||||
@classmethod
|
||||
def _setup_aliases(mcs, cls):
|
||||
if hasattr(cls, 'aliases'):
|
||||
aliases, cls.aliases = cls.aliases, AliasCollection()
|
||||
for alias in aliases:
|
||||
if isinstance(alias, basestring):
|
||||
alias = Alias(alias)
|
||||
alias.validate(cls)
|
||||
alias.plugin_name = cls.name
|
||||
cls.aliases.add(alias)
|
||||
|
||||
@classmethod
|
||||
def _implement_virtual(mcs, cls, bases):
|
||||
"""
|
||||
This implements automatic method propagation to the bases, so
|
||||
that you don't have to do something like
|
||||
|
||||
super(cls, self).vmname()
|
||||
|
||||
This also ensures that the methods that have beend identified as
|
||||
"globally virtual" are executed exactly once per WA execution, even if
|
||||
invoked through instances of different subclasses
|
||||
|
||||
"""
|
||||
methods = {}
|
||||
called_globals = set()
|
||||
for vmname in mcs.virtual_methods:
|
||||
clsmethod = getattr(cls, vmname, None)
|
||||
if clsmethod:
|
||||
basemethods = [getattr(b, vmname) for b in bases if hasattr(b, vmname)]
|
||||
methods[vmname] = [bm for bm in basemethods if bm != clsmethod]
|
||||
methods[vmname].append(clsmethod)
|
||||
|
||||
def generate_method_wrapper(vname): # pylint: disable=unused-argument
|
||||
# this creates a closure with the method name so that it
|
||||
# does not need to be passed to the wrapper as an argument,
|
||||
# leaving the wrapper to accept exactly the same set of
|
||||
# arguments as the method it is wrapping.
|
||||
name__ = vmname # pylint: disable=cell-var-from-loop
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
for dm in methods[name__]:
|
||||
if name__ in mcs.global_virtuals:
|
||||
if dm not in called_globals:
|
||||
dm(self, *args, **kwargs)
|
||||
called_globals.add(dm)
|
||||
else:
|
||||
dm(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
setattr(cls, vmname, generate_method_wrapper(vmname))
|
||||
|
||||
|
||||
class Plugin(object):
|
||||
"""
|
||||
Base class for all WA plugins. An plugin is basically a plug-in.
|
||||
It extends the functionality of WA in some way. Plugins are discovered
|
||||
and loaded dynamically by the plugin loader upon invocation of WA scripts.
|
||||
Adding an plugin is a matter of placing a class that implements an appropriate
|
||||
interface somewhere it would be discovered by the loader. That "somewhere" is
|
||||
typically one of the plugin subdirectories under ``~/.workload_automation/``.
|
||||
|
||||
"""
|
||||
__metaclass__ = PluginMeta
|
||||
|
||||
kind = None
|
||||
name = None
|
||||
parameters = [
|
||||
Parameter('modules', kind=list,
|
||||
description="""
|
||||
Lists the modules to be loaded by this plugin. A module is a plug-in that
|
||||
further extends functionality of an plugin.
|
||||
"""),
|
||||
]
|
||||
artifacts = []
|
||||
aliases = []
|
||||
core_modules = []
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
return {p.name: p.default for p in cls.parameters}
|
||||
|
||||
@property
|
||||
def dependencies_directory(self):
|
||||
return _d(os.path.join(settings.dependencies_directory, self.name))
|
||||
|
||||
@property
|
||||
def _classname(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.logger = logging.getLogger(self._classname)
|
||||
self._modules = []
|
||||
self.capabilities = getattr(self.__class__, 'capabilities', [])
|
||||
for param in self.parameters:
|
||||
param.set_value(self, kwargs.get(param.name))
|
||||
for key in kwargs:
|
||||
if key not in self.parameters:
|
||||
message = 'Unexpected parameter "{}" for {}'
|
||||
raise ConfigError(message.format(key, self.name))
|
||||
|
||||
def get_config(self):
|
||||
"""
|
||||
Returns current configuration (i.e. parameter values) of this plugin.
|
||||
|
||||
"""
|
||||
config = {}
|
||||
for param in self.parameters:
|
||||
config[param.name] = getattr(self, param.name, None)
|
||||
return config
|
||||
|
||||
def validate(self):
|
||||
"""
|
||||
Perform basic validation to ensure that this plugin is capable of running.
|
||||
This is intended as an early check to ensure the plugin has not been mis-configured,
|
||||
rather than a comprehensive check (that may, e.g., require access to the execution
|
||||
context).
|
||||
|
||||
This method may also be used to enforce (i.e. set as well as check) inter-parameter
|
||||
constraints for the plugin (e.g. if valid values for parameter A depend on the value
|
||||
of parameter B -- something that is not possible to enfroce using ``Parameter``\ 's
|
||||
``constraint`` attribute.
|
||||
|
||||
"""
|
||||
if self.name is None:
|
||||
raise ValidationError('Name not set for {}'.format(self._classname))
|
||||
for param in self.parameters:
|
||||
param.validate(self)
|
||||
|
||||
def initialize(self, context):
|
||||
pass
|
||||
|
||||
def finalize(self, context):
|
||||
pass
|
||||
|
||||
def check_artifacts(self, context, level):
|
||||
"""
|
||||
Make sure that all mandatory artifacts have been generated.
|
||||
|
||||
"""
|
||||
for artifact in self.artifacts:
|
||||
if artifact.level != level or not artifact.mandatory:
|
||||
continue
|
||||
fullpath = os.path.join(context.output_directory, artifact.path)
|
||||
if not os.path.exists(fullpath):
|
||||
message = 'Mandatory "{}" has not been generated for {}.'
|
||||
raise ValidationError(message.format(artifact.path, self.name))
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == '_modules':
|
||||
raise ValueError('_modules accessed too early!')
|
||||
for module in self._modules:
|
||||
if hasattr(module, name):
|
||||
return getattr(module, name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def load_modules(self, loader):
|
||||
"""
|
||||
Load the modules specified by the "modules" Parameter using the provided loader. A loader
|
||||
can be any object that has an atribute called "get_module" that implements the following
|
||||
signature::
|
||||
|
||||
get_module(name, owner, **kwargs)
|
||||
|
||||
and returns an instance of :class:`wlauto.core.plugin.Module`. If the module with the
|
||||
specified name is not found, the loader must raise an appropriate exception.
|
||||
|
||||
"""
|
||||
modules = list(reversed(self.core_modules)) + list(reversed(self.modules or []))
|
||||
if not modules:
|
||||
return
|
||||
for module_spec in modules:
|
||||
if not module_spec:
|
||||
continue
|
||||
module = self._load_module(loader, module_spec)
|
||||
self._install_module(module)
|
||||
|
||||
def has(self, capability):
|
||||
"""Check if this plugin has the specified capability. The alternative method ``can`` is
|
||||
identical to this. Which to use is up to the caller depending on what makes semantic sense
|
||||
in the context of the capability, e.g. ``can('hard_reset')`` vs ``has('active_cooling')``."""
|
||||
return capability in self.capabilities
|
||||
|
||||
can = has
|
||||
|
||||
def _load_module(self, loader, module_spec):
|
||||
if isinstance(module_spec, basestring):
|
||||
name = module_spec
|
||||
params = {}
|
||||
elif isinstance(module_spec, dict):
|
||||
if len(module_spec) != 1:
|
||||
message = 'Invalid module spec: {}; dict must have exctly one key -- the module name.'
|
||||
raise ValueError(message.format(module_spec))
|
||||
name, params = module_spec.items()[0]
|
||||
else:
|
||||
message = 'Invalid module spec: {}; must be a string or a one-key dict.'
|
||||
raise ValueError(message.format(module_spec))
|
||||
|
||||
if not isinstance(params, dict):
|
||||
message = 'Invalid module spec: {}; dict value must also be a dict.'
|
||||
raise ValueError(message.format(module_spec))
|
||||
|
||||
module = loader.get_module(name, owner=self, **params)
|
||||
module.initialize(None)
|
||||
return module
|
||||
|
||||
def _install_module(self, module):
|
||||
for capability in module.capabilities:
|
||||
if capability not in self.capabilities:
|
||||
self.capabilities.append(capability)
|
||||
self._modules.append(module)
|
||||
|
||||
|
||||
class PluginLoaderItem(object):
|
||||
|
||||
def __init__(self, ext_tuple):
|
||||
self.name = ext_tuple.name
|
||||
self.default_package = ext_tuple.default_package
|
||||
self.default_path = ext_tuple.default_path
|
||||
self.cls = load_class(ext_tuple.cls)
|
||||
|
||||
|
||||
class PluginLoader(object):
|
||||
"""
|
||||
Discovers, enumerates and loads available devices, configs, etc.
|
||||
The loader will attempt to discover things on construction by looking
|
||||
in predetermined set of locations defined by default_paths. Optionally,
|
||||
additional locations may specified through paths parameter that must
|
||||
be a list of additional Python module paths (i.e. dot-delimited).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, packages=None, paths=None, ignore_paths=None, keep_going=False):
|
||||
"""
|
||||
params::
|
||||
|
||||
:packages: List of packages to load plugins from.
|
||||
:paths: List of paths to be searched for Python modules containing
|
||||
WA plugins.
|
||||
:ignore_paths: List of paths to ignore when search for WA plugins (these would
|
||||
typically be subdirectories of one or more locations listed in
|
||||
``paths`` parameter.
|
||||
:keep_going: Specifies whether to keep going if an error occurs while loading
|
||||
plugins.
|
||||
"""
|
||||
self.logger = logging.getLogger('pluginloader')
|
||||
self.keep_going = keep_going
|
||||
self.packages = packages or []
|
||||
self.paths = paths or []
|
||||
self.ignore_paths = ignore_paths or []
|
||||
self.plugins = {}
|
||||
self.kind_map = defaultdict(dict)
|
||||
self.aliases = {}
|
||||
self.global_param_aliases = {}
|
||||
self._discover_from_packages(self.packages)
|
||||
self._discover_from_paths(self.paths, self.ignore_paths)
|
||||
|
||||
def update(self, packages=None, paths=None, ignore_paths=None):
|
||||
""" Load plugins from the specified paths/packages
|
||||
without clearing or reloading existing plugin. """
|
||||
msg = 'Updating from: packages={} paths={}'
|
||||
self.logger.debug(msg.format(packages, paths))
|
||||
if packages:
|
||||
self.packages.extend(packages)
|
||||
self._discover_from_packages(packages)
|
||||
if paths:
|
||||
self.paths.extend(paths)
|
||||
self.ignore_paths.extend(ignore_paths or [])
|
||||
self._discover_from_paths(paths, ignore_paths or [])
|
||||
|
||||
def clear(self):
|
||||
""" Clear all discovered items. """
|
||||
self.plugins = []
|
||||
self.kind_map.clear()
|
||||
|
||||
def reload(self):
|
||||
""" Clear all discovered items and re-run the discovery. """
|
||||
self.logger.debug('Reloading')
|
||||
self.clear()
|
||||
self._discover_from_packages(self.packages)
|
||||
self._discover_from_paths(self.paths, self.ignore_paths)
|
||||
|
||||
def get_plugin_class(self, name, kind=None):
|
||||
"""
|
||||
Return the class for the specified plugin if found or raises ``ValueError``.
|
||||
|
||||
"""
|
||||
name, _ = self.resolve_alias(name)
|
||||
if kind is None:
|
||||
try:
|
||||
return self.plugins[name]
|
||||
except KeyError:
|
||||
raise NotFoundError('plugins {} not found.'.format(name))
|
||||
if kind not in self.kind_map:
|
||||
raise ValueError('Unknown plugin type: {}'.format(kind))
|
||||
store = self.kind_map[kind]
|
||||
if name not in store:
|
||||
msg = 'plugins {} is not {} {}.'
|
||||
raise NotFoundError(msg.format(name, get_article(kind), kind))
|
||||
return store[name]
|
||||
|
||||
def get_plugin(self, name=None, kind=None, *args, **kwargs):
|
||||
"""
|
||||
Return plugin of the specified kind with the specified name. Any
|
||||
additional parameters will be passed to the plugin's __init__.
|
||||
|
||||
"""
|
||||
name, base_kwargs = self.resolve_alias(name)
|
||||
kwargs = OrderedDict(chain(base_kwargs.iteritems(), kwargs.iteritems()))
|
||||
cls = self.get_plugin_class(name, kind)
|
||||
plugin = cls(*args, **kwargs)
|
||||
return plugin
|
||||
|
||||
def get_default_config(self, name):
|
||||
"""
|
||||
Returns the default configuration for the specified plugin name. The
|
||||
name may be an alias, in which case, the returned config will be
|
||||
augmented with appropriate alias overrides.
|
||||
|
||||
"""
|
||||
real_name, alias_config = self.resolve_alias(name)
|
||||
base_default_config = self.get_plugin_class(real_name).get_default_config()
|
||||
return merge_dicts_simple(base_default_config, alias_config)
|
||||
|
||||
def list_plugins(self, kind=None):
|
||||
"""
|
||||
List discovered plugin classes. Optionally, only list plugins of a
|
||||
particular type.
|
||||
|
||||
"""
|
||||
if kind is None:
|
||||
return self.plugins.values()
|
||||
if kind not in self.kind_map:
|
||||
raise ValueError('Unknown plugin type: {}'.format(kind))
|
||||
return self.kind_map[kind].values()
|
||||
|
||||
def has_plugin(self, name, kind=None):
|
||||
"""
|
||||
Returns ``True`` if an plugins with the specified ``name`` has been
|
||||
discovered by the loader. If ``kind`` was specified, only returns ``True``
|
||||
if the plugin has been found, *and* it is of the specified kind.
|
||||
|
||||
"""
|
||||
try:
|
||||
self.get_plugin_class(name, kind)
|
||||
return True
|
||||
except NotFoundError:
|
||||
return False
|
||||
|
||||
def resolve_alias(self, alias_name):
|
||||
"""
|
||||
Try to resolve the specified name as an plugin alias. Returns a
|
||||
two-tuple, the first value of which is actual plugin name, and the
|
||||
iisecond is a dict of parameter values for this alias. If the name passed
|
||||
is already an plugin name, then the result is ``(alias_name, {})``.
|
||||
|
||||
"""
|
||||
alias_name = identifier(alias_name.lower())
|
||||
if alias_name in self.plugins:
|
||||
return (alias_name, {})
|
||||
if alias_name in self.aliases:
|
||||
alias = self.aliases[alias_name]
|
||||
return (alias.plugin_name, alias.params)
|
||||
raise NotFoundError('Could not find plugin or alias "{}"'.format(alias_name))
|
||||
|
||||
# Internal methods.
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
This resolves methods for specific plugins types based on corresponding
|
||||
generic plugin methods. So it's possible to say things like ::
|
||||
|
||||
loader.get_device('foo')
|
||||
|
||||
instead of ::
|
||||
|
||||
loader.get_plugin('foo', kind='device')
|
||||
|
||||
"""
|
||||
if name.startswith('get_'):
|
||||
name = name.replace('get_', '', 1)
|
||||
if name in self.kind_map:
|
||||
def __wrapper(pname, *args, **kwargs):
|
||||
return self.get_plugin(pname, name, *args, **kwargs)
|
||||
return __wrapper
|
||||
if name.startswith('list_'):
|
||||
name = name.replace('list_', '', 1).rstrip('s')
|
||||
if name in self.kind_map:
|
||||
def __wrapper(*args, **kwargs): # pylint: disable=E0102
|
||||
return self.list_plugins(name, *args, **kwargs)
|
||||
return __wrapper
|
||||
if name.startswith('has_'):
|
||||
name = name.replace('has_', '', 1)
|
||||
if name in self.kind_map:
|
||||
def __wrapper(pname, *args, **kwargs): # pylint: disable=E0102
|
||||
return self.has_plugin(pname, name, *args, **kwargs)
|
||||
return __wrapper
|
||||
raise AttributeError(name)
|
||||
|
||||
def _discover_from_packages(self, packages):
|
||||
self.logger.debug('Discovering plugins in packages')
|
||||
try:
|
||||
for package in packages:
|
||||
for module in walk_modules(package):
|
||||
self._discover_in_module(module)
|
||||
except HostError as e:
|
||||
message = 'Problem loading plugins from {}: {}'
|
||||
raise LoaderError(message.format(e.module, str(e.orig_exc)))
|
||||
|
||||
def _discover_from_paths(self, paths, ignore_paths):
|
||||
paths = paths or []
|
||||
ignore_paths = ignore_paths or []
|
||||
|
||||
self.logger.debug('Discovering plugins in paths')
|
||||
for path in paths:
|
||||
self.logger.debug('Checking path %s', path)
|
||||
if os.path.isfile(path):
|
||||
self._discover_from_file(path)
|
||||
for root, _, files in os.walk(path, followlinks=True):
|
||||
should_skip = False
|
||||
for igpath in ignore_paths:
|
||||
if root.startswith(igpath):
|
||||
should_skip = True
|
||||
break
|
||||
if should_skip:
|
||||
continue
|
||||
for fname in files:
|
||||
if os.path.splitext(fname)[1].lower() != '.py':
|
||||
continue
|
||||
filepath = os.path.join(root, fname)
|
||||
self._discover_from_file(filepath)
|
||||
|
||||
def _discover_from_file(self, filepath):
|
||||
try:
|
||||
modname = os.path.splitext(filepath[1:])[0].translate(MODNAME_TRANS)
|
||||
module = imp.load_source(modname, filepath)
|
||||
self._discover_in_module(module)
|
||||
except (SystemExit, ImportError), e:
|
||||
if self.keep_going:
|
||||
self.logger.warning('Failed to load {}'.format(filepath))
|
||||
self.logger.warning('Got: {}'.format(e))
|
||||
else:
|
||||
raise LoaderError('Failed to load {}'.format(filepath), sys.exc_info())
|
||||
except Exception as e:
|
||||
message = 'Problem loading plugins from {}: {}'
|
||||
raise LoaderError(message.format(filepath, e))
|
||||
|
||||
def _discover_in_module(self, module): # NOQA pylint: disable=too-many-branches
|
||||
self.logger.debug('Checking module %s', module.__name__)
|
||||
#log.indent()
|
||||
try:
|
||||
for obj in vars(module).itervalues():
|
||||
if inspect.isclass(obj):
|
||||
if not issubclass(obj, Plugin):
|
||||
continue
|
||||
if not obj.kind:
|
||||
message = 'Skipping plugin {} as it does not define a kind'
|
||||
self.logger.debug(message.format(obj.__name__))
|
||||
continue
|
||||
if not obj.name:
|
||||
message = 'Skipping {} {} as it does not define a name'
|
||||
self.logger.debug(message.format(obj.kind, obj.__name__))
|
||||
continue
|
||||
try:
|
||||
self._add_found_plugin(obj)
|
||||
except LoaderError as e:
|
||||
if self.keep_going:
|
||||
self.logger.warning(e)
|
||||
else:
|
||||
raise e
|
||||
finally:
|
||||
# log.dedent()
|
||||
pass
|
||||
|
||||
def _add_found_plugin(self, obj):
|
||||
"""
|
||||
:obj: Found plugin class
|
||||
:ext: matching plugin item.
|
||||
"""
|
||||
self.logger.debug('Adding %s %s', obj.kind, obj.name)
|
||||
key = identifier(obj.name.lower())
|
||||
if key in self.plugins or key in self.aliases:
|
||||
raise LoaderError('{} "{}" already exists.'.format(obj.kind, obj.name))
|
||||
# plugins are tracked both, in a common plugins
|
||||
# dict, and in per-plugin kind dict (as retrieving
|
||||
# plugins by kind is a common use case.
|
||||
self.plugins[key] = obj
|
||||
self.kind_map[obj.kind][key] = obj
|
||||
|
||||
for alias in obj.aliases:
|
||||
alias_id = identifier(alias.name.lower())
|
||||
if alias_id in self.plugins or alias_id in self.aliases:
|
||||
raise LoaderError('{} "{}" already exists.'.format(obj.kind, obj.name))
|
||||
self.aliases[alias_id] = alias
|
@ -1,89 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
import sys
|
||||
|
||||
|
||||
class __LoaderWrapper(object):
|
||||
|
||||
@property
|
||||
def kinds(self):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.kind_map.keys()
|
||||
|
||||
@property
|
||||
def kind_map(self):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.kind_map
|
||||
|
||||
def __init__(self):
|
||||
self._loader = None
|
||||
|
||||
def reset(self):
|
||||
# These imports cannot be done at top level, because of
|
||||
# sys.modules manipulation below
|
||||
from wlauto.core.plugin import PluginLoader
|
||||
from wlauto.core.configuration import settings
|
||||
self._loader = PluginLoader(settings.plugin_packages,
|
||||
[settings.plugins_directory], [])
|
||||
|
||||
def update(self, packages=None, paths=None, ignore_paths=None):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
self._loader.update(packages, paths, ignore_paths)
|
||||
|
||||
def reload(self):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
self._loader.reload()
|
||||
|
||||
def list_plugins(self, kind=None):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.list_plugins(kind)
|
||||
|
||||
def has_plugin(self, name, kind=None):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.has_plugin(name, kind)
|
||||
|
||||
def get_plugin_class(self, name, kind=None):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.get_plugin_class(name, kind)
|
||||
|
||||
def get_plugin(self, name=None, kind=None, *args, **kwargs):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.get_plugin(name=name, kind=kind, *args, **kwargs)
|
||||
|
||||
def get_default_config(self, name):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.get_default_config(name)
|
||||
|
||||
def resolve_alias(self, name):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return self._loader.resolve_alias(name)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if not self._loader:
|
||||
self.reset()
|
||||
return getattr(self._loader, name)
|
||||
|
||||
|
||||
sys.modules[__name__] = __LoaderWrapper()
|
@ -1,111 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
Defines infrastructure for resource resolution. This is used to find
|
||||
various dependencies/assets/etc that WA objects rely on in a flexible way.
|
||||
|
||||
"""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
# Note: this is the modified louie library in wlauto/external.
|
||||
# prioritylist does not exist in vanilla louie.
|
||||
from wlauto.utils.types import prioritylist # pylint: disable=E0611,F0401
|
||||
|
||||
from wlauto.exceptions import ResourceError
|
||||
from wlauto.core import pluginloader
|
||||
|
||||
class ResourceResolver(object):
|
||||
"""
|
||||
Discovers and registers getters, and then handles requests for
|
||||
resources using registered getters.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.getters = defaultdict(prioritylist)
|
||||
self.config = config
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Discover getters under the specified source. The source could
|
||||
be either a python package/module or a path.
|
||||
|
||||
"""
|
||||
|
||||
for rescls in pluginloader.list_resource_getters():
|
||||
getter = self.config.get_plugin(name=rescls.name, kind="resource_getter", resolver=self)
|
||||
getter.register()
|
||||
|
||||
def get(self, resource, strict=True, *args, **kwargs):
|
||||
"""
|
||||
Uses registered getters to attempt to discover a resource of the specified
|
||||
kind and matching the specified criteria. Returns path to the resource that
|
||||
has been discovered. If a resource has not been discovered, this will raise
|
||||
a ``ResourceError`` or, if ``strict`` has been set to ``False``, will return
|
||||
``None``.
|
||||
|
||||
"""
|
||||
self.logger.debug('Resolving {}'.format(resource))
|
||||
for getter in self.getters[resource.name]:
|
||||
self.logger.debug('Trying {}'.format(getter))
|
||||
result = getter.get(resource, *args, **kwargs)
|
||||
if result is not None:
|
||||
self.logger.debug('Resource {} found using {}:'.format(resource, getter))
|
||||
self.logger.debug('\t{}'.format(result))
|
||||
return result
|
||||
if strict:
|
||||
raise ResourceError('{} could not be found'.format(resource))
|
||||
self.logger.debug('Resource {} not found.'.format(resource))
|
||||
return None
|
||||
|
||||
def register(self, getter, kind, priority=0):
|
||||
"""
|
||||
Register the specified resource getter as being able to discover a resource
|
||||
of the specified kind with the specified priority.
|
||||
|
||||
This method would typically be invoked by a getter inside its __init__.
|
||||
The idea being that getters register themselves for resources they know
|
||||
they can discover.
|
||||
|
||||
*priorities*
|
||||
|
||||
getters that are registered with the highest priority will be invoked first. If
|
||||
multiple getters are registered under the same priority, they will be invoked
|
||||
in the order they were registered (i.e. in the order they were discovered). This is
|
||||
essentially non-deterministic.
|
||||
|
||||
Generally getters that are more likely to find a resource, or would find a
|
||||
"better" version of the resource should register with higher (positive) priorities.
|
||||
Fall-back getters that should only be invoked if a resource is not found by usual
|
||||
means should register with lower (negative) priorities.
|
||||
|
||||
"""
|
||||
self.logger.debug('Registering {} for {} resources'.format(getter.name, kind))
|
||||
self.getters[kind].add(getter, priority)
|
||||
|
||||
def unregister(self, getter, kind):
|
||||
"""
|
||||
Unregister a getter that has been registered earlier.
|
||||
|
||||
"""
|
||||
self.logger.debug('Unregistering {}'.format(getter.name))
|
||||
try:
|
||||
self.getters[kind].remove(getter)
|
||||
except ValueError:
|
||||
raise ValueError('Resource getter {} is not installed.'.format(getter.name))
|
@ -1,185 +0,0 @@
|
||||
# Copyright 2013-2015 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 wlauto.core.configuration import settings
|
||||
from wlauto.core.plugin import Plugin
|
||||
|
||||
|
||||
class GetterPriority(object):
|
||||
"""
|
||||
Enumerates standard ResourceGetter priorities. In general, getters should register
|
||||
under one of these, rather than specifying other priority values.
|
||||
|
||||
|
||||
:cached: The cached version of the resource. Look here first. This priority also implies
|
||||
that the resource at this location is a "cache" and is not the only version of the
|
||||
resource, so it may be cleared without losing access to the resource.
|
||||
:preferred: Take this resource in favour of the environment resource.
|
||||
:environment: Found somewhere under ~/.workload_automation/ or equivalent, or
|
||||
from environment variables, external configuration files, etc.
|
||||
These will override resource supplied with the package.
|
||||
:external_package: Resource provided by another package.
|
||||
:package: Resource provided with the package.
|
||||
:remote: Resource will be downloaded from a remote location (such as an HTTP server
|
||||
or a samba share). Try this only if no other getter was successful.
|
||||
|
||||
"""
|
||||
cached = 20
|
||||
preferred = 10
|
||||
remote = 5
|
||||
environment = 0
|
||||
external_package = -5
|
||||
package = -10
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
Represents a resource that needs to be resolved. This can be pretty much
|
||||
anything: a file, environment variable, a Python object, etc. The only thing
|
||||
a resource *has* to have is an owner (which would normally be the
|
||||
Workload/Instrument/Device/etc object that needs the resource). In addition,
|
||||
a resource have any number of attributes to identify, but all of them are resource
|
||||
type specific.
|
||||
|
||||
"""
|
||||
|
||||
name = None
|
||||
|
||||
def __init__(self, owner):
|
||||
self.owner = owner
|
||||
|
||||
def delete(self, instance):
|
||||
"""
|
||||
Delete an instance of this resource type. This must be implemented by the concrete
|
||||
subclasses based on what the resource looks like, e.g. deleting a file or a directory
|
||||
tree, or removing an entry from a database.
|
||||
|
||||
:note: Implementation should *not* contain any logic for deciding whether or not
|
||||
a resource should be deleted, only the actual deletion. The assumption is
|
||||
that if this method is invoked, then the decision has already been made.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __str__(self):
|
||||
return '<{}\'s {}>'.format(self.owner, self.name)
|
||||
|
||||
|
||||
class ResourceGetter(Plugin):
|
||||
"""
|
||||
Base class for implementing resolvers. Defines resolver interface. Resolvers are
|
||||
responsible for discovering resources (such as particular kinds of files) they know
|
||||
about based on the parameters that are passed to them. Each resolver also has a dict of
|
||||
attributes that describe its operation, and may be used to determine which get invoked.
|
||||
There is no pre-defined set of attributes and resolvers may define their own.
|
||||
|
||||
Class attributes:
|
||||
|
||||
:name: Name that uniquely identifies this getter. Must be set by any concrete subclass.
|
||||
:resource_type: Identifies resource type(s) that this getter can handle. This must
|
||||
be either a string (for a single type) or a list of strings for
|
||||
multiple resource types. This must be set by any concrete subclass.
|
||||
:priority: Priority with which this getter will be invoked. This should be one of
|
||||
the standard priorities specified in ``GetterPriority`` enumeration. If not
|
||||
set, this will default to ``GetterPriority.environment``.
|
||||
|
||||
"""
|
||||
|
||||
kind = "resource_getter"
|
||||
name = None
|
||||
resource_type = None
|
||||
priority = GetterPriority.environment
|
||||
|
||||
def __init__(self, resolver=None, **kwargs):
|
||||
super(ResourceGetter, self).__init__(**kwargs)
|
||||
self.resolver = resolver
|
||||
|
||||
def register(self):
|
||||
"""
|
||||
Registers with a resource resolver. Concrete implementations must override this
|
||||
to invoke ``self.resolver.register()`` method to register ``self`` for specific
|
||||
resource types.
|
||||
|
||||
"""
|
||||
if self.resource_type is None:
|
||||
raise ValueError('No resource type specified for {}'.format(self.name))
|
||||
elif isinstance(self.resource_type, list):
|
||||
for rt in self.resource_type:
|
||||
self.resolver.register(self, rt, self.priority)
|
||||
else:
|
||||
self.resolver.register(self, self.resource_type, self.priority)
|
||||
|
||||
def unregister(self):
|
||||
"""Unregister from a resource resolver."""
|
||||
if self.resource_type is None:
|
||||
raise ValueError('No resource type specified for {}'.format(self.name))
|
||||
elif isinstance(self.resource_type, list):
|
||||
for rt in self.resource_type:
|
||||
self.resolver.unregister(self, rt)
|
||||
else:
|
||||
self.resolver.unregister(self, self.resource_type)
|
||||
|
||||
def get(self, resource, **kwargs):
|
||||
"""
|
||||
This will get invoked by the resolver when attempting to resolve a resource, passing
|
||||
in the resource to be resolved as the first parameter. Any additional parameters would
|
||||
be specific to a particular resource type.
|
||||
|
||||
This method will only be invoked for resource types that the getter has registered for.
|
||||
|
||||
:param resource: an instance of :class:`wlauto.core.resource.Resource`.
|
||||
|
||||
:returns: Implementations of this method must return either the discovered resource or
|
||||
``None`` if the resource could not be discovered.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete(self, resource, *args, **kwargs):
|
||||
"""
|
||||
Delete the resource if it is discovered. All arguments are passed to a call
|
||||
to``self.get()``. If that call returns a resource, it is deleted.
|
||||
|
||||
:returns: ``True`` if the specified resource has been discovered and deleted,
|
||||
and ``False`` otherwise.
|
||||
|
||||
"""
|
||||
discovered = self.get(resource, *args, **kwargs)
|
||||
if discovered:
|
||||
resource.delete(discovered)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return '<ResourceGetter {}>'.format(self.name)
|
||||
|
||||
|
||||
class __NullOwner(object):
|
||||
"""Represents an owner for a resource not owned by anyone."""
|
||||
|
||||
name = 'noone'
|
||||
dependencies_directory = settings.dependencies_directory
|
||||
|
||||
def __getattr__(self, name):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return 'no-one'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
NO_ONE = __NullOwner()
|
@ -1,319 +0,0 @@
|
||||
# Copyright 2013-2015 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=no-member
|
||||
|
||||
"""
|
||||
This module defines the classes used to handle result
|
||||
processing inside Workload Automation. There will be a
|
||||
:class:`wlauto.core.workload.WorkloadResult` object generated for
|
||||
every workload iteration executed. This object will have a list of
|
||||
:class:`wlauto.core.workload.WorkloadMetric` objects. This list will be
|
||||
populated by the workload itself and may also be updated by instrumentation
|
||||
(e.g. to add power measurements). Once the result object has been fully
|
||||
populated, it will be passed into the ``process_iteration_result`` method of
|
||||
:class:`ResultProcessor`. Once the entire run has completed, a list containing
|
||||
result objects from all iterations will be passed into ``process_results``
|
||||
method of :class`ResultProcessor`.
|
||||
|
||||
Which result processors will be active is defined by the ``result_processors``
|
||||
list in the ``~/.workload_automation/config.py``. Only the result_processors
|
||||
who's names appear in this list will be used.
|
||||
|
||||
A :class:`ResultsManager` keeps track of active results processors.
|
||||
|
||||
"""
|
||||
import logging
|
||||
import traceback
|
||||
from copy import copy
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
|
||||
from wlauto.core.plugin import Plugin
|
||||
from wlauto.core.configuration.configuration import ITERATION_STATUS
|
||||
from wlauto.exceptions import WAError
|
||||
from wlauto.utils.types import numeric
|
||||
from wlauto.utils.misc import enum_metaclass, merge_dicts_simple
|
||||
|
||||
|
||||
class ResultManager(object):
|
||||
"""
|
||||
Keeps track of result processors and passes on the results onto the individual processors.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('ResultsManager')
|
||||
self.processors = []
|
||||
self._bad = []
|
||||
|
||||
def install(self, processor):
|
||||
self.logger.debug('Installing results processor %s', processor.name)
|
||||
self.processors.append(processor)
|
||||
|
||||
def uninstall(self, processor):
|
||||
if processor in self.processors:
|
||||
self.logger.debug('Uninstalling results processor %s', processor.name)
|
||||
self.processors.remove(processor)
|
||||
else:
|
||||
self.logger.warning('Attempting to uninstall results processor %s, which is not installed.',
|
||||
processor.name)
|
||||
|
||||
def initialize(self, context):
|
||||
# Errors aren't handled at this stage, because this gets executed
|
||||
# before workload execution starts and we just want to propagte them
|
||||
# and terminate (so that error can be corrected and WA restarted).
|
||||
for processor in self.processors:
|
||||
processor.initialize(context)
|
||||
|
||||
def add_result(self, result, context):
|
||||
with self._manage_processors(context):
|
||||
for processor in self.processors:
|
||||
with self._handle_errors(processor):
|
||||
processor.process_iteration_result(result, context)
|
||||
for processor in self.processors:
|
||||
with self._handle_errors(processor):
|
||||
processor.export_iteration_result(result, context)
|
||||
|
||||
def process_run_result(self, result, context):
|
||||
with self._manage_processors(context):
|
||||
for processor in self.processors:
|
||||
with self._handle_errors(processor):
|
||||
processor.process_run_result(result, context)
|
||||
for processor in self.processors:
|
||||
with self._handle_errors(processor):
|
||||
processor.export_run_result(result, context)
|
||||
|
||||
def finalize(self, context):
|
||||
with self._manage_processors(context):
|
||||
for processor in self.processors:
|
||||
with self._handle_errors(processor):
|
||||
processor.finalize(context)
|
||||
|
||||
def validate(self):
|
||||
for processor in self.processors:
|
||||
processor.validate()
|
||||
|
||||
@contextmanager
|
||||
def _manage_processors(self, context, finalize_bad=True):
|
||||
yield
|
||||
for processor in self._bad:
|
||||
if finalize_bad:
|
||||
processor.finalize(context)
|
||||
self.uninstall(processor)
|
||||
self._bad = []
|
||||
|
||||
@contextmanager
|
||||
def _handle_errors(self, processor):
|
||||
try:
|
||||
yield
|
||||
except KeyboardInterrupt, e:
|
||||
raise e
|
||||
except WAError, we:
|
||||
self.logger.error('"{}" result processor has encountered an error'.format(processor.name))
|
||||
self.logger.error('{}("{}")'.format(we.__class__.__name__, we.message))
|
||||
self._bad.append(processor)
|
||||
except Exception, e: # pylint: disable=W0703
|
||||
self.logger.error('"{}" result processor has encountered an error'.format(processor.name))
|
||||
self.logger.error('{}("{}")'.format(e.__class__.__name__, e))
|
||||
self.logger.error(traceback.format_exc())
|
||||
self._bad.append(processor)
|
||||
|
||||
|
||||
class ResultProcessor(Plugin):
|
||||
"""
|
||||
Base class for result processors. Defines an interface that should be implemented
|
||||
by the subclasses. A result processor can be used to do any kind of post-processing
|
||||
of the results, from writing them out to a file, to uploading them to a database,
|
||||
performing calculations, generating plots, etc.
|
||||
|
||||
"""
|
||||
kind = "result_processor"
|
||||
def initialize(self, context):
|
||||
pass
|
||||
|
||||
def process_iteration_result(self, result, context):
|
||||
pass
|
||||
|
||||
def export_iteration_result(self, result, context):
|
||||
pass
|
||||
|
||||
def process_run_result(self, result, context):
|
||||
pass
|
||||
|
||||
def export_run_result(self, result, context):
|
||||
pass
|
||||
|
||||
def finalize(self, context):
|
||||
pass
|
||||
|
||||
|
||||
class RunResult(object):
|
||||
"""
|
||||
Contains overall results for a run.
|
||||
|
||||
"""
|
||||
|
||||
__metaclass__ = enum_metaclass('values', return_name=True)
|
||||
|
||||
values = [
|
||||
'OK',
|
||||
'OKISH',
|
||||
'PARTIAL',
|
||||
'FAILED',
|
||||
'UNKNOWN',
|
||||
]
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if not self.iteration_results or all([s.status == IterationResult.FAILED for s in self.iteration_results]):
|
||||
return self.FAILED
|
||||
elif any([s.status == IterationResult.FAILED for s in self.iteration_results]):
|
||||
return self.PARTIAL
|
||||
elif any([s.status == IterationResult.ABORTED for s in self.iteration_results]):
|
||||
return self.PARTIAL
|
||||
elif (any([s.status == IterationResult.PARTIAL for s in self.iteration_results]) or
|
||||
self.non_iteration_errors):
|
||||
return self.OKISH
|
||||
elif all([s.status == IterationResult.OK for s in self.iteration_results]):
|
||||
return self.OK
|
||||
else:
|
||||
return self.UNKNOWN # should never happen
|
||||
|
||||
def __init__(self, run_info, output_directory=None):
|
||||
self.info = run_info
|
||||
self.iteration_results = []
|
||||
self.artifacts = []
|
||||
self.events = []
|
||||
self.non_iteration_errors = False
|
||||
self.output_directory = output_directory
|
||||
|
||||
|
||||
class RunEvent(object):
|
||||
"""
|
||||
An event that occured during a run.
|
||||
|
||||
"""
|
||||
def __init__(self, message):
|
||||
self.timestamp = datetime.utcnow()
|
||||
self.message = message
|
||||
|
||||
def to_dict(self):
|
||||
return copy(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.timestamp, self.message)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class IterationResult(object):
|
||||
"""
|
||||
Contains the result of running a single iteration of a workload. It is the
|
||||
responsibility of a workload to instantiate a IterationResult, populate it,
|
||||
and return it form its get_result() method.
|
||||
|
||||
Status explanations:
|
||||
|
||||
:NOT_STARTED: This iteration has not yet started.
|
||||
:RUNNING: This iteration is currently running and no errors have been detected.
|
||||
:OK: This iteration has completed and no errors have been detected
|
||||
:PARTIAL: One or more instruments have failed (the iteration may still be running).
|
||||
:FAILED: The workload itself has failed.
|
||||
:ABORTED: The user interupted the workload
|
||||
:SKIPPED: The iteration was skipped due to a previous failure
|
||||
|
||||
"""
|
||||
|
||||
__metaclass__ = enum_metaclass('values', return_name=True)
|
||||
|
||||
values = ITERATION_STATUS
|
||||
|
||||
def __init__(self, spec):
|
||||
self.spec = spec
|
||||
self.id = spec.id
|
||||
self.workload = spec.workload
|
||||
self.classifiers = copy(spec.classifiers)
|
||||
self.iteration = None
|
||||
self.status = self.NOT_STARTED
|
||||
self.output_directory = None
|
||||
self.events = []
|
||||
self.metrics = []
|
||||
self.artifacts = []
|
||||
|
||||
def add_metric(self, name, value, units=None, lower_is_better=False, classifiers=None):
|
||||
self.metrics.append(Metric(name, value, units, lower_is_better,
|
||||
merge_dicts_simple(self.classifiers, classifiers)))
|
||||
|
||||
def has_metric(self, name):
|
||||
for metric in self.metrics:
|
||||
if metric.name == name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_event(self, message):
|
||||
self.events.append(RunEvent(message))
|
||||
|
||||
def to_dict(self):
|
||||
d = copy(self.__dict__)
|
||||
d['events'] = [e.to_dict() for e in self.events]
|
||||
return d
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.metrics)
|
||||
|
||||
def __getitem__(self, name):
|
||||
for metric in self.metrics:
|
||||
if metric.name == name:
|
||||
return metric
|
||||
raise KeyError('Metric {} not found.'.format(name))
|
||||
|
||||
|
||||
class Metric(object):
|
||||
"""
|
||||
This is a single metric collected from executing a workload.
|
||||
|
||||
:param name: the name of the metric. Uniquely identifies the metric
|
||||
within the results.
|
||||
:param value: The numerical value of the metric for this execution of
|
||||
a workload. This can be either an int or a float.
|
||||
:param units: Units for the collected value. Can be None if the value
|
||||
has no units (e.g. it's a count or a standardised score).
|
||||
:param lower_is_better: Boolean flag indicating where lower values are
|
||||
better than higher ones. Defaults to False.
|
||||
:param classifiers: A set of key-value pairs to further classify this metric
|
||||
beyond current iteration (e.g. this can be used to identify
|
||||
sub-tests).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, value, units=None, lower_is_better=False, classifiers=None):
|
||||
self.name = name
|
||||
self.value = numeric(value)
|
||||
self.units = units
|
||||
self.lower_is_better = lower_is_better
|
||||
self.classifiers = classifiers or {}
|
||||
|
||||
def to_dict(self):
|
||||
return self.__dict__
|
||||
|
||||
def __str__(self):
|
||||
result = '{}: {}'.format(self.name, self.value)
|
||||
if self.units:
|
||||
result += ' ' + self.units
|
||||
result += ' ({})'.format('-' if self.lower_is_better else '+')
|
||||
return '<{}>'.format(result)
|
||||
|
||||
__repr__ = __str__
|
@ -1,272 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
This module wraps louie signalling mechanism. It relies on modified version of loiue
|
||||
that has prioritization added to handler invocation.
|
||||
|
||||
"""
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
|
||||
from louie import dispatcher
|
||||
|
||||
from wlauto.utils.types import prioritylist
|
||||
|
||||
|
||||
logger = logging.getLogger('dispatcher')
|
||||
|
||||
|
||||
class Signal(object):
|
||||
"""
|
||||
This class implements the signals to be used for notifiying callbacks
|
||||
registered to respond to different states and stages of the execution of workload
|
||||
automation.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, description='no description', invert_priority=False):
|
||||
"""
|
||||
Instantiates a Signal.
|
||||
|
||||
:param name: name is the identifier of the Signal object. Signal instances with
|
||||
the same name refer to the same execution stage/stage.
|
||||
:param invert_priority: boolean parameter that determines whether multiple
|
||||
callbacks for the same signal should be ordered with
|
||||
ascending or descending priorities. Typically this flag
|
||||
should be set to True if the Signal is triggered AFTER an
|
||||
a state/stage has been reached. That way callbacks with high
|
||||
priorities will be called right after the event has occured.
|
||||
"""
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.invert_priority = invert_priority
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __hash__(self):
|
||||
return id(self.name)
|
||||
|
||||
|
||||
# These are paired events -- if the before_event is sent, the after_ signal is
|
||||
# guaranteed to also be sent. In particular, the after_ signals will be sent
|
||||
# even if there is an error, so you cannot assume in the handler that the
|
||||
# device has booted successfully. In most cases, you should instead use the
|
||||
# non-paired signals below.
|
||||
BEFORE_FLASHING = Signal('before-flashing-signal', invert_priority=True)
|
||||
SUCCESSFUL_FLASHING = Signal('successful-flashing-signal')
|
||||
AFTER_FLASHING = Signal('after-flashing-signal')
|
||||
|
||||
BEFORE_BOOT = Signal('before-boot-signal', invert_priority=True)
|
||||
SUCCESSFUL_BOOT = Signal('successful-boot-signal')
|
||||
AFTER_BOOT = Signal('after-boot-signal')
|
||||
|
||||
BEFORE_INITIAL_BOOT = Signal('before-initial-boot-signal', invert_priority=True)
|
||||
SUCCESSFUL_INITIAL_BOOT = Signal('successful-initial-boot-signal')
|
||||
AFTER_INITIAL_BOOT = Signal('after-initial-boot-signal')
|
||||
|
||||
BEFORE_FIRST_ITERATION_BOOT = Signal('before-first-iteration-boot-signal', invert_priority=True)
|
||||
SUCCESSFUL_FIRST_ITERATION_BOOT = Signal('successful-first-iteration-boot-signal')
|
||||
AFTER_FIRST_ITERATION_BOOT = Signal('after-first-iteration-boot-signal')
|
||||
|
||||
BEFORE_WORKLOAD_SETUP = Signal('before-workload-setup-signal', invert_priority=True)
|
||||
SUCCESSFUL_WORKLOAD_SETUP = Signal('successful-workload-setup-signal')
|
||||
AFTER_WORKLOAD_SETUP = Signal('after-workload-setup-signal')
|
||||
|
||||
BEFORE_WORKLOAD_EXECUTION = Signal('before-workload-execution-signal', invert_priority=True)
|
||||
SUCCESSFUL_WORKLOAD_EXECUTION = Signal('successful-workload-execution-signal')
|
||||
AFTER_WORKLOAD_EXECUTION = Signal('after-workload-execution-signal')
|
||||
|
||||
BEFORE_WORKLOAD_RESULT_UPDATE = Signal('before-iteration-result-update-signal', invert_priority=True)
|
||||
SUCCESSFUL_WORKLOAD_RESULT_UPDATE = Signal('successful-iteration-result-update-signal')
|
||||
AFTER_WORKLOAD_RESULT_UPDATE = Signal('after-iteration-result-update-signal')
|
||||
|
||||
BEFORE_WORKLOAD_TEARDOWN = Signal('before-workload-teardown-signal', invert_priority=True)
|
||||
SUCCESSFUL_WORKLOAD_TEARDOWN = Signal('successful-workload-teardown-signal')
|
||||
AFTER_WORKLOAD_TEARDOWN = Signal('after-workload-teardown-signal')
|
||||
|
||||
BEFORE_OVERALL_RESULTS_PROCESSING = Signal('before-overall-results-process-signal', invert_priority=True)
|
||||
SUCCESSFUL_OVERALL_RESULTS_PROCESSING = Signal('successful-overall-results-process-signal')
|
||||
AFTER_OVERALL_RESULTS_PROCESSING = Signal('after-overall-results-process-signal')
|
||||
|
||||
# These are the not-paired signals; they are emitted independently. E.g. the
|
||||
# fact that RUN_START was emitted does not mean run end will be.
|
||||
RUN_START = Signal('start-signal', invert_priority=True)
|
||||
RUN_END = Signal('end-signal')
|
||||
WORKLOAD_SPEC_START = Signal('workload-spec-start-signal', invert_priority=True)
|
||||
WORKLOAD_SPEC_END = Signal('workload-spec-end-signal')
|
||||
ITERATION_START = Signal('iteration-start-signal', invert_priority=True)
|
||||
ITERATION_END = Signal('iteration-end-signal')
|
||||
|
||||
RUN_INIT = Signal('run-init-signal')
|
||||
SPEC_INIT = Signal('spec-init-signal')
|
||||
ITERATION_INIT = Signal('iteration-init-signal')
|
||||
|
||||
RUN_FIN = Signal('run-fin-signal')
|
||||
|
||||
# These signals are used by the LoggerFilter to tell about logging events
|
||||
ERROR_LOGGED = Signal('error_logged')
|
||||
WARNING_LOGGED = Signal('warning_logged')
|
||||
|
||||
|
||||
class CallbackPriority(object):
|
||||
|
||||
EXTREMELY_HIGH = 30
|
||||
VERY_HIGH = 20
|
||||
HIGH = 10
|
||||
NORMAL = 0
|
||||
LOW = -10
|
||||
VERY_LOW = -20
|
||||
EXTREMELY_LOW = -30
|
||||
|
||||
def __init__(self):
|
||||
raise ValueError('Cannot instantiate')
|
||||
|
||||
|
||||
class _prioritylist_wrapper(prioritylist):
|
||||
"""
|
||||
This adds a NOP append() method so that when louie invokes it to add the
|
||||
handler to receivers, nothing will happen; the handler is actually added inside
|
||||
the connect() below according to priority, before louie's connect() gets invoked.
|
||||
|
||||
"""
|
||||
|
||||
def append(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def connect(handler, signal, sender=dispatcher.Any, priority=0):
|
||||
"""
|
||||
Connects a callback to a signal, so that the callback will be automatically invoked
|
||||
when that signal is sent.
|
||||
|
||||
Parameters:
|
||||
|
||||
:handler: This can be any callable that that takes the right arguments for
|
||||
the signal. For most signals this means a single argument that
|
||||
will be an ``ExecutionContext`` instance. But please see documentation
|
||||
for individual signals in the :ref:`signals reference <instrumentation_method_map>`.
|
||||
:signal: The signal to which the handler will be subscribed. Please see
|
||||
:ref:`signals reference <instrumentation_method_map>` for the list of standard WA
|
||||
signals.
|
||||
|
||||
.. note:: There is nothing that prevents instrumentation from sending their
|
||||
own signals that are not part of the standard set. However the signal
|
||||
must always be an :class:`wlauto.core.signal.Signal` instance.
|
||||
|
||||
:sender: The handler will be invoked only for the signals emitted by this sender. By
|
||||
default, this is set to :class:`louie.dispatcher.Any`, so the handler will
|
||||
be invoked for signals from any sender.
|
||||
:priority: An integer (positive or negative) the specifies the priority of the handler.
|
||||
Handlers with higher priority will be called before handlers with lower
|
||||
priority. The call order of handlers with the same priority is not specified.
|
||||
Defaults to 0.
|
||||
|
||||
.. note:: Priorities for some signals are inverted (so highest priority
|
||||
handlers get executed last). Please see :ref:`signals reference <instrumentation_method_map>`
|
||||
for details.
|
||||
|
||||
"""
|
||||
if getattr(signal, 'invert_priority', False):
|
||||
priority = -priority
|
||||
senderkey = id(sender)
|
||||
if senderkey in dispatcher.connections:
|
||||
signals = dispatcher.connections[senderkey]
|
||||
else:
|
||||
dispatcher.connections[senderkey] = signals = {}
|
||||
if signal in signals:
|
||||
receivers = signals[signal]
|
||||
else:
|
||||
receivers = signals[signal] = _prioritylist_wrapper()
|
||||
receivers.add(handler, priority)
|
||||
dispatcher.connect(handler, signal, sender)
|
||||
|
||||
|
||||
def disconnect(handler, signal, sender=dispatcher.Any):
|
||||
"""
|
||||
Disconnect a previously connected handler form the specified signal, optionally, only
|
||||
for the specified sender.
|
||||
|
||||
Parameters:
|
||||
|
||||
:handler: The callback to be disconnected.
|
||||
:signal: The signal the handler is to be disconnected form. It will
|
||||
be an :class:`wlauto.core.signal.Signal` instance.
|
||||
:sender: If specified, the handler will only be disconnected from the signal
|
||||
sent by this sender.
|
||||
|
||||
"""
|
||||
dispatcher.disconnect(handler, signal, sender)
|
||||
|
||||
|
||||
def send(signal, sender=dispatcher.Anonymous, *args, **kwargs):
|
||||
"""
|
||||
Sends a signal, causing connected handlers to be invoked.
|
||||
|
||||
Paramters:
|
||||
|
||||
:signal: Signal to be sent. This must be an instance of :class:`wlauto.core.signal.Signal`
|
||||
or its subclasses.
|
||||
:sender: The sender of the signal (typically, this would be ``self``). Some handlers may only
|
||||
be subscribed to signals from a particular sender.
|
||||
|
||||
The rest of the parameters will be passed on as aruments to the handler.
|
||||
|
||||
"""
|
||||
return dispatcher.send(signal, sender, *args, **kwargs)
|
||||
|
||||
|
||||
# This will normally be set to log_error() by init_logging(); see wa.framework/log.py.
|
||||
# Done this way to prevent a circular import dependency.
|
||||
log_error_func = logger.error
|
||||
|
||||
|
||||
def safe_send(signal, sender=dispatcher.Anonymous,
|
||||
propagate=[KeyboardInterrupt], *args, **kwargs):
|
||||
"""
|
||||
Same as ``send``, except this will catch and log all exceptions raised
|
||||
by handlers, except those specified in ``propagate`` argument (defaults
|
||||
to just ``[KeyboardInterrupt]``).
|
||||
"""
|
||||
try:
|
||||
send(singnal, sender, *args, **kwargs)
|
||||
except Exception as e:
|
||||
if any(isinstance(e, p) for p in propagate):
|
||||
raise e
|
||||
log_error_func(e)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def wrap(signal_name, sender=dispatcher.Anonymous, safe=False, *args, **kwargs):
|
||||
"""Wraps the suite in before/after signals, ensuring
|
||||
that after signal is always sent."""
|
||||
signal_name = signal_name.upper().replace('-', '_')
|
||||
send_func = safe_send if safe else send
|
||||
try:
|
||||
before_signal = globals()['BEFORE_' + signal_name]
|
||||
success_signal = globals()['SUCCESSFUL_' + signal_name]
|
||||
after_signal = globals()['AFTER_' + signal_name]
|
||||
except KeyError:
|
||||
raise ValueError('Invalid wrapped signal name: {}'.format(signal_name))
|
||||
try:
|
||||
send_func(before_signal, sender, *args, **kwargs)
|
||||
yield
|
||||
send_func(success_signal, sender, *args, **kwargs)
|
||||
finally:
|
||||
send_func(after_signal, sender, *args, **kwargs)
|
@ -1,26 +0,0 @@
|
||||
# Copyright 2014-2015 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 collections import namedtuple
|
||||
|
||||
VersionTuple = namedtuple('Version', ['major', 'minor', 'revision'])
|
||||
|
||||
version = VersionTuple(2, 4, 0)
|
||||
|
||||
|
||||
def get_wa_version():
|
||||
version_string = '{}.{}.{}'.format(version.major, version.minor, version.revision)
|
||||
return version_string
|
@ -1,104 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
A workload is the unit of execution. It represents a set of activities are are performed
|
||||
and measured together, as well as the necessary setup and teardown procedures. A single
|
||||
execution of a workload produces one :class:`wlauto.core.result.WorkloadResult` that is populated with zero or more
|
||||
:class:`wlauto.core.result.WorkloadMetric`\ s and/or
|
||||
:class:`wlauto.core.result.Artifact`\s by the workload and active instrumentation.
|
||||
|
||||
"""
|
||||
from wlauto.core.plugin import Plugin
|
||||
from wlauto.exceptions import WorkloadError
|
||||
|
||||
|
||||
class Workload(Plugin):
|
||||
"""
|
||||
This is the base class for the workloads executed by the framework.
|
||||
Each of the methods throwing NotImplementedError *must* be implemented
|
||||
by the derived classes.
|
||||
|
||||
"""
|
||||
kind = "workload"
|
||||
supported_devices = []
|
||||
supported_platforms = []
|
||||
summary_metrics = []
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
"""
|
||||
Creates a new Workload.
|
||||
|
||||
:param device: the Device on which the workload will be executed.
|
||||
"""
|
||||
super(Workload, self).__init__(**kwargs)
|
||||
if self.supported_devices and device.name not in self.supported_devices:
|
||||
raise WorkloadError('Workload {} does not support device {}'.format(self.name, device.name))
|
||||
|
||||
if self.supported_platforms and device.os not in self.supported_platforms:
|
||||
raise WorkloadError('Workload {} does not support platform {}'.format(self.name, device.os))
|
||||
self.device = device
|
||||
|
||||
def init_resources(self, context):
|
||||
"""
|
||||
This method may be used to perform early resource discovery and initialization. This is invoked
|
||||
during the initial loading stage and before the device is ready, so cannot be used for any
|
||||
device-dependent initialization. This method is invoked before the workload instance is
|
||||
validated.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def initialize(self, context):
|
||||
"""
|
||||
This method should be used to perform once-per-run initialization of a workload instance, i.e.,
|
||||
unlike ``setup()`` it will not be invoked on each iteration.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup(self, context):
|
||||
"""
|
||||
Perform the setup necessary to run the workload, such as copying the necessary files
|
||||
to the device, configuring the environments, etc.
|
||||
|
||||
This is also the place to perform any on-device checks prior to attempting to execute
|
||||
the workload.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def run(self, context):
|
||||
"""Execute the workload. This is the method that performs the actual "work" of the"""
|
||||
pass
|
||||
|
||||
def update_result(self, context):
|
||||
"""
|
||||
Update the result within the specified execution context with the metrics
|
||||
form this workload iteration.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def teardown(self, context):
|
||||
""" Perform any final clean up for the Workload. """
|
||||
pass
|
||||
|
||||
def finalize(self, context):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return '<Workload {}>'.format(self.name)
|
@ -1,162 +0,0 @@
|
||||
# Copyright 2013-2015 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 wlauto.utils.misc import get_traceback
|
||||
|
||||
from devlib.exception import DevlibError, HostError, TargetError, TimeoutError
|
||||
|
||||
|
||||
class WAError(Exception):
|
||||
"""Base class for all Workload Automation exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(WAError):
|
||||
"""Raised when the specified item is not found."""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(WAError):
|
||||
"""Raised on failure to validate an plugin."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceError(WAError):
|
||||
"""General Device error."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceNotRespondingError(WAError):
|
||||
"""The device is not responding."""
|
||||
|
||||
def __init__(self, device):
|
||||
super(DeviceNotRespondingError, self).__init__('Device {} is not responding.'.format(device))
|
||||
|
||||
|
||||
class WorkloadError(WAError):
|
||||
"""General Workload error."""
|
||||
pass
|
||||
|
||||
|
||||
class HostError(WAError):
|
||||
"""Problem with the host on which WA is running."""
|
||||
pass
|
||||
|
||||
|
||||
class ModuleError(WAError):
|
||||
"""
|
||||
Problem with a module.
|
||||
|
||||
.. note:: Modules for specific plugin types should raise execeptions
|
||||
appropriate to that plugin. E.g. a ``Device`` module should raise
|
||||
``DeviceError``. This is intended for situation where a module is
|
||||
unsure (and/or doesn't care) what its owner is.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InstrumentError(WAError):
|
||||
"""General Instrument error."""
|
||||
pass
|
||||
|
||||
|
||||
class ResultProcessorError(WAError):
|
||||
"""General ResultProcessor error."""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceError(WAError):
|
||||
"""General Resolver error."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(WAError):
|
||||
"""Raised by commands when they have encountered an error condition
|
||||
during execution."""
|
||||
pass
|
||||
|
||||
|
||||
class ToolError(WAError):
|
||||
"""Raised by tools when they have encountered an error condition
|
||||
during execution."""
|
||||
pass
|
||||
|
||||
|
||||
class LoaderError(WAError):
|
||||
"""Raised when there is an error loading an plugin or
|
||||
an external resource. Apart form the usual message, the __init__
|
||||
takes an exc_info parameter which should be the result of
|
||||
sys.exc_info() for the original exception (if any) that
|
||||
caused the error."""
|
||||
|
||||
def __init__(self, message, exc_info=None):
|
||||
super(LoaderError, self).__init__(message)
|
||||
self.exc_info = exc_info
|
||||
|
||||
def __str__(self):
|
||||
if self.exc_info:
|
||||
orig = self.exc_info[1]
|
||||
orig_name = type(orig).__name__
|
||||
if isinstance(orig, WAError):
|
||||
reason = 'because of:\n{}: {}'.format(orig_name, orig)
|
||||
else:
|
||||
reason = 'because of:\n{}\n{}: {}'.format(get_traceback(self.exc_info), orig_name, orig)
|
||||
return '\n'.join([self.message, reason])
|
||||
else:
|
||||
return self.message
|
||||
|
||||
|
||||
class ConfigError(WAError):
|
||||
"""Raised when configuration provided is invalid. This error suggests that
|
||||
the user should modify their config and try again."""
|
||||
pass
|
||||
|
||||
|
||||
class WorkerThreadError(WAError):
|
||||
"""
|
||||
This should get raised in the main thread if a non-WAError-derived exception occurs on
|
||||
a worker/background thread. If a WAError-derived exception is raised in the worker, then
|
||||
it that exception should be re-raised on the main thread directly -- the main point of this is
|
||||
to preserve the backtrace in the output, and backtrace doesn't get output for WAErrors.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, thread, exc_info):
|
||||
self.thread = thread
|
||||
self.exc_info = exc_info
|
||||
orig = self.exc_info[1]
|
||||
orig_name = type(orig).__name__
|
||||
message = 'Exception of type {} occured on thread {}:\n'.format(orig_name, thread)
|
||||
message += '{}\n{}: {}'.format(get_traceback(self.exc_info), orig_name, orig)
|
||||
super(WorkerThreadError, self).__init__(message)
|
||||
|
||||
|
||||
class SerializerSyntaxError(Exception):
|
||||
"""
|
||||
Error loading a serialized structure from/to a file handle.
|
||||
"""
|
||||
|
||||
def __init__(self, message, line=None, column=None):
|
||||
super(SerializerSyntaxError, self).__init__(message)
|
||||
self.line = line
|
||||
self.column = column
|
||||
|
||||
def __str__(self):
|
||||
linestring = ' on line {}'.format(self.line) if self.line else ''
|
||||
colstring = ' in column {}'.format(self.column) if self.column else ''
|
||||
message = 'Syntax Error{}: {}'
|
||||
return message.format(''.join([linestring, colstring]), self.message)
|
74
wlauto/external/README
vendored
74
wlauto/external/README
vendored
@ -1,74 +0,0 @@
|
||||
This directory contains external libraries and standalone utilities which have
|
||||
been written/modified to work with Workload Automation (and thus need to be
|
||||
included with WA rather than obtained from orignal sources).
|
||||
|
||||
|
||||
bbench_server
|
||||
=============
|
||||
|
||||
This is a small sever that is used to detect when ``bbench`` workload has completed.
|
||||
``bbench`` navigates though a bunch of web pages in a browser using javascript.
|
||||
It will cause the browser to sent a GET request to the port the bbench_server is
|
||||
listening on, indicating the end of workload.
|
||||
|
||||
|
||||
daq_server
|
||||
==========
|
||||
|
||||
Contains Daq server files that will run on a Windows machine. Please refer to
|
||||
daq instrument documentation.
|
||||
|
||||
|
||||
louie (third party)
|
||||
=====
|
||||
|
||||
Python package that is itself a fork (and now, a replacement for) pydispatcher.
|
||||
This library provides a signal dispatching mechanism. This has been modified for
|
||||
WA to add prioritization to callbacks.
|
||||
|
||||
|
||||
pmu_logger
|
||||
==========
|
||||
|
||||
Source for the kernel driver that enable the logging of CCI counters to ftrace
|
||||
on periodic basis. This driver is required by the ``cci_pmu_logger`` instrument.
|
||||
|
||||
|
||||
readenergy
|
||||
==========
|
||||
|
||||
Outputs Juno internal energy/power/voltage/current measurments by reading APB
|
||||
regesiters from memory. This is used by ``juno_energy`` instrument.
|
||||
|
||||
|
||||
revent
|
||||
======
|
||||
|
||||
This is a tool that is used to both record and playback key press and screen tap
|
||||
events. It is used to record UI manipulation for some workloads (such as games)
|
||||
where it is not possible to use the Android UI Automator.
|
||||
|
||||
The tools is also included in binary form in wlauto/common/. In order to build
|
||||
the tool from source, you will need to have Android NDK in your PATH.
|
||||
|
||||
|
||||
stacktracer.py (third party)
|
||||
==============
|
||||
|
||||
A module based on an ActiveState recipe that allows tracing thread stacks during
|
||||
execution of a Python program. This is used through the ``--debug`` flag in WA
|
||||
to ease debuging multi-threaded parts of the code.
|
||||
|
||||
|
||||
terminalsize.py (third party)
|
||||
===============
|
||||
|
||||
Implements a platform-agnostic way of determining terminal window size. Taken
|
||||
from a public Github gist.
|
||||
|
||||
|
||||
uiauto
|
||||
======
|
||||
|
||||
Contains the utilities library for UI automation.
|
||||
|
31
wlauto/external/bbench_server/build.sh
vendored
31
wlauto/external/bbench_server/build.sh
vendored
@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
|
||||
BUILD_COMMAND=ndk-build
|
||||
|
||||
if [[ $(which $BUILD_COMMAND) ]] ; then
|
||||
$BUILD_COMMAND
|
||||
if [[ $? ]]; then
|
||||
echo Coping to ../../workloads/bbench/
|
||||
cp libs/armeabi/bbench_server ../../workloads/bbench/bin/armeabi/bbench_server
|
||||
fi
|
||||
else
|
||||
echo Please make sure you have Android NDK in your PATH.
|
||||
exit 1
|
||||
fi
|
||||
|
9
wlauto/external/bbench_server/jni/Android.mk
vendored
9
wlauto/external/bbench_server/jni/Android.mk
vendored
@ -1,9 +0,0 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_SRC_FILES:= bbench_server.cpp
|
||||
LOCAL_MODULE := bbench_server
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_STATIC_LIBRARIES := libc
|
||||
LOCAL_SHARED_LIBRARIES :=
|
||||
include $(BUILD_EXECUTABLE)
|
151
wlauto/external/bbench_server/jni/bbench_server.cpp
vendored
151
wlauto/external/bbench_server/jni/bbench_server.cpp
vendored
@ -1,151 +0,0 @@
|
||||
/* Copyright 2012-2015 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.
|
||||
*/
|
||||
|
||||
/**************************************************************************/
|
||||
/* Simple HTTP server program that will return on accepting connection */
|
||||
/**************************************************************************/
|
||||
|
||||
/* Tested on Android ICS browser and FireFox browser */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netdb.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#define SERVERPORT "3030"
|
||||
|
||||
void ExitOnError(int condition, const char *msg)
|
||||
{
|
||||
if(condition) { printf("Server: %s\n", msg); exit(1);}
|
||||
}
|
||||
|
||||
void *GetInetAddr(struct sockaddr *sa)
|
||||
{
|
||||
if (sa->sa_family == AF_INET)
|
||||
{
|
||||
return &(((struct sockaddr_in*)sa)->sin_addr);
|
||||
}
|
||||
else
|
||||
{
|
||||
return &(((struct sockaddr_in6*)sa)->sin6_addr);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
||||
socklen_t addr_size;
|
||||
struct addrinfo hints, *res;
|
||||
int server_fd, client_fd;
|
||||
int retval;
|
||||
int timeout_in_seconds;
|
||||
|
||||
// Get the timeout value in seconds
|
||||
if(argc < 2)
|
||||
{
|
||||
printf("Usage %s <timeout in seconds>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
timeout_in_seconds = atoi(argv[1]);
|
||||
printf("Server: Waiting for connection on port %s with timeout of %d seconds\n", SERVERPORT, timeout_in_seconds);
|
||||
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/* Listen to a socket */
|
||||
/**************************************************************************/
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
|
||||
|
||||
getaddrinfo(NULL, SERVERPORT, &hints, &res);
|
||||
|
||||
|
||||
server_fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
|
||||
ExitOnError(server_fd < 0, "Socket creation failed");
|
||||
|
||||
retval = bind(server_fd, res->ai_addr, res->ai_addrlen);
|
||||
ExitOnError(retval < 0, "Bind failed");
|
||||
|
||||
retval = listen(server_fd, 10);
|
||||
ExitOnError(retval < 0, "Listen failed");
|
||||
|
||||
/**************************************************************************/
|
||||
/* Wait for connection to arrive or time out */
|
||||
/**************************************************************************/
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(server_fd, &readfds);
|
||||
|
||||
// Timeout parameter
|
||||
timeval tv;
|
||||
tv.tv_sec = timeout_in_seconds;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int ret = select(server_fd+1, &readfds, NULL, NULL, &tv);
|
||||
ExitOnError(ret <= 0, "No connection established, timed out");
|
||||
ExitOnError(FD_ISSET(server_fd, &readfds) == 0, "Error occured in select");
|
||||
|
||||
/**************************************************************************/
|
||||
/* Accept connection and print the information */
|
||||
/**************************************************************************/
|
||||
{
|
||||
struct sockaddr_storage client_addr;
|
||||
char client_addr_string[INET6_ADDRSTRLEN];
|
||||
addr_size = sizeof client_addr;
|
||||
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_size);
|
||||
ExitOnError(client_fd < 0, "Accept failed");
|
||||
|
||||
inet_ntop(client_addr.ss_family,
|
||||
GetInetAddr((struct sockaddr *)&client_addr),
|
||||
client_addr_string,
|
||||
sizeof client_addr_string);
|
||||
printf("Server: Received connection from %s\n", client_addr_string);
|
||||
}
|
||||
|
||||
|
||||
/**************************************************************************/
|
||||
/* Send a acceptable HTTP response */
|
||||
/**************************************************************************/
|
||||
{
|
||||
|
||||
char response[] = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/html\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"<html>"
|
||||
"<head>Local Server: Connection Accepted</head>"
|
||||
"<body></body>"
|
||||
"</html>";
|
||||
int bytes_sent;
|
||||
bytes_sent = send(client_fd, response, strlen(response), 0);
|
||||
ExitOnError(bytes_sent < 0, "Sending Response failed");
|
||||
}
|
||||
|
||||
|
||||
close(client_fd);
|
||||
close(server_fd);
|
||||
return 0;
|
||||
}
|
BIN
wlauto/external/daq_server/daqpower-1.0.5.tar.gz
vendored
BIN
wlauto/external/daq_server/daqpower-1.0.5.tar.gz
vendored
Binary file not shown.
0
wlauto/external/daq_server/src/README
vendored
0
wlauto/external/daq_server/src/README
vendored
25
wlauto/external/daq_server/src/build.sh
vendored
25
wlauto/external/daq_server/src/build.sh
vendored
@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
python setup.py sdist
|
||||
rm -rf build
|
||||
rm -f MANIFEST
|
||||
if [[ -d dist ]]; then
|
||||
mv dist/*.tar.gz ..
|
||||
rm -rf dist
|
||||
fi
|
||||
find . -iname \*.pyc -delete
|
@ -1,17 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
__version__ = '1.0.5'
|
380
wlauto/external/daq_server/src/daqpower/client.py
vendored
380
wlauto/external/daq_server/src/daqpower/client.py
vendored
@ -1,380 +0,0 @@
|
||||
# Copyright 2014-2015 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=E1101,E1103,wrong-import-position
|
||||
import os
|
||||
import sys
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.protocol import Protocol, ClientFactory, ReconnectingClientFactory
|
||||
from twisted.internet.error import ConnectionLost, ConnectionDone
|
||||
from twisted.protocols.basic import LineReceiver
|
||||
|
||||
if __name__ == '__main__': # for debugging
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from daqpower import log
|
||||
from daqpower.common import DaqServerRequest, DaqServerResponse, Status
|
||||
from daqpower.config import get_config_parser
|
||||
|
||||
|
||||
__all__ = ['execute_command', 'run_send_command', 'Status']
|
||||
|
||||
|
||||
class Command(object):
|
||||
|
||||
def __init__(self, name, **params):
|
||||
self.name = name
|
||||
self.params = params
|
||||
|
||||
|
||||
class CommandResult(object):
|
||||
|
||||
def __init__(self):
|
||||
self.status = None
|
||||
self.message = None
|
||||
self.data = None
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.status, self.message)
|
||||
|
||||
|
||||
class CommandExecutorProtocol(Protocol):
|
||||
|
||||
def __init__(self, command, timeout=10, retries=1):
|
||||
self.command = command
|
||||
self.sent_request = None
|
||||
self.waiting_for_response = False
|
||||
self.keep_going = None
|
||||
self.ports_to_pull = None
|
||||
self.factory = None
|
||||
self.timeoutCallback = None
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.retry_count = 0
|
||||
|
||||
def connectionMade(self):
|
||||
if self.command.name == 'get_data':
|
||||
self.sendRequest('list_port_files')
|
||||
else:
|
||||
self.sendRequest(self.command.name, **self.command.params)
|
||||
|
||||
def connectionLost(self, reason=ConnectionDone):
|
||||
if isinstance(reason, ConnectionLost):
|
||||
self.errorOut('connection lost: {}'.format(reason))
|
||||
elif self.waiting_for_response:
|
||||
self.errorOut('Server closed connection without sending a response.')
|
||||
else:
|
||||
log.debug('connection terminated.')
|
||||
|
||||
def sendRequest(self, command, **params):
|
||||
self.sent_request = DaqServerRequest(command, params)
|
||||
request_string = self.sent_request.serialize()
|
||||
log.debug('sending request: {}'.format(request_string))
|
||||
self.transport.write(''.join([request_string, '\r\n']))
|
||||
self.timeoutCallback = reactor.callLater(self.timeout, self.requestTimedOut)
|
||||
self.waiting_for_response = True
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.keep_going = False
|
||||
if self.waiting_for_response:
|
||||
self.waiting_for_response = False
|
||||
self.timeoutCallback.cancel()
|
||||
try:
|
||||
response = DaqServerResponse.deserialize(data)
|
||||
except Exception, e: # pylint: disable=W0703
|
||||
self.errorOut('Invalid response: {} ({})'.format(data, e))
|
||||
else:
|
||||
if response.status != Status.ERROR:
|
||||
self.processResponse(response) # may set self.keep_going
|
||||
if not self.keep_going:
|
||||
self.commandCompleted(response.status, response.message, response.data)
|
||||
else:
|
||||
self.errorOut(response.message)
|
||||
else:
|
||||
self.errorOut('unexpected data received: {}\n'.format(data))
|
||||
|
||||
def processResponse(self, response):
|
||||
if self.sent_request.command in ['list_ports', 'list_port_files']:
|
||||
self.processPortsResponse(response)
|
||||
elif self.sent_request.command == 'list_devices':
|
||||
self.processDevicesResponse(response)
|
||||
elif self.sent_request.command == 'pull':
|
||||
self.processPullResponse(response)
|
||||
|
||||
def processPortsResponse(self, response):
|
||||
if 'ports' not in response.data:
|
||||
self.errorOut('Response did not containt ports data: {} ({}).'.format(response, response.data))
|
||||
ports = response.data['ports']
|
||||
response.data = ports
|
||||
if self.command.name == 'get_data':
|
||||
if ports:
|
||||
self.ports_to_pull = ports
|
||||
self.sendPullRequest(self.ports_to_pull.pop())
|
||||
else:
|
||||
response.status = Status.OKISH
|
||||
response.message = 'No ports were returned.'
|
||||
|
||||
def processDevicesResponse(self, response):
|
||||
if response.status == Status.OK:
|
||||
if 'devices' not in response.data:
|
||||
self.errorOut('Response did not containt devices data: {} ({}).'.format(response, response.data))
|
||||
devices = response.data['devices']
|
||||
response.data = devices
|
||||
|
||||
def sendPullRequest(self, port_id):
|
||||
self.sendRequest('pull', port_id=port_id)
|
||||
self.keep_going = True
|
||||
|
||||
def processPullResponse(self, response):
|
||||
if 'port_number' not in response.data:
|
||||
self.errorOut('Response does not contain port number: {} ({}).'.format(response, response.data))
|
||||
port_number = response.data.pop('port_number')
|
||||
filename = self.sent_request.params['port_id'] + '.csv'
|
||||
self.factory.initiateFileTransfer(filename, port_number)
|
||||
if self.ports_to_pull:
|
||||
self.sendPullRequest(self.ports_to_pull.pop())
|
||||
|
||||
def commandCompleted(self, status, message=None, data=None):
|
||||
self.factory.result.status = status
|
||||
self.factory.result.message = message
|
||||
self.factory.result.data = data
|
||||
self.transport.loseConnection()
|
||||
|
||||
def requestTimedOut(self):
|
||||
self.retry_count += 1
|
||||
if self.retry_count > self.retries:
|
||||
self.errorOut("Request timed out; server failed to respond.")
|
||||
else:
|
||||
log.debug('Retrying...')
|
||||
self.connectionMade()
|
||||
|
||||
def errorOut(self, message):
|
||||
self.factory.errorOut(message)
|
||||
|
||||
|
||||
class CommandExecutorFactory(ClientFactory):
|
||||
|
||||
protocol = CommandExecutorProtocol
|
||||
wait_delay = 1
|
||||
|
||||
def __init__(self, config, command, timeout=10, retries=1):
|
||||
self.config = config
|
||||
self.command = command
|
||||
self.timeout = timeout
|
||||
self.retries = retries
|
||||
self.result = CommandResult()
|
||||
self.done = False
|
||||
self.transfers_in_progress = {}
|
||||
if command.name == 'get_data':
|
||||
if 'output_directory' not in command.params:
|
||||
self.errorOut('output_directory not specifed for get_data command.')
|
||||
self.output_directory = command.params['output_directory']
|
||||
if not os.path.isdir(self.output_directory):
|
||||
log.debug('Creating output directory {}'.format(self.output_directory))
|
||||
os.makedirs(self.output_directory)
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
protocol = CommandExecutorProtocol(self.command, self.timeout, self.retries)
|
||||
protocol.factory = self
|
||||
return protocol
|
||||
|
||||
def initiateFileTransfer(self, filename, port):
|
||||
log.debug('Downloading {} from port {}'.format(filename, port))
|
||||
filepath = os.path.join(self.output_directory, filename)
|
||||
session = FileReceiverFactory(filepath, self)
|
||||
connector = reactor.connectTCP(self.config.host, port, session)
|
||||
self.transfers_in_progress[session] = connector
|
||||
|
||||
def transferComplete(self, session):
|
||||
connector = self.transfers_in_progress[session]
|
||||
log.debug('Transfer on port {} complete.'.format(connector.port))
|
||||
del self.transfers_in_progress[session]
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
if self.transfers_in_progress:
|
||||
log.debug('Waiting for the transfer(s) to complete.')
|
||||
self.waitForTransfersToCompleteAndExit()
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
self.result.status = Status.ERROR
|
||||
self.result.message = 'Could not connect to server.'
|
||||
self.waitForTransfersToCompleteAndExit()
|
||||
|
||||
def waitForTransfersToCompleteAndExit(self):
|
||||
if self.transfers_in_progress:
|
||||
reactor.callLater(self.wait_delay, self.waitForTransfersToCompleteAndExit)
|
||||
else:
|
||||
log.debug('Stopping the reactor.')
|
||||
reactor.stop()
|
||||
|
||||
def errorOut(self, message):
|
||||
self.result.status = Status.ERROR
|
||||
self.result.message = message
|
||||
reactor.crash()
|
||||
|
||||
def __str__(self):
|
||||
return '<CommandExecutorProtocol {}>'.format(self.command.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class FileReceiver(LineReceiver): # pylint: disable=W0223
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.fh = None
|
||||
self.factory = None
|
||||
|
||||
def connectionMade(self):
|
||||
if os.path.isfile(self.path):
|
||||
log.warning('overriding existing file.')
|
||||
os.remove(self.path)
|
||||
self.fh = open(self.path, 'w')
|
||||
|
||||
def connectionLost(self, reason=ConnectionDone):
|
||||
if self.fh:
|
||||
self.fh.close()
|
||||
|
||||
def lineReceived(self, line):
|
||||
line = line.rstrip('\r\n') + '\n'
|
||||
self.fh.write(line)
|
||||
|
||||
|
||||
class FileReceiverFactory(ReconnectingClientFactory):
|
||||
|
||||
def __init__(self, path, owner):
|
||||
self.path = path
|
||||
self.owner = owner
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
protocol = FileReceiver(self.path)
|
||||
protocol.factory = self
|
||||
self.resetDelay()
|
||||
return protocol
|
||||
|
||||
def clientConnectionLost(self, conector, reason):
|
||||
if isinstance(reason, ConnectionLost):
|
||||
log.error('Connection lost: {}'.format(reason))
|
||||
ReconnectingClientFactory.clientConnectionLost(self, conector, reason)
|
||||
else:
|
||||
self.owner.transferComplete(self)
|
||||
|
||||
def clientConnectionFailed(self, conector, reason):
|
||||
if isinstance(reason, ConnectionLost):
|
||||
log.error('Connection failed: {}'.format(reason))
|
||||
ReconnectingClientFactory.clientConnectionFailed(self, conector, reason)
|
||||
|
||||
def __str__(self):
|
||||
return '<FileReceiver {}>'.format(self.path)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
def execute_command(server_config, command, **kwargs):
|
||||
before_fds = _get_open_fds() # see the comment in the finally clause below
|
||||
if isinstance(command, basestring):
|
||||
command = Command(command, **kwargs)
|
||||
timeout = 300 if command.name in ['stop', 'pull'] else 10
|
||||
factory = CommandExecutorFactory(server_config, command, timeout)
|
||||
|
||||
# reactors aren't designed to be re-startable. In order to be
|
||||
# able to call execute_command multiple times, we need to froce
|
||||
# re-installation of the reactor; hence this hackery.
|
||||
# TODO: look into implementing restartable reactors. According to the
|
||||
# Twisted FAQ, there is no good reason why there isn't one:
|
||||
# http://twistedmatrix.com/trac/wiki/FrequentlyAskedQuestions#WhycanttheTwistedsreactorberestarted
|
||||
from twisted.internet import default
|
||||
del sys.modules['twisted.internet.reactor']
|
||||
default.install()
|
||||
global reactor # pylint: disable=W0603
|
||||
reactor = sys.modules['twisted.internet.reactor']
|
||||
|
||||
try:
|
||||
reactor.connectTCP(server_config.host, server_config.port, factory)
|
||||
reactor.run()
|
||||
return factory.result
|
||||
finally:
|
||||
# re-startable reactor hack part 2.
|
||||
# twisted hijacks SIGINT and doesn't bother to un-hijack it when the reactor
|
||||
# stops. So we have to do it for it *rolls eye*.
|
||||
import signal
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
# OK, the reactor is also leaking file descriptors. Tracking down all
|
||||
# of them is non trivial, so instead we're just comparing the before
|
||||
# and after lists of open FDs for the current process, and closing all
|
||||
# new ones, as execute_command should never leave anything open after
|
||||
# it exits (even when downloading data files from the server).
|
||||
# TODO: This is way too hacky even compared to the rest of this function.
|
||||
# Additionally, the current implementation ties this to UNIX,
|
||||
# so in the long run, we need to do this properly and get the FDs
|
||||
# from the reactor.
|
||||
after_fds = _get_open_fds()
|
||||
for fd in after_fds - before_fds:
|
||||
try:
|
||||
os.close(int(fd[1:]))
|
||||
except OSError:
|
||||
pass
|
||||
# Below is the alternative code that gets FDs from the reactor, however
|
||||
# at the moment it doesn't seem to get everything, which is why code
|
||||
# above is used instead.
|
||||
#for fd in readtor._selectables:
|
||||
# os.close(fd)
|
||||
#reactor._poller.close()
|
||||
|
||||
|
||||
def _get_open_fds():
|
||||
if os.name == 'posix':
|
||||
import subprocess
|
||||
pid = os.getpid()
|
||||
procs = subprocess.check_output(["lsof", '-w', '-Ff', "-p", str(pid)])
|
||||
return set(procs.split())
|
||||
else:
|
||||
# TODO: Implement the Windows equivalent.
|
||||
return []
|
||||
|
||||
|
||||
def run_send_command():
|
||||
"""Main entry point when running as a script -- should not be invoked form another module."""
|
||||
parser = get_config_parser()
|
||||
parser.add_argument('command')
|
||||
parser.add_argument('-o', '--output-directory', metavar='DIR', default='.',
|
||||
help='Directory used to output data files (defaults to the current directory).')
|
||||
parser.add_argument('--verbose', help='Produce verobose output.', action='store_true', default=False)
|
||||
args = parser.parse_args()
|
||||
if not args.device_config.labels:
|
||||
args.device_config.labels = ['PORT_{}'.format(i) for i in xrange(len(args.device_config.resistor_values))]
|
||||
|
||||
if args.verbose:
|
||||
log.start_logging('DEBUG')
|
||||
else:
|
||||
log.start_logging('INFO', fmt='%(levelname)-8s %(message)s')
|
||||
|
||||
if args.command == 'configure':
|
||||
args.device_config.validate()
|
||||
command = Command(args.command, config=args.device_config)
|
||||
elif args.command == 'get_data':
|
||||
command = Command(args.command, output_directory=args.output_directory)
|
||||
else:
|
||||
command = Command(args.command)
|
||||
|
||||
result = execute_command(args.server_config, command)
|
||||
print result
|
||||
if result.data:
|
||||
print result.data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_send_command()
|
103
wlauto/external/daq_server/src/daqpower/common.py
vendored
103
wlauto/external/daq_server/src/daqpower/common.py
vendored
@ -1,103 +0,0 @@
|
||||
# Copyright 2014-2015 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=E1101
|
||||
import json
|
||||
|
||||
|
||||
class Serializer(json.JSONEncoder):
|
||||
|
||||
def default(self, o): # pylint: disable=E0202
|
||||
if isinstance(o, Serializable):
|
||||
return o.serialize()
|
||||
if isinstance(o, EnumEntry):
|
||||
return o.name
|
||||
return json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
class Serializable(object):
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, text):
|
||||
return cls(**json.loads(text))
|
||||
|
||||
def serialize(self, d=None):
|
||||
if d is None:
|
||||
d = self.__dict__
|
||||
return json.dumps(d, cls=Serializer)
|
||||
|
||||
|
||||
class DaqServerRequest(Serializable):
|
||||
|
||||
def __init__(self, command, params=None): # pylint: disable=W0231
|
||||
self.command = command
|
||||
self.params = params or {}
|
||||
|
||||
|
||||
class DaqServerResponse(Serializable):
|
||||
|
||||
def __init__(self, status, message=None, data=None): # pylint: disable=W0231
|
||||
self.status = status
|
||||
self.message = message.strip().replace('\r\n', ' ') if message else ''
|
||||
self.data = data or {}
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.status, self.message or '')
|
||||
|
||||
|
||||
class EnumEntry(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.name, str(other))
|
||||
|
||||
|
||||
class Enum(object):
|
||||
"""
|
||||
Assuming MyEnum = Enum('A', 'B'),
|
||||
|
||||
MyEnum.A and MyEnum.B are valid values.
|
||||
|
||||
a = MyEnum.A
|
||||
(a == MyEnum.A) == True
|
||||
(a in MyEnum) == True
|
||||
|
||||
MyEnum('A') == MyEnum.A
|
||||
|
||||
str(MyEnum.A) == 'A'
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
for a in args:
|
||||
setattr(self, a, EnumEntry(a))
|
||||
|
||||
def __call__(self, value):
|
||||
if value not in self.__dict__:
|
||||
raise ValueError('Not enum value: {}'.format(value))
|
||||
return self.__dict__[value]
|
||||
|
||||
def __iter__(self):
|
||||
for e in self.__dict__:
|
||||
yield self.__dict__[e]
|
||||
|
||||
|
||||
Status = Enum('OK', 'OKISH', 'ERROR')
|
153
wlauto/external/daq_server/src/daqpower/config.py
vendored
153
wlauto/external/daq_server/src/daqpower/config.py
vendored
@ -1,153 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import argparse
|
||||
|
||||
from daqpower.common import Serializable
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
"""Raised when configuration passed into DaqServer is invaid."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceConfiguration(Serializable):
|
||||
"""Encapulates configuration for the DAQ, typically, passed from
|
||||
the client."""
|
||||
|
||||
valid_settings = ['device_id', 'v_range', 'dv_range', 'sampling_rate', 'resistor_values', 'labels']
|
||||
|
||||
default_device_id = 'Dev1'
|
||||
default_v_range = 2.5
|
||||
default_dv_range = 0.2
|
||||
default_sampling_rate = 10000
|
||||
# Channel map used in DAQ 6363 and similar.
|
||||
default_channel_map = (0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23)
|
||||
|
||||
@property
|
||||
def number_of_ports(self):
|
||||
return len(self.resistor_values)
|
||||
|
||||
def __init__(self, **kwargs): # pylint: disable=W0231
|
||||
try:
|
||||
self.device_id = kwargs.pop('device_id') or self.default_device_id
|
||||
self.v_range = float(kwargs.pop('v_range') or self.default_v_range)
|
||||
self.dv_range = float(kwargs.pop('dv_range') or self.default_dv_range)
|
||||
self.sampling_rate = int(kwargs.pop('sampling_rate') or self.default_sampling_rate)
|
||||
self.resistor_values = kwargs.pop('resistor_values') or []
|
||||
self.channel_map = kwargs.pop('channel_map') or self.default_channel_map
|
||||
self.labels = (kwargs.pop('labels') or
|
||||
['PORT_{}.csv'.format(i) for i in xrange(len(self.resistor_values))])
|
||||
except KeyError, e:
|
||||
raise ConfigurationError('Missing config: {}'.format(e.message))
|
||||
if kwargs:
|
||||
raise ConfigurationError('Unexpected config: {}'.format(kwargs))
|
||||
|
||||
def validate(self):
|
||||
if not self.number_of_ports:
|
||||
raise ConfigurationError('No resistor values were specified.')
|
||||
if len(self.resistor_values) != len(self.labels):
|
||||
message = 'The number of resistors ({}) does not match the number of labels ({})'
|
||||
raise ConfigurationError(message.format(len(self.resistor_values), len(self.labels)))
|
||||
|
||||
def __str__(self):
|
||||
return self.serialize()
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class ServerConfiguration(object):
|
||||
"""Client-side server configuration."""
|
||||
|
||||
valid_settings = ['host', 'port']
|
||||
|
||||
default_host = '127.0.0.1'
|
||||
default_port = 45677
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.host = kwargs.pop('host', None) or self.default_host
|
||||
self.port = kwargs.pop('port', None) or self.default_port
|
||||
if kwargs:
|
||||
raise ConfigurationError('Unexpected config: {}'.format(kwargs))
|
||||
|
||||
def validate(self):
|
||||
if not self.host:
|
||||
raise ConfigurationError('Server host not specified.')
|
||||
if not self.port:
|
||||
raise ConfigurationError('Server port not specified.')
|
||||
elif not isinstance(self.port, int):
|
||||
raise ConfigurationError('Server port must be an integer.')
|
||||
|
||||
|
||||
class UpdateDeviceConfig(argparse.Action):
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
setting = option_string.strip('-').replace('-', '_')
|
||||
if setting not in DeviceConfiguration.valid_settings:
|
||||
raise ConfigurationError('Unkown option: {}'.format(option_string))
|
||||
setattr(namespace._device_config, setting, values) # pylint: disable=protected-access
|
||||
|
||||
|
||||
class UpdateServerConfig(argparse.Action):
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
setting = option_string.strip('-').replace('-', '_')
|
||||
if setting not in namespace.server_config.valid_settings:
|
||||
raise ConfigurationError('Unkown option: {}'.format(option_string))
|
||||
setattr(namespace.server_config, setting, values)
|
||||
|
||||
|
||||
class ConfigNamespace(object):
|
||||
|
||||
class _N(object):
|
||||
def __init__(self):
|
||||
self.device_id = None
|
||||
self.v_range = None
|
||||
self.dv_range = None
|
||||
self.sampling_rate = None
|
||||
self.resistor_values = None
|
||||
self.labels = None
|
||||
self.channel_map = None
|
||||
|
||||
@property
|
||||
def device_config(self):
|
||||
return DeviceConfiguration(**self._device_config.__dict__)
|
||||
|
||||
def __init__(self):
|
||||
self._device_config = self._N()
|
||||
self.server_config = ServerConfiguration()
|
||||
|
||||
|
||||
class ConfigArgumentParser(argparse.ArgumentParser):
|
||||
|
||||
def parse_args(self, *args, **kwargs):
|
||||
kwargs['namespace'] = ConfigNamespace()
|
||||
return super(ConfigArgumentParser, self).parse_args(*args, **kwargs)
|
||||
|
||||
|
||||
def get_config_parser(server=True, device=True):
|
||||
parser = ConfigArgumentParser()
|
||||
if device:
|
||||
parser.add_argument('--device-id', action=UpdateDeviceConfig)
|
||||
parser.add_argument('--v-range', action=UpdateDeviceConfig, type=float)
|
||||
parser.add_argument('--dv-range', action=UpdateDeviceConfig, type=float)
|
||||
parser.add_argument('--sampling-rate', action=UpdateDeviceConfig, type=int)
|
||||
parser.add_argument('--resistor-values', action=UpdateDeviceConfig, type=float, nargs='*')
|
||||
parser.add_argument('--labels', action=UpdateDeviceConfig, nargs='*')
|
||||
if server:
|
||||
parser.add_argument('--host', action=UpdateServerConfig)
|
||||
parser.add_argument('--port', action=UpdateServerConfig, type=int)
|
||||
return parser
|
347
wlauto/external/daq_server/src/daqpower/daq.py
vendored
347
wlauto/external/daq_server/src/daqpower/daq.py
vendored
@ -1,347 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Creates a new DAQ device class. This class assumes that there is a
|
||||
DAQ connected and mapped as Dev1. It assumes a specific syndesmology on the DAQ (it is not
|
||||
meant to be a generic DAQ interface). The following diagram shows the wiring for one DaqDevice
|
||||
port::
|
||||
|
||||
Port 0
|
||||
========
|
||||
| A0+ <--- Vr -------------------------|
|
||||
| |
|
||||
| A0- <--- GND -------------------// |
|
||||
| |
|
||||
| A1+ <--- V+ ------------|-------V+ |
|
||||
| r | |
|
||||
| A1- <--- Vr --/\/\/\----| |
|
||||
| | |
|
||||
| | |
|
||||
| |--------------------------|
|
||||
========
|
||||
|
||||
:number_of_ports: The number of ports connected on the DAQ. Each port requires 2 DAQ Channels
|
||||
one for the source voltage and one for the Voltage drop over the
|
||||
resistor r (V+ - Vr) allows us to detect the current.
|
||||
:resistor_value: The resistance of r. Typically a few milliOhm
|
||||
:downsample: The number of samples combined to create one Power point. If set to one
|
||||
each sample corresponds to one reported power point.
|
||||
:sampling_rate: The rate at which DAQ takes a sample from each channel.
|
||||
|
||||
"""
|
||||
# pylint: disable=F0401,E1101,W0621,no-name-in-module,wrong-import-position,wrong-import-order
|
||||
import os
|
||||
import sys
|
||||
import csv
|
||||
import time
|
||||
import threading
|
||||
from Queue import Queue, Empty
|
||||
|
||||
import numpy
|
||||
|
||||
from PyDAQmx import Task, DAQError
|
||||
try:
|
||||
from PyDAQmx.DAQmxFunctions import DAQmxGetSysDevNames
|
||||
CAN_ENUMERATE_DEVICES = True
|
||||
except ImportError: # earlier driver version
|
||||
DAQmxGetSysDevNames = None
|
||||
CAN_ENUMERATE_DEVICES = False
|
||||
|
||||
from PyDAQmx.DAQmxTypes import int32, byref, create_string_buffer
|
||||
from PyDAQmx.DAQmxConstants import (DAQmx_Val_Diff, DAQmx_Val_Volts, DAQmx_Val_GroupByScanNumber, DAQmx_Val_Auto,
|
||||
DAQmx_Val_Rising, DAQmx_Val_ContSamps)
|
||||
|
||||
try:
|
||||
from PyDAQmx.DAQmxConstants import DAQmx_Val_Acquired_Into_Buffer
|
||||
callbacks_supported = True
|
||||
except ImportError: # earlier driver version
|
||||
DAQmx_Val_Acquired_Into_Buffer = None
|
||||
callbacks_supported = False
|
||||
|
||||
|
||||
from daqpower import log
|
||||
|
||||
|
||||
def list_available_devices():
|
||||
"""Returns the list of DAQ devices visible to the driver."""
|
||||
if DAQmxGetSysDevNames:
|
||||
bufsize = 2048 # Should be plenty for all but the most pathalogical of situations.
|
||||
buf = create_string_buffer('\000' * bufsize)
|
||||
DAQmxGetSysDevNames(buf, bufsize)
|
||||
return buf.value.split(',')
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class ReadSamplesBaseTask(Task):
|
||||
|
||||
def __init__(self, config, consumer):
|
||||
Task.__init__(self)
|
||||
self.config = config
|
||||
self.consumer = consumer
|
||||
self.sample_buffer_size = (self.config.sampling_rate + 1) * self.config.number_of_ports * 2
|
||||
self.samples_read = int32()
|
||||
self.remainder = []
|
||||
# create voltage channels
|
||||
for i in xrange(0, 2 * self.config.number_of_ports, 2):
|
||||
self.CreateAIVoltageChan('{}/ai{}'.format(config.device_id, config.channel_map[i]),
|
||||
'', DAQmx_Val_Diff,
|
||||
-config.v_range, config.v_range,
|
||||
DAQmx_Val_Volts, None)
|
||||
self.CreateAIVoltageChan('{}/ai{}'.format(config.device_id, config.channel_map[i + 1]),
|
||||
'', DAQmx_Val_Diff,
|
||||
-config.dv_range, config.dv_range,
|
||||
DAQmx_Val_Volts, None)
|
||||
# configure sampling rate
|
||||
self.CfgSampClkTiming('',
|
||||
self.config.sampling_rate,
|
||||
DAQmx_Val_Rising,
|
||||
DAQmx_Val_ContSamps,
|
||||
self.config.sampling_rate)
|
||||
|
||||
|
||||
class ReadSamplesCallbackTask(ReadSamplesBaseTask):
|
||||
"""
|
||||
More recent verisons of the driver (on Windows) support callbacks
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config, consumer):
|
||||
ReadSamplesBaseTask.__init__(self, config, consumer)
|
||||
# register callbacks
|
||||
self.AutoRegisterEveryNSamplesEvent(DAQmx_Val_Acquired_Into_Buffer, self.config.sampling_rate // 2, 0)
|
||||
self.AutoRegisterDoneEvent(0)
|
||||
|
||||
def EveryNCallback(self):
|
||||
# Note to future self: do NOT try to "optimize" this but re-using the same array and just
|
||||
# zeroing it out each time. The writes happen asynchronously and if your zero it out too soon,
|
||||
# you'll see a whole bunch of 0.0's in the output. If you wanna go down that route, you'll need
|
||||
# cycler through several arrays and have the code that's actually doing the writing zero them out
|
||||
# mark them as available to be used by this call. But, honestly, numpy array allocation does not
|
||||
# appear to be a bottleneck at the moment, so the current solution is "good enough".
|
||||
samples_buffer = numpy.zeros((self.sample_buffer_size,), dtype=numpy.float64)
|
||||
self.ReadAnalogF64(DAQmx_Val_Auto, 0.0, DAQmx_Val_GroupByScanNumber, samples_buffer,
|
||||
self.sample_buffer_size, byref(self.samples_read), None)
|
||||
self.consumer.write((samples_buffer, self.samples_read.value))
|
||||
|
||||
def DoneCallback(self, status): # pylint: disable=W0613,R0201
|
||||
return 0 # The function should return an integer
|
||||
|
||||
|
||||
class ReadSamplesThreadedTask(ReadSamplesBaseTask):
|
||||
"""
|
||||
Earlier verisons of the driver (on CentOS) do not support callbacks. So need
|
||||
to create a thread to periodically poll the buffer
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config, consumer):
|
||||
ReadSamplesBaseTask.__init__(self, config, consumer)
|
||||
self.poller = DaqPoller(self)
|
||||
|
||||
def StartTask(self):
|
||||
ReadSamplesBaseTask.StartTask(self)
|
||||
self.poller.start()
|
||||
|
||||
def StopTask(self):
|
||||
self.poller.stop()
|
||||
ReadSamplesBaseTask.StopTask(self)
|
||||
|
||||
|
||||
class DaqPoller(threading.Thread):
|
||||
|
||||
def __init__(self, task, wait_period=1):
|
||||
super(DaqPoller, self).__init__()
|
||||
self.task = task
|
||||
self.wait_period = wait_period
|
||||
self._stop_signal = threading.Event()
|
||||
self.samples_buffer = numpy.zeros((self.task.sample_buffer_size,), dtype=numpy.float64)
|
||||
|
||||
def run(self):
|
||||
while not self._stop_signal.is_set():
|
||||
# Note to future self: see the comment inside EventNCallback() above
|
||||
samples_buffer = numpy.zeros((self.task.sample_buffer_size,), dtype=numpy.float64)
|
||||
try:
|
||||
self.task.ReadAnalogF64(DAQmx_Val_Auto, self.wait_period, DAQmx_Val_GroupByScanNumber, samples_buffer,
|
||||
self.task.sample_buffer_size, byref(self.task.samples_read), None)
|
||||
except DAQError:
|
||||
pass
|
||||
self.task.consumer.write((samples_buffer, self.task.samples_read.value))
|
||||
|
||||
def stop(self):
|
||||
self._stop_signal.set()
|
||||
self.join()
|
||||
|
||||
|
||||
class AsyncWriter(threading.Thread):
|
||||
|
||||
def __init__(self, wait_period=1):
|
||||
super(AsyncWriter, self).__init__()
|
||||
self.daemon = True
|
||||
self.wait_period = wait_period
|
||||
self.running = threading.Event()
|
||||
self._stop_signal = threading.Event()
|
||||
self._queue = Queue()
|
||||
|
||||
def write(self, stuff):
|
||||
if self._stop_signal.is_set():
|
||||
raise IOError('Attempting to writer to {} after it has been closed.'.format(self.__class__.__name__))
|
||||
self._queue.put(stuff)
|
||||
|
||||
def do_write(self, stuff):
|
||||
raise NotImplementedError()
|
||||
|
||||
def run(self):
|
||||
self.running.set()
|
||||
while True:
|
||||
if self._stop_signal.is_set() and self._queue.empty():
|
||||
break
|
||||
try:
|
||||
self.do_write(self._queue.get(block=True, timeout=self.wait_period))
|
||||
except Empty:
|
||||
pass # carry on
|
||||
self.running.clear()
|
||||
|
||||
def stop(self):
|
||||
self._stop_signal.set()
|
||||
|
||||
def wait(self):
|
||||
while self.running.is_set():
|
||||
time.sleep(self.wait_period)
|
||||
|
||||
|
||||
class PortWriter(object):
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.fh = open(path, 'w', 0)
|
||||
self.writer = csv.writer(self.fh)
|
||||
self.writer.writerow(['power', 'voltage'])
|
||||
|
||||
def write(self, row):
|
||||
self.writer.writerow(row)
|
||||
|
||||
def close(self):
|
||||
self.fh.close()
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
|
||||
class SamplePorcessorError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SampleProcessor(AsyncWriter):
|
||||
|
||||
def __init__(self, resistor_values, output_directory, labels):
|
||||
super(SampleProcessor, self).__init__()
|
||||
self.resistor_values = resistor_values
|
||||
self.output_directory = output_directory
|
||||
self.labels = labels
|
||||
self.number_of_ports = len(resistor_values)
|
||||
if len(self.labels) != self.number_of_ports:
|
||||
message = 'Number of labels ({}) does not match number of ports ({}).'
|
||||
raise SamplePorcessorError(message.format(len(self.labels), self.number_of_ports))
|
||||
self.port_writers = []
|
||||
|
||||
def do_write(self, sample_tuple):
|
||||
samples, number_of_samples = sample_tuple
|
||||
for i in xrange(0, number_of_samples * self.number_of_ports * 2, self.number_of_ports * 2):
|
||||
for j in xrange(self.number_of_ports):
|
||||
V = float(samples[i + 2 * j])
|
||||
DV = float(samples[i + 2 * j + 1])
|
||||
P = V * (DV / self.resistor_values[j])
|
||||
self.port_writers[j].write([P, V])
|
||||
|
||||
def start(self):
|
||||
for label in self.labels:
|
||||
port_file = self.get_port_file_path(label)
|
||||
writer = PortWriter(port_file)
|
||||
self.port_writers.append(writer)
|
||||
super(SampleProcessor, self).start()
|
||||
|
||||
def stop(self):
|
||||
super(SampleProcessor, self).stop()
|
||||
self.wait()
|
||||
for writer in self.port_writers:
|
||||
writer.close()
|
||||
|
||||
def get_port_file_path(self, port_id):
|
||||
if port_id in self.labels:
|
||||
return os.path.join(self.output_directory, port_id + '.csv')
|
||||
else:
|
||||
raise SamplePorcessorError('Invalid port ID: {}'.format(port_id))
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
|
||||
|
||||
class DaqRunner(object):
|
||||
|
||||
@property
|
||||
def number_of_ports(self):
|
||||
return self.config.number_of_ports
|
||||
|
||||
def __init__(self, config, output_directory):
|
||||
self.config = config
|
||||
self.processor = SampleProcessor(config.resistor_values, output_directory, config.labels)
|
||||
if callbacks_supported:
|
||||
self.task = ReadSamplesCallbackTask(config, self.processor)
|
||||
else:
|
||||
self.task = ReadSamplesThreadedTask(config, self.processor) # pylint: disable=redefined-variable-type
|
||||
self.is_running = False
|
||||
|
||||
def start(self):
|
||||
log.debug('Starting sample processor.')
|
||||
self.processor.start()
|
||||
log.debug('Starting DAQ Task.')
|
||||
self.task.StartTask()
|
||||
self.is_running = True
|
||||
log.debug('Runner started.')
|
||||
|
||||
def stop(self):
|
||||
self.is_running = False
|
||||
log.debug('Stopping DAQ Task.')
|
||||
self.task.StopTask()
|
||||
log.debug('Stopping sample processor.')
|
||||
self.processor.stop()
|
||||
log.debug('Runner stopped.')
|
||||
|
||||
def get_port_file_path(self, port_id):
|
||||
return self.processor.get_port_file_path(port_id)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from collections import namedtuple
|
||||
DeviceConfig = namedtuple('DeviceConfig', ['device_id', 'channel_map', 'resistor_values',
|
||||
'v_range', 'dv_range', 'sampling_rate',
|
||||
'number_of_ports', 'labels'])
|
||||
channel_map = (0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23)
|
||||
resistor_values = [0.005]
|
||||
labels = ['PORT_0']
|
||||
dev_config = DeviceConfig('Dev1', channel_map, resistor_values, 2.5, 0.2, 10000, len(resistor_values), labels)
|
||||
if len(sys.argv) != 3:
|
||||
print 'Usage: {} OUTDIR DURATION'.format(os.path.basename(__file__))
|
||||
sys.exit(1)
|
||||
output_directory = sys.argv[1]
|
||||
duration = float(sys.argv[2])
|
||||
|
||||
print "Avialable devices:", list_available_devices()
|
||||
runner = DaqRunner(dev_config, output_directory)
|
||||
runner.start()
|
||||
time.sleep(duration)
|
||||
runner.stop()
|
58
wlauto/external/daq_server/src/daqpower/log.py
vendored
58
wlauto/external/daq_server/src/daqpower/log.py
vendored
@ -1,58 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.python import log
|
||||
|
||||
__all__ = ['debug', 'info', 'warning', 'error', 'critical', 'start_logging']
|
||||
|
||||
debug = lambda x: log.msg(x, logLevel=logging.DEBUG)
|
||||
info = lambda x: log.msg(x, logLevel=logging.INFO)
|
||||
warning = lambda x: log.msg(x, logLevel=logging.WARNING)
|
||||
error = lambda x: log.msg(x, logLevel=logging.ERROR)
|
||||
critical = lambda x: log.msg(x, logLevel=logging.CRITICAL)
|
||||
|
||||
|
||||
class CustomLoggingObserver(log.PythonLoggingObserver):
|
||||
|
||||
def __init__(self, loggerName="twisted"):
|
||||
super(CustomLoggingObserver, self).__init__(loggerName)
|
||||
if hasattr(self, '_newObserver'): # new vesions of Twisted
|
||||
self.logger = self._newObserver.logger # pylint: disable=no-member
|
||||
|
||||
def emit(self, eventDict):
|
||||
if 'logLevel' in eventDict:
|
||||
level = eventDict['logLevel']
|
||||
elif eventDict['isError']:
|
||||
level = logging.ERROR
|
||||
else:
|
||||
# All of that just just to override this one line from
|
||||
# default INFO level...
|
||||
level = logging.DEBUG
|
||||
text = log.textFromEventDict(eventDict)
|
||||
if text is None:
|
||||
return
|
||||
self.logger.log(level, text)
|
||||
|
||||
|
||||
logObserver = CustomLoggingObserver()
|
||||
logObserver.start()
|
||||
|
||||
|
||||
def start_logging(level, fmt='%(asctime)s %(levelname)-8s: %(message)s'):
|
||||
logging.basicConfig(level=getattr(logging, level), format=fmt)
|
||||
|
526
wlauto/external/daq_server/src/daqpower/server.py
vendored
526
wlauto/external/daq_server/src/daqpower/server.py
vendored
@ -1,526 +0,0 @@
|
||||
# Copyright 2014-2015 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=E1101,W0613,wrong-import-position
|
||||
from __future__ import division
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import shutil
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from zope.interface import implements
|
||||
from twisted.protocols.basic import LineReceiver
|
||||
from twisted.internet.protocol import Factory, Protocol
|
||||
from twisted.internet import reactor, interfaces
|
||||
from twisted.internet.error import ConnectionLost, ConnectionDone
|
||||
|
||||
if __name__ == "__main__": # for debugging
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
from daqpower import log
|
||||
from daqpower.config import DeviceConfiguration
|
||||
from daqpower.common import DaqServerRequest, DaqServerResponse, Status
|
||||
|
||||
try:
|
||||
from daqpower.daq import DaqRunner, list_available_devices, CAN_ENUMERATE_DEVICES
|
||||
__import_error = None
|
||||
except ImportError as e:
|
||||
# May be using debug mode.
|
||||
__import_error = e
|
||||
DaqRunner = None
|
||||
list_available_devices = lambda: ['Dev1']
|
||||
|
||||
|
||||
class ProtocolError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DummyDaqRunner(object):
|
||||
"""Dummy stub used when running in debug mode."""
|
||||
|
||||
num_rows = 200
|
||||
|
||||
@property
|
||||
def number_of_ports(self):
|
||||
return self.config.number_of_ports
|
||||
|
||||
def __init__(self, config, output_directory):
|
||||
log.info('Creating runner with {} {}'.format(config, output_directory))
|
||||
self.config = config
|
||||
self.output_directory = output_directory
|
||||
self.is_running = False
|
||||
|
||||
def start(self):
|
||||
import csv, random # pylint: disable=multiple-imports
|
||||
log.info('runner started')
|
||||
for i in xrange(self.config.number_of_ports):
|
||||
rows = [['power', 'voltage']] + [[random.gauss(1.0, 1.0), random.gauss(1.0, 0.1)]
|
||||
for _ in xrange(self.num_rows)]
|
||||
with open(self.get_port_file_path(self.config.labels[i]), 'wb') as wfh:
|
||||
writer = csv.writer(wfh)
|
||||
writer.writerows(rows)
|
||||
|
||||
self.is_running = True
|
||||
|
||||
def stop(self):
|
||||
self.is_running = False
|
||||
log.info('runner stopped')
|
||||
|
||||
def get_port_file_path(self, port_id):
|
||||
if port_id in self.config.labels:
|
||||
return os.path.join(self.output_directory, '{}.csv'.format(port_id))
|
||||
else:
|
||||
raise Exception('Invalid port id: {}'.format(port_id))
|
||||
|
||||
|
||||
class DaqServer(object):
|
||||
|
||||
def __init__(self, base_output_directory):
|
||||
self.base_output_directory = os.path.abspath(base_output_directory)
|
||||
if os.path.isdir(self.base_output_directory):
|
||||
log.info('Using output directory: {}'.format(self.base_output_directory))
|
||||
else:
|
||||
log.info('Creating new output directory: {}'.format(self.base_output_directory))
|
||||
os.makedirs(self.base_output_directory)
|
||||
self.runner = None
|
||||
self.output_directory = None
|
||||
self.labels = None
|
||||
|
||||
def configure(self, config_string):
|
||||
message = None
|
||||
if self.runner:
|
||||
message = 'Configuring a new session before previous session has been terminated.'
|
||||
log.warning(message)
|
||||
if self.runner.is_running:
|
||||
self.runner.stop()
|
||||
config = DeviceConfiguration.deserialize(config_string)
|
||||
config.validate()
|
||||
self.output_directory = self._create_output_directory()
|
||||
self.labels = config.labels
|
||||
log.info('Writing port files to {}'.format(self.output_directory))
|
||||
self.runner = DaqRunner(config, self.output_directory)
|
||||
return message
|
||||
|
||||
def start(self):
|
||||
if self.runner:
|
||||
if not self.runner.is_running:
|
||||
self.runner.start()
|
||||
else:
|
||||
message = 'Calling start() before stop() has been called. Data up to this point will be lost.'
|
||||
log.warning(message)
|
||||
self.runner.stop()
|
||||
self.runner.start()
|
||||
return message
|
||||
else:
|
||||
raise ProtocolError('Start called before a session has been configured.')
|
||||
|
||||
def stop(self):
|
||||
if self.runner:
|
||||
if self.runner.is_running:
|
||||
self.runner.stop()
|
||||
else:
|
||||
message = 'Attempting to stop() before start() was invoked.'
|
||||
log.warning(message)
|
||||
self.runner.stop()
|
||||
return message
|
||||
else:
|
||||
raise ProtocolError('Stop called before a session has been configured.')
|
||||
|
||||
def list_devices(self): # pylint: disable=no-self-use
|
||||
return list_available_devices()
|
||||
|
||||
def list_ports(self):
|
||||
return self.labels
|
||||
|
||||
def list_port_files(self):
|
||||
if not self.runner:
|
||||
raise ProtocolError('Attempting to list port files before session has been configured.')
|
||||
ports_with_files = []
|
||||
for port_id in self.labels:
|
||||
path = self.get_port_file_path(port_id)
|
||||
if os.path.isfile(path):
|
||||
ports_with_files.append(port_id)
|
||||
return ports_with_files
|
||||
|
||||
def get_port_file_path(self, port_id):
|
||||
if not self.runner:
|
||||
raise ProtocolError('Attepting to get port file path before session has been configured.')
|
||||
return self.runner.get_port_file_path(port_id)
|
||||
|
||||
def terminate(self):
|
||||
message = None
|
||||
if self.runner:
|
||||
if self.runner.is_running:
|
||||
message = 'Terminating session before runner has been stopped.'
|
||||
log.warning(message)
|
||||
self.runner.stop()
|
||||
self.runner = None
|
||||
if self.output_directory and os.path.isdir(self.output_directory):
|
||||
shutil.rmtree(self.output_directory)
|
||||
self.output_directory = None
|
||||
log.info('Session terminated.')
|
||||
else: # Runner has not been created.
|
||||
message = 'Attempting to close session before it has been configured.'
|
||||
log.warning(message)
|
||||
return message
|
||||
|
||||
def _create_output_directory(self):
|
||||
basename = datetime.now().strftime('%Y-%m-%d_%H%M%S%f')
|
||||
dirname = os.path.join(self.base_output_directory, basename)
|
||||
os.makedirs(dirname)
|
||||
return dirname
|
||||
|
||||
def __del__(self):
|
||||
if self.runner:
|
||||
self.runner.stop()
|
||||
|
||||
def __str__(self):
|
||||
return '({})'.format(self.base_output_directory)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class DaqControlProtocol(LineReceiver): # pylint: disable=W0223
|
||||
|
||||
def __init__(self, daq_server):
|
||||
self.daq_server = daq_server
|
||||
self.factory = None
|
||||
|
||||
def lineReceived(self, line):
|
||||
line = line.strip()
|
||||
log.info('Received: {}'.format(line))
|
||||
try:
|
||||
request = DaqServerRequest.deserialize(line)
|
||||
except Exception, e: # pylint: disable=W0703
|
||||
# PyDAQmx exceptions use "mess" rather than the standard "message"
|
||||
# to pass errors...
|
||||
message = getattr(e, 'mess', e.message)
|
||||
self.sendError('Received bad request ({}: {})'.format(e.__class__.__name__, message))
|
||||
else:
|
||||
self.processRequest(request)
|
||||
|
||||
def processRequest(self, request):
|
||||
try:
|
||||
if request.command == 'configure':
|
||||
self.configure(request)
|
||||
elif request.command == 'start':
|
||||
self.start(request)
|
||||
elif request.command == 'stop':
|
||||
self.stop(request)
|
||||
elif request.command == 'list_devices':
|
||||
self.list_devices(request)
|
||||
elif request.command == 'list_ports':
|
||||
self.list_ports(request)
|
||||
elif request.command == 'list_port_files':
|
||||
self.list_port_files(request)
|
||||
elif request.command == 'pull':
|
||||
self.pull_port_data(request)
|
||||
elif request.command == 'close':
|
||||
self.terminate(request)
|
||||
else:
|
||||
self.sendError('Received unknown command: {}'.format(request.command))
|
||||
except Exception, e: # pylint: disable=W0703
|
||||
message = getattr(e, 'mess', e.message)
|
||||
self.sendError('{}: {}'.format(e.__class__.__name__, message))
|
||||
|
||||
def configure(self, request):
|
||||
if 'config' in request.params:
|
||||
result = self.daq_server.configure(request.params['config'])
|
||||
if not result:
|
||||
self.sendResponse(Status.OK)
|
||||
else:
|
||||
self.sendResponse(Status.OKISH, message=result)
|
||||
else:
|
||||
self.sendError('Invalid config; config string not provided.')
|
||||
|
||||
def start(self, request):
|
||||
result = self.daq_server.start()
|
||||
if not result:
|
||||
self.sendResponse(Status.OK)
|
||||
else:
|
||||
self.sendResponse(Status.OKISH, message=result)
|
||||
|
||||
def stop(self, request):
|
||||
result = self.daq_server.stop()
|
||||
if not result:
|
||||
self.sendResponse(Status.OK)
|
||||
else:
|
||||
self.sendResponse(Status.OKISH, message=result)
|
||||
|
||||
def pull_port_data(self, request):
|
||||
if 'port_id' in request.params:
|
||||
port_id = request.params['port_id']
|
||||
port_file = self.daq_server.get_port_file_path(port_id)
|
||||
if os.path.isfile(port_file):
|
||||
port = self._initiate_file_transfer(port_file)
|
||||
self.sendResponse(Status.OK, data={'port_number': port})
|
||||
else:
|
||||
self.sendError('File for port {} does not exist.'.format(port_id))
|
||||
else:
|
||||
self.sendError('Invalid pull request; port id not provided.')
|
||||
|
||||
def list_devices(self, request):
|
||||
if CAN_ENUMERATE_DEVICES:
|
||||
devices = self.daq_server.list_devices()
|
||||
self.sendResponse(Status.OK, data={'devices': devices})
|
||||
else:
|
||||
message = "Server does not support DAQ device enumration"
|
||||
self.sendResponse(Status.OKISH, message=message)
|
||||
|
||||
def list_ports(self, request):
|
||||
port_labels = self.daq_server.list_ports()
|
||||
self.sendResponse(Status.OK, data={'ports': port_labels})
|
||||
|
||||
def list_port_files(self, request):
|
||||
port_labels = self.daq_server.list_port_files()
|
||||
self.sendResponse(Status.OK, data={'ports': port_labels})
|
||||
|
||||
def terminate(self, request):
|
||||
status = Status.OK
|
||||
message = ''
|
||||
if self.factory.transfer_sessions:
|
||||
message = 'Terminating with file tranfer sessions in progress. '
|
||||
log.warning(message)
|
||||
for session in self.factory.transfer_sessions:
|
||||
self.factory.transferComplete(session)
|
||||
message += self.daq_server.terminate() or ''
|
||||
if message:
|
||||
status = Status.OKISH
|
||||
self.sendResponse(status, message)
|
||||
|
||||
def sendError(self, message):
|
||||
log.error(message)
|
||||
self.sendResponse(Status.ERROR, message)
|
||||
|
||||
def sendResponse(self, status, message=None, data=None):
|
||||
response = DaqServerResponse(status, message=message, data=data)
|
||||
self.sendLine(response.serialize())
|
||||
|
||||
def sendLine(self, line):
|
||||
log.info('Responding: {}'.format(line))
|
||||
LineReceiver.sendLine(self, line.replace('\r\n', ''))
|
||||
|
||||
def _initiate_file_transfer(self, filepath):
|
||||
sender_factory = FileSenderFactory(filepath, self.factory)
|
||||
connector = reactor.listenTCP(0, sender_factory)
|
||||
self.factory.transferInitiated(sender_factory, connector)
|
||||
return connector.getHost().port
|
||||
|
||||
|
||||
class DaqFactory(Factory):
|
||||
|
||||
protocol = DaqControlProtocol
|
||||
check_alive_period = 5 * 60
|
||||
max_transfer_lifetime = 30 * 60
|
||||
|
||||
def __init__(self, server, cleanup_period=24 * 60 * 60, cleanup_after_days=5):
|
||||
self.server = server
|
||||
self.cleanup_period = cleanup_period
|
||||
self.cleanup_threshold = timedelta(cleanup_after_days)
|
||||
self.transfer_sessions = {}
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
proto = DaqControlProtocol(self.server)
|
||||
proto.factory = self
|
||||
reactor.callLater(self.check_alive_period, self.pulse)
|
||||
reactor.callLater(self.cleanup_period, self.perform_cleanup)
|
||||
return proto
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
log.msg('client connection lost: {}.'.format(reason))
|
||||
if not isinstance(reason, ConnectionLost):
|
||||
log.msg('ERROR: Client terminated connection mid-transfer.')
|
||||
for session in self.transfer_sessions:
|
||||
self.transferComplete(session)
|
||||
|
||||
def transferInitiated(self, session, connector):
|
||||
self.transfer_sessions[session] = (time.time(), connector)
|
||||
|
||||
def transferComplete(self, session, reason='OK'):
|
||||
if reason != 'OK':
|
||||
log.error(reason)
|
||||
self.transfer_sessions[session][1].stopListening()
|
||||
del self.transfer_sessions[session]
|
||||
|
||||
def pulse(self):
|
||||
"""Close down any file tranfer sessions that have been open for too long."""
|
||||
current_time = time.time()
|
||||
for session in self.transfer_sessions:
|
||||
start_time, conn = self.transfer_sessions[session]
|
||||
if (current_time - start_time) > self.max_transfer_lifetime:
|
||||
message = '{} session on port {} timed out'
|
||||
self.transferComplete(session, message.format(session, conn.getHost().port))
|
||||
if self.transfer_sessions:
|
||||
reactor.callLater(self.check_alive_period, self.pulse)
|
||||
|
||||
def perform_cleanup(self):
|
||||
"""
|
||||
Cleanup and old uncollected data files to recover disk space.
|
||||
|
||||
"""
|
||||
log.msg('Performing cleanup of the output directory...')
|
||||
base_directory = self.server.base_output_directory
|
||||
current_time = datetime.now()
|
||||
for entry in os.listdir(base_directory):
|
||||
entry_path = os.path.join(base_directory, entry)
|
||||
entry_ctime = datetime.fromtimestamp(os.path.getctime(entry_path))
|
||||
existence_time = current_time - entry_ctime
|
||||
if existence_time > self.cleanup_threshold:
|
||||
log.debug('Removing {} (existed for {})'.format(entry, existence_time))
|
||||
shutil.rmtree(entry_path)
|
||||
else:
|
||||
log.debug('Keeping {} (existed for {})'.format(entry, existence_time))
|
||||
log.msg('Cleanup complete.')
|
||||
|
||||
def __str__(self):
|
||||
return '<DAQ {}>'.format(self.server)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class FileReader(object):
|
||||
|
||||
implements(interfaces.IPushProducer)
|
||||
|
||||
def __init__(self, filepath):
|
||||
self.fh = open(filepath)
|
||||
self.proto = None
|
||||
self.done = False
|
||||
self._paused = True
|
||||
|
||||
def setProtocol(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def resumeProducing(self):
|
||||
if not self.proto:
|
||||
raise ProtocolError('resumeProducing called with no protocol set.')
|
||||
self._paused = False
|
||||
try:
|
||||
while not self._paused:
|
||||
line = self.fh.next().rstrip('\n') + '\r\n'
|
||||
self.proto.transport.write(line)
|
||||
except StopIteration:
|
||||
log.debug('Sent everything.')
|
||||
self.stopProducing()
|
||||
|
||||
def pauseProducing(self):
|
||||
self._paused = True
|
||||
|
||||
def stopProducing(self):
|
||||
self.done = True
|
||||
self.fh.close()
|
||||
self.proto.transport.unregisterProducer()
|
||||
self.proto.transport.loseConnection()
|
||||
|
||||
|
||||
class FileSenderProtocol(Protocol):
|
||||
|
||||
def __init__(self, reader):
|
||||
self.reader = reader
|
||||
self.factory = None
|
||||
|
||||
def connectionMade(self):
|
||||
self.transport.registerProducer(self.reader, True)
|
||||
self.reader.resumeProducing()
|
||||
|
||||
def connectionLost(self, reason=ConnectionDone):
|
||||
if self.reader.done:
|
||||
self.factory.transferComplete()
|
||||
else:
|
||||
self.reader.pauseProducing()
|
||||
self.transport.unregisterProducer()
|
||||
|
||||
|
||||
class FileSenderFactory(Factory):
|
||||
|
||||
@property
|
||||
def done(self):
|
||||
if self.reader:
|
||||
return self.reader.done
|
||||
else:
|
||||
return None
|
||||
|
||||
def __init__(self, path, owner):
|
||||
self.path = os.path.abspath(path)
|
||||
self.reader = None
|
||||
self.owner = owner
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
if not self.reader:
|
||||
self.reader = FileReader(self.path)
|
||||
proto = FileSenderProtocol(self.reader)
|
||||
proto.factory = self
|
||||
self.reader.setProtocol(proto)
|
||||
return proto
|
||||
|
||||
def transferComplete(self):
|
||||
self.owner.transferComplete(self)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.path)
|
||||
|
||||
def __str__(self):
|
||||
return '<FileSender {}>'.format(self.path)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
def run_server():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-d', '--directory', help='Working directory', metavar='DIR', default='.')
|
||||
parser.add_argument('-p', '--port', help='port the server will listen on.',
|
||||
metavar='PORT', default=45677, type=int)
|
||||
parser.add_argument('-c', '--cleanup-after', type=int, default=5, metavar='DAYS',
|
||||
help="""
|
||||
Sever will perodically clean up data files that are older than the number of
|
||||
days specfied by this parameter.
|
||||
""")
|
||||
parser.add_argument('--cleanup-period', type=int, default=1, metavar='DAYS',
|
||||
help='Specifies how ofte the server will attempt to clean up old files.')
|
||||
parser.add_argument('--debug', help='Run in debug mode (no DAQ connected).',
|
||||
action='store_true', default=False)
|
||||
parser.add_argument('--verbose', help='Produce verobose output.', action='store_true', default=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.debug:
|
||||
global DaqRunner # pylint: disable=W0603
|
||||
DaqRunner = DummyDaqRunner
|
||||
else:
|
||||
if not DaqRunner:
|
||||
raise __import_error # pylint: disable=raising-bad-type
|
||||
if args.verbose or args.debug:
|
||||
log.start_logging('DEBUG')
|
||||
else:
|
||||
log.start_logging('INFO')
|
||||
|
||||
# days to seconds
|
||||
cleanup_period = args.cleanup_period * 24 * 60 * 60
|
||||
|
||||
server = DaqServer(args.directory)
|
||||
factory = DaqFactory(server, cleanup_period, args.cleanup_after)
|
||||
reactor.listenTCP(args.port, factory).getHost()
|
||||
try:
|
||||
hostname = socket.gethostbyname(socket.gethostname())
|
||||
except socket.gaierror:
|
||||
hostname = 'localhost'
|
||||
log.info('Listening on {}:{}'.format(hostname, args.port))
|
||||
reactor.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server()
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from daqpower.server import run_server
|
||||
run_server()
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from daqpower.client import run_send_command
|
||||
run_send_command()
|
52
wlauto/external/daq_server/src/setup.py
vendored
52
wlauto/external/daq_server/src/setup.py
vendored
@ -1,52 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import warnings
|
||||
from distutils.core import setup
|
||||
|
||||
import daqpower
|
||||
|
||||
|
||||
warnings.filterwarnings('ignore', "Unknown distribution option: 'install_requires'")
|
||||
|
||||
params = dict(
|
||||
name='daqpower',
|
||||
version=daqpower.__version__,
|
||||
packages=[
|
||||
'daqpower',
|
||||
],
|
||||
scripts=[
|
||||
'scripts/run-daq-server',
|
||||
'scripts/send-daq-command',
|
||||
],
|
||||
url='N/A',
|
||||
maintainer='workload-automation',
|
||||
maintainer_email='workload-automation@arm.com',
|
||||
install_requires=[
|
||||
'twisted',
|
||||
'PyDAQmx',
|
||||
],
|
||||
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'License :: Other/Proprietary License',
|
||||
'Operating System :: Unix',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
],
|
||||
)
|
||||
|
||||
setup(**params)
|
7
wlauto/external/pmu_logger/Makefile
vendored
7
wlauto/external/pmu_logger/Makefile
vendored
@ -1,7 +0,0 @@
|
||||
# To build the pmu_logger module use the following command line
|
||||
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -C ../kernel/out SUBDIRS=$PWD modules
|
||||
# where
|
||||
# CROSS_COMPILE - prefix of the arm linux compiler
|
||||
# -C - location of the configured kernel source tree
|
||||
|
||||
obj-m := pmu_logger.o
|
35
wlauto/external/pmu_logger/README
vendored
35
wlauto/external/pmu_logger/README
vendored
@ -1,35 +0,0 @@
|
||||
The pmu_logger module provides the ability to periodically trace CCI PMU counters. The trace destinations can be ftrace buffer and/or kernel logs. This file gives a quick overview of the funcationality provided by the module and how to use it.
|
||||
|
||||
The pmu_logger module creates a directory in the debugfs filesystem called cci_pmu_logger which can be used to enable/disable the counters and control the events that are counted.
|
||||
|
||||
To configure the events being counted write the corresponding event id to the counter* files. The list of CCI PMU events can be found at http://arminfo.emea.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0470d/CJHICFBF.html.
|
||||
|
||||
The "period_jiffies" can be used to control the periodicity of tracing. It accepts values in kernel jiffies.
|
||||
|
||||
To enable tracing, write a 1 to "control". To disable write another 1 to "control". The files "enable_console" and "enable_ftrace" control where the trace is written to. To check if the counters are currently running or not, you can read the control file.
|
||||
|
||||
The current values of the counters can be read from the "values" file.
|
||||
|
||||
Eg. To trace, A15 and A7 snoop hit rate every 10 jiffies the following command are required -
|
||||
|
||||
|
||||
trace-cmd reset
|
||||
|
||||
echo 0x63 > counter0
|
||||
echo 0x6A > counter1
|
||||
echo 0x83 > counter2
|
||||
echo 0x8A > counter3
|
||||
|
||||
echo 10 > period_jiffies
|
||||
|
||||
trace-cmd start -b 20000 -e "sched:sched_wakeup"
|
||||
|
||||
echo 1 > control
|
||||
|
||||
# perform the activity for which you would like to collect the CCI PMU trace.
|
||||
|
||||
trace-cmd stop && trace-cmd extract
|
||||
|
||||
echo 1 > control
|
||||
|
||||
trace-cmd report trace.dat | grep print # shows the trace of the CCI PMU counters along with the cycle counter values.
|
294
wlauto/external/pmu_logger/pmu_logger.c
vendored
294
wlauto/external/pmu_logger/pmu_logger.c
vendored
@ -1,294 +0,0 @@
|
||||
/* Copyright 2013-2015 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.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* pmu_logger.c - Kernel module to log the CCI PMU counters
|
||||
*/
|
||||
|
||||
#include <linux/init.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/debugfs.h>
|
||||
#include <linux/timer.h>
|
||||
#include <asm/io.h>
|
||||
|
||||
#define MODULE_NAME "cci_pmu_logger"
|
||||
|
||||
// CCI_BASE needs to be modified to point to the mapped location of CCI in
|
||||
// memory on your device.
|
||||
#define CCI_BASE 0x2C090000 // TC2
|
||||
//#define CCI_BASE 0x10D20000
|
||||
#define CCI_SIZE 0x00010000
|
||||
|
||||
#define PMCR 0x100
|
||||
|
||||
#define PMCR_CEN (1 << 0)
|
||||
#define PMCR_RST (1 << 1)
|
||||
#define PMCR_CCR (1 << 2)
|
||||
#define PMCR_CCD (1 << 3)
|
||||
#define PMCR_EX (1 << 4)
|
||||
#define PMCR_DP (1 << 5)
|
||||
|
||||
#define CC_BASE 0x9000
|
||||
#define PC0_BASE 0xA000
|
||||
#define PC1_BASE 0xB000
|
||||
#define PC2_BASE 0xC000
|
||||
#define PC3_BASE 0xD000
|
||||
|
||||
#define PC_ESR 0x0
|
||||
#define CNT_VALUE 0x4
|
||||
#define CNT_CONTROL 0x8
|
||||
|
||||
#define CNT_ENABLE (1 << 0)
|
||||
|
||||
u32 counter0_event = 0x6A;
|
||||
u32 counter1_event = 0x63;
|
||||
u32 counter2_event = 0x8A;
|
||||
u32 counter3_event = 0x83;
|
||||
|
||||
u32 enable_console = 0;
|
||||
u32 enable_ftrace = 1;
|
||||
|
||||
void *cci_base = 0;
|
||||
|
||||
static struct dentry *module_debugfs_root;
|
||||
static int enabled = false;
|
||||
|
||||
u32 delay = 10; //jiffies. This translates to 1 sample every 100 ms
|
||||
struct timer_list timer;
|
||||
|
||||
static void call_after_delay(void)
|
||||
{
|
||||
timer.expires = jiffies + delay;
|
||||
add_timer(&timer);
|
||||
}
|
||||
|
||||
|
||||
static void setup_and_call_after_delay(void (*fn)(unsigned long))
|
||||
{
|
||||
init_timer(&timer);
|
||||
timer.data = (unsigned long)&timer;
|
||||
timer.function = fn;
|
||||
|
||||
call_after_delay();
|
||||
}
|
||||
|
||||
static void print_counter_configuration(void)
|
||||
{
|
||||
if (enable_ftrace)
|
||||
trace_printk("Counter_0: %02x Counter_1: %02x Counter_2: %02x Counter_3: %02x\n", \
|
||||
counter0_event, counter1_event, counter2_event, counter3_event);
|
||||
|
||||
if (enable_console)
|
||||
printk("Counter_0: %02x Counter_1: %02x Counter_2: %02x Counter_3: %02x\n", \
|
||||
counter0_event, counter1_event, counter2_event, counter3_event);
|
||||
}
|
||||
|
||||
static void initialize_cci_pmu(void)
|
||||
{
|
||||
u32 val;
|
||||
|
||||
// Select the events counted
|
||||
iowrite32(counter0_event, cci_base + PC0_BASE + PC_ESR);
|
||||
iowrite32(counter1_event, cci_base + PC1_BASE + PC_ESR);
|
||||
iowrite32(counter2_event, cci_base + PC2_BASE + PC_ESR);
|
||||
iowrite32(counter3_event, cci_base + PC3_BASE + PC_ESR);
|
||||
|
||||
// Enable the individual PMU counters
|
||||
iowrite32(CNT_ENABLE, cci_base + PC0_BASE + CNT_CONTROL);
|
||||
iowrite32(CNT_ENABLE, cci_base + PC1_BASE + CNT_CONTROL);
|
||||
iowrite32(CNT_ENABLE, cci_base + PC2_BASE + CNT_CONTROL);
|
||||
iowrite32(CNT_ENABLE, cci_base + PC3_BASE + CNT_CONTROL);
|
||||
iowrite32(CNT_ENABLE, cci_base + CC_BASE + CNT_CONTROL);
|
||||
|
||||
// Reset the counters and configure the Cycle Count Divider
|
||||
val = ioread32(cci_base + PMCR);
|
||||
iowrite32(val | PMCR_RST | PMCR_CCR | PMCR_CCD, cci_base + PMCR);
|
||||
}
|
||||
|
||||
static void enable_cci_pmu_counters(void)
|
||||
{
|
||||
u32 val = ioread32(cci_base + PMCR);
|
||||
iowrite32(val | PMCR_CEN, cci_base + PMCR);
|
||||
}
|
||||
|
||||
static void disable_cci_pmu_counters(void)
|
||||
{
|
||||
u32 val = ioread32(cci_base + PMCR);
|
||||
iowrite32(val & ~PMCR_CEN, cci_base + PMCR);
|
||||
}
|
||||
|
||||
static void trace_values(unsigned long arg)
|
||||
{
|
||||
u32 cycles;
|
||||
u32 counter[4];
|
||||
|
||||
cycles = ioread32(cci_base + CC_BASE + CNT_VALUE);
|
||||
counter[0] = ioread32(cci_base + PC0_BASE + CNT_VALUE);
|
||||
counter[1] = ioread32(cci_base + PC1_BASE + CNT_VALUE);
|
||||
counter[2] = ioread32(cci_base + PC2_BASE + CNT_VALUE);
|
||||
counter[3] = ioread32(cci_base + PC3_BASE + CNT_VALUE);
|
||||
|
||||
if (enable_ftrace)
|
||||
trace_printk("Cycles: %08x Counter_0: %08x"
|
||||
" Counter_1: %08x Counter_2: %08x Counter_3: %08x\n", \
|
||||
cycles, counter[0], counter[1], counter[2], counter[3]);
|
||||
|
||||
if (enable_console)
|
||||
printk("Cycles: %08x Counter_0: %08x"
|
||||
" Counter_1: %08x Counter_2: %08x Counter_3: %08x\n", \
|
||||
cycles, counter[0], counter[1], counter[2], counter[3]);
|
||||
|
||||
if (enabled) {
|
||||
u32 val;
|
||||
// Reset the counters
|
||||
val = ioread32(cci_base + PMCR);
|
||||
iowrite32(val | PMCR_RST | PMCR_CCR, cci_base + PMCR);
|
||||
|
||||
call_after_delay();
|
||||
}
|
||||
}
|
||||
|
||||
static ssize_t read_control(struct file *file, char __user *buf, size_t count, loff_t *ppos)
|
||||
{
|
||||
char status[16];
|
||||
/* printk(KERN_DEBUG "%s\n", __func__); */
|
||||
|
||||
if (enabled)
|
||||
snprintf(status, 16, "enabled\n");
|
||||
else
|
||||
snprintf(status, 16, "disabled\n");
|
||||
|
||||
return simple_read_from_buffer(buf, count, ppos, status, strlen(status));
|
||||
}
|
||||
|
||||
static ssize_t write_control(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
|
||||
{
|
||||
if (enabled) {
|
||||
disable_cci_pmu_counters();
|
||||
enabled = false;
|
||||
} else {
|
||||
initialize_cci_pmu();
|
||||
enable_cci_pmu_counters();
|
||||
enabled = true;
|
||||
|
||||
print_counter_configuration();
|
||||
setup_and_call_after_delay(trace_values);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
static ssize_t read_values(struct file *file, char __user *buf, size_t count, loff_t *ppos)
|
||||
{
|
||||
char values[256];
|
||||
/* u32 val; */
|
||||
|
||||
snprintf(values, 256, "Cycles: %08x Counter_0: %08x"
|
||||
" Counter_1: %08x Counter_2: %08x Counter_3: %08x\n", \
|
||||
ioread32(cci_base + CC_BASE + CNT_VALUE), \
|
||||
ioread32(cci_base + PC0_BASE + CNT_VALUE), \
|
||||
ioread32(cci_base + PC1_BASE + CNT_VALUE), \
|
||||
ioread32(cci_base + PC2_BASE + CNT_VALUE), \
|
||||
ioread32(cci_base + PC3_BASE + CNT_VALUE));
|
||||
|
||||
return simple_read_from_buffer(buf, count, ppos, values, strlen(values));
|
||||
}
|
||||
|
||||
static const struct file_operations control_fops = {
|
||||
.owner = THIS_MODULE,
|
||||
.read = read_control,
|
||||
.write = write_control,
|
||||
};
|
||||
|
||||
static const struct file_operations value_fops = {
|
||||
.owner = THIS_MODULE,
|
||||
.read = read_values,
|
||||
};
|
||||
|
||||
static int __init pmu_logger_init(void)
|
||||
{
|
||||
struct dentry *retval;
|
||||
|
||||
module_debugfs_root = debugfs_create_dir(MODULE_NAME, NULL);
|
||||
if (!module_debugfs_root || IS_ERR(module_debugfs_root)) {
|
||||
printk(KERN_ERR "error creating debugfs dir.\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
retval = debugfs_create_file("control", S_IRUGO | S_IWUGO, module_debugfs_root, NULL, &control_fops);
|
||||
if (!retval)
|
||||
goto out;
|
||||
|
||||
retval = debugfs_create_file("values", S_IRUGO, module_debugfs_root, NULL, &value_fops);
|
||||
if (!retval)
|
||||
goto out;
|
||||
|
||||
retval = debugfs_create_bool("enable_console", S_IRUGO | S_IWUGO, module_debugfs_root, &enable_console);
|
||||
if (!retval)
|
||||
goto out;
|
||||
|
||||
retval = debugfs_create_bool("enable_ftrace", S_IRUGO | S_IWUGO, module_debugfs_root, &enable_ftrace);
|
||||
if (!retval)
|
||||
goto out;
|
||||
|
||||
retval = debugfs_create_u32("period_jiffies", S_IRUGO | S_IWUGO, module_debugfs_root, &delay);
|
||||
if (!retval)
|
||||
goto out;
|
||||
|
||||
retval = debugfs_create_x32("counter0", S_IRUGO | S_IWUGO, module_debugfs_root, &counter0_event);
|
||||
if (!retval)
|
||||
goto out;
|
||||
retval = debugfs_create_x32("counter1", S_IRUGO | S_IWUGO, module_debugfs_root, &counter1_event);
|
||||
if (!retval)
|
||||
goto out;
|
||||
retval = debugfs_create_x32("counter2", S_IRUGO | S_IWUGO, module_debugfs_root, &counter2_event);
|
||||
if (!retval)
|
||||
goto out;
|
||||
retval = debugfs_create_x32("counter3", S_IRUGO | S_IWUGO, module_debugfs_root, &counter3_event);
|
||||
if (!retval)
|
||||
goto out;
|
||||
|
||||
cci_base = ioremap(CCI_BASE, CCI_SIZE);
|
||||
if (!cci_base)
|
||||
goto out;
|
||||
|
||||
printk(KERN_INFO "CCI PMU Logger loaded.\n");
|
||||
return 0;
|
||||
|
||||
out:
|
||||
debugfs_remove_recursive(module_debugfs_root);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void __exit pmu_logger_exit(void)
|
||||
{
|
||||
if (module_debugfs_root) {
|
||||
debugfs_remove_recursive(module_debugfs_root);
|
||||
module_debugfs_root = NULL;
|
||||
}
|
||||
if (cci_base)
|
||||
iounmap(cci_base);
|
||||
|
||||
printk(KERN_INFO "CCI PMU Logger removed.\n");
|
||||
}
|
||||
|
||||
module_init(pmu_logger_init);
|
||||
module_exit(pmu_logger_exit);
|
||||
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_AUTHOR("Punit Agrawal");
|
||||
MODULE_DESCRIPTION("logger for CCI PMU counters");
|
BIN
wlauto/external/pmu_logger/pmu_logger.ko
vendored
BIN
wlauto/external/pmu_logger/pmu_logger.ko
vendored
Binary file not shown.
11
wlauto/external/readenergy/Makefile
vendored
11
wlauto/external/readenergy/Makefile
vendored
@ -1,11 +0,0 @@
|
||||
# To build:
|
||||
#
|
||||
# CROSS_COMPILE=aarch64-linux-gnu- make
|
||||
#
|
||||
CROSS_COMPILE?=aarch64-linux-gnu-
|
||||
CC=$(CROSS_COMPILE)gcc
|
||||
CFLAGS='-Wl,-static -Wl,-lc'
|
||||
|
||||
readenergy: readenergy.c
|
||||
$(CC) $(CFLAGS) readenergy.c -o readenergy
|
||||
cp readenergy ../../instrumentation/juno_energy/readenergy
|
BIN
wlauto/external/readenergy/readenergy
vendored
BIN
wlauto/external/readenergy/readenergy
vendored
Binary file not shown.
345
wlauto/external/readenergy/readenergy.c
vendored
345
wlauto/external/readenergy/readenergy.c
vendored
@ -1,345 +0,0 @@
|
||||
/* Copyright 2014-2015 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.
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
* readenergy.c
|
||||
*
|
||||
* Reads APB energy registers in Juno and outputs the measurements (converted to appropriate units).
|
||||
*
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// The following values obtained from Juno TRM 2014/03/04 section 4.5
|
||||
|
||||
// Location of APB registers in memory
|
||||
#define APB_BASE_MEMORY 0x1C010000
|
||||
// APB energy counters start at offset 0xD0 from the base APB address.
|
||||
#define BASE_INDEX 0xD0 / 4
|
||||
// the one-past last APB counter
|
||||
#define APB_SIZE 0x120
|
||||
|
||||
// Masks specifying the bits that contain the actual counter values
|
||||
#define CMASK 0xFFF
|
||||
#define VMASK 0xFFF
|
||||
#define PMASK 0xFFFFFF
|
||||
|
||||
// Sclaing factor (divisor) or getting measured values from counters
|
||||
#define SYS_ADC_CH0_PM1_SYS_SCALE 761
|
||||
#define SYS_ADC_CH1_PM2_A57_SCALE 381
|
||||
#define SYS_ADC_CH2_PM3_A53_SCALE 761
|
||||
#define SYS_ADC_CH3_PM4_GPU_SCALE 381
|
||||
#define SYS_ADC_CH4_VSYS_SCALE 1622
|
||||
#define SYS_ADC_CH5_VA57_SCALE 1622
|
||||
#define SYS_ADC_CH6_VA53_SCALE 1622
|
||||
#define SYS_ADC_CH7_VGPU_SCALE 1622
|
||||
#define SYS_POW_CH04_SYS_SCALE (SYS_ADC_CH0_PM1_SYS_SCALE * SYS_ADC_CH4_VSYS_SCALE)
|
||||
#define SYS_POW_CH15_A57_SCALE (SYS_ADC_CH1_PM2_A57_SCALE * SYS_ADC_CH5_VA57_SCALE)
|
||||
#define SYS_POW_CH26_A53_SCALE (SYS_ADC_CH2_PM3_A53_SCALE * SYS_ADC_CH6_VA53_SCALE)
|
||||
#define SYS_POW_CH37_GPU_SCALE (SYS_ADC_CH3_PM4_GPU_SCALE * SYS_ADC_CH7_VGPU_SCALE)
|
||||
#define SYS_ENM_CH0_SYS_SCALE 12348030000
|
||||
#define SYS_ENM_CH1_A57_SCALE 6174020000
|
||||
#define SYS_ENM_CH0_A53_SCALE 12348030000
|
||||
#define SYS_ENM_CH0_GPU_SCALE 6174020000
|
||||
|
||||
// Original values prior to re-callibrations.
|
||||
/*#define SYS_ADC_CH0_PM1_SYS_SCALE 819.2*/
|
||||
/*#define SYS_ADC_CH1_PM2_A57_SCALE 409.6*/
|
||||
/*#define SYS_ADC_CH2_PM3_A53_SCALE 819.2*/
|
||||
/*#define SYS_ADC_CH3_PM4_GPU_SCALE 409.6*/
|
||||
/*#define SYS_ADC_CH4_VSYS_SCALE 1638.4*/
|
||||
/*#define SYS_ADC_CH5_VA57_SCALE 1638.4*/
|
||||
/*#define SYS_ADC_CH6_VA53_SCALE 1638.4*/
|
||||
/*#define SYS_ADC_CH7_VGPU_SCALE 1638.4*/
|
||||
/*#define SYS_POW_CH04_SYS_SCALE (SYS_ADC_CH0_PM1_SYS_SCALE * SYS_ADC_CH4_VSYS_SCALE)*/
|
||||
/*#define SYS_POW_CH15_A57_SCALE (SYS_ADC_CH1_PM2_A57_SCALE * SYS_ADC_CH5_VA57_SCALE)*/
|
||||
/*#define SYS_POW_CH26_A53_SCALE (SYS_ADC_CH2_PM3_A53_SCALE * SYS_ADC_CH6_VA53_SCALE)*/
|
||||
/*#define SYS_POW_CH37_GPU_SCALE (SYS_ADC_CH3_PM4_GPU_SCALE * SYS_ADC_CH7_VGPU_SCALE)*/
|
||||
/*#define SYS_ENM_CH0_SYS_SCALE 13421772800.0*/
|
||||
/*#define SYS_ENM_CH1_A57_SCALE 6710886400.0*/
|
||||
/*#define SYS_ENM_CH0_A53_SCALE 13421772800.0*/
|
||||
/*#define SYS_ENM_CH0_GPU_SCALE 6710886400.0*/
|
||||
|
||||
// Ignore individual errors but if see too many, abort.
|
||||
#define ERROR_THRESHOLD 10
|
||||
|
||||
// Default counter poll period (in milliseconds).
|
||||
#define DEFAULT_PERIOD 100
|
||||
|
||||
// A single reading from the energy meter. The values are the proper readings converted
|
||||
// to appropriate units (e.g. Watts for power); they are *not* raw counter values.
|
||||
struct reading
|
||||
{
|
||||
double sys_adc_ch0_pm1_sys;
|
||||
double sys_adc_ch1_pm2_a57;
|
||||
double sys_adc_ch2_pm3_a53;
|
||||
double sys_adc_ch3_pm4_gpu;
|
||||
double sys_adc_ch4_vsys;
|
||||
double sys_adc_ch5_va57;
|
||||
double sys_adc_ch6_va53;
|
||||
double sys_adc_ch7_vgpu;
|
||||
double sys_pow_ch04_sys;
|
||||
double sys_pow_ch15_a57;
|
||||
double sys_pow_ch26_a53;
|
||||
double sys_pow_ch37_gpu;
|
||||
double sys_enm_ch0_sys;
|
||||
double sys_enm_ch1_a57;
|
||||
double sys_enm_ch0_a53;
|
||||
double sys_enm_ch0_gpu;
|
||||
};
|
||||
|
||||
inline uint64_t join_64bit_register(uint32_t *buffer, int index)
|
||||
{
|
||||
uint64_t result = 0;
|
||||
result |= buffer[index];
|
||||
result |= (uint64_t)(buffer[index+1]) << 32;
|
||||
return result;
|
||||
}
|
||||
|
||||
int nsleep(const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
struct timespec temp_rem;
|
||||
if (nanosleep(req, rem) == -1)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
{
|
||||
nsleep(rem, &temp_rem);
|
||||
}
|
||||
else
|
||||
{
|
||||
return errno;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void print_help()
|
||||
{
|
||||
fprintf(stderr, "Usage: readenergy [-t PERIOD] -o OUTFILE\n\n"
|
||||
"Read Juno energy counters every PERIOD milliseconds, writing them\n"
|
||||
"to OUTFILE in CSV format until SIGTERM is received.\n\n"
|
||||
"Parameters:\n"
|
||||
" PERIOD is the counter poll period in milliseconds.\n"
|
||||
" (Defaults to 100 milliseconds.)\n"
|
||||
" OUTFILE is the output file path\n");
|
||||
}
|
||||
|
||||
// debugging only...
|
||||
inline void dprint(char *msg)
|
||||
{
|
||||
fprintf(stderr, "%s\n", msg);
|
||||
sync();
|
||||
}
|
||||
|
||||
// -------------------------------------- config ----------------------------------------------------
|
||||
|
||||
struct config
|
||||
{
|
||||
struct timespec period;
|
||||
char *output_file;
|
||||
};
|
||||
|
||||
void config_init_period_from_millis(struct config *this, long millis)
|
||||
{
|
||||
this->period.tv_sec = (time_t)(millis / 1000);
|
||||
this->period.tv_nsec = (millis % 1000) * 1000000;
|
||||
}
|
||||
|
||||
void config_init(struct config *this, int argc, char *argv[])
|
||||
{
|
||||
this->output_file = NULL;
|
||||
config_init_period_from_millis(this, DEFAULT_PERIOD);
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "ht:o:")) != -1)
|
||||
{
|
||||
switch(opt)
|
||||
{
|
||||
case 't':
|
||||
config_init_period_from_millis(this, atol(optarg));
|
||||
break;
|
||||
case 'o':
|
||||
this->output_file = optarg;
|
||||
break;
|
||||
case 'h':
|
||||
print_help();
|
||||
exit(EXIT_SUCCESS);
|
||||
break;
|
||||
default:
|
||||
fprintf(stderr, "ERROR: Unexpected option %s\n\n", opt);
|
||||
print_help();
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->output_file == NULL)
|
||||
{
|
||||
fprintf(stderr, "ERROR: Mandatory -o option not specified.\n\n");
|
||||
print_help();
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------- /config ---------------------------------------------------
|
||||
|
||||
// -------------------------------------- emeter ----------------------------------------------------
|
||||
|
||||
struct emeter
|
||||
{
|
||||
int fd;
|
||||
FILE *out;
|
||||
void *mmap_base;
|
||||
};
|
||||
|
||||
void emeter_init(struct emeter *this, char *outfile)
|
||||
{
|
||||
this->out = fopen(outfile, "w");
|
||||
if (this->out == NULL)
|
||||
{
|
||||
fprintf(stderr, "ERROR: Could not open output file %s; got %s\n", outfile, strerror(errno));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
this->fd = open("/dev/mem", O_RDONLY);
|
||||
if(this->fd < 0)
|
||||
{
|
||||
fprintf(stderr, "ERROR: Can't open /dev/mem; got %s\n", strerror(errno));
|
||||
fclose(this->out);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
this->mmap_base = mmap(NULL, APB_SIZE, PROT_READ, MAP_SHARED, this->fd, APB_BASE_MEMORY);
|
||||
if (this->mmap_base == MAP_FAILED)
|
||||
{
|
||||
fprintf(stderr, "ERROR: mmap failed; got %s\n", strerror(errno));
|
||||
close(this->fd);
|
||||
fclose(this->out);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
fprintf(this->out, "sys_curr,a57_curr,a53_curr,gpu_curr,"
|
||||
"sys_volt,a57_volt,a53_volt,gpu_volt,"
|
||||
"sys_pow,a57_pow,a53_pow,gpu_pow,"
|
||||
"sys_cenr,a57_cenr,a53_cenr,gpu_cenr\n");
|
||||
}
|
||||
|
||||
void emeter_read_measurements(struct emeter *this, struct reading *reading)
|
||||
{
|
||||
uint32_t *buffer = (uint32_t *)this->mmap_base;
|
||||
reading->sys_adc_ch0_pm1_sys = (double)(CMASK & buffer[BASE_INDEX+0]) / SYS_ADC_CH0_PM1_SYS_SCALE;
|
||||
reading->sys_adc_ch1_pm2_a57 = (double)(CMASK & buffer[BASE_INDEX+1]) / SYS_ADC_CH1_PM2_A57_SCALE;
|
||||
reading->sys_adc_ch2_pm3_a53 = (double)(CMASK & buffer[BASE_INDEX+2]) / SYS_ADC_CH2_PM3_A53_SCALE;
|
||||
reading->sys_adc_ch3_pm4_gpu = (double)(CMASK & buffer[BASE_INDEX+3]) / SYS_ADC_CH3_PM4_GPU_SCALE;
|
||||
reading->sys_adc_ch4_vsys = (double)(VMASK & buffer[BASE_INDEX+4]) / SYS_ADC_CH4_VSYS_SCALE;
|
||||
reading->sys_adc_ch5_va57 = (double)(VMASK & buffer[BASE_INDEX+5]) / SYS_ADC_CH5_VA57_SCALE;
|
||||
reading->sys_adc_ch6_va53 = (double)(VMASK & buffer[BASE_INDEX+6]) / SYS_ADC_CH6_VA53_SCALE;
|
||||
reading->sys_adc_ch7_vgpu = (double)(VMASK & buffer[BASE_INDEX+7]) / SYS_ADC_CH7_VGPU_SCALE;
|
||||
reading->sys_pow_ch04_sys = (double)(PMASK & buffer[BASE_INDEX+8]) / SYS_POW_CH04_SYS_SCALE;
|
||||
reading->sys_pow_ch15_a57 = (double)(PMASK & buffer[BASE_INDEX+9]) / SYS_POW_CH15_A57_SCALE;
|
||||
reading->sys_pow_ch26_a53 = (double)(PMASK & buffer[BASE_INDEX+10]) / SYS_POW_CH26_A53_SCALE;
|
||||
reading->sys_pow_ch37_gpu = (double)(PMASK & buffer[BASE_INDEX+11]) / SYS_POW_CH37_GPU_SCALE;
|
||||
reading->sys_enm_ch0_sys = (double)join_64bit_register(buffer, BASE_INDEX+12) / SYS_ENM_CH0_SYS_SCALE;
|
||||
reading->sys_enm_ch1_a57 = (double)join_64bit_register(buffer, BASE_INDEX+14) / SYS_ENM_CH1_A57_SCALE;
|
||||
reading->sys_enm_ch0_a53 = (double)join_64bit_register(buffer, BASE_INDEX+16) / SYS_ENM_CH0_A53_SCALE;
|
||||
reading->sys_enm_ch0_gpu = (double)join_64bit_register(buffer, BASE_INDEX+18) / SYS_ENM_CH0_GPU_SCALE;
|
||||
}
|
||||
|
||||
void emeter_take_reading(struct emeter *this)
|
||||
{
|
||||
static struct reading reading;
|
||||
int error_count = 0;
|
||||
emeter_read_measurements(this, &reading);
|
||||
int ret = fprintf(this->out, "%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f\n",
|
||||
reading.sys_adc_ch0_pm1_sys,
|
||||
reading.sys_adc_ch1_pm2_a57,
|
||||
reading.sys_adc_ch2_pm3_a53,
|
||||
reading.sys_adc_ch3_pm4_gpu,
|
||||
reading.sys_adc_ch4_vsys,
|
||||
reading.sys_adc_ch5_va57,
|
||||
reading.sys_adc_ch6_va53,
|
||||
reading.sys_adc_ch7_vgpu,
|
||||
reading.sys_pow_ch04_sys,
|
||||
reading.sys_pow_ch15_a57,
|
||||
reading.sys_pow_ch26_a53,
|
||||
reading.sys_pow_ch37_gpu,
|
||||
reading.sys_enm_ch0_sys,
|
||||
reading.sys_enm_ch1_a57,
|
||||
reading.sys_enm_ch0_a53,
|
||||
reading.sys_enm_ch0_gpu);
|
||||
if (ret < 0)
|
||||
{
|
||||
fprintf(stderr, "ERROR: while writing a meter reading: %s\n", strerror(errno));
|
||||
if (++error_count > ERROR_THRESHOLD)
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void emeter_finalize(struct emeter *this)
|
||||
{
|
||||
if (munmap(this->mmap_base, APB_SIZE) == -1)
|
||||
{
|
||||
// Report the error but don't bother doing anything else, as we're not gonna do
|
||||
// anything with emeter after this point anyway.
|
||||
fprintf(stderr, "ERROR: munmap failed; got %s\n", strerror(errno));
|
||||
}
|
||||
close(this->fd);
|
||||
fclose(this->out);
|
||||
}
|
||||
|
||||
// -------------------------------------- /emeter ----------------------------------------------------
|
||||
|
||||
int done = 0;
|
||||
|
||||
void term_handler(int signum)
|
||||
{
|
||||
done = 1;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct sigaction action;
|
||||
memset(&action, 0, sizeof(struct sigaction));
|
||||
action.sa_handler = term_handler;
|
||||
sigaction(SIGTERM, &action, NULL);
|
||||
|
||||
struct config config;
|
||||
struct emeter emeter;
|
||||
config_init(&config, argc, argv);
|
||||
emeter_init(&emeter, config.output_file);
|
||||
|
||||
struct timespec remaining;
|
||||
while (!done)
|
||||
{
|
||||
emeter_take_reading(&emeter);
|
||||
nsleep(&config.period, &remaining);
|
||||
}
|
||||
|
||||
emeter_finalize(&emeter);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
12
wlauto/external/revent/Makefile
vendored
12
wlauto/external/revent/Makefile
vendored
@ -1,12 +0,0 @@
|
||||
# CROSS_COMPILE=aarch64-linux-gnu- make
|
||||
#
|
||||
CC=gcc
|
||||
CFLAGS=-static -lc
|
||||
|
||||
revent: revent.c
|
||||
$(CROSS_COMPILE)$(CC) $(CFLAGS) revent.c -o revent
|
||||
|
||||
clean:
|
||||
rm -rf revent
|
||||
|
||||
.PHONY: clean
|
636
wlauto/external/revent/revent.c
vendored
636
wlauto/external/revent/revent.c
vendored
@ -1,636 +0,0 @@
|
||||
/* Copyright 2012-2015 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.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <linux/input.h>
|
||||
#include <sys/stat.h>
|
||||
#include <signal.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#ifdef ANDROID
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
|
||||
|
||||
#define die(args...) do { \
|
||||
fprintf(stderr, "ERROR: "); \
|
||||
fprintf(stderr, args); \
|
||||
exit(EXIT_FAILURE); \
|
||||
} while(0)
|
||||
|
||||
#define dprintf(args...) if (verbose) printf(args)
|
||||
|
||||
|
||||
#define INPDEV_MAX_DEVICES 16
|
||||
#define INPDEV_MAX_PATH 30
|
||||
|
||||
|
||||
#ifndef ANDROID
|
||||
int strlcpy(char *dest, char *source, size_t size)
|
||||
{
|
||||
strncpy(dest, source, size-1);
|
||||
dest[size-1] = '\0';
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FALSE=0,
|
||||
TRUE
|
||||
} bool_t;
|
||||
|
||||
typedef enum {
|
||||
RECORD=0,
|
||||
REPLAY,
|
||||
DUMP,
|
||||
INFO,
|
||||
INVALID
|
||||
} revent_mode_t;
|
||||
|
||||
typedef struct {
|
||||
revent_mode_t mode;
|
||||
int32_t record_time;
|
||||
int32_t device_number;
|
||||
char *file;
|
||||
} revent_args_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t id_pathc; /* Count of total paths so far. */
|
||||
char id_pathv[INPDEV_MAX_DEVICES][INPDEV_MAX_PATH]; /* List of paths matching pattern. */
|
||||
} inpdev_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t dev_idx;
|
||||
int32_t _padding;
|
||||
struct input_event event;
|
||||
} replay_event_t;
|
||||
|
||||
typedef struct {
|
||||
int32_t num_fds;
|
||||
int32_t num_events;
|
||||
int *fds;
|
||||
replay_event_t *events;
|
||||
} replay_buffer_t;
|
||||
|
||||
|
||||
bool_t verbose = FALSE;
|
||||
bool_t wait_for_stdin = TRUE;
|
||||
|
||||
bool_t is_numeric(char *string)
|
||||
{
|
||||
int len = strlen(string);
|
||||
|
||||
int i = 0;
|
||||
while(i < len)
|
||||
{
|
||||
if(!isdigit(string[i]))
|
||||
return FALSE;
|
||||
i++;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
off_t get_file_size(const char *filename) {
|
||||
struct stat st;
|
||||
|
||||
if (stat(filename, &st) == 0)
|
||||
return st.st_size;
|
||||
|
||||
die("Cannot determine size of %s: %s\n", filename, strerror(errno));
|
||||
}
|
||||
|
||||
int inpdev_init(inpdev_t **inpdev, int devid)
|
||||
{
|
||||
int32_t i;
|
||||
int fd;
|
||||
int32_t num_devices;
|
||||
|
||||
*inpdev = malloc(sizeof(inpdev_t));
|
||||
(*inpdev)->id_pathc = 0;
|
||||
|
||||
if (devid == -1) {
|
||||
// device id was not specified so we want to record from all available input devices.
|
||||
for(i = 0; i < INPDEV_MAX_DEVICES; ++i)
|
||||
{
|
||||
sprintf((*inpdev)->id_pathv[(*inpdev)->id_pathc], "/dev/input/event%d", i);
|
||||
fd = open((*inpdev)->id_pathv[(*inpdev)->id_pathc], O_RDONLY);
|
||||
if(fd > 0)
|
||||
{
|
||||
close(fd);
|
||||
dprintf("opened %s\n", (*inpdev)->id_pathv[(*inpdev)->id_pathc]);
|
||||
(*inpdev)->id_pathc++;
|
||||
}
|
||||
else
|
||||
{
|
||||
dprintf("could not open %s\n", (*inpdev)->id_pathv[(*inpdev)->id_pathc]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// device id was specified so record just that device.
|
||||
sprintf((*inpdev)->id_pathv[0], "/dev/input/event%d", devid);
|
||||
fd = open((*inpdev)->id_pathv[0], O_RDONLY);
|
||||
if(fd > 0)
|
||||
{
|
||||
close(fd);
|
||||
dprintf("opened %s\n", (*inpdev)->id_pathv[0]);
|
||||
(*inpdev)->id_pathc++;
|
||||
}
|
||||
else
|
||||
{
|
||||
die("could not open %s\n", (*inpdev)->id_pathv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int inpdev_close(inpdev_t *inpdev)
|
||||
{
|
||||
free(inpdev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void printDevProperties(const char* aDev)
|
||||
{
|
||||
int fd = -1;
|
||||
char name[256]= "Unknown";
|
||||
if ((fd = open(aDev, O_RDONLY)) < 0)
|
||||
die("could not open %s\n", aDev);
|
||||
|
||||
if(ioctl(fd, EVIOCGNAME(sizeof(name)), name) < 0)
|
||||
die("evdev ioctl failed on %s\n", aDev);
|
||||
|
||||
printf("The device on %s says its name is %s\n",
|
||||
aDev, name);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
void dump(const char *logfile)
|
||||
{
|
||||
int fdin = open(logfile, O_RDONLY);
|
||||
if (fdin < 0) die("Could not open eventlog %s\n", logfile);
|
||||
|
||||
int nfds;
|
||||
size_t rb = read(fdin, &nfds, sizeof(nfds));
|
||||
if (rb != sizeof(nfds)) die("problems reading eventlog\n");
|
||||
int *fds = malloc(sizeof(int)*nfds);
|
||||
if (!fds) die("out of memory\n");
|
||||
|
||||
int32_t len;
|
||||
int32_t i;
|
||||
char buf[INPDEV_MAX_PATH];
|
||||
|
||||
inpdev_t *inpdev = malloc(sizeof(inpdev_t));
|
||||
inpdev->id_pathc = 0;
|
||||
for (i=0; i<nfds; i++) {
|
||||
memset(buf, 0, sizeof(buf));
|
||||
rb = read(fdin, &len, sizeof(len));
|
||||
if (rb != sizeof(len)) die("problems reading eventlog\n");
|
||||
rb = read(fdin, &buf[0], len);
|
||||
if (rb != len) die("problems reading eventlog\n");
|
||||
strlcpy(inpdev->id_pathv[inpdev->id_pathc], buf, INPDEV_MAX_PATH);
|
||||
inpdev->id_pathv[inpdev->id_pathc][INPDEV_MAX_PATH-1] = '\0';
|
||||
inpdev->id_pathc++;
|
||||
}
|
||||
|
||||
struct input_event ev;
|
||||
int count = 0;
|
||||
while(1) {
|
||||
int32_t idx;
|
||||
rb = read(fdin, &idx, sizeof(idx));
|
||||
if (rb != sizeof(idx)) break;
|
||||
rb = read(fdin, &ev, sizeof(ev));
|
||||
if (rb < (int)sizeof(ev)) break;
|
||||
|
||||
printf("%10u.%-6u %30s type %2d code %3d value %4d\n",
|
||||
(unsigned int)ev.time.tv_sec, (unsigned int)ev.time.tv_usec,
|
||||
inpdev->id_pathv[idx], ev.type, ev.code, ev.value);
|
||||
count++;
|
||||
}
|
||||
|
||||
printf("\nTotal: %d events\n", count);
|
||||
close(fdin);
|
||||
free(inpdev);
|
||||
}
|
||||
|
||||
int replay_buffer_init(replay_buffer_t **buffer, const char *logfile)
|
||||
{
|
||||
*buffer = malloc(sizeof(replay_buffer_t));
|
||||
replay_buffer_t *buff = *buffer;
|
||||
off_t fsize = get_file_size(logfile);
|
||||
buff->events = (replay_event_t *)malloc((size_t)fsize);
|
||||
if (!buff->events)
|
||||
die("out of memory\n");
|
||||
|
||||
int fdin = open(logfile, O_RDONLY);
|
||||
if (fdin < 0)
|
||||
die("Could not open eventlog %s\n", logfile);
|
||||
|
||||
size_t rb = read(fdin, &(buff->num_fds), sizeof(buff->num_fds));
|
||||
if (rb!=sizeof(buff->num_fds))
|
||||
die("problems reading eventlog\n");
|
||||
|
||||
buff->fds = malloc(sizeof(int) * buff->num_fds);
|
||||
if (!buff->fds)
|
||||
die("out of memory\n");
|
||||
|
||||
int32_t len, i;
|
||||
char path_buff[256]; // should be more than enough
|
||||
for (i = 0; i < buff->num_fds; i++) {
|
||||
memset(path_buff, 0, sizeof(path_buff));
|
||||
rb = read(fdin, &len, sizeof(len));
|
||||
if (rb!=sizeof(len))
|
||||
die("problems reading eventlog\n");
|
||||
rb = read(fdin, &path_buff[0], len);
|
||||
if (rb != len)
|
||||
die("problems reading eventlog\n");
|
||||
|
||||
buff->fds[i] = open(path_buff, O_WRONLY | O_NDELAY);
|
||||
if (buff->fds[i] < 0)
|
||||
die("could not open device file %s\n", path_buff);
|
||||
}
|
||||
|
||||
struct timeval start_time;
|
||||
replay_event_t rep_ev;
|
||||
i = 0;
|
||||
while(1) {
|
||||
rb = read(fdin, &rep_ev, sizeof(rep_ev));
|
||||
if (rb < (int)sizeof(rep_ev))
|
||||
break;
|
||||
|
||||
if (i == 0) {
|
||||
start_time = rep_ev.event.time;
|
||||
}
|
||||
timersub(&(rep_ev.event.time), &start_time, &(rep_ev.event.time));
|
||||
memcpy(&(buff->events[i]), &rep_ev, sizeof(rep_ev));
|
||||
i++;
|
||||
}
|
||||
buff->num_events = i - 1;
|
||||
close(fdin);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int replay_buffer_close(replay_buffer_t *buff)
|
||||
{
|
||||
free(buff->fds);
|
||||
free(buff->events);
|
||||
free(buff);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int replay_buffer_play(replay_buffer_t *buff)
|
||||
{
|
||||
int32_t i = 0, rb;
|
||||
struct timeval start_time, now, desired_time, last_event_delta, delta;
|
||||
memset(&last_event_delta, 0, sizeof(struct timeval));
|
||||
gettimeofday(&start_time, NULL);
|
||||
|
||||
while (i < buff->num_events) {
|
||||
gettimeofday(&now, NULL);
|
||||
timeradd(&start_time, &last_event_delta, &desired_time);
|
||||
|
||||
if (timercmp(&desired_time, &now, >)) {
|
||||
timersub(&desired_time, &now, &delta);
|
||||
useconds_t d = (useconds_t)delta.tv_sec * 1000000 + delta.tv_usec;
|
||||
dprintf("now %u.%u desiredtime %u.%u sleeping %u uS\n",
|
||||
(unsigned int)now.tv_sec, (unsigned int)now.tv_usec,
|
||||
(unsigned int)desired_time.tv_sec, (unsigned int)desired_time.tv_usec, d);
|
||||
usleep(d);
|
||||
}
|
||||
|
||||
int32_t idx = (buff->events[i]).dev_idx;
|
||||
struct input_event ev = (buff->events[i]).event;
|
||||
while((i < buff->num_events) && !timercmp(&ev.time, &last_event_delta, !=)) {
|
||||
rb = write(buff->fds[idx], &ev, sizeof(ev));
|
||||
if (rb!=sizeof(ev))
|
||||
die("problems writing\n");
|
||||
dprintf("replayed event: type %d code %d value %d\n", ev.type, ev.code, ev.value);
|
||||
|
||||
i++;
|
||||
idx = (buff->events[i]).dev_idx;
|
||||
ev = (buff->events[i]).event;
|
||||
}
|
||||
last_event_delta = ev.time;
|
||||
}
|
||||
}
|
||||
|
||||
void replay(const char *logfile)
|
||||
{
|
||||
replay_buffer_t *replay_buffer;
|
||||
replay_buffer_init(&replay_buffer, logfile);
|
||||
#ifdef ANDROID
|
||||
__android_log_write(ANDROID_LOG_INFO, "REVENT", "Replay starting");
|
||||
#endif
|
||||
replay_buffer_play(replay_buffer);
|
||||
#ifdef ANDROID
|
||||
__android_log_write(ANDROID_LOG_INFO, "REVENT", "Replay complete");
|
||||
#endif
|
||||
replay_buffer_close(replay_buffer);
|
||||
}
|
||||
|
||||
void usage()
|
||||
{
|
||||
printf("usage:\n revent [-h] [-v] COMMAND [OPTIONS] \n"
|
||||
"\n"
|
||||
" Options:\n"
|
||||
" -h print this help message and quit.\n"
|
||||
" -v enable verbose output.\n"
|
||||
"\n"
|
||||
" Commands:\n"
|
||||
" record [-t SECONDS] [-d DEVICE] FILE\n"
|
||||
" Record input event. stops after return on STDIN (or, optionally, \n"
|
||||
" a fixed delay)\n"
|
||||
"\n"
|
||||
" FILE file into which events will be recorded.\n"
|
||||
" -t SECONDS time, in seconds, for which to record events.\n"
|
||||
" if not specifed, recording will continue until\n"
|
||||
" return key is pressed.\n"
|
||||
" -d DEVICE the number of the input device form which\n"
|
||||
" events will be recoreded. If not specified, \n"
|
||||
" all available inputs will be used.\n"
|
||||
"\n"
|
||||
" replay FILE\n"
|
||||
" replays previously recorded events from the specified file.\n"
|
||||
"\n"
|
||||
" FILE file into which events will be recorded.\n"
|
||||
"\n"
|
||||
" dump FILE\n"
|
||||
" dumps the contents of the specified event log to STDOUT in\n"
|
||||
" human-readable form.\n"
|
||||
"\n"
|
||||
" FILE event log which will be dumped.\n"
|
||||
"\n"
|
||||
" info\n"
|
||||
" shows info about each event char device\n"
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
|
||||
void revent_args_init(revent_args_t **rargs, int argc, char** argv)
|
||||
{
|
||||
*rargs = malloc(sizeof(revent_args_t));
|
||||
revent_args_t *revent_args = *rargs;
|
||||
revent_args->mode = INVALID;
|
||||
revent_args->record_time = INT_MAX;
|
||||
revent_args->device_number = -1;
|
||||
revent_args->file = NULL;
|
||||
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "ht:d:vs")) != -1)
|
||||
{
|
||||
switch (opt) {
|
||||
case 'h':
|
||||
usage();
|
||||
exit(0);
|
||||
break;
|
||||
case 't':
|
||||
if (is_numeric(optarg)) {
|
||||
revent_args->record_time = atoi(optarg);
|
||||
dprintf("timeout: %d\n", revent_args->record_time);
|
||||
} else {
|
||||
die("-t parameter must be numeric; got %s.\n", optarg);
|
||||
}
|
||||
break;
|
||||
case 'd':
|
||||
if (is_numeric(optarg)) {
|
||||
revent_args->device_number = atoi(optarg);
|
||||
dprintf("device: %d\n", revent_args->device_number);
|
||||
} else {
|
||||
die("-d parameter must be numeric; got %s.\n", optarg);
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
verbose = TRUE;
|
||||
break;
|
||||
case 's':
|
||||
wait_for_stdin = FALSE;
|
||||
break;
|
||||
|
||||
default:
|
||||
die("Unexpected option: %c", opt);
|
||||
}
|
||||
}
|
||||
|
||||
int next_arg = optind;
|
||||
if (next_arg == argc) {
|
||||
usage();
|
||||
die("Must specify a command.\n");
|
||||
}
|
||||
if (!strcmp(argv[next_arg], "record"))
|
||||
revent_args->mode = RECORD;
|
||||
else if (!strcmp(argv[next_arg], "replay"))
|
||||
revent_args->mode = REPLAY;
|
||||
else if (!strcmp(argv[next_arg], "dump"))
|
||||
revent_args->mode = DUMP;
|
||||
else if (!strcmp(argv[next_arg], "info"))
|
||||
revent_args->mode = INFO;
|
||||
else {
|
||||
usage();
|
||||
die("Unknown command -- %s\n", argv[next_arg]);
|
||||
}
|
||||
next_arg++;
|
||||
|
||||
if (next_arg != argc) {
|
||||
revent_args->file = argv[next_arg];
|
||||
dprintf("file: %s\n", revent_args->file);
|
||||
next_arg++;
|
||||
if (next_arg != argc) {
|
||||
die("Trailling arguments (use -h for help).\n");
|
||||
}
|
||||
}
|
||||
|
||||
if ((revent_args->mode != RECORD) && (revent_args->record_time != INT_MAX)) {
|
||||
die("-t parameter is only valid for \"record\" command.\n");
|
||||
}
|
||||
if ((revent_args->mode != RECORD) && (revent_args->device_number != -1)) {
|
||||
die("-d parameter is only valid for \"record\" command.\n");
|
||||
}
|
||||
if ((revent_args->mode == INFO) && (revent_args->file != NULL)) {
|
||||
die("File path cannot be specified for \"info\" command.\n");
|
||||
}
|
||||
if (((revent_args->mode == RECORD) || (revent_args->mode == REPLAY)) && (revent_args->file == NULL)) {
|
||||
die("Must specify a file for recording/replaying (use -h for help).\n");
|
||||
}
|
||||
}
|
||||
|
||||
int revent_args_close(revent_args_t *rargs)
|
||||
{
|
||||
free(rargs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int* fds = NULL;
|
||||
FILE* fdout = NULL;
|
||||
revent_args_t *rargs = NULL;
|
||||
inpdev_t *inpdev = NULL;
|
||||
int count;
|
||||
|
||||
void term_handler(int signum)
|
||||
{
|
||||
int32_t i;
|
||||
for (i=0; i < inpdev->id_pathc; i++)
|
||||
{
|
||||
close(fds[i]);
|
||||
}
|
||||
|
||||
fclose(fdout);
|
||||
free(fds);
|
||||
dprintf("Recorded %d events\n", count);
|
||||
|
||||
inpdev_close(inpdev);
|
||||
revent_args_close(rargs);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void record(inpdev_t *inpdev, int delay, const char *logfile)
|
||||
{
|
||||
fd_set readfds;
|
||||
struct input_event ev;
|
||||
int32_t i;
|
||||
int32_t _padding = 0xdeadbeef;
|
||||
int32_t maxfd = 0;
|
||||
int32_t keydev=0;
|
||||
|
||||
//signal handler
|
||||
struct sigaction action;
|
||||
memset(&action, 0, sizeof(struct sigaction));
|
||||
action.sa_handler = term_handler;
|
||||
sigaction(SIGTERM, &action, NULL);
|
||||
|
||||
fds = malloc(sizeof(int)*inpdev->id_pathc);
|
||||
if (!fds) die("out of memory\n");
|
||||
|
||||
fdout = fopen(logfile, "wb");
|
||||
if (!fdout) die("Could not open eventlog %s\n", logfile);
|
||||
|
||||
fwrite(&inpdev->id_pathc, sizeof(inpdev->id_pathc), 1, fdout);
|
||||
for (i=0; i<inpdev->id_pathc; i++) {
|
||||
int32_t len = strlen(inpdev->id_pathv[i]);
|
||||
fwrite(&len, sizeof(len), 1, fdout);
|
||||
fwrite(inpdev->id_pathv[i], len, 1, fdout);
|
||||
}
|
||||
|
||||
for (i=0; i < inpdev->id_pathc; i++)
|
||||
{
|
||||
fds[i] = open(inpdev->id_pathv[i], O_RDONLY);
|
||||
if (fds[i]>maxfd) maxfd = fds[i];
|
||||
dprintf("opened %s with %d\n", inpdev->id_pathv[i], fds[i]);
|
||||
if (fds[i]<0) die("could not open \%s\n", inpdev->id_pathv[i]);
|
||||
}
|
||||
|
||||
count = 0;
|
||||
struct timeval tout;
|
||||
while(1)
|
||||
{
|
||||
FD_ZERO(&readfds);
|
||||
if (wait_for_stdin)
|
||||
{
|
||||
FD_SET(STDIN_FILENO, &readfds);
|
||||
}
|
||||
for (i=0; i < inpdev->id_pathc; i++)
|
||||
FD_SET(fds[i], &readfds);
|
||||
/* wait for input */
|
||||
tout.tv_sec = delay;
|
||||
tout.tv_usec = 0;
|
||||
int32_t r = select(maxfd+1, &readfds, NULL, NULL, &tout);
|
||||
/* dprintf("got %d (err %d)\n", r, errno); */
|
||||
if (!r) break;
|
||||
if (wait_for_stdin && FD_ISSET(STDIN_FILENO, &readfds)) {
|
||||
// in this case the key down for the return key will be recorded
|
||||
// so we need to up the key up
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
ev.type = EV_KEY;
|
||||
ev.code = KEY_ENTER;
|
||||
ev.value = 0;
|
||||
gettimeofday(&ev.time, NULL);
|
||||
fwrite(&keydev, sizeof(keydev), 1, fdout);
|
||||
fwrite(&_padding, sizeof(_padding), 1, fdout);
|
||||
fwrite(&ev, sizeof(ev), 1, fdout);
|
||||
memset(&ev, 0, sizeof(ev)); // SYN
|
||||
gettimeofday(&ev.time, NULL);
|
||||
fwrite(&keydev, sizeof(keydev), 1, fdout);
|
||||
fwrite(&_padding, sizeof(_padding), 1, fdout);
|
||||
fwrite(&ev, sizeof(ev), 1, fdout);
|
||||
dprintf("added fake return exiting...\n");
|
||||
break;
|
||||
}
|
||||
|
||||
for (i=0; i < inpdev->id_pathc; i++)
|
||||
{
|
||||
if (FD_ISSET(fds[i], &readfds))
|
||||
{
|
||||
dprintf("Got event from %s\n", inpdev->id_pathv[i]);
|
||||
memset(&ev, 0, sizeof(ev));
|
||||
size_t rb = read(fds[i], (void*) &ev, sizeof(ev));
|
||||
dprintf("%d event: type %d code %d value %d\n",
|
||||
(unsigned int)rb, ev.type, ev.code, ev.value);
|
||||
if (ev.type == EV_KEY && ev.code == KEY_ENTER && ev.value == 1)
|
||||
keydev = i;
|
||||
fwrite(&i, sizeof(i), 1, fdout);
|
||||
fwrite(&_padding, sizeof(_padding), 1, fdout);
|
||||
fwrite(&ev, sizeof(ev), 1, fdout);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i=0; i < inpdev->id_pathc; i++)
|
||||
{
|
||||
close(fds[i]);
|
||||
}
|
||||
|
||||
fclose(fdout);
|
||||
free(fds);
|
||||
dprintf("Recorded %d events\n", count);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
int i;
|
||||
char *logfile = NULL;
|
||||
|
||||
revent_args_init(&rargs, argc, argv);
|
||||
|
||||
inpdev_init(&inpdev, rargs->device_number);
|
||||
|
||||
switch(rargs->mode) {
|
||||
case RECORD:
|
||||
record(inpdev, rargs->record_time, rargs->file);
|
||||
break;
|
||||
case REPLAY:
|
||||
replay(rargs->file);
|
||||
break;
|
||||
case DUMP:
|
||||
dump(rargs->file);
|
||||
break;
|
||||
case INFO:
|
||||
for (i = 0; i < inpdev->id_pathc; i++) {
|
||||
printDevProperties(inpdev->id_pathv[i]);
|
||||
}
|
||||
};
|
||||
|
||||
inpdev_close(inpdev);
|
||||
revent_args_close(rargs);
|
||||
return 0;
|
||||
}
|
21
wlauto/external/uiauto/build.sh
vendored
21
wlauto/external/uiauto/build.sh
vendored
@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
|
||||
ant build
|
||||
|
||||
cp bin/classes/com/arm/wlauto/uiauto/BaseUiAutomation.class ../../common
|
92
wlauto/external/uiauto/build.xml
vendored
92
wlauto/external/uiauto/build.xml
vendored
@ -1,92 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project name="com.arm.wlauto.uiauto" default="help">
|
||||
|
||||
<!-- The local.properties file is created and updated by the 'android' tool.
|
||||
It contains the path to the SDK. It should *NOT* be checked into
|
||||
Version Control Systems. -->
|
||||
<property file="local.properties" />
|
||||
|
||||
<!-- The ant.properties file can be created by you. It is only edited by the
|
||||
'android' tool to add properties to it.
|
||||
This is the place to change some Ant specific build properties.
|
||||
Here are some properties you may want to change/update:
|
||||
|
||||
source.dir
|
||||
The name of the source directory. Default is 'src'.
|
||||
out.dir
|
||||
The name of the output directory. Default is 'bin'.
|
||||
|
||||
For other overridable properties, look at the beginning of the rules
|
||||
files in the SDK, at tools/ant/build.xml
|
||||
|
||||
Properties related to the SDK location or the project target should
|
||||
be updated using the 'android' tool with the 'update' action.
|
||||
|
||||
This file is an integral part of the build system for your
|
||||
application and should be checked into Version Control Systems.
|
||||
|
||||
-->
|
||||
<property file="ant.properties" />
|
||||
|
||||
<!-- if sdk.dir was not set from one of the property file, then
|
||||
get it from the ANDROID_HOME env var.
|
||||
This must be done before we load project.properties since
|
||||
the proguard config can use sdk.dir -->
|
||||
<property environment="env" />
|
||||
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
|
||||
<isset property="env.ANDROID_HOME" />
|
||||
</condition>
|
||||
|
||||
<!-- The project.properties file is created and updated by the 'android'
|
||||
tool, as well as ADT.
|
||||
|
||||
This contains project specific properties such as project target, and library
|
||||
dependencies. Lower level build properties are stored in ant.properties
|
||||
(or in .classpath for Eclipse projects).
|
||||
|
||||
This file is an integral part of the build system for your
|
||||
application and should be checked into Version Control Systems. -->
|
||||
<loadproperties srcFile="project.properties" />
|
||||
|
||||
<!-- quick check on sdk.dir -->
|
||||
<fail
|
||||
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
|
||||
unless="sdk.dir"
|
||||
/>
|
||||
|
||||
<!--
|
||||
Import per project custom build rules if present at the root of the project.
|
||||
This is the place to put custom intermediary targets such as:
|
||||
-pre-build
|
||||
-pre-compile
|
||||
-post-compile (This is typically used for code obfuscation.
|
||||
Compiled code location: ${out.classes.absolute.dir}
|
||||
If this is not done in place, override ${out.dex.input.absolute.dir})
|
||||
-post-package
|
||||
-post-build
|
||||
-pre-clean
|
||||
-->
|
||||
<import file="custom_rules.xml" optional="true" />
|
||||
|
||||
<!-- Import the actual build file.
|
||||
|
||||
To customize existing targets, there are two options:
|
||||
- Customize only one target:
|
||||
- copy/paste the target into this file, *before* the
|
||||
<import> task.
|
||||
- customize it to your needs.
|
||||
- Customize the whole content of build.xml
|
||||
- copy/paste the content of the rules files (minus the top node)
|
||||
into this file, replacing the <import> task.
|
||||
- customize to your needs.
|
||||
|
||||
***********************
|
||||
****** IMPORTANT ******
|
||||
***********************
|
||||
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
|
||||
in order to avoid having your file be overridden by tools such as "android update project"
|
||||
-->
|
||||
<!-- version-tag: VERSION_TAG -->
|
||||
<import file="${sdk.dir}/tools/ant/uibuild.xml" />
|
||||
|
||||
</project>
|
14
wlauto/external/uiauto/project.properties
vendored
14
wlauto/external/uiauto/project.properties
vendored
@ -1,14 +0,0 @@
|
||||
# This file is automatically generated by Android Tools.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file must be checked in Version Control Systems.
|
||||
#
|
||||
# To customize properties used by the Ant build system edit
|
||||
# "ant.properties", and override values to adapt the script to your
|
||||
# project structure.
|
||||
#
|
||||
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
|
||||
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||
|
||||
# Project target.
|
||||
target=android-17
|
@ -1,113 +0,0 @@
|
||||
/* Copyright 2013-2015 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.
|
||||
*/
|
||||
|
||||
|
||||
package com.arm.wlauto.uiauto;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
// Import the uiautomator libraries
|
||||
import com.android.uiautomator.core.UiObject;
|
||||
import com.android.uiautomator.core.UiObjectNotFoundException;
|
||||
import com.android.uiautomator.core.UiScrollable;
|
||||
import com.android.uiautomator.core.UiSelector;
|
||||
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
|
||||
|
||||
public class BaseUiAutomation extends UiAutomatorTestCase {
|
||||
|
||||
|
||||
public void sleep(int second) {
|
||||
super.sleep(second * 1000);
|
||||
}
|
||||
|
||||
public boolean takeScreenshot(String name) {
|
||||
Bundle params = getParams();
|
||||
String png_dir = params.getString("workdir");
|
||||
|
||||
try {
|
||||
return getUiDevice().takeScreenshot(new File(png_dir, name + ".png"));
|
||||
} catch(NoSuchMethodError e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void waitText(String text) throws UiObjectNotFoundException {
|
||||
waitText(text, 600);
|
||||
}
|
||||
|
||||
public void waitText(String text, int second) throws UiObjectNotFoundException {
|
||||
UiSelector selector = new UiSelector();
|
||||
UiObject text_obj = new UiObject(selector.text(text)
|
||||
.className("android.widget.TextView"));
|
||||
waitObject(text_obj, second);
|
||||
}
|
||||
|
||||
public void waitObject(UiObject obj) throws UiObjectNotFoundException {
|
||||
waitObject(obj, 600);
|
||||
}
|
||||
|
||||
public void waitObject(UiObject obj, int second) throws UiObjectNotFoundException {
|
||||
if (! obj.waitForExists(second * 1000)){
|
||||
throw new UiObjectNotFoundException("UiObject is not found: "
|
||||
+ obj.getSelector().toString());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean waitUntilNoObject(UiObject obj, int second) {
|
||||
return obj.waitUntilGone(second * 1000);
|
||||
}
|
||||
|
||||
public void clearLogcat() throws Exception {
|
||||
Runtime.getRuntime().exec("logcat -c");
|
||||
}
|
||||
|
||||
public void waitForLogcatText(String searchText, long timeout) throws Exception {
|
||||
long startTime = System.currentTimeMillis();
|
||||
Process process = Runtime.getRuntime().exec("logcat");
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
String line;
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
boolean found = false;
|
||||
while ((currentTime - startTime) < timeout){
|
||||
sleep(2); // poll every two seconds
|
||||
|
||||
while((line=reader.readLine())!=null) {
|
||||
if (line.contains(searchText)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
currentTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
process.destroy();
|
||||
|
||||
if ((currentTime - startTime) >= timeout) {
|
||||
throw new TimeoutException("Timed out waiting for Logcat text \"%s\"".format(searchText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
# Copyright 2013-2015 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 wlauto.core import instrumentation
|
||||
|
||||
|
||||
def instrument_is_installed(instrument):
|
||||
"""Returns ``True`` if the specified instrument is installed, and ``False``
|
||||
other wise. The insturment maybe specified either as a name or a subclass (or
|
||||
instance of subclass) of :class:`wlauto.core.Instrument`."""
|
||||
return instrumentation.is_installed(instrument)
|
||||
|
||||
|
||||
def instrument_is_enabled(instrument):
|
||||
"""Returns ``True`` if the specified instrument is installed and is currently
|
||||
enabled, and ``False`` other wise. The insturment maybe specified either
|
||||
as a name or a subclass (or instance of subclass) of
|
||||
:class:`wlauto.core.Instrument`."""
|
||||
return instrumentation.is_enabled(instrument)
|
||||
|
||||
|
||||
def clear_instrumentation():
|
||||
instrumentation.installed = []
|
@ -1,278 +0,0 @@
|
||||
# Copyright 2013-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import subprocess
|
||||
import tempfile
|
||||
import csv
|
||||
|
||||
from wlauto import Instrument, Parameter
|
||||
from wlauto.core.execution import ExecutionContext
|
||||
from wlauto.exceptions import InstrumentError, WorkerThreadError
|
||||
from wlauto.core import signal
|
||||
|
||||
|
||||
class CoreUtilization(Instrument):
|
||||
|
||||
name = 'coreutil'
|
||||
description = """
|
||||
Measures CPU core activity during workload execution in terms of the percentage of time a number
|
||||
of cores were utilized above the specfied threshold.
|
||||
|
||||
This workload generates ``coreutil.csv`` report in the workload's output directory. The report is
|
||||
formatted as follows::
|
||||
|
||||
<threshold,1core,2core,3core,4core
|
||||
18.098132,38.650248000000005,10.736180000000001,3.6809760000000002,28.834312000000001
|
||||
|
||||
Interpretation of the result:
|
||||
|
||||
- 38.65% of total time only single core is running above or equal to threshold value
|
||||
- 10.736% of total time two cores are running simultaneously above or equal to threshold value
|
||||
- 3.6809% of total time three cores are running simultaneously above or equal to threshold value
|
||||
- 28.8314% of total time four cores are running simultaneously above or equal to threshold value
|
||||
- 18.098% of time all core are running below threshold value.
|
||||
|
||||
..note : This instrument doesn't work on ARM big.LITTLE IKS implementation
|
||||
|
||||
"""
|
||||
|
||||
parameters = [
|
||||
Parameter('threshold', kind=int, default=50,
|
||||
constraint=lambda x: 0 < x <= 100,
|
||||
description='Cores with percentage utilization above this value will be considered '
|
||||
'as "utilized". This value may need to be adjusted based on the background '
|
||||
'activity and the intensity of the workload being instrumented (e.g. it may '
|
||||
'need to be lowered for low-intensity workloads such as video playback).'
|
||||
)
|
||||
]
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
super(CoreUtilization, self).__init__(device, **kwargs)
|
||||
self.collector = None
|
||||
self.output_dir = None
|
||||
self.cores = None
|
||||
self.output_artifact_registered = False
|
||||
|
||||
def setup(self, context):
|
||||
''' Calls ProcCollect class '''
|
||||
self.output_dir = context.output_directory
|
||||
self.collector = ProcCollect(self.device, self.logger, self.output_dir)
|
||||
self.cores = self.device.number_of_cores
|
||||
|
||||
def start(self, context): # pylint: disable=W0613
|
||||
''' Starts collecting data once the workload starts '''
|
||||
self.logger.debug('Starting to collect /proc/stat data')
|
||||
self.collector.start()
|
||||
|
||||
def stop(self, context): # pylint: disable=W0613
|
||||
''' Stops collecting data once the workload stops '''
|
||||
self.logger.debug('Stopping /proc/stat data collection')
|
||||
self.collector.stop()
|
||||
|
||||
def update_result(self, context):
|
||||
''' updates result into coreutil.csv '''
|
||||
self.collector.join() # wait for "proc.txt" to generate.
|
||||
context.add_artifact('proctxt', 'proc.txt', 'raw')
|
||||
calc = Calculator(self.cores, self.threshold, context) # pylint: disable=E1101
|
||||
calc.calculate()
|
||||
if not self.output_artifact_registered:
|
||||
context.add_run_artifact('cpuutil', 'coreutil.csv', 'data')
|
||||
self.output_artifact_registered = True
|
||||
|
||||
|
||||
class ProcCollect(threading.Thread):
|
||||
''' Dumps data into proc.txt '''
|
||||
|
||||
def __init__(self, device, logger, out_dir):
|
||||
super(ProcCollect, self).__init__()
|
||||
self.device = device
|
||||
self.logger = logger
|
||||
self.dire = out_dir
|
||||
self.stop_signal = threading.Event()
|
||||
self.command = 'cat /proc/stat'
|
||||
self.exc = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.stop_signal.clear()
|
||||
_, temp_file = tempfile.mkstemp()
|
||||
self.logger.debug('temp file : {}'.format(temp_file))
|
||||
with open(temp_file, 'wb') as tempfp:
|
||||
while not self.stop_signal.is_set():
|
||||
tempfp.write(self.device.execute(self.command))
|
||||
tempfp.write('\n')
|
||||
time.sleep(0.5)
|
||||
raw_file = os.path.join(self.dire, 'proc.txt')
|
||||
shutil.copy(temp_file, raw_file)
|
||||
os.unlink(temp_file)
|
||||
except Exception, error: # pylint: disable=W0703
|
||||
self.logger.warning('Exception on collector thread : {}({})'.format(error.__class__.__name__, error))
|
||||
self.exc = WorkerThreadError(self.name, sys.exc_info())
|
||||
|
||||
def stop(self):
|
||||
'''Executed once the workload stops'''
|
||||
self.stop_signal.set()
|
||||
if self.exc is not None:
|
||||
raise self.exc # pylint: disable=E0702
|
||||
|
||||
|
||||
class Calculator(object):
|
||||
"""
|
||||
Read /proc/stat and dump data into ``proc.txt`` which is parsed to generate ``coreutil.csv``
|
||||
Sample output from 'proc.txt' ::
|
||||
|
||||
----------------------------------------------------------------------
|
||||
cpu 9853753 51448 3248855 12403398 4241 111 14996 0 0 0
|
||||
cpu0 1585220 7756 1103883 4977224 552 97 10505 0 0 0
|
||||
cpu1 2141168 7243 564347 972273 504 4 1442 0 0 0
|
||||
cpu2 1940681 7994 651946 1005534 657 3 1424 0 0 0
|
||||
cpu3 1918013 8833 667782 1012249 643 3 1326 0 0 0
|
||||
cpu4 165429 5363 50289 1118910 474 0 148 0 0 0
|
||||
cpu5 1661299 4910 126654 1104018 480 0 53 0 0 0
|
||||
cpu6 333642 4657 48296 1102531 482 2 55 0 0 0
|
||||
cpu7 108299 4691 35656 1110658 448 0 41 0 0 0
|
||||
----------------------------------------------------------------------
|
||||
Description:
|
||||
|
||||
1st column : cpu_id( cpu0, cpu1, cpu2,......)
|
||||
Next all column represents the amount of time, measured in units of USER_HZ
|
||||
2nd column : Time spent in user mode
|
||||
3rd column : Time spent in user mode with low priority
|
||||
4th column : Time spent in system mode
|
||||
5th column : Time spent in idle task
|
||||
6th column : Time waiting for i/o to compelete
|
||||
7th column : Time servicing interrupts
|
||||
8th column : Time servicing softirqs
|
||||
9th column : Stolen time is the time spent in other operating systems
|
||||
10th column : Time spent running a virtual CPU
|
||||
11th column : Time spent running a niced guest
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
Procedure to calculate instantaneous CPU utilization:
|
||||
|
||||
1) Subtract two consecutive samples for every column( except 1st )
|
||||
2) Sum all the values except "Time spent in idle task"
|
||||
3) CPU utilization(%) = ( value obtained in 2 )/sum of all the values)*100
|
||||
|
||||
"""
|
||||
|
||||
idle_time_index = 3
|
||||
|
||||
def __init__(self, cores, threshold, context):
|
||||
self.cores = cores
|
||||
self.threshold = threshold
|
||||
self.context = context
|
||||
self.cpu_util = None # Store CPU utilization for each core
|
||||
self.active = None # Store active time(total time - idle)
|
||||
self.total = None # Store the total amount of time (in USER_HZ)
|
||||
self.output = None
|
||||
self.cpuid_regex = re.compile(r'cpu(\d+)')
|
||||
self.outfile = os.path.join(context.run_output_directory, 'coreutil.csv')
|
||||
self.infile = os.path.join(context.output_directory, 'proc.txt')
|
||||
|
||||
def calculate(self):
|
||||
self.calculate_total_active()
|
||||
self.calculate_core_utilization()
|
||||
self.generate_csv(self.context)
|
||||
|
||||
def calculate_total_active(self):
|
||||
""" Read proc.txt file and calculate 'self.active' and 'self.total' """
|
||||
all_cores = set(xrange(self.cores))
|
||||
self.total = [[] for _ in all_cores]
|
||||
self.active = [[] for _ in all_cores]
|
||||
with open(self.infile, "r") as fh:
|
||||
# parsing logic:
|
||||
# - keep spinning through lines until see the cpu summary line
|
||||
# (taken to indicate start of new record).
|
||||
# - extract values for individual cores after the summary line,
|
||||
# keeping track of seen cores until no more lines match 'cpu\d+'
|
||||
# pattern.
|
||||
# - For every core not seen in this record, pad zeros.
|
||||
# - Loop
|
||||
try:
|
||||
while True:
|
||||
line = fh.next()
|
||||
if not line.startswith('cpu '):
|
||||
continue
|
||||
|
||||
seen_cores = set([])
|
||||
line = fh.next()
|
||||
match = self.cpuid_regex.match(line)
|
||||
while match:
|
||||
cpu_id = int(match.group(1))
|
||||
seen_cores.add(cpu_id)
|
||||
times = map(int, line.split()[1:]) # first column is the cpu_id
|
||||
self.total[cpu_id].append(sum(times))
|
||||
self.active[cpu_id].append(sum(times) - times[self.idle_time_index])
|
||||
line = fh.next()
|
||||
match = self.cpuid_regex.match(line)
|
||||
|
||||
for unseen_core in all_cores - seen_cores:
|
||||
self.total[unseen_core].append(0)
|
||||
self.active[unseen_core].append(0)
|
||||
except StopIteration: # EOF
|
||||
pass
|
||||
|
||||
def calculate_core_utilization(self):
|
||||
"""Calculates CPU utilization"""
|
||||
diff_active = [[] for _ in xrange(self.cores)]
|
||||
diff_total = [[] for _ in xrange(self.cores)]
|
||||
self.cpu_util = [[] for _ in xrange(self.cores)]
|
||||
for i in xrange(self.cores):
|
||||
for j in xrange(len(self.active[i]) - 1):
|
||||
temp = self.active[i][j + 1] - self.active[i][j]
|
||||
diff_active[i].append(temp)
|
||||
diff_total[i].append(self.total[i][j + 1] - self.total[i][j])
|
||||
if diff_total[i][j] == 0:
|
||||
self.cpu_util[i].append(0)
|
||||
else:
|
||||
temp = float(diff_active[i][j]) / diff_total[i][j]
|
||||
self.cpu_util[i].append(round((float(temp)) * 100, 2))
|
||||
|
||||
def generate_csv(self, context):
|
||||
""" generates ``coreutil.csv``"""
|
||||
self.output = [0 for _ in xrange(self.cores + 1)]
|
||||
for i in range(len(self.cpu_util[0])):
|
||||
count = 0
|
||||
for j in xrange(len(self.cpu_util)):
|
||||
if self.cpu_util[j][i] > round(float(self.threshold), 2):
|
||||
count = count + 1
|
||||
self.output[count] += 1
|
||||
if self.cpu_util[0]:
|
||||
scale_factor = round((float(1) / len(self.cpu_util[0])) * 100, 6)
|
||||
else:
|
||||
scale_factor = 0
|
||||
for i in xrange(len(self.output)):
|
||||
self.output[i] = self.output[i] * scale_factor
|
||||
with open(self.outfile, 'a+') as tem:
|
||||
writer = csv.writer(tem)
|
||||
reader = csv.reader(tem)
|
||||
if sum(1 for row in reader) == 0:
|
||||
row = ['workload', 'iteration', '<threshold']
|
||||
for i in xrange(1, self.cores + 1):
|
||||
row.append('{}core'.format(i))
|
||||
writer.writerow(row)
|
||||
row = [context.result.workload.name, context.result.iteration]
|
||||
row.extend(self.output)
|
||||
writer.writerow(row)
|
@ -1,416 +0,0 @@
|
||||
# Copyright 2013-2015 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,access-member-before-definition,attribute-defined-outside-init
|
||||
from __future__ import division
|
||||
import os
|
||||
import sys
|
||||
import csv
|
||||
import shutil
|
||||
import tempfile
|
||||
from collections import OrderedDict, defaultdict
|
||||
from string import ascii_lowercase
|
||||
|
||||
from multiprocessing import Process, Queue
|
||||
|
||||
from wlauto import Instrument, Parameter
|
||||
from wlauto.core import signal
|
||||
from wlauto.exceptions import ConfigError, InstrumentError, DeviceError
|
||||
from wlauto.utils.misc import ensure_directory_exists as _d
|
||||
from wlauto.utils.types import list_of_ints, list_of_strs, boolean
|
||||
|
||||
# pylint: disable=wrong-import-position,wrong-import-order
|
||||
daqpower_path = os.path.join(os.path.dirname(__file__), '..', '..', 'external', 'daq_server', 'src')
|
||||
sys.path.insert(0, daqpower_path)
|
||||
try:
|
||||
import daqpower.client as daq # pylint: disable=F0401
|
||||
from daqpower.config import DeviceConfiguration, ServerConfiguration, ConfigurationError # pylint: disable=F0401
|
||||
except ImportError, e:
|
||||
daq, DeviceConfiguration, ServerConfiguration, ConfigurationError = None, None, None, None
|
||||
import_error_mesg = e.message
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
UNITS = {
|
||||
'energy': 'Joules',
|
||||
'power': 'Watts',
|
||||
'voltage': 'Volts',
|
||||
}
|
||||
|
||||
|
||||
GPIO_ROOT = '/sys/class/gpio'
|
||||
TRACE_MARKER_PATH = '/sys/kernel/debug/tracing/trace_marker'
|
||||
|
||||
|
||||
def dict_or_bool(value):
|
||||
"""
|
||||
Ensures that either a dictionary or a boolean is used as a parameter.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return boolean(value)
|
||||
|
||||
|
||||
class Daq(Instrument):
|
||||
|
||||
name = 'daq'
|
||||
description = """
|
||||
DAQ instrument obtains the power consumption of the target device's core
|
||||
measured by National Instruments Data Acquisition(DAQ) device.
|
||||
|
||||
WA communicates with a DAQ device server running on a Windows machine
|
||||
(Please refer to :ref:`daq_setup`) over a network. You must specify the IP
|
||||
address and port the server is listening on in the config file as follows ::
|
||||
|
||||
daq_server_host = '10.1.197.176'
|
||||
daq_server_port = 45677
|
||||
|
||||
These values will be output by the server when you run it on Windows.
|
||||
|
||||
You must also specify the values of resistors (in Ohms) across which the
|
||||
voltages are measured (Please refer to :ref:`daq_setup`). The values should be
|
||||
specified as a list with an entry for each resistor, e.g.::
|
||||
|
||||
daq_resistor_values = [0.005, 0.005]
|
||||
|
||||
In addition to this mandatory configuration, you can also optionally specify the
|
||||
following::
|
||||
|
||||
:daq_labels: Labels to be used for ports. Defaults to ``'PORT_<pnum>'``, where
|
||||
'pnum' is the number of the port.
|
||||
:daq_device_id: The ID under which the DAQ is registered with the driver.
|
||||
Defaults to ``'Dev1'``.
|
||||
:daq_v_range: Specifies the voltage range for the SOC voltage channel on the DAQ
|
||||
(please refer to :ref:`daq_setup` for details). Defaults to ``2.5``.
|
||||
:daq_dv_range: Specifies the voltage range for the resistor voltage channel on
|
||||
the DAQ (please refer to :ref:`daq_setup` for details).
|
||||
Defaults to ``0.2``.
|
||||
:daq_sampling_rate: DAQ sampling rate. DAQ will take this many samples each
|
||||
second. Please note that this maybe limitted by your DAQ model
|
||||
and then number of ports you're measuring (again, see
|
||||
:ref:`daq_setup`). Defaults to ``10000``.
|
||||
:daq_channel_map: 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.
|
||||
|
||||
"""
|
||||
|
||||
parameters = [
|
||||
Parameter('server_host', kind=str, default='localhost',
|
||||
global_alias='daq_server_host',
|
||||
description='The host address of the machine that runs the daq Server which the '
|
||||
'insturment communicates with.'),
|
||||
Parameter('server_port', kind=int, default=45677,
|
||||
global_alias='daq_server_port',
|
||||
description='The port number for daq Server in which daq insturment communicates '
|
||||
'with.'),
|
||||
Parameter('device_id', kind=str, default='Dev1',
|
||||
global_alias='daq_device_id',
|
||||
description='The ID under which the DAQ is registered with the driver.'),
|
||||
Parameter('v_range', kind=float, default=2.5,
|
||||
global_alias='daq_v_range',
|
||||
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=float, default=0.2,
|
||||
global_alias='daq_dv_range',
|
||||
description='Specifies the voltage range for the resistor voltage channel on '
|
||||
'the DAQ (please refer to :ref:`daq_setup` for details).'),
|
||||
Parameter('sampling_rate', kind=int, default=10000,
|
||||
global_alias='daq_sampling_rate',
|
||||
description='DAQ sampling rate. DAQ will take this many samples each '
|
||||
'second. Please note that this maybe limitted by your DAQ model '
|
||||
'and then number of ports you\'re measuring (again, see '
|
||||
':ref:`daq_setup`)'),
|
||||
Parameter('resistor_values', kind=list, mandatory=True,
|
||||
global_alias='daq_resistor_values',
|
||||
description='The values of resistors (in Ohms) across which the voltages are measured on '
|
||||
'each port.'),
|
||||
Parameter('channel_map', kind=list_of_ints, default=(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23),
|
||||
global_alias='daq_channel_map',
|
||||
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.'),
|
||||
Parameter('labels', kind=list_of_strs,
|
||||
global_alias='daq_labels',
|
||||
description='List of port labels. If specified, the lenght of the list must match '
|
||||
'the length of ``resistor_values``. Defaults to "PORT_<pnum>", where '
|
||||
'"pnum" is the number of the port.'),
|
||||
Parameter('negative_samples', default='keep', allowed_values=['keep', 'zero', 'drop', 'abs'],
|
||||
global_alias='daq_negative_samples',
|
||||
description="""
|
||||
Specifies how negative power samples should be handled. The following
|
||||
methods are possible:
|
||||
|
||||
:keep: keep them as they are
|
||||
:zero: turn negative values to zero
|
||||
:drop: drop samples if they contain negative values. *warning:* this may result in
|
||||
port files containing different numbers of samples
|
||||
:abs: take the absoulte value of negave samples
|
||||
|
||||
"""),
|
||||
Parameter('gpio_sync', kind=int, constraint=lambda x: x > 0,
|
||||
description="""
|
||||
If specified, the instrument will simultaneously set the
|
||||
specified GPIO pin high and put a marker into ftrace. This is
|
||||
to facillitate syncing kernel trace events to DAQ power
|
||||
trace.
|
||||
"""),
|
||||
Parameter('merge_channels', kind=dict_or_bool, default=False,
|
||||
description="""
|
||||
If set to ``True``, channels with consecutive letter suffixes will be summed.
|
||||
e.g. If you have channels A7a, A7b, A7c, A15a, A15b they will be summed to A7, A15
|
||||
|
||||
You can also manually specify the name of channels to be merged and the name of the
|
||||
result like so:
|
||||
|
||||
merge_channels:
|
||||
A15: [A15dvfs, A15ram]
|
||||
NonCPU: [GPU, RoS, Mem]
|
||||
|
||||
In the above exaples the DAQ channels labeled A15a and A15b will be summed together
|
||||
with the results being saved as 'channel' ''a''. A7, GPU and RoS will be summed to 'c'
|
||||
""")
|
||||
]
|
||||
|
||||
def initialize(self, context):
|
||||
status, devices = self._execute_command('list_devices')
|
||||
if status == daq.Status.OK and not devices:
|
||||
raise InstrumentError('DAQ: server did not report any devices registered with the driver.')
|
||||
self._results = OrderedDict()
|
||||
self.gpio_path = None
|
||||
if self.gpio_sync:
|
||||
if not self.device.file_exists(GPIO_ROOT):
|
||||
raise InstrumentError('GPIO sysfs not enabled on the device.')
|
||||
try:
|
||||
export_path = self.device.path.join(GPIO_ROOT, 'export')
|
||||
self.device.write_value(export_path, self.gpio_sync, verify=False)
|
||||
pin_root = self.device.path.join(GPIO_ROOT, 'gpio{}'.format(self.gpio_sync))
|
||||
direction_path = self.device.path.join(pin_root, 'direction')
|
||||
self.device.write_value(direction_path, 'out')
|
||||
self.gpio_path = self.device.path.join(pin_root, 'value')
|
||||
self.device.write_value(self.gpio_path, 0, verify=False)
|
||||
signal.connect(self.insert_start_marker, signal.BEFORE_WORKLOAD_EXECUTION, priority=11)
|
||||
signal.connect(self.insert_stop_marker, signal.AFTER_WORKLOAD_EXECUTION, priority=11)
|
||||
except DeviceError as e:
|
||||
raise InstrumentError('Could not configure GPIO on device: {}'.format(e))
|
||||
|
||||
def setup(self, context):
|
||||
self.logger.debug('Initialising session.')
|
||||
self._execute_command('configure', config=self.device_config)
|
||||
|
||||
def slow_start(self, context):
|
||||
self.logger.debug('Starting collecting measurements.')
|
||||
self._execute_command('start')
|
||||
|
||||
def slow_stop(self, context):
|
||||
self.logger.debug('Stopping collecting measurements.')
|
||||
self._execute_command('stop')
|
||||
|
||||
def update_result(self, context): # pylint: disable=R0914
|
||||
self.logger.debug('Downloading data files.')
|
||||
output_directory = _d(os.path.join(context.output_directory, 'daq'))
|
||||
self._execute_command('get_data', output_directory=output_directory)
|
||||
|
||||
if self.merge_channels:
|
||||
self._merge_channels(context)
|
||||
|
||||
for entry in os.listdir(output_directory):
|
||||
context.add_iteration_artifact('DAQ_{}'.format(os.path.splitext(entry)[0]),
|
||||
path=os.path.join('daq', entry),
|
||||
kind='data',
|
||||
description='DAQ power measurments.')
|
||||
port = os.path.splitext(entry)[0]
|
||||
path = os.path.join(output_directory, entry)
|
||||
key = (context.spec.id, context.spec.label, context.current_iteration)
|
||||
if key not in self._results:
|
||||
self._results[key] = {}
|
||||
|
||||
temp_file = os.path.join(tempfile.gettempdir(), entry)
|
||||
writer, wfh = None, None
|
||||
|
||||
with open(path) as fh:
|
||||
if self.negative_samples != 'keep':
|
||||
wfh = open(temp_file, 'wb')
|
||||
writer = csv.writer(wfh)
|
||||
|
||||
reader = csv.reader(fh)
|
||||
metrics = reader.next()
|
||||
if writer:
|
||||
writer.writerow(metrics)
|
||||
self._metrics |= set(metrics)
|
||||
|
||||
rows = _get_rows(reader, writer, self.negative_samples)
|
||||
data = zip(*rows)
|
||||
|
||||
if writer:
|
||||
wfh.close()
|
||||
shutil.move(temp_file, os.path.join(output_directory, entry))
|
||||
|
||||
n = len(data[0])
|
||||
means = [s / n for s in map(sum, data)]
|
||||
for metric, value in zip(metrics, means):
|
||||
metric_name = '{}_{}'.format(port, metric)
|
||||
context.result.add_metric(metric_name, round(value, 3), UNITS[metric])
|
||||
self._results[key][metric_name] = round(value, 3)
|
||||
energy = sum(data[metrics.index('power')]) * (self.sampling_rate / 1000000)
|
||||
context.result.add_metric('{}_energy'.format(port), round(energy, 3), UNITS['energy'])
|
||||
|
||||
def teardown(self, context):
|
||||
self.logger.debug('Terminating session.')
|
||||
self._execute_command('close')
|
||||
|
||||
def finalize(self, context):
|
||||
if self.gpio_path:
|
||||
unexport_path = self.device.path.join(GPIO_ROOT, 'unexport')
|
||||
self.device.write_value(unexport_path, self.gpio_sync, verify=False)
|
||||
|
||||
def validate(self): # pylint: disable=too-many-branches
|
||||
if not daq:
|
||||
raise ImportError(import_error_mesg)
|
||||
self._results = None
|
||||
self._metrics = set()
|
||||
if self.labels:
|
||||
if len(self.labels) != len(self.resistor_values):
|
||||
raise ConfigError('Number of DAQ port labels does not match the number of resistor values.')
|
||||
else:
|
||||
self.labels = ['PORT_{}'.format(i) for i, _ in enumerate(self.resistor_values)]
|
||||
self.server_config = ServerConfiguration(host=self.server_host,
|
||||
port=self.server_port)
|
||||
self.device_config = DeviceConfiguration(device_id=self.device_id,
|
||||
v_range=self.v_range,
|
||||
dv_range=self.dv_range,
|
||||
sampling_rate=self.sampling_rate,
|
||||
resistor_values=self.resistor_values,
|
||||
channel_map=self.channel_map,
|
||||
labels=self.labels)
|
||||
try:
|
||||
self.server_config.validate()
|
||||
self.device_config.validate()
|
||||
except ConfigurationError, ex:
|
||||
raise ConfigError('DAQ configuration: ' + ex.message) # Re-raise as a WA error
|
||||
self.grouped_suffixes = defaultdict(str)
|
||||
if isinstance(self.merge_channels, bool):
|
||||
if self.merge_channels:
|
||||
# Create a dict of potential prefixes and a list of their suffixes
|
||||
grouped_suffixes = {label[:-1]: label for label in sorted(self.labels) if len(label) > 1}
|
||||
# Only merge channels if more than one channel has the same prefix and the prefixes
|
||||
# are consecutive letters starting with 'a'.
|
||||
self.label_map = {}
|
||||
for channel, suffixes in grouped_suffixes.iteritems():
|
||||
if len(suffixes) > 1:
|
||||
if "".join([s[-1] for s in suffixes]) in ascii_lowercase[:len(suffixes)]:
|
||||
self.label_map[channel] = suffixes
|
||||
|
||||
elif isinstance(self.merge_channels, dict):
|
||||
# Check if given channel names match labels
|
||||
for old_names in self.merge_channels.values():
|
||||
for name in old_names:
|
||||
if name not in self.labels:
|
||||
raise ConfigError("No channel with label {} specified".format(name))
|
||||
self.label_map = self.merge_channels # pylint: disable=redefined-variable-type
|
||||
self.merge_channels = True
|
||||
else: # Should never reach here
|
||||
raise AssertionError("``merge_channels`` is of invalid type")
|
||||
|
||||
def before_overall_results_processing(self, context):
|
||||
if self._results:
|
||||
headers = ['id', 'workload', 'iteration']
|
||||
metrics = ['{}_{}'.format(p, m) for p in self.labels for m in sorted(self._metrics)]
|
||||
headers += metrics
|
||||
rows = [headers]
|
||||
for key, value in self._results.iteritems():
|
||||
rows.append(list(key) + [value[m] for m in metrics])
|
||||
|
||||
outfile = os.path.join(context.output_directory, 'daq_power.csv')
|
||||
with open(outfile, 'wb') as fh:
|
||||
writer = csv.writer(fh)
|
||||
writer.writerows(rows)
|
||||
|
||||
def insert_start_marker(self, context):
|
||||
if self.gpio_path:
|
||||
command = 'echo DAQ_START_MARKER > {}; echo 1 > {}'.format(TRACE_MARKER_PATH, self.gpio_path)
|
||||
self.device.execute(command, as_root=self.device.is_rooted)
|
||||
|
||||
def insert_stop_marker(self, context):
|
||||
if self.gpio_path:
|
||||
command = 'echo DAQ_STOP_MARKER > {}; echo 0 > {}'.format(TRACE_MARKER_PATH, self.gpio_path)
|
||||
self.device.execute(command, as_root=self.device.is_rooted)
|
||||
|
||||
def _execute_command(self, command, **kwargs):
|
||||
# pylint: disable=E1101
|
||||
q = Queue()
|
||||
p = Process(target=_send_daq_command, args=(q, self.server_config, command), kwargs=kwargs)
|
||||
p.start()
|
||||
result = q.get()
|
||||
p.join()
|
||||
if result.status == daq.Status.OK:
|
||||
pass # all good
|
||||
elif result.status == daq.Status.OKISH:
|
||||
self.logger.debug(result.message)
|
||||
elif result.status == daq.Status.ERROR:
|
||||
raise InstrumentError('DAQ: {}'.format(result.message))
|
||||
else:
|
||||
raise InstrumentError('DAQ: Unexpected result: {} - {}'.format(result.status, result.message))
|
||||
return (result.status, result.data)
|
||||
|
||||
def _merge_channels(self, context): # pylint: disable=r0914
|
||||
output_directory = _d(os.path.join(context.output_directory, 'daq'))
|
||||
for name, labels in self.label_map.iteritems():
|
||||
summed = None
|
||||
for label in labels:
|
||||
path = os.path.join(output_directory, "{}.csv".format(label))
|
||||
with open(path) as fh:
|
||||
reader = csv.reader(fh)
|
||||
metrics = reader.next()
|
||||
rows = _get_rows(reader, None, self.negative_samples)
|
||||
if summed:
|
||||
summed = [[x + y for x, y in zip(a, b)] for a, b in zip(rows, summed)]
|
||||
else:
|
||||
summed = rows
|
||||
output_path = os.path.join(output_directory, "{}.csv".format(name))
|
||||
with open(output_path, 'wb') as wfh:
|
||||
writer = csv.writer(wfh)
|
||||
writer.writerow(metrics)
|
||||
for row in summed:
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
def _send_daq_command(q, *args, **kwargs):
|
||||
result = daq.execute_command(*args, **kwargs)
|
||||
q.put(result)
|
||||
|
||||
|
||||
def _get_rows(reader, writer, negative_samples):
|
||||
rows = []
|
||||
for row in reader:
|
||||
row = map(float, row)
|
||||
if negative_samples == 'keep':
|
||||
rows.append(row)
|
||||
elif negative_samples == 'zero':
|
||||
def nonneg(v):
|
||||
return v if v >= 0 else 0
|
||||
rows.append([nonneg(v) for v in row])
|
||||
elif negative_samples == 'drop':
|
||||
if all(v >= 0 for v in row):
|
||||
rows.append(row)
|
||||
elif negative_samples == 'abs':
|
||||
rows.append([abs(v) for v in row])
|
||||
else:
|
||||
raise AssertionError(negative_samples) # should never get here
|
||||
if writer:
|
||||
writer.writerow(row)
|
||||
return rows
|
@ -1,199 +0,0 @@
|
||||
# Copyright 2013-2015 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,E0203,W0201
|
||||
import time
|
||||
|
||||
from wlauto import Instrument, Parameter
|
||||
from wlauto.exceptions import ConfigError, InstrumentError
|
||||
from wlauto.utils.types import boolean
|
||||
|
||||
|
||||
class DelayInstrument(Instrument):
|
||||
|
||||
name = 'delay'
|
||||
description = """
|
||||
This instrument introduces a delay before executing either an iteration
|
||||
or all iterations for a spec.
|
||||
|
||||
The delay may be specified as either a fixed period or a temperature
|
||||
threshold that must be reached.
|
||||
|
||||
Optionally, if an active cooling solution is employed to speed up temperature drop between
|
||||
runs, it may be controlled using this instrument.
|
||||
|
||||
"""
|
||||
|
||||
parameters = [
|
||||
Parameter('temperature_file', default='/sys/devices/virtual/thermal/thermal_zone0/temp',
|
||||
global_alias='thermal_temp_file',
|
||||
description="""Full path to the sysfile on the device that contains the device's
|
||||
temperature."""),
|
||||
Parameter('temperature_timeout', kind=int, default=600,
|
||||
global_alias='thermal_timeout',
|
||||
description="""
|
||||
The timeout after which the instrument will stop waiting even if the specified threshold
|
||||
temperature is not reached. If this timeout is hit, then a warning will be logged stating
|
||||
the actual temperature at which the timeout has ended.
|
||||
"""),
|
||||
Parameter('temperature_poll_period', kind=int, default=5,
|
||||
global_alias='thermal_sleep_time',
|
||||
description="""How long to sleep (in seconds) between polling current device temperature."""),
|
||||
Parameter('temperature_between_specs', kind=int, default=None,
|
||||
global_alias='thermal_threshold_between_specs',
|
||||
description="""
|
||||
Temperature (in device-specific units) the device must cool down to before
|
||||
the iteration spec will be run.
|
||||
|
||||
.. note:: This cannot be specified at the same time as ``fixed_between_specs``
|
||||
|
||||
"""),
|
||||
Parameter('temperature_between_iterations', kind=int, default=None,
|
||||
global_alias='thermal_threshold_between_iterations',
|
||||
description="""
|
||||
Temperature (in device-specific units) the device must cool down to before
|
||||
the next spec will be run.
|
||||
|
||||
.. note:: This cannot be specified at the same time as ``fixed_between_iterations``
|
||||
|
||||
"""),
|
||||
Parameter('temperature_before_start', kind=int, default=None,
|
||||
global_alias='thermal_threshold_before_start',
|
||||
description="""
|
||||
Temperature (in device-specific units) the device must cool down to just before
|
||||
the actual workload execution (after setup has been performed).
|
||||
|
||||
.. note:: This cannot be specified at the same time as ``fixed_between_iterations``
|
||||
|
||||
"""),
|
||||
Parameter('fixed_between_specs', kind=int, default=None,
|
||||
global_alias='fixed_delay_between_specs',
|
||||
description="""
|
||||
How long to sleep (in seconds) after all iterations for a workload spec have
|
||||
executed.
|
||||
|
||||
.. note:: This cannot be specified at the same time as ``temperature_between_specs``
|
||||
|
||||
"""),
|
||||
Parameter('fixed_between_iterations', kind=int, default=None,
|
||||
global_alias='fixed_delay_between_iterations',
|
||||
description="""
|
||||
How long to sleep (in seconds) after each iterations for a workload spec has
|
||||
executed.
|
||||
|
||||
.. note:: This cannot be specified at the same time as ``temperature_between_iterations``
|
||||
|
||||
"""),
|
||||
Parameter('fixed_before_start', kind=int, default=None,
|
||||
global_alias='fixed_delay_before_start',
|
||||
description="""
|
||||
|
||||
How long to sleep (in seconds) after setup for an iteration has been perfromed but
|
||||
before running the workload.
|
||||
|
||||
.. note:: This cannot be specified at the same time as ``temperature_before_start``
|
||||
|
||||
"""),
|
||||
Parameter('active_cooling', kind=boolean, default=False,
|
||||
global_alias='thermal_active_cooling',
|
||||
description="""
|
||||
This instrument supports an active cooling solution while waiting for the device temperature
|
||||
to drop to the threshold. The solution involves an mbed controlling a fan. The mbed is signaled
|
||||
over a serial port. If this solution is present in the setup, this should be set to ``True``.
|
||||
"""),
|
||||
]
|
||||
|
||||
def initialize(self, context):
|
||||
if self.temperature_between_iterations == 0:
|
||||
temp = self.device.get_sysfile_value(self.temperature_file, int)
|
||||
self.logger.debug('Setting temperature threshold between iterations to {}'.format(temp))
|
||||
self.temperature_between_iterations = temp
|
||||
if self.temperature_between_specs == 0:
|
||||
temp = self.device.get_sysfile_value(self.temperature_file, int)
|
||||
self.logger.debug('Setting temperature threshold between workload specs to {}'.format(temp))
|
||||
self.temperature_between_specs = temp
|
||||
|
||||
def very_slow_on_iteration_start(self, context):
|
||||
if self.active_cooling:
|
||||
self.device.stop_active_cooling()
|
||||
if self.fixed_between_iterations:
|
||||
self.logger.debug('Waiting for a fixed period after iteration...')
|
||||
time.sleep(self.fixed_between_iterations)
|
||||
elif self.temperature_between_iterations:
|
||||
self.logger.debug('Waiting for temperature drop before iteration...')
|
||||
self.wait_for_temperature(self.temperature_between_iterations)
|
||||
|
||||
def very_slow_on_spec_start(self, context):
|
||||
if self.active_cooling:
|
||||
self.device.stop_active_cooling()
|
||||
if self.fixed_between_specs:
|
||||
self.logger.debug('Waiting for a fixed period after spec execution...')
|
||||
time.sleep(self.fixed_between_specs)
|
||||
elif self.temperature_between_specs:
|
||||
self.logger.debug('Waiting for temperature drop before spec execution...')
|
||||
self.wait_for_temperature(self.temperature_between_specs)
|
||||
|
||||
def very_slow_start(self, context):
|
||||
if self.active_cooling:
|
||||
self.device.stop_active_cooling()
|
||||
if self.fixed_before_start:
|
||||
self.logger.debug('Waiting for a fixed period after iteration...')
|
||||
time.sleep(self.fixed_before_start)
|
||||
elif self.temperature_before_start:
|
||||
self.logger.debug('Waiting for temperature drop before commencing execution...')
|
||||
self.wait_for_temperature(self.temperature_before_start)
|
||||
|
||||
def wait_for_temperature(self, temperature):
|
||||
if self.active_cooling:
|
||||
self.device.start_active_cooling()
|
||||
self.do_wait_for_temperature(temperature)
|
||||
self.device.stop_active_cooling()
|
||||
else:
|
||||
self.do_wait_for_temperature(temperature)
|
||||
|
||||
def do_wait_for_temperature(self, temperature):
|
||||
reading = self.device.get_sysfile_value(self.temperature_file, int)
|
||||
waiting_start_time = time.time()
|
||||
while reading > temperature:
|
||||
self.logger.debug('Device temperature: {}'.format(reading))
|
||||
if time.time() - waiting_start_time > self.temperature_timeout:
|
||||
self.logger.warning('Reached timeout; current temperature: {}'.format(reading))
|
||||
break
|
||||
time.sleep(self.temperature_poll_period)
|
||||
reading = self.device.get_sysfile_value(self.temperature_file, int)
|
||||
|
||||
def validate(self):
|
||||
if (self.temperature_between_specs is not None and
|
||||
self.fixed_between_specs is not None):
|
||||
raise ConfigError('Both fixed delay and thermal threshold specified for specs.')
|
||||
|
||||
if (self.temperature_between_iterations is not None and
|
||||
self.fixed_between_iterations is not None):
|
||||
raise ConfigError('Both fixed delay and thermal threshold specified for iterations.')
|
||||
|
||||
if (self.temperature_before_start is not None and
|
||||
self.fixed_before_start is not None):
|
||||
raise ConfigError('Both fixed delay and thermal threshold specified before start.')
|
||||
|
||||
if not any([self.temperature_between_specs, self.fixed_between_specs, self.temperature_before_start,
|
||||
self.temperature_between_iterations, self.fixed_between_iterations,
|
||||
self.fixed_before_start]):
|
||||
raise ConfigError('delay instrument is enabled, but no delay is specified.')
|
||||
|
||||
if self.active_cooling and not self.device.has('active_cooling'):
|
||||
message = 'Your device does not support active cooling. Did you configure it with an approprite module?'
|
||||
raise InstrumentError(message)
|
||||
|
@ -1,62 +0,0 @@
|
||||
# Copyright 2014-2015 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.
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from wlauto import Instrument, Parameter
|
||||
from wlauto.utils.misc import ensure_file_directory_exists as _f
|
||||
|
||||
|
||||
class DmesgInstrument(Instrument):
|
||||
# pylint: disable=no-member,attribute-defined-outside-init
|
||||
"""
|
||||
Collected dmesg output before and during the run.
|
||||
|
||||
"""
|
||||
|
||||
name = 'dmesg'
|
||||
|
||||
parameters = [
|
||||
Parameter('loglevel', kind=int, allowed_values=range(8),
|
||||
description='Set loglevel for console output.')
|
||||
]
|
||||
|
||||
loglevel_file = '/proc/sys/kernel/printk'
|
||||
|
||||
def setup(self, context):
|
||||
if self.loglevel:
|
||||
self.old_loglevel = self.device.get_sysfile_value(self.loglevel_file)
|
||||
self.device.write_value(self.loglevel_file, self.loglevel, verify=False)
|
||||
self.before_file = _f(os.path.join(context.output_directory, 'dmesg', 'before'))
|
||||
self.after_file = _f(os.path.join(context.output_directory, 'dmesg', 'after'))
|
||||
|
||||
def slow_start(self, context):
|
||||
with open(self.before_file, 'w') as wfh:
|
||||
wfh.write(self.device.execute('dmesg'))
|
||||
context.add_artifact('dmesg_before', self.before_file, kind='data')
|
||||
if self.device.is_rooted:
|
||||
self.device.execute('dmesg -c', as_root=True)
|
||||
|
||||
def slow_stop(self, context):
|
||||
with open(self.after_file, 'w') as wfh:
|
||||
wfh.write(self.device.execute('dmesg'))
|
||||
context.add_artifact('dmesg_after', self.after_file, kind='data')
|
||||
|
||||
def teardown(self, context): # pylint: disable=unused-argument
|
||||
if self.loglevel:
|
||||
self.device.write_value(self.loglevel_file, self.old_loglevel, verify=False)
|
||||
|
||||
|
@ -1,850 +0,0 @@
|
||||
# Copyright 2015 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=attribute-defined-outside-init,access-member-before-definition,redefined-outer-name
|
||||
from __future__ import division
|
||||
import os
|
||||
import math
|
||||
import time
|
||||
from tempfile import mktemp
|
||||
from base64 import b64encode
|
||||
from collections import Counter, namedtuple
|
||||
|
||||
try:
|
||||
import jinja2
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
matplotlib.use('AGG')
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
low_filter = np.vectorize(lambda x: x > 0 and x or 0) # pylint: disable=no-member
|
||||
import_error = None
|
||||
except ImportError as e:
|
||||
import_error = e
|
||||
jinja2 = None
|
||||
pd = None
|
||||
plt = None
|
||||
np = None
|
||||
low_filter = None
|
||||
|
||||
from wlauto import Instrument, Parameter, File
|
||||
from wlauto.exceptions import ConfigError, InstrumentError, DeviceError
|
||||
from wlauto.instrumentation import instrument_is_installed
|
||||
from wlauto.utils.types import caseless_string, list_or_caseless_string, list_of_ints
|
||||
from wlauto.utils.misc import list_to_mask
|
||||
|
||||
FREQ_TABLE_FILE = 'frequency_power_perf_data.csv'
|
||||
CPUS_TABLE_FILE = 'projected_cap_power.csv'
|
||||
MEASURED_CPUS_TABLE_FILE = 'measured_cap_power.csv'
|
||||
IDLE_TABLE_FILE = 'idle_power_perf_data.csv'
|
||||
REPORT_TEMPLATE_FILE = 'report.template'
|
||||
EM_TEMPLATE_FILE = 'em.template'
|
||||
|
||||
IdlePowerState = namedtuple('IdlePowerState', ['power'])
|
||||
CapPowerState = namedtuple('CapPowerState', ['cap', 'power'])
|
||||
|
||||
|
||||
class EnergyModel(object):
|
||||
|
||||
def __init__(self):
|
||||
self.big_cluster_idle_states = []
|
||||
self.little_cluster_idle_states = []
|
||||
self.big_cluster_cap_states = []
|
||||
self.little_cluster_cap_states = []
|
||||
self.big_core_idle_states = []
|
||||
self.little_core_idle_states = []
|
||||
self.big_core_cap_states = []
|
||||
self.little_core_cap_states = []
|
||||
|
||||
def add_cap_entry(self, cluster, perf, clust_pow, core_pow):
|
||||
if cluster == 'big':
|
||||
self.big_cluster_cap_states.append(CapPowerState(perf, clust_pow))
|
||||
self.big_core_cap_states.append(CapPowerState(perf, core_pow))
|
||||
elif cluster == 'little':
|
||||
self.little_cluster_cap_states.append(CapPowerState(perf, clust_pow))
|
||||
self.little_core_cap_states.append(CapPowerState(perf, core_pow))
|
||||
else:
|
||||
raise ValueError('Unexpected cluster: {}'.format(cluster))
|
||||
|
||||
def add_cluster_idle(self, cluster, values):
|
||||
for value in values:
|
||||
if cluster == 'big':
|
||||
self.big_cluster_idle_states.append(IdlePowerState(value))
|
||||
elif cluster == 'little':
|
||||
self.little_cluster_idle_states.append(IdlePowerState(value))
|
||||
else:
|
||||
raise ValueError('Unexpected cluster: {}'.format(cluster))
|
||||
|
||||
def add_core_idle(self, cluster, values):
|
||||
for value in values:
|
||||
if cluster == 'big':
|
||||
self.big_core_idle_states.append(IdlePowerState(value))
|
||||
elif cluster == 'little':
|
||||
self.little_core_idle_states.append(IdlePowerState(value))
|
||||
else:
|
||||
raise ValueError('Unexpected cluster: {}'.format(cluster))
|
||||
|
||||
|
||||
class PowerPerformanceAnalysis(object):
|
||||
|
||||
def __init__(self, data):
|
||||
self.summary = {}
|
||||
big_freqs = data[data.cluster == 'big'].frequency.unique()
|
||||
little_freqs = data[data.cluster == 'little'].frequency.unique()
|
||||
self.summary['frequency'] = max(set(big_freqs).intersection(set(little_freqs)))
|
||||
|
||||
big_sc = data[(data.cluster == 'big') &
|
||||
(data.frequency == self.summary['frequency']) &
|
||||
(data.cpus == 1)]
|
||||
little_sc = data[(data.cluster == 'little') &
|
||||
(data.frequency == self.summary['frequency']) &
|
||||
(data.cpus == 1)]
|
||||
self.summary['performance_ratio'] = big_sc.performance.item() / little_sc.performance.item()
|
||||
self.summary['power_ratio'] = big_sc.power.item() / little_sc.power.item()
|
||||
self.summary['max_performance'] = data[data.cpus == 1].performance.max()
|
||||
self.summary['max_power'] = data[data.cpus == 1].power.max()
|
||||
|
||||
|
||||
def build_energy_model(freq_power_table, cpus_power, idle_power, first_cluster_idle_state):
|
||||
# pylint: disable=too-many-locals
|
||||
em = EnergyModel()
|
||||
idle_power_sc = idle_power[idle_power.cpus == 1]
|
||||
perf_data = get_normalized_single_core_data(freq_power_table)
|
||||
|
||||
for cluster in ['little', 'big']:
|
||||
cluster_cpus_power = cpus_power[cluster].dropna()
|
||||
cluster_power = cluster_cpus_power['cluster'].apply(int)
|
||||
core_power = (cluster_cpus_power['1'] - cluster_power).apply(int)
|
||||
performance = (perf_data[perf_data.cluster == cluster].performance_norm * 1024 / 100).apply(int)
|
||||
for perf, clust_pow, core_pow in zip(performance, cluster_power, core_power):
|
||||
em.add_cap_entry(cluster, perf, clust_pow, core_pow)
|
||||
|
||||
all_idle_power = idle_power_sc[idle_power_sc.cluster == cluster].power.values
|
||||
# CORE idle states
|
||||
# We want the delta of each state w.r.t. the power
|
||||
# consumption of the shallowest one at this level (core_ref)
|
||||
idle_core_power = low_filter(all_idle_power[:first_cluster_idle_state] -
|
||||
all_idle_power[first_cluster_idle_state - 1])
|
||||
# CLUSTER idle states
|
||||
# We want the absolute value of each idle state
|
||||
idle_cluster_power = low_filter(all_idle_power[first_cluster_idle_state - 1:])
|
||||
em.add_cluster_idle(cluster, idle_cluster_power)
|
||||
em.add_core_idle(cluster, idle_core_power)
|
||||
|
||||
return em
|
||||
|
||||
|
||||
def generate_em_c_file(em, big_core, little_core, em_template_file, outfile):
|
||||
with open(em_template_file) as fh:
|
||||
em_template = jinja2.Template(fh.read())
|
||||
em_text = em_template.render(
|
||||
big_core=big_core,
|
||||
little_core=little_core,
|
||||
em=em,
|
||||
)
|
||||
with open(outfile, 'w') as wfh:
|
||||
wfh.write(em_text)
|
||||
return em_text
|
||||
|
||||
|
||||
def generate_report(freq_power_table, measured_cpus_table, cpus_table, idle_power_table, # pylint: disable=unused-argument
|
||||
report_template_file, device_name, em_text, outfile):
|
||||
# pylint: disable=too-many-locals
|
||||
cap_power_analysis = PowerPerformanceAnalysis(freq_power_table)
|
||||
single_core_norm = get_normalized_single_core_data(freq_power_table)
|
||||
cap_power_plot = get_cap_power_plot(single_core_norm)
|
||||
idle_power_plot = get_idle_power_plot(idle_power_table)
|
||||
|
||||
fig, axes = plt.subplots(1, 2)
|
||||
fig.set_size_inches(16, 8)
|
||||
for i, cluster in enumerate(reversed(cpus_table.columns.levels[0])):
|
||||
projected = cpus_table[cluster].dropna(subset=['1'])
|
||||
plot_cpus_table(projected, axes[i], cluster)
|
||||
cpus_plot_data = get_figure_data(fig)
|
||||
|
||||
with open(report_template_file) as fh:
|
||||
report_template = jinja2.Template(fh.read())
|
||||
html = report_template.render(
|
||||
device_name=device_name,
|
||||
freq_power_table=freq_power_table.set_index(['cluster', 'cpus', 'frequency']).to_html(),
|
||||
cap_power_analysis=cap_power_analysis,
|
||||
cap_power_plot=get_figure_data(cap_power_plot),
|
||||
idle_power_table=idle_power_table.set_index(['cluster', 'cpus', 'state']).to_html(),
|
||||
idle_power_plot=get_figure_data(idle_power_plot),
|
||||
cpus_table=cpus_table.to_html(),
|
||||
cpus_plot=cpus_plot_data,
|
||||
em_text=em_text,
|
||||
)
|
||||
with open(outfile, 'w') as wfh:
|
||||
wfh.write(html)
|
||||
return html
|
||||
|
||||
|
||||
def wa_result_to_power_perf_table(df, performance_metric, index):
|
||||
table = df.pivot_table(index=index + ['iteration'],
|
||||
columns='metric', values='value').reset_index()
|
||||
result_mean = table.groupby(index).mean()
|
||||
result_std = table.groupby(index).std()
|
||||
result_std.columns = [c + ' std' for c in result_std.columns]
|
||||
result_count = table.groupby(index).count()
|
||||
result_count.columns = [c + ' count' for c in result_count.columns]
|
||||
count_sqrt = result_count.apply(lambda x: x.apply(math.sqrt))
|
||||
count_sqrt.columns = result_std.columns # match column names for division
|
||||
result_error = 1.96 * result_std / count_sqrt # 1.96 == 95% confidence interval
|
||||
result_error.columns = [c + ' error' for c in result_mean.columns]
|
||||
|
||||
result = pd.concat([result_mean, result_std, result_count, result_error], axis=1)
|
||||
del result['iteration']
|
||||
del result['iteration std']
|
||||
del result['iteration count']
|
||||
del result['iteration error']
|
||||
|
||||
updated_columns = []
|
||||
for column in result.columns:
|
||||
if column == performance_metric:
|
||||
updated_columns.append('performance')
|
||||
elif column == performance_metric + ' std':
|
||||
updated_columns.append('performance_std')
|
||||
elif column == performance_metric + ' error':
|
||||
updated_columns.append('performance_error')
|
||||
else:
|
||||
updated_columns.append(column.replace(' ', '_'))
|
||||
result.columns = updated_columns
|
||||
result = result[sorted(result.columns)]
|
||||
result.reset_index(inplace=True)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_figure_data(fig, fmt='png'):
|
||||
tmp = mktemp()
|
||||
fig.savefig(tmp, format=fmt, bbox_inches='tight')
|
||||
with open(tmp, 'rb') as fh:
|
||||
image_data = b64encode(fh.read())
|
||||
os.remove(tmp)
|
||||
return image_data
|
||||
|
||||
|
||||
def get_normalized_single_core_data(data):
|
||||
finite_power = np.isfinite(data.power) # pylint: disable=no-member
|
||||
finite_perf = np.isfinite(data.performance) # pylint: disable=no-member
|
||||
data_single_core = data[(data.cpus == 1) & finite_perf & finite_power].copy()
|
||||
data_single_core['performance_norm'] = (data_single_core.performance /
|
||||
data_single_core.performance.max() * 100).apply(int)
|
||||
data_single_core['power_norm'] = (data_single_core.power /
|
||||
data_single_core.power.max() * 100).apply(int)
|
||||
return data_single_core
|
||||
|
||||
|
||||
def get_cap_power_plot(data_single_core):
|
||||
big_single_core = data_single_core[(data_single_core.cluster == 'big') &
|
||||
(data_single_core.cpus == 1)]
|
||||
little_single_core = data_single_core[(data_single_core.cluster == 'little') &
|
||||
(data_single_core.cpus == 1)]
|
||||
|
||||
fig, axes = plt.subplots(1, 1, figsize=(12, 8))
|
||||
axes.plot(big_single_core.performance_norm,
|
||||
big_single_core.power_norm,
|
||||
marker='o')
|
||||
axes.plot(little_single_core.performance_norm,
|
||||
little_single_core.power_norm,
|
||||
marker='o')
|
||||
axes.set_xlim(0, 105)
|
||||
axes.set_ylim(0, 105)
|
||||
axes.set_xlabel('Performance (Normalized)')
|
||||
axes.set_ylabel('Power (Normalized)')
|
||||
axes.grid()
|
||||
axes.legend(['big cluster', 'little cluster'], loc=0)
|
||||
return fig
|
||||
|
||||
|
||||
def get_idle_power_plot(df):
|
||||
fig, axes = plt.subplots(1, 2, figsize=(15, 7))
|
||||
for cluster, ax in zip(['little', 'big'], axes):
|
||||
data = df[df.cluster == cluster].pivot_table(index=['state'], columns='cpus', values='power')
|
||||
err = df[df.cluster == cluster].pivot_table(index=['state'], columns='cpus', values='power_error')
|
||||
data.plot(kind='bar', ax=ax, rot=30, yerr=err)
|
||||
ax.set_title('{} cluster'.format(cluster))
|
||||
ax.set_xlim(-1, len(data.columns) - 0.5)
|
||||
ax.set_ylabel('Power (mW)')
|
||||
return fig
|
||||
|
||||
|
||||
def fit_polynomial(s, n):
|
||||
# pylint: disable=no-member
|
||||
coeffs = np.polyfit(s.index, s.values, n)
|
||||
poly = np.poly1d(coeffs)
|
||||
return poly(s.index)
|
||||
|
||||
|
||||
def get_cpus_power_table(data, index, opps, leak_factors): # pylint: disable=too-many-locals
|
||||
# pylint: disable=no-member
|
||||
power_table = data[[index, 'cluster', 'cpus', 'power']].pivot_table(index=index,
|
||||
columns=['cluster', 'cpus'],
|
||||
values='power')
|
||||
bs_power_table = pd.DataFrame(index=power_table.index, columns=power_table.columns)
|
||||
for cluster in power_table.columns.levels[0]:
|
||||
power_table[cluster, 0] = (power_table[cluster, 1] -
|
||||
(power_table[cluster, 2] -
|
||||
power_table[cluster, 1]))
|
||||
bs_power_table.loc[power_table[cluster, 1].notnull(), (cluster, 1)] = fit_polynomial(power_table[cluster, 1].dropna(), 2)
|
||||
bs_power_table.loc[power_table[cluster, 2].notnull(), (cluster, 2)] = fit_polynomial(power_table[cluster, 2].dropna(), 2)
|
||||
|
||||
if opps[cluster] is None:
|
||||
bs_power_table.loc[bs_power_table[cluster, 1].notnull(), (cluster, 0)] = \
|
||||
(2 * power_table[cluster, 1] - power_table[cluster, 2]).values
|
||||
else:
|
||||
voltages = opps[cluster].set_index('frequency').sort_index()
|
||||
leakage = leak_factors[cluster] * 2 * voltages['voltage']**3 / 0.9**3
|
||||
leakage_delta = leakage - leakage[leakage.index[0]]
|
||||
bs_power_table.loc[:, (cluster, 0)] = \
|
||||
(2 * bs_power_table[cluster, 1] + leakage_delta - bs_power_table[cluster, 2])
|
||||
|
||||
# re-order columns and rename colum '0' to 'cluster'
|
||||
power_table = power_table[sorted(power_table.columns,
|
||||
cmp=lambda x, y: cmp(y[0], x[0]) or cmp(x[1], y[1]))]
|
||||
bs_power_table = bs_power_table[sorted(bs_power_table.columns,
|
||||
cmp=lambda x, y: cmp(y[0], x[0]) or cmp(x[1], y[1]))]
|
||||
old_levels = power_table.columns.levels
|
||||
power_table.columns.set_levels([old_levels[0], list(map(str, old_levels[1])[:-1]) + ['cluster']],
|
||||
inplace=True)
|
||||
bs_power_table.columns.set_levels([old_levels[0], list(map(str, old_levels[1])[:-1]) + ['cluster']],
|
||||
inplace=True)
|
||||
return power_table, bs_power_table
|
||||
|
||||
|
||||
def plot_cpus_table(projected, ax, cluster):
|
||||
projected.T.plot(ax=ax, marker='o')
|
||||
ax.set_title('{} cluster'.format(cluster))
|
||||
ax.set_xticklabels(projected.columns)
|
||||
ax.set_xticks(range(0, 5))
|
||||
ax.set_xlim(-0.5, len(projected.columns) - 0.5)
|
||||
ax.set_ylabel('Power (mW)')
|
||||
ax.grid(True)
|
||||
|
||||
|
||||
def opp_table(d):
|
||||
if d is None:
|
||||
return None
|
||||
return pd.DataFrame(d.items(), columns=['frequency', 'voltage'])
|
||||
|
||||
|
||||
class EnergyModelInstrument(Instrument):
|
||||
|
||||
name = 'energy_model'
|
||||
desicription = """
|
||||
Generates a power mode for the device based on specified workload.
|
||||
|
||||
This insturment will execute the workload specified by the agenda (currently, only ``sysbench`` is
|
||||
supported) and will use the resulting performance and power measurments to generate a power mode for
|
||||
the device.
|
||||
|
||||
This instrument requires certain features to be present in the kernel:
|
||||
|
||||
1. cgroups and cpusets must be enabled.
|
||||
2. cpufreq and userspace governor must be enabled.
|
||||
3. cpuidle must be enabled.
|
||||
|
||||
"""
|
||||
|
||||
parameters = [
|
||||
Parameter('device_name', kind=caseless_string,
|
||||
description="""The name of the device to be used in generating the model. If not specified,
|
||||
``device.name`` will be used. """),
|
||||
Parameter('big_core', kind=caseless_string,
|
||||
description="""The name of the "big" core in the big.LITTLE system; must match
|
||||
one of the values in ``device.core_names``. """),
|
||||
Parameter('performance_metric', kind=caseless_string, mandatory=True,
|
||||
description="""Metric to be used as the performance indicator."""),
|
||||
Parameter('power_metric', kind=list_or_caseless_string,
|
||||
description="""Metric to be used as the power indicator. The value may contain a
|
||||
``{core}`` format specifier that will be replaced with names of big
|
||||
and little cores to drive the name of the metric for that cluster.
|
||||
Ether this or ``energy_metric`` must be specified but not both."""),
|
||||
Parameter('energy_metric', kind=list_or_caseless_string,
|
||||
description="""Metric to be used as the energy indicator. The value may contain a
|
||||
``{core}`` format specifier that will be replaced with names of big
|
||||
and little cores to drive the name of the metric for that cluster.
|
||||
this metric will be used to derive power by deviding through by
|
||||
execution time. Either this or ``power_metric`` must be specified, but
|
||||
not both."""),
|
||||
Parameter('power_scaling_factor', kind=float, default=1.0,
|
||||
description="""Power model specfies power in milliWatts. This is a scaling factor that
|
||||
power_metric values will be multiplied by to get milliWatts."""),
|
||||
Parameter('big_frequencies', kind=list_of_ints,
|
||||
description="""List of frequencies to be used for big cores. These frequencies must
|
||||
be supported by the cores. If this is not specified, all available
|
||||
frequencies for the core (as read from cpufreq) will be used."""),
|
||||
Parameter('little_frequencies', kind=list_of_ints,
|
||||
description="""List of frequencies to be used for little cores. These frequencies must
|
||||
be supported by the cores. If this is not specified, all available
|
||||
frequencies for the core (as read from cpufreq) will be used."""),
|
||||
Parameter('idle_workload', kind=str, default='idle',
|
||||
description="Workload to be used while measuring idle power."),
|
||||
Parameter('idle_workload_params', kind=dict, default={},
|
||||
description="Parameter to pass to the idle workload."),
|
||||
Parameter('first_cluster_idle_state', kind=int, default=-1,
|
||||
description='''The index of the first cluster idle state on the device. Previous states
|
||||
are assumed to be core idles. The default is ``-1``, i.e. only the last
|
||||
idle state is assumed to affect the entire cluster.'''),
|
||||
Parameter('no_hotplug', kind=bool, default=False,
|
||||
description='''This options allows running the instrument without hotpluging cores on and off.
|
||||
Disabling hotplugging will most likely produce a less accurate power model.'''),
|
||||
Parameter('num_of_freqs_to_thermal_adjust', kind=int, default=0,
|
||||
description="""The number of frequencies begining from the highest, to be adjusted for
|
||||
the thermal effect."""),
|
||||
Parameter('big_opps', kind=opp_table,
|
||||
description="""OPP table mapping frequency to voltage (kHz --> mV) for the big cluster."""),
|
||||
Parameter('little_opps', kind=opp_table,
|
||||
description="""OPP table mapping frequency to voltage (kHz --> mV) for the little cluster."""),
|
||||
Parameter('big_leakage', kind=int, default=120,
|
||||
description="""
|
||||
Leakage factor for the big cluster (this is specific to a particular core implementation).
|
||||
"""),
|
||||
Parameter('little_leakage', kind=int, default=60,
|
||||
description="""
|
||||
Leakage factor for the little cluster (this is specific to a particular core implementation).
|
||||
"""),
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
if import_error:
|
||||
message = 'energy_model instrument requires pandas, jinja2 and matplotlib Python packages to be installed; got: "{}"'
|
||||
raise InstrumentError(message.format(import_error.message))
|
||||
for capability in ['cgroups', 'cpuidle']:
|
||||
if not self.device.has(capability):
|
||||
message = 'The Device does not appear to support {}; does it have the right module installed?'
|
||||
raise ConfigError(message.format(capability))
|
||||
device_cores = set(self.device.core_names)
|
||||
if (self.power_metric and self.energy_metric) or not (self.power_metric or self.energy_metric):
|
||||
raise ConfigError('Either power_metric or energy_metric must be specified (but not both).')
|
||||
if not device_cores:
|
||||
raise ConfigError('The Device does not appear to have core_names configured.')
|
||||
elif len(device_cores) != 2:
|
||||
raise ConfigError('The Device does not appear to be a big.LITTLE device.')
|
||||
if self.big_core and self.big_core not in self.device.core_names:
|
||||
raise ConfigError('Specified big_core "{}" is in divice {}'.format(self.big_core, self.device.name))
|
||||
if not self.big_core:
|
||||
self.big_core = self.device.core_names[-1] # the last core is usually "big" in existing big.LITTLE devices
|
||||
if not self.device_name:
|
||||
self.device_name = self.device.name
|
||||
if self.num_of_freqs_to_thermal_adjust and not instrument_is_installed('daq'):
|
||||
self.logger.warn('Adjustment for thermal effect requires daq instrument. Disabling adjustment')
|
||||
self.num_of_freqs_to_thermal_adjust = 0
|
||||
|
||||
def initialize(self, context):
|
||||
self.number_of_cpus = {}
|
||||
self.report_template_file = context.resolver.get(File(self, REPORT_TEMPLATE_FILE))
|
||||
self.em_template_file = context.resolver.get(File(self, EM_TEMPLATE_FILE))
|
||||
self.little_core = (set(self.device.core_names) - set([self.big_core])).pop()
|
||||
self.perform_runtime_validation()
|
||||
self.enable_all_cores()
|
||||
self.configure_clusters()
|
||||
self.discover_idle_states()
|
||||
self.disable_thermal_management()
|
||||
self.initialize_job_queue(context)
|
||||
self.initialize_result_tracking()
|
||||
|
||||
def setup(self, context):
|
||||
if not context.spec.label.startswith('idle_'):
|
||||
return
|
||||
for idle_state in self.get_device_idle_states(self.measured_cluster):
|
||||
if idle_state.index > context.spec.idle_state_index:
|
||||
idle_state.disable = 1
|
||||
else:
|
||||
idle_state.disable = 0
|
||||
|
||||
def fast_start(self, context): # pylint: disable=unused-argument
|
||||
self.start_time = time.time()
|
||||
|
||||
def fast_stop(self, context): # pylint: disable=unused-argument
|
||||
self.run_time = time.time() - self.start_time
|
||||
|
||||
def on_iteration_start(self, context):
|
||||
self.setup_measurement(context.spec.cluster)
|
||||
|
||||
def thermal_correction(self, context):
|
||||
if not self.num_of_freqs_to_thermal_adjust or self.num_of_freqs_to_thermal_adjust > len(self.big_frequencies):
|
||||
return 0
|
||||
freqs = self.big_frequencies[-self.num_of_freqs_to_thermal_adjust:]
|
||||
spec = context.result.spec
|
||||
if spec.frequency not in freqs:
|
||||
return 0
|
||||
data_path = os.path.join(context.output_directory, 'daq', '{}.csv'.format(self.big_core))
|
||||
data = pd.read_csv(data_path)['power']
|
||||
return _adjust_for_thermal(data, filt_method=lambda x: pd.rolling_median(x, 1000), thresh=0.9, window=5000)
|
||||
|
||||
# slow to make sure power results have been generated
|
||||
def slow_update_result(self, context): # pylint: disable=too-many-branches
|
||||
spec = context.result.spec
|
||||
cluster = spec.cluster
|
||||
is_freq_iteration = spec.label.startswith('freq_')
|
||||
perf_metric = 0
|
||||
power_metric = 0
|
||||
thermal_adjusted_power = 0
|
||||
if is_freq_iteration and cluster == 'big':
|
||||
thermal_adjusted_power = self.thermal_correction(context)
|
||||
for metric in context.result.metrics:
|
||||
if metric.name == self.performance_metric:
|
||||
perf_metric = metric.value
|
||||
elif thermal_adjusted_power and metric.name in self.big_power_metrics:
|
||||
power_metric += thermal_adjusted_power * self.power_scaling_factor
|
||||
elif (cluster == 'big') and metric.name in self.big_power_metrics:
|
||||
power_metric += metric.value * self.power_scaling_factor
|
||||
elif (cluster == 'little') and metric.name in self.little_power_metrics:
|
||||
power_metric += metric.value * self.power_scaling_factor
|
||||
elif thermal_adjusted_power and metric.name in self.big_energy_metrics:
|
||||
power_metric += thermal_adjusted_power / self.run_time * self.power_scaling_factor
|
||||
elif (cluster == 'big') and metric.name in self.big_energy_metrics:
|
||||
power_metric += metric.value / self.run_time * self.power_scaling_factor
|
||||
elif (cluster == 'little') and metric.name in self.little_energy_metrics:
|
||||
power_metric += metric.value / self.run_time * self.power_scaling_factor
|
||||
|
||||
if not (power_metric and (perf_metric or not is_freq_iteration)):
|
||||
message = 'Incomplete results for {} iteration{}'
|
||||
raise InstrumentError(message.format(context.result.spec.id, context.current_iteration))
|
||||
|
||||
if is_freq_iteration:
|
||||
index_matter = [cluster, spec.num_cpus,
|
||||
spec.frequency, context.result.iteration]
|
||||
data = self.freq_data
|
||||
else:
|
||||
index_matter = [cluster, spec.num_cpus,
|
||||
spec.idle_state_id, spec.idle_state_desc, context.result.iteration]
|
||||
data = self.idle_data
|
||||
if self.no_hotplug:
|
||||
# due to that fact that hotpluging was disabled, power has to be artificially scaled
|
||||
# to the number of cores that should have been active if hotplugging had occurred.
|
||||
power_metric = spec.num_cpus * (power_metric / self.number_of_cpus[cluster])
|
||||
|
||||
data.append(index_matter + ['performance', perf_metric])
|
||||
data.append(index_matter + ['power', power_metric])
|
||||
|
||||
def before_overall_results_processing(self, context):
|
||||
# pylint: disable=too-many-locals
|
||||
if not self.idle_data or not self.freq_data:
|
||||
self.logger.warning('Run aborted early; not generating energy_model.')
|
||||
return
|
||||
output_directory = os.path.join(context.output_directory, 'energy_model')
|
||||
os.makedirs(output_directory)
|
||||
|
||||
df = pd.DataFrame(self.idle_data, columns=['cluster', 'cpus', 'state_id',
|
||||
'state', 'iteration', 'metric', 'value'])
|
||||
idle_power_table = wa_result_to_power_perf_table(df, '', index=['cluster', 'cpus', 'state'])
|
||||
idle_output = os.path.join(output_directory, IDLE_TABLE_FILE)
|
||||
with open(idle_output, 'w') as wfh:
|
||||
idle_power_table.to_csv(wfh, index=False)
|
||||
context.add_artifact('idle_power_table', idle_output, 'export')
|
||||
|
||||
df = pd.DataFrame(self.freq_data,
|
||||
columns=['cluster', 'cpus', 'frequency', 'iteration', 'metric', 'value'])
|
||||
freq_power_table = wa_result_to_power_perf_table(df, self.performance_metric,
|
||||
index=['cluster', 'cpus', 'frequency'])
|
||||
freq_output = os.path.join(output_directory, FREQ_TABLE_FILE)
|
||||
with open(freq_output, 'w') as wfh:
|
||||
freq_power_table.to_csv(wfh, index=False)
|
||||
context.add_artifact('freq_power_table', freq_output, 'export')
|
||||
|
||||
if self.big_opps is None or self.little_opps is None:
|
||||
message = 'OPPs not specified for one or both clusters; cluster power will not be adjusted for leakage.'
|
||||
self.logger.warning(message)
|
||||
opps = {'big': self.big_opps, 'little': self.little_opps}
|
||||
leakages = {'big': self.big_leakage, 'little': self.little_leakage}
|
||||
try:
|
||||
measured_cpus_table, cpus_table = get_cpus_power_table(freq_power_table, 'frequency', opps, leakages)
|
||||
except (ValueError, KeyError, IndexError) as e:
|
||||
self.logger.error('Could not create cpu power tables: {}'.format(e))
|
||||
return
|
||||
measured_cpus_output = os.path.join(output_directory, MEASURED_CPUS_TABLE_FILE)
|
||||
with open(measured_cpus_output, 'w') as wfh:
|
||||
measured_cpus_table.to_csv(wfh)
|
||||
context.add_artifact('measured_cpus_table', measured_cpus_output, 'export')
|
||||
cpus_output = os.path.join(output_directory, CPUS_TABLE_FILE)
|
||||
with open(cpus_output, 'w') as wfh:
|
||||
cpus_table.to_csv(wfh)
|
||||
context.add_artifact('cpus_table', cpus_output, 'export')
|
||||
|
||||
em = build_energy_model(freq_power_table, cpus_table, idle_power_table, self.first_cluster_idle_state)
|
||||
em_file = os.path.join(output_directory, '{}_em.c'.format(self.device_name))
|
||||
em_text = generate_em_c_file(em, self.big_core, self.little_core,
|
||||
self.em_template_file, em_file)
|
||||
context.add_artifact('em', em_file, 'data')
|
||||
|
||||
report_file = os.path.join(output_directory, 'report.html')
|
||||
generate_report(freq_power_table, measured_cpus_table, cpus_table,
|
||||
idle_power_table, self.report_template_file,
|
||||
self.device_name, em_text, report_file)
|
||||
context.add_artifact('pm_report', report_file, 'export')
|
||||
|
||||
def initialize_result_tracking(self):
|
||||
self.freq_data = []
|
||||
self.idle_data = []
|
||||
self.big_power_metrics = []
|
||||
self.little_power_metrics = []
|
||||
self.big_energy_metrics = []
|
||||
self.little_energy_metrics = []
|
||||
if self.power_metric:
|
||||
self.big_power_metrics = [pm.format(core=self.big_core) for pm in self.power_metric]
|
||||
self.little_power_metrics = [pm.format(core=self.little_core) for pm in self.power_metric]
|
||||
else: # must be energy_metric
|
||||
self.big_energy_metrics = [em.format(core=self.big_core) for em in self.energy_metric]
|
||||
self.little_energy_metrics = [em.format(core=self.little_core) for em in self.energy_metric]
|
||||
|
||||
def configure_clusters(self):
|
||||
self.measured_cores = None
|
||||
self.measuring_cores = None
|
||||
self.cpuset = self.device.get_cgroup_controller('cpuset')
|
||||
self.cpuset.create_group('big', self.big_cpus, [0])
|
||||
self.cpuset.create_group('little', self.little_cpus, [0])
|
||||
for cluster in set(self.device.core_clusters):
|
||||
self.device.set_cluster_governor(cluster, 'userspace')
|
||||
|
||||
def discover_idle_states(self):
|
||||
online_cpu = self.device.get_online_cpus(self.big_core)[0]
|
||||
self.big_idle_states = self.device.get_cpuidle_states(online_cpu)
|
||||
online_cpu = self.device.get_online_cpus(self.little_core)[0]
|
||||
self.little_idle_states = self.device.get_cpuidle_states(online_cpu)
|
||||
if not (len(self.big_idle_states) >= 2 and len(self.little_idle_states) >= 2):
|
||||
raise DeviceError('There do not appeart to be at least two idle states '
|
||||
'on at least one of the clusters.')
|
||||
|
||||
def setup_measurement(self, measured):
|
||||
measuring = 'big' if measured == 'little' else 'little'
|
||||
self.measured_cluster = measured
|
||||
self.measuring_cluster = measuring
|
||||
self.measured_cpus = self.big_cpus if measured == 'big' else self.little_cpus
|
||||
self.measuring_cpus = self.little_cpus if measured == 'big' else self.big_cpus
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.enable_all_cores()
|
||||
self.enable_all_idle_states()
|
||||
self.reset_cgroups()
|
||||
self.cpuset.move_all_tasks_to(self.measuring_cluster)
|
||||
server_process = 'adbd' if self.device.os == 'android' else 'sshd'
|
||||
server_pids = self.device.get_pids_of(server_process)
|
||||
children_ps = [e for e in self.device.ps()
|
||||
if e.ppid in server_pids and e.name != 'sshd']
|
||||
children_pids = [e.pid for e in children_ps]
|
||||
pids_to_move = server_pids + children_pids
|
||||
self.cpuset.root.add_tasks(pids_to_move)
|
||||
for pid in pids_to_move:
|
||||
try:
|
||||
self.device.execute('busybox taskset -p 0x{:x} {}'.format(list_to_mask(self.measuring_cpus), pid))
|
||||
except DeviceError:
|
||||
pass
|
||||
|
||||
def enable_all_cores(self):
|
||||
counter = Counter(self.device.core_names)
|
||||
for core, number in counter.iteritems():
|
||||
self.device.set_number_of_online_cpus(core, number)
|
||||
self.big_cpus = self.device.get_online_cpus(self.big_core)
|
||||
self.little_cpus = self.device.get_online_cpus(self.little_core)
|
||||
|
||||
def enable_all_idle_states(self):
|
||||
for cpu in self.device.online_cpus:
|
||||
for state in self.device.get_cpuidle_states(cpu):
|
||||
state.disable = 0
|
||||
|
||||
def reset_cgroups(self):
|
||||
self.big_cpus = self.device.get_online_cpus(self.big_core)
|
||||
self.little_cpus = self.device.get_online_cpus(self.little_core)
|
||||
self.cpuset.big.set(self.big_cpus, 0)
|
||||
self.cpuset.little.set(self.little_cpus, 0)
|
||||
|
||||
def perform_runtime_validation(self):
|
||||
if not self.device.is_rooted:
|
||||
raise InstrumentError('the device must be rooted to generate energy models')
|
||||
if 'userspace' not in self.device.list_available_cluster_governors(0):
|
||||
raise InstrumentError('userspace cpufreq governor must be enabled')
|
||||
|
||||
error_message = 'Frequency {} is not supported by {} cores'
|
||||
available_frequencies = self.device.list_available_core_frequencies(self.big_core)
|
||||
if self.big_frequencies:
|
||||
for freq in self.big_frequencies:
|
||||
if freq not in available_frequencies:
|
||||
raise ConfigError(error_message.format(freq, self.big_core))
|
||||
else:
|
||||
self.big_frequencies = available_frequencies
|
||||
available_frequencies = self.device.list_available_core_frequencies(self.little_core)
|
||||
if self.little_frequencies:
|
||||
for freq in self.little_frequencies:
|
||||
if freq not in available_frequencies:
|
||||
raise ConfigError(error_message.format(freq, self.little_core))
|
||||
else:
|
||||
self.little_frequencies = available_frequencies
|
||||
|
||||
def initialize_job_queue(self, context):
|
||||
old_specs = []
|
||||
for job in context.runner.job_queue:
|
||||
if job.spec not in old_specs:
|
||||
old_specs.append(job.spec)
|
||||
new_specs = self.get_cluster_specs(old_specs, 'big', context)
|
||||
new_specs.extend(self.get_cluster_specs(old_specs, 'little', context))
|
||||
|
||||
# Update config to refect jobs that will actually run.
|
||||
context.config.workload_specs = new_specs
|
||||
config_file = os.path.join(context.host_working_directory, 'run_config.json')
|
||||
with open(config_file, 'wb') as wfh:
|
||||
context.config.serialize(wfh)
|
||||
|
||||
context.runner.init_queue(new_specs)
|
||||
|
||||
def get_cluster_specs(self, old_specs, cluster, context):
|
||||
core = self.get_core_name(cluster)
|
||||
self.number_of_cpus[cluster] = sum([1 for c in self.device.core_names if c == core])
|
||||
|
||||
cluster_frequencies = self.get_frequencies_param(cluster)
|
||||
if not cluster_frequencies:
|
||||
raise InstrumentError('Could not read available frequencies for {}'.format(core))
|
||||
min_frequency = min(cluster_frequencies)
|
||||
|
||||
idle_states = self.get_device_idle_states(cluster)
|
||||
new_specs = []
|
||||
for state in idle_states:
|
||||
for num_cpus in xrange(1, self.number_of_cpus[cluster] + 1):
|
||||
spec = old_specs[0].copy()
|
||||
spec.workload_name = self.idle_workload
|
||||
spec.workload_parameters = self.idle_workload_params
|
||||
spec.idle_state_id = state.id
|
||||
spec.idle_state_desc = state.desc
|
||||
spec.idle_state_index = state.index
|
||||
if not self.no_hotplug:
|
||||
spec.runtime_parameters['{}_cores'.format(core)] = num_cpus
|
||||
spec.runtime_parameters['{}_frequency'.format(core)] = min_frequency
|
||||
spec.runtime_parameters['ui'] = 'off'
|
||||
spec.cluster = cluster
|
||||
spec.num_cpus = num_cpus
|
||||
spec.id = '{}_idle_{}_{}'.format(cluster, state.id, num_cpus)
|
||||
spec.label = 'idle_{}'.format(cluster)
|
||||
spec.number_of_iterations = old_specs[0].number_of_iterations
|
||||
spec.load(self.device, context.config.ext_loader)
|
||||
spec.workload.init_resources(context)
|
||||
spec.workload.validate()
|
||||
new_specs.append(spec)
|
||||
for old_spec in old_specs:
|
||||
if old_spec.workload_name not in ['sysbench', 'dhrystone']:
|
||||
raise ConfigError('Only sysbench and dhrystone workloads currently supported for energy_model generation.')
|
||||
for freq in cluster_frequencies:
|
||||
for num_cpus in xrange(1, self.number_of_cpus[cluster] + 1):
|
||||
spec = old_spec.copy()
|
||||
spec.runtime_parameters['{}_frequency'.format(core)] = freq
|
||||
if not self.no_hotplug:
|
||||
spec.runtime_parameters['{}_cores'.format(core)] = num_cpus
|
||||
spec.runtime_parameters['ui'] = 'off'
|
||||
spec.id = '{}_{}_{}'.format(cluster, num_cpus, freq)
|
||||
spec.label = 'freq_{}_{}'.format(cluster, spec.label)
|
||||
spec.workload_parameters['taskset_mask'] = list_to_mask(self.get_cpus(cluster))
|
||||
spec.workload_parameters['threads'] = num_cpus
|
||||
if old_spec.workload_name == 'sysbench':
|
||||
# max_requests set to an arbitrary high values to make sure
|
||||
# sysbench runs for full duriation even on highly
|
||||
# performant cores.
|
||||
spec.workload_parameters['max_requests'] = 10000000
|
||||
spec.cluster = cluster
|
||||
spec.num_cpus = num_cpus
|
||||
spec.frequency = freq
|
||||
spec.load(self.device, context.config.ext_loader)
|
||||
spec.workload.init_resources(context)
|
||||
spec.workload.validate()
|
||||
new_specs.append(spec)
|
||||
return new_specs
|
||||
|
||||
def disable_thermal_management(self):
|
||||
if self.device.file_exists('/sys/class/thermal/thermal_zone0'):
|
||||
tzone_paths = self.device.execute('ls /sys/class/thermal/thermal_zone*')
|
||||
for tzpath in tzone_paths.strip().split():
|
||||
mode_file = '{}/mode'.format(tzpath)
|
||||
if self.device.file_exists(mode_file):
|
||||
self.device.write_value(mode_file, 'disabled')
|
||||
|
||||
def get_device_idle_states(self, cluster):
|
||||
if cluster == 'big':
|
||||
online_cpus = self.device.get_online_cpus(self.big_core)
|
||||
else:
|
||||
online_cpus = self.device.get_online_cpus(self.little_core)
|
||||
idle_states = []
|
||||
for cpu in online_cpus:
|
||||
idle_states.extend(self.device.get_cpuidle_states(cpu))
|
||||
return idle_states
|
||||
|
||||
def get_core_name(self, cluster):
|
||||
if cluster == 'big':
|
||||
return self.big_core
|
||||
else:
|
||||
return self.little_core
|
||||
|
||||
def get_cpus(self, cluster):
|
||||
if cluster == 'big':
|
||||
return self.big_cpus
|
||||
else:
|
||||
return self.little_cpus
|
||||
|
||||
def get_frequencies_param(self, cluster):
|
||||
if cluster == 'big':
|
||||
return self.big_frequencies
|
||||
else:
|
||||
return self.little_frequencies
|
||||
|
||||
|
||||
def _adjust_for_thermal(data, filt_method=lambda x: x, thresh=0.9, window=5000, tdiff_threshold=10000):
|
||||
n = filt_method(data)
|
||||
n = n[~np.isnan(n)] # pylint: disable=no-member
|
||||
|
||||
d = np.diff(n) # pylint: disable=no-member
|
||||
d = d[~np.isnan(d)] # pylint: disable=no-member
|
||||
dmin = min(d)
|
||||
dmax = max(d)
|
||||
|
||||
index_up = np.max((d > dmax * thresh).nonzero()) # pylint: disable=no-member
|
||||
index_down = np.min((d < dmin * thresh).nonzero()) # pylint: disable=no-member
|
||||
low_average = np.average(n[index_up:index_up + window]) # pylint: disable=no-member
|
||||
high_average = np.average(n[index_down - window:index_down]) # pylint: disable=no-member
|
||||
if low_average > high_average or index_down - index_up < tdiff_threshold:
|
||||
return 0
|
||||
else:
|
||||
return low_average
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys # pylint: disable=wrong-import-position,wrong-import-order
|
||||
indir, outdir = sys.argv[1], sys.argv[2]
|
||||
device_name = 'odroidxu3'
|
||||
big_core = 'a15'
|
||||
little_core = 'a7'
|
||||
first_cluster_idle_state = -1
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
report_template_file = os.path.join(this_dir, REPORT_TEMPLATE_FILE)
|
||||
em_template_file = os.path.join(this_dir, EM_TEMPLATE_FILE)
|
||||
|
||||
freq_power_table = pd.read_csv(os.path.join(indir, FREQ_TABLE_FILE))
|
||||
measured_cpus_table, cpus_table = pd.read_csv(os.path.join(indir, CPUS_TABLE_FILE), # pylint: disable=unbalanced-tuple-unpacking
|
||||
header=range(2), index_col=0)
|
||||
idle_power_table = pd.read_csv(os.path.join(indir, IDLE_TABLE_FILE))
|
||||
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
report_file = os.path.join(outdir, 'report.html')
|
||||
em_file = os.path.join(outdir, '{}_em.c'.format(device_name))
|
||||
|
||||
em = build_energy_model(freq_power_table, cpus_table,
|
||||
idle_power_table, first_cluster_idle_state)
|
||||
em_text = generate_em_c_file(em, big_core, little_core,
|
||||
em_template_file, em_file)
|
||||
generate_report(freq_power_table, measured_cpus_table, cpus_table,
|
||||
idle_power_table, report_template_file, device_name,
|
||||
em_text, report_file)
|
@ -1,51 +0,0 @@
|
||||
static struct idle_state idle_states_cluster_{{ little_core|lower }}[] = {
|
||||
{% for entry in em.little_cluster_idle_states -%}
|
||||
{ .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
static struct idle_state idle_states_cluster_{{ big_core|lower }}[] = {
|
||||
{% for entry in em.big_cluster_idle_states -%}
|
||||
{ .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
static struct capacity_state cap_states_cluster_{{ little_core|lower }}[] = {
|
||||
/* Power per cluster */
|
||||
{% for entry in em.little_cluster_cap_states -%}
|
||||
{ .cap = {{ entry.cap }}, .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
static struct capacity_state cap_states_cluster_{{ big_core|lower }}[] = {
|
||||
/* Power per cluster */
|
||||
{% for entry in em.big_cluster_cap_states -%}
|
||||
{ .cap = {{ entry.cap }}, .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
static struct idle_state idle_states_core_{{ little_core|lower }}[] = {
|
||||
{% for entry in em.little_core_idle_states -%}
|
||||
{ .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
static struct idle_state idle_states_core_{{ big_core|lower }}[] = {
|
||||
{% for entry in em.big_core_idle_states -%}
|
||||
{ .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
static struct capacity_state cap_states_core_{{ little_core|lower }}[] = {
|
||||
/* Power per cpu */
|
||||
{% for entry in em.little_core_cap_states -%}
|
||||
{ .cap = {{ entry.cap }}, .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
static struct capacity_state cap_states_core_{{ big_core|lower }}[] = {
|
||||
/* Power per cpu */
|
||||
{% for entry in em.big_core_cap_states -%}
|
||||
{ .cap = {{ entry.cap }}, .power = {{ entry.power }}, },
|
||||
{% endfor %}
|
||||
};
|
@ -1,123 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
.toggle-box {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-box + label {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
line-height: 21px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.toggle-box + label + div {
|
||||
display: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.toggle-box:checked + label + div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.toggle-box + label:before {
|
||||
background-color: #4F5150;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
color: #FFFFFF;
|
||||
content: "+";
|
||||
display: block;
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-right: 5px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.toggle-box:checked + label:before {
|
||||
content: "\2212";
|
||||
}
|
||||
|
||||
.document {
|
||||
width: 800px;
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="document">
|
||||
<h1 class="title">{{ device_name }} Energy Model Report</h1>
|
||||
|
||||
<h2>Power/Performance Analysis</h2>
|
||||
<div>
|
||||
<h3>Summary</h3>
|
||||
At {{ cap_power_analysis.summary['frequency']|round(2) }} Hz<br />
|
||||
big is {{ cap_power_analysis.summary['performance_ratio']|round(2) }} times faster<br />
|
||||
big consumes {{ cap_power_analysis.summary['power_ratio']|round(2) }} times more power<br />
|
||||
<br />
|
||||
max performance: {{ cap_power_analysis.summary['max_performance']|round(2) }}<br />
|
||||
max power: {{ cap_power_analysis.summary['max_power']|round(2) }}<br />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>Single Core Power/Perfromance Plot</h3>
|
||||
These are the traditional power-performance curves for the single-core runs.
|
||||
<img align="middle" width="600px" src="data:image/png;base64,{{ cap_power_plot }}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input class="toggle-box" id="freq_table" type="checkbox" >
|
||||
<label for="freq_table">Expand view all power/performance data</label>
|
||||
<div>
|
||||
{{ freq_power_table }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>CPUs Power Plot</h3>
|
||||
Each line correspond to the cluster running at a different OPP. Each
|
||||
point corresponds to the average power with a certain number of CPUs
|
||||
executing. To get the contribution of the cluster we have to extend the
|
||||
lines on the left (what it would be the average power of just the cluster).
|
||||
<img align="middle" width="600px" src="data:image/png;base64,{{ cpus_plot }}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input class="toggle-box" id="cpus_table" type="checkbox" >
|
||||
<label for="cpus_table">Expand view CPUS power data</label>
|
||||
<div>
|
||||
{{ cpus_table }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Idle Power</h3>
|
||||
<img align="middle" width="600px" src="data:image/png;base64,{{ idle_power_plot }}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input class="toggle-box" id="idle_power_table" type="checkbox" >
|
||||
<label for="idle_power_table">Expand view idle power data</label>
|
||||
<div>
|
||||
{{ idle_power_table }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- vim: ft=htmljinja
|
||||
-->
|
@ -1,147 +0,0 @@
|
||||
# Copyright 2013-2015 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,access-member-before-definition,attribute-defined-outside-init
|
||||
import os
|
||||
import subprocess
|
||||
import signal
|
||||
import struct
|
||||
import csv
|
||||
try:
|
||||
import pandas
|
||||
except ImportError:
|
||||
pandas = None
|
||||
|
||||
from wlauto import Instrument, Parameter, Executable
|
||||
from wlauto.exceptions import InstrumentError, ConfigError
|
||||
from wlauto.utils.types import list_of_numbers
|
||||
|
||||
|
||||
class EnergyProbe(Instrument):
|
||||
|
||||
name = 'energy_probe'
|
||||
description = """Collects power traces using the ARM energy probe.
|
||||
|
||||
This instrument requires ``caiman`` utility to be installed in the workload automation
|
||||
host and be in the PATH. Caiman is part of DS-5 and should be in ``/path/to/DS-5/bin/`` .
|
||||
Energy probe can simultaneously collect energy from up to 3 power rails.
|
||||
|
||||
To connect the energy probe on a rail, connect the white wire to the pin that is closer to the
|
||||
Voltage source and the black wire to the pin that is closer to the load (the SoC or the device
|
||||
you are probing). Between the pins there should be a shunt resistor of known resistance in the
|
||||
range of 5 to 20 mOhm. The resistance of the shunt resistors is a mandatory parameter
|
||||
``resistor_values``.
|
||||
|
||||
.. note:: This instrument can process results a lot faster if python pandas is installed.
|
||||
"""
|
||||
|
||||
parameters = [
|
||||
Parameter('resistor_values', kind=list_of_numbers, default=[],
|
||||
description="""The value of shunt resistors. This is a mandatory parameter."""),
|
||||
Parameter('labels', kind=list, default=[],
|
||||
description="""Meaningful labels for each of the monitored rails."""),
|
||||
Parameter('device_entry', kind=str, default='/dev/ttyACM0',
|
||||
description="""Path to /dev entry for the energy probe (it should be /dev/ttyACMx)"""),
|
||||
]
|
||||
|
||||
MAX_CHANNELS = 3
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
super(EnergyProbe, self).__init__(device, **kwargs)
|
||||
self.attributes_per_sample = 3
|
||||
self.bytes_per_sample = self.attributes_per_sample * 4
|
||||
self.attributes = ['power', 'voltage', 'current']
|
||||
for i, val in enumerate(self.resistor_values):
|
||||
self.resistor_values[i] = int(1000 * float(val))
|
||||
|
||||
def validate(self):
|
||||
if subprocess.call('which caiman', stdout=subprocess.PIPE, shell=True):
|
||||
raise InstrumentError('caiman not in PATH. Cannot enable energy probe')
|
||||
if not self.resistor_values:
|
||||
raise ConfigError('At least one resistor value must be specified')
|
||||
if len(self.resistor_values) > self.MAX_CHANNELS:
|
||||
raise ConfigError('{} Channels where specified when Energy Probe supports up to {}'
|
||||
.format(len(self.resistor_values), self.MAX_CHANNELS))
|
||||
if pandas is None:
|
||||
self.logger.warning("pandas package will significantly speed up this instrument")
|
||||
self.logger.warning("to install it try: pip install pandas")
|
||||
|
||||
def setup(self, context):
|
||||
if not self.labels:
|
||||
self.labels = ["PORT_{}".format(channel) for channel, _ in enumerate(self.resistor_values)]
|
||||
self.output_directory = os.path.join(context.output_directory, 'energy_probe')
|
||||
rstring = ""
|
||||
for i, rval in enumerate(self.resistor_values):
|
||||
rstring += '-r {}:{} '.format(i, rval)
|
||||
self.command = 'caiman -d {} -l {} {}'.format(self.device_entry, rstring, self.output_directory)
|
||||
os.makedirs(self.output_directory)
|
||||
|
||||
def start(self, context):
|
||||
self.logger.debug(self.command)
|
||||
self.caiman = subprocess.Popen(self.command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
preexec_fn=os.setpgrp,
|
||||
shell=True)
|
||||
|
||||
def stop(self, context):
|
||||
os.killpg(self.caiman.pid, signal.SIGTERM)
|
||||
|
||||
def update_result(self, context): # pylint: disable=too-many-locals
|
||||
num_of_channels = len(self.resistor_values)
|
||||
processed_data = [[] for _ in xrange(num_of_channels)]
|
||||
filenames = [os.path.join(self.output_directory, '{}.csv'.format(label)) for label in self.labels]
|
||||
struct_format = '{}I'.format(num_of_channels * self.attributes_per_sample)
|
||||
not_a_full_row_seen = False
|
||||
with open(os.path.join(self.output_directory, "0000000000"), "rb") as bfile:
|
||||
while True:
|
||||
data = bfile.read(num_of_channels * self.bytes_per_sample)
|
||||
if data == '':
|
||||
break
|
||||
try:
|
||||
unpacked_data = struct.unpack(struct_format, data)
|
||||
except struct.error:
|
||||
if not_a_full_row_seen:
|
||||
self.logger.warn('possibly missaligned caiman raw data, row contained {} bytes'.format(len(data)))
|
||||
continue
|
||||
else:
|
||||
not_a_full_row_seen = True
|
||||
for i in xrange(num_of_channels):
|
||||
index = i * self.attributes_per_sample
|
||||
processed_data[i].append({attr: val for attr, val in
|
||||
zip(self.attributes, unpacked_data[index:index + self.attributes_per_sample])})
|
||||
for i, path in enumerate(filenames):
|
||||
with open(path, 'w') as f:
|
||||
if pandas is not None:
|
||||
self._pandas_produce_csv(processed_data[i], f)
|
||||
else:
|
||||
self._slow_produce_csv(processed_data[i], f)
|
||||
|
||||
# pylint: disable=R0201
|
||||
def _pandas_produce_csv(self, data, f):
|
||||
dframe = pandas.DataFrame(data)
|
||||
dframe = dframe / 1000.0
|
||||
dframe.to_csv(f)
|
||||
|
||||
def _slow_produce_csv(self, data, f):
|
||||
new_data = []
|
||||
for entry in data:
|
||||
new_data.append({key: val / 1000.0 for key, val in entry.items()})
|
||||
writer = csv.DictWriter(f, self.attributes)
|
||||
writer.writeheader()
|
||||
writer.writerows(new_data)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user