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