mirror of
https://github.com/ARM-software/workload-automation.git
synced 2025-01-18 03:56:04 +00:00
Initial commit of open source Workload Automation.
This commit is contained in:
commit
a747ec7e4c
30
.gitignore
vendored
Executable file
30
.gitignore
vendored
Executable file
@ -0,0 +1,30 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
*.bak
|
||||
*.o
|
||||
*.cmd
|
||||
Module.symvers
|
||||
modules.order
|
||||
*~
|
||||
tags
|
||||
build/
|
||||
dist/
|
||||
.ropeproject/
|
||||
wa_output/
|
||||
doc/source/api/
|
||||
doc/source/extensions/
|
||||
MANIFEST
|
||||
wlauto/external/uiautomator/bin/
|
||||
wlauto/external/uiautomator/*.properties
|
||||
wlauto/external/uiautomator/build.xml
|
||||
*.orig
|
||||
local.properties
|
||||
wlauto/external/revent/libs/
|
||||
wlauto/external/revent/obj/
|
||||
wlauto/external/bbench_server/libs/
|
||||
wlauto/external/bbench_server/obj/
|
||||
pmu_logger.mod.c
|
||||
.tmp_versions
|
||||
obj/
|
||||
libs/armeabi
|
||||
wlauto/workloads/*/uiauto/bin/
|
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
recursive-include scripts *
|
||||
recursive-include doc *
|
73
README.rst
Normal file
73
README.rst
Normal file
@ -0,0 +1,73 @@
|
||||
Workload Automation
|
||||
+++++++++++++++++++
|
||||
|
||||
Workload Automation (WA) is a framework for executing workloads and collecting
|
||||
measurements on Android and Linux devices. WA includes automation for nearly 50
|
||||
workloads (mostly Android), some common instrumentation (ftrace, ARM
|
||||
Streamline, hwmon). A number of output formats are supported.
|
||||
|
||||
Workload Automation is designed primarily as a developer tool/framework to
|
||||
facilitate data driven development by providing a method of collecting
|
||||
measurements from a device in a repeatable way.
|
||||
|
||||
Workload Automation is highly extensible. Most of the concrete functionality is
|
||||
implemented via plug-ins, and it is easy to write new plug-ins to support new
|
||||
device types, workloads, instrumentation or output processing.
|
||||
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
- Python 2.7
|
||||
- Linux (should work on other Unixes, but untested)
|
||||
- Latest Android SDK (ANDROID_HOME must be set) for Android devices, or
|
||||
- SSH for Linux devices
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To install::
|
||||
|
||||
python setup.py sdist
|
||||
sudo pip install dist/wlauto-*.tar.gz
|
||||
|
||||
Please refer to the `installation section <./doc/source/installation.rst>`_
|
||||
in the documentation for more details.
|
||||
|
||||
|
||||
Basic Usage
|
||||
===========
|
||||
|
||||
Please see the `Quickstart <./doc/source/quickstart.rst>`_ section of the
|
||||
documentation.
|
||||
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
Documentation in reStructuredText format may be found under ``doc/source``. To
|
||||
compile it into cross-linked HTML, make sure you have `Sphinx
|
||||
<http://sphinx-doc.org/install.html>`_ installed, and then ::
|
||||
|
||||
cd doc
|
||||
make html
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Workload Automation is distributed under `Apache v2.0 License
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>`_. Workload automation includes
|
||||
binaries distributed under differnt licenses (see LICENSE files in specfic
|
||||
directories).
|
||||
|
||||
|
||||
Feedback, Contrubutions and Support
|
||||
===================================
|
||||
|
||||
- Please use the GitHub Issue Tracker associated with this repository for
|
||||
feedback.
|
||||
- ARM licensees may contact ARM directly via their partner managers.
|
||||
- We welcome code contributions via GitHub Pull requests. Please see
|
||||
"Contributing Code" section of the documentation for details.
|
23
dev_scripts/README
Normal file
23
dev_scripts/README
Normal file
@ -0,0 +1,23 @@
|
||||
This directory contains scripts that aid the development of Workload Automation.
|
||||
They were written to work as part of WA development environment and are not
|
||||
guarnteed to work if moved outside their current location. They should not be
|
||||
distributed as part of WA releases.
|
||||
|
||||
Scripts
|
||||
-------
|
||||
|
||||
:clean_install: Performs a clean install of WA from source. This will remove any
|
||||
existing WA install (regardless of whether it was made from
|
||||
source or through a tarball with pip).
|
||||
|
||||
:clear_env: Clears ~/.workload_automation.
|
||||
|
||||
:get_apk_versions: Prints out a table of APKs and their versons found under the
|
||||
path specified as the argument.
|
||||
|
||||
:pep8: Runs pep8 code checker (must be installed) over wlauto with the correct
|
||||
settings for WA.
|
||||
|
||||
:pylint: Runs pylint (must be installed) over wlauto with the correct settings
|
||||
for WA.
|
||||
|
34
dev_scripts/clean_install
Executable file
34
dev_scripts/clean_install
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def get_installed_path():
|
||||
paths = [p for p in sys.path if len(p) > 2]
|
||||
for path in paths:
|
||||
candidate = os.path.join(path, 'wlauto')
|
||||
if os.path.isdir(candidate):
|
||||
return candidate
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
installed_path = get_installed_path()
|
||||
if installed_path:
|
||||
logging.info('Removing installed package from {}.'.format(installed_path))
|
||||
shutil.rmtree(installed_path)
|
||||
if os.path.isdir('build'):
|
||||
logging.info('Removing local build directory.')
|
||||
shutil.rmtree('build')
|
||||
logging.info('Removing *.pyc files.')
|
||||
for root, dirs, files in os.walk('wlauto'):
|
||||
for file in files:
|
||||
if file.lower().endswith('.pyc'):
|
||||
os.remove(os.path.join(root, file))
|
||||
|
||||
os.system('python setup.py install')
|
||||
|
3
dev_scripts/clear_env
Executable file
3
dev_scripts/clear_env
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# Clear workload automation user environment.
|
||||
rm -rf ~/.workload_automation/
|
25
dev_scripts/get_apk_versions
Executable file
25
dev_scripts/get_apk_versions
Executable file
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from wlauto.exceptions import WAError
|
||||
from wlauto.utils.misc import write_table
|
||||
from distmanagement.apk import get_aapt_path, get_apk_versions
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
aapt = get_aapt_path()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('path', metavar='PATH', help='Location to look for APKs.')
|
||||
args = parser.parse_args()
|
||||
|
||||
versions = get_apk_versions(args.path, aapt)
|
||||
write_table([v.to_tuple() for v in versions], sys.stdout,
|
||||
align='<<<>>', headers=['path', 'package', 'name', 'version code', 'version name'])
|
||||
except WAError, e:
|
||||
logging.error(e)
|
||||
sys.exit(1)
|
22
dev_scripts/pep8
Executable file
22
dev_scripts/pep8
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
EXCLUDE=wlauto/external/,wlauto/tests
|
||||
EXCLUDE_COMMA=wlauto/core/bootstrap.py,wlauto/workloads/geekbench/__init__.py
|
||||
IGNORE=E501,E265,E266,W391
|
||||
|
||||
if ! hash pep8 2>/dev/null; then
|
||||
echo "pep8 not found in PATH"
|
||||
echo "you can install it with \"sudo pip install pep8\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$1" == "" ]]; then
|
||||
THIS_DIR="`dirname \"$0\"`"
|
||||
pushd $THIS_DIR/.. > /dev/null
|
||||
pep8 --exclude=$EXCLUDE,$EXCLUDE_COMMA --ignore=$IGNORE wlauto
|
||||
pep8 --exclude=$EXCLUDE --ignore=$IGNORE,E241 $(echo "$EXCLUDE_COMMA" | sed 's/,/ /g')
|
||||
popd > /dev/null
|
||||
else
|
||||
pep8 --exclude=$EXCLUDE,$EXCLUDE_COMMA --ignore=$IGNORE $1
|
||||
fi
|
||||
|
47
dev_scripts/pylint
Executable file
47
dev_scripts/pylint
Executable file
@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
target=$1
|
||||
|
||||
compare_versions() {
|
||||
if [[ $1 == $2 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local IFS=.
|
||||
local i ver1=($1) ver2=($2)
|
||||
|
||||
for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
|
||||
ver1[i]=0
|
||||
done
|
||||
|
||||
for ((i=0; i<${#ver1[@]}; i++)); do
|
||||
if [[ -z ${ver2[i]} ]]; then
|
||||
ver2[i]=0
|
||||
fi
|
||||
if ((10#${ver1[i]} > 10#${ver2[i]})); then
|
||||
return 1
|
||||
fi
|
||||
if ((10#${ver1[i]} < 10#${ver2[i]})); then
|
||||
return 2
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
pylint_version=$(python -c 'from pylint.__pkginfo__ import version; print version')
|
||||
compare_versions $pylint_version "1.3.0"
|
||||
result=$?
|
||||
if [ "$result" == "2" ]; then
|
||||
echo "ERROR: pylint version must be at least 1.3.0; found $pylint_version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
THIS_DIR="`dirname \"$0\"`"
|
||||
if [[ "$target" == "" ]]; then
|
||||
pushd $THIS_DIR/.. > /dev/null
|
||||
pylint --rcfile extras/pylintrc wlauto
|
||||
popd > /dev/null
|
||||
else
|
||||
pylint --rcfile $THIS_DIR/../extras/pylintrc $target
|
||||
fi
|
184
doc/Makefile
Normal file
184
doc/Makefile
Normal file
@ -0,0 +1,184 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
SPHINXAPI = sphinx-apidoc
|
||||
SPHINXAPIOPTS =
|
||||
|
||||
WAEXT = ./build_extension_docs.py
|
||||
WAEXTOPTS = source/extensions ../wlauto ../wlauto/external ../wlauto/tests
|
||||
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
ALLSPHINXAPIOPTS = -f $(SPHINXAPIOPTS) -o source/api ../wlauto
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run documentation coverage checks"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
rm -rf source/api/*
|
||||
rm -rf source/extensions/*
|
||||
rm -rf source/instrumentation_method_map.rst
|
||||
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo
|
||||
@echo "Build finished. The coverage reports are in $(BUILDDIR)/coverage."
|
||||
|
||||
api: ../wlauto
|
||||
rm -rf source/api/*
|
||||
$(SPHINXAPI) $(ALLSPHINXAPIOPTS)
|
||||
|
||||
waext: ../wlauto
|
||||
rm -rf source/extensions
|
||||
mkdir -p source/extensions
|
||||
$(WAEXT) $(WAEXTOPTS)
|
||||
|
||||
|
||||
sigtab: ../wlauto/core/instrumentation.py source/instrumentation_method_map.template
|
||||
rm -rf source/instrumentation_method_map.rst
|
||||
./build_instrumentation_method_map.py source/instrumentation_method_map.rst
|
||||
|
||||
html: api waext sigtab
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml: api waext sigtab
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml: api waext sigtab
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle: api waext sigtab
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json: api waext sigtab
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp: api waext sigtab
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp: api waext sigtab
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WorkloadAutomation2.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WorkloadAutomation2.qhc"
|
||||
|
||||
devhelp: api
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/WorkloadAutomation2"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WorkloadAutomation2"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub: api waext sigtab
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex: api waext sigtab
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf: api waext sigtab
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text: api waext sigtab
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man: api waext sigtab
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo: api waext sigtab
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info: api waext sigtab
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext: api waext sigtab
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes: api waext sigtab
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck: api waext sigtab
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest: api waext sigtab
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
46
doc/build_extension_docs.py
Executable file
46
doc/build_extension_docs.py
Executable file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python
|
||||
# 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 ExtensionLoader
|
||||
from wlauto.utils.doc import get_rst_from_extension, underline
|
||||
from wlauto.utils.misc import capitalize
|
||||
|
||||
|
||||
GENERATE_FOR = ['workload', 'instrument', 'result_processor', 'device']
|
||||
|
||||
|
||||
def generate_extension_documentation(source_dir, outdir, ignore_paths):
|
||||
loader = ExtensionLoader(keep_going=True)
|
||||
loader.clear()
|
||||
loader.update(paths=[source_dir], ignore_paths=ignore_paths)
|
||||
for ext_type in loader.extension_kinds:
|
||||
if not ext_type in GENERATE_FOR:
|
||||
continue
|
||||
outfile = os.path.join(outdir, '{}s.rst'.format(ext_type))
|
||||
with open(outfile, 'w') as wfh:
|
||||
wfh.write('.. _{}s:\n\n'.format(ext_type))
|
||||
wfh.write(underline(capitalize('{}s'.format(ext_type))))
|
||||
exts = loader.list_extensions(ext_type)
|
||||
for ext in sorted(exts, key=lambda x: x.name):
|
||||
wfh.write(get_rst_from_extension(ext))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_extension_documentation(sys.argv[2], sys.argv[1], sys.argv[3:])
|
48
doc/build_instrumentation_method_map.py
Executable file
48
doc/build_instrumentation_method_map.py
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2015-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 string
|
||||
from copy import copy
|
||||
|
||||
from wlauto.core.instrumentation import SIGNAL_MAP, PRIORITY_MAP
|
||||
from wlauto.utils.doc import format_simple_table
|
||||
|
||||
|
||||
CONVINIENCE_ALIASES = ['initialize', 'setup', 'start', 'stop', 'process_workload_result',
|
||||
'update_result', 'teardown', 'finalize']
|
||||
|
||||
OUTPUT_TEMPLATE_FILE = os.path.join(os.path.dirname(__file__), 'source', 'instrumentation_method_map.template')
|
||||
|
||||
|
||||
def escape_trailing_underscore(value):
|
||||
if value.endswith('_'):
|
||||
return value[:-1] + '\_'
|
||||
|
||||
|
||||
def generate_instrumentation_method_map(outfile):
|
||||
signal_table = format_simple_table([(k, v) for k, v in SIGNAL_MAP.iteritems()],
|
||||
headers=['method name', 'signal'], align='<<')
|
||||
priority_table = format_simple_table([(escape_trailing_underscore(k), v) for k, v in PRIORITY_MAP.iteritems()],
|
||||
headers=['prefix', 'priority'], align='<>')
|
||||
with open(OUTPUT_TEMPLATE_FILE) as fh:
|
||||
template = string.Template(fh.read())
|
||||
with open(outfile, 'w') as wfh:
|
||||
wfh.write(template.substitute(signal_names=signal_table, priority_prefixes=priority_table))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate_instrumentation_method_map(sys.argv[1])
|
0
doc/source/_static/.gitignore
vendored
Normal file
0
doc/source/_static/.gitignore
vendored
Normal file
0
doc/source/_templates/.gitignore
vendored
Normal file
0
doc/source/_templates/.gitignore
vendored
Normal file
101
doc/source/additional_topics.rst
Normal file
101
doc/source/additional_topics.rst
Normal file
@ -0,0 +1,101 @@
|
||||
Additional Topics
|
||||
+++++++++++++++++
|
||||
|
||||
Modules
|
||||
=======
|
||||
|
||||
Modules are essentially plug-ins for Extensions. They provide a way of defining
|
||||
common and reusable functionality. An Extension can load zero or more modules
|
||||
during it's creation. Loaded modules will then add their capabilities (see
|
||||
Capabilities_) to those of the Extension. When calling code tries to access an
|
||||
attribute of an Extension the Extension doesn't have, it will try to find the
|
||||
attribute among it's loaded modules and will return that instead.
|
||||
|
||||
.. note:: Modules are themselves extensions, and can therefore load their own
|
||||
modules. *Do not* abuse this.
|
||||
|
||||
For example, calling code may wish to reboot an unresponsive device by calling
|
||||
``device.hard_reset()``, but the ``Device`` in question does not have a
|
||||
``hard_reset`` method; however the ``Device`` has loaded ``netio_switch``
|
||||
module which allows to disable power supply over a network (say this device
|
||||
is in a rack and is powered through such a switch). The module has
|
||||
``reset_power`` capability (see Capabilities_ below) and so implements
|
||||
``hard_reset``. This will get invoked when ``device.hard_rest()`` is called.
|
||||
|
||||
.. note:: Modules can only extend Extensions with new attributes; they cannot
|
||||
override existing functionality. In the example above, if the
|
||||
``Device`` has implemented ``hard_reset()`` itself, then *that* will
|
||||
get invoked irrespective of which modules it has loaded.
|
||||
|
||||
If two loaded modules have the same capability or implement the same method,
|
||||
then the last module to be loaded "wins" and its method will be invoke,
|
||||
effectively overriding the module that was loaded previously.
|
||||
|
||||
Specifying Modules
|
||||
------------------
|
||||
|
||||
Modules get loaded when an Extension is instantiated by the extension loader.
|
||||
There are two ways to specify which modules should be loaded for a device.
|
||||
|
||||
|
||||
Capabilities
|
||||
============
|
||||
|
||||
Capabilities define the functionality that is implemented by an Extension,
|
||||
either within the Extension itself or through loadable modules. A capability is
|
||||
just a label, but there is an implied contract. When an Extension claims to have
|
||||
a particular capability, it promises to expose a particular set of
|
||||
functionality through a predefined interface.
|
||||
|
||||
Currently used capabilities are described below.
|
||||
|
||||
.. note:: Since capabilities are basically random strings, the user can always
|
||||
define their own; and it is then up to the user to define, enforce and
|
||||
document the contract associated with their capability. Below, are the
|
||||
"standard" capabilities used in WA.
|
||||
|
||||
|
||||
.. note:: The method signatures in the descriptions below show the calling
|
||||
signature (i.e. they're omitting the initial self parameter).
|
||||
|
||||
active_cooling
|
||||
--------------
|
||||
|
||||
Intended to be used by devices and device modules, this capability implies
|
||||
that the device implements a controllable active cooling solution (e.g.
|
||||
a programmable fan). The device/module must implement the following methods:
|
||||
|
||||
start_active_cooling()
|
||||
Active cooling is started (e.g. the fan is turned on)
|
||||
|
||||
stop_active_cooling()
|
||||
Active cooling is stopped (e.g. the fan is turned off)
|
||||
|
||||
|
||||
reset_power
|
||||
-----------
|
||||
|
||||
Intended to be used by devices and device modules, this capability implies
|
||||
that the device is capable of performing a hard reset by toggling power. The
|
||||
device/module must implement the following method:
|
||||
|
||||
hard_reset()
|
||||
The device is restarted. This method cannot rely on the device being
|
||||
responsive and must work even if the software on the device has crashed.
|
||||
|
||||
|
||||
flash
|
||||
-----
|
||||
|
||||
Intended to be used by devices and device modules, this capability implies
|
||||
that the device can be flashed with new images. The device/module must
|
||||
implement the following method:
|
||||
|
||||
flash(image_bundle=None, images=None)
|
||||
``image_bundle`` is a path to a "bundle" (e.g. a tarball) that contains
|
||||
all the images to be flashed. Which images go where must also be defined
|
||||
within the bundle. ``images`` is a dict mapping image destination (e.g.
|
||||
partition name) to the path to that specific image. Both
|
||||
``image_bundle`` and ``images`` may be specified at the same time. If
|
||||
there is overlap between the two, ``images`` wins and its contents will
|
||||
be flashed in preference to the ``image_bundle``.
|
608
doc/source/agenda.rst
Normal file
608
doc/source/agenda.rst
Normal file
@ -0,0 +1,608 @@
|
||||
.. _agenda:
|
||||
|
||||
======
|
||||
Agenda
|
||||
======
|
||||
|
||||
An agenda specifies what is to be done during a Workload Automation run,
|
||||
including which workloads will be run, with what configuration, which
|
||||
instruments and result processors will be enabled, etc. Agenda syntax is
|
||||
designed to be both succinct and expressive.
|
||||
|
||||
Agendas are specified using YAML_ notation. It is recommended that you
|
||||
familiarize yourself with the linked page.
|
||||
|
||||
.. _YAML: http://en.wikipedia.org/wiki/YAML
|
||||
|
||||
.. note:: Earlier versions of WA have supported CSV-style agendas. These were
|
||||
there to facilitate transition from WA1 scripts. The format was more
|
||||
awkward and supported only a limited subset of the features. Support
|
||||
for it has now been removed.
|
||||
|
||||
|
||||
Specifying which workloads to run
|
||||
=================================
|
||||
|
||||
The central purpose of an agenda is to specify what workloads to run. A
|
||||
minimalist agenda contains a single entry at the top level called "workloads"
|
||||
that maps onto a list of workload names to run:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
workloads:
|
||||
- dhrystone
|
||||
- memcpy
|
||||
- cyclictest
|
||||
|
||||
This specifies a WA run consisting of ``dhrystone`` followed by ``memcpy``, followed by
|
||||
``cyclictest`` workloads, and using instruments and result processors specified in
|
||||
config.py (see :ref:`configuration-specification` section).
|
||||
|
||||
.. note:: If you're familiar with YAML, you will recognize the above as a single-key
|
||||
associative array mapping onto a list. YAML has two notations for both
|
||||
associative arrays and lists: block notation (seen above) and also
|
||||
in-line notation. This means that the above agenda can also be
|
||||
written in a single line as ::
|
||||
|
||||
workloads: [dhrystone, memcpy, cyclictest]
|
||||
|
||||
(with the list in-lined), or ::
|
||||
|
||||
{workloads: [dhrystone, memcpy, cyclictest]}
|
||||
|
||||
(with both the list and the associative array in-line). WA doesn't
|
||||
care which of the notations is used as they all get parsed into the
|
||||
same structure by the YAML parser. You can use whatever format you
|
||||
find easier/clearer.
|
||||
|
||||
Multiple iterations
|
||||
-------------------
|
||||
|
||||
There will normally be some variability in workload execution when running on a
|
||||
real device. In order to quantify it, multiple iterations of the same workload
|
||||
are usually performed. You can specify the number of iterations for each
|
||||
workload by adding ``iterations`` field to the workload specifications (or
|
||||
"specs"):
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
workloads:
|
||||
- name: dhrystone
|
||||
iterations: 5
|
||||
- name: memcpy
|
||||
iterations: 5
|
||||
- name: cyclictest
|
||||
iterations: 5
|
||||
|
||||
Now that we're specifying both the workload name and the number of iterations in
|
||||
each spec, we have to explicitly name each field of the spec.
|
||||
|
||||
It is often the case that, as in in the example above, you will want to run all
|
||||
workloads for the same number of iterations. Rather than having to specify it
|
||||
for each and every spec, you can do with a single entry by adding a ``global``
|
||||
section to your agenda:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- dhrystone
|
||||
- memcpy
|
||||
- cyclictest
|
||||
|
||||
The global section can contain the same fields as a workload spec. The
|
||||
fields in the global section will get added to each spec. If the same field is
|
||||
defined both in global section and in a spec, then the value in the spec will
|
||||
overwrite the global value. For example, suppose we wanted to run all our workloads
|
||||
for five iterations, except cyclictest which we want to run for ten (e.g.
|
||||
because we know it to be particularly unstable). This can be specified like
|
||||
this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- dhrystone
|
||||
- memcpy
|
||||
- name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
Again, because we are now specifying two fields for cyclictest spec, we have to
|
||||
explicitly name them.
|
||||
|
||||
Configuring workloads
|
||||
---------------------
|
||||
|
||||
Some workloads accept configuration parameters that modify their behavior. These
|
||||
parameters are specific to a particular workload and can alter the workload in
|
||||
any number of ways, e.g. set the duration for which to run, or specify a media
|
||||
file to be used, etc. The vast majority of workload parameters will have some
|
||||
default value, so it is only necessary to specify the name of the workload in
|
||||
order for WA to run it. However, sometimes you want more control over how a
|
||||
workload runs.
|
||||
|
||||
For example, by default, dhrystone will execute 10 million loops across four
|
||||
threads. Suppose you device has six cores available and you want the workload to
|
||||
load them all. You also want to increase the total number of loops accordingly
|
||||
to 15 million. You can specify this using dhrystone's parameters:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- name: dhrystone
|
||||
params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- memcpy
|
||||
- name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
.. note:: You can find out what parameters a workload accepts by looking it up
|
||||
in the :ref:`Workloads` section. You can also look it up using WA itself
|
||||
with "show" command::
|
||||
|
||||
wa show dhrystone
|
||||
|
||||
see the :ref:`Invocation` section for details.
|
||||
|
||||
In addition to configuring the workload itself, we can also specify
|
||||
configuration for the underlying device. This can be done by setting runtime
|
||||
parameters in the workload spec. For example, suppose we want to ensure the
|
||||
maximum score for our benchmarks, at the expense of power consumption, by
|
||||
setting the cpufreq governor to "performance" on cpu0 (assuming all our cores
|
||||
are in the same DVFS domain and so setting the governor for cpu0 will affect all
|
||||
cores). This can be done like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- name: dhrystone
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- memcpy
|
||||
- name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
|
||||
Here, we're specifying ``sysfile_values`` runtime parameter for the device. The
|
||||
value for this parameter is a mapping (an associative array, in YAML) of file
|
||||
paths onto values that should be written into those files. ``sysfile_values`` is
|
||||
the only runtime parameter that is available for any (Linux) device. Other
|
||||
runtime parameters will depend on the specifics of the device used (e.g. its
|
||||
CPU cores configuration). I've renamed ``params`` to ``workload_params`` for
|
||||
clarity, but that wasn't strictly necessary as ``params`` is interpreted as
|
||||
``workload_params`` inside a workload spec.
|
||||
|
||||
.. note:: ``params`` field is interpreted differently depending on whether it's in a
|
||||
workload spec or the global section. In a workload spec, it translates to
|
||||
``workload_params``, in the global section it translates to ``runtime_params``.
|
||||
|
||||
Runtime parameters do not automatically reset at the end of workload spec
|
||||
execution, so all subsequent iterations will also be affected unless they
|
||||
explicitly change the parameter (in the example above, performance governor will
|
||||
also be used for ``memcpy`` and ``cyclictest``. There are two ways around this:
|
||||
either set ``reboot_policy`` WA setting (see :ref:`configuration-specification` section) such that
|
||||
the device gets rebooted between spec executions, thus being returned to its
|
||||
initial state, or set the default runtime parameter values in the ``global``
|
||||
section of the agenda so that they get set for every spec that doesn't
|
||||
explicitly override them.
|
||||
|
||||
.. note:: "In addition to ``runtime_params`` there are also ``boot_params`` that
|
||||
work in a similar way, but they get passed to the device when it
|
||||
reboots. At the moment ``TC2`` is the only device that defines a boot
|
||||
parameter, which is explained in ``TC2`` documentation, so boot
|
||||
parameters will not be mentioned further.
|
||||
|
||||
IDs and Labels
|
||||
--------------
|
||||
|
||||
It is possible to list multiple specs with the same workload in an agenda. You
|
||||
may wish to this if you want to run a workload with different parameter values
|
||||
or under different runtime configurations of the device. The workload name
|
||||
therefore does not uniquely identify a spec. To be able to distinguish between
|
||||
different specs (e.g. in reported results), each spec has an ID which is unique
|
||||
to all specs within an agenda (and therefore with a single WA run). If an ID
|
||||
isn't explicitly specified using ``id`` field (note that the field name is in
|
||||
lower case), one will be automatically assigned to the spec at the beginning of
|
||||
the WA run based on the position of the spec within the list. The first spec
|
||||
*without an explicit ID* will be assigned ID ``1``, the second spec *without an
|
||||
explicit ID* will be assigned ID ``2``, and so forth.
|
||||
|
||||
Numerical IDs aren't particularly easy to deal with, which is why it is
|
||||
recommended that, for non-trivial agendas, you manually set the ids to something
|
||||
more meaningful (or use labels -- see below). An ID can be pretty much anything
|
||||
that will pass through the YAML parser. The only requirement is that it is
|
||||
unique to the agenda. However, is usually better to keep them reasonably short
|
||||
(they don't need to be *globally* unique), and to stick with alpha-numeric
|
||||
characters and underscores/dashes. While WA can handle other characters as well,
|
||||
getting too adventurous with your IDs may cause issues further down the line
|
||||
when processing WA results (e.g. when uploading them to a database that may have
|
||||
its own restrictions).
|
||||
|
||||
In addition to IDs, you can also specify labels for your workload specs. These
|
||||
are similar to IDs but do not have the uniqueness restriction. If specified,
|
||||
labels will be used by some result processes instead of (or in addition to) the
|
||||
workload name. For example, the ``csv`` result processor will put the label in the
|
||||
"workload" column of the CSV file.
|
||||
|
||||
It is up to you how you chose to use IDs and labels. WA itself doesn't expect
|
||||
any particular format (apart from uniqueness for IDs). Below is the earlier
|
||||
example updated to specify explicit IDs and label dhrystone spec to reflect
|
||||
parameters used.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- id: 01_dhry
|
||||
name: dhrystone
|
||||
label: dhrystone_15over6
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- id: 02_memc
|
||||
name: memcpy
|
||||
- id: 03_cycl
|
||||
name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
|
||||
Result Processors and Instrumentation
|
||||
=====================================
|
||||
|
||||
Result Processors
|
||||
-----------------
|
||||
|
||||
Result processors, as the name suggests, handle the processing of results
|
||||
generated form running workload specs. By default, WA enables a couple of basic
|
||||
result processors (e.g. one generates a csv file with all scores reported by
|
||||
workloads), which you can see in ``~/.workload_automation/config.py``. However,
|
||||
WA has a number of other, more specialized, result processors (e.g. for
|
||||
uploading to databases). You can list available result processors with
|
||||
``wa list result_processors`` command. If you want to permanently enable a
|
||||
result processor, you can add it to your ``config.py``. You can also enable a
|
||||
result processor for a particular run by specifying it in the ``config`` section
|
||||
in the agenda. As the name suggests, ``config`` section mirrors the structure of
|
||||
``config.py``\ (although using YAML rather than Python), and anything that can
|
||||
be specified in the latter, can also be specified in the former.
|
||||
|
||||
As with workloads, result processors may have parameters that define their
|
||||
behavior. Parameters of result processors are specified a little differently,
|
||||
however. Result processor parameter values are listed in the config section,
|
||||
namespaced under the name of the result processor.
|
||||
|
||||
For example, suppose we want to be able to easily query the results generated by
|
||||
the workload specs we've defined so far. We can use ``sqlite`` result processor
|
||||
to have WA create an sqlite_ database file with the results. By default, this
|
||||
file will be generated in WA's output directory (at the same level as
|
||||
results.csv); but suppose we want to store the results in the same file for
|
||||
every run of the agenda we do. This can be done by specifying an alternative
|
||||
database file with ``database`` parameter of the result processor:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
config:
|
||||
result_processors: [sqlite]
|
||||
sqlite:
|
||||
database: ~/my_wa_results.sqlite
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- id: 01_dhry
|
||||
name: dhrystone
|
||||
label: dhrystone_15over6
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- id: 02_memc
|
||||
name: memcpy
|
||||
- id: 03_cycl
|
||||
name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
A couple of things to observe here:
|
||||
|
||||
- There is no need to repeat the result processors listed in ``config.py``. The
|
||||
processors listed in ``result_processors`` entry in the agenda will be used
|
||||
*in addition to* those defined in the ``config.py``.
|
||||
- The database file is specified under "sqlite" entry in the config section.
|
||||
Note, however, that this entry alone is not enough to enable the result
|
||||
processor, it must be listed in ``result_processors``, otherwise the "sqilte"
|
||||
config entry will be ignored.
|
||||
- The database file must be specified as an absolute path, however it may use
|
||||
the user home specifier '~' and/or environment variables.
|
||||
|
||||
.. _sqlite: http://www.sqlite.org/
|
||||
|
||||
|
||||
Instrumentation
|
||||
---------------
|
||||
|
||||
WA can enable various "instruments" to be used during workload execution.
|
||||
Instruments can be quite diverse in their functionality, but the majority of
|
||||
instruments available in WA today are there to collect additional data (such as
|
||||
trace) from the device during workload execution. You can view the list of
|
||||
available instruments by using ``wa list instruments`` command. As with result
|
||||
processors, a few are enabled by default in the ``config.py`` and additional
|
||||
ones may be added in the same place, or specified in the agenda using
|
||||
``instrumentation`` entry.
|
||||
|
||||
For example, we can collect core utilisation statistics (for what proportion of
|
||||
workload execution N cores were utilized above a specified threshold) using
|
||||
``coreutil`` instrument.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
config:
|
||||
instrumentation: [coreutil]
|
||||
coreutil:
|
||||
threshold: 80
|
||||
result_processors: [sqlite]
|
||||
sqlite:
|
||||
database: ~/my_wa_results.sqlite
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- id: 01_dhry
|
||||
name: dhrystone
|
||||
label: dhrystone_15over6
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- id: 02_memc
|
||||
name: memcpy
|
||||
- id: 03_cycl
|
||||
name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
Instrumentation isn't "free" and it is advisable not to have too many
|
||||
instruments enabled at once as that might skew results. For example, you don't
|
||||
want to have power measurement enabled at the same time as event tracing, as the
|
||||
latter may prevent cores from going into idle states and thus affecting the
|
||||
reading collected by the former.
|
||||
|
||||
Unlike result processors, instrumentation may be enabled (and disabled -- see below)
|
||||
on per-spec basis. For example, suppose we want to collect /proc/meminfo from the
|
||||
device when we run ``memcpy`` workload, but not for the other two. We can do that using
|
||||
``sysfs_extractor`` instrument, and we will only enable it for ``memcpy``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
config:
|
||||
instrumentation: [coreutil]
|
||||
coreutil:
|
||||
threshold: 80
|
||||
sysfs_extractor:
|
||||
paths: [/proc/meminfo]
|
||||
result_processors: [sqlite]
|
||||
sqlite:
|
||||
database: ~/my_wa_results.sqlite
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- id: 01_dhry
|
||||
name: dhrystone
|
||||
label: dhrystone_15over6
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- id: 02_memc
|
||||
name: memcpy
|
||||
instrumentation: [sysfs_extractor]
|
||||
- id: 03_cycl
|
||||
name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
As with ``config`` sections, ``instrumentation`` entry in the spec needs only to
|
||||
list additional instruments and does not need to repeat instruments specified
|
||||
elsewhere.
|
||||
|
||||
.. note:: At present, it is only possible to enable/disable instrumentation on
|
||||
per-spec base. It is *not* possible to provide configuration on
|
||||
per-spec basis in the current version of WA (e.g. in our example, it
|
||||
is not possible to specify different ``sysfs_extractor`` paths for
|
||||
different workloads). This restriction may be lifted in future
|
||||
versions of WA.
|
||||
|
||||
Disabling result processors and instrumentation
|
||||
-----------------------------------------------
|
||||
|
||||
As seen above, extensions specified with ``instrumentation`` and
|
||||
``result_processor`` clauses get added to those already specified previously.
|
||||
Just because an instrument specified in ``config.py`` is not listed in the
|
||||
``config`` section of the agenda, does not mean it will be disabled. If you do
|
||||
want to disable an instrument, you can always remove/comment it out from
|
||||
``config.py``. However that will be introducing a permanent configuration change
|
||||
to your environment (one that can be easily reverted, but may be just as
|
||||
easily forgotten). If you want to temporarily disable a result processor or an
|
||||
instrument for a particular run, you can do that in your agenda by prepending a
|
||||
tilde (``~``) to its name.
|
||||
|
||||
For example, let's say we want to disable ``cpufreq`` instrument enabled in our
|
||||
``config.py`` (suppose we're going to send results via email and so want to
|
||||
reduce to total size of the output directory):
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
config:
|
||||
instrumentation: [coreutil, ~cpufreq]
|
||||
coreutil:
|
||||
threshold: 80
|
||||
sysfs_extractor:
|
||||
paths: [/proc/meminfo]
|
||||
result_processors: [sqlite]
|
||||
sqlite:
|
||||
database: ~/my_wa_results.sqlite
|
||||
global:
|
||||
iterations: 5
|
||||
workloads:
|
||||
- id: 01_dhry
|
||||
name: dhrystone
|
||||
label: dhrystone_15over6
|
||||
runtime_params:
|
||||
sysfile_values:
|
||||
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor: performance
|
||||
workload_params:
|
||||
threads: 6
|
||||
mloops: 15
|
||||
- id: 02_memc
|
||||
name: memcpy
|
||||
instrumentation: [sysfs_extractor]
|
||||
- id: 03_cycl
|
||||
name: cyclictest
|
||||
iterations: 10
|
||||
|
||||
|
||||
Sections
|
||||
========
|
||||
|
||||
It is a common requirement to be able to run the same set of workloads under
|
||||
different device configurations. E.g. you may want to investigate impact of
|
||||
changing a particular setting to different values on the benchmark scores, or to
|
||||
quantify the impact of enabling a particular feature in the kernel. WA allows
|
||||
this by defining "sections" of configuration with an agenda.
|
||||
|
||||
For example, suppose what we really want, is to measure the impact of using
|
||||
interactive cpufreq governor vs the performance governor on the three
|
||||
benchmarks. We could create another three workload spec entries similar to the
|
||||
ones we already have and change the sysfile value being set to "interactive".
|
||||
However, this introduces a lot of duplication; and what if we want to change
|
||||
spec configuration? We would have to change it in multiple places, running the
|
||||
risk of forgetting one.
|
||||
|
||||
A better way is to keep the three workload specs and define a section for each
|
||||
governor:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
config:
|
||||
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
|
||||
|
||||
A section, just like an workload spec, needs to have a unique ID. Apart from
|
||||
that, a "section" is similar to the ``global`` section we've already seen --
|
||||
everything that goes into a section will be applied to each workload spec.
|
||||
Workload specs defined under top-level ``workloads`` entry will be executed for
|
||||
each of the sections listed under ``sections``.
|
||||
|
||||
.. note:: It is also possible to have a ``workloads`` entry within a section,
|
||||
in which case, those workloads will only be executed for that specific
|
||||
section.
|
||||
|
||||
In order to maintain the uniqueness requirement of workload spec IDs, they will
|
||||
be namespaced under each section by prepending the section ID to the spec ID
|
||||
with an under score. So in the agenda above, we no longer have a workload spec
|
||||
with ID ``01_dhry``, instead there are two specs with IDs ``perf_01_dhry`` and
|
||||
``inter_01_dhry``.
|
||||
|
||||
Note that the ``global`` section still applies to every spec in the agenda. So
|
||||
the precedence order is -- spec settings override section settings, which in
|
||||
turn override global settings.
|
||||
|
||||
|
||||
Other Configuration
|
||||
===================
|
||||
|
||||
.. _configuration_in_agenda:
|
||||
|
||||
As mentioned previously, ``config`` section in an agenda can contain anything
|
||||
that can be defined in ``config.py`` (with Python syntax translated to the
|
||||
equivalent YAML). Certain configuration (e.g. ``run_name``) makes more sense
|
||||
to define in an agenda than a config file. Refer to the
|
||||
:ref:`configuration-specification` section for details.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
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
|
||||
|
7
doc/source/changes.rst
Normal file
7
doc/source/changes.rst
Normal file
@ -0,0 +1,7 @@
|
||||
What's New in Workload Automation
|
||||
=================================
|
||||
|
||||
Version 2.3.0
|
||||
-------------
|
||||
|
||||
- First publicly-released version.
|
270
doc/source/conf.py
Normal file
270
doc/source/conf.py
Normal file
@ -0,0 +1,270 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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.
|
||||
#
|
||||
|
||||
#
|
||||
# Workload Automation 2 documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Jul 15 09:00:46 2013.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings('ignore', "Module louie was already imported")
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
sys.path.insert(0, os.path.join(this_dir, '../..'))
|
||||
import wlauto
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Workload Automation'
|
||||
copyright = u'2013, ARM Ltd'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = wlauto.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = wlauto.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['**/*-example']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'WorkloadAutomationdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'WorkloadAutomation.tex', u'Workload Automation Documentation',
|
||||
u'WA Mailing List \\textless{}workload-automation@arm.com\\textgreater{},Sergei Trofimov \\textless{}sergei.trofimov@arm.com\\textgreater{}, Vasilis Flouris \\textless{}vasilis.flouris@arm.com\\textgreater{}, Mohammed Binsabbar \\textless{}mohammed.binsabbar@arm.com\\textgreater{}', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'workloadautomation', u'Workload Automation Documentation',
|
||||
[u'WA Mailing List <workload-automation@arm.com>, Sergei Trofimov <sergei.trofimov@arm.com>, Vasilis Flouris <vasilis.flouris@arm.com>'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'WorkloadAutomation', u'Workload Automation Documentation',
|
||||
u'WA Mailing List <workload-automation@arm.com>, Sergei Trofimov <sergei.trofimov@arm.com>, Vasilis Flouris <vasilis.flouris@arm.com>', 'WorkloadAutomation', 'A framwork for automationg workload execution on mobile devices.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_object_type('confval', 'confval',
|
||||
objname='configuration value',
|
||||
indextemplate='pair: %s; configuration value')
|
188
doc/source/configuration.rst
Normal file
188
doc/source/configuration.rst
Normal file
@ -0,0 +1,188 @@
|
||||
.. _configuration-specification:
|
||||
|
||||
=============
|
||||
Configuration
|
||||
=============
|
||||
|
||||
In addition to specifying run execution parameters through an agenda, the
|
||||
behavior of WA can be modified through configuration file(s). The default
|
||||
configuration file is ``~/.workload_automation/config.py`` (the location can be
|
||||
changed by setting ``WA_USER_DIRECTORY`` environment variable, see :ref:`envvars`
|
||||
section below). This file will be
|
||||
created when you first run WA if it does not already exist. This file must
|
||||
always exist and will always be loaded. You can add to or override the contents
|
||||
of that file on invocation of Workload Automation by specifying an additional
|
||||
configuration file using ``--config`` option.
|
||||
|
||||
The config file is just a Python source file, so it can contain any valid Python
|
||||
code (though execution of arbitrary code through the config file is
|
||||
discouraged). Variables with specific names will be picked up by the framework
|
||||
and used to modify the behavior of Workload automation.
|
||||
|
||||
.. note:: As of version 2.1.3 is also possible to specify the following
|
||||
configuration in the agenda. See :ref:`configuration in an agenda <configuration_in_agenda>`\ .
|
||||
|
||||
|
||||
.. _available_settings:
|
||||
|
||||
Available Settings
|
||||
==================
|
||||
|
||||
.. note:: Extensions such as workloads, instrumentation or result processors
|
||||
may also pick up certain settings from this file, so the list below is
|
||||
not exhaustive. Please refer to the documentation for the specific
|
||||
extensions to see what settings they accept.
|
||||
|
||||
.. confval:: device
|
||||
|
||||
This setting defines what specific Device subclass will be used to interact
|
||||
the connected device. Obviously, this must match your setup.
|
||||
|
||||
.. confval:: device_config
|
||||
|
||||
This must be a Python dict containing setting-value mapping for the
|
||||
configured :rst:dir:`device`. What settings and values are valid is specific
|
||||
to each device. Please refer to the documentation for your device.
|
||||
|
||||
.. confval:: reboot_policy
|
||||
|
||||
This defines when during execution of a run the Device will be rebooted. The
|
||||
possible values are:
|
||||
|
||||
``"never"``
|
||||
The device will never be rebooted.
|
||||
``"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.
|
||||
Note: this acts the same as each_iteration when execution order is set to by_iteration
|
||||
``"each_iteration"``
|
||||
The device will be rebooted before each new iteration.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`execution_model`
|
||||
|
||||
.. confval:: execution_order
|
||||
|
||||
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 after the other,
|
||||
so all workloads are executed before proceeding on to the second iteration.
|
||||
E.g. A1 B1 C1 A2 C2 A3. This is the default if no order is explicitly specified.
|
||||
|
||||
In case of multiple sections, this will spread them out, such that specs
|
||||
from the same section are further part. E.g. given sections X and Y, global
|
||||
specs A and B, and two iterations, this will run ::
|
||||
|
||||
X.A1, Y.A1, X.B1, Y.B1, X.A2, Y.A2, X.B2, Y.B2
|
||||
|
||||
``"by_section"``
|
||||
Same as ``"by_iteration"``, however this will group specs from the same
|
||||
section together, so given sections X and Y, global specs A and B, and two iterations,
|
||||
this will run ::
|
||||
|
||||
X.A1, X.B1, Y.A1, Y.B1, X.A2, X.B2, Y.A2, Y.B2
|
||||
|
||||
``"by_spec"``
|
||||
All iterations of the first spec are executed before moving on to the next
|
||||
spec. E.g. A1 A2 A3 B1 C1 C2 This may also be specified as ``"classic"``,
|
||||
as this was the way workloads were executed in earlier versions of WA.
|
||||
|
||||
``"random"``
|
||||
Execution order is entirely random.
|
||||
|
||||
Added in version 2.1.5.
|
||||
|
||||
.. confval:: instrumentation
|
||||
|
||||
This should be a list of instruments to be enabled during run execution.
|
||||
Values must be names of available instruments. Instruments are used to
|
||||
collect additional data, such as energy measurements or execution time,
|
||||
during runs.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`api/wlauto.instrumentation`
|
||||
|
||||
.. confval:: result_processors
|
||||
|
||||
This should be a list of result processors to be enabled during run execution.
|
||||
Values must be names of available result processors. Result processor define
|
||||
how data is output from WA.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`api/wlauto.result_processors`
|
||||
|
||||
.. confval:: logging
|
||||
|
||||
A dict that contains logging setting. At the moment only three settings are
|
||||
supported:
|
||||
|
||||
``"file format"``
|
||||
Controls how logging output appears in the run.log file in the output
|
||||
directory.
|
||||
``"verbose format"``
|
||||
Controls how logging output appear on the console when ``--verbose`` flag
|
||||
was used.
|
||||
``"regular format"``
|
||||
Controls how logging output appear on the console when ``--verbose`` flag
|
||||
was not used.
|
||||
|
||||
All three values should be Python `old-style format strings`_ specifying which
|
||||
`log record attributes`_ should be displayed.
|
||||
|
||||
There are also a couple of settings are used to provide additional metadata
|
||||
for a run. These may get picked up by instruments or result processors to
|
||||
attach context to results.
|
||||
|
||||
.. confval:: project
|
||||
|
||||
A string naming the project for which data is being collected. This may be
|
||||
useful, e.g. when uploading data to a shared database that is populated from
|
||||
multiple projects.
|
||||
|
||||
.. confval:: project_stage
|
||||
|
||||
A dict or a string that allows adding additional identifier. This is may be
|
||||
useful for long-running projects.
|
||||
|
||||
.. confval:: run_name
|
||||
|
||||
A string that labels the WA run that is bing performed. This would typically
|
||||
be set in the ``config`` section of an agenda (see
|
||||
:ref:`configuration in an agenda <configuration_in_agenda>`) rather than in the config file.
|
||||
|
||||
.. _old-style format strings: http://docs.python.org/2/library/stdtypes.html#string-formatting-operations
|
||||
.. _log record attributes: http://docs.python.org/2/library/logging.html#logrecord-attributes
|
||||
|
||||
|
||||
.. _envvars:
|
||||
|
||||
Environment Variables
|
||||
=====================
|
||||
|
||||
In addition to standard configuration described above, WA behaviour can be
|
||||
altered through environment variables. These can determine where WA looks for
|
||||
various assets when it starts.
|
||||
|
||||
.. confval:: WA_USER_DIRECTORY
|
||||
|
||||
This is the location WA will look for config.py, inustrumentation , and it
|
||||
will also be used for local caches, etc. If this variable is not set, the
|
||||
default location is ``~/.workload_automation`` (this is created when WA
|
||||
is installed).
|
||||
|
||||
.. note:: This location **must** be writable by the user who runs WA.
|
||||
|
||||
|
||||
.. confval:: WA_EXTENSION_PATHS
|
||||
|
||||
By default, WA will look for extensions in its own package and in
|
||||
subdirectories under ``WA_USER_DIRECTORY``. This environment variable can
|
||||
be used specify a colon-separated list of additional locations WA should
|
||||
use to look for extensions.
|
45
doc/source/contributing.rst
Normal file
45
doc/source/contributing.rst
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
Contributing Code
|
||||
=================
|
||||
|
||||
We welcome code contributions via GitHub pull requests to the official WA
|
||||
repository. To help with maintainability of the code line we ask that the code
|
||||
uses a coding style consistent with the rest of WA code, which is basically
|
||||
`PEP8 <https://www.python.org/dev/peps/pep-0008/>`_ with line length and block
|
||||
comment rules relaxed (the wrapper for PEP8 checker inside ``dev_scripts`` will
|
||||
run it with appropriate configuration).
|
||||
|
||||
We ask that the following checks are performed on the modified code prior to
|
||||
submitting a pull request:
|
||||
|
||||
.. note:: You will need pylint and pep8 static checkers installed::
|
||||
|
||||
pip install pep8
|
||||
pip install pylint
|
||||
|
||||
It is recommened that you install via pip rather than through your
|
||||
distribution's package mananger because the latter is likely to
|
||||
contain out-of-date version of these tools.
|
||||
|
||||
- ``./dev_scripts/pylint`` should be run without arguments and should produce no
|
||||
output (any output should be addressed by making appropriate changes in the
|
||||
code or adding a pylint ignore directive, if there is a good reason for
|
||||
keeping the code as is).
|
||||
- ``./dev_scripts/pep8`` should be run without arguments and should produce no
|
||||
output (any output should be addressed by making appropriate changes in the
|
||||
code).
|
||||
- If the modifications touch core framework (anything under ``wlauto/core``), unit
|
||||
tests should be run using ``nosetests``, and they should all pass.
|
||||
|
||||
- If significant additions have been made to the framework, unit
|
||||
tests should be added to cover the new functionality.
|
||||
|
||||
- If modifications have been made to documentation (this includes description
|
||||
attributes for Parameters and Extensions), documentation should be built to
|
||||
make sure no errors or warning during build process, and a visual inspection
|
||||
of new/updated sections in resulting HTML should be performed to ensure
|
||||
everything renders as expected.
|
||||
|
||||
Once you have your contribution is ready, please follow instructions in `GitHub
|
||||
documentation <https://help.github.com/articles/creating-a-pull-request/>`_ to
|
||||
create a pull request.
|
74
doc/source/conventions.rst
Normal file
74
doc/source/conventions.rst
Normal file
@ -0,0 +1,74 @@
|
||||
===========
|
||||
Conventions
|
||||
===========
|
||||
|
||||
Interface Definitions
|
||||
=====================
|
||||
|
||||
Throughout this documentation a number of stubbed-out class definitions will be
|
||||
presented showing an interface defined by a base class that needs to be
|
||||
implemented by the deriving classes. The following conventions will be used when
|
||||
presenting such an interface:
|
||||
|
||||
- Methods shown raising :class:`NotImplementedError` are abstract and *must*
|
||||
be overridden by subclasses.
|
||||
- Methods with ``pass`` in their body *may* be (but do not need to be) overridden
|
||||
by subclasses. If not overridden, these methods will default to the base
|
||||
class implementation, which may or may not be a no-op (the ``pass`` in the
|
||||
interface specification does not necessarily mean that the method does not have an
|
||||
actual implementation in the base class).
|
||||
|
||||
.. note:: If you *do* override these methods you must remember to call the
|
||||
base class' version inside your implementation as well.
|
||||
|
||||
- Attributes who's value is shown as ``None`` *must* be redefined by the
|
||||
subclasses with an appropriate value.
|
||||
- Attributes who's value is shown as something other than ``None`` (including
|
||||
empty strings/lists/dicts) *may* be (but do not need to be) overridden by
|
||||
subclasses. If not overridden, they will default to the value shown.
|
||||
|
||||
Keep in mind that the above convention applies only when showing interface
|
||||
definitions and may not apply elsewhere in the documentation. Also, in the
|
||||
interest of clarity, only the relevant parts of the base class definitions will
|
||||
be shown some members (such as internal methods) may be omitted.
|
||||
|
||||
|
||||
Code Snippets
|
||||
=============
|
||||
|
||||
Code snippets provided are intended to be valid Python code, and to be complete.
|
||||
However, for the sake of clarity, in some cases only the relevant parts will be
|
||||
shown with some details omitted (details that may necessary to validity of the code
|
||||
but not to understanding of the concept being illustrated). In such cases, a
|
||||
commented ellipsis will be used to indicate that parts of the code have been
|
||||
dropped. E.g. ::
|
||||
|
||||
# ...
|
||||
|
||||
def update_result(self, context):
|
||||
# ...
|
||||
context.result.add_metric('energy', 23.6, 'Joules', lower_is_better=True)
|
||||
|
||||
# ...
|
||||
|
||||
|
||||
Core Class Names
|
||||
================
|
||||
|
||||
When core classes are referenced throughout the documentation, usually their
|
||||
fully-qualified names are given e.g. :class:`wlauto.core.workload.Workload`.
|
||||
This is done so that Sphinx_ can resolve them and provide a link. While
|
||||
implementing extensions, however, you should *not* be importing anything
|
||||
directly form under :mod:`wlauto.core`. Instead, classes you are meant to
|
||||
instantiate or subclass have been aliased in the root :mod:`wlauto` package,
|
||||
and should be imported from there, e.g. ::
|
||||
|
||||
from wlauto import Workload
|
||||
|
||||
All examples given in the documentation follow this convention. Please note that
|
||||
this only applies to the :mod:`wlauto.core` subpackage; all other classes
|
||||
should be imported for their corresponding subpackages.
|
||||
|
||||
.. _Sphinx: http://sphinx-doc.org/
|
||||
|
||||
|
246
doc/source/daq_device_setup.rst
Normal file
246
doc/source/daq_device_setup.rst
Normal file
@ -0,0 +1,246 @@
|
||||
.. _daq_setup:
|
||||
|
||||
DAQ Server Guide
|
||||
================
|
||||
|
||||
NI-DAQ, or just "DAQ", is the Data Acquisition device developed by National
|
||||
Instruments:
|
||||
|
||||
http://www.ni.com/data-acquisition/
|
||||
|
||||
WA uses the DAQ to collect power measurements during workload execution. A
|
||||
client/server solution for this is distributed as part of WA, though it is
|
||||
distinct from WA and may be used separately (by invoking the client APIs from a
|
||||
Python script, or used directly from the command line).
|
||||
|
||||
This solution is dependent on the NI-DAQmx driver for the DAQ device. At the
|
||||
time of writing, only Windows versions of the driver are supported (there is an
|
||||
old Linux version that works on some versions of RHEL and Centos, but it is
|
||||
unsupported and won't work with recent Linux kernels). Because of this, the
|
||||
server part of the solution will need to be run on a Windows machine (though it
|
||||
should also work on Linux, if the driver becomes available).
|
||||
|
||||
|
||||
.. _daq_wiring:
|
||||
|
||||
DAQ Device Wiring
|
||||
-----------------
|
||||
|
||||
The server expects the device to be wired in a specific way in order to be able
|
||||
to collect power measurements. Two consecutive Analogue Input (AI) channels on
|
||||
the DAQ are used to form a logical "port" (starting with AI/0 and AI/1 for port
|
||||
0). Of these, the lower/even channel (e.g. AI/0) is used to measure the voltage
|
||||
on the rail we're interested in; the higher/odd channel (e.g. AI/1) is used to
|
||||
measure the voltage drop across a known very small resistor on the same rail,
|
||||
which is then used to calculate current. The logical wiring diagram looks like
|
||||
this::
|
||||
|
||||
Port N
|
||||
======
|
||||
|
|
||||
| AI/(N*2)+ <--- Vr -------------------------|
|
||||
| |
|
||||
| AI/(N*2)- <--- GND -------------------// |
|
||||
| |
|
||||
| AI/(N*2+1)+ <--- V ------------|-------V |
|
||||
| r | |
|
||||
| AI/(N*2+1)- <--- Vr --/\/\/\----| |
|
||||
| | |
|
||||
| | |
|
||||
| |------------------------------|
|
||||
======
|
||||
|
||||
Where:
|
||||
V: Voltage going into the resistor
|
||||
Vr: Voltage between resistor and the SOC
|
||||
GND: Ground
|
||||
r: The resistor across the rail with a known
|
||||
small value.
|
||||
|
||||
|
||||
The physical wiring will depend on the specific DAQ device, as channel layout
|
||||
varies between models.
|
||||
|
||||
.. note:: Current solution supports variable number of ports, however it
|
||||
assumes that the ports are sequential and start at zero. E.g. if you
|
||||
want to measure power on three rails, you will need to wire ports 0-2
|
||||
(AI/0 to AI/5 channels on the DAQ) to do it. It is not currently
|
||||
possible to use any other configuration (e.g. ports 1, 2 and 5).
|
||||
|
||||
|
||||
Setting up NI-DAQmx driver on a Windows Machine
|
||||
-----------------------------------------------
|
||||
|
||||
- The NI-DAQmx driver is pretty big in size, 1.5 GB. The driver name is
|
||||
'NI-DAQmx' and its version '9.7.0f0' which you can obtain it from National
|
||||
Instruments website by downloading NI Measurement & Automation Explorer (Ni
|
||||
MAX) from: http://joule.ni.com/nidu/cds/view/p/id/3811/lang/en
|
||||
|
||||
.. note:: During the installation process, you might be prompted to install
|
||||
.NET framework 4.
|
||||
|
||||
- The installation process is quite long, 7-15 minutes.
|
||||
- Once installed, open NI MAX, which should be in your desktop, if not type its
|
||||
name in the start->search.
|
||||
- Connect the NI-DAQ device to your machine. You should see it appear under
|
||||
'Devices and Interfaces'. If not, press 'F5' to refresh the list.
|
||||
- Complete the device wiring as described in the :ref:`daq_wiring` section.
|
||||
- Quit NI MAX.
|
||||
|
||||
|
||||
Setting up DAQ server
|
||||
---------------------
|
||||
|
||||
The DAQ power measurement solution is implemented in daqpower Python library,
|
||||
the package for which can be found in WA's install location under
|
||||
``wlauto/external/daq_server/daqpower-1.0.0.tar.gz`` (the version number in your
|
||||
installation may be different).
|
||||
|
||||
- Install NI-DAQmx driver, as described in the previous section.
|
||||
- Install Python 2.7.
|
||||
- Download and install ``pip``, ``numpy`` and ``twisted`` Python packages.
|
||||
These packages have C extensions, an so you will need a native compiler set
|
||||
up if you want to install them from PyPI. As an easier alternative, you can
|
||||
find pre-built Windows installers for these packages here_ (the versions are
|
||||
likely to be older than what's on PyPI though).
|
||||
- Install the daqpower package using pip::
|
||||
|
||||
pip install C:\Python27\Lib\site-packages\wlauto\external\daq_server\daqpower-1.0.0.tar.gz
|
||||
|
||||
This should automatically download and install ``PyDAQmx`` package as well
|
||||
(the Python bindings for the NI-DAQmx driver).
|
||||
|
||||
.. _here: http://www.lfd.uci.edu/~gohlke/pythonlibs/
|
||||
|
||||
|
||||
Running DAQ server
|
||||
------------------
|
||||
|
||||
Once you have installed the ``daqpower`` package and the required dependencies as
|
||||
described above, you can start the server by executing ``run-daq-server`` from the
|
||||
command line. The server will start listening on the default port, 45677.
|
||||
|
||||
.. note:: There is a chance that pip will not add ``run-daq-server`` into your
|
||||
path. In that case, you can run daq server as such:
|
||||
``python C:\path to python\Scripts\run-daq-server``
|
||||
|
||||
You can optionally specify flags to control the behaviour or the server::
|
||||
|
||||
usage: run-daq-server [-h] [-d DIR] [-p PORT] [--debug] [--verbose]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-d DIR, --directory DIR
|
||||
Working directory
|
||||
-p PORT, --port PORT port the server will listen on.
|
||||
--debug Run in debug mode (no DAQ connected).
|
||||
--verbose Produce verobose output.
|
||||
|
||||
.. note:: The server will use a working directory (by default, the directory
|
||||
the run-daq-server command was executed in, or the location specified
|
||||
with -d flag) to store power traces before they are collected by the
|
||||
client. This directory must be read/write-able by the user running
|
||||
the server.
|
||||
|
||||
|
||||
Collecting Power with WA
|
||||
------------------------
|
||||
|
||||
.. note:: You do *not* need to install the ``daqpower`` package on the machine
|
||||
running WA, as it is already included in the WA install structure.
|
||||
However, you do need to make sure that ``twisted`` package is
|
||||
installed.
|
||||
|
||||
You can enable ``daq`` instrument your agenda/config.py in order to get WA to
|
||||
collect power measurements. At minimum, you will also need to specify the
|
||||
resistor values for each port in your configuration, e.g.::
|
||||
|
||||
resistor_values = [0.005, 0.005] # in Ohms
|
||||
|
||||
This also specifies the number of logical ports (measurement sites) you want to
|
||||
use, and, implicitly, the port numbers (ports 0 to N-1 will be used).
|
||||
|
||||
.. note:: "ports" here refers to the logical ports wired on the DAQ (see :ref:`daq_wiring`,
|
||||
not to be confused with the TCP port the server is listening on.
|
||||
|
||||
Unless you're running the DAQ server and WA on the same machine (unlikely
|
||||
considering that WA is officially supported only on Linux and recent NI-DAQmx
|
||||
drivers are only available on Windows), you will also need to specify the IP
|
||||
address of the server::
|
||||
|
||||
daq_server = 127.0.0.1
|
||||
|
||||
There are a number of other settings that can optionally be specified in the
|
||||
configuration (e.g. the labels to be used for DAQ ports). Please refer to the
|
||||
:class:`wlauto.instrumentation.daq.Daq` documentation for details.
|
||||
|
||||
|
||||
Collecting Power from the Command Line
|
||||
--------------------------------------
|
||||
|
||||
``daqpower`` package also comes with a client that may be used from the command
|
||||
line. Unlike when collecting power with WA, you *will* need to install the
|
||||
``daqpower`` package. Once installed, you will be able to interract with a
|
||||
running DAQ server by invoking ``send-daq-command``. The invocation syntax is ::
|
||||
|
||||
send-daq-command --host HOST [--port PORT] COMMAND [OPTIONS]
|
||||
|
||||
Options are command-specific. COMMAND may be one of the following (and they
|
||||
should generally be inoked in that order):
|
||||
|
||||
:configure: Set up a new session, specifying the configuration values to
|
||||
be used. If there is already a configured session, it will
|
||||
be terminated. OPTIONS for this this command are the DAQ
|
||||
configuration parameters listed in the DAQ instrument
|
||||
documentation with all ``_`` replaced by ``-`` and prefixed
|
||||
with ``--``, e.g. ``--resistor-values``.
|
||||
:start: Start collecting power measurments.
|
||||
:stop: Stop collecting power measurments.
|
||||
:get_data: Pull files containg power measurements from the server.
|
||||
There is one option for this command:
|
||||
``--output-directory`` which specifies where the files will
|
||||
be pulled to; if this is not specified, the will be in the
|
||||
current directory.
|
||||
:close: Close the currently configured server session. This will get rid
|
||||
of the data files and configuration on the server, so it would
|
||||
no longer be possible to use "start" or "get_data" commands
|
||||
before a new session is configured.
|
||||
|
||||
A typical command line session would go like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
send-daq-command --host 127.0.0.1 configure --resistor-values 0.005 0.005
|
||||
# set up and kick off the use case you want to measure
|
||||
send-daq-command --host 127.0.0.1 start
|
||||
# wait for the use case to complete
|
||||
send-daq-command --host 127.0.0.1 stop
|
||||
send-daq-command --host 127.0.0.1 get_data
|
||||
# files called PORT_0.csv and PORT_1.csv will appear in the current directory
|
||||
# containing measurements collected during use case execution
|
||||
send-daq-command --host 127.0.0.1 close
|
||||
# the session is terminated and the csv files on the server have been
|
||||
# deleted. A new session may now be configured.
|
||||
|
||||
In addtion to these "standard workflow" commands, the following commands are
|
||||
also available:
|
||||
|
||||
:list_devices: Returns a list of DAQ devices detected by the NI-DAQmx
|
||||
driver. In case mutiple devices are connected to the
|
||||
server host, you can specify the device you want to use
|
||||
with ``--device-id`` option when configuring a session.
|
||||
:list_ports: Returns a list of ports tha have been configured for the
|
||||
current session, e.g. ``['PORT_0', 'PORT_1']``.
|
||||
:list_port_files: Returns a list of data files that have been geneted
|
||||
(unless something went wrong, there should be one for
|
||||
each port).
|
||||
|
||||
|
||||
Collecting Power from another Python Script
|
||||
-------------------------------------------
|
||||
|
||||
You can invoke the above commands from a Python script using
|
||||
:py:func:`daqpower.client.execute_command` function, passing in
|
||||
:class:`daqpower.config.ServerConfiguration` and, in case of the configure command,
|
||||
:class:`daqpower.config.DeviceConfigruation`. Please see the implementation of
|
||||
the ``daq`` WA instrument for examples of how these APIs can be used.
|
407
doc/source/device_setup.rst
Normal file
407
doc/source/device_setup.rst
Normal file
@ -0,0 +1,407 @@
|
||||
Setting Up A Device
|
||||
===================
|
||||
|
||||
WA should work with most Android devices out-of-the box, as long as the device
|
||||
is discoverable by ``adb`` (i.e. gets listed when you run ``adb devices``). For
|
||||
USB-attached devices, that should be the case; for network devices, ``adb connect``
|
||||
would need to be invoked with the IP address of the device. If there is only one
|
||||
device connected to the host running WA, then no further configuration should be
|
||||
necessary (though you may want to :ref:`tweak some Android settings <configuring-android>`\ ).
|
||||
|
||||
If you have multiple devices connected, have a non-standard Android build (e.g.
|
||||
on a development board), or want to use of the more advanced WA functionality,
|
||||
further configuration will be required.
|
||||
|
||||
Android
|
||||
+++++++
|
||||
|
||||
General Device Setup
|
||||
--------------------
|
||||
|
||||
You can specify the device interface by setting ``device`` setting in
|
||||
``~/.workload_automation/config.py``. Available interfaces can be viewed by
|
||||
running ``wa list devices`` command. If you don't see your specific device
|
||||
listed (which is likely unless you're using one of the ARM-supplied platforms), then
|
||||
you should use ``generic_android`` interface (this is set in the config by
|
||||
default).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
device = 'generic_android'
|
||||
|
||||
The device interface may be configured through ``device_config`` setting, who's
|
||||
value is a ``dict`` mapping setting names to their values. You can find the full
|
||||
list of available parameter by looking up your device interface in the
|
||||
:ref:`devices` section of the documentation. Some of the most common parameters
|
||||
you might want to change are outlined below.
|
||||
|
||||
.. confval:: adb_name
|
||||
|
||||
If you have multiple Android devices connected to the host machine, you will
|
||||
need to set this to indicate to WA which device you want it to use.
|
||||
|
||||
.. confval:: working_directory
|
||||
|
||||
WA needs a "working" directory on the device which it will use for collecting
|
||||
traces, caching assets it pushes to the device, etc. By default, it will
|
||||
create one under ``/sdcard`` which should be mapped and writable on standard
|
||||
Android builds. If this is not the case for your device, you will need to
|
||||
specify an alternative working directory (e.g. under ``/data/local``).
|
||||
|
||||
.. confval:: scheduler
|
||||
|
||||
This specifies the scheduling mechanism (from the perspective of core layout)
|
||||
utilized by the device). For recent big.LITTLE devices, this should generally
|
||||
be "hmp" (ARM Hetrogeneous Mutli-Processing); some legacy development
|
||||
platforms might have Linaro IKS kernels, in which case it should be "iks".
|
||||
For homogeneous (single-cluster) devices, it should be "smp". Please see
|
||||
``scheduler`` parameter in the ``generic_android`` device documentation for
|
||||
more details.
|
||||
|
||||
.. confval:: core_names
|
||||
|
||||
This and ``core_clusters`` need to be set if you want to utilize some more
|
||||
advanced WA functionality (like setting of core-related runtime parameters
|
||||
such as governors, frequencies, etc). ``core_names`` should be a list of
|
||||
core names matching the order in which they are exposed in sysfs. For
|
||||
example, ARM TC2 SoC is a 2x3 big.LITTLE system; it's core_names would be
|
||||
``['a7', 'a7', 'a7', 'a15', 'a15']``, indicating that cpu0-cpu2 in cpufreq
|
||||
sysfs structure are A7's and cpu3 and cpu4 are A15's.
|
||||
|
||||
.. confval:: core_clusters
|
||||
|
||||
If ``core_names`` is defined, this must also be defined. This is a list of
|
||||
integer values indicating the cluster the corresponding core in
|
||||
``cores_names`` belongs to. For example, for TC2, this would be
|
||||
``[0, 0, 0, 1, 1]``, indicating that A7's are on cluster 0 and A15's are on
|
||||
cluster 1.
|
||||
|
||||
A typical ``device_config`` inside ``config.py`` may look something like
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
device_config = dict(
|
||||
'adb_name'='0123456789ABCDEF',
|
||||
'working_direcory'='/sdcard/wa-working',
|
||||
'core_names'=['a7', 'a7', 'a7', 'a15', 'a15'],
|
||||
'core_clusters'=[0, 0, 0, 1, 1],
|
||||
# ...
|
||||
)
|
||||
|
||||
.. _configuring-android:
|
||||
|
||||
Configuring Android
|
||||
-------------------
|
||||
|
||||
There are a few additional tasks you may need to perform once you have a device
|
||||
booted into Android (especially if this is an initial boot of a fresh OS
|
||||
deployment):
|
||||
|
||||
- You have gone through FTU (first time usage) on the home screen and
|
||||
in the apps menu.
|
||||
- You have disabled the screen lock.
|
||||
- You have set sleep timeout to the highest possible value (30 mins on
|
||||
most devices).
|
||||
- You have disabled brightness auto-adjust and have set the brightness
|
||||
to a fixed level.
|
||||
- You have set the locale language to "English" (this is important for
|
||||
some workloads in which UI automation looks for specific text in UI
|
||||
elements).
|
||||
|
||||
TC2 Setup
|
||||
---------
|
||||
|
||||
This section outlines how to setup ARM TC2 development platform to work with WA.
|
||||
|
||||
Pre-requisites
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
You can obtain the full set of images for TC2 from Linaro:
|
||||
|
||||
https://releases.linaro.org/latest/android/vexpress-lsk.
|
||||
|
||||
For the easiest setup, follow the instructions on the "Firmware" and "Binary
|
||||
Image Installation" tabs on that page.
|
||||
|
||||
.. note:: The default ``reboot_policy`` in ``config.py`` is to not reboot. With
|
||||
this WA will assume that the device is already booted into Android
|
||||
prior to WA being invoked. If you want to WA to do the initial boot of
|
||||
the TC2, you will have to change reboot policy to at least
|
||||
``initial``.
|
||||
|
||||
|
||||
Setting Up Images
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. note:: Make sure that both DIP switches near the black reset button on TC2
|
||||
are up (this is counter to the Linaro guide that instructs to lower
|
||||
one of the switches).
|
||||
|
||||
.. note:: The TC2 must have an Ethernet connection.
|
||||
|
||||
|
||||
If you have followed the setup instructions on the Linaro page, you should have
|
||||
a USB stick or an SD card with the file system, and internal microSD on the
|
||||
board (VEMSD) with the firmware images. The default Linaro configuration is to
|
||||
boot from the image on the boot partition in the file system you have just
|
||||
created. This is not supported by WA, which expects the image to be in NOR flash
|
||||
on the board. This requires you to copy the images from the boot partition onto
|
||||
the internal microSD card.
|
||||
|
||||
Assuming the boot partition of the Linaro file system is mounted on
|
||||
``/media/boot`` and the internal microSD is mounted on ``/media/VEMSD``, copy
|
||||
the following images::
|
||||
|
||||
cp /media/boot/zImage /media/VEMSD/SOFTWARE/kern_mp.bin
|
||||
cp /media/boot/initrd /media/VEMSD/SOFTWARE/init_mp.bin
|
||||
cp /media/boot/v2p-ca15-tc2.dtb /media/VEMSD/SOFTWARE/mp_a7bc.dtb
|
||||
|
||||
Optionally
|
||||
##########
|
||||
|
||||
The default device tree configuration the TC2 is to boot on the A7 cluster. It
|
||||
is also possible to configure the device tree to boot on the A15 cluster, or to
|
||||
boot with one of the clusters disabled (turning TC2 into an A7-only or A15-only
|
||||
device). Please refer to the "Firmware" tab on the Linaro paged linked above for
|
||||
instructions on how to compile the appropriate device tree configurations.
|
||||
|
||||
WA allows selecting between these configurations using ``os_mode`` boot
|
||||
parameter of the TC2 device interface. In order for this to work correctly,
|
||||
device tree files for the A15-bootcluster, A7-only and A15-only configurations
|
||||
should be copied into ``/media/VEMSD/SOFTWARE/`` as ``mp_a15bc.dtb``,
|
||||
``mp_a7.dtb`` and ``mp_a15.dtb`` respectively.
|
||||
|
||||
This is entirely optional. If you're not planning on switching boot cluster
|
||||
configuration, those files do not need to be present in VEMSD.
|
||||
|
||||
config.txt
|
||||
##########
|
||||
|
||||
Also, make sure that ``USB_REMOTE`` setting in ``/media/VEMSD/config.txt`` is set
|
||||
to ``TRUE`` (this will allow rebooting the device by writing reboot.txt to
|
||||
VEMSD). ::
|
||||
|
||||
USB_REMOTE: TRUE ;Selects remote command via USB
|
||||
|
||||
|
||||
TC2-specific device_config settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are a few settings that may need to be set in ``device_config`` inside
|
||||
your ``config.py`` which are specific to TC2:
|
||||
|
||||
.. note:: TC2 *does not* accept most "standard" android ``device_config``
|
||||
settings.
|
||||
|
||||
adb_name
|
||||
If you're running WA with reboots disabled (which is the default reboot
|
||||
policy), you will need to manually run ``adb connect`` with TC2's IP
|
||||
address and set this.
|
||||
|
||||
root_mount
|
||||
WA expects TC2's internal microSD to be mounted on the host under
|
||||
``/media/VEMSD``. If this location is different, it needs to be specified
|
||||
using this setting.
|
||||
|
||||
boot_firmware
|
||||
WA defaults to try booting using UEFI, which will require some additional
|
||||
firmware from ARM that may not be provided with Linaro releases (see the
|
||||
UEFI and PSCI section below). If you do not have those images, you will
|
||||
need to set ``boot_firmware`` to ``bootmon``.
|
||||
|
||||
fs_medium
|
||||
TC2's file system can reside either on an SD card or on a USB stick. Boot
|
||||
configuration is different depending on this. By default, WA expects it
|
||||
to be on ``usb``; if you are using and SD card, you should set this to
|
||||
``sd``.
|
||||
|
||||
bm_image
|
||||
Bootmon image that comes as part of TC2 firmware periodically gets
|
||||
updated. At the time of the release, ``bm_v519r.axf`` was used by
|
||||
ARM. If you are using a more recent image, you will need to set this
|
||||
indicating the image name (just the name of the actual file, *not* the
|
||||
path). Note: this setting only applies if using ``bootmon`` boot
|
||||
firmware.
|
||||
|
||||
serial_device
|
||||
WA will assume TC2 is connected on ``/dev/ttyS0`` by default. If the
|
||||
serial port is different, you will need to set this.
|
||||
|
||||
|
||||
UEFI and PSCI
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
UEFI is a boot firmware alternative to bootmon. Currently UEFI is coupled with PSCI (Power State Coordination Interface). That means
|
||||
that in order to use PSCI, UEFI has to be the boot firmware. Currently the reverse dependency is true as well (for TC2). Therefore
|
||||
using UEFI requires enabling PSCI.
|
||||
|
||||
In case you intend to use uefi/psci mode instead of bootmon, you will need two additional files: tc2_sec.bin and tc2_uefi.bin.
|
||||
after obtaining those files, place them inside /media/VEMSD/SOFTWARE/ directory as such::
|
||||
|
||||
cp tc2_sec.bin /media/VEMSD/SOFTWARE/
|
||||
cp tc2_uefi.bin /media/VEMSD/SOFTWARE/
|
||||
|
||||
|
||||
Juno Setup
|
||||
----------
|
||||
|
||||
.. note:: At the time of writing, the Android software stack on Juno was still
|
||||
very immature. Some workloads may not run, and there maybe stability
|
||||
issues with the device.
|
||||
|
||||
|
||||
The full software stack can be obtained from Linaro:
|
||||
|
||||
https://releases.linaro.org/14.08/members/arm/android/images/armv8-android-juno-lsk
|
||||
|
||||
Please follow the instructions on the "Binary Image Installation" tab on that
|
||||
page. More up-to-date firmware and kernel may also be obtained by registered
|
||||
members from ARM Connected Community: http://www.arm.com/community/ (though this
|
||||
is not guaranteed to work with the Linaro file system).
|
||||
|
||||
UEFI
|
||||
~~~~
|
||||
|
||||
Juno uses UEFI_ to boot the kernel image. UEFI supports multiple boot
|
||||
configurations, and presents a menu on boot to select (in default configuration
|
||||
it will automatically boot the first entry in the menu if not interrupted before
|
||||
a timeout). WA will look for a specific entry in the UEFI menu
|
||||
(``'WA'`` by default, but that may be changed by setting ``uefi_entry`` in the
|
||||
``device_config``). When following the UEFI instructions on the above Linaro
|
||||
page, please make sure to name the entry appropriately (or to correctly set the
|
||||
``uefi_entry``).
|
||||
|
||||
.. _UEFI: http://en.wikipedia.org/wiki/UEFI
|
||||
|
||||
There are two supported way for Juno to discover kernel images through UEFI. It
|
||||
can either load them from NOR flash on the board, or form boot partition on the
|
||||
file system. The setup described on the Linaro page uses the boot partition
|
||||
method.
|
||||
|
||||
If WA does not find the UEFI entry it expects, it will create one. However, it
|
||||
will assume that the kernel image resides in NOR flash, which means it will not
|
||||
work with Linaro file system. So if you're replicating the Linaro setup exactly,
|
||||
you will need to create the entry manually, as outline on the above-linked page.
|
||||
|
||||
Rebooting
|
||||
~~~~~~~~~
|
||||
|
||||
At the time of writing, normal Android reboot did not work properly on Juno
|
||||
Android, causing the device to crash into an irrecoverable state. Therefore, WA
|
||||
will perform a hard reset to reboot the device. It will attempt to do this by
|
||||
toggling the DTR line on the serial connection to the device. In order for this
|
||||
to work, you need to make sure that SW1 configuration switch on the back panel of
|
||||
the board (the right-most DIP switch) is toggled *down*.
|
||||
|
||||
|
||||
Linux
|
||||
+++++
|
||||
|
||||
General Device Setup
|
||||
--------------------
|
||||
|
||||
You can specify the device interface by setting ``device`` setting in
|
||||
``~/.workload_automation/config.py``. Available interfaces can be viewed by
|
||||
running ``wa list devices`` command. If you don't see your specific device
|
||||
listed (which is likely unless you're using one of the ARM-supplied platforms), then
|
||||
you should use ``generic_linux`` interface (this is set in the config by
|
||||
default).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
device = 'generic_linux'
|
||||
|
||||
The device interface may be configured through ``device_config`` setting, who's
|
||||
value is a ``dict`` mapping setting names to their values. You can find the full
|
||||
list of available parameter by looking up your device interface in the
|
||||
:ref:`devices` section of the documentation. Some of the most common parameters
|
||||
you might want to change are outlined below.
|
||||
|
||||
Currently, the only only supported method for talking to a Linux device is over
|
||||
SSH. Device configuration must specify the parameters need to establish the
|
||||
connection.
|
||||
|
||||
.. confval:: host
|
||||
|
||||
This should be either the the DNS name or IP address of the device.
|
||||
|
||||
.. confval:: username
|
||||
|
||||
The login name of the user on the device that WA will use. This user should
|
||||
have a home directory (unless an alternative working directory is specified
|
||||
using ``working_directory`` config -- see below), and, for full
|
||||
functionality, the user should have sudo rights (WA will be able to use
|
||||
sudo-less acounts but some instruments or workload may not work).
|
||||
|
||||
.. confval:: password
|
||||
|
||||
Password for the account on the device. Either this of a ``keyfile`` (see
|
||||
below) must be specified.
|
||||
|
||||
.. confval:: keyfile
|
||||
|
||||
If key-based authentication is used, this may be used to specify the SSH identity
|
||||
file instead of the password.
|
||||
|
||||
.. confval:: property_files
|
||||
|
||||
This is a list of paths that will be pulled for each WA run into the __meta
|
||||
subdirectory in the results. The intention is to collect meta-data about the
|
||||
device that may aid in reporducing the results later. The paths specified do
|
||||
not have to exist on the device (they will be ignored if they do not). The
|
||||
default list is ``['/proc/version', '/etc/debian_version', '/etc/lsb-release', '/etc/arch-release']``
|
||||
|
||||
|
||||
In addition, ``working_directory``, ``scheduler``, ``core_names``, and
|
||||
``core_clusters`` can also be specified and have the same meaning as for Android
|
||||
devices (see above).
|
||||
|
||||
A typical ``device_config`` inside ``config.py`` may look something like
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
device_config = dict(
|
||||
'host'='192.168.0.7',
|
||||
'username'='guest',
|
||||
'password'='guest',
|
||||
'core_names'=['a7', 'a7', 'a7', 'a15', 'a15'],
|
||||
'core_clusters'=[0, 0, 0, 1, 1],
|
||||
# ...
|
||||
)
|
||||
|
||||
|
||||
Related Settings
|
||||
++++++++++++++++
|
||||
|
||||
Reboot Policy
|
||||
-------------
|
||||
|
||||
This indicates when during WA execution the device will be rebooted. By default
|
||||
this is set to ``never``, indicating that WA will not reboot the device. Please
|
||||
see ``reboot_policy`` documentation in :ref:`configuration-specification` for
|
||||
|
||||
more details.
|
||||
|
||||
Execution Order
|
||||
---------------
|
||||
|
||||
``execution_order`` defines the order in which WA will execute workloads.
|
||||
``by_iteration`` (set by default) will execute the first iteration of each spec
|
||||
first, followed by the second iteration of each spec (that defines more than one
|
||||
iteration) and so forth. The alternative will loop through all iterations for
|
||||
the first first spec first, then move on to second spec, etc. Again, please see
|
||||
:ref:`configuration-specification` for more details.
|
||||
|
||||
|
||||
Adding a new device interface
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
If you are working with a particularly unusual device (e.g. a early stage
|
||||
development board) or need to be able to handle some quirk of your Android build,
|
||||
configuration available in ``generic_android`` interface may not be enough for
|
||||
you. In that case, you may need to write a custom interface for your device. A
|
||||
device interface is an ``Extension`` (a plug-in) type in WA and is implemented
|
||||
similar to other extensions (such as workloads or instruments). Pleaser refer to
|
||||
:ref:`adding_a_device` section for information on how this may be done.
|
115
doc/source/execution_model.rst
Normal file
115
doc/source/execution_model.rst
Normal file
@ -0,0 +1,115 @@
|
||||
++++++++++++++++++
|
||||
Framework Overview
|
||||
++++++++++++++++++
|
||||
|
||||
Execution Model
|
||||
===============
|
||||
|
||||
At the high level, the execution model looks as follows:
|
||||
|
||||
.. image:: wa-execution.png
|
||||
:scale: 50 %
|
||||
|
||||
After some initial setup, the framework initializes the device, loads and initialized
|
||||
instrumentation and begins executing jobs defined by the workload specs in the agenda. Each job
|
||||
executes in four basic stages:
|
||||
|
||||
setup
|
||||
Initial setup for the workload is performed. E.g. required assets are deployed to the
|
||||
devices, required services or applications are launched, etc. Run time configuration of the
|
||||
device for the workload is also performed at this time.
|
||||
|
||||
run
|
||||
This is when the workload actually runs. This is defined as the part of the workload that is
|
||||
to be measured. Exactly what happens at this stage depends entirely on the workload.
|
||||
|
||||
result processing
|
||||
Results generated during the execution of the workload, if there are any, are collected,
|
||||
parsed and extracted metrics are passed up to the core framework.
|
||||
|
||||
teardown
|
||||
Final clean up is performed, e.g. applications may closed, files generated during execution
|
||||
deleted, etc.
|
||||
|
||||
Signals are dispatched (see signal_dispatch_ below) at each stage of workload execution,
|
||||
which installed instrumentation can hook into in order to collect measurements, alter workload
|
||||
execution, etc. Instrumentation implementation usually mirrors that of workloads, defining
|
||||
setup, teardown and result processing stages for a particular instrument. Instead of a ``run``,
|
||||
instruments usually implement a ``start`` and a ``stop`` which get triggered just before and just
|
||||
after a workload run. However, the signal dispatch mechanism give a high degree of flexibility
|
||||
to instruments allowing them to hook into almost any stage of a WA run (apart from the very
|
||||
early initialization).
|
||||
|
||||
Metrics and artifacts generated by workloads and instrumentation are accumulated by the framework
|
||||
and are then passed to active result processors. This happens after each individual workload
|
||||
execution and at the end of the run. A result process may chose to act at either or both of these
|
||||
points.
|
||||
|
||||
|
||||
Control Flow
|
||||
============
|
||||
|
||||
This section goes into more detail explaining the relationship between the major components of the
|
||||
framework and how control passes between them during a run. It will only go through the major
|
||||
transition and interactions and will not attempt to describe very single thing that happens.
|
||||
|
||||
.. note:: This is the control flow for the ``wa run`` command which is the main functionality
|
||||
of WA. Other commands are much simpler and most of what is described below does not
|
||||
apply to them.
|
||||
|
||||
#. ``wlauto.core.entry_point`` parses the command form the arguments and executes the run command
|
||||
(``wlauto.commands.run.RunCommand``).
|
||||
#. Run command initializes the output directory and creates a ``wlauto.core.agenda.Agenda`` based on
|
||||
the command line arguments. Finally, it instantiates a ``wlauto.core.execution.Executor`` and
|
||||
passes it the Agenda.
|
||||
#. The Executor uses the Agenda to create a ``wlauto.core.configuraiton.RunConfiguration`` fully
|
||||
defines the configuration for the run (it will be serialised into ``__meta`` subdirectory under
|
||||
the output directory.
|
||||
#. The Executor proceeds to instantiate and install instrumentation, result processors and the
|
||||
device interface, based on the RunConfiguration. The executor also initialise a
|
||||
``wlauto.core.execution.ExecutionContext`` which is used to track the current state of the run
|
||||
execution and also serves as a means of communication between the core framework and the
|
||||
extensions.
|
||||
#. Finally, the Executor instantiates a ``wlauto.core.execution.Runner``, initializes its job
|
||||
queue with workload specs from the RunConfiguraiton, and kicks it off.
|
||||
#. The Runner performs the run time initialization of the device and goes through the workload specs
|
||||
(in the order defined by ``execution_order`` setting), running each spec according to the
|
||||
execution model described in the previous section. The Runner sends signals (see below) at
|
||||
appropriate points during execution.
|
||||
#. At the end of the run, the control is briefly passed back to the Executor, which outputs a
|
||||
summary for the run.
|
||||
|
||||
|
||||
.. _signal_dispatch:
|
||||
|
||||
Signal Dispatch
|
||||
===============
|
||||
|
||||
WA uses the `louie <https://pypi.python.org/pypi/Louie/1.1>`_ (formerly, pydispatcher) library
|
||||
for signal dispatch. Callbacks can be registered for signals emitted during the run. WA uses a
|
||||
version of louie that has been modified to introduce priority to registered callbacks (so that
|
||||
callbacks that are know to be slow can be registered with a lower priority so that they do not
|
||||
interfere with other callbacks).
|
||||
|
||||
This mechanism is abstracted for instrumentation. Methods of an :class:`wlauto.core.Instrument`
|
||||
subclass automatically get hooked to appropriate signals based on their names when the instrument
|
||||
is "installed" for the run. Priority can be specified by adding ``very_fast_``, ``fast_`` ,
|
||||
``slow_`` or ``very_slow_`` prefixes to method names.
|
||||
|
||||
The full list of method names and the signals they map to may be viewed
|
||||
:ref:`here <instrumentation_method_map>`.
|
||||
|
||||
Signal dispatching mechanism may also be used directly, for example to dynamically register
|
||||
callbacks at runtime or allow extensions other than ``Instruments`` to access stages of the run
|
||||
they are normally not aware of.
|
||||
|
||||
The sending of signals is the responsibility of the Runner. Signals gets sent during transitions
|
||||
between execution stages and when special evens, such as errors or device reboots, occur.
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
instrumentation_method_map
|
138
doc/source/index.rst
Normal file
138
doc/source/index.rst
Normal file
@ -0,0 +1,138 @@
|
||||
.. Workload Automation 2 documentation master file, created by
|
||||
sphinx-quickstart on Mon Jul 15 09:00:46 2013.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Documentation for Workload Automation
|
||||
================================================
|
||||
|
||||
Workload Automation (WA) is a framework for running workloads on real hardware devices. WA
|
||||
supports a number of output formats as well as additional instrumentation (such as Streamline
|
||||
traces). A number of workloads are included with the framework.
|
||||
|
||||
|
||||
.. contents:: Contents
|
||||
|
||||
|
||||
What's New
|
||||
~~~~~~~~~~
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changes
|
||||
|
||||
|
||||
Usage
|
||||
~~~~~
|
||||
|
||||
This section lists general usage documentation. If you're new to WA2, it is
|
||||
recommended you start with the :doc:`quickstart` page. This section also contains
|
||||
installation and configuration guides.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
quickstart
|
||||
installation
|
||||
device_setup
|
||||
invocation
|
||||
agenda
|
||||
configuration
|
||||
|
||||
|
||||
Extensions
|
||||
~~~~~~~~~~
|
||||
|
||||
This section lists extensions that currently come with WA2. Each package below
|
||||
represents a particular type of extension (e.g. a workload); each sub-package of
|
||||
that package is a particular instance of that extension (e.g. the Andebench
|
||||
workload). Clicking on a link will show what the individual extension does,
|
||||
what configuration parameters it takes, etc.
|
||||
|
||||
For how to implement you own extensions, please refer to the guides in the
|
||||
:ref:`in-depth` section.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<style>
|
||||
td {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
</style>
|
||||
<table <tr><td>
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
extensions/workloads
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</td><td>
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
extensions/instruments
|
||||
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</td><td>
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
extensions/result_processors
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</td><td>
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
extensions/devices
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</td></tr></table>
|
||||
|
||||
.. _in-depth:
|
||||
|
||||
In-depth
|
||||
~~~~~~~~
|
||||
|
||||
This section contains more advanced topics, such how to write your own extensions
|
||||
and detailed descriptions of how WA functions under the hood.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
conventions
|
||||
writing_extensions
|
||||
execution_model
|
||||
resources
|
||||
additional_topics
|
||||
daq_device_setup
|
||||
revent
|
||||
contributing
|
||||
|
||||
API Reference
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 5
|
||||
|
||||
api/modules
|
||||
|
||||
|
||||
Indices and tables
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
144
doc/source/installation.rst
Normal file
144
doc/source/installation.rst
Normal file
@ -0,0 +1,144 @@
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
.. module:: wlauto
|
||||
|
||||
This page describes how to install Workload Automation 2.
|
||||
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
Operating System
|
||||
----------------
|
||||
|
||||
WA runs on a native Linux install. It was tested with Ubuntu 12.04,
|
||||
but any recent Linux distribution should work. It should run on either
|
||||
32bit or 64bit OS, provided the correct version of Android (see below)
|
||||
was installed. Officially, **other environments are not supported**. WA
|
||||
has been known to run on Linux Virtual machines and in Cygwin environments,
|
||||
though additional configuration maybe required in both cases (known issues
|
||||
include makings sure USB/serial connections are passed to the VM, and wrong
|
||||
python/pip binaries being picked up in Cygwin). WA *should* work on other
|
||||
Unix-based systems such as BSD or Mac OS X, but it has not been tested
|
||||
in those environments. WA *does not* run on Windows (though it should be
|
||||
possible to get limited functionality with minimal porting effort).
|
||||
|
||||
|
||||
Android SDK
|
||||
-----------
|
||||
|
||||
You need to have the Android SDK with at least one platform installed.
|
||||
To install it, download the ADT Bundle from here_. Extract it
|
||||
and add ``<path_to_android_sdk>/sdk/platform-tools`` and ``<path_to_android_sdk>/sdk/tools``
|
||||
to your ``PATH``. To test that you've installed it properly run ``adb
|
||||
version``, the output should be similar to this::
|
||||
|
||||
$$ adb version
|
||||
Android Debug Bridge version 1.0.31
|
||||
$$
|
||||
|
||||
.. _here: https://developer.android.com/sdk/index.html
|
||||
|
||||
Once that is working, run ::
|
||||
|
||||
android update sdk
|
||||
|
||||
This will open up a dialog box listing available android platforms and
|
||||
corresponding API levels, e.g. ``Android 4.3 (API 18)``. For WA, you will need
|
||||
at least API level 18 (i.e. Android 4.3), though installing the latest is
|
||||
usually the best bet.
|
||||
|
||||
Optionally (but recommended), you should also set ``ANDROID_HOME`` to point to
|
||||
the install location of the SDK (i.e. ``<path_to_android_sdk>/sdk``).
|
||||
|
||||
|
||||
Python
|
||||
------
|
||||
|
||||
Workload Automation 2 requires Python 2.7 (Python 3 is not supported, at the moment).
|
||||
|
||||
|
||||
pip
|
||||
---
|
||||
|
||||
pip is the recommended package manager for Python. It is not part of standard
|
||||
Python distribution and would need to be installed separately. On Ubuntu and
|
||||
similar distributions, this may be done with APT::
|
||||
|
||||
sudo apt-get install python-pip
|
||||
|
||||
|
||||
Python Packages
|
||||
---------------
|
||||
|
||||
.. note:: pip should automatically download and install missing dependencies,
|
||||
so if you're using pip, you can skip this section.
|
||||
|
||||
Workload Automation 2 depends on the following additional libraries:
|
||||
|
||||
* pexpect
|
||||
* docutils
|
||||
* pySerial
|
||||
* pyYAML
|
||||
* python-dateutil
|
||||
|
||||
You can install these with pip::
|
||||
|
||||
sudo pip install pexpect
|
||||
sudo pip install pyserial
|
||||
sudo pip install pyyaml
|
||||
sudo pip install docutils
|
||||
sudo pip install python-dateutil
|
||||
|
||||
Some of these may also be available in your distro's repositories, e.g. ::
|
||||
|
||||
sudo apt-get install python-serial
|
||||
|
||||
Distro package versions tend to be older, so pip installation is recommended.
|
||||
However, pip will always download and try to build the source, so in some
|
||||
situations distro binaries may provide an easier fall back. Please also note that
|
||||
distro package names may differ from pip packages.
|
||||
|
||||
|
||||
Optional Python Packages
|
||||
------------------------
|
||||
|
||||
.. note:: unlike the mandatory dependencies in the previous section,
|
||||
pip will *not* install these automatically, so you will have
|
||||
to explicitly install them if/when you need them.
|
||||
|
||||
In addition to the mandatory packages listed in the previous sections, some WA
|
||||
functionality (e.g. certain extensions) may have additional dependencies. Since
|
||||
they are not necessary to be able to use most of WA, they are not made mandatory
|
||||
to simplify initial WA installation. If you try to use an extension that has
|
||||
additional, unmet dependencies, WA will tell you before starting the run, and
|
||||
you can install it then. They are listed here for those that would rather
|
||||
install them upfront (e.g. if you're planning to use WA to an environment that
|
||||
may not always have Internet access).
|
||||
|
||||
* nose
|
||||
* pandas
|
||||
* PyDAQmx
|
||||
* pymongo
|
||||
* jinja2
|
||||
|
||||
|
||||
.. note:: Some packages have C extensions and will require Python development
|
||||
headers to install. You can get those by installing ``python-dev``
|
||||
package in apt on Ubuntu (or the equivalent for your distribution).
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
Download the tarball and run pip::
|
||||
|
||||
sudo pip install wlauto-$version.tar.gz
|
||||
|
||||
If the above succeeds, try ::
|
||||
|
||||
wa --version
|
||||
|
||||
Hopefully, this should output something along the lines of "Workload Automation
|
||||
version $version".
|
73
doc/source/instrumentation_method_map.rst
Normal file
73
doc/source/instrumentation_method_map.rst
Normal file
@ -0,0 +1,73 @@
|
||||
Instrumentation Signal-Method Mapping
|
||||
=====================================
|
||||
|
||||
.. _instrumentation_method_map:
|
||||
|
||||
Instrument methods get automatically hooked up to signals based on their names. Mostly, the method
|
||||
name correponds to the name of the signal, however there are a few convienience aliases defined
|
||||
(listed first) to make easier to relate instrumenation code to the workload execution model.
|
||||
|
||||
======================================== =========================================
|
||||
method name signal
|
||||
======================================== =========================================
|
||||
initialize run-init-signal
|
||||
setup successful-workload-setup-signal
|
||||
start before-workload-execution-signal
|
||||
stop after-workload-execution-signal
|
||||
process_workload_result successful-iteration-result-update-signal
|
||||
update_result after-iteration-result-update-signal
|
||||
teardown after-workload-teardown-signal
|
||||
finalize run-fin-signal
|
||||
on_run_start start-signal
|
||||
on_run_end end-signal
|
||||
on_workload_spec_start workload-spec-start-signal
|
||||
on_workload_spec_end workload-spec-end-signal
|
||||
on_iteration_start iteration-start-signal
|
||||
on_iteration_end iteration-end-signal
|
||||
before_initial_boot before-initial-boot-signal
|
||||
on_successful_initial_boot successful-initial-boot-signal
|
||||
after_initial_boot after-initial-boot-signal
|
||||
before_first_iteration_boot before-first-iteration-boot-signal
|
||||
on_successful_first_iteration_boot successful-first-iteration-boot-signal
|
||||
after_first_iteration_boot after-first-iteration-boot-signal
|
||||
before_boot before-boot-signal
|
||||
on_successful_boot successful-boot-signal
|
||||
after_boot after-boot-signal
|
||||
on_spec_init spec-init-signal
|
||||
on_run_init run-init-signal
|
||||
on_iteration_init iteration-init-signal
|
||||
before_workload_setup before-workload-setup-signal
|
||||
on_successful_workload_setup successful-workload-setup-signal
|
||||
after_workload_setup after-workload-setup-signal
|
||||
before_workload_execution before-workload-execution-signal
|
||||
on_successful_workload_execution successful-workload-execution-signal
|
||||
after_workload_execution after-workload-execution-signal
|
||||
before_workload_result_update before-iteration-result-update-signal
|
||||
on_successful_workload_result_update successful-iteration-result-update-signal
|
||||
after_workload_result_update after-iteration-result-update-signal
|
||||
before_workload_teardown before-workload-teardown-signal
|
||||
on_successful_workload_teardown successful-workload-teardown-signal
|
||||
after_workload_teardown after-workload-teardown-signal
|
||||
before_overall_results_processing before-overall-results-process-signal
|
||||
on_successful_overall_results_processing successful-overall-results-process-signal
|
||||
after_overall_results_processing after-overall-results-process-signal
|
||||
on_error error_logged
|
||||
on_warning warning_logged
|
||||
======================================== =========================================
|
||||
|
||||
|
||||
The names above may be prefixed with one of pre-defined prefixes to set the priority of the
|
||||
Instrument method realive to other callbacks registered for the signal (within the same priority
|
||||
level, callbacks are invoked in the order they were registered). The table below shows the mapping
|
||||
of the prifix to the corresponding priority:
|
||||
|
||||
=========== ===
|
||||
prefix priority
|
||||
=========== ===
|
||||
very_fast\_ 20
|
||||
fast\_ 10
|
||||
normal\_ 0
|
||||
slow\_ -10
|
||||
very_slow\_ -20
|
||||
=========== ===
|
||||
|
17
doc/source/instrumentation_method_map.template
Normal file
17
doc/source/instrumentation_method_map.template
Normal file
@ -0,0 +1,17 @@
|
||||
Instrumentation Signal-Method Mapping
|
||||
=====================================
|
||||
|
||||
.. _instrumentation_method_map:
|
||||
|
||||
Instrument methods get automatically hooked up to signals based on their names. Mostly, the method
|
||||
name correponds to the name of the signal, however there are a few convienience aliases defined
|
||||
(listed first) to make easier to relate instrumenation code to the workload execution model.
|
||||
|
||||
$signal_names
|
||||
|
||||
The names above may be prefixed with one of pre-defined prefixes to set the priority of the
|
||||
Instrument method realive to other callbacks registered for the signal (within the same priority
|
||||
level, callbacks are invoked in the order they were registered). The table below shows the mapping
|
||||
of the prifix to the corresponding priority:
|
||||
|
||||
$priority_prefixes
|
135
doc/source/invocation.rst
Normal file
135
doc/source/invocation.rst
Normal file
@ -0,0 +1,135 @@
|
||||
.. _invocation:
|
||||
|
||||
========
|
||||
Commands
|
||||
========
|
||||
|
||||
Installing the wlauto package will add ``wa`` command to your system,
|
||||
which you can run from anywhere. This has a number of sub-commands, which can
|
||||
be viewed by executing ::
|
||||
|
||||
wa -h
|
||||
|
||||
Individual sub-commands are discussed in detail below.
|
||||
|
||||
run
|
||||
---
|
||||
|
||||
The most common sub-command you will use is ``run``. This will run specfied
|
||||
workload(s) and process resulting output. This takes a single mandatory
|
||||
argument that specifies what you want WA to run. This could be either a
|
||||
workload name, or a path to an "agenda" file that allows to specify multiple
|
||||
workloads as well as a lot additional configuration (see :ref:`agenda`
|
||||
section for details). Executing ::
|
||||
|
||||
wa run -h
|
||||
|
||||
Will display help for this subcommand that will look somehtign like this::
|
||||
|
||||
usage: run [-d DIR] [-f] AGENDA
|
||||
|
||||
Execute automated workloads on a remote device and process the resulting
|
||||
output.
|
||||
|
||||
positional arguments:
|
||||
AGENDA Agenda for this workload automation run. This defines
|
||||
which workloads will be executed, how many times, with
|
||||
which tunables, etc. See /usr/local/lib/python2.7
|
||||
/dist-packages/wlauto/agenda-example.csv for an
|
||||
example of how this file should be structured.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG, --config CONFIG
|
||||
specify an additional config.py
|
||||
-v, --verbose The scripts will produce verbose output.
|
||||
--version Output the version of Workload Automation and exit.
|
||||
--debug Enable debug mode. Note: this implies --verbose.
|
||||
-d DIR, --output-directory DIR
|
||||
Specify a directory where the output will be
|
||||
generated. If the directoryalready 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 optionis not specified, then
|
||||
wa_output will be used instead.
|
||||
-f, --force Overwrite output directory if it exists. By default,
|
||||
the script will abort in thissituation to prevent
|
||||
accidental data loss.
|
||||
-i ID, --id ID 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.
|
||||
|
||||
|
||||
Output Directory
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The exact contents on the output directory will depend on configuration options
|
||||
used, instrumentation and output processors enabled, etc. Typically, the output
|
||||
directory will contain a results file at the top level that lists all
|
||||
measurements that were collected (currently, csv and json formats are
|
||||
supported), along with a subdirectory for each iteration executed with output
|
||||
for that specific iteration.
|
||||
|
||||
At the top level, there will also be a run.log file containing the complete log
|
||||
output for the execution. The contents of this file is equivalent to what you
|
||||
would get in the console when using --verbose option.
|
||||
|
||||
Finally, there will be a __meta subdirectory. This will contain a copy of the
|
||||
agenda file used to run the workloads along with any other device-specific
|
||||
configuration files used during execution.
|
||||
|
||||
|
||||
list
|
||||
----
|
||||
|
||||
This lists all extensions of a particular type. For example ::
|
||||
|
||||
wa list workloads
|
||||
|
||||
will list all workloads currently included in WA. The list will consist of
|
||||
extension names and short descriptions of the functionality they offer.
|
||||
|
||||
|
||||
show
|
||||
----
|
||||
|
||||
This will show detailed information about an extension, including more in-depth
|
||||
description and any parameters/configuration that are available. For example
|
||||
executing ::
|
||||
|
||||
wa show andebench
|
||||
|
||||
will produce something like ::
|
||||
|
||||
|
||||
andebench
|
||||
|
||||
AndEBench is an industry standard Android benchmark provided by The Embedded Microprocessor Benchmark Consortium
|
||||
(EEMBC).
|
||||
|
||||
parameters:
|
||||
|
||||
number_of_threads
|
||||
Number of threads that will be spawned by AndEBench.
|
||||
type: int
|
||||
|
||||
single_threaded
|
||||
If ``true``, AndEBench will run with a single thread. Note: this must not be specified if ``number_of_threads``
|
||||
has been specified.
|
||||
type: bool
|
||||
|
||||
http://www.eembc.org/andebench/about.php
|
||||
|
||||
From the website:
|
||||
|
||||
- Initial focus on CPU and Dalvik interpreter performance
|
||||
- Internal algorithms concentrate on integer operations
|
||||
- Compares the difference between native and Java performance
|
||||
- Implements flexible multicore performance analysis
|
||||
- Results displayed in Iterations per second
|
||||
- Detailed log file for comprehensive engineering analysis
|
||||
|
||||
|
||||
|
162
doc/source/quickstart.rst
Normal file
162
doc/source/quickstart.rst
Normal file
@ -0,0 +1,162 @@
|
||||
==========
|
||||
Quickstart
|
||||
==========
|
||||
|
||||
This sections will show you how to quickly start running workloads using
|
||||
Workload Automation 2.
|
||||
|
||||
|
||||
Install
|
||||
=======
|
||||
|
||||
.. note:: This is a quick summary. For more detailed instructions, please see
|
||||
the :doc:`installation` section.
|
||||
|
||||
Make sure you have Python 2.7 and a recent Android SDK with API level 18 or above
|
||||
installed on your system. For the SDK, make sure that either ``ANDROID_HOME``
|
||||
environment variable is set, or that ``adb`` is in your ``PATH``.
|
||||
|
||||
.. note:: A complete install of the Android SDK is required, as WA uses a
|
||||
number of its utilities, not just adb.
|
||||
|
||||
In addition to the base Python 2.7 install, you will also need to have ``pip``
|
||||
(Python's package manager) installed as well. This is usually a separate package.
|
||||
|
||||
Once you have the pre-requisites and a tarball with the workload automation package,
|
||||
you can install it with pip::
|
||||
|
||||
sudo pip install wlauto-2.2.0dev.tar.gz
|
||||
|
||||
This will install Workload Automation on your system, along with the Python
|
||||
packages it depends on.
|
||||
|
||||
(Optional) Verify installation
|
||||
-------------------------------
|
||||
|
||||
Once the tarball has been installed, try executing ::
|
||||
|
||||
wa -h
|
||||
|
||||
You should see a help message outlining available subcommands.
|
||||
|
||||
|
||||
(Optional) APK files
|
||||
--------------------
|
||||
|
||||
A large number of WA workloads are installed as APK files. These cannot be
|
||||
distributed with WA and so you will need to obtain those separately.
|
||||
|
||||
For more details, please see the :doc:`installation` section.
|
||||
|
||||
|
||||
Configure Your Device
|
||||
=====================
|
||||
|
||||
Out of the box, WA is configured to work with a generic Android device through
|
||||
``adb``. If you only have one device listed when you execute ``adb devices``,
|
||||
and your device has a standard Android configuration, then no extra configuration
|
||||
is required (if your device is connected via network, you will have to manually execute
|
||||
``adb connect <device ip>`` so that it appears in the device listing).
|
||||
|
||||
If you have multiple devices connected, you will need to tell WA which one you
|
||||
want it to use. You can do that by setting ``adb_name`` in device configuration inside
|
||||
``~/.workload_automation/config.py``\ , e.g.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# ...
|
||||
|
||||
device_config = dict(
|
||||
adb_name = 'abcdef0123456789',
|
||||
# ...
|
||||
)
|
||||
|
||||
# ...
|
||||
|
||||
This should give you basic functionality. If your device has non-standard
|
||||
Android configuration (e.g. it's a development board) or your need some advanced
|
||||
functionality (e.g. big.LITTLE tuning parameters), additional configuration may
|
||||
be required. Please see the :doc:`device_setup` section for more details.
|
||||
|
||||
|
||||
Running Your First Workload
|
||||
===========================
|
||||
|
||||
The simplest way to run a workload is to specify it as a parameter to WA ``run``
|
||||
sub-command::
|
||||
|
||||
wa run dhrystone
|
||||
|
||||
You will see INFO output from WA as it executes each stage of the run. A
|
||||
completed run output should look something like this::
|
||||
|
||||
INFO Initializing
|
||||
INFO Running workloads
|
||||
INFO Connecting to device
|
||||
INFO Initializing device
|
||||
INFO Running workload 1 dhrystone (iteration 1)
|
||||
INFO Setting up
|
||||
INFO Executing
|
||||
INFO Processing result
|
||||
INFO Tearing down
|
||||
INFO Processing overall results
|
||||
INFO Status available in wa_output/status.txt
|
||||
INFO Done.
|
||||
INFO Ran a total of 1 iterations: 1 OK
|
||||
INFO Results can be found in wa_output
|
||||
|
||||
Once the run has completed, you will find a directory called ``wa_output``
|
||||
in the location where you have invoked ``wa run``. Within this directory,
|
||||
you will find a "results.csv" file which will contain results obtained for
|
||||
dhrystone, as well as a "run.log" file containing detailed log output for
|
||||
the run. You will also find a sub-directory called 'drystone_1_1' that
|
||||
contains the results for that iteration. Finally, you will find a copy of the
|
||||
agenda file in the ``wa_output/__meta`` subdirectory. The contents of
|
||||
iteration-specific subdirectories will vary from workload to workload, and,
|
||||
along with the contents of the main output directory, will depend on the
|
||||
instrumentation and result processors that were enabled for that run.
|
||||
|
||||
The ``run`` sub-command takes a number of options that control its behavior,
|
||||
you can view those by executing ``wa run -h``. Please see the :doc:`invocation`
|
||||
section for details.
|
||||
|
||||
|
||||
Create an Agenda
|
||||
================
|
||||
|
||||
Simply running a single workload is normally of little use. Typically, you would
|
||||
want to specify several workloads, setup the device state and, possibly, enable
|
||||
additional instrumentation. To do this, you would need to create an "agenda" for
|
||||
the run that outlines everything you want WA to do.
|
||||
|
||||
Agendas are written using YAML_ markup language. A simple agenda might look
|
||||
like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
config:
|
||||
instrumentation: [~execution_time]
|
||||
result_processors: [json]
|
||||
global:
|
||||
iterations: 2
|
||||
workloads:
|
||||
- memcpy
|
||||
- name: dhrystone
|
||||
params:
|
||||
mloops: 5
|
||||
threads: 1
|
||||
|
||||
This agenda
|
||||
|
||||
- Specifies two workloads: memcpy and dhrystone.
|
||||
- Specifies that dhrystone should run in one thread and execute five million loops.
|
||||
- Specifies that each of the two workloads should be run twice.
|
||||
- Enables json result processor, in addition to the result processors enabled in
|
||||
the config.py.
|
||||
- Disables execution_time instrument, if it is enabled in the config.py
|
||||
|
||||
There is a lot more that could be done with an agenda. Please see :doc:`agenda`
|
||||
section for details.
|
||||
|
||||
.. _YAML: http://en.wikipedia.org/wiki/YAML
|
||||
|
45
doc/source/resources.rst
Normal file
45
doc/source/resources.rst
Normal file
@ -0,0 +1,45 @@
|
||||
Dynamic Resource Resolution
|
||||
===========================
|
||||
|
||||
Introduced in version 2.1.3.
|
||||
|
||||
The idea is to decouple resource identification from resource discovery.
|
||||
Workloads/instruments/devices/etc state *what* resources they need, and not
|
||||
*where* to look for them -- this instead is left to the resource resolver that
|
||||
is now part of the execution context. The actual discovery of resources is
|
||||
performed by resource getters that are registered with the resolver.
|
||||
|
||||
A resource type is defined by a subclass of
|
||||
:class:`wlauto.core.resource.Resource`. An instance of this class describes a
|
||||
resource that is to be obtained. At minimum, a ``Resource`` instance has an
|
||||
owner (which is typically the object that is looking for the resource), but
|
||||
specific resource types may define other parameters that describe an instance of
|
||||
that resource (such as file names, URLs, etc).
|
||||
|
||||
An object looking for a resource invokes a resource resolver with an instance of
|
||||
``Resource`` describing the resource it is after. The resolver goes through the
|
||||
getters registered for that resource type in priority order attempting to obtain
|
||||
the resource; once the resource is obtained, it is returned to the calling
|
||||
object. If none of the registered getters could find the resource, ``None`` is
|
||||
returned instead.
|
||||
|
||||
The most common kind of object looking for resources is a ``Workload``, and
|
||||
since v2.1.3, ``Workload`` class defines
|
||||
:py:meth:`wlauto.core.workload.Workload.init_resources` method that may be
|
||||
overridden by subclasses to perform resource resolution. For example, a workload
|
||||
looking for an APK file would do so like this::
|
||||
|
||||
from wlauto import Workload
|
||||
from wlauto.common.resources import ApkFile
|
||||
|
||||
class AndroidBenchmark(Workload):
|
||||
|
||||
# ...
|
||||
|
||||
def init_resources(self, context):
|
||||
self.apk_file = context.resource.get(ApkFile(self))
|
||||
|
||||
# ...
|
||||
|
||||
|
||||
Currently available resource types are defined in :py:mod:`wlauto.common.resources`.
|
97
doc/source/revent.rst
Normal file
97
doc/source/revent.rst
Normal file
@ -0,0 +1,97 @@
|
||||
.. _revent_files_creation:
|
||||
|
||||
revent
|
||||
======
|
||||
|
||||
revent utility can be used to record and later play back a sequence of user
|
||||
input events, such as key presses and touch screen taps. This is an alternative
|
||||
to Android UI Automator for providing automation for workloads. ::
|
||||
|
||||
|
||||
usage:
|
||||
revent [record time file|replay file|info] [verbose]
|
||||
record: stops after either return on stdin
|
||||
or time (in seconds)
|
||||
and stores in file
|
||||
replay: replays eventlog from file
|
||||
info:shows info about each event char device
|
||||
any additional parameters make it verbose
|
||||
|
||||
Recording
|
||||
---------
|
||||
|
||||
To record, transfer the revent binary to the device, then invoke ``revent
|
||||
record``, giving it the time (in seconds) you want to record for, and the
|
||||
file you want to record to (WA expects these files to have .revent
|
||||
extension)::
|
||||
|
||||
host$ adb push revent /data/local/revent
|
||||
host$ adb shell
|
||||
device# cd /data/local
|
||||
device# ./revent record 1000 my_recording.revent
|
||||
|
||||
The recording has now started and button presses, taps, etc you perform on the
|
||||
device will go into the .revent file. The recording will stop after the
|
||||
specified time period, and you can also stop it by hitting return in the adb
|
||||
shell.
|
||||
|
||||
Replaying
|
||||
---------
|
||||
|
||||
To replay a recorded file, run ``revent replay`` on the device, giving it the
|
||||
file you want to replay::
|
||||
|
||||
device# ./revent replay my_recording.revent
|
||||
|
||||
|
||||
Using revent With Workloads
|
||||
---------------------------
|
||||
|
||||
Some workloads (pretty much all games) rely on recorded revents for their
|
||||
execution. :class:`wlauto.common.GameWorkload`-derived workloads expect two
|
||||
revent files -- one for performing the initial setup (navigating menus,
|
||||
selecting game modes, etc), and one for the actual execution of the game.
|
||||
Because revents are very device-specific\ [*]_, these two files would need to
|
||||
be recorded for each device.
|
||||
|
||||
The files must be called ``<device name>.(setup|run).revent``, where
|
||||
``<device name>`` is the name of your device (as defined by the ``name``
|
||||
attribute of your device's class). WA will look for these files in two
|
||||
places: ``<install dir>/wlauto/workloads/<workload name>/revent_files``
|
||||
and ``~/.workload_automation/dependencies/<workload name>``. The first
|
||||
location is primarily intended for revent files that come with WA (and if
|
||||
you did a system-wide install, you'll need sudo to add files there), so it's
|
||||
probably easier to use the second location for the files you record. Also,
|
||||
if revent files for a workload exist in both locations, the files under
|
||||
``~/.workload_automation/dependencies`` will be used in favor of those
|
||||
installed with WA.
|
||||
|
||||
For example, if you wanted to run angrybirds workload on "Acme" device, you would
|
||||
record the setup and run revent files using the method outlined in the section
|
||||
above and then pull them for the devices into the following locations::
|
||||
|
||||
~/workload_automation/dependencies/angrybirds/Acme.setup.revent
|
||||
~/workload_automation/dependencies/angrybirds/Acme.run.revent
|
||||
|
||||
(you may need to create the intermediate directories if they don't already
|
||||
exist).
|
||||
|
||||
.. [*] It's not just about screen resolution -- the event codes may be different
|
||||
even if devices use the same screen.
|
||||
|
||||
|
||||
revent vs. UiAutomator
|
||||
----------------------
|
||||
|
||||
In general, Android UI Automator is the preferred way of automating user input
|
||||
for workloads because, unlike revent, UI Automator does not depend on a
|
||||
particular screen resolution, and so is more portable across different devices.
|
||||
It also gives better control and can potentially be faster for ling UI
|
||||
manipulations, as input events are scripted based on the available UI elements,
|
||||
rather than generated by human input.
|
||||
|
||||
On the other hand, revent can be used to manipulate pretty much any workload,
|
||||
where as UI Automator only works for Android UI elements (such as text boxes or
|
||||
radio buttons), which makes the latter useless for things like games. Recording
|
||||
revent sequence is also faster than writing automation code (on the other hand,
|
||||
one would need maintain a different revent log for each screen resolution).
|
BIN
doc/source/wa-execution.png
Normal file
BIN
doc/source/wa-execution.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
956
doc/source/writing_extensions.rst
Normal file
956
doc/source/writing_extensions.rst
Normal file
@ -0,0 +1,956 @@
|
||||
==================
|
||||
Writing Extensions
|
||||
==================
|
||||
|
||||
Workload Automation offers several extension points (or plugin types).The most
|
||||
interesting of these are
|
||||
|
||||
:workloads: These are the tasks that get executed and measured on the device. These
|
||||
can be benchmarks, high-level use cases, or pretty much anything else.
|
||||
:devices: These are interfaces to the physical devices (development boards or end-user
|
||||
devices, such as smartphones) that use cases run on. Typically each model of a
|
||||
physical device would require it's own interface class (though some functionality
|
||||
may be reused by subclassing from an existing base).
|
||||
:instruments: Instruments allow collecting additional data from workload execution (e.g.
|
||||
system traces). Instruments are not specific to a particular Workload. Instruments
|
||||
can hook into any stage of workload execution.
|
||||
:result processors: These are used to format the results of workload execution once they have been
|
||||
collected. Depending on the callback used, these will run either after each
|
||||
iteration or at the end of the run, after all of the results have been
|
||||
collected.
|
||||
|
||||
You create an extension by subclassing the appropriate base class, defining
|
||||
appropriate methods and attributes, and putting the .py file with the class into
|
||||
an appropriate subdirectory under ``~/.workload_automation`` (there is one for
|
||||
each extension type).
|
||||
|
||||
|
||||
Extension Basics
|
||||
================
|
||||
|
||||
This sub-section covers things common to implementing extensions of all types.
|
||||
It is recommended you familiarize yourself with the information here before
|
||||
proceeding onto guidance for specific extension types.
|
||||
|
||||
To create an extension, you basically subclass an appropriate base class and them
|
||||
implement the appropriate methods
|
||||
|
||||
The Context
|
||||
-----------
|
||||
|
||||
The majority of methods in extensions accept a context argument. This is an
|
||||
instance of :class:`wlauto.core.execution.ExecutionContext`. If contains
|
||||
of information about current state of execution of WA and keeps track of things
|
||||
like which workload is currently running and the current iteration.
|
||||
|
||||
Notable attributes of the context are
|
||||
|
||||
context.spec
|
||||
the current workload specification being executed. This is an
|
||||
instance of :class:`wlauto.core.configuration.WorkloadRunSpec`
|
||||
and defines the workload and the parameters under which it is
|
||||
being executed.
|
||||
|
||||
context.workload
|
||||
``Workload`` object that is currently being executed.
|
||||
|
||||
context.current_iteration
|
||||
The current iteration of the spec that is being executed. Note that this
|
||||
is the iteration for that spec, i.e. the number of times that spec has
|
||||
been run, *not* the total number of all iterations have been executed so
|
||||
far.
|
||||
|
||||
context.result
|
||||
This is the result object for the current iteration. This is an instance
|
||||
of :class:`wlauto.core.result.IterationResult`. It contains the status
|
||||
of the iteration as well as the metrics and artifacts generated by the
|
||||
workload and enable instrumentation.
|
||||
|
||||
context.device
|
||||
The device interface object that can be used to interact with the
|
||||
device. Note that workloads and instruments have their own device
|
||||
attribute and they should be using that instead.
|
||||
|
||||
In addition to these, context also defines a few useful paths (see below).
|
||||
|
||||
|
||||
Paths
|
||||
-----
|
||||
|
||||
You should avoid using hard-coded absolute paths in your extensions whenever
|
||||
possible, as they make your code too dependent on a particular environment and
|
||||
may mean having to make adjustments when moving to new (host and/or device)
|
||||
platforms. To help avoid hard-coded absolute paths, WA automation defines
|
||||
a number of standard locations. You should strive to define your paths relative
|
||||
to one of those.
|
||||
|
||||
On the host
|
||||
~~~~~~~~~~~
|
||||
|
||||
Host paths are available through the context object, which is passed to most
|
||||
extension methods.
|
||||
|
||||
context.run_output_directory
|
||||
This is the top-level output directory for all WA results (by default,
|
||||
this will be "wa_output" in the directory in which WA was invoked.
|
||||
|
||||
context.output_directory
|
||||
This is the output directory for the current iteration. This will an
|
||||
iteration-specific subdirectory under the main results location. If
|
||||
there is no current iteration (e.g. when processing overall run results)
|
||||
this will point to the same location as ``root_output_directory``.
|
||||
|
||||
context.host_working_directory
|
||||
This an addition location that may be used by extensions to store
|
||||
non-iteration specific intermediate files (e.g. configuration).
|
||||
|
||||
Additionally, the global ``wlauto.settings`` object exposes on other location:
|
||||
|
||||
settings.dependency_directory
|
||||
this is the root directory for all extension dependencies (e.g. media
|
||||
files, assets etc) that are not included within the extension itself.
|
||||
|
||||
As per Python best practice, it is recommended that methods and values in
|
||||
``os.path`` standard library module are used for host path manipulation.
|
||||
|
||||
On the device
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Workloads and instruments have a ``device`` attribute, which is an interface to
|
||||
the device used by WA. It defines the following location:
|
||||
|
||||
device.working_directory
|
||||
This is the directory for all WA-related files on the device. All files
|
||||
deployed to the device should be pushed to somewhere under this location
|
||||
(the only exception being executables installed with ``device.install``
|
||||
method).
|
||||
|
||||
Since there could be a mismatch between path notation used by the host and the
|
||||
device, the ``os.path`` modules should *not* be used for on-device path
|
||||
manipulation. Instead device has an equipment module exposed through
|
||||
``device.path`` attribute. This has all the same attributes and behaves the
|
||||
same way as ``os.path``, but is guaranteed to produce valid paths for the device,
|
||||
irrespective of the host's path notation.
|
||||
|
||||
.. note:: result processors, unlike workloads and instruments, do not have their
|
||||
own device attribute; however they can access the device through the
|
||||
context.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
All extensions can be parameterized. Parameters are specified using
|
||||
``parameters`` class attribute. This should be a list of
|
||||
:class:`wlauto.core.Parameter` instances. The following attributes can be
|
||||
specified on parameter creation:
|
||||
|
||||
name
|
||||
This is the only mandatory argument. The name will be used to create a
|
||||
corresponding attribute in the extension instance, so it must be a valid
|
||||
Python identifier.
|
||||
|
||||
kind
|
||||
This is the type of the value of the parameter. This could be an
|
||||
callable. Normally this should be a standard Python type, e.g. ``int`
|
||||
or ``float``, or one the types defined in :mod:`wlauto.utils.types`.
|
||||
If not explicitly specified, this will default to ``str``.
|
||||
|
||||
.. note:: Irrespective of the ``kind`` specified, ``None`` is always a
|
||||
valid value for a parameter. If you don't want to allow
|
||||
``None``, then set ``mandatory`` (see below) to ``True``.
|
||||
|
||||
allowed_values
|
||||
A list of the only allowed values for this parameter.
|
||||
|
||||
.. note:: For composite types, such as ``list_of_strings`` or
|
||||
``list_of_ints`` in :mod:`wlauto.utils.types`, each element of
|
||||
the value will be checked against ``allowed_values`` rather
|
||||
than the composite value itself.
|
||||
|
||||
default
|
||||
The default value to be used for this parameter if one has not been
|
||||
specified by the user. Defaults to ``None``.
|
||||
|
||||
mandatory
|
||||
A ``bool`` indicating whether this parameter is mandatory. Setting this
|
||||
to ``True`` will make ``None`` an illegal value for the parameter.
|
||||
Defaults to ``False``.
|
||||
|
||||
.. note:: Specifying a ``default`` will mean that this parameter will,
|
||||
effectively, be ignored (unless the user sets the param to ``None``).
|
||||
|
||||
.. note:: Mandatory parameters are *bad*. If at all possible, you should
|
||||
strive to provide a sensible ``default`` or to make do without
|
||||
the parameter. Only when the param is absolutely necessary,
|
||||
and there really is no sensible default that could be given
|
||||
(e.g. something like login credentials), should you consider
|
||||
making it mandatory.
|
||||
|
||||
constraint
|
||||
This is an additional constraint to be enforced on the parameter beyond
|
||||
its type or fixed allowed values set. This should be a predicate (a function
|
||||
that takes a single argument -- the user-supplied value -- and returns
|
||||
a ``bool`` indicating whether the constraint has been satisfied).
|
||||
|
||||
override
|
||||
A parameter name must be unique not only within an extension but also
|
||||
with that extension's class hierarchy. If you try to declare a parameter
|
||||
with the same name as already exists, you will get an error. If you do
|
||||
want to override a parameter from further up in the inheritance
|
||||
hierarchy, you can indicate that by setting ``override`` attribute to
|
||||
``True``.
|
||||
|
||||
When overriding, you do not need to specify every other attribute of the
|
||||
parameter, just the ones you what to override. Values for the rest will
|
||||
be taken from the parameter in the base class.
|
||||
|
||||
|
||||
Validation and cross-parameter constraints
|
||||
------------------------------------------
|
||||
|
||||
An extension will get validated at some point after constructions. When exactly
|
||||
this occurs depends on the extension type, but it *will* be validated before it
|
||||
is used.
|
||||
|
||||
You can implement ``validate`` method in your extension (that takes no arguments
|
||||
beyond the ``self``) to perform any additions *internal* validation in your
|
||||
extension. By "internal", I mean that you cannot make assumptions about the
|
||||
surrounding environment (e.g. that the device has been initialized).
|
||||
|
||||
The contract for ``validate`` method is that it should raise an exception
|
||||
(either ``wlauto.exceptions.ConfigError`` or extension-specific exception type -- see
|
||||
further on this page) if some validation condition has not, and cannot, been met.
|
||||
If the method returns without raising an exception, then the extension is in a
|
||||
valid internal state.
|
||||
|
||||
Note that ``validate`` can be used not only to verify, but also to impose a
|
||||
valid internal state. In particular, this where cross-parameter constraints can
|
||||
be resolved. If the ``default`` or ``allowed_values`` of one parameter depend on
|
||||
another parameter, there is no way to express that declaratively when specifying
|
||||
the parameters. In that case the dependent attribute should be left unspecified
|
||||
on creation and should instead be set inside ``validate``.
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
Every extension class has it's own logger that you can access through
|
||||
``self.logger`` inside the extension's methods. Generally, a :class:`Device` will log
|
||||
everything it is doing, so you shouldn't need to add much additional logging in
|
||||
your expansion's. But you might what to log additional information, e.g.
|
||||
what settings your extension is using, what it is doing on the host, etc.
|
||||
Operations on the host will not normally be logged, so your extension should
|
||||
definitely log what it is doing on the host. One situation in particular where
|
||||
you should add logging is before doing something that might take a significant amount
|
||||
of time, such as downloading a file.
|
||||
|
||||
|
||||
Documenting
|
||||
-----------
|
||||
|
||||
All extensions and their parameter should be documented. For extensions
|
||||
themselves, this is done through ``description`` class attribute. The convention
|
||||
for an extension description is that the first paragraph should be a short
|
||||
summary description of what the extension does and why one would want to use it
|
||||
(among other things, this will get extracted and used by ``wa list`` command).
|
||||
Subsequent paragraphs (separated by blank lines) can then provide a more
|
||||
detailed description, including any limitations and setup instructions.
|
||||
|
||||
For parameters, the description is passed as an argument on creation. Please
|
||||
note that if ``default``, ``allowed_values``, or ``constraint``, are set in the
|
||||
parameter, they do not need to be explicitly mentioned in the description (wa
|
||||
documentation utilities will automatically pull those). If the ``default`` is set
|
||||
in ``validate`` or additional cross-parameter constraints exist, this *should*
|
||||
be documented in the parameter description.
|
||||
|
||||
Both extensions and their parameters should be documented using reStructureText
|
||||
markup (standard markup for Python documentation). See:
|
||||
|
||||
http://docutils.sourceforge.net/rst.html
|
||||
|
||||
Aside from that, it is up to you how you document your extension. You should try
|
||||
to provide enough information so that someone unfamiliar with your extension is
|
||||
able to use it, e.g. you should document all settings and parameters your
|
||||
extension expects (including what the valid value are).
|
||||
|
||||
|
||||
Error Notification
|
||||
------------------
|
||||
|
||||
When you detect an error condition, you should raise an appropriate exception to
|
||||
notify the user. The exception would typically be :class:`ConfigError` or
|
||||
(depending the type of the extension)
|
||||
:class:`WorkloadError`/:class:`DeviceError`/:class:`InstrumentError`/:class:`ResultProcessorError`.
|
||||
All these errors are defined in :mod:`wlauto.exception` module.
|
||||
|
||||
:class:`ConfigError` should be raised where there is a problem in configuration
|
||||
specified by the user (either through the agenda or config files). These errors
|
||||
are meant to be resolvable by simple adjustments to the configuration (and the
|
||||
error message should suggest what adjustments need to be made. For all other
|
||||
errors, such as missing dependencies, mis-configured environment, problems
|
||||
performing operations, etc., the extension type-specific exceptions should be
|
||||
used.
|
||||
|
||||
If the extension itself is capable of recovering from the error and carrying
|
||||
on, it may make more sense to log an ERROR or WARNING level message using the
|
||||
extension's logger and to continue operation.
|
||||
|
||||
|
||||
Utils
|
||||
-----
|
||||
|
||||
Workload Automation defines a number of utilities collected under
|
||||
:mod:`wlauto.utils` subpackage. These utilities were created to help with the
|
||||
implementation of the framework itself, but may be also be useful when
|
||||
implementing extensions.
|
||||
|
||||
|
||||
Adding a Workload
|
||||
=================
|
||||
|
||||
.. note:: You can use ``wa create workload [name]`` script to generate a new workload
|
||||
structure for you. This script can also create the boilerplate for
|
||||
UI automation, if your workload needs it. See ``wa create -h`` for more
|
||||
details.
|
||||
|
||||
New workloads can be added by subclassing :class:`wlauto.core.workload.Workload`
|
||||
|
||||
|
||||
The Workload class defines the following interface::
|
||||
|
||||
class Workload(Extension):
|
||||
|
||||
name = None
|
||||
|
||||
def init_resources(self, context):
|
||||
pass
|
||||
|
||||
def setup(self, context):
|
||||
raise NotImplementedError()
|
||||
|
||||
def run(self, context):
|
||||
raise NotImplementedError()
|
||||
|
||||
def update_result(self, context):
|
||||
raise NotImplementedError()
|
||||
|
||||
def teardown(self, context):
|
||||
raise NotImplementedError()
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
|
||||
.. note:: Please see :doc:`conventions` section for notes on how to interpret
|
||||
this.
|
||||
|
||||
The interface should be implemented as follows
|
||||
|
||||
:name: This identifies the workload (e.g. it used to specify it in the
|
||||
agenda_.
|
||||
:init_resources: This method may be optionally override to implement dynamic
|
||||
resource discovery for the workload.
|
||||
**Added in version 2.1.3**
|
||||
:setup: Everything that needs to be in place for workload execution should
|
||||
be done in this method. This includes copying files to the device,
|
||||
starting up an application, configuring communications channels,
|
||||
etc.
|
||||
:run: This method should perform the actual task that is being measured.
|
||||
When this method exits, the task is assumed to be complete.
|
||||
|
||||
.. note:: Instrumentation is kicked off just before calling this
|
||||
method and is disabled right after, so everything in this
|
||||
method is being measured. Therefore this method should
|
||||
contain the least code possible to perform the operations
|
||||
you are interested in measuring. Specifically, things like
|
||||
installing or starting applications, processing results, or
|
||||
copying files to/from the device should be done elsewhere if
|
||||
possible.
|
||||
|
||||
:update_result: This method gets invoked after the task execution has
|
||||
finished and should be used to extract metrics and add them
|
||||
to the result (see below).
|
||||
:teardown: This could be used to perform any cleanup you may wish to do,
|
||||
e.g. Uninstalling applications, deleting file on the device, etc.
|
||||
|
||||
:validate: This method can be used to validate any assumptions your workload
|
||||
makes about the environment (e.g. that required files are
|
||||
present, environment variables are set, etc) and should raise
|
||||
a :class:`wlauto.exceptions.WorkloadError` if that is not the
|
||||
case. The base class implementation only makes sure sure that
|
||||
the name attribute has been set.
|
||||
|
||||
.. _agenda: agenda.html
|
||||
|
||||
Workload methods (except for ``validate``) take a single argument that is a
|
||||
:class:`wlauto.core.execution.ExecutionContext` instance. This object keeps
|
||||
track of the current execution state (such as the current workload, iteration
|
||||
number, etc), and contains, among other things, a
|
||||
:class:`wlauto.core.workload.WorkloadResult` instance that should be populated
|
||||
from the ``update_result`` method with the results of the execution. ::
|
||||
|
||||
# ...
|
||||
|
||||
def update_result(self, context):
|
||||
# ...
|
||||
context.result.add_metric('energy', 23.6, 'Joules', lower_is_better=True)
|
||||
|
||||
# ...
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
This example shows a simple workload that times how long it takes to compress a
|
||||
file of a particular size on the device.
|
||||
|
||||
.. note:: This is intended as an example of how to implement the Workload
|
||||
interface. The methodology used to perform the actual measurement is
|
||||
not necessarily sound, and this Workload should not be used to collect
|
||||
real measurements.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import os
|
||||
from wlauto import Workload, Parameter
|
||||
|
||||
class ZiptestWorkload(Workload):
|
||||
|
||||
name = 'ziptest'
|
||||
description = '''
|
||||
Times how long it takes to gzip a file of a particular size on a device.
|
||||
|
||||
This workload was created for illustration purposes only. It should not be
|
||||
used to collect actual measurements.
|
||||
|
||||
'''
|
||||
|
||||
parameters = [
|
||||
Parameter('file_size', kind=int, default=2000000,
|
||||
description='Size of the file (in bytes) to be gzipped.')
|
||||
]
|
||||
|
||||
def setup(self, context):
|
||||
# Generate a file of the specified size containing random garbage.
|
||||
host_infile = os.path.join(context.output_directory, 'infile')
|
||||
command = 'openssl rand -base64 {} > {}'.format(self.file_size, host_infile)
|
||||
os.system(command)
|
||||
# Set up on-device paths
|
||||
devpath = self.device.path # os.path equivalent for the device
|
||||
self.device_infile = devpath.join(self.device.working_directory, 'infile')
|
||||
self.device_outfile = devpath.join(self.device.working_directory, 'outfile')
|
||||
# Push the file to the device
|
||||
self.device.push_file(host_infile, self.device_infile)
|
||||
|
||||
def run(self, context):
|
||||
self.device.execute('cd {} && (time gzip {}) &>> {}'.format(self.device.working_directory,
|
||||
self.device_infile,
|
||||
self.device_outfile))
|
||||
|
||||
def update_result(self, context):
|
||||
# Pull the results file to the host
|
||||
host_outfile = os.path.join(context.output_directory, 'outfile')
|
||||
self.device.pull_file(self.device_outfile, host_outfile)
|
||||
# Extract metrics form the file's contents and update the result
|
||||
# with them.
|
||||
content = iter(open(host_outfile).read().strip().split())
|
||||
for value, metric in zip(content, content):
|
||||
mins, secs = map(float, value[:-1].split('m'))
|
||||
context.result.add_metric(metric, secs + 60 * mins)
|
||||
|
||||
def teardown(self, context):
|
||||
# Clean up on-device file.
|
||||
self.device.delete_file(self.device_infile)
|
||||
self.device.delete_file(self.device_outfile)
|
||||
|
||||
|
||||
|
||||
.. _GameWorkload:
|
||||
|
||||
Adding revent-dependent Workload:
|
||||
---------------------------------
|
||||
|
||||
:class:`wlauto.common.game.GameWorkload` is the base class for all the workloads
|
||||
that depend on :ref:`revent_files_creation` files. It implements all the methods
|
||||
needed to push the files to the device and run them. New GameWorkload can be
|
||||
added by subclassing :class:`wlauto.common.game.GameWorkload`:
|
||||
|
||||
The GameWorkload class defines the following interface::
|
||||
|
||||
class GameWorkload(Workload):
|
||||
|
||||
name = None
|
||||
package = None
|
||||
activity = None
|
||||
|
||||
The interface should be implemented as follows
|
||||
|
||||
:name: This identifies the workload (e.g. it used to specify it in the
|
||||
agenda_.
|
||||
:package: This is the name of the '.apk' package without its file extension.
|
||||
:activity: The name of the main activity that runs the package.
|
||||
|
||||
Example:
|
||||
--------
|
||||
|
||||
This example shows a simple GameWorkload that plays a game.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wlauto.common.game import GameWorkload
|
||||
|
||||
class MyGame(GameWorkload):
|
||||
|
||||
name = 'mygame'
|
||||
package = 'com.mylogo.mygame'
|
||||
activity = 'myActivity.myGame'
|
||||
|
||||
Convention for Naming revent Files for :class:`wlauto.common.game.GameWorkload`
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
There is a convention for naming revent files which you should follow if you
|
||||
want to record your own revent files. Each revent file must start with the
|
||||
device name(case sensitive) then followed by a dot '.' then the stage name
|
||||
then '.revent'. All your custom revent files should reside at
|
||||
'~/.workload_automation/dependencies/WORKLOAD NAME/'. These are the current
|
||||
supported stages:
|
||||
|
||||
:setup: This stage is where the game is loaded. It is a good place to
|
||||
record revent here to modify the game settings and get it ready
|
||||
to start.
|
||||
:run: This stage is where the game actually starts. This will allow for
|
||||
more accurate results if the revent file for this stage only
|
||||
records the game being played.
|
||||
|
||||
For instance, to add a custom revent files for a device named mydevice and
|
||||
a workload name mygame, you create a new directory called mygame in
|
||||
'~/.workload_automation/dependencies/'. Then you add the revent files for
|
||||
the stages you want in ~/.workload_automation/dependencies/mygame/::
|
||||
|
||||
mydevice.setup.revent
|
||||
mydevice.run.revent
|
||||
|
||||
Any revent file in the dependencies will always overwrite the revent file in the
|
||||
workload directory. So it is possible for example to just provide one revent for
|
||||
setup in the dependencies and use the run.revent that is in the workload directory.
|
||||
|
||||
Adding an Instrument
|
||||
====================
|
||||
|
||||
Instruments can be used to collect additional measurements during workload
|
||||
execution (e.g. collect power readings). An instrument can hook into almost any
|
||||
stage of workload execution. A typical instrument would implement a subset of
|
||||
the following interface::
|
||||
|
||||
class Instrument(Extension):
|
||||
|
||||
name = None
|
||||
description = None
|
||||
|
||||
parameters = [
|
||||
]
|
||||
|
||||
def initialize(self, context):
|
||||
pass
|
||||
|
||||
def setup(self, context):
|
||||
pass
|
||||
|
||||
def start(self, context):
|
||||
pass
|
||||
|
||||
def stop(self, context):
|
||||
pass
|
||||
|
||||
def update_result(self, context):
|
||||
pass
|
||||
|
||||
def teardown(self, context):
|
||||
pass
|
||||
|
||||
def finalize(self, context):
|
||||
pass
|
||||
|
||||
This is similar to a Workload, except all methods are optional. In addition to
|
||||
the workload-like methods, instruments can define a number of other methods that
|
||||
will get invoked at various points during run execution. The most useful of
|
||||
which is perhaps ``initialize`` that gets invoked after the device has been
|
||||
initialised for the first time, and can be used to perform one-time setup (e.g.
|
||||
copying files to the device -- there is no point in doing that for each
|
||||
iteration). The full list of available methods can be found in
|
||||
:ref:`Signals Documentation <instrument_name_mapping>`.
|
||||
|
||||
|
||||
Prioritization
|
||||
--------------
|
||||
|
||||
Callbacks (e.g. ``setup()`` methods) for all instrumentation get executed at the
|
||||
same point during workload execution, one after another. The order in which the
|
||||
callbacks get invoked should be considered arbitrary and should not be relied
|
||||
on (e.g. you cannot expect that just because instrument A is listed before
|
||||
instrument B in the config, instrument A's callbacks will run first).
|
||||
|
||||
In some cases (e.g. in ``start()`` and ``stop()`` methods), it is important to
|
||||
ensure that a particular instrument's callbacks run a closely as possible to the
|
||||
workload's invocations in order to maintain accuracy of readings; or,
|
||||
conversely, that a callback is executed after the others, because it takes a
|
||||
long time and may throw off the accuracy of other instrumentation. You can do
|
||||
this by prepending ``fast_`` or ``slow_`` to your callbacks' names. For
|
||||
example::
|
||||
|
||||
class PreciseInstrument(Instument):
|
||||
|
||||
# ...
|
||||
|
||||
def fast_start(self, context):
|
||||
pass
|
||||
|
||||
def fast_stop(self, context):
|
||||
pass
|
||||
|
||||
# ...
|
||||
|
||||
``PreciseInstrument`` will be started after all other instrumentation (i.e.
|
||||
*just* before the workload runs), and it will stopped before all other
|
||||
instrumentation (i.e. *just* after the workload runs). It is also possible to
|
||||
use ``very_fast_`` and ``very_slow_`` prefixes when you want to be really
|
||||
sure that your callback will be the last/first to run.
|
||||
|
||||
If more than one active instrument have specified fast (or slow) callbacks, then
|
||||
their execution order with respect to each other is not guaranteed. In general,
|
||||
having a lot of instrumentation enabled is going to necessarily affect the
|
||||
readings. The best way to ensure accuracy of measurements is to minimize the
|
||||
number of active instruments (perhaps doing several identical runs with
|
||||
different instruments enabled).
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Below is a simple instrument that measures the execution time of a workload::
|
||||
|
||||
class ExecutionTimeInstrument(Instrument):
|
||||
"""
|
||||
Measure how long it took to execute the run() methods of a Workload.
|
||||
|
||||
"""
|
||||
|
||||
name = 'execution_time'
|
||||
|
||||
def initialize(self, context):
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
|
||||
def fast_start(self, context):
|
||||
self.start_time = time.time()
|
||||
|
||||
def fast_stop(self, context):
|
||||
self.end_time = time.time()
|
||||
|
||||
def update_result(self, context):
|
||||
execution_time = self.end_time - self.start_time
|
||||
context.result.add_metric('execution_time', execution_time, 'seconds')
|
||||
|
||||
|
||||
Adding a Result Processor
|
||||
=========================
|
||||
|
||||
A result processor is responsible for processing the results. This may
|
||||
involve formatting and writing them to a file, uploading them to a database,
|
||||
generating plots, etc. WA comes with a few result processors that output
|
||||
results in a few common formats (such as csv or JSON).
|
||||
|
||||
You can add your own result processors by creating a Python file in
|
||||
``~/.workload_automation/result_processors`` with a class that derives from
|
||||
:class:`wlauto.core.result.ResultProcessor`, which has the following interface::
|
||||
|
||||
class ResultProcessor(Extension):
|
||||
|
||||
name = None
|
||||
description = None
|
||||
|
||||
parameters = [
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
|
||||
The method names should be fairly self-explanatory. The difference between
|
||||
"process" and "export" methods is that export methods will be invoke after
|
||||
process methods for all result processors have been generated. Process methods
|
||||
may generated additional artifacts (metrics, files, etc), while export methods
|
||||
should not -- the should only handle existing results (upload them to a
|
||||
database, archive on a filer, etc).
|
||||
|
||||
The result object passed to iteration methods is an instance of
|
||||
:class:`wlauto.core.result.IterationResult`, the result object passed to run
|
||||
methods is an instance of :class:`wlauto.core.result.RunResult`. Please refer to
|
||||
their API documentation for details.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here is an example result processor that formats the results as a column-aligned
|
||||
table::
|
||||
|
||||
import os
|
||||
from wlauto import ResultProcessor
|
||||
from wlauto.utils.misc import write_table
|
||||
|
||||
|
||||
class Table(ResultProcessor):
|
||||
|
||||
name = 'table'
|
||||
description = 'Gerates a text file containing a column-aligned table with run results.'
|
||||
|
||||
def process_run_result(self, result, context):
|
||||
rows = []
|
||||
for iteration_result in result.iteration_results:
|
||||
for metric in iteration_result.metrics:
|
||||
rows.append([metric.name, str(metric.value), metric.units or '',
|
||||
metric.lower_is_better and '-' or '+'])
|
||||
|
||||
outfile = os.path.join(context.output_directory, 'table.txt')
|
||||
with open(outfile, 'w') as wfh:
|
||||
write_table(rows, wfh)
|
||||
|
||||
|
||||
Adding a Resource Getter
|
||||
========================
|
||||
|
||||
A resource getter is a new extension type added in version 2.1.3. A resource
|
||||
getter implement a method of acquiring resources of a particular type (such as
|
||||
APK files or additional workload assets). Resource getters are invoked in
|
||||
priority order until one returns the desired resource.
|
||||
|
||||
If you want WA to look for resources somewhere it doesn't by default (e.g. you
|
||||
have a repository of APK files), you can implement a getter for the resource and
|
||||
register it with a higher priority than the standard WA getters, so that it gets
|
||||
invoked first.
|
||||
|
||||
Instances of a resource getter should implement the following interface::
|
||||
|
||||
class ResourceGetter(Extension):
|
||||
|
||||
name = None
|
||||
resource_type = None
|
||||
priority = GetterPriority.environment
|
||||
|
||||
def get(self, resource, **kwargs):
|
||||
raise NotImplementedError()
|
||||
|
||||
The getter should define a name (as with all extensions), a resource
|
||||
type, which should be a string, e.g. ``'jar'``, and a priority (see `Getter
|
||||
Prioritization`_ below). In addition, ``get`` method should be implemented. The
|
||||
first argument is an instance of :class:`wlauto.core.resource.Resource`
|
||||
representing the resource that should be obtained. Additional keyword
|
||||
arguments may be used by the invoker to provide additional information about
|
||||
the resource. This method should return an instance of the resource that
|
||||
has been discovered (what "instance" means depends on the resource, e.g. it
|
||||
could be a file path), or ``None`` if this getter was unable to discover
|
||||
that resource.
|
||||
|
||||
Getter Prioritization
|
||||
---------------------
|
||||
|
||||
A priority is an integer with higher numeric values indicating a higher
|
||||
priority. The following standard priority aliases are defined for getters:
|
||||
|
||||
|
||||
: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.
|
||||
: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.
|
||||
|
||||
These priorities are defined as class members of
|
||||
:class:`wlauto.core.resource.GetterPriority`, e.g. ``GetterPriority.cached``.
|
||||
|
||||
Most getters in WA will be registered with either ``environment`` or
|
||||
``package`` priorities. So if you want your getter to override the default, it
|
||||
should typically be registered as ``preferred``.
|
||||
|
||||
You don't have to stick to standard priority levels (though you should, unless
|
||||
there is a good reason). Any integer is a valid priority. The standard priorities
|
||||
range from -20 to 20 in increments of 10.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
The following is an implementation of a getter for a workload APK file that
|
||||
looks for the file under
|
||||
``~/.workload_automation/dependencies/<workload_name>``::
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
from wlauto import ResourceGetter, GetterPriority, settings
|
||||
from wlauto.exceptions import ResourceError
|
||||
|
||||
|
||||
class EnvironmentApkGetter(ResourceGetter):
|
||||
|
||||
name = 'environment_apk'
|
||||
resource_type = 'apk'
|
||||
priority = GetterPriority.environment
|
||||
|
||||
def get(self, resource):
|
||||
resource_dir = _d(os.path.join(settings.dependency_directory, resource.owner.name))
|
||||
version = kwargs.get('version')
|
||||
found_files = glob.glob(os.path.join(resource_dir, '*.apk'))
|
||||
if version:
|
||||
found_files = [ff for ff in found_files if version.lower() in ff.lower()]
|
||||
if len(found_files) == 1:
|
||||
return found_files[0]
|
||||
elif not found_files:
|
||||
return None
|
||||
else:
|
||||
raise ResourceError('More than one .apk found in {} for {}.'.format(resource_dir,
|
||||
resource.owner.name))
|
||||
|
||||
.. _adding_a_device:
|
||||
|
||||
Adding a Device
|
||||
===============
|
||||
|
||||
At the moment, only Android devices are supported. Most of the functionality for
|
||||
interacting with a device is implemented in
|
||||
:class:`wlauto.common.AndroidDevice` and is exposed through ``generic_android``
|
||||
device interface, which should suffice for most purposes. The most common area
|
||||
where custom functionality may need to be implemented is during device
|
||||
initialization. Usually, once the device gets to the Android home screen, it's
|
||||
just like any other Android device (modulo things like differences between
|
||||
Android versions).
|
||||
|
||||
If your device doesn't not work with ``generic_device`` interface and you need
|
||||
to write a custom interface to handle it, you would do that by subclassing
|
||||
``AndroidDevice`` and then just overriding the methods you need. Typically you
|
||||
will want to override one or more of the following:
|
||||
|
||||
reset
|
||||
Trigger a device reboot. The default implementation just sends ``adb
|
||||
reboot`` to the device. If this command does not work, an alternative
|
||||
implementation may need to be provided.
|
||||
|
||||
hard_reset
|
||||
This is a harsher reset that involves cutting the power to a device
|
||||
(e.g. holding down power button or removing battery from a phone). The
|
||||
default implementation is a no-op that just sets some internal flags. If
|
||||
you're dealing with unreliable prototype hardware that can crash and
|
||||
become unresponsive, you may want to implement this in order for WA to
|
||||
be able to recover automatically.
|
||||
|
||||
connect
|
||||
When this method returns, adb connection to the device has been
|
||||
established. This gets invoked after a reset. The default implementation
|
||||
just waits for the device to appear in the adb list of connected
|
||||
devices. If this is not enough (e.g. your device is connected via
|
||||
Ethernet and requires an explicit ``adb connect`` call), you may wish to
|
||||
override this to perform the necessary actions before invoking the
|
||||
``AndroidDevice``\ s version.
|
||||
|
||||
init
|
||||
This gets called once at the beginning of the run once the connection to
|
||||
the device has been established. There is no default implementation.
|
||||
It's there to allow whatever custom initialisation may need to be
|
||||
performed for the device (setting properties, configuring services,
|
||||
etc).
|
||||
|
||||
Please refer to the API documentation for :class:`wlauto.common.AndroidDevice`
|
||||
for the full list of its methods and their functionality.
|
||||
|
||||
|
||||
Other Extension Types
|
||||
=====================
|
||||
|
||||
In addition to extension types covered above, there are few other, more
|
||||
specialized ones. They will not be covered in as much detail. Most of them
|
||||
expose relatively simple interfaces with only a couple of methods and it is
|
||||
expected that if the need arises to extend them, the API-level documentation
|
||||
that accompanies them, in addition to what has been outlined here, should
|
||||
provide enough guidance.
|
||||
|
||||
:commands: This allows extending WA with additional sub-commands (to supplement
|
||||
exiting ones outlined in the :ref:`invocation` section).
|
||||
:modules: Modules are "extensions for extensions". They can be loaded by other
|
||||
extensions to expand their functionality (for example, a flashing
|
||||
module maybe loaded by a device in order to support flashing).
|
||||
|
||||
|
||||
Packaging Your Extensions
|
||||
=========================
|
||||
|
||||
If your have written a bunch of extensions, and you want to make it easy to
|
||||
deploy them to new systems and/or to update them on existing systems, you can
|
||||
wrap them in a Python package. You can use ``wa create package`` command to
|
||||
generate appropriate boiler plate. This will create a ``setup.py`` and a
|
||||
directory for your package that you can place your extensions into.
|
||||
|
||||
For example, if you have a workload inside ``my_workload.py`` and a result
|
||||
processor in ``my_result_processor.py``, and you want to package them as
|
||||
``my_wa_exts`` package, first run the create command ::
|
||||
|
||||
wa create package my_wa_exts
|
||||
|
||||
This will create a ``my_wa_exts`` directory which contains a
|
||||
``my_wa_exts/setup.py`` and a subdirectory ``my_wa_exts/my_wa_exts`` which is
|
||||
the package directory for your extensions (you can rename the top-level
|
||||
``my_wa_exts`` directory to anything you like -- it's just a "container" for the
|
||||
setup.py and the package directory). Once you have that, you can then copy your
|
||||
extensions into the package directory, creating
|
||||
``my_wa_exts/my_wa_exts/my_workload.py`` and
|
||||
``my_wa_exts/my_wa_exts/my_result_processor.py``. If you have a lot of
|
||||
extensions, you might want to organize them into subpackages, but only the
|
||||
top-level package directory is created by default, and it is OK to have
|
||||
everything in there.
|
||||
|
||||
.. note:: When discovering extensions thorugh this mechanism, WA traveries the
|
||||
Python module/submodule tree, not the directory strucuter, therefore,
|
||||
if you are going to create subdirectories under the top level dictory
|
||||
created for you, it is important that your make sure they are valid
|
||||
Python packages; i.e. each subdirectory must contain a __init__.py
|
||||
(even if blank) in order for the code in that directory and its
|
||||
subdirectories to be discoverable.
|
||||
|
||||
At this stage, you may want to edit ``params`` structure near the bottom of
|
||||
the ``setup.py`` to add correct author, license and contact information (see
|
||||
"Writing the Setup Script" section in standard Python documentation for
|
||||
details). You may also want to add a README and/or a COPYING file at the same
|
||||
level as the setup.py. Once you have the contents of your package sorted,
|
||||
you can generate the package by running ::
|
||||
|
||||
cd my_wa_exts
|
||||
python setup.py sdist
|
||||
|
||||
This will generate ``my_wa_exts/dist/my_wa_exts-0.0.1.tar.gz`` package which
|
||||
can then be deployed on the target system with standard Python package
|
||||
management tools, e.g. ::
|
||||
|
||||
sudo pip install my_wa_exts-0.0.1.tar.gz
|
||||
|
||||
As part of the installation process, the setup.py in the package, will write the
|
||||
package's name into ``~/.workoad_automoation/packages``. This will tell WA that
|
||||
the package contains extension and it will load them next time it runs.
|
||||
|
||||
.. note:: There are no unistall hooks in ``setuputils``, so if you ever
|
||||
uninstall your WA extensions package, you will have to manually remove
|
||||
it from ``~/.workload_automation/packages`` otherwise WA will complain
|
||||
abou a missing package next time you try to run it.
|
12
extras/README
Normal file
12
extras/README
Normal file
@ -0,0 +1,12 @@
|
||||
This directory is intended for miscellaneous extra stuff that may be useful while developing
|
||||
Workload Automation. It should *NOT* contain anything necessary for *using* workload automation.
|
||||
Whenever you add something to this directory, please also add a short description of what it is in
|
||||
this file.
|
||||
|
||||
pylintrc
|
||||
pylint configuration file set up for WA development (see comment at the top of the file
|
||||
for how to use).
|
||||
|
||||
walog.vim
|
||||
Vim syntax file for WA logs; adds highlighting similar to what comes out
|
||||
in the console. See comment in the file for how to enable it.
|
70
extras/pylintrc
Normal file
70
extras/pylintrc
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# pylint configuration for Workload Automation.
|
||||
#
|
||||
# To install pylint run
|
||||
#
|
||||
# sudo apt-get install pylint
|
||||
#
|
||||
# copy this file to ~/.pylintrc in order for pylint to pick it up.
|
||||
# (Or alternatively, specify it with --rcfile option on invocation.)
|
||||
#
|
||||
# Note: If you're adding something to disable setting, please also add the
|
||||
# explanation of the code in the comment above it. Messages should only
|
||||
# be added here we really don't *ever* care about them. For ignoring
|
||||
# messages on specific lines or in specific files, add the appropriate
|
||||
# pylint disable clause in the source.
|
||||
#
|
||||
[MASTER]
|
||||
|
||||
profile=no
|
||||
|
||||
ignore=external
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# Disable the following messags:
|
||||
# C0301: Line too long (%s/%s)
|
||||
# C0103: Invalid name "%s" (should match %s)
|
||||
# C0111: Missing docstring
|
||||
# W0142 - Used * or ** magic
|
||||
# R0903: Too few public methods
|
||||
# R0904: Too many public methods
|
||||
# R0922: Abstract class is only referenced 1 times
|
||||
# W0511: TODO Note: this is disabled for a cleaner output, but should be reenabled
|
||||
# occasionally (through command line argument) to make sure all
|
||||
# TODO's are addressed, e.g. before a release.
|
||||
# W0141: Used builtin function (map|filter)
|
||||
# I0011: Locally disabling %s
|
||||
# R0921: %s: Abstract class not referenced
|
||||
# Note: this needs to be in the rc file due to a known bug in pylint:
|
||||
# http://www.logilab.org/ticket/111138
|
||||
# W1401: nomalous-backslash-in-string, due to:
|
||||
# https://bitbucket.org/logilab/pylint/issue/272/anomalous-backslash-in-string-for-raw
|
||||
# C0330: bad continuation, due to:
|
||||
# https://bitbucket.org/logilab/pylint/issue/232/wrong-hanging-indentation-false-positive
|
||||
disable=C0301,C0103,C0111,W0142,R0903,R0904,R0922,W0511,W0141,I0011,R0921,W1401,C0330
|
||||
|
||||
[FORMAT]
|
||||
max-module-lines=4000
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# We have DeviceConfig classes that are basically just repositories of confuration
|
||||
# settings.
|
||||
max-args=30
|
||||
max-attributes=30
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
min-similarity-lines=10
|
||||
|
||||
[REPORTS]
|
||||
|
||||
output-format=colorized
|
||||
|
||||
reports=no
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Parts of string are not deprecated. Throws too many false positives.
|
||||
deprecated-modules=
|
21
extras/walog.vim
Normal file
21
extras/walog.vim
Normal file
@ -0,0 +1,21 @@
|
||||
" Copy this into ~/.vim/syntax/ and add the following to your ~/.vimrc:
|
||||
" au BufRead,BufNewFile run.log set filetype=walog
|
||||
"
|
||||
if exists("b:current_syntax")
|
||||
finish
|
||||
endif
|
||||
|
||||
syn region debugPreamble start='\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d,\d\d\d DEBUG' end=':'
|
||||
syn region infoPreamble start='\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d,\d\d\d INFO' end=':'
|
||||
syn region warningPreamble start='\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d,\d\d\d WARNING' end=':'
|
||||
syn region errorPreamble start='\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d,\d\d\d ERROR' end=':'
|
||||
syn region critPreamble start='\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d,\d\d\d CRITICAL' end=':'
|
||||
|
||||
hi debugPreamble guifg=Blue ctermfg=DarkBlue
|
||||
hi infoPreamble guifg=Green ctermfg=DarkGreen
|
||||
hi warningPreamble guifg=Yellow ctermfg=178
|
||||
hi errorPreamble guifg=Red ctermfg=DarkRed
|
||||
hi critPreamble guifg=Red ctermfg=DarkRed cterm=bold gui=bold
|
||||
|
||||
let b:current_syntax='walog'
|
||||
|
17
scripts/create_workload
Normal file
17
scripts/create_workload
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# $Copyright:
|
||||
# ----------------------------------------------------------------
|
||||
# This confidential and proprietary software may be used only as
|
||||
# authorised by a licensing agreement from ARM Limited
|
||||
# (C) COPYRIGHT 2013 ARM Limited
|
||||
# ALL RIGHTS RESERVED
|
||||
# The entire notice above must be reproduced on all authorised
|
||||
# copies and copies may only be made to the extent permitted
|
||||
# by a licensing agreement from ARM Limited.
|
||||
# ----------------------------------------------------------------
|
||||
# File: create_workload
|
||||
# ----------------------------------------------------------------
|
||||
# $
|
||||
#
|
||||
wa create workload $@
|
||||
|
16
scripts/list_extensions
Normal file
16
scripts/list_extensions
Normal file
@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# $Copyright:
|
||||
# ----------------------------------------------------------------
|
||||
# This confidential and proprietary software may be used only as
|
||||
# authorised by a licensing agreement from ARM Limited
|
||||
# (C) COPYRIGHT 2013 ARM Limited
|
||||
# ALL RIGHTS RESERVED
|
||||
# The entire notice above must be reproduced on all authorised
|
||||
# copies and copies may only be made to the extent permitted
|
||||
# by a licensing agreement from ARM Limited.
|
||||
# ----------------------------------------------------------------
|
||||
# File: list_extensions
|
||||
# ----------------------------------------------------------------
|
||||
# $
|
||||
#
|
||||
wa list $@
|
17
scripts/run_workloads
Normal file
17
scripts/run_workloads
Normal file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# $Copyright:
|
||||
# ----------------------------------------------------------------
|
||||
# This confidential and proprietary software may be used only as
|
||||
# authorised by a licensing agreement from ARM Limited
|
||||
# (C) COPYRIGHT 2013 ARM Limited
|
||||
# ALL RIGHTS RESERVED
|
||||
# The entire notice above must be reproduced on all authorised
|
||||
# copies and copies may only be made to the extent permitted
|
||||
# by a licensing agreement from ARM Limited.
|
||||
# ----------------------------------------------------------------
|
||||
# File: run_workloads
|
||||
# ----------------------------------------------------------------
|
||||
# $
|
||||
#
|
||||
wa run $@
|
||||
|
17
scripts/wa
Normal file
17
scripts/wa
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python
|
||||
# $Copyright:
|
||||
# ----------------------------------------------------------------
|
||||
# This confidential and proprietary software may be used only as
|
||||
# authorised by a licensing agreement from ARM Limited
|
||||
# (C) COPYRIGHT 2013 ARM Limited
|
||||
# ALL RIGHTS RESERVED
|
||||
# The entire notice above must be reproduced on all authorised
|
||||
# copies and copies may only be made to the extent permitted
|
||||
# by a licensing agreement from ARM Limited.
|
||||
# ----------------------------------------------------------------
|
||||
# File: run_workloads
|
||||
# ----------------------------------------------------------------
|
||||
# $
|
||||
#
|
||||
from wlauto.core.entry_point import main
|
||||
main()
|
96
setup.py
Normal file
96
setup.py
Normal file
@ -0,0 +1,96 @@
|
||||
# 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 warnings
|
||||
from itertools import chain
|
||||
|
||||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distutils.core import setup
|
||||
|
||||
sys.path.insert(0, './wlauto/core/')
|
||||
from version import get_wa_version
|
||||
|
||||
# happends if falling back to distutils
|
||||
warnings.filterwarnings('ignore', "Unknown distribution option: 'install_requires'")
|
||||
warnings.filterwarnings('ignore', "Unknown distribution option: 'extras_require'")
|
||||
|
||||
try:
|
||||
os.remove('MANIFEST')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
packages = []
|
||||
data_files = {}
|
||||
source_dir = os.path.dirname(__file__)
|
||||
for root, dirs, files in os.walk('wlauto'):
|
||||
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])
|
||||
|
||||
scripts = [os.path.join('scripts', s) for s in os.listdir('scripts')]
|
||||
|
||||
params = dict(
|
||||
name='wlauto',
|
||||
description='A framework for automating workload execution and measurment collection on ARM devices.',
|
||||
version=get_wa_version(),
|
||||
packages=packages,
|
||||
package_data=data_files,
|
||||
scripts=scripts,
|
||||
url='N/A',
|
||||
license='Apache v2',
|
||||
maintainer='ARM Architecture & Technology Device Lab',
|
||||
maintainer_email='workload-automation@arm.com',
|
||||
install_requires=[
|
||||
'python-dateutil', # converting between UTC and local time.
|
||||
'pexpect>=3.3', # Send/recieve to/from device
|
||||
'pyserial', # Serial port interface
|
||||
'colorama', # Printing with colors
|
||||
'pyYAML', # YAML-formatted agenda parsing
|
||||
],
|
||||
extras_require={
|
||||
'other': ['jinja2', 'pandas>=0.13.1'],
|
||||
'test': ['nose'],
|
||||
'mongodb': ['pymongo'],
|
||||
'doc': ['sphinx'],
|
||||
},
|
||||
# https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
],
|
||||
)
|
||||
|
||||
all_extras = list(chain(params['extras_require'].itervalues()))
|
||||
params['extras_require']['everything'] = all_extras
|
||||
|
||||
setup(**params)
|
36
wlauto/__init__.py
Normal file
36
wlauto/__init__.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 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.bootstrap import settings # NOQA
|
||||
from wlauto.core.device import Device, RuntimeParameter, CoreParameter # NOQA
|
||||
from wlauto.core.command import Command # NOQA
|
||||
from wlauto.core.workload import Workload # NOQA
|
||||
from wlauto.core.extension import Module, Parameter, Artifact, Alias # NOQA
|
||||
from wlauto.core.extension_loader import ExtensionLoader # 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_extension_type # NOQA Note: MUST be imported after other core imports.
|
||||
|
||||
from wlauto.common.resources import File, ExtensionAsset, Executable
|
||||
from wlauto.common.linux.device import LinuxDevice # NOQA
|
||||
from wlauto.common.android.device import AndroidDevice, BigLittleDevice # NOQA
|
||||
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()
|
79
wlauto/agenda-example-biglittle.yaml
Normal file
79
wlauto/agenda-example-biglittle.yaml
Normal file
@ -0,0 +1,79 @@
|
||||
# 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
|
43
wlauto/agenda-example-tutorial.yaml
Normal file
43
wlauto/agenda-example-tutorial.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
# 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
|
||||
|
16
wlauto/commands/__init__.py
Normal file
16
wlauto/commands/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
300
wlauto/commands/create.py
Normal file
300
wlauto/commands/create.py
Normal file
@ -0,0 +1,300 @@
|
||||
# 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 stat
|
||||
import string
|
||||
import textwrap
|
||||
import argparse
|
||||
import shutil
|
||||
import getpass
|
||||
|
||||
from wlauto import ExtensionLoader, Command, settings
|
||||
from wlauto.exceptions import CommandError
|
||||
from wlauto.utils.cli 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, 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 extensions. On installation,
|
||||
this package will "advertise" itself to WA so that Extensions 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_extensions_package(package_dir, args.name, template_path, args.force)
|
||||
|
||||
def create_extensions_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 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]
|
||||
|
||||
def initialize(self):
|
||||
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 = ExtensionLoader(packages=settings.extension_packages, paths=settings.extension_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.environment_root, '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)
|
||||
check_output(command, shell=True)
|
||||
|
||||
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 wfh: # pylint: disable=unused-variable
|
||||
pass
|
59
wlauto/commands/list.py
Normal file
59
wlauto/commands/list.py
Normal file
@ -0,0 +1,59 @@
|
||||
# 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 ExtensionLoader, Command, settings
|
||||
from wlauto.utils.formatter import DescriptionListFormatter
|
||||
from wlauto.utils.doc import get_summary
|
||||
|
||||
|
||||
class ListCommand(Command):
|
||||
|
||||
name = 'list'
|
||||
description = 'List available WA extensions with a short description of each.'
|
||||
|
||||
def initialize(self):
|
||||
extension_types = ['{}s'.format(ext.name) for ext in settings.extensions]
|
||||
self.parser.add_argument('kind', metavar='KIND',
|
||||
help=('Specify the kind of extension to list. Must be '
|
||||
'one of: {}'.format(', '.join(extension_types))),
|
||||
choices=extension_types)
|
||||
self.parser.add_argument('-n', '--name', help='Filter results by the name specified')
|
||||
|
||||
def execute(self, args):
|
||||
filters = {}
|
||||
if args.name:
|
||||
filters['name'] = args.name
|
||||
|
||||
ext_loader = ExtensionLoader(packages=settings.extension_packages, paths=settings.extension_paths)
|
||||
results = ext_loader.list_extensions(args.kind[:-1])
|
||||
if filters:
|
||||
filtered_results = []
|
||||
for result in results:
|
||||
passed = True
|
||||
for k, v in filters.iteritems():
|
||||
if getattr(result, k) != v:
|
||||
passed = False
|
||||
break
|
||||
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()
|
87
wlauto/commands/run.py
Normal file
87
wlauto/commands/run.py
Normal file
@ -0,0 +1,87 @@
|
||||
# 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.agenda import Agenda
|
||||
from wlauto.core.execution import Executor
|
||||
from wlauto.utils.log import add_log_file
|
||||
|
||||
|
||||
class RunCommand(Command):
|
||||
|
||||
name = 'run'
|
||||
description = 'Execute automated workloads on a remote device and process the resulting output.'
|
||||
|
||||
def initialize(self):
|
||||
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 {} '.format(os.path.dirname(wlauto.__file__)) +
|
||||
'for an example of how this file should be structured.')
|
||||
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.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.')
|
||||
|
||||
def execute(self, args): # NOQA
|
||||
self.set_up_output_directory(args)
|
||||
add_log_file(settings.log_file)
|
||||
|
||||
if os.path.isfile(args.agenda):
|
||||
agenda = Agenda(args.agenda)
|
||||
settings.agenda = args.agenda
|
||||
shutil.copy(args.agenda, settings.meta_directory)
|
||||
else:
|
||||
self.logger.debug('{} is not a file; assuming workload name.'.format(args.agenda))
|
||||
agenda = Agenda()
|
||||
agenda.add_workload_entry(args.agenda)
|
||||
|
||||
file_name = 'config_{}.py'
|
||||
for file_number, path in enumerate(settings.get_config_paths(), 1):
|
||||
shutil.copy(path, os.path.join(settings.meta_directory, file_name.format(file_number)))
|
||||
|
||||
executor = Executor()
|
||||
executor.execute(agenda, selectors={'ids': args.only_run_ids})
|
||||
|
||||
def set_up_output_directory(self, args):
|
||||
if args.output_directory:
|
||||
settings.output_directory = args.output_directory
|
||||
self.logger.debug('Using output directory: {}'.format(settings.output_directory))
|
||||
if os.path.exists(settings.output_directory):
|
||||
if args.force:
|
||||
self.logger.info('Removing existing output directory.')
|
||||
shutil.rmtree(settings.output_directory)
|
||||
else:
|
||||
self.logger.error('Output directory {} exists.'.format(settings.output_directory))
|
||||
self.logger.error('Please specify another location, or use -f option to overwrite.\n')
|
||||
sys.exit(1)
|
||||
|
||||
self.logger.info('Creating output directory.')
|
||||
os.makedirs(settings.output_directory)
|
||||
os.makedirs(settings.meta_directory)
|
101
wlauto/commands/show.py
Normal file
101
wlauto/commands/show.py
Normal file
@ -0,0 +1,101 @@
|
||||
# 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 terminalsize import get_terminal_size # pylint: disable=import-error
|
||||
from wlauto import Command, ExtensionLoader, settings
|
||||
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
|
||||
|
||||
|
||||
class ShowCommand(Command):
|
||||
|
||||
name = 'show'
|
||||
|
||||
description = """
|
||||
Display documentation for the specified extension (workload, instrument, etc.).
|
||||
"""
|
||||
|
||||
def initialize(self):
|
||||
self.parser.add_argument('name', metavar='EXTENSION',
|
||||
help='''The name of the extension for which information will
|
||||
be shown.''')
|
||||
|
||||
def execute(self, args):
|
||||
ext_loader = ExtensionLoader(packages=settings.extension_packages, paths=settings.extension_paths)
|
||||
extension = ext_loader.get_extension_class(args.name)
|
||||
out = StringIO()
|
||||
term_width, term_height = get_terminal_size()
|
||||
format_extension(extension, out, term_width)
|
||||
text = out.getvalue()
|
||||
pager = get_pager()
|
||||
if len(text.split('\n')) > term_height and pager:
|
||||
sp = subprocess.Popen(pager, stdin=subprocess.PIPE)
|
||||
sp.communicate(text)
|
||||
else:
|
||||
sys.stdout.write(text)
|
||||
|
||||
|
||||
def format_extension(extension, out, width):
|
||||
format_extension_name(extension, out)
|
||||
out.write('\n')
|
||||
format_extension_summary(extension, out, width)
|
||||
out.write('\n')
|
||||
if extension.parameters:
|
||||
format_extension_parameters(extension, out, width)
|
||||
out.write('\n')
|
||||
format_extension_description(extension, out, width)
|
||||
|
||||
|
||||
def format_extension_name(extension, out):
|
||||
out.write('\n{}\n'.format(extension.name))
|
||||
|
||||
|
||||
def format_extension_summary(extension, out, width):
|
||||
out.write('{}\n'.format(format_body(strip_inlined_text(get_summary(extension)), width)))
|
||||
|
||||
|
||||
def format_extension_description(extension, out, width):
|
||||
# skip the initial paragraph of multi-paragraph description, as already
|
||||
# listed above.
|
||||
description = get_description(extension).split('\n\n', 1)[-1]
|
||||
out.write('{}\n'.format(format_body(strip_inlined_text(description), width)))
|
||||
|
||||
|
||||
def format_extension_parameters(extension, out, width, shift=4):
|
||||
out.write('parameters:\n\n')
|
||||
param_texts = []
|
||||
for param in extension.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:
|
||||
param_text += indent('default: {}\n'.format(param.default))
|
||||
param_texts.append(indent(param_text, shift))
|
||||
|
||||
out.write(format_column('\n'.join(param_texts), width))
|
||||
|
25
wlauto/commands/templates/UiAutomation.java
Normal file
25
wlauto/commands/templates/UiAutomation.java
Normal file
@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
27
wlauto/commands/templates/android_benchmark
Normal file
27
wlauto/commands/templates/android_benchmark
Normal file
@ -0,0 +1,27 @@
|
||||
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
|
24
wlauto/commands/templates/android_uiauto_benchmark
Normal file
24
wlauto/commands/templates/android_uiauto_benchmark
Normal file
@ -0,0 +1,24 @@
|
||||
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
|
28
wlauto/commands/templates/basic_workload
Normal file
28
wlauto/commands/templates/basic_workload
Normal file
@ -0,0 +1,28 @@
|
||||
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
|
102
wlauto/commands/templates/setup.template
Normal file
102
wlauto/commands/templates/setup.template
Normal file
@ -0,0 +1,102 @@
|
||||
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)
|
35
wlauto/commands/templates/uiauto_workload
Normal file
35
wlauto/commands/templates/uiauto_workload
Normal file
@ -0,0 +1,35 @@
|
||||
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
|
||||
|
||||
|
16
wlauto/common/__init__.py
Normal file
16
wlauto/common/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
BIN
wlauto/common/android/BaseUiAutomation.class
Normal file
BIN
wlauto/common/android/BaseUiAutomation.class
Normal file
Binary file not shown.
16
wlauto/common/android/__init__.py
Normal file
16
wlauto/common/android/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
678
wlauto/common/android/device.py
Normal file
678
wlauto/common/android/device.py
Normal file
@ -0,0 +1,678 @@
|
||||
# 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 re
|
||||
import time
|
||||
import tempfile
|
||||
import shutil
|
||||
import threading
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
from wlauto.core.extension import Parameter
|
||||
from wlauto.common.linux.device import BaseLinuxDevice
|
||||
from wlauto.exceptions import DeviceError, WorkerThreadError, TimeoutError, DeviceNotRespondingError
|
||||
from wlauto.utils.misc import convert_new_lines
|
||||
from wlauto.utils.types import boolean, regex
|
||||
from wlauto.utils.android import (adb_shell, adb_background_shell, adb_list_devices,
|
||||
adb_command, AndroidProperties, ANDROID_VERSION_MAP)
|
||||
|
||||
|
||||
SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn)=([0-9]+|true|false)', re.I)
|
||||
|
||||
|
||||
class AndroidDevice(BaseLinuxDevice): # pylint: disable=W0223
|
||||
"""
|
||||
Device running Android OS.
|
||||
|
||||
"""
|
||||
|
||||
platform = 'android'
|
||||
|
||||
parameters = [
|
||||
Parameter('adb_name',
|
||||
description='The unique ID of the device as output by "adb devices".'),
|
||||
Parameter('android_prompt', kind=regex, default=re.compile('^.*(shell|root)@.*:/ [#$] ', re.MULTILINE),
|
||||
description='The format of matching the shell prompt in Android.'),
|
||||
Parameter('working_directory', default='/sdcard/wa-working',
|
||||
description='Directory that will be used WA on the device for output files etc.'),
|
||||
Parameter('binaries_directory', default='/system/bin',
|
||||
description='Location of binaries on the device.'),
|
||||
Parameter('package_data_directory', default='/data/data',
|
||||
description='Location of of data for an installed package (APK).'),
|
||||
Parameter('external_storage_directory', default='/sdcard',
|
||||
description='Mount point for external storage.'),
|
||||
Parameter('connection', default='usb', allowed_values=['usb', 'ethernet'],
|
||||
description='Specified the nature of adb connection.'),
|
||||
Parameter('logcat_poll_period', kind=int,
|
||||
description="""
|
||||
If specified and is not ``0``, logcat will be polled every
|
||||
``logcat_poll_period`` seconds, and buffered on the host. This
|
||||
can be used if a lot of output is expected in logcat and the fixed
|
||||
logcat buffer on the device is not big enough. The trade off is that
|
||||
this introduces some minor runtime overhead. Not set by default.
|
||||
"""),
|
||||
Parameter('enable_screen_check', kind=boolean, default=False,
|
||||
description="""
|
||||
Specified whether the device should make sure that the screen is on
|
||||
during initialization.
|
||||
"""),
|
||||
]
|
||||
|
||||
default_timeout = 30
|
||||
delay = 2
|
||||
long_delay = 3 * delay
|
||||
ready_timeout = 60
|
||||
|
||||
# Overwritten from Device. For documentation, see corresponding method in
|
||||
# Device.
|
||||
|
||||
@property
|
||||
def is_rooted(self):
|
||||
if self._is_rooted is None:
|
||||
try:
|
||||
result = adb_shell(self.adb_name, 'su', timeout=1)
|
||||
if 'not found' in result:
|
||||
self._is_rooted = False
|
||||
else:
|
||||
self._is_rooted = True
|
||||
except TimeoutError:
|
||||
self._is_rooted = True
|
||||
except DeviceError:
|
||||
self._is_rooted = False
|
||||
return self._is_rooted
|
||||
|
||||
@property
|
||||
def abi(self):
|
||||
return self.getprop()['ro.product.cpu.abi'].split('-')[0]
|
||||
|
||||
@property
|
||||
def supported_eabi(self):
|
||||
props = self.getprop()
|
||||
result = [props['ro.product.cpu.abi']]
|
||||
if 'ro.product.cpu.abi2' in props:
|
||||
result.append(props['ro.product.cpu.abi2'])
|
||||
if 'ro.product.cpu.abilist' in props:
|
||||
for eabi in props['ro.product.cpu.abilist'].split(','):
|
||||
if eabi not in result:
|
||||
result.append(eabi)
|
||||
return result
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AndroidDevice, self).__init__(**kwargs)
|
||||
self._logcat_poller = None
|
||||
|
||||
def reset(self):
|
||||
self._is_ready = False
|
||||
self._just_rebooted = True
|
||||
adb_command(self.adb_name, 'reboot', timeout=self.default_timeout)
|
||||
|
||||
def hard_reset(self):
|
||||
super(AndroidDevice, self).hard_reset()
|
||||
self._is_ready = False
|
||||
self._just_rebooted = True
|
||||
|
||||
def boot(self, **kwargs):
|
||||
self.reset()
|
||||
|
||||
def connect(self): # NOQA pylint: disable=R0912
|
||||
iteration_number = 0
|
||||
max_iterations = self.ready_timeout / self.delay
|
||||
available = False
|
||||
self.logger.debug('Polling for device {}...'.format(self.adb_name))
|
||||
while iteration_number < max_iterations:
|
||||
devices = adb_list_devices()
|
||||
if self.adb_name:
|
||||
for device in devices:
|
||||
if device.name == self.adb_name and device.status != 'offline':
|
||||
available = True
|
||||
else: # adb_name not set
|
||||
if len(devices) == 1:
|
||||
available = True
|
||||
elif len(devices) > 1:
|
||||
raise DeviceError('More than one device is connected and adb_name is not set.')
|
||||
|
||||
if available:
|
||||
break
|
||||
else:
|
||||
time.sleep(self.delay)
|
||||
iteration_number += 1
|
||||
else:
|
||||
raise DeviceError('Could not boot {} ({}).'.format(self.name, self.adb_name))
|
||||
|
||||
while iteration_number < max_iterations:
|
||||
available = (1 == int('0' + adb_shell(self.adb_name, 'getprop sys.boot_completed', timeout=self.default_timeout)))
|
||||
if available:
|
||||
break
|
||||
else:
|
||||
time.sleep(self.delay)
|
||||
iteration_number += 1
|
||||
else:
|
||||
raise DeviceError('Could not boot {} ({}).'.format(self.name, self.adb_name))
|
||||
|
||||
if self._just_rebooted:
|
||||
self.logger.debug('Waiting for boot to complete...')
|
||||
# On some devices, adb connection gets reset some time after booting.
|
||||
# This causes errors during execution. To prevent this, open a shell
|
||||
# session and wait for it to be killed. Once its killed, give adb
|
||||
# enough time to restart, and then the device should be ready.
|
||||
# TODO: This is more of a work-around rather than an actual solution.
|
||||
# Need to figure out what is going on the "proper" way of handling it.
|
||||
try:
|
||||
adb_shell(self.adb_name, '', timeout=20)
|
||||
time.sleep(5) # give adb time to re-initialize
|
||||
except TimeoutError:
|
||||
pass # timed out waiting for the session to be killed -- assume not going to be.
|
||||
|
||||
self.logger.debug('Boot completed.')
|
||||
self._just_rebooted = False
|
||||
self._is_ready = True
|
||||
|
||||
def initialize(self, context, *args, **kwargs):
|
||||
self.execute('mkdir -p {}'.format(self.working_directory))
|
||||
if self.is_rooted:
|
||||
if not self.executable_is_installed('busybox'):
|
||||
self.busybox = self.deploy_busybox(context)
|
||||
else:
|
||||
self.busybox = 'busybox'
|
||||
self.disable_screen_lock()
|
||||
self.disable_selinux()
|
||||
if self.enable_screen_check:
|
||||
self.ensure_screen_is_on()
|
||||
self.init(context, *args, **kwargs)
|
||||
|
||||
def disconnect(self):
|
||||
if self._logcat_poller:
|
||||
self._logcat_poller.close()
|
||||
|
||||
def ping(self):
|
||||
try:
|
||||
# May be triggered inside initialize()
|
||||
adb_shell(self.adb_name, 'ls /', timeout=10)
|
||||
except (TimeoutError, CalledProcessError):
|
||||
raise DeviceNotRespondingError(self.adb_name or self.name)
|
||||
|
||||
def start(self):
|
||||
if self.logcat_poll_period:
|
||||
if self._logcat_poller:
|
||||
self._logcat_poller.close()
|
||||
self._logcat_poller = _LogcatPoller(self, self.logcat_poll_period, timeout=self.default_timeout)
|
||||
self._logcat_poller.start()
|
||||
|
||||
def stop(self):
|
||||
if self._logcat_poller:
|
||||
self._logcat_poller.stop()
|
||||
|
||||
def get_android_version(self):
|
||||
return ANDROID_VERSION_MAP.get(self.get_sdk_version(), None)
|
||||
|
||||
def get_android_id(self):
|
||||
"""
|
||||
Get the device's ANDROID_ID. Which is
|
||||
|
||||
"A 64-bit number (as a hex string) that is randomly generated when the user
|
||||
first sets up the device and should remain constant for the lifetime of the
|
||||
user's device."
|
||||
|
||||
.. note:: This will get reset on userdata erasure.
|
||||
|
||||
"""
|
||||
return self.execute('settings get secure android_id').strip()
|
||||
|
||||
def get_sdk_version(self):
|
||||
try:
|
||||
return int(self.getprop('ro.build.version.sdk'))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def get_installed_package_version(self, package):
|
||||
"""
|
||||
Returns the version (versionName) of the specified package if it is installed
|
||||
on the device, or ``None`` otherwise.
|
||||
|
||||
Added in version 2.1.4
|
||||
|
||||
"""
|
||||
output = self.execute('dumpsys package {}'.format(package))
|
||||
for line in convert_new_lines(output).split('\n'):
|
||||
if 'versionName' in line:
|
||||
return line.split('=', 1)[1]
|
||||
return None
|
||||
|
||||
def list_packages(self):
|
||||
"""
|
||||
List packages installed on the device.
|
||||
|
||||
Added in version 2.1.4
|
||||
|
||||
"""
|
||||
output = self.execute('pm list packages')
|
||||
output = output.replace('package:', '')
|
||||
return output.split()
|
||||
|
||||
def package_is_installed(self, package_name):
|
||||
"""
|
||||
Returns ``True`` the if a package with the specified name is installed on
|
||||
the device, and ``False`` otherwise.
|
||||
|
||||
Added in version 2.1.4
|
||||
|
||||
"""
|
||||
return package_name in self.list_packages()
|
||||
|
||||
def executable_is_installed(self, executable_name):
|
||||
return executable_name in self.listdir(self.binaries_directory)
|
||||
|
||||
def is_installed(self, name):
|
||||
return self.executable_is_installed(name) or self.package_is_installed(name)
|
||||
|
||||
def listdir(self, path, as_root=False, **kwargs):
|
||||
contents = self.execute('ls {}'.format(path), as_root=as_root)
|
||||
return [x.strip() for x in contents.split()]
|
||||
|
||||
def push_file(self, source, dest, as_root=False, timeout=default_timeout): # pylint: disable=W0221
|
||||
"""
|
||||
Modified in version 2.1.4: added ``as_root`` parameter.
|
||||
|
||||
"""
|
||||
self._check_ready()
|
||||
if not as_root:
|
||||
adb_command(self.adb_name, "push '{}' '{}'".format(source, dest), timeout=timeout)
|
||||
else:
|
||||
device_tempfile = self.path.join(self.file_transfer_cache, source.lstrip(self.path.sep))
|
||||
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
|
||||
adb_command(self.adb_name, "push '{}' '{}'".format(source, device_tempfile), timeout=timeout)
|
||||
self.execute('cp {} {}'.format(device_tempfile, dest), as_root=True)
|
||||
|
||||
def pull_file(self, source, dest, as_root=False, timeout=default_timeout): # pylint: disable=W0221
|
||||
"""
|
||||
Modified in version 2.1.4: added ``as_root`` parameter.
|
||||
|
||||
"""
|
||||
self._check_ready()
|
||||
if not as_root:
|
||||
adb_command(self.adb_name, "pull '{}' '{}'".format(source, dest), timeout=timeout)
|
||||
else:
|
||||
device_tempfile = self.path.join(self.file_transfer_cache, source.lstrip(self.path.sep))
|
||||
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
|
||||
self.execute('cp {} {}'.format(source, device_tempfile), as_root=True)
|
||||
adb_command(self.adb_name, "pull '{}' '{}'".format(device_tempfile, dest), timeout=timeout)
|
||||
|
||||
def delete_file(self, filepath, as_root=False): # pylint: disable=W0221
|
||||
self._check_ready()
|
||||
adb_shell(self.adb_name, "rm '{}'".format(filepath), as_root=as_root, timeout=self.default_timeout)
|
||||
|
||||
def file_exists(self, filepath):
|
||||
self._check_ready()
|
||||
output = adb_shell(self.adb_name, 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath),
|
||||
timeout=self.default_timeout)
|
||||
if int(output):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def install(self, filepath, timeout=default_timeout, with_name=None): # pylint: disable=W0221
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
if ext == '.apk':
|
||||
return self.install_apk(filepath, timeout)
|
||||
else:
|
||||
return self.install_executable(filepath, with_name)
|
||||
|
||||
def install_apk(self, filepath, timeout=default_timeout): # pylint: disable=W0221
|
||||
self._check_ready()
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
if ext == '.apk':
|
||||
return adb_command(self.adb_name, "install {}".format(filepath), timeout=timeout)
|
||||
else:
|
||||
raise DeviceError('Can\'t install {}: unsupported format.'.format(filepath))
|
||||
|
||||
def install_executable(self, filepath, with_name=None):
|
||||
"""
|
||||
Installs a binary executable on device. Requires root access. Returns
|
||||
the path to the installed binary, or ``None`` if the installation has failed.
|
||||
Optionally, ``with_name`` parameter may be used to specify a different name under
|
||||
which the executable will be installed.
|
||||
|
||||
Added in version 2.1.3.
|
||||
Updated in version 2.1.5 with ``with_name`` parameter.
|
||||
|
||||
"""
|
||||
executable_name = with_name or os.path.basename(filepath)
|
||||
on_device_file = self.path.join(self.working_directory, executable_name)
|
||||
on_device_executable = self.path.join(self.binaries_directory, executable_name)
|
||||
self.push_file(filepath, on_device_file)
|
||||
matched = []
|
||||
for entry in self.list_file_systems():
|
||||
if self.binaries_directory.rstrip('/').startswith(entry.mount_point):
|
||||
matched.append(entry)
|
||||
|
||||
if matched:
|
||||
entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
|
||||
if 'rw' not in entry.options:
|
||||
self.execute('mount -o rw,remount {} {}'.format(entry.device, entry.mount_point), as_root=True)
|
||||
self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=True)
|
||||
self.execute('chmod 0777 {}'.format(on_device_executable), as_root=True)
|
||||
return on_device_executable
|
||||
else:
|
||||
raise DeviceError('Could not find mount point for binaries directory {}'.format(self.binaries_directory))
|
||||
|
||||
def uninstall(self, package):
|
||||
self._check_ready()
|
||||
adb_command(self.adb_name, "uninstall {}".format(package), timeout=self.default_timeout)
|
||||
|
||||
def uninstall_executable(self, executable_name):
|
||||
"""
|
||||
Requires root access.
|
||||
|
||||
Added in version 2.1.3.
|
||||
|
||||
"""
|
||||
on_device_executable = self.path.join(self.binaries_directory, executable_name)
|
||||
for entry in self.list_file_systems():
|
||||
if entry.mount_point == '/system':
|
||||
if 'rw' not in entry.options:
|
||||
self.execute('mount -o rw,remount {} /system'.format(entry.device), as_root=True)
|
||||
self.delete_file(on_device_executable)
|
||||
|
||||
def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False,
|
||||
as_root=False, busybox=False, **kwargs):
|
||||
"""
|
||||
Execute the specified command on the device using adb.
|
||||
|
||||
Parameters:
|
||||
|
||||
:param command: The command to be executed. It should appear exactly
|
||||
as if you were typing it into a shell.
|
||||
:param timeout: Time, in seconds, to wait for adb to return before aborting
|
||||
and raising an error. Defaults to ``AndroidDevice.default_timeout``.
|
||||
:param check_exit_code: If ``True``, the return code of the command on the Device will
|
||||
be check and exception will be raised if it is not 0.
|
||||
Defaults to ``True``.
|
||||
:param background: If ``True``, will execute adb in a subprocess, and will return
|
||||
immediately, not waiting for adb to return. Defaults to ``False``
|
||||
:param busybox: If ``True``, will use busybox to execute the command. Defaults to ``False``.
|
||||
|
||||
Added in version 2.1.3
|
||||
|
||||
.. note:: The device must be rooted to be able to use busybox.
|
||||
|
||||
:param as_root: If ``True``, will attempt to execute command in privileged mode. The device
|
||||
must be rooted, otherwise an error will be raised. Defaults to ``False``.
|
||||
|
||||
Added in version 2.1.3
|
||||
|
||||
:returns: If ``background`` parameter is set to ``True``, the subprocess object will
|
||||
be returned; otherwise, the contents of STDOUT from the device will be returned.
|
||||
|
||||
:raises: DeviceError if adb timed out or if the command returned non-zero exit
|
||||
code on the device, or if attempting to execute a command in privileged mode on an
|
||||
unrooted device.
|
||||
|
||||
"""
|
||||
self._check_ready()
|
||||
if as_root and not self.is_rooted:
|
||||
raise DeviceError('Attempting to execute "{}" as root on unrooted device.'.format(command))
|
||||
if busybox:
|
||||
if not self.is_rooted:
|
||||
DeviceError('Attempting to execute "{}" with busybox. '.format(command) +
|
||||
'Busybox can only be deployed to rooted devices.')
|
||||
command = ' '.join([self.busybox, command])
|
||||
if background:
|
||||
return adb_background_shell(self.adb_name, command, as_root=as_root)
|
||||
else:
|
||||
return adb_shell(self.adb_name, command, timeout, check_exit_code, as_root)
|
||||
|
||||
def kick_off(self, command):
|
||||
"""
|
||||
Like execute but closes adb session and returns immediately, leaving the command running on the
|
||||
device (this is different from execute(background=True) which keeps adb connection open and returns
|
||||
a subprocess object).
|
||||
|
||||
.. note:: This relies on busybox's nohup applet and so won't work on unrooted devices.
|
||||
|
||||
Added in version 2.1.4
|
||||
|
||||
"""
|
||||
if not self.is_rooted:
|
||||
raise DeviceError('kick_off uses busybox\'s nohup applet and so can only be run a rooted device.')
|
||||
try:
|
||||
command = 'cd {} && busybox nohup {}'.format(self.working_directory, command)
|
||||
output = self.execute(command, timeout=1, as_root=True)
|
||||
except TimeoutError:
|
||||
pass
|
||||
else:
|
||||
raise ValueError('Background command exited before timeout; got "{}"'.format(output))
|
||||
|
||||
def get_properties(self, context):
|
||||
"""Captures and saves the information from /system/build.prop and /proc/version"""
|
||||
props = {}
|
||||
props['android_id'] = self.get_android_id()
|
||||
buildprop_file = os.path.join(context.host_working_directory, 'build.prop')
|
||||
if not os.path.isfile(buildprop_file):
|
||||
self.pull_file('/system/build.prop', context.host_working_directory)
|
||||
self._update_build_properties(buildprop_file, props)
|
||||
context.add_run_artifact('build_properties', buildprop_file, 'export')
|
||||
|
||||
version_file = os.path.join(context.host_working_directory, 'version')
|
||||
if not os.path.isfile(version_file):
|
||||
self.pull_file('/proc/version', context.host_working_directory)
|
||||
self._update_versions(version_file, props)
|
||||
context.add_run_artifact('device_version', version_file, 'export')
|
||||
return props
|
||||
|
||||
def getprop(self, prop=None):
|
||||
"""Returns parsed output of Android getprop command. If a property is
|
||||
specified, only the value for that property will be returned (with
|
||||
``None`` returned if the property doesn't exist. Otherwise,
|
||||
``wlauto.utils.android.AndroidProperties`` will be returned, which is
|
||||
a dict-like object."""
|
||||
props = AndroidProperties(self.execute('getprop'))
|
||||
if prop:
|
||||
return props[prop]
|
||||
return props
|
||||
|
||||
# Android-specific methods. These either rely on specifics of adb or other
|
||||
# Android-only concepts in their interface and/or implementation.
|
||||
|
||||
def forward_port(self, from_port, to_port):
|
||||
"""
|
||||
Forward a port on the device to a port on localhost.
|
||||
|
||||
:param from_port: Port on the device which to forward.
|
||||
:param to_port: Port on the localhost to which the device port will be forwarded.
|
||||
|
||||
Ports should be specified using adb spec. See the "adb forward" section in "adb help".
|
||||
|
||||
"""
|
||||
adb_command(self.adb_name, 'forward {} {}'.format(from_port, to_port), timeout=self.default_timeout)
|
||||
|
||||
def dump_logcat(self, outfile, filter_spec=None):
|
||||
"""
|
||||
Dump the contents of logcat, for the specified filter spec to the
|
||||
specified output file.
|
||||
See http://developer.android.com/tools/help/logcat.html
|
||||
|
||||
:param outfile: Output file on the host into which the contents of the
|
||||
log will be written.
|
||||
:param filter_spec: Logcat filter specification.
|
||||
see http://developer.android.com/tools/debugging/debugging-log.html#filteringOutput
|
||||
|
||||
"""
|
||||
if self._logcat_poller:
|
||||
return self._logcat_poller.write_log(outfile)
|
||||
else:
|
||||
if filter_spec:
|
||||
command = 'logcat -d -s {} > {}'.format(filter_spec, outfile)
|
||||
else:
|
||||
command = 'logcat -d > {}'.format(outfile)
|
||||
return adb_command(self.adb_name, command, timeout=self.default_timeout)
|
||||
|
||||
def clear_logcat(self):
|
||||
"""Clear (flush) logcat log."""
|
||||
if self._logcat_poller:
|
||||
return self._logcat_poller.clear_buffer()
|
||||
else:
|
||||
return adb_shell(self.adb_name, 'logcat -c', timeout=self.default_timeout)
|
||||
|
||||
def capture_screen(self, filepath):
|
||||
"""Caputers the current device screen into the specified file in a PNG format."""
|
||||
on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
|
||||
self.execute('screencap -p {}'.format(on_device_file))
|
||||
self.pull_file(on_device_file, filepath)
|
||||
self.delete_file(on_device_file)
|
||||
|
||||
def is_screen_on(self):
|
||||
"""Returns ``True`` if the device screen is currently on, ``False`` otherwise."""
|
||||
output = self.execute('dumpsys power')
|
||||
match = SCREEN_STATE_REGEX.search(output)
|
||||
if match:
|
||||
return boolean(match.group(1))
|
||||
else:
|
||||
raise DeviceError('Could not establish screen state.')
|
||||
|
||||
def ensure_screen_is_on(self):
|
||||
if not self.is_screen_on():
|
||||
self.execute('input keyevent 26')
|
||||
|
||||
def disable_screen_lock(self):
|
||||
"""
|
||||
Attempts to disable he screen lock on the device.
|
||||
|
||||
.. note:: This does not always work...
|
||||
|
||||
Added inversion 2.1.4
|
||||
|
||||
"""
|
||||
lockdb = '/data/system/locksettings.db'
|
||||
sqlcommand = "update locksettings set value=\\'0\\' where name=\\'screenlock.disabled\\';"
|
||||
self.execute('sqlite3 {} "{}"'.format(lockdb, sqlcommand), as_root=True)
|
||||
|
||||
def disable_selinux(self):
|
||||
# This may be invoked from intialize() so we can't use execute() or the
|
||||
# standard API for doing this.
|
||||
api_level = int(adb_shell(self.adb_name, 'getprop ro.build.version.sdk',
|
||||
timeout=self.default_timeout).strip())
|
||||
# SELinux was added in Android 4.3 (API level 18). Trying to
|
||||
# 'getenforce' in earlier versions will produce an error.
|
||||
if api_level >= 18:
|
||||
se_status = self.execute('getenforce', as_root=True).strip()
|
||||
if se_status == 'Enforcing':
|
||||
self.execute('setenforce 0', as_root=True)
|
||||
|
||||
# Internal methods: do not use outside of the class.
|
||||
|
||||
def _update_build_properties(self, filepath, props):
|
||||
try:
|
||||
with open(filepath) as fh:
|
||||
for line in fh:
|
||||
line = re.sub(r'#.*', '', line).strip()
|
||||
if not line:
|
||||
continue
|
||||
key, value = line.split('=', 1)
|
||||
props[key] = value
|
||||
except ValueError:
|
||||
self.logger.warning('Could not parse build.prop.')
|
||||
|
||||
def _update_versions(self, filepath, props):
|
||||
with open(filepath) as fh:
|
||||
text = fh.read()
|
||||
props['version'] = text
|
||||
text = re.sub(r'#.*', '', text).strip()
|
||||
match = re.search(r'^(Linux version .*?)\s*\((gcc version .*)\)$', text)
|
||||
if match:
|
||||
props['linux_version'] = match.group(1).strip()
|
||||
props['gcc_version'] = match.group(2).strip()
|
||||
else:
|
||||
self.logger.warning('Could not parse version string.')
|
||||
|
||||
|
||||
class _LogcatPoller(threading.Thread):
|
||||
|
||||
join_timeout = 5
|
||||
|
||||
def __init__(self, device, period, timeout=None):
|
||||
super(_LogcatPoller, self).__init__()
|
||||
self.adb_device = device.adb_name
|
||||
self.logger = device.logger
|
||||
self.period = period
|
||||
self.timeout = timeout
|
||||
self.stop_signal = threading.Event()
|
||||
self.lock = threading.RLock()
|
||||
self.buffer_file = tempfile.mktemp()
|
||||
self.last_poll = 0
|
||||
self.daemon = True
|
||||
self.exc = None
|
||||
|
||||
def run(self):
|
||||
self.logger.debug('Starting logcat polling.')
|
||||
try:
|
||||
while True:
|
||||
if self.stop_signal.is_set():
|
||||
break
|
||||
with self.lock:
|
||||
current_time = time.time()
|
||||
if (current_time - self.last_poll) >= self.period:
|
||||
self._poll()
|
||||
time.sleep(0.5)
|
||||
except Exception: # pylint: disable=W0703
|
||||
self.exc = WorkerThreadError(self.name, sys.exc_info())
|
||||
self.logger.debug('Logcat polling stopped.')
|
||||
|
||||
def stop(self):
|
||||
self.logger.debug('Stopping logcat polling.')
|
||||
self.stop_signal.set()
|
||||
self.join(self.join_timeout)
|
||||
if self.is_alive():
|
||||
self.logger.error('Could not join logcat poller thread.')
|
||||
if self.exc:
|
||||
raise self.exc # pylint: disable=E0702
|
||||
|
||||
def clear_buffer(self):
|
||||
self.logger.debug('Clearing logcat buffer.')
|
||||
with self.lock:
|
||||
adb_shell(self.adb_device, 'logcat -c', timeout=self.timeout)
|
||||
with open(self.buffer_file, 'w') as _: # NOQA
|
||||
pass
|
||||
|
||||
def write_log(self, outfile):
|
||||
self.logger.debug('Writing logbuffer to {}.'.format(outfile))
|
||||
with self.lock:
|
||||
self._poll()
|
||||
if os.path.isfile(self.buffer_file):
|
||||
shutil.copy(self.buffer_file, outfile)
|
||||
else: # there was no logcat trace at this time
|
||||
with open(outfile, 'w') as _: # NOQA
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
self.logger.debug('Closing logcat poller.')
|
||||
if os.path.isfile(self.buffer_file):
|
||||
os.remove(self.buffer_file)
|
||||
|
||||
def _poll(self):
|
||||
with self.lock:
|
||||
self.last_poll = time.time()
|
||||
adb_command(self.adb_device, 'logcat -d >> {}'.format(self.buffer_file), timeout=self.timeout)
|
||||
adb_command(self.adb_device, 'logcat -c', timeout=self.timeout)
|
||||
|
||||
|
||||
class BigLittleDevice(AndroidDevice): # pylint: disable=W0223
|
||||
|
||||
parameters = [
|
||||
Parameter('scheduler', default='hmp', override=True),
|
||||
]
|
||||
|
36
wlauto/common/android/resources.py
Normal file
36
wlauto/common/android/resources.py
Normal file
@ -0,0 +1,36 @@
|
||||
# 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'
|
425
wlauto/common/android/workload.py
Normal file
425
wlauto/common/android/workload.py
Normal file
@ -0,0 +1,425 @@
|
||||
# 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.extension import Parameter
|
||||
from wlauto.core.workload import Workload
|
||||
from wlauto.core.resource import NO_ONE
|
||||
from wlauto.common.resources import ExtensionAsset, Executable
|
||||
from wlauto.exceptions import WorkloadError, ResourceError
|
||||
from wlauto.utils.android import ApkInfo
|
||||
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_file(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.delete_file(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
|
||||
install_timeout = None
|
||||
default_install_timeout = 300
|
||||
|
||||
parameters = [
|
||||
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
|
||||
self.force_reinstall = kwargs.get('force_reinstall', False)
|
||||
if not self.install_timeout:
|
||||
self.install_timeout = self.default_install_timeout
|
||||
|
||||
def init_resources(self, context):
|
||||
self.apk_file = context.resolver.get(wlauto.common.android.resources.ApkFile(self), version=getattr(self, 'version', None))
|
||||
|
||||
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_installed_package_version(self.package)
|
||||
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_reinstall = True
|
||||
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_reinstall:
|
||||
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))
|
||||
|
||||
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 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')
|
||||
self.device.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)
|
||||
|
||||
def validate(self):
|
||||
if not self.apk_file:
|
||||
raise WorkloadError('No APK file found for workload {}.'.format(self.name))
|
||||
|
||||
|
||||
AndroidBenchmark = ApkWorkload # backward compatibility
|
||||
|
||||
|
||||
class ReventWorkload(Workload):
|
||||
|
||||
default_setup_timeout = 5 * 60 # in seconds
|
||||
default_run_timeout = 10 * 60 # in seconds
|
||||
|
||||
def __init__(self, device, _call_super=True, **kwargs):
|
||||
if _call_super:
|
||||
super(ReventWorkload, self).__init__(device, **kwargs)
|
||||
devpath = self.device.path
|
||||
self.on_device_revent_binary = devpath.join(self.device.working_directory, 'revent')
|
||||
self.on_device_setup_revent = devpath.join(self.device.working_directory, '{}.setup.revent'.format(self.device.name))
|
||||
self.on_device_run_revent = devpath.join(self.device.working_directory, '{}.run.revent'.format(self.device.name))
|
||||
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 init_resources(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.delete_file(self.on_device_setup_revent)
|
||||
self.device.delete_file(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_file(self.revent_run_file, self.on_device_run_revent)
|
||||
self.device.push_file(self.revent_setup_file, self.on_device_setup_revent)
|
||||
|
||||
|
||||
class AndroidUiAutoBenchmark(UiAutomatorWorkload, AndroidBenchmark):
|
||||
|
||||
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'
|
||||
install_timeout = 500
|
||||
loading_time = 10
|
||||
|
||||
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 init_resources(self, context):
|
||||
ApkWorkload.init_resources(self, 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)
|
||||
|
||||
def reset(self, context):
|
||||
# If saved state exists, restore it; if not, do full
|
||||
# uninstall/install cycle.
|
||||
if self.saved_state_file:
|
||||
self._deploy_resource_tarball(context, self.saved_state_file)
|
||||
else:
|
||||
ApkWorkload.reset(self, context)
|
||||
self._deploy_assets(context)
|
||||
|
||||
def run(self, context):
|
||||
ReventWorkload.run(self, context)
|
||||
|
||||
def teardown(self, context):
|
||||
if not self.saved_state_file:
|
||||
ApkWorkload.teardown(self, context)
|
||||
else:
|
||||
self.device.execute('am force-stop {}'.format(self.package))
|
||||
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.resource_cache, self.name, resource_file)
|
||||
if not self.device.file_exists(ondevice_cache):
|
||||
asset_tarball = context.resolver.get(ExtensionAsset(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_file(asset_tarball, ondevice_cache)
|
||||
|
||||
device_asset_directory = self.device.path.join(self.device.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)
|
BIN
wlauto/common/bin/arm64/busybox
Executable file
BIN
wlauto/common/bin/arm64/busybox
Executable file
Binary file not shown.
BIN
wlauto/common/bin/arm64/revent
Executable file
BIN
wlauto/common/bin/arm64/revent
Executable file
Binary file not shown.
BIN
wlauto/common/bin/armeabi/busybox
Executable file
BIN
wlauto/common/bin/armeabi/busybox
Executable file
Binary file not shown.
BIN
wlauto/common/bin/armeabi/revent
Executable file
BIN
wlauto/common/bin/armeabi/revent
Executable file
Binary file not shown.
16
wlauto/common/linux/__init__.py
Normal file
16
wlauto/common/linux/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
966
wlauto/common/linux/device.py
Normal file
966
wlauto/common/linux/device.py
Normal file
@ -0,0 +1,966 @@
|
||||
# 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 os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
from wlauto.core.extension import Parameter
|
||||
from wlauto.core.device import Device, RuntimeParameter, CoreParameter
|
||||
from wlauto.core.resource import NO_ONE
|
||||
from wlauto.exceptions import ConfigError, DeviceError, TimeoutError, DeviceNotRespondingError
|
||||
from wlauto.common.resources import Executable
|
||||
from wlauto.utils.cpuinfo import Cpuinfo
|
||||
from wlauto.utils.misc import convert_new_lines, escape_double_quotes
|
||||
from wlauto.utils.ssh import SshShell
|
||||
from wlauto.utils.types import boolean, list_of_strings
|
||||
|
||||
|
||||
# a dict of governor name and a list of it tunables that can't be read
|
||||
WRITE_ONLY_TUNABLES = {
|
||||
'interactive': ['boostpulse']
|
||||
}
|
||||
|
||||
FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
|
||||
PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
|
||||
|
||||
|
||||
class BaseLinuxDevice(Device): # pylint: disable=abstract-method
|
||||
|
||||
path_module = 'posixpath'
|
||||
has_gpu = True
|
||||
|
||||
parameters = [
|
||||
Parameter('scheduler', kind=str, default='unknown',
|
||||
allowed_values=['unknown', 'smp', 'hmp', 'iks', 'ea', 'other'],
|
||||
description="""
|
||||
Specifies the type of multi-core scheduling model utilized in the device. The value
|
||||
must be one of the following:
|
||||
|
||||
:unknown: A generic Device interface is used to interact with the underlying device
|
||||
and the underlying scheduling model is unkown.
|
||||
:smp: A standard single-core or Symmetric Multi-Processing system.
|
||||
:hmp: ARM Heterogeneous Multi-Processing system.
|
||||
:iks: Linaro In-Kernel Switcher.
|
||||
:ea: ARM Energy-Aware scheduler.
|
||||
:other: Any other system not covered by the above.
|
||||
|
||||
.. note:: most currently-available systems would fall under ``smp`` rather than
|
||||
this value. ``other`` is there to future-proof against new schemes
|
||||
not yet covered by WA.
|
||||
|
||||
"""),
|
||||
Parameter('iks_switch_frequency', kind=int, default=None,
|
||||
description="""
|
||||
This is the switching frequency, in kilohertz, of IKS devices. This parameter *MUST NOT*
|
||||
be set for non-IKS device (i.e. ``scheduler != 'iks'``). If left unset for IKS devices,
|
||||
it will default to ``800000``, i.e. 800MHz.
|
||||
"""),
|
||||
|
||||
]
|
||||
|
||||
runtime_parameters = [
|
||||
RuntimeParameter('sysfile_values', 'get_sysfile_values', 'set_sysfile_values', value_name='params'),
|
||||
CoreParameter('${core}_cores', 'get_number_of_active_cores', 'set_number_of_active_cores',
|
||||
value_name='number'),
|
||||
CoreParameter('${core}_min_frequency', 'get_core_min_frequency', 'set_core_min_frequency',
|
||||
value_name='freq'),
|
||||
CoreParameter('${core}_max_frequency', 'get_core_max_frequency', 'set_core_max_frequency',
|
||||
value_name='freq'),
|
||||
CoreParameter('${core}_governor', 'get_core_governor', 'set_core_governor',
|
||||
value_name='governor'),
|
||||
CoreParameter('${core}_governor_tunables', 'get_core_governor_tunables', 'set_core_governor_tunables',
|
||||
value_name='tunables'),
|
||||
]
|
||||
|
||||
@property
|
||||
def active_cpus(self):
|
||||
val = self.get_sysfile_value('/sys/devices/system/cpu/online')
|
||||
cpus = re.findall(r"([\d]\-[\d]|[\d])", val)
|
||||
active_cpus = []
|
||||
for cpu in cpus:
|
||||
if '-' in cpu:
|
||||
lo, hi = cpu.split('-')
|
||||
active_cpus.extend(range(int(lo), int(hi) + 1))
|
||||
else:
|
||||
active_cpus.append(int(cpu))
|
||||
return active_cpus
|
||||
|
||||
@property
|
||||
def number_of_cores(self):
|
||||
"""
|
||||
Added in version 2.1.4.
|
||||
|
||||
"""
|
||||
if self._number_of_cores is None:
|
||||
corere = re.compile('^\s*cpu\d+\s*$')
|
||||
output = self.execute('ls /sys/devices/system/cpu')
|
||||
self._number_of_cores = 0
|
||||
for entry in output.split():
|
||||
if corere.match(entry):
|
||||
self._number_of_cores += 1
|
||||
return self._number_of_cores
|
||||
|
||||
@property
|
||||
def resource_cache(self):
|
||||
return self.path.join(self.working_directory, '.cache')
|
||||
|
||||
@property
|
||||
def file_transfer_cache(self):
|
||||
return self.path.join(self.working_directory, '.transfer')
|
||||
|
||||
@property
|
||||
def cpuinfo(self):
|
||||
if not self._cpuinfo:
|
||||
self._cpuinfo = Cpuinfo(self.execute('cat /proc/cpuinfo'))
|
||||
return self._cpuinfo
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(BaseLinuxDevice, self).__init__(**kwargs)
|
||||
self.busybox = None
|
||||
self._is_initialized = False
|
||||
self._is_ready = False
|
||||
self._just_rebooted = False
|
||||
self._is_rooted = None
|
||||
self._available_frequencies = {}
|
||||
self._available_governors = {}
|
||||
self._available_governor_tunables = {}
|
||||
self._number_of_cores = None
|
||||
self._written_sysfiles = []
|
||||
self._cpuinfo = None
|
||||
|
||||
def validate(self):
|
||||
if len(self.core_names) != len(self.core_clusters):
|
||||
raise ConfigError('core_names and core_clusters are of different lengths.')
|
||||
if self.iks_switch_frequency is not None and self.scheduler != 'iks': # pylint: disable=E0203
|
||||
raise ConfigError('iks_switch_frequency must NOT be set for non-IKS devices.')
|
||||
if self.iks_switch_frequency is None and self.scheduler == 'iks': # pylint: disable=E0203
|
||||
self.iks_switch_frequency = 800000 # pylint: disable=W0201
|
||||
|
||||
def initialize(self, context, *args, **kwargs):
|
||||
self.execute('mkdir -p {}'.format(self.working_directory))
|
||||
if self.is_rooted:
|
||||
if not self.is_installed('busybox'):
|
||||
self.busybox = self.deploy_busybox(context)
|
||||
else:
|
||||
self.busybox = 'busybox'
|
||||
self.init(context, *args, **kwargs)
|
||||
|
||||
def get_sysfile_value(self, sysfile, kind=None):
|
||||
"""
|
||||
Get the contents of the specified sysfile.
|
||||
|
||||
:param sysfile: The file who's contents will be returned.
|
||||
|
||||
:param kind: The type of value to be expected in the sysfile. This can
|
||||
be any Python callable that takes a single str argument.
|
||||
If not specified or is None, the contents will be returned
|
||||
as a string.
|
||||
|
||||
"""
|
||||
output = self.execute('cat \'{}\''.format(sysfile), as_root=True).strip() # pylint: disable=E1103
|
||||
if kind:
|
||||
return kind(output)
|
||||
else:
|
||||
return output
|
||||
|
||||
def set_sysfile_value(self, sysfile, value, verify=True):
|
||||
"""
|
||||
Set the value of the specified sysfile. By default, the value will be checked afterwards.
|
||||
Can be overridden by setting ``verify`` parameter to ``False``.
|
||||
|
||||
"""
|
||||
value = str(value)
|
||||
self.execute('echo {} > \'{}\''.format(value, sysfile), check_exit_code=False, as_root=True)
|
||||
if verify:
|
||||
output = self.get_sysfile_value(sysfile)
|
||||
if not output.strip() == value: # pylint: disable=E1103
|
||||
message = 'Could not set the value of {} to {}'.format(sysfile, value)
|
||||
raise DeviceError(message)
|
||||
self._written_sysfiles.append(sysfile)
|
||||
|
||||
def get_sysfile_values(self):
|
||||
"""
|
||||
Returns a dict mapping paths of sysfiles that were previously set to their
|
||||
current values.
|
||||
|
||||
"""
|
||||
values = {}
|
||||
for sysfile in self._written_sysfiles:
|
||||
values[sysfile] = self.get_sysfile_value(sysfile)
|
||||
return values
|
||||
|
||||
def set_sysfile_values(self, params):
|
||||
"""
|
||||
The plural version of ``set_sysfile_value``. Takes a single parameter which is a mapping of
|
||||
file paths to values to be set. By default, every value written will be verified. The can
|
||||
be disabled for individual paths by appending ``'!'`` to them.
|
||||
|
||||
"""
|
||||
for sysfile, value in params.iteritems():
|
||||
verify = not sysfile.endswith('!')
|
||||
sysfile = sysfile.rstrip('!')
|
||||
self.set_sysfile_value(sysfile, value, verify=verify)
|
||||
|
||||
def deploy_busybox(self, context, force=False):
|
||||
"""
|
||||
Deploys the busybox Android binary (hence in android module) to the
|
||||
specified device, and returns the path to the binary on the device.
|
||||
|
||||
:param device: device to deploy the binary to.
|
||||
:param context: an instance of ExecutionContext
|
||||
:param force: by default, if the binary is already present on the
|
||||
device, it will not be deployed again. Setting force
|
||||
to ``True`` overrides that behavior and ensures that the
|
||||
binary is always copied. Defaults to ``False``.
|
||||
|
||||
:returns: The on-device path to the busybox binary.
|
||||
|
||||
"""
|
||||
on_device_executable = self.path.join(self.binaries_directory, 'busybox')
|
||||
if not force and self.file_exists(on_device_executable):
|
||||
return on_device_executable
|
||||
host_file = context.resolver.get(Executable(NO_ONE, self.abi, 'busybox'))
|
||||
return self.install(host_file)
|
||||
|
||||
def list_file_systems(self):
|
||||
output = self.execute('mount')
|
||||
fstab = []
|
||||
for line in output.split('\n'):
|
||||
fstab.append(FstabEntry(*line.split()))
|
||||
return fstab
|
||||
|
||||
# Process query and control
|
||||
|
||||
def get_pids_of(self, process_name):
|
||||
"""Returns a list of PIDs of all processes with the specified name."""
|
||||
result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
|
||||
if result and 'not found' not in result:
|
||||
return [int(x.split()[1]) for x in result.split('\n')[1:]]
|
||||
else:
|
||||
return []
|
||||
|
||||
def ps(self, **kwargs):
|
||||
"""
|
||||
Returns the list of running processes on the device. Keyword arguments may
|
||||
be used to specify simple filters for columns.
|
||||
|
||||
Added in version 2.1.4
|
||||
|
||||
"""
|
||||
lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
|
||||
lines.next() # header
|
||||
result = []
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if parts:
|
||||
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
|
||||
if not kwargs:
|
||||
return result
|
||||
else:
|
||||
filtered_result = []
|
||||
for entry in result:
|
||||
if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
|
||||
filtered_result.append(entry)
|
||||
return filtered_result
|
||||
|
||||
def kill(self, pid, signal=None, as_root=False): # pylint: disable=W0221
|
||||
"""
|
||||
Kill the specified process.
|
||||
|
||||
:param pid: PID of the process to kill.
|
||||
:param signal: Specify which singal to send to the process. This must
|
||||
be a valid value for -s option of kill. Defaults to ``None``.
|
||||
|
||||
Modified in version 2.1.4: added ``signal`` parameter.
|
||||
|
||||
"""
|
||||
signal_string = '-s {}'.format(signal) if signal else ''
|
||||
self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
|
||||
|
||||
def killall(self, process_name, signal=None, as_root=False): # pylint: disable=W0221
|
||||
"""
|
||||
Kill all processes with the specified name.
|
||||
|
||||
:param process_name: The name of the process(es) to kill.
|
||||
:param signal: Specify which singal to send to the process. This must
|
||||
be a valid value for -s option of kill. Defaults to ``None``.
|
||||
|
||||
Modified in version 2.1.5: added ``as_root`` parameter.
|
||||
|
||||
"""
|
||||
for pid in self.get_pids_of(process_name):
|
||||
self.kill(pid, signal=signal, as_root=as_root)
|
||||
|
||||
# cpufreq
|
||||
|
||||
def list_available_cpu_governors(self, cpu):
|
||||
"""Returns a list of governors supported by the cpu."""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
if cpu not in self._available_governors:
|
||||
cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/scaling_available_governors'.format(cpu)
|
||||
output = self.execute(cmd, check_exit_code=True)
|
||||
self._available_governors[cpu] = output.strip().split() # pylint: disable=E1103
|
||||
return self._available_governors[cpu]
|
||||
|
||||
def get_cpu_governor(self, cpu):
|
||||
"""Returns the governor currently set for the specified CPU."""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_governor'.format(cpu)
|
||||
return self.get_sysfile_value(sysfile)
|
||||
|
||||
def set_cpu_governor(self, cpu, governor, **kwargs):
|
||||
"""
|
||||
Set the governor for the specified CPU.
|
||||
See https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt
|
||||
|
||||
:param cpu: The CPU for which the governor is to be set. This must be
|
||||
the full name as it appears in sysfs, e.g. "cpu0".
|
||||
:param governor: The name of the governor to be used. This must be
|
||||
supported by the specific device.
|
||||
|
||||
Additional keyword arguments can be used to specify governor tunables for
|
||||
governors that support them.
|
||||
|
||||
:note: On big.LITTLE all cores in a cluster must be using the same governor.
|
||||
Setting the governor on any core in a cluster will also set it on all
|
||||
other cores in that cluster.
|
||||
|
||||
:raises: ConfigError if governor is not supported by the CPU.
|
||||
:raises: DeviceError if, for some reason, the governor could not be set.
|
||||
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
supported = self.list_available_cpu_governors(cpu)
|
||||
if governor not in supported:
|
||||
raise ConfigError('Governor {} not supported for cpu {}'.format(governor, cpu))
|
||||
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_governor'.format(cpu)
|
||||
self.set_sysfile_value(sysfile, governor)
|
||||
self.set_cpu_governor_tunables(cpu, governor, **kwargs)
|
||||
|
||||
def list_available_cpu_governor_tunables(self, cpu):
|
||||
"""Returns a list of tunables available for the governor on the specified CPU."""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
governor = self.get_cpu_governor(cpu)
|
||||
if governor not in self._available_governor_tunables:
|
||||
try:
|
||||
tunables_path = '/sys/devices/system/cpu/{}/cpufreq/{}'.format(cpu, governor)
|
||||
self._available_governor_tunables[governor] = self.listdir(tunables_path)
|
||||
except DeviceError: # probably an older kernel
|
||||
try:
|
||||
tunables_path = '/sys/devices/system/cpu/cpufreq/{}'.format(governor)
|
||||
self._available_governor_tunables[governor] = self.listdir(tunables_path)
|
||||
except DeviceError: # governor does not support tunables
|
||||
self._available_governor_tunables[governor] = []
|
||||
return self._available_governor_tunables[governor]
|
||||
|
||||
def get_cpu_governor_tunables(self, cpu):
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
governor = self.get_cpu_governor(cpu)
|
||||
tunables = {}
|
||||
for tunable in self.list_available_cpu_governor_tunables(cpu):
|
||||
if tunable not in WRITE_ONLY_TUNABLES.get(governor, []):
|
||||
try:
|
||||
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
|
||||
tunables[tunable] = self.get_sysfile_value(path)
|
||||
except DeviceError: # May be an older kernel
|
||||
path = '/sys/devices/system/cpu/cpufreq/{}/{}'.format(governor, tunable)
|
||||
tunables[tunable] = self.get_sysfile_value(path)
|
||||
return tunables
|
||||
|
||||
def set_cpu_governor_tunables(self, cpu, governor, **kwargs):
|
||||
"""
|
||||
Set tunables for the specified governor. Tunables should be specified as
|
||||
keyword arguments. Which tunables and values are valid depends on the
|
||||
governor.
|
||||
|
||||
:param cpu: The cpu for which the governor will be set. This must be the
|
||||
full cpu name as it appears in sysfs, e.g. ``cpu0``.
|
||||
:param governor: The name of the governor. Must be all lower case.
|
||||
|
||||
The rest should be keyword parameters mapping tunable name onto the value to
|
||||
be set for it.
|
||||
|
||||
:raises: ConfigError if governor specified is not a valid governor name, or if
|
||||
a tunable specified is not valid for the governor.
|
||||
:raises: DeviceError if could not set tunable.
|
||||
|
||||
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
valid_tunables = self.list_available_cpu_governor_tunables(cpu)
|
||||
for tunable, value in kwargs.iteritems():
|
||||
if tunable in valid_tunables:
|
||||
try:
|
||||
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
|
||||
self.set_sysfile_value(path, value)
|
||||
except DeviceError: # May be an older kernel
|
||||
path = '/sys/devices/system/cpu/cpufreq/{}/{}'.format(governor, tunable)
|
||||
self.set_sysfile_value(path, value)
|
||||
else:
|
||||
message = 'Unexpected tunable {} for governor {} on {}.\n'.format(tunable, governor, cpu)
|
||||
message += 'Available tunables are: {}'.format(valid_tunables)
|
||||
raise ConfigError(message)
|
||||
|
||||
def enable_cpu(self, cpu):
|
||||
"""
|
||||
Enable the specified core.
|
||||
|
||||
:param cpu: CPU core to enable. This must be the full name as it
|
||||
appears in sysfs, e.g. "cpu0".
|
||||
|
||||
"""
|
||||
self.hotplug_cpu(cpu, online=True)
|
||||
|
||||
def disable_cpu(self, cpu):
|
||||
"""
|
||||
Disable the specified core.
|
||||
|
||||
:param cpu: CPU core to disable. This must be the full name as it
|
||||
appears in sysfs, e.g. "cpu0".
|
||||
"""
|
||||
self.hotplug_cpu(cpu, online=False)
|
||||
|
||||
def hotplug_cpu(self, cpu, online):
|
||||
"""
|
||||
Hotplug the specified CPU either on or off.
|
||||
See https://www.kernel.org/doc/Documentation/cpu-hotplug.txt
|
||||
|
||||
:param cpu: The CPU for which the governor is to be set. This must be
|
||||
the full name as it appears in sysfs, e.g. "cpu0".
|
||||
:param online: CPU will be enabled if this value bool()'s to True, and
|
||||
will be disabled otherwise.
|
||||
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
status = 1 if online else 0
|
||||
sysfile = '/sys/devices/system/cpu/{}/online'.format(cpu)
|
||||
self.set_sysfile_value(sysfile, status)
|
||||
|
||||
def list_available_cpu_frequencies(self, cpu):
|
||||
"""Returns a list of frequencies supported by the cpu or an empty list
|
||||
if not could be found."""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
if cpu not in self._available_frequencies:
|
||||
try:
|
||||
cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/scaling_available_frequencies'.format(cpu)
|
||||
output = self.execute(cmd)
|
||||
self._available_frequencies[cpu] = map(int, output.strip().split()) # pylint: disable=E1103
|
||||
except DeviceError:
|
||||
# we return an empty list because on some devices scaling_available_frequencies
|
||||
# is not generated. So we are returing an empty list as an indication
|
||||
# http://adrynalyne-teachtofish.blogspot.co.uk/2011/11/how-to-enable-scalingavailablefrequenci.html
|
||||
self._available_frequencies[cpu] = []
|
||||
return self._available_frequencies[cpu]
|
||||
|
||||
def get_cpu_min_frequency(self, cpu):
|
||||
"""
|
||||
Returns the min frequency currently set for the specified CPU.
|
||||
|
||||
Warning, this method does not check if the cpu is online or not. It will
|
||||
try to read the minimum frequency and the following exception will be
|
||||
raised ::
|
||||
|
||||
:raises: DeviceError if for some reason the frequency could not be read.
|
||||
|
||||
"""
|
||||
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_min_freq'.format(cpu)
|
||||
return self.get_sysfile_value(sysfile)
|
||||
|
||||
def set_cpu_min_frequency(self, cpu, frequency):
|
||||
"""
|
||||
Set's the minimum value for CPU frequency. Actual frequency will
|
||||
depend on the Governor used and may vary during execution. The value should be
|
||||
either an int or a string representing an integer. The Value must also be
|
||||
supported by the device. The available frequencies can be obtained by calling
|
||||
get_available_frequencies() or examining
|
||||
|
||||
/sys/devices/system/cpu/cpuX/cpufreq/scaling_available_frequencies
|
||||
|
||||
on the device.
|
||||
|
||||
:raises: ConfigError if the frequency is not supported by the CPU.
|
||||
:raises: DeviceError if, for some reason, frequency could not be set.
|
||||
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
available_frequencies = self.list_available_cpu_frequencies(cpu)
|
||||
try:
|
||||
value = int(frequency)
|
||||
if available_frequencies and value not in available_frequencies:
|
||||
raise ConfigError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
|
||||
value,
|
||||
available_frequencies))
|
||||
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_min_freq'.format(cpu)
|
||||
self.set_sysfile_value(sysfile, value)
|
||||
except ValueError:
|
||||
raise ValueError('value must be an integer; got: "{}"'.format(value))
|
||||
|
||||
def get_cpu_max_frequency(self, cpu):
|
||||
"""
|
||||
Returns the max frequency currently set for the specified CPU.
|
||||
|
||||
Warning, this method does not check if the cpu is online or not. It will
|
||||
try to read the maximum frequency and the following exception will be
|
||||
raised ::
|
||||
|
||||
:raises: DeviceError if for some reason the frequency could not be read.
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_max_freq'.format(cpu)
|
||||
return self.get_sysfile_value(sysfile)
|
||||
|
||||
def set_cpu_max_frequency(self, cpu, frequency):
|
||||
"""
|
||||
Set's the minimum value for CPU frequency. Actual frequency will
|
||||
depend on the Governor used and may vary during execution. The value should be
|
||||
either an int or a string representing an integer. The Value must also be
|
||||
supported by the device. The available frequencies can be obtained by calling
|
||||
get_available_frequencies() or examining
|
||||
|
||||
/sys/devices/system/cpu/cpuX/cpufreq/scaling_available_frequencies
|
||||
|
||||
on the device.
|
||||
|
||||
:raises: ConfigError if the frequency is not supported by the CPU.
|
||||
:raises: DeviceError if, for some reason, frequency could not be set.
|
||||
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
available_frequencies = self.list_available_cpu_frequencies(cpu)
|
||||
try:
|
||||
value = int(frequency)
|
||||
if available_frequencies and value not in available_frequencies:
|
||||
raise DeviceError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
|
||||
value,
|
||||
available_frequencies))
|
||||
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_max_freq'.format(cpu)
|
||||
self.set_sysfile_value(sysfile, value)
|
||||
except ValueError:
|
||||
raise ValueError('value must be an integer; got: "{}"'.format(value))
|
||||
|
||||
def get_cpuidle_states(self, cpu=0):
|
||||
"""
|
||||
Return map of cpuidle states with their descriptive names.
|
||||
"""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
cpuidle_states = {}
|
||||
statere = re.compile('^\s*state\d+\s*$')
|
||||
output = self.execute("ls /sys/devices/system/cpu/{}/cpuidle".format(cpu))
|
||||
for entry in output.split():
|
||||
if statere.match(entry):
|
||||
cpuidle_states[entry] = self.get_sysfile_value("/sys/devices/system/cpu/{}/cpuidle/{}/desc".format(cpu, entry))
|
||||
return cpuidle_states
|
||||
|
||||
# Core- and cluster-level mapping for the above cpu-level APIs above. The
|
||||
# APIs make the following assumptions, which were True for all devices that
|
||||
# existed at the time of writing:
|
||||
# 1. A cluster can only contain cores of one type.
|
||||
# 2. All cores in a cluster are tied to the same DVFS domain, therefore
|
||||
# changes to cpufreq for a core will affect all other cores on the
|
||||
# same cluster.
|
||||
|
||||
def get_core_clusters(self, core, strict=True):
|
||||
"""Returns the list of clusters that contain the specified core. if ``strict``
|
||||
is ``True``, raises ValueError if no clusters has been found (returns empty list
|
||||
if ``strict`` is ``False``)."""
|
||||
core_indexes = [i for i, c in enumerate(self.core_names) if c == core]
|
||||
clusters = sorted(list(set(self.core_clusters[i] for i in core_indexes)))
|
||||
if strict and not clusters:
|
||||
raise ValueError('No active clusters for core {}'.format(core))
|
||||
return clusters
|
||||
|
||||
def get_cluster_cpu(self, cluster):
|
||||
"""Returns the first *active* cpu for the cluster. If the entire cluster
|
||||
has been hotplugged, this will raise a ``ValueError``."""
|
||||
cpu_indexes = set([i for i, c in enumerate(self.core_clusters) if c == cluster])
|
||||
active_cpus = sorted(list(cpu_indexes.intersection(self.active_cpus)))
|
||||
if not active_cpus:
|
||||
raise ValueError('All cpus for cluster {} are offline'.format(cluster))
|
||||
return active_cpus[0]
|
||||
|
||||
def list_available_cluster_governors(self, cluster):
|
||||
return self.list_available_cpu_governors(self.get_cluster_cpu(cluster))
|
||||
|
||||
def get_cluster_governor(self, cluster):
|
||||
return self.get_cpu_governor(self.get_cluster_cpu(cluster))
|
||||
|
||||
def set_cluster_governor(self, cluster, governor, **tunables):
|
||||
return self.set_cpu_governor(self.get_cluster_cpu(cluster), governor, **tunables)
|
||||
|
||||
def list_available_cluster_governor_tunables(self, cluster):
|
||||
return self.list_available_cpu_governor_tunables(self.get_cluster_cpu(cluster))
|
||||
|
||||
def get_cluster_governor_tunables(self, cluster):
|
||||
return self.get_cpu_governor_tunables(self.get_cluster_cpu(cluster))
|
||||
|
||||
def set_cluster_governor_tunables(self, cluster, governor, **tunables):
|
||||
return self.set_cpu_governor_tunables(self.get_cluster_cpu(cluster), governor, **tunables)
|
||||
|
||||
def get_cluster_min_frequency(self, cluster):
|
||||
return self.get_cpu_min_frequency(self.get_cluster_cpu(cluster))
|
||||
|
||||
def set_cluster_min_frequency(self, cluster, freq):
|
||||
return self.set_cpu_min_frequency(self.get_cluster_cpu(cluster), freq)
|
||||
|
||||
def get_cluster_max_frequency(self, cluster):
|
||||
return self.get_cpu_max_frequency(self.get_cluster_cpu(cluster))
|
||||
|
||||
def set_cluster_max_frequency(self, cluster, freq):
|
||||
return self.set_cpu_max_frequency(self.get_cluster_cpu(cluster), freq)
|
||||
|
||||
def get_core_cpu(self, core):
|
||||
for cluster in self.get_core_clusters(core):
|
||||
try:
|
||||
return self.get_cluster_cpu(cluster)
|
||||
except ValueError:
|
||||
pass
|
||||
raise ValueError('No active CPUs found for core {}'.format(core))
|
||||
|
||||
def list_available_core_governors(self, core):
|
||||
return self.list_available_cpu_governors(self.get_core_cpu(core))
|
||||
|
||||
def get_core_governor(self, core):
|
||||
return self.get_cpu_governor(self.get_core_cpu(core))
|
||||
|
||||
def set_core_governor(self, core, governor, **tunables):
|
||||
for cluster in self.get_core_clusters(core):
|
||||
self.set_cluster_governor(cluster, governor, **tunables)
|
||||
|
||||
def list_available_core_governor_tunables(self, core):
|
||||
return self.list_available_cpu_governor_tunables(self.get_core_cpu(core))
|
||||
|
||||
def get_core_governor_tunables(self, core):
|
||||
return self.get_cpu_governor_tunables(self.get_core_cpu(core))
|
||||
|
||||
def set_core_governor_tunables(self, core, tunables):
|
||||
for cluster in self.get_core_clusters(core):
|
||||
governor = self.get_cluster_governor(cluster)
|
||||
self.set_cluster_governor_tunables(cluster, governor, **tunables)
|
||||
|
||||
def get_core_min_frequency(self, core):
|
||||
return self.get_cpu_min_frequency(self.get_core_cpu(core))
|
||||
|
||||
def set_core_min_frequency(self, core, freq):
|
||||
for cluster in self.get_core_clusters(core):
|
||||
self.set_cluster_min_frequency(cluster, freq)
|
||||
|
||||
def get_core_max_frequency(self, core):
|
||||
return self.get_cpu_max_frequency(self.get_core_cpu(core))
|
||||
|
||||
def set_core_max_frequency(self, core, freq):
|
||||
for cluster in self.get_core_clusters(core):
|
||||
self.set_cluster_max_frequency(cluster, freq)
|
||||
|
||||
def get_number_of_active_cores(self, core):
|
||||
if core not in self.core_names:
|
||||
raise ValueError('Unexpected core: {}; must be in {}'.format(core, list(set(self.core_names))))
|
||||
active_cpus = self.active_cpus
|
||||
num_active_cores = 0
|
||||
for i, c in enumerate(self.core_names):
|
||||
if c == core and i in active_cpus:
|
||||
num_active_cores += 1
|
||||
return num_active_cores
|
||||
|
||||
def set_number_of_active_cores(self, core, number):
|
||||
if core not in self.core_names:
|
||||
raise ValueError('Unexpected core: {}; must be in {}'.format(core, list(set(self.core_names))))
|
||||
core_ids = [i for i, c in enumerate(self.core_names) if c == core]
|
||||
max_cores = len(core_ids)
|
||||
if number > max_cores:
|
||||
message = 'Attempting to set the number of active {} to {}; maximum is {}'
|
||||
raise ValueError(message.format(core, number, max_cores))
|
||||
for i in xrange(0, number):
|
||||
self.enable_cpu(core_ids[i])
|
||||
for i in xrange(number, max_cores):
|
||||
self.disable_cpu(core_ids[i])
|
||||
|
||||
# internal methods
|
||||
|
||||
def _check_ready(self):
|
||||
if not self._is_ready:
|
||||
raise AttributeError('Device not ready.')
|
||||
|
||||
def _get_core_cluster(self, core):
|
||||
"""Returns the first cluster that has cores of the specified type. Raises
|
||||
value error if no cluster for the specified type has been found"""
|
||||
core_indexes = [i for i, c in enumerate(self.core_names) if c == core]
|
||||
core_clusters = set(self.core_clusters[i] for i in core_indexes)
|
||||
if not core_clusters:
|
||||
raise ValueError('No cluster found for core {}'.format(core))
|
||||
return sorted(list(core_clusters))[0]
|
||||
|
||||
|
||||
class LinuxDevice(BaseLinuxDevice):
|
||||
|
||||
platform = 'linux'
|
||||
|
||||
default_timeout = 30
|
||||
delay = 2
|
||||
long_delay = 3 * delay
|
||||
ready_timeout = 60
|
||||
|
||||
parameters = [
|
||||
Parameter('host', mandatory=True, description='Host name or IP address for the device.'),
|
||||
Parameter('username', mandatory=True, description='User name for the account on the device.'),
|
||||
Parameter('password', description='Password for the account on the device (for password-based auth).'),
|
||||
Parameter('keyfile', description='Keyfile to be used for key-based authentication.'),
|
||||
Parameter('port', kind=int, description='SSH port number on the device.'),
|
||||
|
||||
Parameter('use_telnet', kind=boolean, default=False,
|
||||
description='Optionally, telnet may be used instead of ssh, though this is discouraged.'),
|
||||
|
||||
Parameter('working_directory', default=None,
|
||||
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', default='/usr/local/bin',
|
||||
description='Location of executable binaries on this device (must be in PATH).'),
|
||||
Parameter('property_files', kind=list_of_strings,
|
||||
default=['/proc/version', '/etc/debian_version', '/etc/lsb-release', '/etc/arch-release'],
|
||||
description='''
|
||||
A list of paths to files containing static OS properties. These will be pulled into the
|
||||
__meta directory in output for each run in order to provide information about the platfrom.
|
||||
These paths do not have to exist and will be ignored if the path is not present on a
|
||||
particular device.
|
||||
'''),
|
||||
]
|
||||
|
||||
@property
|
||||
def is_rooted(self):
|
||||
if self._is_rooted is None:
|
||||
try:
|
||||
self.execute('ls /', as_root=True)
|
||||
self._is_rooted = True
|
||||
except DeviceError:
|
||||
self._is_rooted = False
|
||||
return self._is_rooted
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LinuxDevice, self).__init__(*args, **kwargs)
|
||||
self.shell = None
|
||||
self.local_binaries_directory = None
|
||||
self._is_rooted = None
|
||||
|
||||
def validate(self):
|
||||
if not self.password and not self.keyfile:
|
||||
raise ConfigError('Either a password or a keyfile must be provided.')
|
||||
if self.working_directory is None: # pylint: disable=access-member-before-definition
|
||||
if self.username == 'root':
|
||||
self.working_directory = '/root/wa' # pylint: disable=attribute-defined-outside-init
|
||||
else:
|
||||
self.working_directory = '/home/{}/wa'.format(self.username) # pylint: disable=attribute-defined-outside-init
|
||||
self.local_binaries_directory = self.path.join(self.working_directory, 'bin')
|
||||
|
||||
def initialize(self, context, *args, **kwargs):
|
||||
self.execute('mkdir -p {}'.format(self.local_binaries_directory))
|
||||
self.execute('export PATH={}:$PATH'.format(self.local_binaries_directory))
|
||||
super(LinuxDevice, self).initialize(context, *args, **kwargs)
|
||||
|
||||
# Power control
|
||||
|
||||
def reset(self):
|
||||
self._is_ready = False
|
||||
self.execute('reboot', as_root=True)
|
||||
|
||||
def hard_reset(self):
|
||||
super(LinuxDevice, self).hard_reset()
|
||||
self._is_ready = False
|
||||
|
||||
def boot(self, **kwargs):
|
||||
self.reset()
|
||||
|
||||
def connect(self): # NOQA pylint: disable=R0912
|
||||
self.shell = SshShell(timeout=self.default_timeout)
|
||||
self.shell.login(self.host, self.username, self.password, self.keyfile, self.port, telnet=self.use_telnet)
|
||||
self._is_ready = True
|
||||
|
||||
def disconnect(self): # NOQA pylint: disable=R0912
|
||||
self.shell.logout()
|
||||
self._is_ready = False
|
||||
|
||||
# Execution
|
||||
|
||||
def has_root(self):
|
||||
try:
|
||||
self.execute('ls /', as_root=True)
|
||||
return True
|
||||
except DeviceError as e:
|
||||
if 'not in the sudoers file' not in e.message:
|
||||
raise e
|
||||
return False
|
||||
|
||||
def execute(self, command, timeout=default_timeout, check_exit_code=True, background=False,
|
||||
as_root=False, strip_colors=True, **kwargs):
|
||||
"""
|
||||
Execute the specified command on the device using adb.
|
||||
|
||||
Parameters:
|
||||
|
||||
:param command: The command to be executed. It should appear exactly
|
||||
as if you were typing it into a shell.
|
||||
:param timeout: Time, in seconds, to wait for adb to return before aborting
|
||||
and raising an error. Defaults to ``AndroidDevice.default_timeout``.
|
||||
:param check_exit_code: If ``True``, the return code of the command on the Device will
|
||||
be check and exception will be raised if it is not 0.
|
||||
Defaults to ``True``.
|
||||
:param background: If ``True``, will execute create a new ssh shell rather than using
|
||||
the default session and will return it immediately. If this is ``True``,
|
||||
``timeout``, ``strip_colors`` and (obvisously) ``check_exit_code`` will
|
||||
be ignored; also, with this, ``as_root=True`` is only valid if ``username``
|
||||
for the device was set to ``root``.
|
||||
:param as_root: If ``True``, will attempt to execute command in privileged mode. The device
|
||||
must be rooted, otherwise an error will be raised. Defaults to ``False``.
|
||||
|
||||
Added in version 2.1.3
|
||||
|
||||
:returns: If ``background`` parameter is set to ``True``, the subprocess object will
|
||||
be returned; otherwise, the contents of STDOUT from the device will be returned.
|
||||
|
||||
"""
|
||||
self._check_ready()
|
||||
if background:
|
||||
if as_root and self.username != 'root':
|
||||
raise DeviceError('Cannot execute in background with as_root=True unless user is root.')
|
||||
return self.shell.background(command)
|
||||
else:
|
||||
return self.shell.execute(command, timeout, check_exit_code, as_root, strip_colors)
|
||||
|
||||
def kick_off(self, command):
|
||||
"""
|
||||
Like execute but closes adb session and returns immediately, leaving the command running on the
|
||||
device (this is different from execute(background=True) which keeps adb connection open and returns
|
||||
a subprocess object).
|
||||
|
||||
"""
|
||||
self._check_ready()
|
||||
command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
|
||||
return self.shell.execute(command)
|
||||
|
||||
# File management
|
||||
|
||||
def push_file(self, source, dest, as_root=False, timeout=default_timeout): # pylint: disable=W0221
|
||||
self._check_ready()
|
||||
if not as_root or self.username == 'root':
|
||||
self.shell.push_file(source, dest, timeout=timeout)
|
||||
else:
|
||||
tempfile = self.path.join(self.working_directory, self.path.basename(dest))
|
||||
self.shell.push_file(source, tempfile, timeout=timeout)
|
||||
self.shell.execute('cp -r {} {}'.format(tempfile, dest), timeout=timeout, as_root=True)
|
||||
|
||||
def pull_file(self, source, dest, as_root=False, timeout=default_timeout): # pylint: disable=W0221
|
||||
self._check_ready()
|
||||
if not as_root or self.username == 'root':
|
||||
self.shell.pull_file(source, dest, timeout=timeout)
|
||||
else:
|
||||
tempfile = self.path.join(self.working_directory, self.path.basename(source))
|
||||
self.shell.execute('cp -r {} {}'.format(source, tempfile), timeout=timeout, as_root=True)
|
||||
self.shell.execute('chown -R {} {}'.format(self.username, tempfile), timeout=timeout, as_root=True)
|
||||
self.shell.pull_file(tempfile, dest, timeout=timeout)
|
||||
|
||||
def delete_file(self, filepath, as_root=False): # pylint: disable=W0221
|
||||
self.execute('rm -rf {}'.format(filepath), as_root=as_root)
|
||||
|
||||
def file_exists(self, filepath):
|
||||
output = self.execute('if [ -e \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
|
||||
return boolean(output.strip()) # pylint: disable=maybe-no-member
|
||||
|
||||
def listdir(self, path, as_root=False, **kwargs):
|
||||
contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
|
||||
return [x.strip() for x in contents.split('\n')] # pylint: disable=maybe-no-member
|
||||
|
||||
def install(self, filepath, timeout=default_timeout, with_name=None): # pylint: disable=W0221
|
||||
if self.is_rooted:
|
||||
destpath = self.path.join(self.binaries_directory,
|
||||
with_name and with_name or self.path.basename(filepath))
|
||||
self.push_file(filepath, destpath, as_root=True)
|
||||
self.execute('chmod a+x {}'.format(destpath), timeout=timeout, as_root=True)
|
||||
else:
|
||||
destpath = self.path.join(self.local_binaries_directory,
|
||||
with_name and with_name or self.path.basename(filepath))
|
||||
self.push_file(filepath, destpath)
|
||||
self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
|
||||
return destpath
|
||||
|
||||
install_executable = install # compatibility
|
||||
|
||||
def uninstall(self, name):
|
||||
path = self.path.join(self.local_binaries_directory, name)
|
||||
self.delete_file(path)
|
||||
|
||||
uninstall_executable = uninstall # compatibility
|
||||
|
||||
def is_installed(self, name):
|
||||
try:
|
||||
self.execute('which {}'.format(name))
|
||||
return True
|
||||
except DeviceError:
|
||||
return False
|
||||
|
||||
# misc
|
||||
|
||||
def ping(self):
|
||||
try:
|
||||
# May be triggered inside initialize()
|
||||
self.shell.execute('ls /', timeout=5)
|
||||
except (TimeoutError, CalledProcessError):
|
||||
raise DeviceNotRespondingError(self.host)
|
||||
|
||||
def capture_screen(self, filepath):
|
||||
if not self.is_installed('scrot'):
|
||||
self.logger.debug('Could not take screenshot as scrot is not installed.')
|
||||
return
|
||||
try:
|
||||
tempfile = self.path.join(self.working_directory, os.path.basename(filepath))
|
||||
self.execute('DISPLAY=:0.0 scrot {}'.format(tempfile))
|
||||
self.pull_file(tempfile, filepath)
|
||||
self.delete_file(tempfile)
|
||||
except DeviceError as e:
|
||||
if "Can't open X dispay." not in e.message:
|
||||
raise e
|
||||
message = e.message.split('OUTPUT:', 1)[1].strip()
|
||||
self.logger.debug('Could not take screenshot: {}'.format(message))
|
||||
|
||||
def is_screen_on(self):
|
||||
pass # TODO
|
||||
|
||||
def ensure_screen_is_on(self):
|
||||
pass # TODO
|
||||
|
||||
def get_properties(self, context):
|
||||
for propfile in self.property_files:
|
||||
if not self.file_exists(propfile):
|
||||
continue
|
||||
normname = propfile.lstrip(self.path.sep).replace(self.path.sep, '.')
|
||||
outfile = os.path.join(context.host_working_directory, normname)
|
||||
self.pull_file(propfile, outfile)
|
||||
return {}
|
||||
|
64
wlauto/common/resources.py
Normal file
64
wlauto/common/resources.py
Normal file
@ -0,0 +1,64 @@
|
||||
# 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 ExtensionAsset(File):
|
||||
|
||||
name = 'extension_asset'
|
||||
|
||||
def __init__(self, owner, path):
|
||||
super(ExtensionAsset, 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)
|
284
wlauto/config_example.py
Normal file
284
wlauto/config_example.py
Normal file
@ -0,0 +1,284 @@
|
||||
"""
|
||||
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'
|
||||
|
||||
####################################################################################################
|
||||
######################################### 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 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']
|
||||
|
||||
####################################################################################################
|
||||
##################################### streamline configuration #####################################
|
||||
|
||||
# The port number on which gatord will listen
|
||||
#port = 8080
|
||||
|
||||
# Enabling/disabling the run of 'streamline -analyze' on the captured data.
|
||||
#streamline_analyze = True
|
||||
|
||||
# Enabling/disabling the generation of a CSV report
|
||||
#streamline_report_csv = True
|
||||
|
||||
####################################################################################################
|
||||
###################################### 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
|
16
wlauto/core/__init__.py
Normal file
16
wlauto/core/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
244
wlauto/core/agenda.py
Normal file
244
wlauto/core/agenda.py
Normal file
@ -0,0 +1,244 @@
|
||||
# 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 copy import copy
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
from wlauto.exceptions import ConfigError
|
||||
from wlauto.utils.misc import load_struct_from_yaml, LoadSyntaxError
|
||||
from wlauto.utils.types import counter, reset_counter
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def get_aliased_param(d, aliases, default=None, pop=True):
|
||||
alias_map = [i for i, a in enumerate(aliases) if a in d]
|
||||
if len(alias_map) > 1:
|
||||
message = 'Only one of {} may be specified in a single entry'
|
||||
raise ConfigError(message.format(aliases))
|
||||
elif alias_map:
|
||||
if pop:
|
||||
return d.pop(aliases[alias_map[0]])
|
||||
else:
|
||||
return d[aliases[alias_map[0]]]
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
class AgendaEntry(object):
|
||||
|
||||
def to_dict(self):
|
||||
return copy(self.__dict__)
|
||||
|
||||
|
||||
class AgendaWorkloadEntry(AgendaEntry):
|
||||
"""
|
||||
Specifies execution of a workload, including things like the number of
|
||||
iterations, device runtime_parameters configuration, etc.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AgendaWorkloadEntry, self).__init__()
|
||||
self.id = kwargs.pop('id')
|
||||
self.workload_name = get_aliased_param(kwargs, ['workload_name', 'name'])
|
||||
if not self.workload_name:
|
||||
raise ConfigError('No workload name specified in entry {}'.format(self.id))
|
||||
self.label = kwargs.pop('label', self.workload_name)
|
||||
self.number_of_iterations = kwargs.pop('iterations', None)
|
||||
self.boot_parameters = get_aliased_param(kwargs,
|
||||
['boot_parameters', 'boot_params'],
|
||||
default=OrderedDict())
|
||||
self.runtime_parameters = get_aliased_param(kwargs,
|
||||
['runtime_parameters', 'runtime_params'],
|
||||
default=OrderedDict())
|
||||
self.workload_parameters = get_aliased_param(kwargs,
|
||||
['workload_parameters', 'workload_params', 'params'],
|
||||
default=OrderedDict())
|
||||
self.instrumentation = kwargs.pop('instrumentation', [])
|
||||
self.flash = kwargs.pop('flash', OrderedDict())
|
||||
if kwargs:
|
||||
raise ConfigError('Invalid entry(ies) in workload {}: {}'.format(self.id, ', '.join(kwargs.keys())))
|
||||
|
||||
|
||||
class AgendaSectionEntry(AgendaEntry):
|
||||
"""
|
||||
Specifies execution of a workload, including things like the number of
|
||||
iterations, device runtime_parameters configuration, etc.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, agenda, **kwargs):
|
||||
super(AgendaSectionEntry, self).__init__()
|
||||
self.id = kwargs.pop('id')
|
||||
self.number_of_iterations = kwargs.pop('iterations', None)
|
||||
self.boot_parameters = get_aliased_param(kwargs,
|
||||
['boot_parameters', 'boot_params'],
|
||||
default=OrderedDict())
|
||||
self.runtime_parameters = get_aliased_param(kwargs,
|
||||
['runtime_parameters', 'runtime_params', 'params'],
|
||||
default=OrderedDict())
|
||||
self.workload_parameters = get_aliased_param(kwargs,
|
||||
['workload_parameters', 'workload_params'],
|
||||
default=OrderedDict())
|
||||
self.instrumentation = kwargs.pop('instrumentation', [])
|
||||
self.flash = kwargs.pop('flash', OrderedDict())
|
||||
self.workloads = []
|
||||
for w in kwargs.pop('workloads', []):
|
||||
self.workloads.append(agenda.get_workload_entry(w))
|
||||
if kwargs:
|
||||
raise ConfigError('Invalid entry(ies) in section {}: {}'.format(self.id, ', '.join(kwargs.keys())))
|
||||
|
||||
def to_dict(self):
|
||||
d = copy(self.__dict__)
|
||||
d['workloads'] = [w.to_dict() for w in self.workloads]
|
||||
return d
|
||||
|
||||
|
||||
class AgendaGlobalEntry(AgendaEntry):
|
||||
"""
|
||||
Workload configuration global to all workloads.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(AgendaGlobalEntry, self).__init__()
|
||||
self.number_of_iterations = kwargs.pop('iterations', None)
|
||||
self.boot_parameters = get_aliased_param(kwargs,
|
||||
['boot_parameters', 'boot_params'],
|
||||
default=OrderedDict())
|
||||
self.runtime_parameters = get_aliased_param(kwargs,
|
||||
['runtime_parameters', 'runtime_params', 'params'],
|
||||
default=OrderedDict())
|
||||
self.workload_parameters = get_aliased_param(kwargs,
|
||||
['workload_parameters', 'workload_params'],
|
||||
default=OrderedDict())
|
||||
self.instrumentation = kwargs.pop('instrumentation', [])
|
||||
self.flash = kwargs.pop('flash', OrderedDict())
|
||||
if kwargs:
|
||||
raise ConfigError('Invalid entries in global section: {}'.format(kwargs))
|
||||
|
||||
|
||||
class Agenda(object):
|
||||
|
||||
def __init__(self, source=None):
|
||||
self.filepath = None
|
||||
self.config = None
|
||||
self.global_ = None
|
||||
self.sections = []
|
||||
self.workloads = []
|
||||
self._seen_ids = defaultdict(set)
|
||||
if source:
|
||||
try:
|
||||
reset_counter('section')
|
||||
reset_counter('workload')
|
||||
self._load(source)
|
||||
except (ConfigError, LoadSyntaxError, SyntaxError), e:
|
||||
raise ConfigError(str(e))
|
||||
|
||||
def add_workload_entry(self, w):
|
||||
entry = self.get_workload_entry(w)
|
||||
self.workloads.append(entry)
|
||||
|
||||
def get_workload_entry(self, w):
|
||||
if isinstance(w, basestring):
|
||||
w = {'name': w}
|
||||
if not isinstance(w, dict):
|
||||
raise ConfigError('Invalid workload entry: "{}" in {}'.format(w, self.filepath))
|
||||
self._assign_id_if_needed(w, 'workload')
|
||||
return AgendaWorkloadEntry(**w)
|
||||
|
||||
def _load(self, source):
|
||||
raw = self._load_raw_from_source(source)
|
||||
if not isinstance(raw, dict):
|
||||
message = '{} does not contain a valid agenda structure; top level must be a dict.'
|
||||
raise ConfigError(message.format(self.filepath))
|
||||
for k, v in raw.iteritems():
|
||||
if k == 'config':
|
||||
self.config = v
|
||||
elif k == 'global':
|
||||
self.global_ = AgendaGlobalEntry(**v)
|
||||
elif k == 'sections':
|
||||
self._collect_existing_ids(v, 'section')
|
||||
for s in v:
|
||||
if not isinstance(s, dict):
|
||||
raise ConfigError('Invalid section entry: "{}" in {}'.format(s, self.filepath))
|
||||
self._collect_existing_ids(s.get('workloads', []), 'workload')
|
||||
for s in v:
|
||||
self._assign_id_if_needed(s, 'section')
|
||||
self.sections.append(AgendaSectionEntry(self, **s))
|
||||
elif k == 'workloads':
|
||||
self._collect_existing_ids(v, 'workload')
|
||||
for w in v:
|
||||
self.workloads.append(self.get_workload_entry(w))
|
||||
else:
|
||||
raise ConfigError('Unexpected agenda entry "{}" in {}'.format(k, self.filepath))
|
||||
|
||||
def _load_raw_from_source(self, source):
|
||||
if hasattr(source, 'read') and hasattr(source, 'name'): # file-like object
|
||||
self.filepath = source.name
|
||||
raw = load_struct_from_yaml(text=source.read())
|
||||
elif isinstance(source, basestring):
|
||||
if os.path.isfile(source):
|
||||
self.filepath = source
|
||||
raw = load_struct_from_yaml(filepath=self.filepath)
|
||||
else: # assume YAML text
|
||||
raw = load_struct_from_yaml(text=source)
|
||||
else:
|
||||
raise ConfigError('Unknown agenda source: {}'.format(source))
|
||||
return raw
|
||||
|
||||
def _collect_existing_ids(self, ds, pool):
|
||||
# Collection needs to take place first so that auto IDs can be
|
||||
# correctly assigned, e.g. if someone explicitly specified an ID
|
||||
# of '1' for one of the workloads.
|
||||
for d in ds:
|
||||
if isinstance(d, dict) and 'id' in d:
|
||||
did = str(d['id'])
|
||||
if did in self._seen_ids[pool]:
|
||||
raise ConfigError('Duplicate {} ID: {}'.format(pool, did))
|
||||
self._seen_ids[pool].add(did)
|
||||
|
||||
def _assign_id_if_needed(self, d, pool):
|
||||
# Also enforces string IDs
|
||||
if d.get('id') is None:
|
||||
did = str(counter(pool))
|
||||
while did in self._seen_ids[pool]:
|
||||
did = str(counter(pool))
|
||||
d['id'] = did
|
||||
self._seen_ids[pool].add(did)
|
||||
else:
|
||||
d['id'] = str(d['id'])
|
||||
|
||||
|
||||
# Modifying the yaml parser to use an OrderedDict, rather then regular Python
|
||||
# dict for mappings. This preservers the order in which the items are
|
||||
# specified. See
|
||||
# http://stackoverflow.com/a/21048064
|
||||
|
||||
_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
|
||||
|
||||
|
||||
def dict_representer(dumper, data):
|
||||
return dumper.represent_mapping(_mapping_tag, data.iteritems())
|
||||
|
||||
|
||||
def dict_constructor(loader, node):
|
||||
return OrderedDict(loader.construct_pairs(node))
|
||||
|
||||
|
||||
yaml.add_representer(OrderedDict, dict_representer)
|
||||
yaml.add_constructor(_mapping_tag, dict_constructor)
|
195
wlauto/core/bootstrap.py
Normal file
195
wlauto/core/bootstrap.py
Normal file
@ -0,0 +1,195 @@
|
||||
# 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 shutil
|
||||
import imp
|
||||
import sys
|
||||
import re
|
||||
from collections import namedtuple, OrderedDict
|
||||
|
||||
from wlauto.exceptions import ConfigError
|
||||
from wlauto.utils.misc import merge_dicts, normalize, unique
|
||||
from wlauto.utils.types import identifier
|
||||
|
||||
|
||||
_this_dir = os.path.dirname(__file__)
|
||||
_user_home = os.path.expanduser('~')
|
||||
|
||||
# loading our external packages over those from the environment
|
||||
sys.path.insert(0, os.path.join(_this_dir, '..', 'external'))
|
||||
|
||||
|
||||
# Defines extension points for the WA framework. This table is used by the
|
||||
# ExtensionLoader (among other places) to identify extensions it should look
|
||||
# for.
|
||||
# Parameters that need to be specified in a tuple for each extension type:
|
||||
# name: The name of the extension type. This will be used to resolve get_
|
||||
# and list_methods in the extension loader.
|
||||
# class: The base class for the extension type. Extension loader will check
|
||||
# whether classes it discovers are subclassed from this.
|
||||
# default package: This is the package that will be searched for extensions
|
||||
# of that type by default (if not other packages are
|
||||
# specified when creating the extension loader). This
|
||||
# package *must* exist.
|
||||
# default path: This is the subdirectory under the environment_root which
|
||||
# will be searched for extensions of this type by default (if
|
||||
# no other paths are specified when creating the extension
|
||||
# loader). This directory will be automatically created if it
|
||||
# does not exist.
|
||||
|
||||
#pylint: disable=C0326
|
||||
_EXTENSION_TYPE_TABLE = [
|
||||
# name, class, default package, default path
|
||||
('command', 'wlauto.core.command.Command', 'wlauto.commands', 'commands'),
|
||||
('device', 'wlauto.core.device.Device', 'wlauto.devices', 'devices'),
|
||||
('instrument', 'wlauto.core.instrumentation.Instrument', 'wlauto.instrumentation', 'instruments'),
|
||||
('module', 'wlauto.core.extension.Module', 'wlauto.modules', 'modules'),
|
||||
('resource_getter', 'wlauto.core.resource.ResourceGetter', 'wlauto.resource_getters', 'resource_getters'),
|
||||
('result_processor', 'wlauto.core.result.ResultProcessor', 'wlauto.result_processors', 'result_processors'),
|
||||
('workload', 'wlauto.core.workload.Workload', 'wlauto.workloads', 'workloads'),
|
||||
]
|
||||
_Extension = namedtuple('_Extension', 'name, cls, default_package, default_path')
|
||||
_extensions = [_Extension._make(ext) for ext in _EXTENSION_TYPE_TABLE] # pylint: disable=W0212
|
||||
|
||||
|
||||
class ConfigLoader(object):
|
||||
"""
|
||||
This class is responsible for loading and validating config files.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._loaded = False
|
||||
self._config = {}
|
||||
self.config_count = 0
|
||||
self._loaded_files = []
|
||||
self.environment_root = None
|
||||
self.output_directory = 'wa_output'
|
||||
self.reboot_after_each_iteration = True
|
||||
self.dependencies_directory = None
|
||||
self.agenda = None
|
||||
self.extension_packages = []
|
||||
self.extension_paths = []
|
||||
self.extensions = []
|
||||
self.verbosity = 0
|
||||
self.debug = False
|
||||
self.package_directory = os.path.dirname(_this_dir)
|
||||
self.commands = {}
|
||||
|
||||
@property
|
||||
def meta_directory(self):
|
||||
return os.path.join(self.output_directory, '__meta')
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
return os.path.join(self.output_directory, 'run.log')
|
||||
|
||||
def update(self, source):
|
||||
if isinstance(source, dict):
|
||||
self.update_from_dict(source)
|
||||
else:
|
||||
self.config_count += 1
|
||||
self.update_from_file(source)
|
||||
|
||||
def update_from_file(self, source):
|
||||
try:
|
||||
new_config = imp.load_source('config_{}'.format(self.config_count), source)
|
||||
except SyntaxError, e:
|
||||
message = 'Sytax error in config: {}'.format(str(e))
|
||||
raise ConfigError(message)
|
||||
self._config = merge_dicts(self._config, vars(new_config),
|
||||
list_duplicates='first', match_types=False, dict_type=OrderedDict)
|
||||
self._loaded_files.append(source)
|
||||
self._loaded = True
|
||||
|
||||
def update_from_dict(self, source):
|
||||
normalized_source = dict((identifier(k), v) for k, v in source.iteritems())
|
||||
self._config = merge_dicts(self._config, normalized_source, list_duplicates='first',
|
||||
match_types=False, dict_type=OrderedDict)
|
||||
self._loaded = True
|
||||
|
||||
def get_config_paths(self):
|
||||
return [lf.rstrip('c') for lf in self._loaded_files]
|
||||
|
||||
def _check_loaded(self):
|
||||
if not self._loaded:
|
||||
raise ConfigError('Config file not loaded.')
|
||||
|
||||
def __getattr__(self, name):
|
||||
self._check_loaded()
|
||||
return self._config.get(normalize(name))
|
||||
|
||||
|
||||
def init_environment(env_root, dep_dir, extension_paths, overwrite_existing=False): # pylint: disable=R0914
|
||||
"""Initialise a fresh user environment creating the workload automation"""
|
||||
if os.path.exists(env_root):
|
||||
if not overwrite_existing:
|
||||
raise ConfigError('Environment {} already exists.'.format(env_root))
|
||||
shutil.rmtree(env_root)
|
||||
|
||||
os.makedirs(env_root)
|
||||
with open(os.path.join(_this_dir, '..', 'config_example.py')) as rf:
|
||||
text = re.sub(r'""".*?"""', '', rf.read(), 1, re.DOTALL)
|
||||
with open(os.path.join(_env_root, 'config.py'), 'w') as wf:
|
||||
wf.write(text)
|
||||
|
||||
os.makedirs(dep_dir)
|
||||
for path in extension_paths:
|
||||
os.makedirs(path)
|
||||
|
||||
# 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(env_root, uid, gid)
|
||||
# why, oh why isn't there a recusive=True option for os.chown?
|
||||
for root, dirs, files in os.walk(env_root):
|
||||
for d in dirs:
|
||||
os.chown(os.path.join(root, d), uid, gid)
|
||||
for f in files: # pylint: disable=W0621
|
||||
os.chown(os.path.join(root, f), uid, gid)
|
||||
|
||||
|
||||
_env_root = os.getenv('WA_USER_DIRECTORY', os.path.join(_user_home, '.workload_automation'))
|
||||
_dep_dir = os.path.join(_env_root, 'dependencies')
|
||||
_extension_paths = [os.path.join(_env_root, ext.default_path) for ext in _extensions]
|
||||
_extension_paths.extend(os.getenv('WA_EXTENSION_PATHS', '').split(os.pathsep))
|
||||
|
||||
if not os.path.isdir(_env_root):
|
||||
init_environment(_env_root, _dep_dir, _extension_paths)
|
||||
elif not os.path.isfile(os.path.join(_env_root, 'config.py')):
|
||||
with open(os.path.join(_this_dir, '..', 'config_example.py')) as f:
|
||||
f_text = re.sub(r'""".*?"""', '', f.read(), 1, re.DOTALL)
|
||||
with open(os.path.join(_env_root, 'config.py'), 'w') as f:
|
||||
f.write(f_text)
|
||||
|
||||
settings = ConfigLoader()
|
||||
settings.environment_root = _env_root
|
||||
settings.dependencies_directory = _dep_dir
|
||||
settings.extension_paths = _extension_paths
|
||||
settings.extensions = _extensions
|
||||
|
||||
_packages_file = os.path.join(_env_root, 'packages')
|
||||
if os.path.isfile(_packages_file):
|
||||
with open(_packages_file) as fh:
|
||||
settings.extension_packages = unique(fh.read().split())
|
||||
|
||||
_env_config = os.path.join(settings.environment_root, 'config.py')
|
||||
settings.update(_env_config)
|
||||
|
67
wlauto/core/command.py
Normal file
67
wlauto/core/command.py
Normal file
@ -0,0 +1,67 @@
|
||||
# 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.extension import Extension
|
||||
from wlauto.core.entry_point import init_argument_parser
|
||||
from wlauto.utils.doc import format_body
|
||||
|
||||
|
||||
class Command(Extension):
|
||||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
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()
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Perform command-specific initialisation (e.g. adding command-specific options to the command's
|
||||
parser).
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def execute(self, args):
|
||||
"""
|
||||
Execute this command.
|
||||
|
||||
: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()
|
756
wlauto/core/configuration.py
Normal file
756
wlauto/core/configuration.py
Normal file
@ -0,0 +1,756 @@
|
||||
# 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 json
|
||||
from copy import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
from wlauto.exceptions import ConfigError
|
||||
from wlauto.utils.misc import merge_dicts, merge_lists, load_struct_from_file
|
||||
from wlauto.utils.types import regex_type, identifier
|
||||
|
||||
|
||||
class SharedConfiguration(object):
|
||||
|
||||
def __init__(self):
|
||||
self.number_of_iterations = None
|
||||
self.workload_name = None
|
||||
self.label = None
|
||||
self.boot_parameters = OrderedDict()
|
||||
self.runtime_parameters = OrderedDict()
|
||||
self.workload_parameters = OrderedDict()
|
||||
self.instrumentation = []
|
||||
|
||||
|
||||
class ConfigurationJSONEncoder(json.JSONEncoder):
|
||||
|
||||
def default(self, obj): # pylint: disable=E0202
|
||||
if isinstance(obj, WorkloadRunSpec):
|
||||
return obj.to_dict()
|
||||
elif isinstance(obj, RunConfiguration):
|
||||
return obj.to_dict()
|
||||
elif isinstance(obj, RebootPolicy):
|
||||
return obj.policy
|
||||
elif isinstance(obj, regex_type):
|
||||
return obj.pattern
|
||||
else:
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class WorkloadRunSpec(object):
|
||||
"""
|
||||
Specifies execution of a workload, including things like the number of
|
||||
iterations, device runtime_parameters configuration, etc.
|
||||
|
||||
"""
|
||||
|
||||
# These should be handled by the framework if not explicitly specified
|
||||
# so it's a programming error if they're not
|
||||
framework_mandatory_parameters = ['id', 'number_of_iterations']
|
||||
|
||||
# These *must* be specified by the user (through one mechanism or another)
|
||||
# and it is a configuration error if they're not.
|
||||
mandatory_parameters = ['workload_name']
|
||||
|
||||
def __init__(self,
|
||||
id=None, # pylint: disable=W0622
|
||||
number_of_iterations=None,
|
||||
workload_name=None,
|
||||
boot_parameters=None,
|
||||
label=None,
|
||||
section_id=None,
|
||||
workload_parameters=None,
|
||||
runtime_parameters=None,
|
||||
instrumentation=None,
|
||||
flash=None,
|
||||
): # pylint: disable=W0622
|
||||
self.id = id
|
||||
self.number_of_iterations = number_of_iterations
|
||||
self.workload_name = workload_name
|
||||
self.label = label or self.workload_name
|
||||
self.section_id = section_id
|
||||
self.boot_parameters = boot_parameters or OrderedDict()
|
||||
self.runtime_parameters = runtime_parameters or OrderedDict()
|
||||
self.workload_parameters = workload_parameters or OrderedDict()
|
||||
self.instrumentation = instrumentation or []
|
||||
self.flash = flash or OrderedDict()
|
||||
self._workload = None
|
||||
self._section = None
|
||||
self.enabled = True
|
||||
|
||||
def set(self, param, value):
|
||||
if param in ['id', 'section_id', 'number_of_iterations', 'workload_name', 'label']:
|
||||
if value is not None:
|
||||
setattr(self, param, value)
|
||||
elif param in ['boot_parameters', 'runtime_parameters', 'workload_parameters', 'flash']:
|
||||
setattr(self, param, merge_dicts(getattr(self, param), value, list_duplicates='last',
|
||||
dict_type=OrderedDict, should_normalize=False))
|
||||
elif param in ['instrumentation']:
|
||||
setattr(self, param, merge_lists(getattr(self, param), value, duplicates='last'))
|
||||
else:
|
||||
raise ValueError('Unexpected workload spec parameter: {}'.format(param))
|
||||
|
||||
def validate(self):
|
||||
for param_name in self.framework_mandatory_parameters:
|
||||
param = getattr(self, param_name)
|
||||
if param is None:
|
||||
msg = '{} not set for workload spec.'
|
||||
raise RuntimeError(msg.format(param_name))
|
||||
for param_name in self.mandatory_parameters:
|
||||
param = getattr(self, param_name)
|
||||
if param is None:
|
||||
msg = '{} not set for workload spec for workload {}'
|
||||
raise ConfigError(msg.format(param_name, self.id))
|
||||
|
||||
def match_selectors(self, selectors):
|
||||
"""
|
||||
Returns ``True`` if this spec matches the specified selectors, and
|
||||
``False`` otherwise. ``selectors`` must be a dict-like object with
|
||||
attribute names mapping onto selector values. At the moment, only equality
|
||||
selection is supported; i.e. the value of the attribute of the spec must
|
||||
match exactly the corresponding value specified in the ``selectors`` dict.
|
||||
|
||||
"""
|
||||
if not selectors:
|
||||
return True
|
||||
for k, v in selectors.iteritems():
|
||||
if getattr(self, k, None) != v:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def workload(self):
|
||||
if self._workload is None:
|
||||
raise RuntimeError("Workload for {} has not been loaded".format(self))
|
||||
return self._workload
|
||||
|
||||
@property
|
||||
def secition(self):
|
||||
if self.section_id and self._section is None:
|
||||
raise RuntimeError("Section for {} has not been loaded".format(self))
|
||||
return self._section
|
||||
|
||||
def load(self, device, ext_loader):
|
||||
"""Loads the workload for the specified device using the specified loader.
|
||||
This must be done before attempting to execute the spec."""
|
||||
self._workload = ext_loader.get_workload(self.workload_name, device, **self.workload_parameters)
|
||||
|
||||
def to_dict(self):
|
||||
d = copy(self.__dict__)
|
||||
del d['_workload']
|
||||
del d['_section']
|
||||
return d
|
||||
|
||||
def __str__(self):
|
||||
return '{} {}'.format(self.id, self.label)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if not isinstance(other, WorkloadRunSpec):
|
||||
return cmp('WorkloadRunSpec', other.__class__.__name__)
|
||||
return cmp(self.id, other.id)
|
||||
|
||||
|
||||
class _SpecConfig(object):
|
||||
# TODO: This is a bit of HACK for alias resolution. This formats Alias
|
||||
# params as if they came from config.
|
||||
|
||||
def __init__(self, name, params=None):
|
||||
setattr(self, name, params or {})
|
||||
|
||||
|
||||
class RebootPolicy(object):
|
||||
"""
|
||||
Represents the reboot policy for the execution -- at what points the device
|
||||
should be rebooted. This, in turn, is controlled by the policy value that is
|
||||
passed in on construction and would typically be read from the user's settings.
|
||||
Valid policy values are:
|
||||
|
||||
:never: The device will never be rebooted.
|
||||
:as_needed: Only reboot the device if it becomes unresponsive, or needs to be flashed, etc.
|
||||
: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.
|
||||
|
||||
"""
|
||||
|
||||
valid_policies = ['never', 'as_needed', 'initial', 'each_spec', 'each_iteration']
|
||||
|
||||
def __init__(self, policy):
|
||||
policy = policy.strip().lower().replace(' ', '_')
|
||||
if policy not in self.valid_policies:
|
||||
message = 'Invalid reboot policy {}; must be one of {}'.format(policy, ', '.join(self.valid_policies))
|
||||
raise ConfigError(message)
|
||||
self.policy = policy
|
||||
|
||||
@property
|
||||
def can_reboot(self):
|
||||
return self.policy != 'never'
|
||||
|
||||
@property
|
||||
def perform_initial_boot(self):
|
||||
return self.policy not in ['never', 'as_needed']
|
||||
|
||||
@property
|
||||
def reboot_on_each_spec(self):
|
||||
return self.policy in ['each_spec', 'each_iteration']
|
||||
|
||||
@property
|
||||
def reboot_on_each_iteration(self):
|
||||
return self.policy == 'each_iteration'
|
||||
|
||||
def __str__(self):
|
||||
return self.policy
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __cmp__(self, other):
|
||||
if isinstance(other, RebootPolicy):
|
||||
return cmp(self.policy, other.policy)
|
||||
else:
|
||||
return cmp(self.policy, other)
|
||||
|
||||
|
||||
class RunConfigurationItem(object):
|
||||
"""
|
||||
This represents a predetermined "configuration point" (an individual setting)
|
||||
and describes how it must be handled when encountered.
|
||||
|
||||
"""
|
||||
|
||||
# Also defines the NULL value for each category
|
||||
valid_categories = {
|
||||
'scalar': None,
|
||||
'list': [],
|
||||
'dict': {},
|
||||
}
|
||||
|
||||
# A callable that takes an arbitrary number of positional arguments
|
||||
# is also valid.
|
||||
valid_methods = ['keep', 'replace', 'merge']
|
||||
|
||||
def __init__(self, name, category, method):
|
||||
if category not in self.valid_categories:
|
||||
raise ValueError('Invalid category: {}'.format(category))
|
||||
if not callable(method) and method not in self.valid_methods:
|
||||
raise ValueError('Invalid method: {}'.format(method))
|
||||
if category == 'scalar' and method == 'merge':
|
||||
raise ValueError('Method cannot be "merge" for a scalar')
|
||||
self.name = name
|
||||
self.category = category
|
||||
self.method = method
|
||||
|
||||
def combine(self, *args):
|
||||
"""
|
||||
Combine the provided values according to the method for this
|
||||
configuration item. Order matters -- values are assumed to be
|
||||
in the order they were specified by the user. The resulting value
|
||||
is also checked to patch the specified type.
|
||||
|
||||
"""
|
||||
args = [a for a in args if a is not None]
|
||||
if not args:
|
||||
return self.valid_categories[self.category]
|
||||
if self.method == 'keep' or len(args) == 1:
|
||||
value = args[0]
|
||||
elif self.method == 'replace':
|
||||
value = args[-1]
|
||||
elif self.method == 'merge':
|
||||
if self.category == 'list':
|
||||
value = merge_lists(*args, duplicates='last', dict_type=OrderedDict)
|
||||
elif self.category == 'dict':
|
||||
value = merge_dicts(*args,
|
||||
should_merge_lists=True,
|
||||
should_normalize=False,
|
||||
list_duplicates='last',
|
||||
dict_type=OrderedDict)
|
||||
else:
|
||||
raise ValueError('Unexpected category for merge : "{}"'.format(self.category))
|
||||
elif callable(self.method):
|
||||
value = self.method(*args)
|
||||
else:
|
||||
raise ValueError('Unexpected method: "{}"'.format(self.method))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def _combine_ids(*args):
|
||||
return '_'.join(args)
|
||||
|
||||
|
||||
class RunConfiguration(object):
|
||||
"""
|
||||
Loads and maintains the unified configuration for this run. This includes configuration
|
||||
for WA execution as a whole, and parameters for specific specs.
|
||||
|
||||
WA configuration mechanism aims to be flexible and easy to use, while at the same
|
||||
time providing storing validation and early failure on error. To meet these requirements,
|
||||
the implementation gets rather complicated. This is going to be a quick overview of
|
||||
the underlying mechanics.
|
||||
|
||||
.. note:: You don't need to know this to use WA, or to write extensions for it. From
|
||||
the point of view of extension writers, configuration from various sources
|
||||
"magically" appears as attributes of their classes. This explanation peels
|
||||
back the curtain and is intended for those who, for one reason or another,
|
||||
need to understand how the magic works.
|
||||
|
||||
**terminology**
|
||||
|
||||
run
|
||||
|
||||
A single execution of a WA agenda.
|
||||
|
||||
run config(uration) (object)
|
||||
|
||||
An instance of this class. There is one per run.
|
||||
|
||||
config(uration) item
|
||||
|
||||
A single configuration entry or "setting", e.g. the device interface to use. These
|
||||
can be for the run as a whole, or for a specific extension.
|
||||
|
||||
(workload) spec
|
||||
|
||||
A specification of a single workload execution. This combines workload configuration
|
||||
with things like the number of iterations to run, which instruments to enable, etc.
|
||||
More concretely, this is an instance of :class:`WorkloadRunSpec`.
|
||||
|
||||
**overview**
|
||||
|
||||
There are three types of WA configuration:
|
||||
|
||||
1. "Meta" configuration that determines how the rest of the configuration is
|
||||
processed (e.g. where extensions get loaded from). Since this does not pertain
|
||||
to *run* configuration, it will not be covered further.
|
||||
2. Global run configuration, e.g. which workloads, result processors and instruments
|
||||
will be enabled for a run.
|
||||
3. Per-workload specification configuration, that determines how a particular workload
|
||||
instance will get executed (e.g. what workload parameters will be used, how many
|
||||
iterations.
|
||||
|
||||
**run configuration**
|
||||
|
||||
Run configuration may appear in a config file (usually ``~/.workload_automation/config.py``),
|
||||
or in the ``config`` section of an agenda. Configuration is specified as a nested structure
|
||||
of dictionaries (associative arrays, or maps) and lists in the syntax following the format
|
||||
implied by the file extension (currently, YAML and Python are supported). If the same
|
||||
configuration item appears in more than one source, they are merged with conflicting entries
|
||||
taking the value from the last source that specified them.
|
||||
|
||||
In addition to a fixed set of global configuration items, configuration for any WA
|
||||
Extension (instrument, result processor, etc) may also be specified, namespaced under
|
||||
the extension's name (i.e. the extensions name is a key in the global config with value
|
||||
being a dict of parameters and their values). Some Extension parameters also specify a
|
||||
"global alias" that may appear at the top-level of the config rather than under the
|
||||
Extension's name. It is *not* an error to specify configuration for an Extension that has
|
||||
not been enabled for a particular run; such configuration will be ignored.
|
||||
|
||||
|
||||
**per-workload configuration**
|
||||
|
||||
Per-workload configuration can be specified in three places in the agenda: the
|
||||
workload entry in the ``workloads`` list, the ``global`` entry (configuration there
|
||||
will be applied to every workload entry), and in a section entry in ``sections`` list
|
||||
( configuration in every section will be applied to every workload entry separately,
|
||||
creating a "cross-product" of section and workload configurations; additionally,
|
||||
sections may specify their own workload lists).
|
||||
|
||||
If they same configuration item appears in more than one of the above places, they will
|
||||
be merged in the following order: ``global``, ``section``, ``workload``, with conflicting
|
||||
scalar values in the later overriding those from previous locations.
|
||||
|
||||
|
||||
**Global parameter aliases**
|
||||
|
||||
As mentioned above, an Extension's parameter may define a global alias, which will be
|
||||
specified and picked up from the top-level config, rather than config for that specific
|
||||
extension. It is an error to specify the value for a parameter both through a global
|
||||
alias and through extension config dict in the same configuration file. It is, however,
|
||||
possible to use a global alias in one file, and specify extension configuration for the
|
||||
same parameter in another file, in which case, the usual merging rules would apply.
|
||||
|
||||
**Loading and validation of configuration**
|
||||
|
||||
Validation of user-specified configuration happens at several stages of run initialisation,
|
||||
to ensure that appropriate context for that particular type of validation is available and
|
||||
that meaningful errors can be reported, as early as is feasible.
|
||||
|
||||
- Syntactic validation is performed when configuration is first loaded.
|
||||
This is done by the loading mechanism (e.g. YAML parser), rather than WA itself. WA
|
||||
propagates any errors encountered as ``ConfigError``\ s.
|
||||
- Once a config file is loaded into a Python structure, it scanned to
|
||||
extract settings. Static configuration is validated and added to the config. Extension
|
||||
configuration is collected into a collection of "raw" config, and merged as appropriate, but
|
||||
is not processed further at this stage.
|
||||
- Once all configuration sources have been processed, the configuration as a whole
|
||||
is validated (to make sure there are no missing settings, etc).
|
||||
- Extensions are loaded through the run config object, which instantiates
|
||||
them with appropriate parameters based on the "raw" config collected earlier. When an
|
||||
Extension is instantiated in such a way, it's config is "officially" added to run configuration
|
||||
tracked by the run config object. Raw config is discarded at the end of the run, so
|
||||
that any config that wasn't loaded in this way is not recoded (as it was not actually used).
|
||||
- Extension parameters a validated individually (for type, value ranges, etc) as they are
|
||||
loaded in the Extension's __init__.
|
||||
- An extension's ``validate()`` method is invoked before it is used (exactly when this
|
||||
happens depends on the extension's type) to perform any final validation *that does not
|
||||
rely on the target being present* (i.e. this would happen before WA connects to the target).
|
||||
This can be used perform inter-parameter validation for an extension (e.g. when valid range for
|
||||
one parameter depends on another), and more general WA state assumptions (e.g. a result
|
||||
processor can check that an instrument it depends on has been installed).
|
||||
- Finally, it is the responsibility of individual extensions to validate any assumptions
|
||||
they make about the target device (usually as part of their ``setup()``).
|
||||
|
||||
**Handling of Extension aliases.**
|
||||
|
||||
WA extensions can have zero or more aliases (not to be confused with global aliases for extension
|
||||
*parameters*). An extension allows associating an alternative name for the extension with a set
|
||||
of parameter values. In other words aliases associate common configurations for an extension with
|
||||
a name, providing a shorthand for it. For example, "t-rex_offscreen" is an alias for "glbenchmark"
|
||||
workload that specifies that "use_case" should be "t-rex" and "variant" should be "offscreen".
|
||||
|
||||
**special loading rules**
|
||||
|
||||
Note that as a consequence of being able to specify configuration for *any* Extension namespaced
|
||||
under the Extension's name in the top-level config, two distinct mechanisms exist form configuring
|
||||
devices and workloads. This is valid, however due to their nature, they are handled in a special way.
|
||||
This may be counter intuitive, so configuration of devices and workloads creating entries for their
|
||||
names in the config is discouraged in favour of using the "normal" mechanisms of configuring them
|
||||
(``device_config`` for devices and workload specs in the agenda for workloads).
|
||||
|
||||
In both cases (devices and workloads), "normal" config will always override named extension config
|
||||
*irrespective of which file it was specified in*. So a ``adb_name`` name specified in ``device_config``
|
||||
inside ``~/.workload_automation/config.py`` will override ``adb_name`` specified for ``juno`` in the
|
||||
agenda (even when device is set to "juno").
|
||||
|
||||
Again, this ignores normal loading rules, so the use of named extension configuration for devices
|
||||
and workloads is discouraged. There maybe some situations where this behaviour is useful however
|
||||
(e.g. maintaining configuration for different devices in one config file).
|
||||
|
||||
"""
|
||||
|
||||
default_reboot_policy = 'as_needed'
|
||||
default_execution_order = 'by_iteration'
|
||||
|
||||
# This is generic top-level configuration.
|
||||
general_config = [
|
||||
RunConfigurationItem('run_name', 'scalar', 'replace'),
|
||||
RunConfigurationItem('project', 'scalar', 'replace'),
|
||||
RunConfigurationItem('project_stage', 'dict', 'replace'),
|
||||
RunConfigurationItem('execution_order', 'scalar', 'replace'),
|
||||
RunConfigurationItem('reboot_policy', 'scalar', 'replace'),
|
||||
RunConfigurationItem('device', 'scalar', 'replace'),
|
||||
RunConfigurationItem('flashing_config', 'dict', 'replace'),
|
||||
]
|
||||
|
||||
# Configuration specified for each workload spec. "workload_parameters"
|
||||
# aren't listed because they are handled separately.
|
||||
workload_config = [
|
||||
RunConfigurationItem('id', 'scalar', _combine_ids),
|
||||
RunConfigurationItem('number_of_iterations', 'scalar', 'replace'),
|
||||
RunConfigurationItem('workload_name', 'scalar', 'replace'),
|
||||
RunConfigurationItem('label', 'scalar', 'replace'),
|
||||
RunConfigurationItem('section_id', 'scalar', 'replace'),
|
||||
RunConfigurationItem('boot_parameters', 'dict', 'merge'),
|
||||
RunConfigurationItem('runtime_parameters', 'dict', 'merge'),
|
||||
RunConfigurationItem('instrumentation', 'list', 'merge'),
|
||||
RunConfigurationItem('flash', 'dict', 'merge'),
|
||||
]
|
||||
|
||||
# List of names that may be present in configuration (and it is valid for
|
||||
# them to be there) but are not handled buy RunConfiguration.
|
||||
ignore_names = ['logging']
|
||||
|
||||
def get_reboot_policy(self):
|
||||
if not self._reboot_policy:
|
||||
self._reboot_policy = RebootPolicy(self.default_reboot_policy)
|
||||
return self._reboot_policy
|
||||
|
||||
def set_reboot_policy(self, value):
|
||||
if isinstance(value, RebootPolicy):
|
||||
self._reboot_policy = value
|
||||
else:
|
||||
self._reboot_policy = RebootPolicy(value)
|
||||
|
||||
reboot_policy = property(get_reboot_policy, set_reboot_policy)
|
||||
|
||||
@property
|
||||
def all_instrumentation(self):
|
||||
result = set()
|
||||
for spec in self.workload_specs:
|
||||
result = result.union(set(spec.instrumentation))
|
||||
return result
|
||||
|
||||
def __init__(self, ext_loader):
|
||||
self.ext_loader = ext_loader
|
||||
self.device = None
|
||||
self.device_config = None
|
||||
self.execution_order = None
|
||||
self.project = None
|
||||
self.project_stage = None
|
||||
self.run_name = None
|
||||
self.instrumentation = {}
|
||||
self.result_processors = {}
|
||||
self.workload_specs = []
|
||||
self.flashing_config = {}
|
||||
self.other_config = {} # keeps track of used config for extensions other than of the four main kinds.
|
||||
self._used_config_items = []
|
||||
self._global_instrumentation = []
|
||||
self._reboot_policy = None
|
||||
self._agenda = None
|
||||
self._finalized = False
|
||||
self._general_config_map = {i.name: i for i in self.general_config}
|
||||
self._workload_config_map = {i.name: i for i in self.workload_config}
|
||||
# Config files may contains static configuration for extensions that
|
||||
# would not be part of this of this run (e.g. DB connection settings
|
||||
# for a result processor that has not been enabled). Such settings
|
||||
# should not be part of configuration for this run (as they will not
|
||||
# be affecting it), but we still need to keep track it in case a later
|
||||
# config (e.g. from the agenda) enables the extension.
|
||||
# For this reason, all extension config is first loaded into the
|
||||
# following dict and when an extension is identified as need for the
|
||||
# run, its config is picked up from this "raw" dict and it becomes part
|
||||
# of the run configuration.
|
||||
self._raw_config = {'instrumentation': [], 'result_processors': []}
|
||||
|
||||
def get_extension(self, ext_name, *args):
|
||||
self._check_finalized()
|
||||
self._load_default_config_if_necessary(ext_name)
|
||||
ext_config = self._raw_config[ext_name]
|
||||
ext_cls = self.ext_loader.get_extension_class(ext_name)
|
||||
if ext_cls.kind not in ['workload', 'device', 'instrument', 'result_processor']:
|
||||
self.other_config[ext_name] = ext_config
|
||||
return self.ext_loader.get_extension(ext_name, *args, **ext_config)
|
||||
|
||||
def to_dict(self):
|
||||
d = copy(self.__dict__)
|
||||
to_remove = ['ext_loader', 'workload_specs'] + [k for k in d.keys() if k.startswith('_')]
|
||||
for attr in to_remove:
|
||||
del d[attr]
|
||||
d['workload_specs'] = [s.to_dict() for s in self.workload_specs]
|
||||
d['reboot_policy'] = self.reboot_policy # this is a property so not in __dict__
|
||||
return d
|
||||
|
||||
def load_config(self, source):
|
||||
"""Load configuration from the specified source. The source must be
|
||||
either a path to a valid config file or a dict-like object. Currently,
|
||||
config files can be either python modules (.py extension) or YAML documents
|
||||
(.yaml extension)."""
|
||||
if self._finalized:
|
||||
raise ValueError('Attempting to load a config file after run configuration has been finalized.')
|
||||
try:
|
||||
config_struct = _load_raw_struct(source)
|
||||
self._merge_config(config_struct)
|
||||
except ConfigError as e:
|
||||
message = 'Error in {}:\n\t{}'
|
||||
raise ConfigError(message.format(getattr(source, 'name', None), e.message))
|
||||
|
||||
def set_agenda(self, agenda, selectors=None):
|
||||
"""Set the agenda for this run; Unlike with config files, there can only be one agenda."""
|
||||
if self._agenda:
|
||||
# note: this also guards against loading an agenda after finalized() has been called,
|
||||
# as that would have required an agenda to be set.
|
||||
message = 'Attempting to set a second agenda {};\n\talready have agenda {} set'
|
||||
raise ValueError(message.format(agenda.filepath, self._agenda.filepath))
|
||||
try:
|
||||
self._merge_config(agenda.config or {})
|
||||
self._load_specs_from_agenda(agenda, selectors)
|
||||
self._agenda = agenda
|
||||
except ConfigError as e:
|
||||
message = 'Error in {}:\n\t{}'
|
||||
raise ConfigError(message.format(agenda.filepath, e.message))
|
||||
|
||||
def finalize(self):
|
||||
"""This must be invoked once all configuration sources have been loaded. This will
|
||||
do the final processing, setting instrumentation and result processor configuration
|
||||
for the run And making sure that all the mandatory config has been specified."""
|
||||
if self._finalized:
|
||||
return
|
||||
if not self._agenda:
|
||||
raise ValueError('Attempting to finalize run configuration before an agenda is loaded.')
|
||||
self._finalize_config_list('instrumentation')
|
||||
self._finalize_config_list('result_processors')
|
||||
if not self.device:
|
||||
raise ConfigError('Device not specified in the config.')
|
||||
self._finalize_device_config()
|
||||
if not self.reboot_policy.reboot_on_each_spec:
|
||||
for spec in self.workload_specs:
|
||||
if spec.boot_parameters:
|
||||
message = 'spec {} specifies boot_parameters; reboot policy must be at least "each_spec"'
|
||||
raise ConfigError(message.format(spec.id))
|
||||
for spec in self.workload_specs:
|
||||
for globinst in self._global_instrumentation:
|
||||
if globinst not in spec.instrumentation:
|
||||
spec.instrumentation.append(globinst)
|
||||
spec.validate()
|
||||
self._finalized = True
|
||||
|
||||
def serialize(self, wfh):
|
||||
json.dump(self, wfh, cls=ConfigurationJSONEncoder, indent=4)
|
||||
|
||||
def _merge_config(self, config):
|
||||
"""
|
||||
Merge the settings specified by the ``config`` dict-like object into current
|
||||
configuration.
|
||||
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
raise ValueError('config must be a dict; found {}'.format(config.__class__.__name__))
|
||||
|
||||
for k, v in config.iteritems():
|
||||
k = identifier(k)
|
||||
if k in self.ext_loader.global_param_aliases:
|
||||
self._resolve_global_alias(k, v)
|
||||
elif k in self._general_config_map:
|
||||
self._set_run_config_item(k, v)
|
||||
elif self.ext_loader.has_extension(k):
|
||||
self._set_extension_config(k, v)
|
||||
elif k == 'device_config':
|
||||
self._set_raw_dict(k, v)
|
||||
elif k in ['instrumentation', 'result_processors']:
|
||||
# Instrumentation can be enabled and disabled by individual
|
||||
# workloads, so we need to track it in two places: a list of
|
||||
# all instruments for the run (as they will all need to be
|
||||
# initialized and installed, and a list of only the "global"
|
||||
# instruments which can then be merged into instrumentation
|
||||
# lists of individual workload specs.
|
||||
self._set_raw_list('_global_{}'.format(k), v)
|
||||
self._set_raw_list(k, v)
|
||||
elif k in self.ignore_names:
|
||||
pass
|
||||
else:
|
||||
raise ConfigError('Unknown configuration option: {}'.format(k))
|
||||
|
||||
def _resolve_global_alias(self, name, value):
|
||||
ga = self.ext_loader.global_param_aliases[name]
|
||||
for param, ext in ga.iteritems():
|
||||
for name in [ext.name] + [a.name for a in ext.aliases]:
|
||||
self._load_default_config_if_necessary(name)
|
||||
self._raw_config[name][param.name] = value
|
||||
|
||||
def _set_run_config_item(self, name, value):
|
||||
item = self._general_config_map[name]
|
||||
combined_value = item.combine(getattr(self, name, None), value)
|
||||
setattr(self, name, combined_value)
|
||||
|
||||
def _set_extension_config(self, name, value):
|
||||
default_config = self.ext_loader.get_default_config(name)
|
||||
self._set_raw_dict(name, value, default_config)
|
||||
|
||||
def _set_raw_dict(self, name, value, default_config=None):
|
||||
existing_config = self._raw_config.get(name, default_config or {})
|
||||
new_config = _merge_config_dicts(existing_config, value)
|
||||
self._raw_config[name] = new_config
|
||||
|
||||
def _set_raw_list(self, name, value):
|
||||
old_value = self._raw_config.get(name, [])
|
||||
new_value = merge_lists(old_value, value, duplicates='last')
|
||||
self._raw_config[name] = new_value
|
||||
|
||||
def _finalize_config_list(self, attr_name):
|
||||
"""Note: the name is somewhat misleading. This finalizes a list
|
||||
form the specified configuration (e.g. "instrumentation"); internal
|
||||
representation is actually a dict, not a list..."""
|
||||
ext_config = {}
|
||||
raw_list = self._raw_config.get(attr_name, [])
|
||||
for extname in raw_list:
|
||||
default_config = self.ext_loader.get_default_config(extname)
|
||||
ext_config[extname] = self._raw_config.get(extname, default_config)
|
||||
list_name = '_global_{}'.format(attr_name)
|
||||
setattr(self, list_name, raw_list)
|
||||
setattr(self, attr_name, ext_config)
|
||||
|
||||
def _finalize_device_config(self):
|
||||
self._load_default_config_if_necessary(self.device)
|
||||
config = _merge_config_dicts(self._raw_config.get(self.device),
|
||||
self._raw_config.get('device_config', {}))
|
||||
self.device_config = config
|
||||
|
||||
def _load_default_config_if_necessary(self, name):
|
||||
if name not in self._raw_config:
|
||||
self._raw_config[name] = self.ext_loader.get_default_config(name)
|
||||
|
||||
def _load_specs_from_agenda(self, agenda, selectors):
|
||||
global_dict = agenda.global_.to_dict() if agenda.global_ else {}
|
||||
if agenda.sections:
|
||||
for section_entry in agenda.sections:
|
||||
section_dict = section_entry.to_dict()
|
||||
for workload_entry in agenda.workloads + section_entry.workloads:
|
||||
workload_dict = workload_entry.to_dict()
|
||||
self._load_workload_spec(global_dict, section_dict, workload_dict, selectors)
|
||||
else: # no sections were specified
|
||||
for workload_entry in agenda.workloads:
|
||||
workload_dict = workload_entry.to_dict()
|
||||
self._load_workload_spec(global_dict, {}, workload_dict, selectors)
|
||||
|
||||
def _load_workload_spec(self, global_dict, section_dict, workload_dict, selectors):
|
||||
spec = WorkloadRunSpec()
|
||||
for name, config in self._workload_config_map.iteritems():
|
||||
value = config.combine(global_dict.get(name), section_dict.get(name), workload_dict.get(name))
|
||||
spec.set(name, value)
|
||||
if section_dict:
|
||||
spec.set('section_id', section_dict.get('id'))
|
||||
|
||||
realname, alias_config = self.ext_loader.resolve_alias(spec.workload_name)
|
||||
if not spec.label:
|
||||
spec.label = spec.workload_name
|
||||
spec.workload_name = realname
|
||||
dicts = [self.ext_loader.get_default_config(realname),
|
||||
alias_config,
|
||||
self._raw_config.get(spec.workload_name),
|
||||
global_dict.get('workload_parameters'),
|
||||
section_dict.get('workload_parameters'),
|
||||
workload_dict.get('workload_parameters')]
|
||||
dicts = [d for d in dicts if d is not None]
|
||||
value = _merge_config_dicts(*dicts)
|
||||
spec.set('workload_parameters', value)
|
||||
|
||||
if not spec.number_of_iterations:
|
||||
spec.number_of_iterations = 1
|
||||
|
||||
if spec.match_selectors(selectors):
|
||||
instrumentation_config = self._raw_config['instrumentation']
|
||||
for instname in spec.instrumentation:
|
||||
if instname not in instrumentation_config:
|
||||
instrumentation_config.append(instname)
|
||||
self.workload_specs.append(spec)
|
||||
|
||||
def _check_finalized(self):
|
||||
if not self._finalized:
|
||||
raise ValueError('Attempting to access configuration before it has been finalized.')
|
||||
|
||||
|
||||
def _load_raw_struct(source):
|
||||
"""Load a raw dict config structure from the specified source."""
|
||||
if isinstance(source, basestring):
|
||||
if os.path.isfile(source):
|
||||
raw = load_struct_from_file(filepath=source)
|
||||
else:
|
||||
raise ConfigError('File "{}" does not exit'.format(source))
|
||||
elif isinstance(source, dict):
|
||||
raw = source
|
||||
else:
|
||||
raise ConfigError('Unknown config source: {}'.format(source))
|
||||
return raw
|
||||
|
||||
|
||||
def _merge_config_dicts(*args, **kwargs):
|
||||
"""Provides a different set of default settings for ```merge_dicts`` """
|
||||
return merge_dicts(*args,
|
||||
should_merge_lists=kwargs.get('should_merge_lists', False),
|
||||
should_normalize=kwargs.get('should_normalize', False),
|
||||
list_duplicates=kwargs.get('list_duplicates', 'last'),
|
||||
dict_type=kwargs.get('dict_type', OrderedDict))
|
418
wlauto/core/device.py
Normal file
418
wlauto/core/device.py
Normal file
@ -0,0 +1,418 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Base classes for device interfaces.
|
||||
|
||||
:Device: The base class for all devices. This defines the interface that must be
|
||||
implemented by all devices and therefore any workload and instrumentation
|
||||
can always rely on.
|
||||
:AndroidDevice: Implements most of the :class:`Device` interface, and extends it
|
||||
with a number of Android-specific methods.
|
||||
:BigLittleDevice: Subclasses :class:`AndroidDevice` to implement big.LITTLE-specific
|
||||
runtime parameters.
|
||||
:SimpleMulticoreDevice: Subclasses :class:`AndroidDevice` to implement homogeneous cores
|
||||
device runtime parameters.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import imp
|
||||
import string
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
|
||||
from wlauto.core.extension import Extension, ExtensionMeta, AttributeCollection, Parameter
|
||||
from wlauto.exceptions import DeviceError, ConfigError
|
||||
from wlauto.utils.types import list_of_strings, list_of_integers
|
||||
|
||||
|
||||
__all__ = ['RuntimeParameter', 'CoreParameter', 'Device', 'DeviceMeta']
|
||||
|
||||
|
||||
class RuntimeParameter(object):
|
||||
"""
|
||||
A runtime parameter which has its getter and setter methods associated it
|
||||
with it.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, getter, setter,
|
||||
getter_args=None, setter_args=None,
|
||||
value_name='value', override=False):
|
||||
"""
|
||||
:param name: the name of the parameter.
|
||||
:param getter: the getter method which returns the value of this parameter.
|
||||
:param setter: the setter method which sets the value of this parameter. The setter
|
||||
always expects to be passed one argument when it is called.
|
||||
:param getter_args: keyword arguments to be used when invoking the getter.
|
||||
:param setter_args: keyword arguments to be used when invoking the setter.
|
||||
:param override: A ``bool`` that specifies whether a parameter of the same name further up the
|
||||
hierarchy should be overridden. If this is ``False`` (the default), an exception
|
||||
will be raised by the ``AttributeCollection`` instead.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self.getter = getter
|
||||
self.setter = setter
|
||||
self.getter_args = getter_args or {}
|
||||
self.setter_args = setter_args or {}
|
||||
self.value_name = value_name
|
||||
self.override = override
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class CoreParameter(RuntimeParameter):
|
||||
"""A runtime parameter that will get expanded into a RuntimeParameter for each core type."""
|
||||
|
||||
def get_runtime_parameters(self, core_names):
|
||||
params = []
|
||||
for core in set(core_names):
|
||||
name = string.Template(self.name).substitute(core=core)
|
||||
getter = string.Template(self.getter).substitute(core=core)
|
||||
setter = string.Template(self.setter).substitute(core=core)
|
||||
getargs = dict(self.getter_args.items() + [('core', core)])
|
||||
setargs = dict(self.setter_args.items() + [('core', core)])
|
||||
params.append(RuntimeParameter(name, getter, setter, getargs, setargs, self.value_name, self.override))
|
||||
return params
|
||||
|
||||
|
||||
class DeviceMeta(ExtensionMeta):
|
||||
|
||||
to_propagate = ExtensionMeta.to_propagate + [
|
||||
('runtime_parameters', RuntimeParameter, AttributeCollection),
|
||||
]
|
||||
|
||||
|
||||
class Device(Extension):
|
||||
"""
|
||||
Base class for all devices supported by Workload Automation. Defines
|
||||
the interface the rest of WA uses to interact with devices.
|
||||
|
||||
:name: Unique name used to identify the device.
|
||||
:platform: The name of the device's platform (e.g. ``Android``) this may
|
||||
be used by workloads and instrumentation to assess whether they
|
||||
can run on the device.
|
||||
:working_directory: a string of the directory which is
|
||||
going to be used by the workloads on the device.
|
||||
:binaries_directory: a string of the binary directory for
|
||||
the device.
|
||||
:has_gpu: Should be ``True`` if the device as a separate GPU, and
|
||||
``False`` if graphics processing is done on a CPU.
|
||||
|
||||
.. note:: Pretty much all devices currently on the market
|
||||
have GPUs, however this may not be the case for some
|
||||
development boards.
|
||||
|
||||
:path_module: The name of one of the modules implementing the os.path
|
||||
interface, e.g. ``posixpath`` or ``ntpath``. You can provide
|
||||
your own implementation rather than relying on one of the
|
||||
standard library modules, in which case you need to specify
|
||||
the *full* path to you module. e.g. '/home/joebloggs/mypathimp.py'
|
||||
:parameters: A list of RuntimeParameter objects. The order of the objects
|
||||
is very important as the setters and getters will be called
|
||||
in the order the RuntimeParameter objects inserted.
|
||||
:active_cores: This should be a list of all the currently active cpus in
|
||||
the device in ``'/sys/devices/system/cpu/online'``. The
|
||||
returned list should be read from the device at the time
|
||||
of read request.
|
||||
|
||||
"""
|
||||
__metaclass__ = DeviceMeta
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', kind=list_of_strings, mandatory=True, default=None,
|
||||
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, mandatory=True, default=None,
|
||||
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 it's index. E.g. ``[0, 0, 1]`` indicates that cpu0 and cpu1 are on
|
||||
cluster 0, while cpu2 is on cluster 1.
|
||||
"""),
|
||||
]
|
||||
|
||||
runtime_parameters = []
|
||||
|
||||
# These must be overwritten by subclasses.
|
||||
name = None
|
||||
platform = None
|
||||
default_working_directory = None
|
||||
has_gpu = None
|
||||
path_module = None
|
||||
active_cores = None
|
||||
|
||||
def __init__(self, **kwargs): # pylint: disable=W0613
|
||||
super(Device, self).__init__(**kwargs)
|
||||
if not self.path_module:
|
||||
raise NotImplementedError('path_module must be specified by the deriving classes.')
|
||||
libpath = os.path.dirname(os.__file__)
|
||||
modpath = os.path.join(libpath, self.path_module)
|
||||
if not modpath.lower().endswith('.py'):
|
||||
modpath += '.py'
|
||||
try:
|
||||
self.path = imp.load_source('device_path', modpath)
|
||||
except IOError:
|
||||
raise DeviceError('Unsupported path module: {}'.format(self.path_module))
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Initiate rebooting of the device.
|
||||
|
||||
Added in version 2.1.3.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def boot(self, *args, **kwargs):
|
||||
"""
|
||||
Perform the seteps necessary to boot the device to the point where it is ready
|
||||
to accept other commands.
|
||||
|
||||
Changed in version 2.1.3: no longer expected to wait until boot completes.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def connect(self, *args, **kwargs):
|
||||
"""
|
||||
Establish a connection to the device that will be used for subsequent commands.
|
||||
|
||||
Added in version 2.1.3.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def disconnect(self):
|
||||
""" Close the established connection to the device. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def initialize(self, context, *args, **kwargs):
|
||||
"""
|
||||
Default implementation just calls through to init(). May be overriden by specialised
|
||||
abstract sub-cleasses to implent platform-specific intialization without requiring
|
||||
concrete implementations to explicitly invoke parent's init().
|
||||
|
||||
Added in version 2.1.3.
|
||||
|
||||
"""
|
||||
self.init(context, *args, **kwargs)
|
||||
|
||||
def init(self, context, *args, **kwargs):
|
||||
"""
|
||||
Initialize the device. This method *must* be called after a device reboot before
|
||||
any other commands can be issued, however it may also be called without rebooting.
|
||||
|
||||
It is up to device-specific implementations to identify what initialisation needs
|
||||
to be preformed on a particular invocation. Bear in mind that no assumptions can be
|
||||
made about the state of the device prior to the initiation of workload execution,
|
||||
so full initialisation must be performed at least once, even if no reboot has occurred.
|
||||
After that, the device-specific implementation may choose to skip initialization if
|
||||
the device has not been rebooted; it is up to the implementation to keep track of
|
||||
that, however.
|
||||
|
||||
All arguments are device-specific (see the documentation for the your device).
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def ping(self):
|
||||
"""
|
||||
This must return successfully if the device is able to receive commands, or must
|
||||
raise :class:`wlauto.exceptions.DeviceUnresponsiveError` if the device cannot respond.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_runtime_parameter_names(self):
|
||||
return [p.name for p in self._expand_runtime_parameters()]
|
||||
|
||||
def get_runtime_parameters(self):
|
||||
""" returns the runtime parameters that have been set. """
|
||||
# pylint: disable=cell-var-from-loop
|
||||
runtime_parameters = OrderedDict()
|
||||
for rtp in self._expand_runtime_parameters():
|
||||
if not rtp.getter:
|
||||
continue
|
||||
getter = getattr(self, rtp.getter)
|
||||
rtp_value = getter(**rtp.getter_args)
|
||||
runtime_parameters[rtp.name] = rtp_value
|
||||
return runtime_parameters
|
||||
|
||||
def set_runtime_parameters(self, params):
|
||||
"""
|
||||
The parameters are taken from the keyword arguments and are specific to
|
||||
a particular device. See the device documentation.
|
||||
|
||||
"""
|
||||
runtime_parameters = self._expand_runtime_parameters()
|
||||
rtp_map = {rtp.name.lower(): rtp for rtp in runtime_parameters}
|
||||
|
||||
params = OrderedDict((k.lower(), v) for k, v in params.iteritems())
|
||||
|
||||
expected_keys = rtp_map.keys()
|
||||
if not set(params.keys()) <= set(expected_keys):
|
||||
unknown_params = list(set(params.keys()).difference(set(expected_keys)))
|
||||
raise ConfigError('Unknown runtime parameter(s): {}'.format(unknown_params))
|
||||
|
||||
for param in params:
|
||||
rtp = rtp_map[param]
|
||||
setter = getattr(self, rtp.setter)
|
||||
args = dict(rtp.setter_args.items() + [(rtp.value_name, params[rtp.name.lower()])])
|
||||
setter(**args)
|
||||
|
||||
def capture_screen(self, filepath):
|
||||
"""Captures the current device screen into the specified file in a PNG format."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_properties(self, output_path):
|
||||
"""Captures and saves the device configuration properties version and
|
||||
any other relevant information. Return them in a dict"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def listdir(self, path, **kwargs):
|
||||
""" List the contents of the specified directory. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def push_file(self, source, dest):
|
||||
""" Push a file from the host file system onto the device. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def pull_file(self, source, dest):
|
||||
""" Pull a file from device system onto the host file system. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_file(self, filepath):
|
||||
""" Delete the specified file on the device. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def file_exists(self, filepath):
|
||||
""" Check if the specified file or directory exist on the device. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_pids_of(self, process_name):
|
||||
""" Returns a list of PIDs of the specified process name. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def kill(self, pid, as_root=False):
|
||||
""" Kill the process with the specified PID. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def killall(self, process_name, as_root=False):
|
||||
""" Kill all running processes with the specified name. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def install(self, filepath, **kwargs):
|
||||
""" Install the specified file on the device. What "install" means is device-specific
|
||||
and may possibly also depend on the type of file."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def uninstall(self, filepath):
|
||||
""" Uninstall the specified file on the device. What "uninstall" means is device-specific
|
||||
and may possibly also depend on the type of file."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute(self, command, timeout=None, **kwargs):
|
||||
"""
|
||||
Execute the specified command command on the device and return the output.
|
||||
|
||||
:param command: Command to be executed on the device.
|
||||
:param timeout: If the command does not return after the specified time,
|
||||
execute() will abort with an error. If there is no timeout for
|
||||
the command, this should be set to 0 or None.
|
||||
|
||||
Other device-specific keyword arguments may also be specified.
|
||||
|
||||
:returns: The stdout output from the command.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_sysfile_value(self, filepath, value, verify=True):
|
||||
"""
|
||||
Write the specified value to the specified file on the device
|
||||
and verify that the value has actually been written.
|
||||
|
||||
:param file: The file to be modified.
|
||||
:param value: The value to be written to the file. Must be
|
||||
an int or a string convertable to an int.
|
||||
:param verify: Specifies whether the value should be verified, once written.
|
||||
|
||||
Should raise DeviceError if could write value.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_sysfile_value(self, sysfile, kind=None):
|
||||
"""
|
||||
Get the contents of the specified sysfile.
|
||||
|
||||
:param sysfile: The file who's contents will be returned.
|
||||
|
||||
:param kind: The type of value to be expected in the sysfile. This can
|
||||
be any Python callable that takes a single str argument.
|
||||
If not specified or is None, the contents will be returned
|
||||
as a string.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
This gets invoked before an iteration is started and is endented to help the
|
||||
device manange any internal supporting functions.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
This gets invoked after iteration execution has completed and is endented to help the
|
||||
device manange any internal supporting functions.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return 'Device<{}>'.format(self.name)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def _expand_runtime_parameters(self):
|
||||
expanded_params = []
|
||||
for param in self.runtime_parameters:
|
||||
if isinstance(param, CoreParameter):
|
||||
expanded_params.extend(param.get_runtime_parameters(self.core_names)) # pylint: disable=no-member
|
||||
else:
|
||||
expanded_params.append(param)
|
||||
return expanded_params
|
||||
|
||||
@contextmanager
|
||||
def _check_alive(self):
|
||||
try:
|
||||
yield
|
||||
except Exception as e:
|
||||
self.ping()
|
||||
raise e
|
||||
|
75
wlauto/core/entry_point.py
Normal file
75
wlauto/core/entry_point.py
Normal file
@ -0,0 +1,75 @@
|
||||
# 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
|
||||
|
||||
from wlauto.core.bootstrap import settings
|
||||
from wlauto.core.extension_loader import ExtensionLoader
|
||||
from wlauto.exceptions import WAError
|
||||
from wlauto.utils.misc import get_traceback
|
||||
from wlauto.utils.log import init_logging
|
||||
from wlauto.utils.cli import init_argument_parser
|
||||
from wlauto.utils.doc import format_body
|
||||
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings(action='ignore', category=UserWarning, module='zope')
|
||||
|
||||
|
||||
logger = logging.getLogger('command_line')
|
||||
|
||||
|
||||
def load_commands(subparsers):
|
||||
ext_loader = ExtensionLoader(paths=settings.extension_paths)
|
||||
for command in ext_loader.list_commands():
|
||||
settings.commands[command.name] = ext_loader.get_command(command.name, subparsers=subparsers)
|
||||
|
||||
|
||||
def main():
|
||||
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)
|
||||
load_commands(parser.add_subparsers(dest='command')) # each command will add its own subparser
|
||||
args = parser.parse_args()
|
||||
settings.verbosity = args.verbose
|
||||
settings.debug = args.debug
|
||||
if args.config:
|
||||
settings.update(args.config)
|
||||
init_logging(settings.verbosity)
|
||||
|
||||
command = settings.commands[args.command]
|
||||
sys.exit(command.execute(args))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logging.info('Got CTRL-C. Aborting.')
|
||||
sys.exit(3)
|
||||
except WAError, e:
|
||||
logging.critical(e)
|
||||
sys.exit(1)
|
||||
except Exception, e: # pylint: disable=broad-except
|
||||
tb = get_traceback()
|
||||
logging.critical(tb)
|
||||
logging.critical('{}({})'.format(e.__class__.__name__, e))
|
||||
sys.exit(2)
|
||||
|
798
wlauto/core/execution.py
Normal file
798
wlauto/core/execution.py
Normal file
@ -0,0 +1,798 @@
|
||||
# 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 os
|
||||
import uuid
|
||||
import logging
|
||||
import subprocess
|
||||
import random
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
from contextlib import contextmanager
|
||||
from collections import Counter, defaultdict, OrderedDict
|
||||
from itertools import izip_longest
|
||||
|
||||
import wlauto.core.signal as signal
|
||||
from wlauto.core import instrumentation
|
||||
from wlauto.core.bootstrap import settings
|
||||
from wlauto.core.extension import Artifact
|
||||
from wlauto.core.configuration import RunConfiguration
|
||||
from wlauto.core.extension_loader import ExtensionLoader
|
||||
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, merge_dicts, format_duration
|
||||
|
||||
|
||||
# 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 RunInfo(object):
|
||||
"""
|
||||
Information about the current run, such as it's unique ID, run
|
||||
time, etc.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.uuid = uuid.uuid4()
|
||||
self.start_time = None
|
||||
self.end_time = None
|
||||
self.duration = None
|
||||
self.project = config.project
|
||||
self.project_stage = config.project_stage
|
||||
self.run_name = config.run_name
|
||||
self.notes = None
|
||||
self.device_properties = {}
|
||||
|
||||
def to_dict(self):
|
||||
d = copy(self.__dict__)
|
||||
d['uuid'] = str(self.uuid)
|
||||
del d['config']
|
||||
d = merge_dicts(d, self.config.to_dict())
|
||||
return d
|
||||
|
||||
|
||||
class ExecutionContext(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 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', None)
|
||||
|
||||
def __init__(self, device, config):
|
||||
self.device = device
|
||||
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 = settings.output_directory
|
||||
self.host_working_directory = settings.meta_directory
|
||||
self.iteration_artifacts = None
|
||||
self.run_artifacts = copy(self.default_run_artifacts)
|
||||
self.job_iteration_counts = defaultdict(int)
|
||||
self.aborted = False
|
||||
if settings.agenda:
|
||||
self.run_artifacts.append(Artifact('agenda',
|
||||
os.path.join(self.host_working_directory,
|
||||
os.path.basename(settings.agenda)),
|
||||
'meta',
|
||||
mandatory=True,
|
||||
description='Agenda for this run.'))
|
||||
for i in xrange(1, settings.config_count + 1):
|
||||
self.run_artifacts.append(Artifact('config_{}'.format(i),
|
||||
os.path.join(self.host_working_directory,
|
||||
'config_{}.py'.format(i)),
|
||||
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)
|
||||
|
||||
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
|
||||
self.current_job.result.iteration = self.current_iteration
|
||||
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]
|
||||
|
||||
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_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 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
|
||||
self.config = None
|
||||
self.ext_loader = None
|
||||
self.device = None
|
||||
self.context = None
|
||||
|
||||
def execute(self, agenda, selectors=None): # NOQA
|
||||
"""
|
||||
Execute the run specified by an agenda. Optionally, selectors may be used to only
|
||||
selecute a subset of the specified agenda.
|
||||
|
||||
Params::
|
||||
|
||||
:agenda: an ``Agenda`` instance to be executed.
|
||||
:selectors: A dict mapping selector name to the coresponding values.
|
||||
|
||||
**Selectors**
|
||||
|
||||
Currently, the following seectors are supported:
|
||||
|
||||
ids
|
||||
The value must be a sequence of workload specfication IDs to be executed. Note
|
||||
that if sections are specified inthe agenda, the workload specifacation ID will
|
||||
be a combination of the section and workload IDs.
|
||||
|
||||
"""
|
||||
signal.connect(self._error_signalled_callback, signal.ERROR_LOGGED)
|
||||
signal.connect(self._warning_signalled_callback, signal.WARNING_LOGGED)
|
||||
|
||||
self.logger.info('Initializing')
|
||||
self.ext_loader = ExtensionLoader(packages=settings.extension_packages,
|
||||
paths=settings.extension_paths)
|
||||
|
||||
self.logger.debug('Loading run configuration.')
|
||||
self.config = RunConfiguration(self.ext_loader)
|
||||
for filepath in settings.get_config_paths():
|
||||
self.config.load_config(filepath)
|
||||
self.config.set_agenda(agenda, selectors)
|
||||
self.config.finalize()
|
||||
config_outfile = os.path.join(settings.meta_directory, 'run_config.json')
|
||||
with open(config_outfile, 'w') as wfh:
|
||||
self.config.serialize(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 = self.ext_loader.get_device(self.config.device, **self.config.device_config)
|
||||
self.device.validate()
|
||||
|
||||
self.context = ExecutionContext(self.device, 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 = self.ext_loader.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 = self.ext_loader.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, self.ext_loader)
|
||||
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(settings.output_directory))
|
||||
|
||||
if self.error_logged:
|
||||
self.logger.warn('There were errors during execution.')
|
||||
self.logger.warn('Please see {}'.format(settings.log_file))
|
||||
elif self.warning_logged:
|
||||
self.logger.warn('There were warnings during execution.')
|
||||
self.logger.warn('Please see {}'.format(settings.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, 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 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):
|
||||
self.spec = spec
|
||||
self.iteration = None
|
||||
self.result = IterationResult(self.spec)
|
||||
|
||||
|
||||
class Runner(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 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, context, result_manager):
|
||||
self.device = device
|
||||
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.hard_reset()
|
||||
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.run_info.start_time = datetime.utcnow()
|
||||
if self.context.reboot_policy.perform_initial_boot:
|
||||
self.logger.info('\tBooting device')
|
||||
with self._signal_wrap('INITIAL_BOOT'):
|
||||
self._reboot_device()
|
||||
else:
|
||||
self.logger.info('Connecting to device')
|
||||
self.device.connect()
|
||||
self.logger.info('Initializing device')
|
||||
self.device.initialize(self.context)
|
||||
|
||||
props = self.device.get_properties(self.context)
|
||||
self.context.run_info.device_properties = props
|
||||
self.result_manager.initialize(self.context)
|
||||
self._send(signal.RUN_INIT)
|
||||
|
||||
if instrumentation.check_failures():
|
||||
raise InstrumentError('Detected failure(s) during instrumentation initialization.')
|
||||
|
||||
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.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.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.stop()
|
||||
|
||||
def _finalize_job(self):
|
||||
self.context.run_result.iteration_results.append(self.current_job.result)
|
||||
self.job_queue[0].iteration = self.context.current_iteration
|
||||
self.completed_jobs.append(self.job_queue.pop(0))
|
||||
self.context.end_job()
|
||||
|
||||
def _finalize_run(self):
|
||||
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.ping()
|
||||
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
|
652
wlauto/core/extension.py
Normal file
652
wlauto/core/extension.py
Normal file
@ -0,0 +1,652 @@
|
||||
# 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 logging
|
||||
import inspect
|
||||
from copy import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
from wlauto.core.bootstrap import settings
|
||||
from wlauto.exceptions import ValidationError, ConfigError
|
||||
from wlauto.utils.misc import isiterable, ensure_directory_exists as _d, get_article
|
||||
from wlauto.utils.types import identifier
|
||||
|
||||
|
||||
class AttributeCollection(object):
|
||||
"""
|
||||
Accumulator for extension 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)
|
||||
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):
|
||||
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))
|
||||
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 Param(object):
|
||||
"""
|
||||
This is a generic parameter for an extension. Extensions instantiate this to declare which parameters
|
||||
are supported.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, kind=None, mandatory=None, default=None, override=False,
|
||||
allowed_values=None, description=None, constraint=None, global_alias=None):
|
||||
"""
|
||||
Create a new Parameter object.
|
||||
|
||||
:param name: The name of the parameter. This will become an instance member of the
|
||||
extension object to which the parameter is applied, so it must be a valid
|
||||
python identifier. This is the only mandatory parameter.
|
||||
:param kind: The type of parameter this is. This must be a callable that takes an arbitrary
|
||||
object and converts it to the expected type, or raised ``ValueError`` if such
|
||||
conversion is not possible. Most Python standard types -- ``str``, ``int``, ``bool``, etc. --
|
||||
can be used here (though for ``bool``, ``wlauto.utils.misc.as_bool`` is preferred
|
||||
as it intuitively handles strings like ``'false'``). This defaults to ``str`` if
|
||||
not specified.
|
||||
:param mandatory: If set to ``True``, then a non-``None`` value for this parameter *must* be
|
||||
provided on extension object construction, otherwise ``ConfigError`` will be
|
||||
raised.
|
||||
:param default: The default value for this parameter. If no value is specified on extension
|
||||
construction, this value will be used instead. (Note: if this is specified and
|
||||
is not ``None``, then ``mandatory`` parameter will be ignored).
|
||||
:param override: A ``bool`` that specifies whether a parameter of the same name further up the
|
||||
hierarchy should be overridden. If this is ``False`` (the default), an exception
|
||||
will be raised by the ``AttributeCollection`` instead.
|
||||
:param allowed_values: This should be the complete list of allowed values for this parameter.
|
||||
Note: ``None`` value will always be allowed, even if it is not in this list.
|
||||
If you want to disallow ``None``, set ``mandatory`` to ``True``.
|
||||
:param constraint: If specified, this must be a callable that takes the parameter value
|
||||
as an argument and return a boolean indicating whether the constraint
|
||||
has been satisfied. Alternatively, can be a two-tuple with said callable as
|
||||
the first element and a string describing the constraint as the second.
|
||||
:param global_alias: This is an alternative alias for this parameter, unlike the name, this
|
||||
alias will not be namespaced under the owning extension's name (hence the
|
||||
global part). This is introduced primarily for backward compatibility -- so
|
||||
that old extension settings names still work. This should not be used for
|
||||
new parameters.
|
||||
|
||||
"""
|
||||
self.name = identifier(name)
|
||||
if kind is not None and not callable(kind):
|
||||
raise ValueError('Kind must be callable.')
|
||||
self.kind = kind
|
||||
self.mandatory = mandatory
|
||||
self.default = default
|
||||
self.override = override
|
||||
self.allowed_values = allowed_values
|
||||
self.description = description
|
||||
if self.kind is None and not self.override:
|
||||
self.kind = str
|
||||
if constraint is not None and not callable(constraint) and not isinstance(constraint, tuple):
|
||||
raise ValueError('Constraint must be callable or a (callable, str) tuple.')
|
||||
self.constraint = constraint
|
||||
self.global_alias = global_alias
|
||||
|
||||
def set_value(self, obj, value=None):
|
||||
if value is None:
|
||||
if self.default is not None:
|
||||
value = self.default
|
||||
elif self.mandatory:
|
||||
msg = 'No values specified for mandatory parameter {} in {}'
|
||||
raise ConfigError(msg.format(self.name, obj.name))
|
||||
else:
|
||||
try:
|
||||
value = self.kind(value)
|
||||
except (ValueError, TypeError):
|
||||
typename = self.get_type_name()
|
||||
msg = 'Bad value "{}" for {}; must be {} {}'
|
||||
article = get_article(typename)
|
||||
raise ConfigError(msg.format(value, self.name, article, typename))
|
||||
current_value = getattr(obj, self.name, None)
|
||||
if current_value is None:
|
||||
setattr(obj, self.name, value)
|
||||
elif not isiterable(current_value):
|
||||
setattr(obj, self.name, value)
|
||||
else:
|
||||
new_value = current_value + [value]
|
||||
setattr(obj, self.name, new_value)
|
||||
|
||||
def validate(self, obj):
|
||||
value = getattr(obj, self.name, None)
|
||||
if value is not None:
|
||||
if self.allowed_values:
|
||||
self._validate_allowed_values(obj, value)
|
||||
if self.constraint:
|
||||
self._validate_constraint(obj, value)
|
||||
else:
|
||||
if self.mandatory:
|
||||
msg = 'No value specified for mandatory parameter {} in {}.'
|
||||
raise ConfigError(msg.format(self.name, obj.name))
|
||||
|
||||
def get_type_name(self):
|
||||
typename = str(self.kind)
|
||||
if '\'' in typename:
|
||||
typename = typename.split('\'')[1]
|
||||
elif typename.startswith('<function'):
|
||||
typename = typename.split()[1]
|
||||
return typename
|
||||
|
||||
def _validate_allowed_values(self, obj, value):
|
||||
if 'list' in str(self.kind):
|
||||
for v in value:
|
||||
if v not in self.allowed_values:
|
||||
msg = 'Invalid value {} for {} in {}; must be in {}'
|
||||
raise ConfigError(msg.format(v, self.name, obj.name, self.allowed_values))
|
||||
else:
|
||||
if value not in self.allowed_values:
|
||||
msg = 'Invalid value {} for {} in {}; must be in {}'
|
||||
raise ConfigError(msg.format(value, self.name, obj.name, self.allowed_values))
|
||||
|
||||
def _validate_constraint(self, obj, value):
|
||||
msg_vals = {'value': value, 'param': self.name, 'extension': obj.name}
|
||||
if isinstance(self.constraint, tuple) and len(self.constraint) == 2:
|
||||
constraint, msg = self.constraint # pylint: disable=unpacking-non-sequence
|
||||
elif callable(self.constraint):
|
||||
constraint = self.constraint
|
||||
msg = '"{value}" failed constraint validation for {param} in {extension}.'
|
||||
else:
|
||||
raise ValueError('Invalid constraint for {}: must be callable or a 2-tuple'.format(self.name))
|
||||
if not constraint(value):
|
||||
raise ConfigError(value, msg.format(**msg_vals))
|
||||
|
||||
def __repr__(self):
|
||||
d = copy(self.__dict__)
|
||||
del d['description']
|
||||
return 'Param({})'.format(d)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
Parameter = Param
|
||||
|
||||
|
||||
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']
|
||||
|
||||
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 extension, 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.extension_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 ExtensionMeta(type):
|
||||
"""
|
||||
This basically adds some magic to extensions to make implementing new extensions, 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']
|
||||
|
||||
def __new__(mcs, clsname, bases, attrs):
|
||||
mcs._propagate_attributes(bases, attrs)
|
||||
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):
|
||||
"""
|
||||
For attributes specified by to_propagate, their values will be a union of
|
||||
that specified for cls and it's 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:
|
||||
propagated += attrs[prop_attr] or []
|
||||
should_propagate = True
|
||||
if should_propagate:
|
||||
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.extension_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()
|
||||
|
||||
.. note:: current implementation imposes a restriction in that
|
||||
parameters into the function *must* be passed as keyword
|
||||
arguments. There *must not* be positional arguments on
|
||||
virutal method invocation.
|
||||
|
||||
"""
|
||||
methods = {}
|
||||
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 wrapper(self, __name=vmname, **kwargs):
|
||||
for dm in methods[__name]:
|
||||
dm(self, **kwargs)
|
||||
|
||||
setattr(cls, vmname, wrapper)
|
||||
|
||||
|
||||
class Extension(object):
|
||||
"""
|
||||
Base class for all WA extensions. An extension is basically a plug-in.
|
||||
It extends the functionality of WA in some way. Extensions are discovered
|
||||
and loaded dynamically by the extension loader upon invocation of WA scripts.
|
||||
Adding an extension 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 extension subdirectories under ``~/.workload_automation/``.
|
||||
|
||||
"""
|
||||
__metaclass__ = ExtensionMeta
|
||||
|
||||
kind = None
|
||||
name = None
|
||||
parameters = [
|
||||
Parameter('modules', kind=list,
|
||||
description="""
|
||||
Lists the modules to be loaded by this extension. A module is a plug-in that
|
||||
further extends functionality of an extension.
|
||||
"""),
|
||||
]
|
||||
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.__check_from_loader()
|
||||
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 extension.
|
||||
|
||||
"""
|
||||
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 extension is capable of running.
|
||||
This is intended as an early check to ensure the extension 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 extension (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 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.extension.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
|
||||
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()
|
||||
for capability in module.capabilities:
|
||||
if capability not in self.capabilities:
|
||||
self.capabilities.append(capability)
|
||||
self._modules.append(module)
|
||||
|
||||
def has(self, capability):
|
||||
"""Check if this extension 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 __check_from_loader(self):
|
||||
"""
|
||||
There are a few things that need to happen in order to get a valide extension instance.
|
||||
Not all of them are currently done through standard Python initialisation mechanisms
|
||||
(specifically, the loading of modules and alias resolution). In order to avoid potential
|
||||
problems with not fully loaded extensions, make sure that an extension is *only* instantiated
|
||||
by the loader.
|
||||
|
||||
"""
|
||||
stack = inspect.stack()
|
||||
stack.pop(0) # current frame
|
||||
frame = stack.pop(0)
|
||||
# skip throuth the init call chain
|
||||
while stack and frame[3] == '__init__':
|
||||
frame = stack.pop(0)
|
||||
if frame[3] != '_instantiate':
|
||||
message = 'Attempting to instantiate {} directly (must be done through an ExtensionLoader)'
|
||||
raise RuntimeError(message.format(self.__class__.__name__))
|
||||
|
||||
|
||||
class Module(Extension):
|
||||
"""
|
||||
This is a "plugin" for an extension this is intended to capture functionality that may be optional
|
||||
for an extension, and so may or may not be present in a particular setup; or, conversely, functionality
|
||||
that may be reusable between multiple devices, even if they are not with the same inheritance hierarchy.
|
||||
|
||||
In other words, a Module is roughly equivalent to a kernel module and its primary purpose is to
|
||||
implement WA "drivers" for various peripherals that may or may not be present in a particular setup.
|
||||
|
||||
.. note:: A mudule is itself an Extension and can therefore have it's own modules.
|
||||
|
||||
"""
|
||||
|
||||
capabilities = []
|
||||
|
||||
@property
|
||||
def root_owner(self):
|
||||
owner = self.owner
|
||||
while isinstance(owner, Module) and owner is not self:
|
||||
owner = owner.owner
|
||||
return owner
|
||||
|
||||
def __init__(self, owner, **kwargs):
|
||||
super(Module, self).__init__(**kwargs)
|
||||
self.owner = owner
|
||||
while isinstance(owner, Module):
|
||||
if owner.name == self.name:
|
||||
raise ValueError('Circular module import for {}'.format(self.name))
|
||||
|
||||
def initialize(self):
|
||||
pass
|
||||
|
400
wlauto/core/extension_loader.py
Normal file
400
wlauto/core/extension_loader.py
Normal file
@ -0,0 +1,400 @@
|
||||
# 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 inspect
|
||||
import imp
|
||||
import string
|
||||
import logging
|
||||
from functools import partial
|
||||
from collections import OrderedDict
|
||||
|
||||
from wlauto.core.bootstrap import settings
|
||||
from wlauto.core.extension import Extension
|
||||
from wlauto.exceptions import NotFoundError, LoaderError
|
||||
from wlauto.utils.misc import walk_modules, load_class, merge_lists, merge_dicts, get_article
|
||||
from wlauto.utils.types import identifier
|
||||
|
||||
|
||||
MODNAME_TRANS = string.maketrans(':/\\.', '____')
|
||||
|
||||
|
||||
class ExtensionLoaderItem(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 GlobalParameterAlias(object):
|
||||
"""
|
||||
Represents a "global alias" for an extension parameter. A global alias
|
||||
is specified at the top-level of config rather namespaced under an extension
|
||||
name.
|
||||
|
||||
Multiple extensions may have parameters with the same global_alias if they are
|
||||
part of the same inheritance hierarchy and one parameter is an override of the
|
||||
other. This class keeps track of all such cases in its extensions dict.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.extensions = {}
|
||||
|
||||
def iteritems(self):
|
||||
for ext in self.extensions.itervalues():
|
||||
yield (self.get_param(ext), ext)
|
||||
|
||||
def get_param(self, ext):
|
||||
for param in ext.parameters:
|
||||
if param.global_alias == self.name:
|
||||
return param
|
||||
message = 'Extension {} does not have a parameter with global alias {}'
|
||||
raise ValueError(message.format(ext.name, self.name))
|
||||
|
||||
def update(self, other_ext):
|
||||
self._validate_ext(other_ext)
|
||||
self.extensions[other_ext.name] = other_ext
|
||||
|
||||
def _validate_ext(self, other_ext):
|
||||
other_param = self.get_param(other_ext)
|
||||
for param, ext in self.iteritems():
|
||||
if ((not (issubclass(ext, other_ext) or issubclass(other_ext, ext))) and
|
||||
other_param.kind != param.kind):
|
||||
message = 'Duplicate global alias {} declared in {} and {} extensions with different types'
|
||||
raise LoaderError(message.format(self.name, ext.name, other_ext.name))
|
||||
if not param.name == other_param.name:
|
||||
message = 'Two params {} in {} and {} in {} both declare global alias {}'
|
||||
raise LoaderError(message.format(param.name, ext.name,
|
||||
other_param.name, other_ext.name, self.name))
|
||||
|
||||
def __str__(self):
|
||||
text = 'GlobalAlias({} => {})'
|
||||
extlist = ', '.join(['{}.{}'.format(e.name, p.name) for p, e in self.iteritems()])
|
||||
return text.format(self.name, extlist)
|
||||
|
||||
|
||||
class ExtensionLoader(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).
|
||||
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
|
||||
# Singleton
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not cls._instance:
|
||||
cls._instance = super(ExtensionLoader, cls).__new__(cls, *args, **kwargs)
|
||||
else:
|
||||
for k, v in kwargs.iteritems():
|
||||
if not hasattr(cls._instance, k):
|
||||
raise ValueError('Invalid parameter for ExtensionLoader: {}'.format(k))
|
||||
setattr(cls._instance, k, v)
|
||||
return cls._instance
|
||||
|
||||
def set_load_defaults(self, value):
|
||||
self._load_defaults = value
|
||||
if value:
|
||||
self.packages = merge_lists(self.default_packages, self.packages, duplicates='last')
|
||||
|
||||
def get_load_defaults(self):
|
||||
return self._load_defaults
|
||||
|
||||
load_defaults = property(get_load_defaults, set_load_defaults)
|
||||
|
||||
def __init__(self, packages=None, paths=None, ignore_paths=None, keep_going=False, load_defaults=True):
|
||||
"""
|
||||
params::
|
||||
|
||||
:packages: List of packages to load extensions from.
|
||||
:paths: List of paths to be searched for Python modules containing
|
||||
WA extensions.
|
||||
:ignore_paths: List of paths to ignore when search for WA extensions (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
|
||||
extensions.
|
||||
:load_defaults: Specifies whether extension should be loaded from default locations
|
||||
(WA package, and user's WA directory) as well as the packages/paths
|
||||
specified explicitly in ``packages`` and ``paths`` parameters.
|
||||
|
||||
"""
|
||||
self._load_defaults = None
|
||||
self.logger = logging.getLogger('ExtensionLoader')
|
||||
self.keep_going = keep_going
|
||||
self.extension_kinds = {ext_tuple.name: ExtensionLoaderItem(ext_tuple)
|
||||
for ext_tuple in settings.extensions}
|
||||
self.default_packages = [ext.default_package for ext in self.extension_kinds.values()]
|
||||
|
||||
self.packages = packages or []
|
||||
self.load_defaults = load_defaults
|
||||
self.paths = paths or []
|
||||
self.ignore_paths = ignore_paths or []
|
||||
self.extensions = {}
|
||||
self.aliases = {}
|
||||
self.global_param_aliases = {}
|
||||
# create an empty dict for each extension type to store discovered
|
||||
# extensions.
|
||||
for ext in self.extension_kinds.values():
|
||||
setattr(self, '_' + ext.name, {})
|
||||
self._load_from_packages(self.packages)
|
||||
self._load_from_paths(self.paths, self.ignore_paths)
|
||||
|
||||
def update(self, packages=None, paths=None, ignore_paths=None):
|
||||
""" Load extensions from the specified paths/packages
|
||||
without clearing or reloading existing extension. """
|
||||
if packages:
|
||||
self.packages.extend(packages)
|
||||
self._load_from_packages(packages)
|
||||
if paths:
|
||||
self.paths.extend(paths)
|
||||
self.ignore_paths.extend(ignore_paths or [])
|
||||
self._load_from_paths(paths, ignore_paths or [])
|
||||
|
||||
def clear(self):
|
||||
""" Clear all discovered items. """
|
||||
self.extensions.clear()
|
||||
for ext in self.extension_kinds.values():
|
||||
self._get_store(ext).clear()
|
||||
|
||||
def reload(self):
|
||||
""" Clear all discovered items and re-run the discovery. """
|
||||
self.clear()
|
||||
self._load_from_packages(self.packages)
|
||||
self._load_from_paths(self.paths, self.ignore_paths)
|
||||
|
||||
def get_extension_class(self, name, kind=None):
|
||||
"""
|
||||
Return the class for the specified extension if found or raises ``ValueError``.
|
||||
|
||||
"""
|
||||
name, _ = self.resolve_alias(name)
|
||||
if kind is None:
|
||||
return self.extensions[name]
|
||||
ext = self.extension_kinds.get(kind)
|
||||
if ext is None:
|
||||
raise ValueError('Unknown extension type: {}'.format(kind))
|
||||
store = self._get_store(ext)
|
||||
if name not in store:
|
||||
raise NotFoundError('Extensions {} is not {} {}.'.format(name, get_article(kind), kind))
|
||||
return store[name]
|
||||
|
||||
def get_extension(self, name, *args, **kwargs):
|
||||
"""
|
||||
Return extension of the specified kind with the specified name. Any additional
|
||||
parameters will be passed to the extension's __init__.
|
||||
|
||||
"""
|
||||
name, base_kwargs = self.resolve_alias(name)
|
||||
kind = kwargs.pop('kind', None)
|
||||
kwargs = merge_dicts(base_kwargs, kwargs, list_duplicates='last', dict_type=OrderedDict)
|
||||
cls = self.get_extension_class(name, kind)
|
||||
extension = _instantiate(cls, args, kwargs)
|
||||
extension.load_modules(self)
|
||||
return extension
|
||||
|
||||
def get_default_config(self, ext_name):
|
||||
"""
|
||||
Returns the default configuration for the specified extension 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(ext_name)
|
||||
base_default_config = self.get_extension_class(real_name).get_default_config()
|
||||
return merge_dicts(base_default_config, alias_config, list_duplicates='last', dict_type=OrderedDict)
|
||||
|
||||
def list_extensions(self, kind=None):
|
||||
"""
|
||||
List discovered extension classes. Optionally, only list extensions of a
|
||||
particular type.
|
||||
|
||||
"""
|
||||
if kind is None:
|
||||
return self.extensions.values()
|
||||
if kind not in self.extension_kinds:
|
||||
raise ValueError('Unknown extension type: {}'.format(kind))
|
||||
return self._get_store(self.extension_kinds[kind]).values()
|
||||
|
||||
def has_extension(self, name, kind=None):
|
||||
"""
|
||||
Returns ``True`` if an extensions with the specified ``name`` has been
|
||||
discovered by the loader. If ``kind`` was specified, only returns ``True``
|
||||
if the extension has been found, *and* it is of the specified kind.
|
||||
|
||||
"""
|
||||
try:
|
||||
self.get_extension_class(name, kind)
|
||||
return True
|
||||
except NotFoundError:
|
||||
return False
|
||||
|
||||
def resolve_alias(self, alias_name):
|
||||
"""
|
||||
Try to resolve the specified name as an extension alias. Returns a
|
||||
two-tuple, the first value of which is actual extension name, and the
|
||||
second is a dict of parameter values for this alias. If the name passed
|
||||
is already an extension name, then the result is ``(alias_name, {})``.
|
||||
|
||||
"""
|
||||
alias_name = identifier(alias_name.lower())
|
||||
if alias_name in self.extensions:
|
||||
return (alias_name, {})
|
||||
if alias_name in self.aliases:
|
||||
alias = self.aliases[alias_name]
|
||||
return (alias.extension_name, alias.params)
|
||||
raise NotFoundError('Could not find extension or alias "{}"'.format(alias_name))
|
||||
|
||||
# Internal methods.
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
This resolves methods for specific extensions types based on corresponding
|
||||
generic extension methods. So it's possible to say things like ::
|
||||
|
||||
loader.get_device('foo')
|
||||
|
||||
instead of ::
|
||||
|
||||
loader.get_extension('foo', kind='device')
|
||||
|
||||
"""
|
||||
if name.startswith('get_'):
|
||||
name = name.replace('get_', '', 1)
|
||||
if name in self.extension_kinds:
|
||||
return partial(self.get_extension, kind=name)
|
||||
if name.startswith('list_'):
|
||||
name = name.replace('list_', '', 1).rstrip('s')
|
||||
if name in self.extension_kinds:
|
||||
return partial(self.list_extensions, kind=name)
|
||||
if name.startswith('has_'):
|
||||
name = name.replace('has_', '', 1)
|
||||
if name in self.extension_kinds:
|
||||
return partial(self.has_extension, kind=name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def _get_store(self, ext):
|
||||
name = getattr(ext, 'name', ext)
|
||||
return getattr(self, '_' + name)
|
||||
|
||||
def _load_from_packages(self, packages):
|
||||
try:
|
||||
for package in packages:
|
||||
for module in walk_modules(package):
|
||||
self._load_module(module)
|
||||
except ImportError as e:
|
||||
message = 'Problem loading extensions from extra packages: {}'
|
||||
raise LoaderError(message.format(e.message))
|
||||
|
||||
def _load_from_paths(self, paths, ignore_paths):
|
||||
self.logger.debug('Loading from paths.')
|
||||
for path in paths:
|
||||
self.logger.debug('Checking path %s', path)
|
||||
for root, _, files in os.walk(path):
|
||||
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 not os.path.splitext(fname)[1].lower() == '.py':
|
||||
continue
|
||||
filepath = os.path.join(root, fname)
|
||||
try:
|
||||
modname = os.path.splitext(filepath[1:])[0].translate(MODNAME_TRANS)
|
||||
module = imp.load_source(modname, filepath)
|
||||
self._load_module(module)
|
||||
except (SystemExit, ImportError), e:
|
||||
if self.keep_going:
|
||||
self.logger.warn('Failed to load {}'.format(filepath))
|
||||
self.logger.warn('Got: {}'.format(e))
|
||||
else:
|
||||
raise LoaderError('Failed to load {}'.format(filepath), sys.exc_info())
|
||||
|
||||
def _load_module(self, module): # NOQA pylint: disable=too-many-branches
|
||||
self.logger.debug('Checking module %s', module.__name__)
|
||||
for obj in vars(module).itervalues():
|
||||
if inspect.isclass(obj):
|
||||
if not issubclass(obj, Extension) or not hasattr(obj, 'name') or not obj.name:
|
||||
continue
|
||||
try:
|
||||
for ext in self.extension_kinds.values():
|
||||
if issubclass(obj, ext.cls):
|
||||
self._add_found_extension(obj, ext)
|
||||
break
|
||||
else: # did not find a matching Extension type
|
||||
message = 'Unknown extension type for {} (type: {})'
|
||||
raise LoaderError(message.format(obj.name, obj.__class__.__name__))
|
||||
except LoaderError as e:
|
||||
if self.keep_going:
|
||||
self.logger.warning(e)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _add_found_extension(self, obj, ext):
|
||||
"""
|
||||
:obj: Found extension class
|
||||
:ext: matching extension item.
|
||||
"""
|
||||
self.logger.debug('\tAdding %s %s', ext.name, obj.name)
|
||||
key = identifier(obj.name.lower())
|
||||
obj.kind = ext.name
|
||||
if key in self.extensions or key in self.aliases:
|
||||
raise LoaderError('{} {} already exists.'.format(ext.name, obj.name))
|
||||
# Extensions are tracked both, in a common extensions
|
||||
# dict, and in per-extension kind dict (as retrieving
|
||||
# extensions by kind is a common use case.
|
||||
self.extensions[key] = obj
|
||||
store = self._get_store(ext)
|
||||
store[key] = obj
|
||||
for alias in obj.aliases:
|
||||
if alias in self.extensions or alias in self.aliases:
|
||||
raise LoaderError('{} {} already exists.'.format(ext.name, obj.name))
|
||||
self.aliases[alias.name] = alias
|
||||
|
||||
# Update global aliases list. If a global alias is already in the list,
|
||||
# then make sure this extension is in the same parent/child hierarchy
|
||||
# as the one already found.
|
||||
for param in obj.parameters:
|
||||
if param.global_alias:
|
||||
if param.global_alias not in self.global_param_aliases:
|
||||
ga = GlobalParameterAlias(param.global_alias)
|
||||
ga.update(obj)
|
||||
self.global_param_aliases[ga.name] = ga
|
||||
else: # global alias already exists.
|
||||
self.global_param_aliases[param.global_alias].update(obj)
|
||||
|
||||
|
||||
# Utility functions.
|
||||
|
||||
def _instantiate(cls, args=None, kwargs=None):
|
||||
args = [] if args is None else args
|
||||
kwargs = {} if kwargs is None else kwargs
|
||||
try:
|
||||
return cls(*args, **kwargs)
|
||||
except Exception:
|
||||
raise LoaderError('Could not load {}'.format(cls), sys.exc_info())
|
||||
|
35
wlauto/core/exttype.py
Normal file
35
wlauto/core/exttype.py
Normal file
@ -0,0 +1,35 @@
|
||||
# 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.bootstrap import settings
|
||||
from wlauto.core.extension import Extension
|
||||
from wlauto.utils.misc import load_class
|
||||
|
||||
|
||||
_extension_bases = {ext.name: load_class(ext.cls) for ext in settings.extensions}
|
||||
|
||||
|
||||
def get_extension_type(ext):
|
||||
"""Given an instance of ``wlauto.core.Extension``, return a string representing
|
||||
the type of the extension (e.g. ``'workload'`` for a Workload subclass instance)."""
|
||||
if not isinstance(ext, Extension):
|
||||
raise ValueError('{} is not an instance of Extension'.format(ext))
|
||||
for name, cls in _extension_bases.iteritems():
|
||||
if isinstance(ext, cls):
|
||||
return name
|
||||
raise ValueError('Unknown extension type: {}'.format(ext.__class__.__name__))
|
||||
|
374
wlauto/core/instrumentation.py
Normal file
374
wlauto/core/instrumentation.py
Normal file
@ -0,0 +1,374 @@
|
||||
# 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_file(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_file(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.delete_file(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.extension import Extension
|
||||
from wlauto.exceptions import WAError, DeviceNotRespondingError, TimeoutError
|
||||
from wlauto.utils.misc import get_traceback, isiterable
|
||||
|
||||
|
||||
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 instrument in [i.name for i in installed]:
|
||||
return True
|
||||
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))
|
||||
arg_num = len(inspect.getargspec(attr).args)
|
||||
if not arg_num == 2:
|
||||
raise ValueError('{} must take exactly 2 arguments; {} given.'.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 installed_inst.name == 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(Extension):
|
||||
"""
|
||||
Base class for instrumentation implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, device, **kwargs):
|
||||
super(Instrument, self).__init__(**kwargs)
|
||||
self.device = device
|
||||
self.is_enabled = True
|
||||
self.is_broken = False
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return 'Instrument({})'.format(self.name)
|
||||
|
109
wlauto/core/resolver.py
Normal file
109
wlauto/core/resolver.py
Normal file
@ -0,0 +1,109 @@
|
||||
# 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 louie.prioritylist import PriorityList # pylint: disable=E0611,F0401
|
||||
|
||||
from wlauto.exceptions import ResourceError
|
||||
|
||||
|
||||
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 self.config.ext_loader.list_resource_getters():
|
||||
getter = self.config.get_extension(rescls.name, 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))
|
||||
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 {}'.format(getter.name))
|
||||
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))
|
182
wlauto/core/resource.py
Normal file
182
wlauto/core/resource.py
Normal file
@ -0,0 +1,182 @@
|
||||
# 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.extension import Extension
|
||||
|
||||
|
||||
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
|
||||
environment = 0
|
||||
external_package = -5
|
||||
package = -10
|
||||
remote = -20
|
||||
|
||||
|
||||
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(Extension):
|
||||
"""
|
||||
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 it's 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``.
|
||||
|
||||
"""
|
||||
|
||||
name = None
|
||||
resource_type = None
|
||||
priority = GetterPriority.environment
|
||||
|
||||
def __init__(self, resolver, **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'
|
||||
|
||||
def __getattr__(self, name):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return 'no-one'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
NO_ONE = __NullOwner()
|
321
wlauto/core/result.py
Normal file
321
wlauto/core/result.py
Normal file
@ -0,0 +1,321 @@
|
||||
# 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.extension import Extension
|
||||
from wlauto.exceptions import WAError
|
||||
from wlauto.utils.types import numeric
|
||||
from wlauto.utils.misc import enum_metaclass
|
||||
|
||||
|
||||
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(Extension):
|
||||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
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):
|
||||
self.info = run_info
|
||||
self.iteration_results = []
|
||||
self.artifacts = []
|
||||
self.events = []
|
||||
self.non_iteration_errors = False
|
||||
|
||||
|
||||
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 = [
|
||||
'NOT_STARTED',
|
||||
'RUNNING',
|
||||
|
||||
'OK',
|
||||
'NONCRITICAL',
|
||||
'PARTIAL',
|
||||
'FAILED',
|
||||
'ABORTED',
|
||||
'SKIPPED',
|
||||
]
|
||||
|
||||
def __init__(self, spec):
|
||||
self.spec = spec
|
||||
self.id = spec.id
|
||||
self.workload = spec.workload
|
||||
self.iteration = None
|
||||
self.status = self.NOT_STARTED
|
||||
self.events = []
|
||||
self.metrics = []
|
||||
self.artifacts = []
|
||||
|
||||
def add_metric(self, name, value, units=None, lower_is_better=False):
|
||||
self.metrics.append(Metric(name, value, units, lower_is_better))
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, value, units=None, lower_is_better=False):
|
||||
self.name = name
|
||||
self.value = numeric(value)
|
||||
self.units = units
|
||||
self.lower_is_better = lower_is_better
|
||||
|
||||
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__
|
||||
|
189
wlauto/core/signal.py
Normal file
189
wlauto/core/signal.py
Normal file
@ -0,0 +1,189 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
from louie import dispatcher # pylint: disable=F0401
|
||||
|
||||
|
||||
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, 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.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')
|
||||
|
||||
|
||||
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 siginals this means a single argument that
|
||||
will be an ``ExecutionContext`` instance. But please see documentaion
|
||||
for individual signals in the :ref:`signals reference <instrumentation_method_map>`.
|
||||
:signal: The signal to which the hanlder 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 sentder.
|
||||
: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 signal.invert_priority:
|
||||
dispatcher.connect(handler, signal, sender, priority=-priority) # pylint: disable=E1123
|
||||
else:
|
||||
dispatcher.connect(handler, signal, sender, priority=priority) # pylint: disable=E1123
|
||||
|
||||
|
||||
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, *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.
|
||||
|
||||
"""
|
||||
dispatcher.send(signal, sender, *args, **kwargs)
|
||||
|
26
wlauto/core/version.py
Normal file
26
wlauto/core/version.py
Normal file
@ -0,0 +1,26 @@
|
||||
# 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, 3, 0)
|
||||
|
||||
|
||||
def get_wa_version():
|
||||
version_string = '{}.{}.{}'.format(version.major, version.minor, version.revision)
|
||||
return version_string
|
94
wlauto/core/workload.py
Normal file
94
wlauto/core/workload.py
Normal file
@ -0,0 +1,94 @@
|
||||
# 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.extension import Extension
|
||||
from wlauto.exceptions import WorkloadError
|
||||
|
||||
|
||||
class Workload(Extension):
|
||||
"""
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
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.platform not in self.supported_platforms:
|
||||
raise WorkloadError('Workload {} does not support platform {}'.format(self.name, device.platform))
|
||||
self.device = device
|
||||
|
||||
def init_resources(self, context):
|
||||
"""
|
||||
May be optionally overridden by concrete instances in order to discover and initialise
|
||||
necessary resources. This method will be invoked at most once during the execution:
|
||||
before running any workloads, and before invocation of ``validate()``, but after it is
|
||||
clear that this workload will run (i.e. this method will not be invoked for workloads
|
||||
that have been discovered but have not been scheduled run in the agenda).
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup(self, context):
|
||||
"""
|
||||
Perform the setup necessary to run the workload, such as copying the necessry 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 __str__(self):
|
||||
return '<Workload {}>'.format(self.name)
|
||||
|
16
wlauto/devices/__init__.py
Normal file
16
wlauto/devices/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
16
wlauto/devices/android/__init__.py
Normal file
16
wlauto/devices/android/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
37
wlauto/devices/android/generic/__init__.py
Normal file
37
wlauto/devices/android/generic/__init__.py
Normal file
@ -0,0 +1,37 @@
|
||||
# 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 import AndroidDevice, Parameter
|
||||
|
||||
|
||||
class GenericDevice(AndroidDevice):
|
||||
name = 'generic_android'
|
||||
description = """
|
||||
Generic Android device. Use this if you do not have a device file for
|
||||
your device.
|
||||
|
||||
This implements the minimum functionality that should be supported by
|
||||
all android devices.
|
||||
|
||||
"""
|
||||
|
||||
default_working_directory = '/storage/sdcard0/working'
|
||||
has_gpu = True
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', default=[], override=True),
|
||||
Parameter('core_clusters', default=[], override=True),
|
||||
]
|
173
wlauto/devices/android/juno/__init__.py
Normal file
173
wlauto/devices/android/juno/__init__.py
Normal file
@ -0,0 +1,173 @@
|
||||
# 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 os
|
||||
import re
|
||||
import time
|
||||
|
||||
import pexpect
|
||||
|
||||
from wlauto import BigLittleDevice, Parameter
|
||||
from wlauto.exceptions import DeviceError
|
||||
from wlauto.utils.serial_port import open_serial_connection, pulse_dtr
|
||||
from wlauto.utils.android import adb_connect, adb_disconnect, adb_list_devices
|
||||
from wlauto.utils.uefi import UefiMenu
|
||||
|
||||
|
||||
AUTOSTART_MESSAGE = 'Press Enter to stop auto boot...'
|
||||
|
||||
|
||||
class Juno(BigLittleDevice):
|
||||
|
||||
name = 'juno'
|
||||
description = """
|
||||
ARM Juno next generation big.LITTLE development platform.
|
||||
"""
|
||||
|
||||
capabilities = ['reset_power']
|
||||
|
||||
has_gpu = True
|
||||
|
||||
modules = [
|
||||
'vexpress',
|
||||
]
|
||||
|
||||
parameters = [
|
||||
Parameter('retries', kind=int, default=2,
|
||||
description="""Specifies the number of times the device will attempt to recover
|
||||
(normally, with a hard reset) if it detects that something went wrong."""),
|
||||
|
||||
# VExpress flasher expects a device to have these:
|
||||
Parameter('uefi_entry', default='WA',
|
||||
description='The name of the entry to use (will be created if does not exist).'),
|
||||
Parameter('microsd_mount_point', default='/media/JUNO',
|
||||
description='Location at which the device\'s MicroSD card will be mounted.'),
|
||||
Parameter('port', default='/dev/ttyS0', description='Serial port on which the device is connected.'),
|
||||
Parameter('baudrate', kind=int, default=115200, description='Serial connection baud.'),
|
||||
Parameter('timeout', kind=int, default=300, description='Serial connection timeout.'),
|
||||
Parameter('core_names', default=['a53', 'a53', 'a53', 'a53', 'a57', 'a57'], override=True),
|
||||
Parameter('core_clusters', default=[0, 0, 0, 0, 1, 1], override=True),
|
||||
]
|
||||
|
||||
short_delay = 1
|
||||
firmware_prompt = 'Cmd>'
|
||||
# this is only used if there is no UEFI entry and one has to be created.
|
||||
kernel_arguments = 'console=ttyAMA0,115200 earlyprintk=pl011,0x7ff80000 verbose debug init=/init root=/dev/sda1 rw ip=dhcp rootwait'
|
||||
|
||||
def boot(self, **kwargs):
|
||||
self.logger.debug('Resetting the device.')
|
||||
self.reset()
|
||||
with open_serial_connection(port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=self.timeout,
|
||||
init_dtr=0) as target:
|
||||
menu = UefiMenu(target)
|
||||
self.logger.debug('Waiting for UEFI menu...')
|
||||
menu.open(timeout=120)
|
||||
try:
|
||||
menu.select(self.uefi_entry)
|
||||
except LookupError:
|
||||
self.logger.debug('{} UEFI entry not found.'.format(self.uefi_entry))
|
||||
self.logger.debug('Attempting to create one using default flasher configuration.')
|
||||
self.flasher.image_args = self.kernel_arguments
|
||||
self.flasher.create_uefi_enty(self, menu)
|
||||
menu.select(self.uefi_entry)
|
||||
self.logger.debug('Waiting for the Android prompt.')
|
||||
target.expect(self.android_prompt, timeout=self.timeout)
|
||||
|
||||
def connect(self):
|
||||
if not self._is_ready:
|
||||
if not self.adb_name: # pylint: disable=E0203
|
||||
with open_serial_connection(timeout=self.timeout,
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
init_dtr=0) as target:
|
||||
target.sendline('')
|
||||
self.logger.debug('Waiting for android prompt.')
|
||||
target.expect(self.android_prompt)
|
||||
|
||||
self.logger.debug('Waiting for IP address...')
|
||||
wait_start_time = time.time()
|
||||
while True:
|
||||
target.sendline('ip addr list eth0')
|
||||
time.sleep(1)
|
||||
try:
|
||||
target.expect('inet ([1-9]\d*.\d+.\d+.\d+)', timeout=10)
|
||||
self.adb_name = target.match.group(1) + ':5555' # pylint: disable=W0201
|
||||
break
|
||||
except pexpect.TIMEOUT:
|
||||
pass # We have our own timeout -- see below.
|
||||
if (time.time() - wait_start_time) > self.ready_timeout:
|
||||
raise DeviceError('Could not acquire IP address.')
|
||||
|
||||
if self.adb_name in adb_list_devices():
|
||||
adb_disconnect(self.adb_name)
|
||||
adb_connect(self.adb_name, timeout=self.timeout)
|
||||
super(Juno, self).connect() # wait for boot to complete etc.
|
||||
self._is_ready = True
|
||||
|
||||
def disconnect(self):
|
||||
if self._is_ready:
|
||||
super(Juno, self).disconnect()
|
||||
adb_disconnect(self.adb_name)
|
||||
self._is_ready = False
|
||||
|
||||
def reset(self):
|
||||
# Currently, reboot is not working in Android on Juno, so
|
||||
# perfrom a ahard reset instead
|
||||
self.hard_reset()
|
||||
|
||||
def get_cpuidle_states(self, cpu=0):
|
||||
return {}
|
||||
|
||||
def hard_reset(self):
|
||||
self.disconnect()
|
||||
self.adb_name = None # Force re-acquire IP address on reboot. pylint: disable=attribute-defined-outside-init
|
||||
with open_serial_connection(port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=self.timeout,
|
||||
init_dtr=0,
|
||||
get_conn=True) as (target, conn):
|
||||
pulse_dtr(conn, state=True, duration=0.1) # TRM specifies a pulse of >=100ms
|
||||
|
||||
i = target.expect([AUTOSTART_MESSAGE, self.firmware_prompt])
|
||||
if i:
|
||||
self.logger.debug('Saw firmware prompt.')
|
||||
time.sleep(self.short_delay)
|
||||
target.sendline('reboot')
|
||||
else:
|
||||
self.logger.debug('Saw auto boot message.')
|
||||
|
||||
def wait_for_microsd_mount_point(self, target, timeout=100):
|
||||
attempts = 1 + self.retries
|
||||
if os.path.exists(os.path.join(self.microsd_mount_point, 'config.txt')):
|
||||
return
|
||||
|
||||
self.logger.debug('Waiting for VExpress MicroSD to mount...')
|
||||
for i in xrange(attempts):
|
||||
if i: # Do not reboot on the first attempt.
|
||||
target.sendline('reboot')
|
||||
for _ in xrange(timeout):
|
||||
time.sleep(self.short_delay)
|
||||
if os.path.exists(os.path.join(self.microsd_mount_point, 'config.txt')):
|
||||
return
|
||||
raise DeviceError('Did not detect MicroSD mount on {}'.format(self.microsd_mount_point))
|
||||
|
||||
def get_android_id(self):
|
||||
# Android ID currenlty not set properly in Juno Android builds.
|
||||
return 'abad1deadeadbeef'
|
||||
|
48
wlauto/devices/android/nexus10/__init__.py
Normal file
48
wlauto/devices/android/nexus10/__init__.py
Normal file
@ -0,0 +1,48 @@
|
||||
# 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 time
|
||||
|
||||
from wlauto import AndroidDevice, Parameter
|
||||
|
||||
|
||||
class Nexus10Device(AndroidDevice):
|
||||
|
||||
name = 'Nexus10'
|
||||
description = """
|
||||
Nexus10 is a 10 inch tablet device, which has dual-core A15.
|
||||
|
||||
To be able to use Nexus10 in WA, the following must be true:
|
||||
|
||||
- USB Debugging Mode is enabled.
|
||||
- Generate USB debugging authorisation for the host machine
|
||||
|
||||
"""
|
||||
|
||||
default_working_directory = '/sdcard/working'
|
||||
has_gpu = True
|
||||
max_cores = 2
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', default=['A15', 'A15'], override=True),
|
||||
Parameter('core_clusters', default=[0, 0], override=True),
|
||||
]
|
||||
|
||||
def init(self, context, *args, **kwargs):
|
||||
time.sleep(self.long_delay)
|
||||
self.execute('svc power stayon true', check_exit_code=False)
|
||||
time.sleep(self.long_delay)
|
||||
self.execute('input keyevent 82')
|
40
wlauto/devices/android/nexus5/__init__.py
Normal file
40
wlauto/devices/android/nexus5/__init__.py
Normal file
@ -0,0 +1,40 @@
|
||||
# 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 import AndroidDevice, Parameter
|
||||
|
||||
|
||||
class Nexus5Device(AndroidDevice):
|
||||
|
||||
name = 'Nexus5'
|
||||
description = """
|
||||
Adapter for Nexus 5.
|
||||
|
||||
To be able to use Nexus5 in WA, the following must be true:
|
||||
|
||||
- USB Debugging Mode is enabled.
|
||||
- Generate USB debugging authorisation for the host machine
|
||||
|
||||
"""
|
||||
|
||||
default_working_directory = '/storage/sdcard0/working'
|
||||
has_gpu = True
|
||||
max_cores = 4
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', default=['krait400', 'krait400', 'krait400', 'krait400'], override=True),
|
||||
Parameter('core_clusters', default=[0, 0, 0, 0], override=True),
|
||||
]
|
76
wlauto/devices/android/note3/__init__.py
Normal file
76
wlauto/devices/android/note3/__init__.py
Normal file
@ -0,0 +1,76 @@
|
||||
# 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 time
|
||||
|
||||
from wlauto import AndroidDevice, Parameter
|
||||
from wlauto.exceptions import TimeoutError
|
||||
from wlauto.utils.android import adb_shell
|
||||
|
||||
|
||||
class Note3Device(AndroidDevice):
|
||||
|
||||
name = 'Note3'
|
||||
description = """
|
||||
Adapter for Galaxy Note 3.
|
||||
|
||||
To be able to use Note3 in WA, the following must be true:
|
||||
|
||||
- USB Debugging Mode is enabled.
|
||||
- Generate USB debugging authorisation for the host machine
|
||||
|
||||
"""
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', default=['A15', 'A15', 'A15', 'A15'], override=True),
|
||||
Parameter('core_clusters', default=[0, 0, 0, 0], override=True),
|
||||
Parameter('working_directory', default='/storage/sdcard0/wa-working', override=True),
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Note3Device, self).__init__(**kwargs)
|
||||
self._just_rebooted = False
|
||||
|
||||
def init(self, context):
|
||||
self.execute('svc power stayon true', check_exit_code=False)
|
||||
|
||||
def reset(self):
|
||||
super(Note3Device, self).reset()
|
||||
self._just_rebooted = True
|
||||
|
||||
def hard_reset(self):
|
||||
super(Note3Device, self).hard_reset()
|
||||
self._just_rebooted = True
|
||||
|
||||
def connect(self): # NOQA pylint: disable=R0912
|
||||
super(Note3Device, self).connect()
|
||||
if self._just_rebooted:
|
||||
self.logger.debug('Waiting for boot to complete...')
|
||||
# On the Note 3, adb connection gets reset some time after booting.
|
||||
# This causes errors during execution. To prevent this, open a shell
|
||||
# session and wait for it to be killed. Once its killed, give adb
|
||||
# enough time to restart, and then the device should be ready.
|
||||
try:
|
||||
adb_shell(self.adb_name, '', timeout=20) # pylint: disable=no-member
|
||||
time.sleep(5) # give adb time to re-initialize
|
||||
except TimeoutError:
|
||||
pass # timed out waiting for the session to be killed -- assume not going to be.
|
||||
|
||||
self.logger.debug('Boot completed.')
|
||||
self._just_rebooted = False
|
||||
# Swipe upwards to unlock the screen.
|
||||
time.sleep(self.long_delay)
|
||||
self.execute('input touchscreen swipe 540 1600 560 800 ')
|
38
wlauto/devices/android/odroidxu3/__init__.py
Normal file
38
wlauto/devices/android/odroidxu3/__init__.py
Normal file
@ -0,0 +1,38 @@
|
||||
# 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 import AndroidDevice, Parameter
|
||||
|
||||
|
||||
class OdroidXU3(AndroidDevice):
|
||||
|
||||
name = "odroidxu3"
|
||||
description = 'HardKernel Odroid XU3 development board.'
|
||||
|
||||
core_modules = [
|
||||
'odroidxu3-fan',
|
||||
]
|
||||
|
||||
parameters = [
|
||||
Parameter('adb_name', default='BABABEEFBABABEEF', override=True),
|
||||
Parameter('working_directory', default='/data/local/wa-working', override=True),
|
||||
Parameter('core_names', default=['a7', 'a7', 'a7', 'a7', 'a15', 'a15', 'a15', 'a15'], override=True),
|
||||
Parameter('core_clusters', default=[0, 0, 0, 0, 1, 1, 1, 1], override=True),
|
||||
Parameter('port', default='/dev/ttyUSB0', kind=str,
|
||||
description='Serial port on which the device is connected'),
|
||||
Parameter('baudrate', default=115200, kind=int, description='Serial connection baud rate'),
|
||||
]
|
||||
|
847
wlauto/devices/android/tc2/__init__.py
Normal file
847
wlauto/devices/android/tc2/__init__.py
Normal file
@ -0,0 +1,847 @@
|
||||
# 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 string
|
||||
import shutil
|
||||
import time
|
||||
from collections import Counter
|
||||
|
||||
import pexpect
|
||||
|
||||
from wlauto import BigLittleDevice, RuntimeParameter, Parameter, settings
|
||||
from wlauto.exceptions import ConfigError, DeviceError
|
||||
from wlauto.utils.android import adb_connect, adb_disconnect, adb_list_devices
|
||||
from wlauto.utils.serial_port import open_serial_connection
|
||||
from wlauto.utils.misc import merge_dicts
|
||||
from wlauto.utils.types import boolean
|
||||
|
||||
|
||||
BOOT_FIRMWARE = {
|
||||
'uefi': {
|
||||
'SCC_0x010': '0x000003E0',
|
||||
'reboot_attempts': 0,
|
||||
},
|
||||
'bootmon': {
|
||||
'SCC_0x010': '0x000003D0',
|
||||
'reboot_attempts': 2,
|
||||
},
|
||||
}
|
||||
|
||||
MODES = {
|
||||
'mp_a7_only': {
|
||||
'images_file': 'images_mp.txt',
|
||||
'dtb': 'mp_a7',
|
||||
'initrd': 'init_mp',
|
||||
'kernel': 'kern_mp',
|
||||
'SCC_0x700': '0x1032F003',
|
||||
'cpus': ['a7', 'a7', 'a7'],
|
||||
},
|
||||
'mp_a7_bootcluster': {
|
||||
'images_file': 'images_mp.txt',
|
||||
'dtb': 'mp_a7bc',
|
||||
'initrd': 'init_mp',
|
||||
'kernel': 'kern_mp',
|
||||
'SCC_0x700': '0x1032F003',
|
||||
'cpus': ['a7', 'a7', 'a7', 'a15', 'a15'],
|
||||
},
|
||||
'mp_a15_only': {
|
||||
'images_file': 'images_mp.txt',
|
||||
'dtb': 'mp_a15',
|
||||
'initrd': 'init_mp',
|
||||
'kernel': 'kern_mp',
|
||||
'SCC_0x700': '0x0032F003',
|
||||
'cpus': ['a15', 'a15'],
|
||||
},
|
||||
'mp_a15_bootcluster': {
|
||||
'images_file': 'images_mp.txt',
|
||||
'dtb': 'mp_a15bc',
|
||||
'initrd': 'init_mp',
|
||||
'kernel': 'kern_mp',
|
||||
'SCC_0x700': '0x0032F003',
|
||||
'cpus': ['a15', 'a15', 'a7', 'a7', 'a7'],
|
||||
},
|
||||
'iks_cpu': {
|
||||
'images_file': 'images_iks.txt',
|
||||
'dtb': 'iks',
|
||||
'initrd': 'init_iks',
|
||||
'kernel': 'kern_iks',
|
||||
'SCC_0x700': '0x1032F003',
|
||||
'cpus': ['a7', 'a7'],
|
||||
},
|
||||
'iks_a15': {
|
||||
'images_file': 'images_iks.txt',
|
||||
'dtb': 'iks',
|
||||
'initrd': 'init_iks',
|
||||
'kernel': 'kern_iks',
|
||||
'SCC_0x700': '0x0032F003',
|
||||
'cpus': ['a15', 'a15'],
|
||||
},
|
||||
'iks_a7': {
|
||||
'images_file': 'images_iks.txt',
|
||||
'dtb': 'iks',
|
||||
'initrd': 'init_iks',
|
||||
'kernel': 'kern_iks',
|
||||
'SCC_0x700': '0x0032F003',
|
||||
'cpus': ['a7', 'a7'],
|
||||
},
|
||||
'iks_ns_a15': {
|
||||
'images_file': 'images_iks.txt',
|
||||
'dtb': 'iks',
|
||||
'initrd': 'init_iks',
|
||||
'kernel': 'kern_iks',
|
||||
'SCC_0x700': '0x0032F003',
|
||||
'cpus': ['a7', 'a7', 'a7', 'a15', 'a15'],
|
||||
},
|
||||
'iks_ns_a7': {
|
||||
'images_file': 'images_iks.txt',
|
||||
'dtb': 'iks',
|
||||
'initrd': 'init_iks',
|
||||
'kernel': 'kern_iks',
|
||||
'SCC_0x700': '0x0032F003',
|
||||
'cpus': ['a7', 'a7', 'a7', 'a15', 'a15'],
|
||||
},
|
||||
}
|
||||
|
||||
A7_ONLY_MODES = ['mp_a7_only', 'iks_a7', 'iks_cpu']
|
||||
A15_ONLY_MODES = ['mp_a15_only', 'iks_a15']
|
||||
|
||||
DEFAULT_A7_GOVERNOR_TUNABLES = {
|
||||
'interactive': {
|
||||
'above_hispeed_delay': 80000,
|
||||
'go_hispeed_load': 85,
|
||||
'hispeed_freq': 800000,
|
||||
'min_sample_time': 80000,
|
||||
'timer_rate': 20000,
|
||||
},
|
||||
'ondemand': {
|
||||
'sampling_rate': 50000,
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_A15_GOVERNOR_TUNABLES = {
|
||||
'interactive': {
|
||||
'above_hispeed_delay': 80000,
|
||||
'go_hispeed_load': 85,
|
||||
'hispeed_freq': 1000000,
|
||||
'min_sample_time': 80000,
|
||||
'timer_rate': 20000,
|
||||
},
|
||||
'ondemand': {
|
||||
'sampling_rate': 50000,
|
||||
},
|
||||
}
|
||||
|
||||
ADB_SHELL_TIMEOUT = 30
|
||||
|
||||
|
||||
class _TC2DeviceConfig(object):
|
||||
|
||||
name = 'TC2 Configuration'
|
||||
device_name = 'TC2'
|
||||
|
||||
def __init__(self, # pylint: disable=R0914,W0613
|
||||
root_mount='/media/VEMSD',
|
||||
|
||||
disable_boot_configuration=False,
|
||||
boot_firmware=None,
|
||||
mode=None,
|
||||
|
||||
fs_medium='usb',
|
||||
|
||||
device_working_directory='/data/local/usecase',
|
||||
|
||||
bm_image='bm_v519r.axf',
|
||||
|
||||
serial_device='/dev/ttyS0',
|
||||
serial_baud=38400,
|
||||
serial_max_timeout=600,
|
||||
serial_log=sys.stdout,
|
||||
|
||||
init_timeout=120,
|
||||
|
||||
always_delete_uefi_entry=True,
|
||||
psci_enable=True,
|
||||
|
||||
host_working_directory=None,
|
||||
|
||||
a7_governor_tunables=None,
|
||||
a15_governor_tunables=None,
|
||||
|
||||
adb_name=None,
|
||||
# Compatibility with other android devices.
|
||||
enable_screen_check=None, # pylint: disable=W0613
|
||||
**kwargs
|
||||
):
|
||||
self.root_mount = root_mount
|
||||
self.disable_boot_configuration = disable_boot_configuration
|
||||
if not disable_boot_configuration:
|
||||
self.boot_firmware = boot_firmware or 'uefi'
|
||||
self.default_mode = mode or 'mp_a7_bootcluster'
|
||||
elif boot_firmware or mode:
|
||||
raise ConfigError('boot_firmware and/or mode cannot be specified when disable_boot_configuration is enabled.')
|
||||
|
||||
self.mode = self.default_mode
|
||||
self.working_directory = device_working_directory
|
||||
self.serial_device = serial_device
|
||||
self.serial_baud = serial_baud
|
||||
self.serial_max_timeout = serial_max_timeout
|
||||
self.serial_log = serial_log
|
||||
self.bootmon_prompt = re.compile('^([KLM]:\\\)?>', re.MULTILINE)
|
||||
|
||||
self.fs_medium = fs_medium.lower()
|
||||
|
||||
self.bm_image = bm_image
|
||||
|
||||
self.init_timeout = init_timeout
|
||||
|
||||
self.always_delete_uefi_entry = always_delete_uefi_entry
|
||||
self.psci_enable = psci_enable
|
||||
|
||||
self.resource_dir = os.path.join(os.path.dirname(__file__), 'resources')
|
||||
self.board_dir = os.path.join(self.root_mount, 'SITE1', 'HBI0249A')
|
||||
self.board_file = 'board.txt'
|
||||
self.board_file_bak = 'board.bak'
|
||||
self.images_file = 'images.txt'
|
||||
|
||||
self.host_working_directory = host_working_directory or settings.meta_directory
|
||||
|
||||
if not a7_governor_tunables:
|
||||
self.a7_governor_tunables = DEFAULT_A7_GOVERNOR_TUNABLES
|
||||
else:
|
||||
self.a7_governor_tunables = merge_dicts(DEFAULT_A7_GOVERNOR_TUNABLES, a7_governor_tunables)
|
||||
|
||||
if not a15_governor_tunables:
|
||||
self.a15_governor_tunables = DEFAULT_A15_GOVERNOR_TUNABLES
|
||||
else:
|
||||
self.a15_governor_tunables = merge_dicts(DEFAULT_A15_GOVERNOR_TUNABLES, a15_governor_tunables)
|
||||
|
||||
self.adb_name = adb_name
|
||||
|
||||
@property
|
||||
def src_images_template_file(self):
|
||||
return os.path.join(self.resource_dir, MODES[self.mode]['images_file'])
|
||||
|
||||
@property
|
||||
def src_images_file(self):
|
||||
return os.path.join(self.host_working_directory, 'images.txt')
|
||||
|
||||
@property
|
||||
def src_board_template_file(self):
|
||||
return os.path.join(self.resource_dir, 'board_template.txt')
|
||||
|
||||
@property
|
||||
def src_board_file(self):
|
||||
return os.path.join(self.host_working_directory, 'board.txt')
|
||||
|
||||
@property
|
||||
def kernel_arguments(self):
|
||||
kernel_args = ' console=ttyAMA0,38400 androidboot.console=ttyAMA0 selinux=0'
|
||||
if self.fs_medium == 'usb':
|
||||
kernel_args += ' androidboot.hardware=arm-versatileexpress-usb'
|
||||
if 'iks' in self.mode:
|
||||
kernel_args += ' no_bL_switcher=0'
|
||||
return kernel_args
|
||||
|
||||
@property
|
||||
def kernel(self):
|
||||
return MODES[self.mode]['kernel']
|
||||
|
||||
@property
|
||||
def initrd(self):
|
||||
return MODES[self.mode]['initrd']
|
||||
|
||||
@property
|
||||
def dtb(self):
|
||||
return MODES[self.mode]['dtb']
|
||||
|
||||
@property
|
||||
def SCC_0x700(self):
|
||||
return MODES[self.mode]['SCC_0x700']
|
||||
|
||||
@property
|
||||
def SCC_0x010(self):
|
||||
return BOOT_FIRMWARE[self.boot_firmware]['SCC_0x010']
|
||||
|
||||
@property
|
||||
def reboot_attempts(self):
|
||||
return BOOT_FIRMWARE[self.boot_firmware]['reboot_attempts']
|
||||
|
||||
def validate(self):
|
||||
valid_modes = MODES.keys()
|
||||
if self.mode not in valid_modes:
|
||||
message = 'Invalid mode: {}; must be in {}'.format(
|
||||
self.mode, valid_modes)
|
||||
raise ConfigError(message)
|
||||
|
||||
valid_boot_firmware = BOOT_FIRMWARE.keys()
|
||||
if self.boot_firmware not in valid_boot_firmware:
|
||||
message = 'Invalid boot_firmware: {}; must be in {}'.format(
|
||||
self.boot_firmware,
|
||||
valid_boot_firmware)
|
||||
raise ConfigError(message)
|
||||
|
||||
if self.fs_medium not in ['usb', 'sdcard']:
|
||||
message = 'Invalid filesystem medium: {} allowed values : usb, sdcard '.format(self.fs_medium)
|
||||
raise ConfigError(message)
|
||||
|
||||
|
||||
class TC2Device(BigLittleDevice):
|
||||
|
||||
name = 'TC2'
|
||||
description = """
|
||||
TC2 is a development board, which has three A7 cores and two A15 cores.
|
||||
|
||||
TC2 has a number of boot parameters which are:
|
||||
|
||||
:root_mount: Defaults to '/media/VEMSD'
|
||||
:boot_firmware: It has only two boot firmware options, which are
|
||||
uefi and bootmon. Defaults to 'uefi'.
|
||||
:fs_medium: Defaults to 'usb'.
|
||||
:device_working_directory: The direcitory that WA will be using to copy
|
||||
files to. Defaults to 'data/local/usecase'
|
||||
:serial_device: The serial device which TC2 is connected to. Defaults to
|
||||
'/dev/ttyS0'.
|
||||
:serial_baud: Defaults to 38400.
|
||||
:serial_max_timeout: Serial timeout value in seconds. Defaults to 600.
|
||||
:serial_log: Defaults to standard output.
|
||||
:init_timeout: The timeout in seconds to init the device. Defaults set
|
||||
to 30.
|
||||
:always_delete_uefi_entry: If true, it will delete the ufi entry.
|
||||
Defaults to True.
|
||||
:psci_enable: Enabling the psci. Defaults to True.
|
||||
:host_working_directory: The host working directory. Defaults to None.
|
||||
:disable_boot_configuration: Disables boot configuration through images.txt and board.txt. When
|
||||
this is ``True``, those two files will not be overwritten in VEMSD.
|
||||
This option may be necessary if the firmware version in the ``TC2``
|
||||
is not compatible with the templates in WA. Please note that enabling
|
||||
this will prevent you form being able to set ``boot_firmware`` and
|
||||
``mode`` parameters. Defaults to ``False``.
|
||||
|
||||
TC2 can also have a number of different booting mode, which are:
|
||||
|
||||
:mp_a7_only: Only the A7 cluster.
|
||||
:mp_a7_bootcluster: Both A7 and A15 clusters, but it boots on A7
|
||||
cluster.
|
||||
:mp_a15_only: Only the A15 cluster.
|
||||
:mp_a15_bootcluster: Both A7 and A15 clusters, but it boots on A15
|
||||
clusters.
|
||||
:iks_cpu: Only A7 cluster with only 2 cpus.
|
||||
:iks_a15: Only A15 cluster.
|
||||
:iks_a7: Same as iks_cpu
|
||||
:iks_ns_a15: Both A7 and A15 clusters.
|
||||
:iks_ns_a7: Both A7 and A15 clusters.
|
||||
|
||||
The difference between mp and iks is the scheduling policy.
|
||||
|
||||
TC2 takes the following runtime parameters
|
||||
|
||||
:a7_cores: Number of active A7 cores.
|
||||
:a15_cores: Number of active A15 cores.
|
||||
:a7_governor: CPUFreq governor for the A7 cluster.
|
||||
:a15_governor: CPUFreq governor for the A15 cluster.
|
||||
:a7_min_frequency: Minimum CPU frequency for the A7 cluster.
|
||||
:a15_min_frequency: Minimum CPU frequency for the A15 cluster.
|
||||
:a7_max_frequency: Maximum CPU frequency for the A7 cluster.
|
||||
:a15_max_frequency: Maximum CPU frequency for the A7 cluster.
|
||||
:irq_affinity: lambda x: Which cluster will receive IRQs.
|
||||
:cpuidle: Whether idle states should be enabled.
|
||||
:sysfile_values: A dict mapping a complete file path to the value that
|
||||
should be echo'd into it. By default, the file will be
|
||||
subsequently read to verify that the value was written
|
||||
into it with DeviceError raised otherwise. For write-only
|
||||
files, this check can be disabled by appending a ``!`` to
|
||||
the end of the file path.
|
||||
|
||||
"""
|
||||
|
||||
has_gpu = False
|
||||
a15_only_modes = A15_ONLY_MODES
|
||||
a7_only_modes = A7_ONLY_MODES
|
||||
not_configurable_modes = ['iks_a7', 'iks_cpu', 'iks_a15']
|
||||
|
||||
parameters = [
|
||||
Parameter('core_names', mandatory=False, override=True,
|
||||
description='This parameter will be ignored for TC2'),
|
||||
Parameter('core_clusters', mandatory=False, override=True,
|
||||
description='This parameter will be ignored for TC2'),
|
||||
]
|
||||
|
||||
runtime_parameters = [
|
||||
RuntimeParameter('irq_affinity', lambda d, x: d.set_irq_affinity(x.lower()), lambda: None),
|
||||
RuntimeParameter('cpuidle', lambda d, x: d.enable_idle_states() if boolean(x) else d.disable_idle_states(),
|
||||
lambda d: d.get_cpuidle())
|
||||
]
|
||||
|
||||
def get_mode(self):
|
||||
return self.config.mode
|
||||
|
||||
def set_mode(self, mode):
|
||||
if self._has_booted:
|
||||
raise DeviceError('Attempting to set boot mode when already booted.')
|
||||
valid_modes = MODES.keys()
|
||||
if mode is None:
|
||||
mode = self.config.default_mode
|
||||
if mode not in valid_modes:
|
||||
message = 'Invalid mode: {}; must be in {}'.format(mode, valid_modes)
|
||||
raise ConfigError(message)
|
||||
self.config.mode = mode
|
||||
|
||||
mode = property(get_mode, set_mode)
|
||||
|
||||
def _get_core_names(self):
|
||||
return MODES[self.mode]['cpus']
|
||||
|
||||
def _set_core_names(self, value):
|
||||
pass
|
||||
|
||||
core_names = property(_get_core_names, _set_core_names)
|
||||
|
||||
def _get_core_clusters(self):
|
||||
seen = set([])
|
||||
core_clusters = []
|
||||
cluster_id = -1
|
||||
for core in MODES[self.mode]['cpus']:
|
||||
if core not in seen:
|
||||
seen.add(core)
|
||||
cluster_id += 1
|
||||
core_clusters.append(cluster_id)
|
||||
return core_clusters
|
||||
|
||||
def _set_core_clusters(self, value):
|
||||
pass
|
||||
|
||||
core_clusters = property(_get_core_clusters, _set_core_clusters)
|
||||
|
||||
@property
|
||||
def cpu_cores(self):
|
||||
return MODES[self.mode]['cpus']
|
||||
|
||||
@property
|
||||
def max_a7_cores(self):
|
||||
return Counter(MODES[self.mode]['cpus'])['a7']
|
||||
|
||||
@property
|
||||
def max_a15_cores(self):
|
||||
return Counter(MODES[self.mode]['cpus'])['a15']
|
||||
|
||||
@property
|
||||
def a7_governor_tunables(self):
|
||||
return self.config.a7_governor_tunables
|
||||
|
||||
@property
|
||||
def a15_governor_tunables(self):
|
||||
return self.config.a15_governor_tunables
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(TC2Device, self).__init__()
|
||||
self.config = _TC2DeviceConfig(**kwargs)
|
||||
self.working_directory = self.config.working_directory
|
||||
self._serial = None
|
||||
self._has_booted = None
|
||||
|
||||
def boot(self, **kwargs): # NOQA
|
||||
mode = kwargs.get('os_mode', None)
|
||||
self._is_ready = False
|
||||
self._has_booted = False
|
||||
|
||||
self.mode = mode
|
||||
self.logger.debug('Booting in {} mode'.format(self.mode))
|
||||
|
||||
with open_serial_connection(timeout=self.config.serial_max_timeout,
|
||||
port=self.config.serial_device,
|
||||
baudrate=self.config.serial_baud) as target:
|
||||
if self.config.boot_firmware == 'bootmon':
|
||||
self._boot_using_bootmon(target)
|
||||
elif self.config.boot_firmware == 'uefi':
|
||||
self._boot_using_uefi(target)
|
||||
else:
|
||||
message = 'Unexpected boot firmware: {}'.format(self.config.boot_firmware)
|
||||
raise ConfigError(message)
|
||||
|
||||
try:
|
||||
target.sendline('')
|
||||
self.logger.debug('Waiting for the Android prompt.')
|
||||
target.expect(self.android_prompt, timeout=40) # pylint: disable=E1101
|
||||
except pexpect.TIMEOUT:
|
||||
# Try a second time before giving up.
|
||||
self.logger.debug('Did not get Android prompt, retrying...')
|
||||
target.sendline('')
|
||||
target.expect(self.android_prompt, timeout=10) # pylint: disable=E1101
|
||||
|
||||
self.logger.debug('Waiting for OS to initialize...')
|
||||
started_waiting_time = time.time()
|
||||
time.sleep(20) # we know it's not going to to take less time than this.
|
||||
boot_completed, got_ip_address = False, False
|
||||
while True:
|
||||
try:
|
||||
if not boot_completed:
|
||||
target.sendline('getprop sys.boot_completed')
|
||||
boot_completed = target.expect(['0.*', '1.*'], timeout=10)
|
||||
if not got_ip_address:
|
||||
target.sendline('getprop dhcp.eth0.ipaddress')
|
||||
# regexes are processed in order, so ip regex has to
|
||||
# come first (as we only want to match new line if we
|
||||
# don't match the IP). We do a "not" make the logic
|
||||
# consistent with boot_completed.
|
||||
got_ip_address = not target.expect(['[1-9]\d*.\d+.\d+.\d+', '\n'], timeout=10)
|
||||
except pexpect.TIMEOUT:
|
||||
pass # We have our own timeout -- see below.
|
||||
if boot_completed and got_ip_address:
|
||||
break
|
||||
time.sleep(5)
|
||||
if (time.time() - started_waiting_time) > self.config.init_timeout:
|
||||
raise DeviceError('Timed out waiting for the device to initialize.')
|
||||
|
||||
self._has_booted = True
|
||||
|
||||
def connect(self):
|
||||
if not self._is_ready:
|
||||
if self.config.adb_name:
|
||||
self.adb_name = self.config.adb_name # pylint: disable=attribute-defined-outside-init
|
||||
else:
|
||||
with open_serial_connection(timeout=self.config.serial_max_timeout,
|
||||
port=self.config.serial_device,
|
||||
baudrate=self.config.serial_baud) as target:
|
||||
# Get IP address and push the Gator and PMU logger.
|
||||
target.sendline('su') # as of Android v5.0.2, Linux does not boot into root shell
|
||||
target.sendline('netcfg')
|
||||
ipaddr_re = re.compile('eth0 +UP +(.+)/.+', re.MULTILINE)
|
||||
target.expect(ipaddr_re)
|
||||
output = target.after
|
||||
match = re.search('eth0 +UP +(.+)/.+', output)
|
||||
if not match:
|
||||
raise DeviceError('Could not get adb IP address.')
|
||||
ipaddr = match.group(1)
|
||||
|
||||
# Connect to device using adb.
|
||||
target.expect(self.android_prompt) # pylint: disable=E1101
|
||||
self.adb_name = ipaddr + ":5555" # pylint: disable=W0201
|
||||
|
||||
if self.adb_name in adb_list_devices():
|
||||
adb_disconnect(self.adb_name)
|
||||
adb_connect(self.adb_name)
|
||||
self._is_ready = True
|
||||
self.execute("input keyevent 82", timeout=ADB_SHELL_TIMEOUT)
|
||||
self.execute("svc power stayon true", timeout=ADB_SHELL_TIMEOUT)
|
||||
|
||||
def disconnect(self):
|
||||
adb_disconnect(self.adb_name)
|
||||
self._is_ready = False
|
||||
|
||||
# TC2-specific methods. You should avoid calling these in
|
||||
# Workloads/Instruments as that would tie them to TC2 (and if that is
|
||||
# the case, then you should set the supported_devices parameter in the
|
||||
# Workload/Instrument accordingly). Most of these can be replace with a
|
||||
# call to set_runtime_parameters.
|
||||
|
||||
def get_cpuidle(self):
|
||||
return self.get_sysfile_value('/sys/devices/system/cpu/cpu0/cpuidle/state1/disable')
|
||||
|
||||
def enable_idle_states(self):
|
||||
"""
|
||||
Fully enables idle states on TC2.
|
||||
See http://wiki.arm.com/Research/TC2SetupAndUsage ("Enabling Idle Modes" section)
|
||||
and http://wiki.arm.com/ASD/ControllingPowerManagementInLinaroKernels
|
||||
|
||||
"""
|
||||
# Enable C1 (cluster shutdown).
|
||||
self.set_sysfile_value('/sys/devices/system/cpu/cpu0/cpuidle/state1/disable', 0, verify=False)
|
||||
# Enable C0 on A15 cluster.
|
||||
self.set_sysfile_value('/sys/kernel/debug/idle_debug/enable_idle', 0, verify=False)
|
||||
# Enable C0 on A7 cluster.
|
||||
self.set_sysfile_value('/sys/kernel/debug/idle_debug/enable_idle', 1, verify=False)
|
||||
|
||||
def disable_idle_states(self):
|
||||
"""
|
||||
Disable idle states on TC2.
|
||||
See http://wiki.arm.com/Research/TC2SetupAndUsage ("Enabling Idle Modes" section)
|
||||
and http://wiki.arm.com/ASD/ControllingPowerManagementInLinaroKernels
|
||||
|
||||
"""
|
||||
# Disable C1 (cluster shutdown).
|
||||
self.set_sysfile_value('/sys/devices/system/cpu/cpu0/cpuidle/state1/disable', 1, verify=False)
|
||||
# Disable C0.
|
||||
self.set_sysfile_value('/sys/kernel/debug/idle_debug/enable_idle', 0xFF, verify=False)
|
||||
|
||||
def set_irq_affinity(self, cluster):
|
||||
"""
|
||||
Set's IRQ affinity to the specified cluster.
|
||||
|
||||
This method will only work if the device mode is mp_a7_bootcluster or
|
||||
mp_a15_bootcluster. This operation does not make sense if there is only one
|
||||
cluster active (all IRQs will obviously go to that), and it will not work for
|
||||
IKS kernel because clusters are not exposed to sysfs.
|
||||
|
||||
:param cluster: must be either 'a15' or 'a7'.
|
||||
|
||||
"""
|
||||
if self.config.mode not in ('mp_a7_bootcluster', 'mp_a15_bootcluster'):
|
||||
raise ConfigError('Cannot set IRQ affinity with mode {}'.format(self.config.mode))
|
||||
if cluster == 'a7':
|
||||
self.execute('/sbin/set_irq_affinity.sh 0xc07', check_exit_code=False)
|
||||
elif cluster == 'a15':
|
||||
self.execute('/sbin/set_irq_affinity.sh 0xc0f', check_exit_code=False)
|
||||
else:
|
||||
raise ConfigError('cluster must either "a15" or "a7"; got {}'.format(cluster))
|
||||
|
||||
def _boot_using_uefi(self, target):
|
||||
self.logger.debug('Booting using UEFI.')
|
||||
self._wait_for_vemsd_mount(target)
|
||||
self._setup_before_reboot()
|
||||
self._perform_uefi_reboot(target)
|
||||
|
||||
# Get to the UEFI menu.
|
||||
self.logger.debug('Waiting for UEFI default selection.')
|
||||
target.sendline('reboot')
|
||||
target.expect('The default boot selection will start in'.rstrip())
|
||||
time.sleep(1)
|
||||
target.sendline(''.rstrip())
|
||||
|
||||
# If delete every time is specified, try to delete entry.
|
||||
if self.config.always_delete_uefi_entry:
|
||||
self._delete_uefi_entry(target, entry='workload_automation_MP')
|
||||
self.config.always_delete_uefi_entry = False
|
||||
|
||||
# Specify argument to be passed specifying that psci is (or is not) enabled
|
||||
if self.config.psci_enable:
|
||||
psci_enable = ' psci=enable'
|
||||
else:
|
||||
psci_enable = ''
|
||||
|
||||
# Identify the workload automation entry.
|
||||
selection_pattern = r'\[([0-9]*)\] '
|
||||
|
||||
try:
|
||||
target.expect(re.compile(selection_pattern + 'workload_automation_MP'), timeout=5)
|
||||
wl_menu_item = target.match.group(1)
|
||||
except pexpect.TIMEOUT:
|
||||
self._create_uefi_entry(target, psci_enable, entry_name='workload_automation_MP')
|
||||
# At this point the board should be rebooted so we need to retry to boot
|
||||
self._boot_using_uefi(target)
|
||||
else: # Did not time out.
|
||||
try:
|
||||
#Identify the boot manager menu item
|
||||
target.expect(re.compile(selection_pattern + 'Boot Manager'))
|
||||
boot_manager_menu_item = target.match.group(1)
|
||||
|
||||
#Update FDT
|
||||
target.sendline(boot_manager_menu_item)
|
||||
target.expect(re.compile(selection_pattern + 'Update FDT path'), timeout=15)
|
||||
update_fdt_menu_item = target.match.group(1)
|
||||
target.sendline(update_fdt_menu_item)
|
||||
target.expect(re.compile(selection_pattern + 'NOR Flash .*'), timeout=15)
|
||||
bootmonfs_menu_item = target.match.group(1)
|
||||
target.sendline(bootmonfs_menu_item)
|
||||
target.expect('File path of the FDT blob:')
|
||||
target.sendline(self.config.dtb)
|
||||
|
||||
#Return to main manu and boot from wl automation
|
||||
target.expect(re.compile(selection_pattern + 'Return to main menu'), timeout=15)
|
||||
return_to_main_menu_item = target.match.group(1)
|
||||
target.sendline(return_to_main_menu_item)
|
||||
target.sendline(wl_menu_item)
|
||||
except pexpect.TIMEOUT:
|
||||
raise DeviceError('Timed out')
|
||||
|
||||
def _setup_before_reboot(self):
|
||||
if not self.config.disable_boot_configuration:
|
||||
self.logger.debug('Performing pre-boot setup.')
|
||||
substitution = {
|
||||
'SCC_0x010': self.config.SCC_0x010,
|
||||
'SCC_0x700': self.config.SCC_0x700,
|
||||
}
|
||||
with open(self.config.src_board_template_file, 'r') as fh:
|
||||
template_board_txt = string.Template(fh.read())
|
||||
with open(self.config.src_board_file, 'w') as wfh:
|
||||
wfh.write(template_board_txt.substitute(substitution))
|
||||
|
||||
with open(self.config.src_images_template_file, 'r') as fh:
|
||||
template_images_txt = string.Template(fh.read())
|
||||
with open(self.config.src_images_file, 'w') as wfh:
|
||||
wfh.write(template_images_txt.substitute({'bm_image': self.config.bm_image}))
|
||||
|
||||
shutil.copyfile(self.config.src_board_file,
|
||||
os.path.join(self.config.board_dir, self.config.board_file))
|
||||
shutil.copyfile(self.config.src_images_file,
|
||||
os.path.join(self.config.board_dir, self.config.images_file))
|
||||
os.system('sync') # make sure everything is flushed to microSD
|
||||
else:
|
||||
self.logger.debug('Boot configuration disabled proceeding with existing board.txt and images.txt.')
|
||||
|
||||
def _delete_uefi_entry(self, target, entry): # pylint: disable=R0201
|
||||
"""
|
||||
this method deletes the entry specified as parameter
|
||||
as a precondition serial port input needs to be parsed AT MOST up to
|
||||
the point BEFORE recognizing this entry (both entry and boot manager has
|
||||
not yet been parsed)
|
||||
|
||||
"""
|
||||
try:
|
||||
selection_pattern = r'\[([0-9]+)\] *'
|
||||
|
||||
try:
|
||||
target.expect(re.compile(selection_pattern + entry), timeout=5)
|
||||
wl_menu_item = target.match.group(1)
|
||||
except pexpect.TIMEOUT:
|
||||
return # Entry does not exist, nothing to delete here...
|
||||
|
||||
# Identify and select boot manager menu item
|
||||
target.expect(selection_pattern + 'Boot Manager', timeout=15)
|
||||
bootmanager_item = target.match.group(1)
|
||||
target.sendline(bootmanager_item)
|
||||
|
||||
# Identify and select 'Remove entry'
|
||||
target.expect(selection_pattern + 'Remove Boot Device Entry', timeout=15)
|
||||
new_entry_item = target.match.group(1)
|
||||
target.sendline(new_entry_item)
|
||||
|
||||
# Delete entry
|
||||
target.expect(re.compile(selection_pattern + entry), timeout=5)
|
||||
wl_menu_item = target.match.group(1)
|
||||
target.sendline(wl_menu_item)
|
||||
|
||||
# Return to main manu
|
||||
target.expect(re.compile(selection_pattern + 'Return to main menu'), timeout=15)
|
||||
return_to_main_menu_item = target.match.group(1)
|
||||
target.sendline(return_to_main_menu_item)
|
||||
except pexpect.TIMEOUT:
|
||||
raise DeviceError('Timed out while deleting UEFI entry.')
|
||||
|
||||
def _create_uefi_entry(self, target, psci_enable, entry_name):
|
||||
"""
|
||||
Creates the default boot entry that is expected when booting in uefi mode.
|
||||
|
||||
"""
|
||||
self._wait_for_vemsd_mount(target)
|
||||
try:
|
||||
selection_pattern = '\[([0-9]+)\] *'
|
||||
|
||||
# Identify and select boot manager menu item.
|
||||
target.expect(selection_pattern + 'Boot Manager', timeout=15)
|
||||
bootmanager_item = target.match.group(1)
|
||||
target.sendline(bootmanager_item)
|
||||
|
||||
# Identify and select 'add new entry'.
|
||||
target.expect(selection_pattern + 'Add Boot Device Entry', timeout=15)
|
||||
new_entry_item = target.match.group(1)
|
||||
target.sendline(new_entry_item)
|
||||
|
||||
# Identify and select BootMonFs.
|
||||
target.expect(selection_pattern + 'NOR Flash .*', timeout=15)
|
||||
BootMonFs_item = target.match.group(1)
|
||||
target.sendline(BootMonFs_item)
|
||||
|
||||
# Specify the parameters of the new entry.
|
||||
target.expect('.+the kernel', timeout=5)
|
||||
target.sendline(self.config.kernel) # kernel path
|
||||
target.expect('Has FDT support\?.*\[y\/n\].*', timeout=5)
|
||||
time.sleep(0.5)
|
||||
target.sendline('y') # Has Fdt support? -> y
|
||||
target.expect('Add an initrd.*\[y\/n\].*', timeout=5)
|
||||
time.sleep(0.5)
|
||||
target.sendline('y') # add an initrd? -> y
|
||||
target.expect('.+the initrd.*', timeout=5)
|
||||
time.sleep(0.5)
|
||||
target.sendline(self.config.initrd) # initrd path
|
||||
target.expect('.+to the binary.*', timeout=5)
|
||||
time.sleep(0.5)
|
||||
_slow_sendline(target, self.config.kernel_arguments + psci_enable) # arguments to pass to binary
|
||||
time.sleep(0.5)
|
||||
target.expect('.+new Entry.+', timeout=5)
|
||||
_slow_sendline(target, entry_name) # Entry name
|
||||
target.expect('Choice.+', timeout=15)
|
||||
time.sleep(2)
|
||||
except pexpect.TIMEOUT:
|
||||
raise DeviceError('Timed out while creating UEFI entry.')
|
||||
self._perform_uefi_reboot(target)
|
||||
|
||||
def _perform_uefi_reboot(self, target):
|
||||
self._wait_for_vemsd_mount(target)
|
||||
open(os.path.join(self.config.root_mount, 'reboot.txt'), 'a').close()
|
||||
|
||||
def _wait_for_vemsd_mount(self, target, timeout=100):
|
||||
attempts = 1 + self.config.reboot_attempts
|
||||
if os.path.exists(os.path.join(self.config.root_mount, 'config.txt')):
|
||||
return
|
||||
|
||||
self.logger.debug('Waiting for VEMSD to mount...')
|
||||
for i in xrange(attempts):
|
||||
if i: # Do not reboot on the first attempt.
|
||||
target.sendline('reboot')
|
||||
target.sendline('usb_on')
|
||||
for _ in xrange(timeout):
|
||||
time.sleep(1)
|
||||
if os.path.exists(os.path.join(self.config.root_mount, 'config.txt')):
|
||||
return
|
||||
|
||||
raise DeviceError('Timed out waiting for VEMSD to mount.')
|
||||
|
||||
def _boot_using_bootmon(self, target):
|
||||
"""
|
||||
This method Boots TC2 using the bootmon interface.
|
||||
"""
|
||||
self.logger.debug('Booting using bootmon.')
|
||||
|
||||
try:
|
||||
self._wait_for_vemsd_mount(target, timeout=20)
|
||||
except DeviceError:
|
||||
# OK, something's wrong. Reboot the board and try again.
|
||||
self.logger.debug('VEMSD not mounted, attempting to power cycle device.')
|
||||
target.sendline(' ')
|
||||
state = target.expect(['Cmd> ', self.config.bootmon_prompt, self.android_prompt]) # pylint: disable=E1101
|
||||
|
||||
if state == 0 or state == 1:
|
||||
# Reboot - Bootmon
|
||||
target.sendline('reboot')
|
||||
target.expect('Powering up system...')
|
||||
elif state == 2:
|
||||
target.sendline('reboot -n')
|
||||
target.expect('Powering up system...')
|
||||
else:
|
||||
raise DeviceError('Unexpected board state {}; should be 0, 1 or 2'.format(state))
|
||||
|
||||
self._wait_for_vemsd_mount(target)
|
||||
|
||||
self._setup_before_reboot()
|
||||
|
||||
# Reboot - Bootmon
|
||||
self.logger.debug('Rebooting into bootloader...')
|
||||
open(os.path.join(self.config.root_mount, 'reboot.txt'), 'a').close()
|
||||
target.expect('Powering up system...')
|
||||
target.expect(self.config.bootmon_prompt)
|
||||
|
||||
# Wait for VEMSD to mount
|
||||
self._wait_for_vemsd_mount(target)
|
||||
|
||||
#Boot Linux - Bootmon
|
||||
target.sendline('fl linux fdt ' + self.config.dtb)
|
||||
target.expect(self.config.bootmon_prompt)
|
||||
target.sendline('fl linux initrd ' + self.config.initrd)
|
||||
target.expect(self.config.bootmon_prompt)
|
||||
target.sendline('fl linux boot ' + self.config.kernel + self.config.kernel_arguments)
|
||||
|
||||
|
||||
# Utility functions.
|
||||
|
||||
def _slow_sendline(target, line):
|
||||
for c in line:
|
||||
target.send(c)
|
||||
time.sleep(0.1)
|
||||
target.sendline('')
|
||||
|
96
wlauto/devices/android/tc2/resources/board_template.txt
Normal file
96
wlauto/devices/android/tc2/resources/board_template.txt
Normal file
@ -0,0 +1,96 @@
|
||||
BOARD: HBI0249
|
||||
TITLE: V2P-CA15_A7 Configuration File
|
||||
|
||||
[DCCS]
|
||||
TOTALDCCS: 1 ;Total Number of DCCS
|
||||
M0FILE: dbb_v110.ebf ;DCC0 Filename
|
||||
M0MODE: MICRO ;DCC0 Programming Mode
|
||||
|
||||
[FPGAS]
|
||||
TOTALFPGAS: 0 ;Total Number of FPGAs
|
||||
|
||||
[TAPS]
|
||||
TOTALTAPS: 3 ;Total Number of TAPs
|
||||
T0NAME: STM32TMC ;TAP0 Device Name
|
||||
T0FILE: NONE ;TAP0 Filename
|
||||
T0MODE: NONE ;TAP0 Programming Mode
|
||||
T1NAME: STM32CM3 ;TAP1 Device Name
|
||||
T1FILE: NONE ;TAP1 Filename
|
||||
T1MODE: NONE ;TAP1 Programming Mode
|
||||
T2NAME: CORTEXA15 ;TAP2 Device Name
|
||||
T2FILE: NONE ;TAP2 Filename
|
||||
T2MODE: NONE ;TAP2 Programming Mode
|
||||
|
||||
[OSCCLKS]
|
||||
TOTALOSCCLKS: 9 ;Total Number of OSCCLKS
|
||||
OSC0: 50.0 ;CPUREFCLK0 A15 CPU (20:1 - 1.0GHz)
|
||||
OSC1: 50.0 ;CPUREFCLK1 A15 CPU (20:1 - 1.0GHz)
|
||||
OSC2: 40.0 ;CPUREFCLK0 A7 CPU (20:1 - 800MHz)
|
||||
OSC3: 40.0 ;CPUREFCLK1 A7 CPU (20:1 - 800MHz)
|
||||
OSC4: 40.0 ;HSBM AXI (40MHz)
|
||||
OSC5: 23.75 ;HDLCD (23.75MHz - TC PLL is in bypass)
|
||||
OSC6: 50.0 ;SMB (50MHz)
|
||||
OSC7: 50.0 ;SYSREFCLK (20:1 - 1.0GHz, ACLK - 500MHz)
|
||||
OSC8: 50.0 ;DDR2 (8:1 - 400MHz)
|
||||
|
||||
[SCC REGISTERS]
|
||||
TOTALSCCS: 33 ;Total Number of SCC registers
|
||||
|
||||
;SCC: 0x010 0x000003D0 ;Remap to NOR0
|
||||
SCC: 0x010 $SCC_0x010 ;Switch between NOR0/NOR1
|
||||
SCC: 0x01C 0xFF00FF00 ;CFGRW3 - SMC CS6/7 N/U
|
||||
SCC: 0x118 0x01CD1011 ;CFGRW17 - HDLCD PLL external bypass
|
||||
;SCC: 0x700 0x00320003 ;CFGRW48 - [25:24]Boot CPU [28]Boot Cluster (default CA7_0)
|
||||
SCC: 0x700 $SCC_0x700 ;CFGRW48 - [25:24]Boot CPU [28]Boot Cluster (default CA7_0)
|
||||
; Bootmon configuration:
|
||||
; [15]: A7 Event stream generation (default: disabled)
|
||||
; [14]: A15 Event stream generation (default: disabled)
|
||||
; [13]: Power down the non-boot cluster (default: disabled)
|
||||
; [12]: Use per-cpu mailboxes for power management (default: disabled)
|
||||
; [11]: A15 executes WFEs as nops (default: disabled)
|
||||
|
||||
SCC: 0x400 0x33330c00 ;CFGREG41 - A15 configuration register 0 (Default 0x33330c80)
|
||||
; [29:28] SPNIDEN
|
||||
; [25:24] SPIDEN
|
||||
; [21:20] NIDEN
|
||||
; [17:16] DBGEN
|
||||
; [13:12] CFGTE
|
||||
; [9:8] VINITHI_CORE
|
||||
; [7] IMINLN
|
||||
; [3:0] CLUSTER_ID
|
||||
|
||||
;Set the CPU clock PLLs
|
||||
SCC: 0x120 0x022F1010 ;CFGRW19 - CA15_0 PLL control - 20:1 (lock OFF)
|
||||
SCC: 0x124 0x0011710D ;CFGRW20 - CA15_0 PLL value
|
||||
SCC: 0x128 0x022F1010 ;CFGRW21 - CA15_1 PLL control - 20:1 (lock OFF)
|
||||
SCC: 0x12C 0x0011710D ;CFGRW22 - CA15_1 PLL value
|
||||
SCC: 0x130 0x022F1010 ;CFGRW23 - CA7_0 PLL control - 20:1 (lock OFF)
|
||||
SCC: 0x134 0x0011710D ;CFGRW24 - CA7_0 PLL value
|
||||
SCC: 0x138 0x022F1010 ;CFGRW25 - CA7_1 PLL control - 20:1 (lock OFF)
|
||||
SCC: 0x13C 0x0011710D ;CFGRW26 - CA7_1 PLL value
|
||||
|
||||
;Power management interface
|
||||
SCC: 0xC00 0x00000005 ;Control: [0]PMI_EN [1]DBG_EN [2]SPC_SYSCFG
|
||||
SCC: 0xC04 0x060E0356 ;Latency in uS max: [15:0]DVFS [31:16]PWRUP
|
||||
SCC: 0xC08 0x00000000 ;Reserved
|
||||
SCC: 0xC0C 0x00000000 ;Reserved
|
||||
|
||||
;CA15 performance values: 0xVVVFFFFF
|
||||
SCC: 0xC10 0x384061A8 ;CA15 PERFVAL0, 900mV, 20,000*20= 500MHz
|
||||
SCC: 0xC14 0x38407530 ;CA15 PERFVAL1, 900mV, 25,000*20= 600MHz
|
||||
SCC: 0xC18 0x384088B8 ;CA15 PERFVAL2, 900mV, 30,000*20= 700MHz
|
||||
SCC: 0xC1C 0x38409C40 ;CA15 PERFVAL3, 900mV, 35,000*20= 800MHz
|
||||
SCC: 0xC20 0x3840AFC8 ;CA15 PERFVAL4, 900mV, 40,000*20= 900MHz
|
||||
SCC: 0xC24 0x3840C350 ;CA15 PERFVAL5, 900mV, 45,000*20=1000MHz
|
||||
SCC: 0xC28 0x3CF0D6D8 ;CA15 PERFVAL6, 975mV, 50,000*20=1100MHz
|
||||
SCC: 0xC2C 0x41A0EA60 ;CA15 PERFVAL7, 1050mV, 55,000*20=1200MHz
|
||||
|
||||
;CA7 performance values: 0xVVVFFFFF
|
||||
SCC: 0xC30 0x3840445C ;CA7 PERFVAL0, 900mV, 10,000*20= 350MHz
|
||||
SCC: 0xC34 0x38404E20 ;CA7 PERFVAL1, 900mV, 15,000*20= 400MHz
|
||||
SCC: 0xC38 0x384061A8 ;CA7 PERFVAL2, 900mV, 20,000*20= 500MHz
|
||||
SCC: 0xC3C 0x38407530 ;CA7 PERFVAL3, 900mV, 25,000*20= 600MHz
|
||||
SCC: 0xC40 0x384088B8 ;CA7 PERFVAL4, 900mV, 30,000*20= 700MHz
|
||||
SCC: 0xC44 0x38409C40 ;CA7 PERFVAL5, 900mV, 35,000*20= 800MHz
|
||||
SCC: 0xC48 0x3CF0AFC8 ;CA7 PERFVAL6, 975mV, 40,000*20= 900MHz
|
||||
SCC: 0xC4C 0x41A0C350 ;CA7 PERFVAL7, 1050mV, 45,000*20=1000MHz
|
25
wlauto/devices/android/tc2/resources/images_iks.txt
Normal file
25
wlauto/devices/android/tc2/resources/images_iks.txt
Normal file
@ -0,0 +1,25 @@
|
||||
TITLE: Versatile Express Images Configuration File
|
||||
|
||||
[IMAGES]
|
||||
TOTALIMAGES: 4 ;Number of Images (Max : 32)
|
||||
NOR0UPDATE: AUTO ;Image Update:NONE/AUTO/FORCE
|
||||
NOR0ADDRESS: BOOT ;Image Flash Address
|
||||
NOR0FILE: \SOFTWARE\$bm_image ;Image File Name
|
||||
|
||||
NOR1UPDATE: AUTO ;IMAGE UPDATE:NONE/AUTO/FORCE
|
||||
NOR1ADDRESS: 0x00000000 ;Image Flash Address
|
||||
NOR1FILE: \SOFTWARE\kern_iks.bin ;Image File Name
|
||||
NOR1LOAD: 0x80008000
|
||||
NOR1ENTRY: 0x80008000
|
||||
|
||||
NOR2UPDATE: AUTO ;IMAGE UPDATE:NONE/AUTO/FORCE
|
||||
NOR2ADDRESS: 0x00000000 ;Image Flash Address
|
||||
NOR2FILE: \SOFTWARE\iks.dtb ;Image File Name for booting in A7 cluster
|
||||
NOR2LOAD: 0x84000000
|
||||
NOR2ENTRY: 0x84000000
|
||||
|
||||
NOR3UPDATE: AUTO ;IMAGE UPDATE:NONE/AUTO/FORCE
|
||||
NOR3ADDRESS: 0x00000000 ;Image Flash Address
|
||||
NOR3FILE: \SOFTWARE\init_iks.bin ;Image File Name
|
||||
NOR3LOAD: 0x90100000
|
||||
NOR3ENTRY: 0x90100000
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user