1
0
mirror of https://github.com/ARM-software/devlib.git synced 2025-09-23 20:31:54 +01:00

338 Commits

Author SHA1 Message Date
Sergei Trofimov
56f3b1c317 setup.py: add -s flag to sdist
Add -s flag to sdist command, which, when used, strips away the git
commit hash from the package version. This is needed for upload to PyPI.
2018-07-06 17:15:10 +01:00
Marc Bonnici
34c6d1983b version: Release version 1.0.0 2018-07-06 14:56:47 +01:00
Marc Bonnici
c4ababcd50 utils/misc: Ensure outputs are strings when raising exceptions
If the process was killed, either the output or error can
be `None` which causes an error when attempting to join the outputs.
Also update existing error message to prevent 'None' appearing in the error
message.
2018-07-05 10:17:32 +01:00
Sergei Trofimov
9fd690efb3 Update copyrights
- Update the year in the copyrights to match the last year the file was
  modified.
- Add the copyright header to files that did not already have one.
2018-07-04 16:01:47 +01:00
Marc Bonnici
e16c42fe2c target: Add new attribute to Target to indicate a container
Allow for a flag to be set to indicate that the target is a container
and therefore may have limited functionality.
2018-07-02 10:49:43 +01:00
Marc Bonnici
8aa9d672a1 devlib: Replace errors when decoding output from subprocess
If an error occurs when attempting to decode the output from subprocess
replace the offending character rather than raising an error.
2018-06-28 12:48:00 +01:00
Marc Bonnici
533a2fd2c1 doc/target: Fix typo in method description 2018-06-28 12:48:00 +01:00
Sergei Trofimov
8e1dc1359a target: fix new line handling in os_version
Use convert_new_lines() before stripping out '\n' character from OS
version strings to ensure no stray '\r's are left in.
2018-06-27 14:41:48 +01:00
Marc Bonnici
fec0868734 setup.py: Change format of version to conform with PEP440
When installing from source devlib attempts to include the commit ID in the
version of the installed pacakge however this caused issues with package
managers like pip. PEP440 specifies that local identifiers must be in the
form `<public version identifier>[+<localversion label>]` so update the
version to conform.

https://www.python.org/dev/peps/pep-0440/#local-version-identifiers
2018-06-27 11:41:28 +01:00
Michele Di Giorgio
0915d97f71 module: Add devfreq module
Add support for devfreq. This is used for example to get/set the frequency
and/or governor of the GPU.
2018-06-26 12:18:06 +01:00
Valentin Schneider
d81b72a91b trace: Add a Systrace TraceCollector 2018-06-22 14:55:26 +01:00
Valentin Schneider
96ffa64ad8 AndroidTarget: Add some more screen utility methods 2018-06-22 10:23:18 +01:00
Valentin Schneider
38037850b6 AndroidTarget: Add force_new parameter to open_url
When True, this will force whatever application Android deems best
for viewing that url to be relaunched.

This can be useful when trying to open e.g. Google Maps search URLs,
as the actual search result can be influenced by the location being
currently displayed. Forcing the app the be relaunched allows us to
have reproducible behaviours.
2018-06-22 10:23:18 +01:00
Valentin Schneider
56a7394d58 utils/android: Add the definition of some intent flags 2018-06-22 10:23:18 +01:00
Valentin Schneider
bda1115adb AndroidTarget: Fix escape_double_quote typo 2018-06-22 10:23:18 +01:00
Sergei Trofimov
cc04e1a839 Implement PEP396
Implement PEP396 which specifies that a package should advertise its
version via __version__ attribute.

As devlib is often used as a development version directly from source,
also add a __full_version__ attribute which appends the current commit
ID to the version.

Use the __full_version__ inside setup.py
2018-06-15 08:58:35 +01:00
Sergei Trofimov
4a862d06bb utils/version: add get_commit
Add a function to get the commit ID of the devlib repository (if running
from source, e.g. via "setup.py develop").
2018-06-15 08:58:35 +01:00
Sergei Trofimov
f1c945bb5e utils/types: implement __ne__ for caseless_string
This should have been handled by the @total_ordering decorator, but
isn't due to

	https://bugs.python.org/issue25732

(briefly, total_ordering is back-ported from Python 3, where the base
object provides the default implementation of __ne__ based on __eq__, so
total_ordering did not override it; this, however does not happen in
Python 2).
2018-06-14 15:04:35 +01:00
Valentin Schneider
51452d204c module: Add sched module
This module is a collection of scheduler data getters.
2018-06-14 12:09:25 +01:00
Valentin Schneider
7231030991 target: Add strict option to KernelConfig.get()
Defaults to False. If True, will raise an exception when a requested
config name is not exposed in the config instance.
2018-06-14 12:09:25 +01:00
Sascha Bischoff
085737bbfa gem5: Add checkpoint support
We add the ability to explicitly take checkpoints when running with a
gem5 system. As we cannot have any state which is shared between the
host and simulated system, we first unmount the VirtIO device, take
the checkpoint, and then remount the VirtIO device into the simulated
system.
2018-06-14 11:35:56 +01:00
Sascha Bischoff
9e45d65c94 gem5: Unmount the VirtIO device at the end of the run
We explicitly unmount the VirtIO device (used to transfer files into
the simulated system) at the end of a run (when the connection is
closed) in order to make checkpointing the simulated system
easier. gem5 supports checkpointing the state of the simulation when
it is terminated, and future simulations are able to resume from this
snapshot. However, for the checkpoint to work correctly, we need to
make sure that there is no shared state between the simulated system
and the host. This mandates that we disconnect the VirtIO device prior
to taking a checkpoint.
2018-06-14 11:35:56 +01:00
Sergei Trofimov
008f96673f utils/android: add recent Android versions
Add the more recent android versions to the version map.
2018-06-14 11:35:09 +01:00
Sergei Trofimov
77a6de9453 utils/android: include stderr in adb_shell output
Include stderr output of the executed command in the output returned by
adb_shell. This will align the AdbConnection behavior with that of
SshConnection and ensure that target.execute() behaves consistently
across Android and Linux targets.
2018-06-14 11:34:55 +01:00
Sergei Trofimov
d4b0dedc2a utils/misc: add combined output option to check_output
Add an option to combine stderr and stdout into a single stream.
2018-06-14 11:34:55 +01:00
Sergei Trofimov
69cd3be96c target: ensure shell_prompt is a bytes_regex
shell_prompt gets passed into expect and therefore must be encoded as
bytes on Python 3.
2018-06-14 11:34:16 +01:00
Sergei Trofimov
7e942cdd4a utils/types: add regex types
Add types for regex and bytes_regex. In Python 3, regular expression
objects differ based on whether they were created with a str  or a
bytes instance as the pattern, and can only match against instances of
the corresponding type.

To make sure we always end up using the right version (e.g. pexpect
needs bytes regexes), create functions to do the appropriate
conversions.
2018-06-14 11:34:16 +01:00
Sascha Bischoff
41f460afbe trace/serial_trace: Flush data to file before copying
We add a missing flush which esures that all data has been synced to
the temporary file before we copy it. Prior to this commit, we would
sometimes miss the last few lines of the trace.
2018-06-14 11:33:10 +01:00
Waleed El-Geresy
804a044efc platform/arm: Add import sys 2018-06-13 15:16:19 +01:00
Waleed El-Geresy
b06035fb12 Fix Python3 Byte and Regex Handling
Convert bytes to strings (utf-8 encoding) to make compatible with
Python3 in arm.py
Use the pattern property to extract the string from the regex pattern,
to pass as a string to tty.expect.
Drop problematic characters when decoding stdout and stderr in misc.py
by setting errors='replace' in the string decode method.
2018-06-13 15:16:19 +01:00
Sascha Bischoff
6abe6067da trace/serial_trace: Add capability to trace serial traffic
We add a TraceCollector which logs the traffic on a serial port. This
can then be used to debug why a board crashes, or to extract extra
information from the device whilst it is running a workload.
2018-06-13 14:36:17 +01:00
Pierre-Clement Tosi
c4f6a1a85f Instrument: Fix & restructure reset()
Calling `Instrument.reset(kinds='some string')` leaves
`self.active_channels` as `[]` which is probably not the expected
behaviour. This is caused by the last nested `else` which refers to
the condition `if isinstance(kinds, basestring)` and might have been
overlooked because of having been confused with the top-level `else`.
Anyhow, an `else` does not seem to be needed there.

This bug illustrates the risk of having too many nested levels and
execution paths which also impact the readability of the code. We
modify the implementation to solve the bug on top of which we:

  - Reduce the maximum order of nested levels from 4 to 3;

  - Express more clearly the potential paths of execution
    (less nested conditions);

  - Replace unnecessary `for`-loops by list comprehensions,
    removing the need for an initialisation of `active_channels`
    and making clearer what each path of execution ends up with;

  - Removed unnecessary `List` copies of `self.channels.values()`;

  - Used the fact that the message of a `KeyError` is the unknown
    key.
2018-06-08 17:32:14 +01:00
Marc Bonnici
fe0d6eda2a utils/android: Try to ping adb devices regardless of connection type
Previously if a device was connected over usb then the adb_connect
method would assume the device was already connected. This can cause
issues when rebooting and the device is not ready by the time devlib
attempts to reconnect to it causing the next command to fail. Now still
only execute the 'connect' command when the device is connected over the
network, however always trying pinging the device to see if it is
connected before returning.
2018-06-07 15:18:23 +01:00
Sergei Trofimov
5cafd2ec4d Add support for Python 3
Add support for running on Python 3 while maintaining Python 2
compatibility.
2018-06-07 14:45:43 +01:00
Sergei Trofimov
0d63386343 module/cpufreq: include policy0 in probe path
Check /sys/devices/system/cpu/cpufreq/policy0 rather than its parent
during the probe. This is to handle the edge case where cpufreq has
been enabled in the kernel, but no frequency domains have been defined
(in which case, the module should not install).
2018-06-06 15:37:10 +01:00
Marc Bonnici
a35f715b63 doc/cpufreq: Add docs for max/min frequency methods.
Adds documentation for the previously added methods in commit
a1e58cd8b1 and clarify existing method
documentation.
2018-06-01 17:27:37 +01:00
Marc Bonnici
55762edf19 cpufreq: Add methods to retrieve max/min available frequencies for a cpu
Adds usability methods methods to allow easy retrieval of the maximum
and minimum frequencies available for a given cpu
2018-05-31 09:49:37 +01:00
Marc Bonnici
1d9dc42af5 target: Increase timeout for checking if rooted.
One some devices executing this command takes longer than the original 2
seconds allowed causing devlib to incorrectly think it was not rooted.
2018-05-31 09:49:37 +01:00
Vincent Guittot
be4f01ebaf cgroups: add execute as root
On latest hikey960 android image, only root can execute: cat /proc/cgroups

Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
2018-05-30 11:45:34 +01:00
Sergei Trofimov
d6ccbb44c3 module/cpuidle: ensure get_states() returns a list
Ensure that cpuidle.get_states() always returns a list, even if no idle
states are available on the target.
2018-05-23 10:31:38 +01:00
Marc Bonnici
329df6f42e AndroidTarget: Ensure path is correctly quoted when listing directories
Previously the path for listing a directory on the device was not quoted
causing it to fail on paths containing spaces. Now ensure the string is
quoted and any quotes contained in the string as escaped.
2018-05-10 11:12:25 +01:00
Marc Bonnici
63bf68b49d LinuxTarget: Ensure path is correctly quoted when listing directories
Previously the path for listing a directory on the device was not quoted
causing it to fail on paths containing spaces. Now ensure the string is
quoted and any quotes contained in the string as escaped.
2018-05-10 10:57:59 +01:00
sergei Trofimov
7e39ecf142 cpufreq: add missing get_driver() method
Add a method to read the name of the driver for cpufreq policy for a
cpu.
2018-05-01 11:38:11 +01:00
Marc Bonnici
1e839028a1 target.py: Update default shell prompt 2018-04-25 17:25:06 +01:00
Sascha Bischoff
9eb88cd598 trace: Add screencapture
Add a poller which takes a screenshot at a configurable interval
(`period`). Files are named based on the device timestamp, and are
placed into a configurable output directory.
2018-04-24 10:04:15 +01:00
Marc Bonnici
bb3ae48d25 exception: Update "TargetNotRespondingError" syntax.
Update the syntax of the TargetNotRespondingError to conform with the
other exceptions of expecting a full message to be displayed rather than just
a target name.
2018-04-24 09:04:28 +01:00
Marc Bonnici
58c0d30b26 target: Make sure xml is encoded properly before writing to file
Some UI elements can contain non ASCII characters so ensure we encode the
output to utf-8 before writing to file.
2018-04-24 09:04:28 +01:00
sergei Trofimov
87b235638a utis/misc: make check_output thread-safe
subprocess.Popen (used internally by check_output) is not thread-safe
and may cause a thread to lock up if called simultaneously from multiple
threads. See

	https://bugs.python.org/issue12739

This is fixed in Python 3.2, but since we're currently still on 2.7,
work around the issue by protecting the call with a lock.
2018-04-20 15:03:33 +01:00
Sergei Trofimov
b88b400d8d doc: document capture_screen timestamp
Updated documentation for capture_screen with the information about the
optional timestamp format tag.
2018-04-11 11:01:45 +01:00
Sergei Trofimov
8370c8fba3 target: timestamp for capture_screen
Add an option to format an ISO8601 timestamp into the screenshot file
name.
2018-04-11 10:51:38 +01:00
Marc Bonnici
2a23c435d4 README: Add link to documentation on readthedocs 2018-04-10 17:36:04 +01:00
Marc Bonnici
59e2f2d126 docs/target: Add install_if_neeeded method info 2018-04-10 17:36:04 +01:00
Sergei Trofimov
56e9147e58 setup.py: update URL
Update the url entry in the package metadata to be a valid URL. This is
now required for upload to PyPI.
2018-03-22 11:10:43 +00:00
Sergei Trofimov
9678c7372e AndroidTarget: add capture_ui_hierarchy
Add a method to dump XML UI hierarchy from uiautomator.
2018-03-21 15:02:02 +00:00
Sascha Bischoff
078f0dc641 derived/energy: Fix energy calculation
Correctly detect whether or not the energy should be calculated from
power or extracted directly. Previously both methods were being used
at the same time, resulting in incorrect total energy values.
2018-03-21 11:48:50 +00:00
Sascha Bischoff
335fa77e4e AndroidTarget: add get_pacakge_info
Add a method to get info (apk path) for an installed package.
2018-03-15 17:06:02 +00:00
Marc Bonnici
c585a4e489 uitils/android: Fix logcat monitor on older devices
The `-e` argument to match logcat output with a regex expression is not
present on older devices. If the target is running pre marshmallow fall
back to pipeing logcat into grep.
2018-03-15 12:26:15 +00:00
Marc Bonnici
a992a890b8 AndroidTarget: Add additional permission error to ignore
Also ignore any errors that granting a permission on an Android device
is not permitted.
2018-03-15 12:26:15 +00:00
Anouk Van Laer
5001fae516 gem5: Update to telnet connection method
gem5 prints a string, to communicate which telnet port can be used to connect
to the gem5 system.  The exact string has recently changed. This commit ensures
both the old and new string are taken into account when devlib parses gem5
output to determine the telnet port.
2018-03-13 14:26:35 +00:00
Douglas Raillard
f515420387 cgroups: fix Controller.tasks()
Fix cgroups tasks parsing in Controller.tasks() method, to ignore lines
that are not formatted properly.
2018-03-13 14:22:43 +00:00
Sergei Trofimov
e3d9c4b2fd bin/trace-cmd: add x86-64
Add an x86-64 version of the statically linked trace-cmd binary.
2018-03-12 16:17:31 +00:00
Marc Bonnici
e22d278267 AndroidTarget: Add method to open a url in the default browser 2018-03-12 11:25:48 +00:00
Sergei Trofimov
17d32a4d40 acmecape: fix warning formatting 2018-03-07 18:19:59 +00:00
Sergei Trofimov
7a8f98720d target: add wait_boot_complete to LinuxTarget
Add an empty implementation, as there is currently no generic way of
detecting boot completion for a random Linux system, and it is
considered to be "fully booted" as soon as it ready to accept
connections.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
328e0ade4b target: fix connect on reboot
Previously, commit

	commit 17bcabd461

	    target: Install busybox before updating modules

moved busybox deployment into Target.connect() so that modules could
make use of it. This means this now happens before AndroidTarget waits
for the boot to complete at the end of its connect(). This means that
an attempt to create the devlib working directory may be made too early.

To get around this, move wait_boot_completed() into Target, and ensure
it returns before attempting to create the working directory.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
d5ff73290e module/vexpress catch CalledProcessError
Depending on what goes wrong, this may also be raised.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
f39631293e utils/android: change adb_shell error type
Re-raise CalledProcessError originating inside adb_shell as a
TargetError to be consistent with other errors pertaining to the target.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
c706e693ba utils/serial_port: fix exception message
SerialError does not populate it's message attribute, so the message was
lost when re-raising as HostError. Pass the string representation of
SerialError into HostError instead.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
f490a55be2 target: conditionally raise from check_responsive
Add an option to check_responsive() to not throw an exception if
unresponsive target is detected, but to act as a predicate instead.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
0e017ddf9f module/vexpress: fix reboot for Juno
- The autoboot message in the firmware has changed; detect both the old
  and the new messages.
- Depending on where exactly the boot was interrupted, either a "reboot"
  or a "reset" may be required; so send both.
2018-03-07 18:19:59 +00:00
Sergei Trofimov
b368acb755 plaform/juno: fix ip address from uart
In recent builds, it seems doing "ip addr list eth0" returns "no such
device" when running as a regular user. Doing so as root, will give the
information on the device.
2018-03-07 18:19:59 +00:00
Sascha Bischoff
83e5ddfd1b target: add option to invoke to redirect stderr
The stderr of a command is not propagated as part of the command
output. However, some workloads/commands write vital output to
stderr. For this reason, we add an option to invoke which redirects
stderr to stdout (redirect_stderr). This is disabled by default.
2018-03-01 15:32:49 +00:00
Elieva Pignat
8f3dc05f97 cgroups.py: Add filters for the tasks() function
The tasks() function allows to get the tasks that are in a cgroup.
Filters for the tasks TID, name and cmdline have been added to the
parameters of the function such that it is possible to select the tasks
that match these patterns.

Signed-off-by: Elieva Pignat <Elieva.Pignat@arm.com>
2018-02-28 14:44:50 +00:00
Sascha Bischoff
bb4f92c326 gem5Connection: Catch EOF errors & make more user friendly
Previously, when the gem5 simulation crashed, one would get errors
relating to pexpect reaching EOF, rather than an informative message
stating the gem5 itself had crashed. With this change, we catch some
of the common cases where this can happen, and inform the user if gem5
itself has crashed. In the event that the gem5 simulation itself has
not reported an error, we instead re-throw the original pexpect error.
2018-02-26 18:02:55 +00:00
Vincent Guittot
a0fc7202a1 Add a new Arm Energy Probe instrument
Add new energy instrument that is based on arm-probe tool to manage AEP
Main advantages of this tool are:
- uses a config file for describing channels and shunt resistors value
- manages power topology description in the config file. This topology
is then used when computing power figures
- can create virtual power channel and aggregate channels
- support multiple AEP
- support auto-zero of AEP's channel

Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
2018-02-26 17:53:58 +00:00
Sergei Trofimov
9e8f77b8f2 utils/misc: fix to_identifier for unicode
string.translate() can fail when passed a unicode object; explicitly str()
it first to avoid this.
2018-02-21 14:01:56 +00:00
Sergei Trofimov
515095d9b2 trace/ftrace: increase trace pull timeout
Double the existing timeout, as the current one is too short for noisy
devices on slower connections.
2018-02-12 17:37:16 +00:00
Sergei Trofimov
f3c8ce975e target: fix get_rotation() when null
Some targets don't seem to set system.user_rotation, resulting in "null"
being returned. This exploded on integer conversion. Handle this case by
returning the Python equivalent, None.
2018-02-08 14:27:47 +00:00
Marc Bonnici
bfda5c4271 utils/misc: Check if identifier starts with a number
Now ensures that the given text does not start with a digit and if so
prefix an underscore to ensure a valid python identifier.
2018-01-31 19:07:20 +00:00
Marc Bonnici
d1b08f6df6 target: Fix typos 2018-01-25 06:27:55 +00:00
Marc Bonnici
17c110cc97 doc/target: Add ChromeOs Target documentation 2018-01-25 06:27:55 +00:00
Marc Bonnici
e9cf7f5cbe target: Adds initial support for ChromeOs targets
Adds initial support for ChromeOs Targets.

If the device does not support running android apps the target will
behave like a `LinuxTarget` however if android is supported, the chromeos
target opens 2 connections, one via shh to the linux target, as normal,
and one via adb to the android container. By default all calls will be
made to the linux target and if not present, will attempt to use the
android container instead. The android container is also exposed via a
`android_container` attribute so can be accessed directly.

In order to detect whether the target supports running android apps the
existance of '/opt/google/containers/android/' will be queried upon
connection to the linux target.
2018-01-25 06:27:55 +00:00
Marc Bonnici
ead0c90069 platfrom: Add 'A73' to list of 'big' cpus 2018-01-23 13:15:00 +00:00
Marc Bonnici
2954a73c1c Target: Generate a new tmp directory when creating shutils
Previously the same temporary directory was used which caused issues
with concurrent runs interfering with each other.
2018-01-18 14:21:39 +00:00
Patrick Bellasi
cc0210af37 LocalLinuxTarget: fix executables folder location
Since:
   cafc0a4 target: do not create shutil in package directory
we generate 'shutils' in /tmp, which is also the executables_path
used by default by a LocalLinuxTarget.

This ultimately results into a:

   self.install(shutils_ofile)
      ...
         shutil.copy(source, dest)

raising an exception since source == dest.

Let's fix this by setting /tmp/devlib-target as a default base path for
all devlib deployed stuff into a localhost target.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2018-01-18 13:03:34 +00:00
Sergei Trofimov
730118d6d0 platform/gem5: better error message on crash
Point to gem5's stderr file in the message of the error raised on crash
during _intercept_telnet_port.
2018-01-17 17:03:24 +00:00
Marc Bonnici
f0b58b32c4 target: Add support for as_root to get_directory method. 2018-01-08 17:05:21 +00:00
Marc Bonnici
30257456ab target: Fix creation of destination
Was previously trying to create the output directory including the
filename.
2018-01-08 17:05:21 +00:00
Marc Bonnici
853bdff936 target: Clean up tmp files afer pulling 2018-01-08 17:05:21 +00:00
Marc Bonnici
54d6a6d39d target: Allow pulling of folders as root
Adds '-r' flag to copy command to allow for copying of folders with their contents
to temporary location on the device before pulling.
2018-01-08 17:05:21 +00:00
Marc Bonnici
3761b488a0 Docs: Fixed typos 2017-12-22 16:22:29 +00:00
Anouk Van Laer
462aecdca0 target, android: Set the default timeout to 30s
Previously, the default timeout was 10s which is too short in some cases.
2017-12-12 17:32:08 +00:00
Sergei Trofimov
cafc0a4bc0 target: do not create shutil in package directory
Do not attempt to create shutil from shutil.in inside
PACKAGE_BIN_DIRECTORY as that may not be writable. Instead, create it in
the temporary directory and remove it right after installing.
2017-12-12 13:46:30 +00:00
Valentin Schneider
724c0ec8df target: LocalLinuxTarget: Populate _file_transfer_cache
Since 1e34390b99, AndroidTarget and
LinuxTarget share the same code for `push` and `pull`. However,
these methods expect a `_file_transfer_cache` field to be present.
It is currently populated in `_resolve_paths` for AndroidTarget
and LinuxTarget, but not for LocalLinuxTarget.

Since LocalLinuxTarget inherits its `push` & `pull` from LinuxTarget,
this causes an exception. This commit fixes it by adding an assignment
to `_file_transfer_cache` in `LocalLinuxTarget:_resolves_paths`.

This is a simple fix, and the actual push/pull behaviour of that local
target class could be improved in regards to that transfer cache, but
it should be good enough for now.
2017-12-12 13:28:24 +00:00
Marc Bonnici
ceb493f98d utils/android: Change directory used for checking connection
Previously the root directory was used when checking for device
connection, on some devices this requires elevated permissions so now
use '/data/local/tmp' which should not have permission restrictions.
2017-12-12 10:33:28 +00:00
Sergei Trofimov
8ac588bc1f utils/serial_port: fix logging
pexpect.spawn object expects the logger to have write() and flush()
methods, neither of which are present in logging.Logger. Create a
subclass that adds these missing methods to enable correct logging for
pexpect.spawn.
2017-12-12 09:24:03 +00:00
Sergei Trofimov
56a5f8ab12 platforms/arm: JunoEnergyInstrument fixes
- Make sure commands are updated after installing readenergy binary
  during setup.
- Add the missing channels argument to reset().
- Set the sample rate for the MeasurementCsv inside get_data().
2017-12-12 09:24:03 +00:00
Sergei Trofimov
75ff31c6c7 readenergy: update table headers
Update CSV headers output by readenergy to match the corresponding
channel names. This will avoid needing to translate them afterwards.
2017-12-12 09:24:03 +00:00
Brendan Jackman
1e34390b99 Target: Implement as_root parameter for push/pull in base class
The AndroidTarget implementation is actually generic, so just move it
into the parent class so that LinuxTarget gets it too.
2017-12-11 08:07:50 +00:00
Brendan Jackman
a2072d5c48 doc: Document as_root param of Target.push/pull 2017-12-11 08:07:50 +00:00
Marc Bonnici
35c7196396 utils/ssh: Fix escaping of command for push/pull
Ensure that source and destination are quoted and all space characters
are also escaped which is required for scp to work for file paths
containing spaces.
2017-12-11 08:06:22 +00:00
Marc Bonnici
0dde18bb56 utils/misc: Add method to escape spaces 2017-12-11 08:06:22 +00:00
Ionela Voinescu
7393ab757e instrument: convert channel labels in valid Python identifiers
Channel labels can contain punctuation which is not accepted in
Python identifiers.

Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
2017-12-08 10:48:02 +00:00
Sascha Bischoff
002939d599 module/cpuidle: remove stray print
Remove a stray debug print which was accidentally committed.
2017-12-08 10:46:38 +00:00
Brendan Jackman
dd4c37901b cpufreq: Fix list_frequencies when not available
On intel_pstate machines, we can't get a list of available CPU
frequencies. In that case, return empty list as per the docastring.
2017-12-08 09:46:58 +00:00
Ionela Voinescu
0c7d440070 gem5power: fix gem5power get_data after channel rename
active_sites is used instead of sites_to_match and that
results in "KeyError: 'timestamp'" when parsing the
statistics file. 'sim_seconds' should be used instead
of 'timestamp'.

Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
2017-12-08 09:40:19 +00:00
Marc Bonnici
e414a3a193 LinuxTarget: Port forbidden char fix from WA2
Fix for Chromebook Plus and possibly other devices - removes forbidden
characters from the device_model such as the null character.
2017-12-08 08:26:53 +00:00
Marc Bonnici
857edbd48b AndroidTarget: Change android default working directory.
Changes the default working directory from
`/data/local/tmp/` to be on the devices external storage
(discovered from $EXTERNAL_STORAGE) which is usually `/sdcard`.
This is due to permission errors on some devices, to be readable
by android applications and will usually have a larger capacity.
2017-11-30 11:46:14 +00:00
Sergei Trofimov
f52ac6650d platform/gem5: always deploy m5 binary
m5 binaries are not properly versioned, so it is not possible to be sure
that the one that is already present on the image has all the features
needed by devlib. Thus always deploy and use our version.
2017-11-22 13:53:12 +00:00
Marc Bonnici
eaafe6c0eb SSH/Gem5Connection: Only remove echo in gem5_shell if required
Only attempt to remove echoed commands from the shell during parsing
if gem5 is actually echoing commands otherwise this can cause incorrect
filtering.
2017-11-22 11:52:04 +00:00
Marc Bonnici
2a8f2c51d7 SSH/Gem5Connection: Adds shell_echo parameter
This flag is used to indicate whether gem5 echos the exectued command
which requires removal for correct parsing.
2017-11-22 11:52:04 +00:00
Sergei Trofimov
01b0ab8dce derived/fps: fix divide by zero
Avoid dividing by frame_count if it's zero when calculating janks_pc.
2017-11-21 17:21:25 +00:00
Sergei Trofimov
c0a896642d target: fix broadcast_media_mounted for Android O
In an effort to reduce thrashing, "implicit" broadcasts are no longer
allowed in Android O. See:

https://issuetracker.google.com/issues/36496703#comment7

This impacts MEDIA_MOUNTED broadcasts. To get around this issue,
explicitly specify the component the broadcast is targeted at.
2017-11-21 14:34:35 +00:00
Michele Di Giorgio
c492f2e191 shutils: consider different hierarchies when running command into cgroups
The target platform can have different hierarchies with different cgroup
controllers mounted. If we have a cgroup that uses different controllers
belonging to different hierarchies, that cgroup will be present under the
different hierarchies. Therefore, in such cases we need to take into all those
paths having that cgroup and move the task into all of them.
2017-11-21 14:32:49 +00:00
Michele Di Giorgio
f3b04fcd73 shutils: look for an exact match of a cgroup
When running a command into a cgroup we want to make sure that the command is
only run inside the intended cgroup. If there is a hierarchy of cgroups named
with a common prefix, the script will move it to all matching cgroup and as a
result the task will end up running always at lowest level. For example, if we
have the following hierarchy of cgroups:

  "/"
   |__ "/tg1"
         |__ "/tg1/tg1_1"

and we want to run something in cgroup "/tg1/", the lowest levels will match the
regexp and the task will ultimately be moved to "/tg1/tg1_1".

This patch fixes the issue by requiring the absolute path of the specified
cgroup to match exactly.
2017-11-17 07:57:13 +00:00
marcbonnici
02384615dd Merge pull request #203 from mdigiorgio/fix-cgroups
module/cgroups: fix typo in exists method
2017-11-15 16:00:45 +00:00
Michele Di Giorgio
791edc297c module/cgroups: fix typo in exists method 2017-11-15 15:56:06 +00:00
setrofim
4ef1e51b97 Merge pull request #201 from marcbonnici/android
Android
2017-11-14 13:51:36 +00:00
Marc Bonnici
899dbfe4fb Target: Ensures path is quoted and escaped 2017-11-13 15:21:27 +00:00
Marc Bonnici
0390c9d26b AndroidTarget: Port methods for refreshing target files
Adds a method to determine the appropriate method of triggering a media
refresh of a given list of file based on the devices android version and root
status. If a device is running android marshmallow and below or has root, trigger a
refresh of the files containing folder otherwise trigger a refresh of each
individual file.
2017-11-13 15:21:27 +00:00
Marc Bonnici
405c155b96 Utils/Misc: Port common prefix function from WA
Adds a utility function to determine the lowest common base path of a
passed list of files.
2017-11-13 15:21:27 +00:00
Marc Bonnici
bd03b2f8ac Utils/Android: Enable parsing of apk permissions
Creates a list of permissions that an apk requires for use, this is
useful for granting of permissions to a previously installed apk.
2017-11-13 15:21:27 +00:00
Marc Bonnici
5d40b23310 AndroidTarget: Adds method to grant permission to an installed package
This is useful on later versions of android where a previously
installed application is missing required permissions without
having to reinstall the whole application.
2017-11-13 15:21:27 +00:00
Marc Bonnici
6fae051deb AndroidTarget: Adds property to retrieve path of external storage
In andoid terms the external storage usually means the internal emulated
sdcard located at '/sdcard/'. This is the usually the root location used for
applications to read files that are not part of the application itself
from.
2017-11-13 15:21:21 +00:00
marcbonnici
aca3d451f7 Merge pull request #200 from Sticklyman1936/gem5_fixes
Gem5 fixes
2017-11-08 10:28:20 +00:00
Sascha Bischoff
fa9d7a17b3 gem5: Mount the VirtIO device as_root
This was failing in some instances as it was not being executed as
root. Hence we now set as_root.
2017-11-08 10:24:34 +00:00
Sascha Bischoff
61bbece59b gem5Connection: Fix "as_root" to actually use root
Previously as_root was ignore. This is no longer the case!
2017-11-08 10:24:32 +00:00
Sascha Bischoff
efbd04992d gem5: Fix prompt matching
Add '$ ' to prompt matching, which was previously missing.
2017-11-08 08:43:06 +00:00
marcbonnici
a7b9ef594f Merge pull request #199 from kdub/gh-kdub-gpufreq
module: add a gpufreq module for interacting with GPUs.
2017-11-06 17:51:07 +00:00
Kevin DuBois
e2ce5689bd module: add a gpufreq module for interacting with GPUs.
Add a module for controlling and interacting with GPUs. The module
currently supports kgsl/adreno based GPUs. This allows for querying
and setting the governor on the GPU, as well as seeing the frequencies
at which the the GPU can can operate.

Change-Id: I02bb997b51426ddaa13e1f8da375aa4c4a0d341a
2017-11-06 09:42:13 -08:00
setrofim
fae12d70a6 Merge pull request #198 from mcgeagh/remove-newline_separator
Remove newline_separator from utils/android.py
2017-11-01 11:08:12 +00:00
Michael McGeagh
61390a714c Remove newline_separator from utils/android.py
Elsewhere in devlib, we strip '\r' characters and then handle it as if
it was just normal unix style '\n'.
However in adb_shell, we have a newline_separator option which defaults
to windows style '\r\n'.

This commit removes the newline_separator() function in the
AdbConnection class, removes the use of this function in execute()
function, and removes the parameter from adb_shell() function.
Instead, in the adb_shell() function, the same action as elsewhere in
devlib is performed: replace('\r\n', '\n').replace('\r', '\n')
2017-11-01 10:42:13 +00:00
setrofim
7b816b2345 Merge pull request #197 from bjackman/pexpect-fix-read
LogcatMonitor: Fix using get_log without wait_for
2017-10-31 14:00:11 +00:00
Brendan Jackman
1b71507d8e LogcatMonitor: Fix using get_log without wait_for
Currently if you never call wait_for, the underlying pexpect will not
read bytes from the logcat command's output into the log file. So
when we get into get_log, we need to force it to read all the
already-available bytes.
2017-10-31 13:55:31 +00:00
setrofim
af0ed2ab48 Merge pull request #196 from bjackman/is-network-connected
target: Ensure returning False when is_network_connected fails
2017-10-24 16:29:17 +01:00
Brendan Jackman
417ab3df3e target: Ensure returning False when is_network_connected fails 2017-10-24 16:05:05 +01:00
setrofim
dcffccbb69 Merge pull request #193 from bjackman/is-network-connected
target: Add is_network_connected method
2017-10-24 14:36:37 +01:00
Brendan Jackman
486b3f524e target: Add is_network_connected method 2017-10-23 14:46:16 +01:00
setrofim
1ce96e0097 Merge pull request #194 from bjackman/screen-rotation-int
AndroidTarget: Make get_rotation return an int
2017-10-23 12:59:08 +01:00
setrofim
3056e333e1 Merge pull request #195 from bjackman/fix-logcat-logfile
LogcatMonitor: Fix opening logfile for write
2017-10-23 12:55:02 +01:00
Brendan Jackman
a679d579fd LogcatMonitor: Fix opening logfile for write 2017-10-20 16:08:32 +01:00
Brendan Jackman
fe403b629e AndroidTarget: Make get_rotation return an int
That means you can pass the result back to set_rotation without
having to change its type.
2017-10-18 14:26:54 +01:00
setrofim
16d5e0b6a7 Merge pull request #192 from derkling/acme_fix_iio-capture_release
Instrument/Acmecape: ensure iio-capture termination
2017-10-12 16:34:48 +01:00
Patrick Bellasi
4a6aacef99 Instrument/Acmecape: ensure iio-capture termination
Once an ACME cape instrument is released, if the stop() method has not
been called by the client code, let's ensure to release the channels by
killing the corresponding iio-caputure process.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2017-10-12 15:01:28 +01:00
setrofim
9837b4012b Merge pull request #191 from bjackman/master
LogcatMonitor: Fix clear_log
2017-10-12 14:46:09 +01:00
Brendan Jackman
71d5b8bc79 LogcatMonitor: Fix clear_log 2017-10-12 14:39:27 +01:00
setrofim
5421ddaae8 Merge pull request #190 from bjackman/logcat-pexpect
android: Use pexpect for LogcatMonitor
2017-10-12 13:43:45 +01:00
Brendan Jackman
1d85501181 utils/android: Use pexpect for LogcatMonitor
Using a pexpect.spawn object simplifies the LogcatMonitor by removing
the need for a separate thread along with the synchronization that
brings. Since pexpect.spawn creates a logfile that is flushed with
each write, it also removes the need for code to handle flushing.

I originally wrote this to allow more complex features that are made
possible by the pexpect API, however I then discovered those features
are not necessary and did not submit this for merging.

However I then discovered that in Python 2.7,
threading.Event.wait (used in the `wait_for` method) makes the task
uninterriptible (i.e. can't be killed with Ctrl+C/SIGINT), which is
rather frustrating. This issue doesn't arise when using pexpect's
expect method, so that's why I'm submitting this now.
2017-10-12 10:35:08 +01:00
Sergei Trofimov
a01418b075 platform/arm: propagate model parameter
VExpress platforms were not updated to handle "model" parameter when it
was added to Platfrom.
2017-10-11 17:09:22 +01:00
setrofim
0f2ac2589f Merge pull request #189 from bjackman/master
AndroidTarget: Add boolean 'state' to airplane mode intent broadcast
2017-10-11 15:21:56 +01:00
Brendan Jackman
da22befd80 libs/android: Add get_adb_command function
This is just like adb_command but instead of executing the command it just
returns it.
2017-10-11 13:40:25 +01:00
Brendan Jackman
0bfb6e4e54 AndroidTarget: Add boolean 'state' to airplane mode intent broadcast
Recently I've been seeing some errors with enabling airplane mode on
hikey960 - firstly I get a 'broken pipe' error followed by a 'cmd:
Can't find service: settings'. This missing boolean was the first
thing I found that had changed wrt. the code I used to use, and
adding it back appears to fix the issue.
2017-10-11 13:22:37 +01:00
setrofim
dc453ad891 Merge pull request #184 from setrofim/master
ANSI escape sequences stripping fixes for gem5
2017-10-10 08:37:08 +01:00
setrofim
b0457f7ed7 Merge pull request #188 from bjackman/apkinfo-better-error
ApkInfo: Improve error for bad .apk files
2017-10-10 08:35:55 +01:00
setrofim
4d269774f7 Merge pull request #187 from bjackman/clear-logcat-lock
AndroidTarget: prevent concurrent invocations of 'logcat -c'
2017-10-10 08:35:03 +01:00
setrofim
34e7e4c895 Merge pull request #186 from bjackman/acmecape-improvements
Acmecape improvements
2017-10-10 08:33:56 +01:00
setrofim
535fc7ea63 Merge pull request #185 from bjackman/master
target: Install busybox before updating modules
2017-10-10 08:31:33 +01:00
Brendan Jackman
99aca25438 ApkInfo: Improve error for bad .apk files
Provide a more specific error, including the output from aapt.
2017-10-09 18:31:32 +01:00
Brendan Jackman
7dd7811355 acmecape: Add note on how to reboot ACME
The ACME firmware sometimes benefits from turning-it-off-and-on-again.
2017-10-09 18:25:03 +01:00
Brendan Jackman
dbe568f51b acmecape: Add check for nonzero return code from iio-capture 2017-10-09 18:25:03 +01:00
Brendan Jackman
0b04ffcc44 acmecape: Fix default iio-capture binary name 2017-10-09 18:25:03 +01:00
Brendan Jackman
8a0554faab AndroidTarget: prevent concurrent invocations of 'logcat -c'
'adb logcat -c' has been observed to fail when called twice concurrently. Rather
than requiring all devlib users to fix their usage patterns, let's just delay
whenever clear_logcat is called twice.
2017-10-09 17:15:41 +01:00
Brendan Jackman
17bcabd461 target: Install busybox before updating modules
Due to the new read_tree_values API, some devlib modules now use
shutils in their __init__, which happens from
Target::connect(). Therefore ensure we have busybox on the target
before we get to that stage.
2017-10-09 15:15:24 +01:00
Sergei Trofimov
1072a1a9f0 utils/ssh: fix Gem5Connection.pull
Gem5Connection lists the path to be pulled, however it was not stripping
ANSI escape sequences from resulting output, which would corrupt the
path.
2017-10-06 16:20:33 +01:00
Sergei Trofimov
661ba19114 utils/misc: strip more with strip_bash_colors
Update regex used by strip_bash_colors to match more ANSI escape
sequencies.
2017-10-06 16:17:56 +01:00
Sergei Trofimov
7e073c1fce read_tree_values: more robust parsing.
Skip entries not containing a ":".
2017-10-06 13:38:41 +01:00
setrofim
98e19ae048 Merge pull request #183 from setrofim/master
shuilt: re-introduce speedup
2017-10-05 16:55:07 +01:00
Sergei Trofimov
3e3f964e43 shuilt: re-introduce speedup
The previous fix for read_tree_values fixed the issue with sh not
supporting arrays, but looping over paths and counting them. Hover each
count increment requires spawning a subshell. For a large number of paths,
this can eat away any performance benefits of using read_tree_values.

Since we only care whether the count is greater than one, detect that
and break out of the loop early to re-introduce the performance
improvement.
2017-10-05 16:41:26 +01:00
setrofim
d1e83b53a3 Merge pull request #182 from setrofim/master
shutil: fix read_tree_values and hotplug_online_all for sh
2017-10-05 14:48:26 +01:00
Sergei Trofimov
a0b273b031 shutil: fix read_tree_values and hotplug_online_all for sh
read_tree_values and hotplug_online_all relied on () array evaluation
which is a Bash thing and is broken in sh; this fixes things for sh.
2017-10-05 14:33:06 +01:00
Sergei Trofimov
5c28e41677 shutils: fix read_tree_values
My bash fu was off on my previous list. The output of find was being
treated as a single string, rather than an array of paths; which ment
that the test that there was more than one path returned failed,
resulting in null output not just for empty directories but for
everyting.

This fixes the issue by converting find output to an array.
2017-10-05 10:59:22 +01:00
Sergei Trofimov
d560aea660 Target: fix read_tree_values for empty dir
grep was existing with 1 when passed an empty directory, resulting in
TargetError being raised unless explicitly suppressed with
check_exit_code=False. This case is now fixed to correctly return an
empty dict without error.

read_tree_values bash function has also been optimized to not
unnecessarily run find in a subshell if the path passed to the function
does not exist.
2017-10-05 09:41:40 +01:00
setrofim
4d8da589f8 Merge pull request #181 from setrofim/master
Module initialization optimizations.
2017-10-04 16:23:18 +01:00
Sergei Trofimov
f042646792 module/hotplug: optimize online_all
Optimize online_all by offloading it to a shutils function which only
requres a single call to the target.
2017-10-04 15:59:19 +01:00
Sergei Trofimov
d7ca39e4d1 module/cpuidle: optimize initialization.
Optimize cpuidle module initialization by using the new Target.grep_values
call to get information about all idle states in a single call to the
target during module intialization, rather lazily fetching them from the
target afterwards.
2017-10-04 15:59:19 +01:00
Sergei Trofimov
5a599f91db module/hwmon: optimize initialization.
Optimize hwmon module initialization by using the new Target.grep_values
call to get information about all HWMON devices in a single call to the
target.
2017-10-04 15:59:19 +01:00
Sergei Trofimov
181bc180c4 Target: add read_tree_values and read_tree_values_flat
Add two new methods to target that allow querying values of all sysfs
nodes in a sub-directory structure all at once. The difference is that
read_tree_values_flat returns a flat dict of path->value mappings;
read_tree_values returns a nested dict structure that mimics the
scanned sub-directories tree.
2017-10-04 15:58:22 +01:00
Sergei Trofimov
92fb54d57b module: nicer logger name
Use Module.name rather than Module.__name__ to name the logger for
slightly more readable log output.
2017-10-04 13:21:26 +01:00
Sergei Trofimov
bfb4721715 Target: ensure shutils is always set
Make sure shutils is always set, even if setup() has not been
called by initializing  it on first access if necessary.
2017-10-04 13:21:26 +01:00
setrofim
e21265f6f6 Merge pull request #177 from bjackman/hwmon-no-permissions
hwmon: Disable if no permissions
2017-10-02 10:37:25 +01:00
setrofim
a3f78cabc1 Merge pull request #179 from setrofim/fps
Add FPS derived metrics
2017-09-29 16:15:25 +01:00
Sergei Trofimov
4593d8605d gfxinfo fixes
- Make sure timestamps are actually reported in microseconds.
- Eliminate duplicate entries from successive dumps
2017-09-27 14:45:27 +01:00
Sergei Trofimov
9f666320f3 derived: add DerivedSurfaceFlingerStats
Add DerivedSurfaceFlingerStats that parse output from
SurfaceFlingerFramesInstrument to produce FPS data and rendering
statistics.
2017-09-27 14:45:27 +01:00
Sergei Trofimov
f692315d9c derived: add DerivedGfxInfoStats
Add DerivedGfxInfoStats that parse output from GfxInfoFramesInstrument
to produce FPS data and rendering statistics.
2017-09-27 14:45:27 +01:00
setrofim
e4fda7898d Merge pull request #178 from setrofim/master
Various fixes.
2017-09-27 14:00:26 +01:00
Sergei Trofimov
109fcc6deb AndroidTarget: fix ps()
ps() splits the output on whiestspace into fields; always expecting
nine. In some cases, wchan field may be blank, resulting in only eight
chunks after the split. Detect that case and insert and empty entry at
the appropriate index.
2017-09-27 10:44:44 +01:00
Sergei Trofimov
96693a3035 AndroidTarget: fix get_pid_of for recent Androids
ps on recent Androids no longer takes an optional comm name; use
Target.ps() instead, and filter by process name on the host.
2017-09-27 10:44:44 +01:00
Sergei Trofimov
d952abf52e utils/rendering: frame collectors should respect column order
Previously FrameCollector.write_frames used "columns" argument only as a
filter for which columns to write, but the order would always be the
same as in raw output.

The Instrument API requires that the column ordering in the resulting
MeasurementsCsv matches the ordering of channels specified in reset()
(if any). This means the collectors should respect the ordering
specified in the "columns" parameter (which gets populated based on
active channels).
2017-09-27 10:44:44 +01:00
Sergei Trofimov
50dfb297cd utils/rendering: fix surfaceflinger list
SurfaceFlingerFrameCollector.list now converts line endings before
splitting, so it now works when the endings are something other than
"\r\n".
2017-09-27 10:44:43 +01:00
setrofim
e7a319b0a7 Merge pull request #176 from bjackman/logcat-docs
utils/anroid: Documentation for LogcatMonitor
2017-09-21 14:08:33 +01:00
Brendan Jackman
6bb24aa12a hwmon: Disable if no permissions
If we don't have permissions, scan() currently raises a
TargetError. Instead we should return False from probe() so the
module is disabled
2017-09-21 13:31:59 +01:00
Brendan Jackman
fb5a260f4b utils/anroid: Documentation for LogcatMonitor 2017-09-21 13:23:10 +01:00
setrofim
e1ec1eacfb Merge pull request #175 from bjackman/logcat-race
Fix race in LogcatMonitor
2017-09-20 15:52:34 +01:00
Brendan Jackman
22c1f5e911 utils/android: Don't lock up if LogcatMonitor stopped before start
Calling stop before start will result in hanging in self._started.wait(),
because that event will never get set. Although stop before start is an illegal
usage pattern, let's try not to fail annoyingly.
2017-09-20 15:08:31 +01:00
Brendan Jackman
8cf4a44bd7 utils/android: Fix race condition in LogcatMonitor
If you call .start then immediately call .stop, the thread may not
yet have set ._logcat, resulting in an AttributeError.

I initially fixed this by setting _logcat = None in __init__, then putting the
`kill` calls inside `if self._logcat`. The problem with this, as pointed out by
@valschneider, is that we can then have this sequence:

 main thread:                          monitor thread

 stop()                                run()
   if self._logcat:                      .
     # False, don't kill process         .
   join()                                .
                                         self._logcat = <...>

Therefore, just have the stop() method wait until the process is started before
unconditionally killing it.
2017-09-20 14:06:34 +01:00
setrofim
a59093465d Merge pull request #174 from bjackman/adb-start-server
utils/android: Start ADB server before listing devices
2017-09-20 13:59:02 +01:00
Brendan Jackman
b3242a1ee4 utils/android: whitespace 2017-09-20 13:42:36 +01:00
Brendan Jackman
a290d28835 utils/android: Start ADB server before listing devices
Otherwise if the server isn't started we fail to parse the output of
'adb devices'
2017-09-20 13:04:25 +01:00
Sergei Trofimov
a8ca0fc6c8 util/rendering: add gfxinfo_get_last_dump
Add gfxinfo_get_last_dump utility function to get the last gfxinfo dump
from a (potentially large) file containing a concatenation of such dumps
(as in the raw output of the GfxinfoFrames instrument).
2017-09-19 13:34:43 +01:00
Sergei Trofimov
01b5cffe03 instrument: Update MeasurementType table
- Add generic "count" and "percent" MeasurementType's.
- Add "fps" MeasurementType.
- Add "thermal" category for "termperature"
- Add a comment describing each category
2017-09-19 13:34:43 +01:00
Sergei Trofimov
adf25f93bb DerivedMeasurements: add process_raw() + doc updates
- Add process_raw() method to the API. This is inteneded to be invoked
  on any raw output (i.e. not MeasurmentCsv) generated by an Instrument.
- Both process() process_raw() are portional to be overriden by
  impolementation; the default behavior is to return an empty list.
- The output specification for both is extened to allow
  MeasurementCsv's, as well as DerivedMetric's.
- Documentation has been reworded for clarity.
2017-09-19 13:34:43 +01:00
Sergei Trofimov
dd26b43ac5 derived: DerivedMeasurments now return DerivedMetrics
DerivedMeasurments processors now return DerviedMetrics rather than
measurments. The notion of an InstrumentChannel doesn't really make
sense in the context of DerivedMeasurments, which are not directly
measured on the target. Since Measurement's require a channel, a simpler
DerviedMetric is added that only requires a name and a type.
2017-09-19 13:34:43 +01:00
Sergei Trofimov
8479af48c4 MeasurementCsv: various enhancements
- Added values() and iter_values() methods. These return each row as a
  named tuple, with channel labels as the field names.
- __cmp__ has been made more generic by checking wether other has
  "value" attribute, rather than wether it is an instance of Measurment.
- MeasurementCsv no longer keeps an open handle to the file, and instead
  re-opens the file each time it needs it. This removes the need for
  managing the open handle, and alows parallel iterations over the
  values (each iteration will have it's own read handle into the files).
2017-09-19 13:34:43 +01:00
Sergei Trofimov
07ba177e58 InstrumentChannel: allow None sites
Allow site to be None (still must be explicitly specified). If the site
is None, the label if created using only the measurement type.
2017-09-19 13:34:43 +01:00
Sergei Trofimov
9192deb8ee InstrumentChannel: name is now an alias for label
In addition to a label constructed form the combination of site and
measurment type, channels had a name that was specified on creation.
This proven to be not particularly useful (there only being one instance
of the name being set to something materially different from the label);
and this has lead to channels being inconsistenly referenced (some times
a channel is identified by its label, and sometimes by the name).

This commit removes the name from __init__ arguments, and
InstrumentChannel.name is now an alias for InstrumentChannel.label.
2017-09-19 13:34:43 +01:00
Sergei Trofimov
823ce718bf instrument: add get_raw() API
Derived metrics may be calculated form data in raw output that is not
present in the resulting MeasurementCSV. This adds a method to provide
uniform access to raw artifacts generated by an instrument.
2017-09-19 13:34:42 +01:00
Sergei Trofimov
2afa8f86a4 insturment: add catergory for time + doc fix
- Add a category name for time MeasurmentType's, as there are now
  multiple.
- Fix the names of time_ms and time_us in the documentation.
2017-09-19 13:34:42 +01:00
Sergei Trofimov
15333eb09c utils/misc: update CPU_PART_MAP with Mongoose
Add Samsung's Mongoose M1 core part identifiers to the map.
2017-09-19 13:34:42 +01:00
Sergei Trofimov
dfd0b8ebd9 MeasurementsCsv: rename itermeasurments
Renamed to iter_measurments for readability.
2017-09-19 13:34:42 +01:00
Sergei Trofimov
ff366b3fd9 derived: renamed derived_measurments --> energy
Renamed to reduce redandancy in import path.
2017-09-19 13:34:42 +01:00
setrofim
25ad53feff Merge pull request #171 from bjackman/instrument-fix-reset
instrument: Clear up Instrument.reset semantics
2017-09-15 13:34:09 +01:00
Brendan Jackman
1513db0951 instrument: Clear up Instrument.reset semantics
- Fix missing parameter in the documentation

- Clarify meaning of `sites` and `kinds` in the documentation.

- With the current implementation the `channels` argument is
  useless: if `sites` and `kinds` are not also specified then all
  channels are enabled anyway. Fix that by making those parameters
  ignored when `channels` is provided.
2017-09-13 15:01:41 +01:00
Sergei Trofimov
90040e8b58 utils/android: grant_app_permissions fix.
Handle the case where an app does not specify any permissions.
2017-09-13 14:03:59 +01:00
setrofim
3658eec66c Merge pull request #169 from valschneider/grant-permission
utils/android: Add grant_app_permissions
2017-09-13 13:29:14 +01:00
Valentin Schneider
24d5630e54 utils/android: Add grant_app_permissions
This is mostly useful to avoid having to manually click/tap
on the permission requests that may pop up when opening apps,
which would ruin automation
2017-09-13 12:13:31 +01:00
setrofim
ee153210c6 Merge pull request #168 from valschneider/logcat-tweaks
utils/android: LogcatMonitor fixes and improvements
2017-09-12 17:41:09 +01:00
Valentin Schneider
6bda0934ad utils/android: LogcatMonitor fixes
host.kill_children() is used to properly kill the logcat process
when it is IO blocked.

The logcat regexp argument is now within double quotes, as having
parenthesis within the regexp could break the command.

LogcatMonitor.search() has been renamed to wait_for() to make the
behaviour of the method more explicit. A non-blocking version of
this method has been added and is named search().
2017-09-12 17:21:31 +01:00
setrofim
a46f1038f8 Merge pull request #167 from valschneider/kill-children
host: Add kill_children utility method
2017-09-12 15:41:18 +01:00
Valentin Schneider
4de973483e host: Add kill_children utility method
This method is useful for killing the children spawned by
Popen() calls with shell=True. For instance:

proc = Popen('sleep 100', shell=True)
proc.kill()

This would spawn a shell task and that shell would spawn the sleep task.
Issuing a kill to the Popen handle will only kill the shell task,
and the sleep task will keep running.

Using host.kill_children(proc.pid) will ensure all child tasks are
killed.
2017-09-12 15:14:23 +01:00
setrofim
0e9221f58e Merge pull request #166 from valschneider/logcat-monitor
Logcat monitor
2017-09-11 18:41:02 +01:00
Valentin Schneider
0d3a0223b3 trace: Add logcat trace collector 2017-09-11 17:53:09 +01:00
Valentin Schneider
7c2fd87a3b target: Add LogcatMonitor getter for android target 2017-09-11 17:15:04 +01:00
Valentin Schneider
035181a8f1 utils/android: Add LogcatMonitor 2017-09-11 17:12:11 +01:00
setrofim
f5a00140e4 Merge pull request #161 from qperret/gem5/stats/match-regex
module/gem5stats: enhance match() with regex support
2017-08-23 17:27:46 +01:00
setrofim
77482a6c70 Merge pull request #159 from qperret/fix/cgroup-freeze
module/cgroups: robustify task freezer
2017-08-22 18:33:40 +01:00
Quentin Perret
34d73e6af1 utils/gem5: try to cast statistics dump values
All values in the gem5 statistics log file are numeric. This commit adds a
cast on the strings read from the stats file to native numeric values when
and logs a warning in case of a malformed entry.
2017-08-22 16:16:09 +01:00
Quentin Perret
4b36439de8 module/cgroups: robustify task freezer
The set() method of the CGroup class used to freeze tasks relies on
target's write_value(). Sometimes, the freezing procedure takes some
time and the call to write_value() in set() fails by reading "FREEZING"
while it expected "FROZEN". To avoid this issue, this commits introduces
a shutil call dedicated to changing the state of the freezer controller.
2017-08-21 18:16:10 +01:00
Quentin Perret
3c8294c6eb module/gem5stats: better document match() and match_iter() behaviours 2017-08-21 11:12:11 +01:00
Quentin Perret
64c865de59 module: gem5stats: enhance match() with regex support
The current implementation of match() in the gem5stats module returns
records matching exactly the specified keys. This commit changes this
behaviour by matching keys over regular expressions, hence resulting in
a much more powerful match() implementation.
2017-08-21 11:12:11 +01:00
setrofim
66a50a2f49 Merge pull request #163 from marcbonnici/Derived_Measurements
Add support for AcmeCape and Derived Measurements
2017-08-21 08:46:43 +01:00
Marc Bonnici
60e69fc4e8 Documentation/DerivedMeasurements: Added documentation for new API 2017-08-18 18:02:48 +01:00
Marc Bonnici
c62905cfdc Instrumentation/Instrumentation: Update timestamp documentation 2017-08-18 18:02:48 +01:00
Marc Bonnici
eeb5e93e6f Documentation/Instrumentation: Fix typos 2017-08-18 18:02:48 +01:00
Marc Bonnici
c093d56754 DerivedMeasurements: Add DerivedEnergyMeasurments
Adds `DerivedMeasurements` which are designed to perform post processing on
a provided MeasurementCsv.
Currently only a `DerivedEnergyMeasurements` class has been added which
has 2 purposes:
- Calculate energy from power results if not present using recorded timestamps,
  falling back to a provided sample rate
- Calculate cumulative energy and average power from a specified MeasurementCSV
  file.
2017-08-18 18:02:48 +01:00
Marc Bonnici
049b275665 Instrumentation: Update to store sample rate in MeasurementCSV file.
This commit updates existing instruments to store their sample rates
when creating the respective MeasurementCsv files.
2017-08-18 18:02:48 +01:00
Marc Bonnici
411719d58d Instrument/gem5power: Rename timestamp channel to match new API
To conform with the new DerivedMeasuements API the "sim_seconds" channel
has been renamed to "timestamp" so that it can be identified in post
processing. As "sim_seconds" needs to be extracted from the gem5
statistics file, an addional mapping has been added to support this.
2017-08-18 18:02:48 +01:00
Marc Bonnici
7dd934a5d8 Instrumentation: Update to populate missing 'sample_rate_hz` attributes
To conform with the Instrumentation API each instrument should populate
the `sample_rate_hz` attribute with the relevant information.
2017-08-18 17:10:17 +01:00
Marc Bonnici
30fdfc23d3 Instrument/Acmecape: Add support for acmecape 2017-08-18 13:33:49 +01:00
Marc Bonnici
d3c3015fc8 Instrument/MeasurementCSV: Add support for recording sample rate.
If performing post processing on a MeasurementCsv file, if a timestamp
is not available then the recorded sample rate can be used as a
substitute.
2017-08-18 13:32:23 +01:00
Marc Bonnici
5ef99f2cff Instrument/MeasurementType: Allow for converting to the same type
When trying to convert measurments to a standarised type some inputs
may already be of the correct type and will now return the same value
unchanged.
2017-08-18 13:32:19 +01:00
Marc Bonnici
9b465c2766 Instruments: Add millisecond MeasurementType and conversion
Allows for reporting times in milliseconds as used with the acmecape
instrument.
2017-08-18 13:32:16 +01:00
Marc Bonnici
2de2b36387 Instrumentation: Fix conversion between microseconds and seconds 2017-08-18 13:32:10 +01:00
marcbonnici
210712b384 Merge pull request #162 from setrofim/numeric-pcent
utils/types: numeric expeanded to handle percentages
2017-08-17 11:17:56 +01:00
setrofim
2a0d110012 Merge pull request #157 from marcbonnici/airplane/brightness
Adds additional Android methods
2017-08-17 10:59:27 +01:00
Marc Bonnici
5b99c1613b Documentation: Adds documentation for Android Target
Adds documentation for the `AndroidTarget` class including the new
android methods.
2017-08-17 10:08:08 +01:00
Sergei Trofimov
3d10e3eae9 utils/types: numeric expeanded to handle percentages
numeric() conversion function is expanded to handle percentage values in
the form "10.123%". The result is the numeric part converted to a float
and divided by 100, e.g. 0.10123.
2017-08-16 16:00:38 +01:00
setrofim
4d95656e49 Merge pull request #160 from qperret/gem5/stats/async-origin
module/gem5stats: enable asynchronous origins in gem5 stats file
2017-08-16 13:39:31 +01:00
Quentin Perret
38258eb74c module: gem5stats: enable asynchronous origins in gem5 stats file
Currently, reset_origin() in the gem5stats module enables to virtually
truncate the existing dumps from the gem5 statistics file so that any
subsequent match() calls will apply only on new dumps. However, the
current implementation resets the origin of the stats file unilaterally,
without other clients knowing about it, hence leading to data being
lost.

This commits removes the reset_origin() method and provides a different
API to handle virtual truncatures in the stats file. Namely, the match()
method now takes a base_dump parameter letting clients specifiying from
which dump they want match() to apply. This feature could also be usefull
if someone wanted to add off-line processing features for statistics
files.
2017-08-16 10:40:10 +01:00
Marc Bonnici
8839ed01ba AndroidTarget: Changes default unlocking method
Instead of using a default 'horizontal' unlocking method, now will try
a diagonal swipe up as this should work most modern devices with vertical or
horizontal unlocking methods.
2017-08-14 09:55:21 +01:00
Marc Bonnici
1229af0895 AndroidTarget: Fixes 'swipe_to_unlock' method. 2017-08-11 14:22:28 +01:00
Marc Bonnici
003785dde1 AndroidTarget: Adds methods to get/set screen rotation 2017-08-10 11:21:04 +01:00
Marc Bonnici
3e751746d6 AndroidTarget: Adds methods to get/set airplane mode
In order to change the state of airplane mode, the setting needs to
be configured before broadcasting an intent to update the system. As of
Android N, root is required to send the broadcast, therefore this method
will raise an error if the requirements are not satisfied.
2017-08-10 11:21:03 +01:00
Marc Bonnici
ddd2e29b87 AndroidTarget: Adds methods to get/set screen brightness 2017-08-10 11:21:03 +01:00
marcbonnici
22b6514c35 Merge pull request #158 from bjackman/ssh-improve-errors
Improve SshConnection.push errors
2017-08-09 16:18:46 +01:00
Brendan Jackman
380ad0515d ssh: Improve error when failing to push file
CalledProcessError doesn't include the output of the failed command
in the message it prints, so use a HostError isntead.
2017-08-09 16:01:49 +01:00
Brendan Jackman
93b39a7f47 ssh: Fix redacting SSH password in errors
IIUC this is supposed to redact the password from exception messages,
but 'pass_string' is not set to the password so nothing is
altered. Fix that.
2017-08-09 16:01:49 +01:00
Basil Eljuse
70d755d75b Mark inline function to be static to avoid link issues with GCC compiler 2017-08-01 08:28:47 +01:00
setrofim
1da8d3f95f Merge pull request #151 from AnthonyARM/master
Add support for arbitrary ADB servers
2017-07-31 15:51:15 +01:00
setrofim
7f347e9d71 Merge pull request #155 from ionela-voinescu/te_missing
gem5stats: fix missing import for TargetError
2017-07-27 11:09:55 +01:00
Ionela Voinescu
d25beb5c8b gem5stats: fix missing import for TargetError
Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
2017-07-27 11:07:05 +01:00
marcbonnici
edf200dbc9 Merge pull request #154 from setrofim/master
Further cpufreq updates.
2017-07-26 15:05:38 +01:00
Sergei Trofimov
63e60401d5 module/cpufreq: rename get_domain_cpus
Rename get_domain_cpus to get_related_cpus to match cpufreq nomenclature.
2017-07-26 14:23:34 +01:00
Sergei Trofimov
e206e9b24a module/cpufreq: fix get_affected_cpus docstring
The docstring was duplicated from get_domain_cpus. This updates the
docstring to reflect the difference from get_domain_cpus.
2017-07-26 14:23:34 +01:00
marcbonnici
0844a393ab Merge pull request #153 from setrofim/master
module/cpufreq: fix domain cpus.
2017-07-26 14:09:13 +01:00
Sergei Trofimov
36aa3af66d module/cpufreq: fix domain cpus.
- Use related_cpus rather than affected_cpus inside get_domain_cpus to
  return all cpus in the domain (including those online). This changes
  the behavior, but old behavior was almost certainly wrong as the
  method is memoized, and so the result should not be affected by
  hotplug.
- Add a non-memoized get_affected_cpus() which implements the old
  behavior.
2017-07-26 14:02:59 +01:00
Anthony Barbier
b392a0a1b4 Use related_cpus instead of affected_cpus
related_cpus doesn't rely on the related cores to be online to work cf https://www.kernel.org/doc/Documentation/cpu-freq/user-guide.txt
2017-07-26 14:00:38 +01:00
setrofim
9f74b9978c Merge pull request #152 from valschneider/thermal-zone-fix
devlib/module/thermal: Fix thermal zone disabling
2017-07-26 11:53:52 +01:00
Valentin Schneider
b54dc19b81 devlib/module/thermal: Fix thermal zone disabling
Calling thermal.disable_all_zones() would raise an exception.
I've changed a few things:
* use self.zones.itervalues() in disable_all_zones to fix that exception
* renamed zone.set_mode() to zone.set_enabled()
2017-07-26 11:39:49 +01:00
Anthony Barbier
7919a5643c Add support for arbitrary ADB servers 2017-07-25 13:48:33 +01:00
setrofim
df4d06bc7f Merge pull request #150 from marcbonnici/apkinfo
Apkinfo
2017-07-20 14:29:21 +01:00
Marc Bonnici
02c93b48ab ABI_MAP: Adds 'armeabi-v7a' to mapping 2017-07-20 14:24:07 +01:00
Marc Bonnici
98fb2e2306 Target: Ported supported_abi property from WA2 2017-07-20 14:24:07 +01:00
Marc Bonnici
68be9d8acc utils/android: Added native code extraction of apks from WA2 2017-07-14 17:20:24 +01:00
setrofim
09f69dcf38 Merge pull request #148 from marcbonnici/Documentation
Documentation: Corrected typos
2017-07-13 08:07:05 +01:00
Marc Bonnici
1fd5636217 Documentation: Corrected typos 2017-07-12 16:54:39 +01:00
marcbonnici
6bc3479abb Merge pull request #147 from setrofim/master
utils/android: better error message when exit code not detected
2017-07-12 16:25:53 +01:00
Sergei Trofimov
85036fbb30 utils/android: better error message when exit code not detected
Include the stdout/stderr output in the message of the exception raised
when the exit code of the adb command cannot be detected.
2017-07-12 13:46:19 +01:00
setrofim
b933dbda67 Merge pull request #146 from derkling/adb_addons
Add a couple of ADB related utility functions
2017-07-12 13:20:53 +01:00
Patrick Bellasi
0c7eb9e91e target: add support to kill/restart and ADB connection
Sometimes it could be useful to disconnect from the target, for example
when a "pass thought" energy monitor is used to control the USB
connection to the device.

This patch adds a couple of utility methods which allows to kill the ADB
server and restart it while waiting for the device to be ready to accept
new connections.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2017-07-12 12:30:30 +01:00
Patrick Bellasi
f26f942723 tagert: factor out the wait_boot_complete code
The connect() method embeds some code to wait for a target to be
completely booted.

Let's move this code into a dedicated function.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2017-07-12 12:27:28 +01:00
setrofim
b062097221 Merge pull request #145 from ionela-voinescu/fix_se
misc.py: fix syntax error introduced by 86c9b6a1c7
2017-07-10 16:09:24 +01:00
Ionela Voinescu
0a95bbed87 misc.py: fix syntax error introduced by 86c9b6a1c7
Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
2017-07-10 16:07:28 +01:00
marcbonnici
85f30ed4c7 Merge pull request #144 from setrofim/master
utils/misc: update MIDR table
2017-07-10 15:08:36 +01:00
Sergei Trofimov
86c9b6a1c7 utils/misc: update MIDR table
Update the MIDR CPU part number table with values from
https://github.com/torvalds/linux/blob/master/arch/arm64/include/asm/cputype.h
2017-07-10 15:03:59 +01:00
setrofim
3f1a1c4086 Merge pull request #134 from qperret/gem5stats-instru
instrument: Add power monitoring support on Gem5 platforms
2017-07-10 12:59:37 +01:00
Quentin Perret
7a827e2b11 instrument: Add power monitoring support on Gem5 platforms 2017-07-10 09:49:39 +01:00
Quentin Perret
ce48ad217d module: gem5stats: add an iterator to match records 2017-07-10 09:49:33 +01:00
setrofim
f9bc6966c0 Merge pull request #142 from qperret/gem5stats-module
module: Control and parse gem5's statistics log file
2017-07-07 11:44:02 +01:00
Quentin Perret
baedd676a9 module: Control and parse gem5's statistics log file
Gem5's statistics log file contains plenty of interesting information
that are not exposed so far. This module enables control and parsing of
the statistics file by:
 - configuring periodic dumps of statistics;
 - marking Regions of Interest (ROIs);
 - and extracting values of specific fields during the ROIs.
2017-07-07 11:33:28 +01:00
setrofim
8f63914b85 Merge pull request #143 from valschneider/invoke_bg
Add option to run target.invoke() in the background
2017-07-06 16:08:12 +01:00
Valentin Schneider
c8af995392 doc/target: Add background_invoke() documentation 2017-07-06 16:01:30 +01:00
Valentin Schneider
92b0c25ed3 target: Add background_invoke() method 2017-07-06 16:01:15 +01:00
setrofim
fb58e47cf5 Merge pull request #141 from AnthonyARM/pull/big_little
Check if any big/LITTLE core is online before dereferencing a potentially empty list
2017-06-30 13:20:44 +01:00
Anthony Barbier
3660361df0 Raise a ValueError when trying to set a property of a type of core which is offline 2017-06-30 13:15:23 +01:00
setrofim
86c6a1a826 Merge pull request #139 from AnthonyARM/pull/sudo
Let the user customize the way to execute superuser commands
2017-06-28 17:23:25 +01:00
Anthony Barbier
1199f2512b Check if any big/LITTLE core is online before dereferencing an potentially empty list 2017-06-28 17:15:05 +01:00
Anthony Barbier
c837a29299 Let the user customize the way to execute superuser commands 2017-06-28 15:08:35 +01:00
setrofim
de15658025 Merge pull request #138 from marcbonnici/apk_install
Ports `install_apk` and `get_sdk_version` from WA2
2017-06-23 16:09:36 +01:00
Marc Bonnici
d3396f2725 Target: Added get_sdk_version method
Added the method to retrieve a devices sdk version taken from WA2.
2017-06-23 15:33:57 +01:00
Marc Bonnici
c33dd65249 Target: Updated install_apk to version from WA2
This version of the method adds support for both replacing and downgrading of
APKS.
2017-06-23 15:33:42 +01:00
setrofim
7145b366ab Merge pull request #133 from valschneider/fix-activity-error
utils/android: Fix error detection in adb_shell()
2017-06-13 08:57:56 +01:00
Valentin Schneider
2d96840873 utils/android: Fix error detection in adb_shell()
If we execute a command such as:
'am start -a android.intent.action.VIEW -d nothing'
on Hikey960 with AOSP master, we do get an error message:
'Error: Activity not started, unable to resolve Intent ...'
but the error code is set to 0. Furthermore, that error message
is outputted to STDERR and not STDOUT.

The current error checking in adb_shell() is done by either
reading the error code or finding an error string in the
standard output. This commit adds a check of the error output,
and also changes the regular expression that is used to find
an error string as it wasn't generic enough.
2017-06-12 19:08:10 +01:00
marcbonnici
48d717b301 Merge pull request #132 from setrofim/fps
Fps
2017-06-07 11:59:32 +01:00
Sergei Trofimov
1d3b4c8062 instrument/frames: Add surfaceflinger frames instrument
Add an instrument that utilizes the SurfaceFringerFrameCollector.
2017-06-07 11:53:57 +01:00
Sergei Trofimov
2b3cee6a7e rendering: Add surfaceflinger frame collector
Add a FrameCollector implementation that utilizes the output from
surfaceflinger (the Android compositor). This is is the only method for
accessing rendering data on older Android devices.
2017-06-07 11:53:57 +01:00
Sergei Trofimov
4adefecb55 instrument/frames: Add GfxInfoFramesInstrument
Added an instrument that exposes the GfxinfoFramesCollector via the
standard instrument APIs.
2017-06-07 11:53:57 +01:00
Sergei Trofimov
195085e28d rendering: Add gfxinfo frame data collector
Add display rendering frame collector that utilizes gfxinfo utility
present on newer Android devices.
2017-06-07 11:53:52 +01:00
Sergei Trofimov
59f36fc768 instrument: Add channel discovery to MeasurementCsv
- Add "unknown" measurement type to be used when the type of measurement
  for a CSV column cannot be established.
- Make channels argument optional for MeasurementCsv.
- If channels for a MeasurementCsv have not been specified, attempt to
  discover them from the CSV header. This will  check if each entry in
  the header ends with the name of a known MeasurementType prefixed with
  a "_"; if so, it will assume the reset of the entry is the site.
  Otherwise, the entire entry will be assumed to be the site and
  "unknown" MeasurementType will be used.
2017-06-07 11:53:06 +01:00
Sergei Trofimov
871c59a3f4 instrument: Add MeasurementType for time_us
Add time_us MeasurementType for time expressed in microseconds and
define conversions to/form time. This is a very common unit for low-level
operations, and it may be desirable to avoid converting large number of
metrics to floats in order to report as a "standard" type.
2017-06-07 11:53:06 +01:00
Sergei Trofimov
df81742100 instrument: add conversion support to MeasurementType
- Change MeasurementType to derive from object rather than tuple.
- There is now support for conversion from one MeasurementType to
  another. A MeasurementType defines what it can be converted to and
  how.
2017-06-07 11:53:06 +01:00
Sergei Trofimov
8296d6c5d6 exception: Add WorkerErrorException and get_traceback
- Add get_traceback -- a utility for getting the traceback for
  the current exception.
- Add WorkerErrorException that is used to capture and propagate
  exceptions from a  worker thread on the main thread.
2017-06-07 11:52:58 +01:00
setrofim
a900f94069 Merge pull request #131 from bjackman/cpufreq-error-message
cpufreq: Add possible error message for failing to set governor
2017-06-06 15:01:31 +01:00
Brendan Jackman
0cac92af27 cpufreq: Add possible error message for failing to set governor
I have observerd the error message "write error: Invalid argument"
when using set_all_governors to set a non-existent governor on a
buildroot Linux system
2017-06-06 14:56:57 +01:00
setrofim
2e106c9f70 Merge pull request #130 from Leo-Yan/fix_energy_probe_signal_2
energy_probe: send signal SIGINT to caiman
2017-06-05 13:24:19 +01:00
Leo Yan
a48775ec5a energy_probe: send signal SIGINT to caiman
ARM energy probe sends SIGTERM to caiman, this signal can be missed by
caiman due caiman has not registered signal handler for SIGTERM. On the
other hand, caiman registers signal handler for SIGINT, this patch is to
change to use signal SIGINT to notify caiman.

Signed-off-by: Leo Yan <leo.yan@linaro.org>
2017-06-05 19:14:19 +08:00
setrofim
d92b18c102 Merge pull request #129 from ARM-software/revert-127-dry-run
Revert "target: add dry run for command execution"
2017-05-23 17:40:06 +01:00
setrofim
b738655050 Revert "target: add dry run for command execution" 2017-05-23 17:39:12 +01:00
setrofim
597231f3d5 Merge pull request #128 from ionela-voinescu/gem5_pull_multiple_files
gem5: modify pull method to support transfer of multiple files
2017-05-23 17:37:20 +01:00
Ionela Voinescu
cac70cba19 gem5: modify pull method to support transfer of multiple files
When providing a source file with wildcards (e.g. *ramp*.log)
to be pulled from the target, this might involve transferring
multiple files.
Add support for the transfer of multiple files in the pull method.

Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
2017-05-23 16:53:18 +01:00
setrofim
1198e42cdf Merge pull request #127 from valschneider/dry-run
target: add dry run for command execution
2017-05-23 10:40:18 +01:00
Valentin Schneider
e11573594a doc/target: Add dry-run documentation 2017-05-23 10:35:53 +01:00
Valentin Schneider
1a5c1dce07 target: Add dry run for command execution
It sometimes can be useful to enable dry-run before calling
high-level functions in order to check what individual commands
are being called (or do whatever else with them).

This patch adds dry-run with an on/off switch:
once start_dryrun() is called, every subsequent command passed to
execute() will be accumulated in a list.
stop_dryrun() disables dry-run, and the accumulated commands can
be fetched via Target.dryrun
2017-05-23 10:32:11 +01:00
Sergei Trofimov
1f7421bc39 setup.py: Add monsoon's dependencies to extras_require 2017-05-22 13:54:49 +01:00
setrofim
64292ad6b4 Merge pull request #126 from bjackman/fix-monsoon-instructions
monsoon: Fix name of gflags pip package in install instructions
2017-05-22 13:52:33 +01:00
Brendan Jackman
3dbd3f7fda monsoon: Fix name of gflags pip package in install instructions 2017-05-22 13:48:47 +01:00
setrofim
4a936da62f Merge pull request #125 from avanlaer/pull_directory
Pull directory
2017-05-18 08:16:44 +01:00
Anouk Van Laer
0b7ab6aa94 target: Added get_directory method
This new method allows to pull over a complete directory from the target.
It does so by compressing the directory, pulling over the
compressed file and extracting the directory on the host.
2017-05-17 17:39:00 +01:00
setrofim
4e0c03ebdd Merge pull request #124 from bjackman/cpufreq-iter-domains
Add iter_domains method to cpufreq module
2017-05-17 16:51:19 +01:00
Brendan Jackman
dc32fa9704 cpufreq: Add iter_domains method
It's common to want to do an operation on cpufreq that affects all
CPUs, but doing so explicitly for all CPUs can be unnecessarily slow
when it only needs to be done once for each cpufreq policy.

Add a method to abstract the pattern of iteration.
2017-05-17 16:37:27 +01:00
Anouk Van Laer
35987d5281 Gem5Connection: Correct pull method
The pull method used when connecting to gem5 uses the 'm5 writefile'.
This only works if the file to pulled in, is in the current working directory
on the target. The file therefore might need to be copied on the target, from its
original location to the working directory.
The previous implemention of this was incorrect and used information about the
current working directory on the host.
2017-05-17 15:49:20 +01:00
Sergei Trofimov
69a83d4128 target: add new methods
Added the following method to targets:

- sleep: sleep on target for the specified duration. In some situations,
         e.g. on simulation platforms, it is important that sleep
	 happens on the target rather than the host.

Added the following methods to Android targets:

- ensure_screen_is_off: complements the existing ensure_screen_is_on.
- homescreen: navigate to home screen.
2017-05-12 11:54:31 +01:00
Sergei Trofimov
8b2ac8d29d AndroidTarget: move _execute_util to internal methods. 2017-05-12 11:48:19 +01:00
Sergei Trofimov
97a89970d0 android: downgrading ls command log to debug
The format of an ls command is an implementation detail rather than
something of immediate interest to the user. Debug level is more
appropriate than info for this kind of message.
2017-05-12 10:45:42 +01:00
Sergei Trofimov
92d9e690f0 ftrace: add an option to report on target
trace-cmd is a Linux-specific executable that is not available on other
operating systems (even on other Unixes). The option to report on target
allows acquiring text trace even if the host is not running Linux.
2017-05-12 09:14:40 +01:00
Sergei Trofimov
61b13383a4 ftrace: chmod trace.dat to make world-readable
On some targets, umask is set such that the generated trace.dat is not
world-readable by default. This means it cannot be pulled from target.
2017-05-12 09:14:36 +01:00
Sergei Trofimov
7e80a381d8 ftrace: fix get_trace when outfile is directory
If the path passed into get_trace() is a directory, the collector is
supposed to use the name of the output file on target for the file on
the host. Until now however, os.path.dirname() was mistakenly called on
the target location (returning the containing directory rather than the
base name of the file).
2017-05-12 09:14:35 +01:00
setrofim
4dc54728c1 Merge pull request #122 from Sticklyman1936/propagate_check_error_code
Propagate check_error_code
2017-05-11 10:37:10 +01:00
Sascha Bischoff
f5906cb4ab Gem5Connection: Propagate the check_error_code flag
Previously, the flag was not propagated, so the error code was always
checked. With this change, we propage it to the _gem5_shell method.
2017-05-11 10:34:24 +01:00
65 changed files with 6069 additions and 689 deletions

View File

@@ -17,7 +17,7 @@ Installation
Usage Usage
----- -----
Please refer to the "Overview" section of the documentation. Please refer to the "Overview" section of the `documentation <http://devlib.readthedocs.io/en/latest/>`_.
License License

View File

@@ -1,4 +1,19 @@
from devlib.target import Target, LinuxTarget, AndroidTarget, LocalLinuxTarget # Copyright 2018 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 devlib.target import Target, LinuxTarget, AndroidTarget, LocalLinuxTarget, ChromeOsTarget
from devlib.host import PACKAGE_BIN_DIRECTORY from devlib.host import PACKAGE_BIN_DIRECTORY
from devlib.exception import DevlibError, TargetError, HostError, TargetNotRespondingError from devlib.exception import DevlibError, TargetError, HostError, TargetNotRespondingError
@@ -13,12 +28,31 @@ from devlib.instrument import Instrument, InstrumentChannel, Measurement, Measur
from devlib.instrument import MEASUREMENT_TYPES, INSTANTANEOUS, CONTINUOUS from devlib.instrument import MEASUREMENT_TYPES, INSTANTANEOUS, CONTINUOUS
from devlib.instrument.daq import DaqInstrument from devlib.instrument.daq import DaqInstrument
from devlib.instrument.energy_probe import EnergyProbeInstrument from devlib.instrument.energy_probe import EnergyProbeInstrument
from devlib.instrument.arm_energy_probe import ArmEnergyProbeInstrument
from devlib.instrument.frames import GfxInfoFramesInstrument, SurfaceFlingerFramesInstrument
from devlib.instrument.hwmon import HwmonInstrument from devlib.instrument.hwmon import HwmonInstrument
from devlib.instrument.monsoon import MonsoonInstrument from devlib.instrument.monsoon import MonsoonInstrument
from devlib.instrument.netstats import NetstatsInstrument from devlib.instrument.netstats import NetstatsInstrument
from devlib.instrument.gem5power import Gem5PowerInstrument
from devlib.derived import DerivedMeasurements, DerivedMetric
from devlib.derived.energy import DerivedEnergyMeasurements
from devlib.derived.fps import DerivedGfxInfoStats, DerivedSurfaceFlingerStats
from devlib.trace.ftrace import FtraceCollector from devlib.trace.ftrace import FtraceCollector
from devlib.trace.serial_trace import SerialTraceCollector
from devlib.host import LocalConnection from devlib.host import LocalConnection
from devlib.utils.android import AdbConnection from devlib.utils.android import AdbConnection
from devlib.utils.ssh import SshConnection, TelnetConnection, Gem5Connection from devlib.utils.ssh import SshConnection, TelnetConnection, Gem5Connection
from devlib.utils.version import get_commit as __get_commit
__version__ = '1.0.0'
__commit = __get_commit()
if __commit:
__full_version__ = '{}-{}'.format(__version__, __commit)
else:
__full_version__ = __version__

Binary file not shown.

View File

@@ -47,6 +47,37 @@ cpufreq_trace_all_frequencies() {
done done
} }
################################################################################
# DevFrequency Utility Functions
################################################################################
devfreq_set_all_frequencies() {
FREQ=$1
for DEV in /sys/class/devfreq/*; do
echo $FREQ > $DEV/min_freq
echo $FREQ > $DEV/max_freq
done
}
devfreq_get_all_frequencies() {
for DEV in /sys/class/devfreq/*; do
echo "`basename $DEV` `cat $DEV/cur_freq`"
done
}
devfreq_set_all_governors() {
GOV=$1
for DEV in /sys/class/devfreq/*; do
echo $GOV > $DEV/governor
done
}
devfreq_get_all_governors() {
for DEV in /sys/class/devfreq/*; do
echo "`basename $DEV` `cat $DEV/governor`"
done
}
################################################################################ ################################################################################
# CPUIdle Utility Functions # CPUIdle Utility Functions
################################################################################ ################################################################################
@@ -124,14 +155,14 @@ cgroups_run_into() {
# Check if the required CGroup exists # Check if the required CGroup exists
$FIND $CGMOUNT -type d -mindepth 1 | \ $FIND $CGMOUNT -type d -mindepth 1 | \
$GREP "$CGP" &>/dev/null $GREP -E "^$CGMOUNT/devlib_cgh[0-9]{1,2}$CGP" &>/dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "ERROR: could not find any $CGP cgroup under $CGMOUNT" echo "ERROR: could not find any $CGP cgroup under $CGMOUNT"
exit 1 exit 1
fi fi
$FIND $CGMOUNT -type d -mindepth 1 | \ $FIND $CGMOUNT -type d -mindepth 1 | \
$GREP "$CGP" | \ $GREP -E "^$CGMOUNT/devlib_cgh[0-9]{1,2}$CGP$" | \
while read CGPATH; do while read CGPATH; do
# Move this shell into that control group # Move this shell into that control group
echo $$ > $CGPATH/cgroup.procs echo $$ > $CGPATH/cgroup.procs
@@ -177,6 +208,61 @@ cgroups_tasks_in() {
exit 0 exit 0
} }
cgroups_freezer_set_state() {
STATE=${1}
SYSFS_ENTRY=${2}/freezer.state
# Set the state of the freezer
echo $STATE > $SYSFS_ENTRY
# And check it applied cleanly
for i in `seq 1 10`; do
[ $($CAT $SYSFS_ENTRY) = $STATE ] && exit 0
sleep 1
done
# We have an issue
echo "ERROR: Freezer stalled while changing state to \"$STATE\"." >&2
exit 1
}
################################################################################
# Hotplug
################################################################################
hotplug_online_all() {
for path in /sys/devices/system/cpu/cpu[0-9]*; do
if [ $(cat $path/online) -eq 0 ]; then
echo 1 > $path/online
fi
done
}
################################################################################
# Misc
################################################################################
read_tree_values() {
BASEPATH=$1
MAXDEPTH=$2
if [ ! -e $BASEPATH ]; then
echo "ERROR: $BASEPATH does not exist"
exit 1
fi
PATHS=$($BUSYBOX find $BASEPATH -follow -maxdepth $MAXDEPTH)
i=0
for path in $PATHS; do
i=$(expr $i + 1)
if [ $i -gt 1 ]; then
break;
fi
done
if [ $i -gt 1 ]; then
$BUSYBOX grep -s '' $PATHS
fi
}
################################################################################ ################################################################################
# Main Function Dispatcher # Main Function Dispatcher
@@ -198,6 +284,18 @@ cpufreq_get_all_governors)
cpufreq_trace_all_frequencies) cpufreq_trace_all_frequencies)
cpufreq_trace_all_frequencies $* cpufreq_trace_all_frequencies $*
;; ;;
devfreq_set_all_frequencies)
devfreq_set_all_frequencies $*
;;
devfreq_get_all_frequencies)
devfreq_get_all_frequencies
;;
devfreq_set_all_governors)
devfreq_set_all_governors $*
;;
devfreq_get_all_governors)
devfreq_get_all_governors
;;
cpuidle_wake_all_cpus) cpuidle_wake_all_cpus)
cpuidle_wake_all_cpus $* cpuidle_wake_all_cpus $*
;; ;;
@@ -213,9 +311,18 @@ cgroups_tasks_move)
cgroups_tasks_in) cgroups_tasks_in)
cgroups_tasks_in $* cgroups_tasks_in $*
;; ;;
cgroups_freezer_set_state)
cgroups_freezer_set_state $*
;;
ftrace_get_function_stats) ftrace_get_function_stats)
ftrace_get_function_stats ftrace_get_function_stats
;; ;;
hotplug_online_all)
hotplug_online_all
;;
read_tree_values)
read_tree_values $*
;;
*) *)
echo "Command [$CMD] not supported" echo "Command [$CMD] not supported"
exit -1 exit -1

BIN
devlib/bin/x86_64/trace-cmd Executable file

Binary file not shown.

View File

@@ -0,0 +1,60 @@
# Copyright 2015-2017 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from devlib.instrument import MeasurementType, MEASUREMENT_TYPES
class DerivedMetric(object):
__slots__ = ['name', 'value', 'measurement_type']
@property
def units(self):
return self.measurement_type.units
def __init__(self, name, value, measurement_type):
self.name = name
self.value = value
if isinstance(measurement_type, MeasurementType):
self.measurement_type = measurement_type
else:
try:
self.measurement_type = MEASUREMENT_TYPES[measurement_type]
except KeyError:
msg = 'Unknown measurement type: {}'
raise ValueError(msg.format(measurement_type))
def __cmp__(self, other):
if hasattr(other, 'value'):
return cmp(self.value, other.value)
else:
return cmp(self.value, other)
def __str__(self):
if self.units:
return '{}: {} {}'.format(self.name, self.value, self.units)
else:
return '{}: {}'.format(self.name, self.value)
__repr__ = __str__
class DerivedMeasurements(object):
def process(self, measurements_csv):
return []
def process_raw(self, *args):
return []

97
devlib/derived/energy.py Normal file
View File

@@ -0,0 +1,97 @@
# Copyright 2013-2018 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 __future__ import division
from collections import defaultdict
from devlib import DerivedMeasurements, DerivedMetric
from devlib.instrument import MEASUREMENT_TYPES, InstrumentChannel
class DerivedEnergyMeasurements(DerivedMeasurements):
@staticmethod
def process(measurements_csv):
should_calculate_energy = []
use_timestamp = False
# Determine sites to calculate energy for
channel_map = defaultdict(list)
for channel in measurements_csv.channels:
channel_map[channel.site].append(channel.kind)
if channel.site == 'timestamp':
use_timestamp = True
time_measurment = channel.measurement_type
for site, kinds in channel_map.items():
if 'power' in kinds and not 'energy' in kinds:
should_calculate_energy.append(site)
if measurements_csv.sample_rate_hz is None and not use_timestamp:
msg = 'Timestamp data is unavailable, please provide a sample rate'
raise ValueError(msg)
if use_timestamp:
# Find index of timestamp column
ts_index = [i for i, chan in enumerate(measurements_csv.channels)
if chan.site == 'timestamp']
if len(ts_index) > 1:
raise ValueError('Multiple timestamps detected')
ts_index = ts_index[0]
row_ts = 0
last_ts = 0
energy_results = defaultdict(dict)
power_results = defaultdict(float)
# Process data
for count, row in enumerate(measurements_csv.iter_measurements()):
if use_timestamp:
last_ts = row_ts
row_ts = time_measurment.convert(float(row[ts_index].value), 'time')
for entry in row:
channel = entry.channel
site = channel.site
if channel.kind == 'energy':
if count == 0:
energy_results[site]['start'] = entry.value
else:
energy_results[site]['end'] = entry.value
if channel.kind == 'power':
power_results[site] += entry.value
if site in should_calculate_energy:
if count == 0:
energy_results[site]['start'] = 0
energy_results[site]['end'] = 0
elif use_timestamp:
energy_results[site]['end'] += entry.value * (row_ts - last_ts)
else:
energy_results[site]['end'] += entry.value * (1 /
measurements_csv.sample_rate_hz)
# Calculate final measurements
derived_measurements = []
for site in energy_results:
total_energy = energy_results[site]['end'] - energy_results[site]['start']
name = '{}_total_energy'.format(site)
derived_measurements.append(DerivedMetric(name, total_energy, MEASUREMENT_TYPES['energy']))
for site in power_results:
power = power_results[site] / (count + 1) #pylint: disable=undefined-loop-variable
name = '{}_average_power'.format(site)
derived_measurements.append(DerivedMetric(name, power, MEASUREMENT_TYPES['power']))
return derived_measurements

232
devlib/derived/fps.py Normal file
View File

@@ -0,0 +1,232 @@
# Copyright 2018 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 __future__ import division
import os
import re
try:
import pandas as pd
except ImportError:
pd = None
from past.builtins import basestring
from devlib import DerivedMeasurements, DerivedMetric, MeasurementsCsv, InstrumentChannel
from devlib.exception import HostError
from devlib.utils.csvutil import csvwriter
from devlib.utils.rendering import gfxinfo_get_last_dump, VSYNC_INTERVAL
from devlib.utils.types import numeric
class DerivedFpsStats(DerivedMeasurements):
def __init__(self, drop_threshold=5, suffix=None, filename=None, outdir=None):
self.drop_threshold = drop_threshold
self.suffix = suffix
self.filename = filename
self.outdir = outdir
if (filename is None) and (suffix is None):
self.suffix = '-fps'
elif (filename is not None) and (suffix is not None):
raise ValueError('suffix and filename cannot be specified at the same time.')
if filename is not None and os.sep in filename:
raise ValueError('filename cannot be a path (cannot countain "{}"'.format(os.sep))
def process(self, measurements_csv):
if isinstance(measurements_csv, basestring):
measurements_csv = MeasurementsCsv(measurements_csv)
if pd is not None:
return self._process_with_pandas(measurements_csv)
return self._process_without_pandas(measurements_csv)
def _get_csv_file_name(self, frames_file):
outdir = self.outdir or os.path.dirname(frames_file)
if self.filename:
return os.path.join(outdir, self.filename)
frames_basename = os.path.basename(frames_file)
rest, ext = os.path.splitext(frames_basename)
csv_basename = rest + self.suffix + ext
return os.path.join(outdir, csv_basename)
class DerivedGfxInfoStats(DerivedFpsStats):
@staticmethod
def process_raw(filepath, *args):
metrics = []
dump = gfxinfo_get_last_dump(filepath)
seen_stats = False
for line in dump.split('\n'):
if seen_stats and not line.strip():
break
elif line.startswith('Janky frames:'):
text = line.split(': ')[-1]
val_text, pc_text = text.split('(')
metrics.append(DerivedMetric('janks', numeric(val_text.strip()), 'count'))
metrics.append(DerivedMetric('janks_pc', numeric(pc_text[:-3]), 'percent'))
elif ' percentile: ' in line:
ptile, val_text = line.split(' percentile: ')
name = 'render_time_{}_ptile'.format(ptile)
value = numeric(val_text.strip()[:-2])
metrics.append(DerivedMetric(name, value, 'time_ms'))
elif line.startswith('Number '):
name_text, val_text = line.strip().split(': ')
name = name_text[7:].lower().replace(' ', '_')
value = numeric(val_text)
metrics.append(DerivedMetric(name, value, 'count'))
else:
continue
seen_stats = True
return metrics
def _process_without_pandas(self, measurements_csv):
per_frame_fps = []
start_vsync, end_vsync = None, None
frame_count = 0
for frame_data in measurements_csv.iter_values():
if frame_data.Flags_flags != 0:
continue
frame_count += 1
if start_vsync is None:
start_vsync = frame_data.Vsync_time_us
end_vsync = frame_data.Vsync_time_us
frame_time = frame_data.FrameCompleted_time_us - frame_data.IntendedVsync_time_us
pff = 1e9 / frame_time
if pff > self.drop_threshold:
per_frame_fps.append([pff])
if frame_count:
duration = end_vsync - start_vsync
fps = (1e6 * frame_count) / float(duration)
else:
duration = 0
fps = 0
csv_file = self._get_csv_file_name(measurements_csv.path)
with csvwriter(csv_file) as writer:
writer.writerow(['fps'])
writer.writerows(per_frame_fps)
return [DerivedMetric('fps', fps, 'fps'),
DerivedMetric('total_frames', frame_count, 'frames'),
MeasurementsCsv(csv_file)]
def _process_with_pandas(self, measurements_csv):
data = pd.read_csv(measurements_csv.path)
data = data[data.Flags_flags == 0]
frame_time = data.FrameCompleted_time_us - data.IntendedVsync_time_us
per_frame_fps = (1e6 / frame_time)
keep_filter = per_frame_fps > self.drop_threshold
per_frame_fps = per_frame_fps[keep_filter]
per_frame_fps.name = 'fps'
frame_count = data.index.size
if frame_count > 1:
duration = data.Vsync_time_us.iloc[-1] - data.Vsync_time_us.iloc[0]
fps = (1e9 * frame_count) / float(duration)
else:
duration = 0
fps = 0
csv_file = self._get_csv_file_name(measurements_csv.path)
per_frame_fps.to_csv(csv_file, index=False, header=True)
return [DerivedMetric('fps', fps, 'fps'),
DerivedMetric('total_frames', frame_count, 'frames'),
MeasurementsCsv(csv_file)]
class DerivedSurfaceFlingerStats(DerivedFpsStats):
def _process_with_pandas(self, measurements_csv):
data = pd.read_csv(measurements_csv.path)
# fiter out bogus frames.
bogus_frames_filter = data.actual_present_time_us != 0x7fffffffffffffff
actual_present_times = data.actual_present_time_us[bogus_frames_filter]
actual_present_time_deltas = actual_present_times.diff().dropna()
vsyncs_to_compose = actual_present_time_deltas.div(VSYNC_INTERVAL)
vsyncs_to_compose.apply(lambda x: int(round(x, 0)))
# drop values lower than drop_threshold FPS as real in-game frame
# rate is unlikely to drop below that (except on loading screens
# etc, which should not be factored in frame rate calculation).
per_frame_fps = (1.0 / (vsyncs_to_compose.multiply(VSYNC_INTERVAL / 1e9)))
keep_filter = per_frame_fps > self.drop_threshold
filtered_vsyncs_to_compose = vsyncs_to_compose[keep_filter]
per_frame_fps.name = 'fps'
csv_file = self._get_csv_file_name(measurements_csv.path)
per_frame_fps.to_csv(csv_file, index=False, header=True)
if not filtered_vsyncs_to_compose.empty:
fps = 0
total_vsyncs = filtered_vsyncs_to_compose.sum()
frame_count = filtered_vsyncs_to_compose.size
if total_vsyncs:
fps = 1e9 * frame_count / (VSYNC_INTERVAL * total_vsyncs)
janks = self._calc_janks(filtered_vsyncs_to_compose)
not_at_vsync = self._calc_not_at_vsync(vsyncs_to_compose)
else:
fps = 0
frame_count = 0
janks = 0
not_at_vsync = 0
janks_pc = 0 if frame_count == 0 else janks * 100 / frame_count
return [DerivedMetric('fps', fps, 'fps'),
DerivedMetric('total_frames', frame_count, 'frames'),
MeasurementsCsv(csv_file),
DerivedMetric('janks', janks, 'count'),
DerivedMetric('janks_pc', janks_pc, 'percent'),
DerivedMetric('missed_vsync', not_at_vsync, 'count')]
def _process_without_pandas(self, measurements_csv):
# Given that SurfaceFlinger has been deprecated in favor of GfxInfo,
# it does not seem worth it implementing this.
raise HostError('Please install "pandas" Python package to process SurfaceFlinger frames')
@staticmethod
def _calc_janks(filtered_vsyncs_to_compose):
"""
Internal method for calculating jank frames.
"""
pause_latency = 20
vtc_deltas = filtered_vsyncs_to_compose.diff().dropna()
vtc_deltas = vtc_deltas.abs()
janks = vtc_deltas.apply(lambda x: (pause_latency > x > 1.5) and 1 or 0).sum()
return janks
@staticmethod
def _calc_not_at_vsync(vsyncs_to_compose):
"""
Internal method for calculating the number of frames that did not
render in a single vsync cycle.
"""
epsilon = 0.0001
func = lambda x: (abs(x - 1.0) > epsilon) and 1 or 0
not_at_vsync = vsyncs_to_compose.apply(func).sum()
return not_at_vsync

View File

@@ -1,4 +1,4 @@
# Copyright 2013-2015 ARM Limited # Copyright 2013-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -13,10 +13,13 @@
# limitations under the License. # limitations under the License.
# #
class DevlibError(Exception): class DevlibError(Exception):
"""Base class for all Devlib exceptions.""" """Base class for all Devlib exceptions."""
pass @property
def message(self):
if self.args:
return self.args[0]
return str(self)
class TargetError(DevlibError): class TargetError(DevlibError):
@@ -26,9 +29,7 @@ class TargetError(DevlibError):
class TargetNotRespondingError(DevlibError): class TargetNotRespondingError(DevlibError):
"""The target is unresponsive.""" """The target is unresponsive."""
pass
def __init__(self, target):
super(TargetNotRespondingError, self).__init__('Target {} is not responding.'.format(target))
class HostError(DevlibError): class HostError(DevlibError):
@@ -49,3 +50,42 @@ class TimeoutError(DevlibError):
def __str__(self): def __str__(self):
return '\n'.join([self.message, 'OUTPUT:', self.output or '']) return '\n'.join([self.message, 'OUTPUT:', self.output or ''])
class WorkerThreadError(DevlibError):
"""
This should get raised in the main thread if a non-WAError-derived
exception occurs on a worker/background thread. If a WAError-derived
exception is raised in the worker, then it that exception should be
re-raised on the main thread directly -- the main point of this is to
preserve the backtrace in the output, and backtrace doesn't get output for
WAErrors.
"""
def __init__(self, thread, exc_info):
self.thread = thread
self.exc_info = exc_info
orig = self.exc_info[1]
orig_name = type(orig).__name__
message = 'Exception of type {} occured on thread {}:\n'.format(orig_name, thread)
message += '{}\n{}: {}'.format(get_traceback(self.exc_info), orig_name, orig)
super(WorkerThreadError, self).__init__(message)
def get_traceback(exc=None):
"""
Returns the string with the traceback for the specifiec exc
object, or for the current exception exc is not specified.
"""
import io, traceback, sys
if exc is None:
exc = sys.exc_info()
if not exc:
return None
tb = exc[2]
sio = io.BytesIO()
traceback.print_tb(tb, file=sio)
del tb # needs to be done explicitly see: http://docs.python.org/2/library/sys.html#sys.exc_info
return sio.getvalue()

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2017 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
# #
from glob import iglob from glob import iglob
import os import os
import signal
import shutil import shutil
import subprocess import subprocess
import logging import logging
@@ -24,6 +25,11 @@ from devlib.utils.misc import check_output
PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin') PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin')
def kill_children(pid, signal=signal.SIGKILL):
with open('/proc/{0}/task/{0}/children'.format(pid), 'r') as fd:
for cpid in map(int, fd.read().strip().split()):
kill_children(cpid, signal)
os.kill(cpid, signal)
class LocalConnection(object): class LocalConnection(object):

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -12,11 +12,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
import csv from __future__ import division
import logging import logging
import collections import collections
from past.builtins import basestring
from devlib.utils.csvutil import csvreader
from devlib.utils.types import numeric from devlib.utils.types import numeric
from devlib.utils.types import identifier
# Channel modes describe what sort of measurement the instrument supports. # Channel modes describe what sort of measurement the instrument supports.
@@ -24,28 +28,35 @@ from devlib.utils.types import numeric
INSTANTANEOUS = 1 INSTANTANEOUS = 1
CONTINUOUS = 2 CONTINUOUS = 2
MEASUREMENT_TYPES = {} # populated further down
class MeasurementType(tuple):
__slots__ = [] class MeasurementType(object):
def __new__(cls, name, units, category=None): def __init__(self, name, units, category=None, conversions=None):
return tuple.__new__(cls, (name, units, category)) self.name = name
self.units = units
self.category = category
self.conversions = {}
if conversions is not None:
for key, value in conversions.items():
if not callable(value):
msg = 'Converter must be callable; got {} "{}"'
raise ValueError(msg.format(type(value), value))
self.conversions[key] = value
@property def convert(self, value, to):
def name(self): if isinstance(to, basestring) and to in MEASUREMENT_TYPES:
return tuple.__getitem__(self, 0) to = MEASUREMENT_TYPES[to]
if not isinstance(to, MeasurementType):
@property msg = 'Unexpected conversion target: "{}"'
def units(self): raise ValueError(msg.format(to))
return tuple.__getitem__(self, 1) if to.name == self.name:
return value
@property if not to.name in self.conversions:
def category(self): msg = 'No conversion from {} to {} available'
return tuple.__getitem__(self, 2) raise ValueError(msg.format(self.name, to.name))
return self.conversions[to.name](value)
def __getitem__(self, item):
raise TypeError()
def __cmp__(self, other): def __cmp__(self, other):
if isinstance(other, MeasurementType): if isinstance(other, MeasurementType):
@@ -55,24 +66,73 @@ class MeasurementType(tuple):
def __str__(self): def __str__(self):
return self.name return self.name
__repr__ = __str__ def __repr__(self):
if self.category:
text = 'MeasurementType({}, {}, {})'
return text.format(self.name, self.units, self.category)
else:
text = 'MeasurementType({}, {})'
return text.format(self.name, self.units)
# Standard measures # Standard measures. In order to make sure that downstream data processing is not tied
# to particular insturments (e.g. a particular method of mearuing power), instruments
# must, where possible, resport their measurments formatted as on of the standard types
# defined here.
_measurement_types = [ _measurement_types = [
MeasurementType('time', 'seconds'), # For whatever reason, the type of measurement could not be established.
MeasurementType('temperature', 'degrees'), MeasurementType('unknown', None),
# Generic measurements
MeasurementType('count', 'count'),
MeasurementType('percent', 'percent'),
# Time measurement. While there is typically a single "canonical" unit
# used for each type of measurmenent, time may be measured to a wide variety
# of events occuring at a wide range of scales. Forcing everying into a
# single scale will lead to inefficient and awkward to work with result tables.
# Coversion functions between the formats are specified, so that downstream
# processors that expect all times time be at a particular scale can automatically
# covert without being familar with individual instruments.
MeasurementType('time', 'seconds', 'time',
conversions={
'time_us': lambda x: x * 1000000,
'time_ms': lambda x: x * 1000,
}
),
MeasurementType('time_us', 'microseconds', 'time',
conversions={
'time': lambda x: x / 1000000,
'time_ms': lambda x: x / 1000,
}
),
MeasurementType('time_ms', 'milliseconds', 'time',
conversions={
'time': lambda x: x / 1000,
'time_us': lambda x: x * 1000,
}
),
# Measurements related to thermals.
MeasurementType('temperature', 'degrees', 'thermal'),
# Measurements related to power end energy consumption.
MeasurementType('power', 'watts', 'power/energy'), MeasurementType('power', 'watts', 'power/energy'),
MeasurementType('voltage', 'volts', 'power/energy'), MeasurementType('voltage', 'volts', 'power/energy'),
MeasurementType('current', 'amps', 'power/energy'), MeasurementType('current', 'amps', 'power/energy'),
MeasurementType('energy', 'joules', 'power/energy'), MeasurementType('energy', 'joules', 'power/energy'),
# Measurments realted to data transfer, e.g. neworking,
# memory, or backing storage.
MeasurementType('tx', 'bytes', 'data transfer'), MeasurementType('tx', 'bytes', 'data transfer'),
MeasurementType('rx', 'bytes', 'data transfer'), MeasurementType('rx', 'bytes', 'data transfer'),
MeasurementType('tx/rx', 'bytes', 'data transfer'), MeasurementType('tx/rx', 'bytes', 'data transfer'),
MeasurementType('fps', 'fps', 'ui render'),
MeasurementType('frames', 'frames', 'ui render'),
] ]
MEASUREMENT_TYPES = {m.name: m for m in _measurement_types} for m in _measurement_types:
MEASUREMENT_TYPES[m.name] = m
class Measurement(object): class Measurement(object):
@@ -92,7 +152,7 @@ class Measurement(object):
self.channel = channel self.channel = channel
def __cmp__(self, other): def __cmp__(self, other):
if isinstance(other, Measurement): if hasattr(other, 'value'):
return cmp(self.value, other.value) return cmp(self.value, other.value)
else: else:
return cmp(self.value, other) return cmp(self.value, other)
@@ -108,28 +168,72 @@ class Measurement(object):
class MeasurementsCsv(object): class MeasurementsCsv(object):
def __init__(self, path, channels): def __init__(self, path, channels=None, sample_rate_hz=None):
self.path = path self.path = path
self.channels = channels self.channels = channels
self._fh = open(path, 'rb') self.sample_rate_hz = sample_rate_hz
if self.channels is None:
self._load_channels()
headings = [chan.label for chan in self.channels]
self.data_tuple = collections.namedtuple('csv_entry',
map(identifier, headings))
def measurements(self): def measurements(self):
return list(self.itermeasurements()) return list(self.iter_measurements())
def itermeasurements(self): def iter_measurements(self):
self._fh.seek(0) for row in self._iter_rows():
reader = csv.reader(self._fh)
reader.next() # headings
for row in reader:
values = map(numeric, row) values = map(numeric, row)
yield [Measurement(v, c) for (v, c) in zip(values, self.channels)] yield [Measurement(v, c) for (v, c) in zip(values, self.channels)]
def values(self):
return list(self.iter_values())
def iter_values(self):
for row in self._iter_rows():
values = list(map(numeric, row))
yield self.data_tuple(*values)
def _load_channels(self):
header = []
with csvreader(self.path) as reader:
header = next(reader)
self.channels = []
for entry in header:
for mt in MEASUREMENT_TYPES:
suffix = '_{}'.format(mt)
if entry.endswith(suffix):
site = entry[:-len(suffix)]
measure = mt
break
else:
if entry in MEASUREMENT_TYPES:
site = None
measure = entry
else:
site = entry
measure = 'unknown'
chan = InstrumentChannel(site, measure)
self.channels.append(chan)
def _iter_rows(self):
with csvreader(self.path) as reader:
next(reader) # headings
for row in reader:
yield row
class InstrumentChannel(object): class InstrumentChannel(object):
@property @property
def label(self): def label(self):
if self.site is not None:
return '{}_{}'.format(self.site, self.kind) return '{}_{}'.format(self.site, self.kind)
return self.kind
name = label
@property @property
def kind(self): def kind(self):
@@ -139,8 +243,7 @@ class InstrumentChannel(object):
def units(self): def units(self):
return self.measurement_type.units return self.measurement_type.units
def __init__(self, name, site, measurement_type, **attrs): def __init__(self, site, measurement_type, **attrs):
self.name = name
self.site = site self.site = site
if isinstance(measurement_type, MeasurementType): if isinstance(measurement_type, MeasurementType):
self.measurement_type = measurement_type self.measurement_type = measurement_type
@@ -149,7 +252,7 @@ class InstrumentChannel(object):
self.measurement_type = MEASUREMENT_TYPES[measurement_type] self.measurement_type = MEASUREMENT_TYPES[measurement_type]
except KeyError: except KeyError:
raise ValueError('Unknown measurement type: {}'.format(measurement_type)) raise ValueError('Unknown measurement type: {}'.format(measurement_type))
for atname, atvalue in attrs.iteritems(): for atname, atvalue in attrs.items():
setattr(self, atname, atvalue) setattr(self, atname, atvalue)
def __str__(self): def __str__(self):
@@ -175,17 +278,15 @@ class Instrument(object):
# channel management # channel management
def list_channels(self): def list_channels(self):
return self.channels.values() return list(self.channels.values())
def get_channels(self, measure): def get_channels(self, measure):
if hasattr(measure, 'name'): if hasattr(measure, 'name'):
measure = measure.name measure = measure.name
return [c for c in self.list_channels() if c.kind == measure] return [c for c in self.list_channels() if c.kind == measure]
def add_channel(self, site, measure, name=None, **attrs): def add_channel(self, site, measure, **attrs):
if name is None: chan = InstrumentChannel(site, measure, **attrs)
name = '{}_{}'.format(site, measure)
chan = InstrumentChannel(name, site, measure, **attrs)
self.channels[chan.label] = chan self.channels[chan.label] = chan
# initialization and teardown # initialization and teardown
@@ -197,24 +298,26 @@ class Instrument(object):
pass pass
def reset(self, sites=None, kinds=None, channels=None): def reset(self, sites=None, kinds=None, channels=None):
if kinds is None and sites is None and channels is None: if channels is not None:
self.active_channels = sorted(self.channels.values(), key=lambda x: x.label) if sites is not None or kinds is not None:
raise ValueError('sites and kinds should not be set if channels is set')
try:
self.active_channels = [self.channels[ch] for ch in channels]
except KeyError as e:
msg = 'Unexpected channel "{}"; must be in {}'
raise ValueError(msg.format(e, self.channels.keys()))
elif sites is None and kinds is None:
self.active_channels = sorted(self.channels.itervalues(), key=lambda x: x.label)
else: else:
if isinstance(sites, basestring): if isinstance(sites, basestring):
sites = [sites] sites = [sites]
if isinstance(kinds, basestring): if isinstance(kinds, basestring):
kinds = [kinds] kinds = [kinds]
self.active_channels = []
for chan_name in (channels or []): wanted = lambda ch : ((kinds is None or ch.kind in kinds) and
try: (sites is None or ch.site in sites))
self.active_channels.append(self.channels[chan_name]) self.active_channels = filter(wanted, self.channels.itervalues())
except KeyError:
msg = 'Unexpected channel "{}"; must be in {}'
raise ValueError(msg.format(chan_name, self.channels.keys()))
for chan in self.channels.values():
if (kinds is None or chan.kind in kinds) and \
(sites is None or chan.site in sites):
self.active_channels.append(chan)
# instantaneous # instantaneous
@@ -231,3 +334,6 @@ class Instrument(object):
def get_data(self, outfile): def get_data(self, outfile):
pass pass
def get_raw(self):
return []

View File

@@ -0,0 +1,157 @@
# Copyright 2018 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#pylint: disable=attribute-defined-outside-init
from __future__ import division
import os
import sys
import time
import tempfile
from fcntl import fcntl, F_GETFL, F_SETFL
from string import Template
from subprocess import Popen, PIPE, STDOUT
from devlib import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError
from devlib.utils.csvutil import csvreader, csvwriter
from devlib.utils.misc import which
OUTPUT_CAPTURE_FILE = 'acme-cape.csv'
IIOCAP_CMD_TEMPLATE = Template("""
${iio_capture} -n ${host} -b ${buffer_size} -c -f ${outfile} ${iio_device}
""")
def _read_nonblock(pipe, size=1024):
fd = pipe.fileno()
flags = fcntl(fd, F_GETFL)
flags |= os.O_NONBLOCK
fcntl(fd, F_SETFL, flags)
output = ''
try:
while True:
output += pipe.read(size)
except IOError:
pass
return output
class AcmeCapeInstrument(Instrument):
mode = CONTINUOUS
def __init__(self, target,
iio_capture=which('iio-capture'),
host='baylibre-acme.local',
iio_device='iio:device0',
buffer_size=256):
super(AcmeCapeInstrument, self).__init__(target)
self.iio_capture = iio_capture
self.host = host
self.iio_device = iio_device
self.buffer_size = buffer_size
self.sample_rate_hz = 100
if self.iio_capture is None:
raise HostError('Missing iio-capture binary')
self.command = None
self.process = None
self.add_channel('shunt', 'voltage')
self.add_channel('bus', 'voltage')
self.add_channel('device', 'power')
self.add_channel('device', 'current')
self.add_channel('timestamp', 'time_ms')
def __del__(self):
if self.process and self.process.pid:
self.logger.warning('killing iio-capture process [{}]...'.format(self.process.pid))
self.process.kill()
def reset(self, sites=None, kinds=None, channels=None):
super(AcmeCapeInstrument, self).reset(sites, kinds, channels)
self.raw_data_file = tempfile.mkstemp('.csv')[1]
params = dict(
iio_capture=self.iio_capture,
host=self.host,
buffer_size=self.buffer_size,
iio_device=self.iio_device,
outfile=self.raw_data_file
)
self.command = IIOCAP_CMD_TEMPLATE.substitute(**params)
self.logger.debug('ACME cape command: {}'.format(self.command))
def start(self):
self.process = Popen(self.command.split(), stdout=PIPE, stderr=STDOUT)
def stop(self):
self.process.terminate()
timeout_secs = 10
output = ''
for _ in range(timeout_secs):
if self.process.poll() is not None:
break
time.sleep(1)
else:
output += _read_nonblock(self.process.stdout)
self.process.kill()
self.logger.error('iio-capture did not terminate gracefully')
if self.process.poll() is None:
msg = 'Could not terminate iio-capture:\n{}'
raise HostError(msg.format(output))
if self.process.returncode != 15: # iio-capture exits with 15 when killed
if sys.version_info[0] == 3:
output += self.process.stdout.read().decode(sys.stdout.encoding, 'replace')
else:
output += self.process.stdout.read()
self.logger.info('ACME instrument encountered an error, '
'you may want to try rebooting the ACME device:\n'
' ssh root@{} reboot'.format(self.host))
raise HostError('iio-capture exited with an error ({}), output:\n{}'
.format(self.process.returncode, output))
if not os.path.isfile(self.raw_data_file):
raise HostError('Output CSV not generated.')
self.process = None
def get_data(self, outfile):
if os.stat(self.raw_data_file).st_size == 0:
self.logger.warning('"{}" appears to be empty'.format(self.raw_data_file))
return
all_channels = [c.label for c in self.list_channels()]
active_channels = [c.label for c in self.active_channels]
active_indexes = [all_channels.index(ac) for ac in active_channels]
with csvreader(self.raw_data_file, skipinitialspace=True) as reader:
with csvwriter(outfile) as writer:
writer.writerow(active_channels)
header = next(reader)
ts_index = header.index('timestamp ms')
for row in reader:
output_row = []
for i in active_indexes:
if i == ts_index:
# Leave time in ms
output_row.append(float(row[i]))
else:
# Convert rest into standard units.
output_row.append(float(row[i])/1000)
writer.writerow(output_row)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self.raw_data_file]

View File

@@ -0,0 +1,145 @@
# Copyright 2018 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.
#
# Copyright 2018 Linaro Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint: disable=W0613,E1101,access-member-before-definition,attribute-defined-outside-init
from __future__ import division
import os
import subprocess
import signal
import struct
import sys
import tempfile
import shutil
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError
from devlib.utils.csvutil import csvreader, csvwriter
from devlib.utils.misc import which
from devlib.utils.parse_aep import AepParser
class ArmEnergyProbeInstrument(Instrument):
"""
Collects power traces using the ARM Energy Probe.
This instrument requires ``arm-probe`` utility to be installed on the host and be in the PATH.
arm-probe is available here:
``https://git.linaro.org/tools/arm-probe.git``.
Details about how to build and use it is available here:
``https://git.linaro.org/tools/arm-probe.git/tree/README``
ARM energy probe (AEP) device can simultaneously collect power from up to 3 power rails and
arm-probe utility can record data from several AEP devices simultaneously.
To connect the energy probe on a rail, connect the white wire to the pin that is closer to the
Voltage source and the black wire to the pin that is closer to the load (the SoC or the device
you are probing). Between the pins there should be a shunt resistor of known resistance in the
range of 5 to 500 mOhm but the voltage on the shunt resistor must stay smaller than 165mV.
The resistance of the shunt resistors is a mandatory parameter to be set in the ``config`` file.
"""
mode = CONTINUOUS
MAX_CHANNELS = 12 # 4 Arm Energy Probes
def __init__(self, target, config_file='./config-aep', ):
super(ArmEnergyProbeInstrument, self).__init__(target)
self.arm_probe = which('arm-probe')
if self.arm_probe is None:
raise HostError('arm-probe must be installed on the host')
#todo detect is config file exist
self.attributes = ['power', 'voltage', 'current']
self.sample_rate_hz = 10000
self.config_file = config_file
self.parser = AepParser()
#TODO make it generic
topo = self.parser.topology_from_config(self.config_file)
for item in topo:
if item == 'time':
self.add_channel('timestamp', 'time')
else:
self.add_channel(item, 'power')
def reset(self, sites=None, kinds=None, channels=None):
super(ArmEnergyProbeInstrument, self).reset(sites, kinds, channels)
self.output_directory = tempfile.mkdtemp(prefix='energy_probe')
self.output_file_raw = os.path.join(self.output_directory, 'data_raw')
self.output_file = os.path.join(self.output_directory, 'data')
self.output_file_figure = os.path.join(self.output_directory, 'summary.txt')
self.output_file_error = os.path.join(self.output_directory, 'error.log')
self.output_fd_error = open(self.output_file_error, 'w')
self.command = 'arm-probe --config {} > {}'.format(self.config_file, self.output_file_raw)
def start(self):
self.logger.debug(self.command)
self.armprobe = subprocess.Popen(self.command,
stderr=self.output_fd_error,
preexec_fn=os.setpgrp,
shell=True)
def stop(self):
self.logger.debug("kill running arm-probe")
os.killpg(self.armprobe.pid, signal.SIGTERM)
def get_data(self, outfile): # pylint: disable=R0914
self.logger.debug("Parse data and compute consumed energy")
self.parser.prepare(self.output_file_raw, self.output_file, self.output_file_figure)
self.parser.parse_aep()
self.parser.unprepare()
skip_header = 1
all_channels = [c.label for c in self.list_channels()]
active_channels = [c.label for c in self.active_channels]
active_indexes = [all_channels.index(ac) for ac in active_channels]
with csvreader(self.output_file, delimiter=' ') as reader:
with csvwriter(outfile) as writer:
for row in reader:
if skip_header == 1:
writer.writerow(active_channels)
skip_header = 0
continue
if len(row) < len(active_channels):
continue
# all data are in micro (seconds/watt)
new = [ float(row[i])/1000000 for i in active_indexes ]
writer.writerow(new)
self.output_fd_error.close()
shutil.rmtree(self.output_directory)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self.output_file_raw]

View File

@@ -1,19 +1,34 @@
# Copyright 2018 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 os
import csv
import tempfile import tempfile
from itertools import chain from itertools import chain
from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
from devlib.exception import HostError from devlib.exception import HostError
from devlib.utils.csvutil import csvwriter, create_reader
from devlib.utils.misc import unique from devlib.utils.misc import unique
try: try:
from daqpower.client import execute_command, Status from daqpower.client import execute_command, Status
from daqpower.config import DeviceConfiguration, ServerConfiguration from daqpower.config import DeviceConfiguration, ServerConfiguration
except ImportError, e: except ImportError as e:
execute_command, Status = None, None execute_command, Status = None, None
DeviceConfiguration, ServerConfiguration, ConfigurationError = None, None, None DeviceConfiguration, ServerConfiguration, ConfigurationError = None, None, None
import_error_mesg = e.message import_error_mesg = e.args[0] if e.args else str(e)
class DaqInstrument(Instrument): class DaqInstrument(Instrument):
@@ -33,10 +48,11 @@ class DaqInstrument(Instrument):
# pylint: disable=no-member # pylint: disable=no-member
super(DaqInstrument, self).__init__(target) super(DaqInstrument, self).__init__(target)
self._need_reset = True self._need_reset = True
self._raw_files = []
if execute_command is None: if execute_command is None:
raise HostError('Could not import "daqpower": {}'.format(import_error_mesg)) raise HostError('Could not import "daqpower": {}'.format(import_error_mesg))
if labels is None: if labels is None:
labels = ['PORT_{}'.format(i) for i in xrange(len(resistor_values))] labels = ['PORT_{}'.format(i) for i in range(len(resistor_values))]
if len(labels) != len(resistor_values): if len(labels) != len(resistor_values):
raise ValueError('"labels" and "resistor_values" must be of the same length') raise ValueError('"labels" and "resistor_values" must be of the same length')
self.server_config = ServerConfiguration(host=host, self.server_config = ServerConfiguration(host=host,
@@ -68,6 +84,7 @@ class DaqInstrument(Instrument):
if not result.status == Status.OK: # pylint: disable=no-member if not result.status == Status.OK: # pylint: disable=no-member
raise HostError(result.message) raise HostError(result.message)
self._need_reset = False self._need_reset = False
self._raw_files = []
def start(self): def start(self):
if self._need_reset: if self._need_reset:
@@ -86,6 +103,7 @@ class DaqInstrument(Instrument):
site = os.path.splitext(entry)[0] site = os.path.splitext(entry)[0]
path = os.path.join(tempdir, entry) path = os.path.join(tempdir, entry)
raw_file_map[site] = path raw_file_map[site] = path
self._raw_files.append(path)
active_sites = unique([c.site for c in self.active_channels]) active_sites = unique([c.site for c in self.active_channels])
file_handles = [] file_handles = []
@@ -94,8 +112,8 @@ class DaqInstrument(Instrument):
for site in active_sites: for site in active_sites:
try: try:
site_file = raw_file_map[site] site_file = raw_file_map[site]
fh = open(site_file, 'rb') reader, fh = create_reader(site_file)
site_readers[site] = csv.reader(fh) site_readers[site] = reader
file_handles.append(fh) file_handles.append(fh)
except KeyError: except KeyError:
message = 'Could not get DAQ trace for {}; Obtained traces are in {}' message = 'Could not get DAQ trace for {}; Obtained traces are in {}'
@@ -103,22 +121,21 @@ class DaqInstrument(Instrument):
# The first row is the headers # The first row is the headers
channel_order = [] channel_order = []
for site, reader in site_readers.iteritems(): for site, reader in site_readers.items():
channel_order.extend(['{}_{}'.format(site, kind) channel_order.extend(['{}_{}'.format(site, kind)
for kind in reader.next()]) for kind in next(reader)])
def _read_next_rows(): def _read_next_rows():
parts = [] parts = []
for reader in site_readers.itervalues(): for reader in site_readers.values():
try: try:
parts.extend(reader.next()) parts.extend(next(reader))
except StopIteration: except StopIteration:
parts.extend([None, None]) parts.extend([None, None])
return list(chain(parts)) return list(chain(parts))
with open(outfile, 'wb') as wfh: with csvwriter(outfile) as writer:
field_names = [c.label for c in self.active_channels] field_names = [c.label for c in self.active_channels]
writer = csv.writer(wfh)
writer.writerow(field_names) writer.writerow(field_names)
raw_row = _read_next_rows() raw_row = _read_next_rows()
while any(raw_row): while any(raw_row):
@@ -126,11 +143,14 @@ class DaqInstrument(Instrument):
writer.writerow(row) writer.writerow(row)
raw_row = _read_next_rows() raw_row = _read_next_rows()
return MeasurementsCsv(outfile, self.active_channels) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
finally: finally:
for fh in file_handles: for fh in file_handles:
fh.close() fh.close()
def get_raw(self):
return self._raw_files
def teardown(self): def teardown(self):
self.execute('close') self.execute('close')

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -14,14 +14,15 @@
# #
from __future__ import division from __future__ import division
import os import os
import csv
import signal import signal
import tempfile import tempfile
import struct import struct
import subprocess import subprocess
import sys
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError from devlib.exception import HostError
from devlib.utils.csvutil import csvwriter
from devlib.utils.misc import which from devlib.utils.misc import which
@@ -39,7 +40,7 @@ class EnergyProbeInstrument(Instrument):
self.labels = labels self.labels = labels
else: else:
self.labels = ['PORT_{}'.format(i) self.labels = ['PORT_{}'.format(i)
for i in xrange(len(resistor_values))] for i in range(len(resistor_values))]
self.device_entry = device_entry self.device_entry = device_entry
self.caiman = which('caiman') self.caiman = which('caiman')
if self.caiman is None: if self.caiman is None:
@@ -52,6 +53,7 @@ class EnergyProbeInstrument(Instrument):
self.raw_output_directory = None self.raw_output_directory = None
self.process = None self.process = None
self.sample_rate_hz = 10000 # Determined empirically self.sample_rate_hz = 10000 # Determined empirically
self.raw_data_file = None
for label in self.labels: for label in self.labels:
for kind in self.attributes: for kind in self.attributes:
@@ -64,6 +66,7 @@ class EnergyProbeInstrument(Instrument):
for i, rval in enumerate(self.resistor_values)] for i, rval in enumerate(self.resistor_values)]
rstring = ''.join(parts) rstring = ''.join(parts)
self.command = '{} -d {} -l {} {}'.format(self.caiman, self.device_entry, rstring, self.raw_output_directory) self.command = '{} -d {} -l {} {}'.format(self.caiman, self.device_entry, rstring, self.raw_output_directory)
self.raw_data_file = None
def start(self): def start(self):
self.logger.debug(self.command) self.logger.debug(self.command)
@@ -78,11 +81,14 @@ class EnergyProbeInstrument(Instrument):
self.process.poll() self.process.poll()
if self.process.returncode is not None: if self.process.returncode is not None:
stdout, stderr = self.process.communicate() stdout, stderr = self.process.communicate()
if sys.version_info[0] == 3:
stdout = stdout.decode(sys.stdout.encoding, 'replace')
stderr = stderr.decode(sys.stdout.encoding, 'replace')
raise HostError( raise HostError(
'Energy Probe: Caiman exited unexpectedly with exit code {}.\n' 'Energy Probe: Caiman exited unexpectedly with exit code {}.\n'
'stdout:\n{}\nstderr:\n{}'.format(self.process.returncode, 'stdout:\n{}\nstderr:\n{}'.format(self.process.returncode,
stdout, stderr)) stdout, stderr))
os.killpg(self.process.pid, signal.SIGTERM) os.killpg(self.process.pid, signal.SIGINT)
def get_data(self, outfile): # pylint: disable=R0914 def get_data(self, outfile): # pylint: disable=R0914
all_channels = [c.label for c in self.list_channels()] all_channels = [c.label for c in self.list_channels()]
@@ -92,12 +98,11 @@ class EnergyProbeInstrument(Instrument):
num_of_ports = len(self.resistor_values) num_of_ports = len(self.resistor_values)
struct_format = '{}I'.format(num_of_ports * self.attributes_per_sample) struct_format = '{}I'.format(num_of_ports * self.attributes_per_sample)
not_a_full_row_seen = False not_a_full_row_seen = False
raw_data_file = os.path.join(self.raw_output_directory, '0000000000') self.raw_data_file = os.path.join(self.raw_output_directory, '0000000000')
self.logger.debug('Parsing raw data file: {}'.format(raw_data_file)) self.logger.debug('Parsing raw data file: {}'.format(self.raw_data_file))
with open(raw_data_file, 'rb') as bfile: with open(self.raw_data_file, 'rb') as bfile:
with open(outfile, 'wb') as wfh: with csvwriter(outfile) as writer:
writer = csv.writer(wfh)
writer.writerow(active_channels) writer.writerow(active_channels)
while True: while True:
data = bfile.read(num_of_ports * self.bytes_per_sample) data = bfile.read(num_of_ports * self.bytes_per_sample)
@@ -113,4 +118,7 @@ class EnergyProbeInstrument(Instrument):
continue continue
else: else:
not_a_full_row_seen = True not_a_full_row_seen = True
return MeasurementsCsv(outfile, self.active_channels) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self.raw_data_file]

View File

@@ -0,0 +1,97 @@
# Copyright 2018 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 __future__ import division
from devlib.instrument import (Instrument, CONTINUOUS,
MeasurementsCsv, MeasurementType)
from devlib.utils.rendering import (GfxinfoFrameCollector,
SurfaceFlingerFrameCollector,
SurfaceFlingerFrame,
read_gfxinfo_columns)
class FramesInstrument(Instrument):
mode = CONTINUOUS
collector_cls = None
def __init__(self, target, collector_target, period=2, keep_raw=True):
super(FramesInstrument, self).__init__(target)
self.collector_target = collector_target
self.period = period
self.keep_raw = keep_raw
self.sample_rate_hz = 1 / self.period
self.collector = None
self.header = None
self._need_reset = True
self._raw_file = None
self._init_channels()
def reset(self, sites=None, kinds=None, channels=None):
super(FramesInstrument, self).reset(sites, kinds, channels)
self.collector = self.collector_cls(self.target, self.period,
self.collector_target, self.header)
self._need_reset = False
self._raw_file = None
def start(self):
if self._need_reset:
self.reset()
self.collector.start()
def stop(self):
self.collector.stop()
self._need_reset = True
def get_data(self, outfile):
if self.keep_raw:
self._raw_file = outfile + '.raw'
self.collector.process_frames(self._raw_file)
active_sites = [chan.label for chan in self.active_channels]
self.collector.write_frames(outfile, columns=active_sites)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self._raw_file] if self._raw_file else []
def _init_channels(self):
raise NotImplementedError()
class GfxInfoFramesInstrument(FramesInstrument):
mode = CONTINUOUS
collector_cls = GfxinfoFrameCollector
def _init_channels(self):
columns = read_gfxinfo_columns(self.target)
for entry in columns:
if entry == 'Flags':
self.add_channel('Flags', MeasurementType('flags', 'flags'))
else:
self.add_channel(entry, 'time_us')
self.header = [chan.label for chan in self.channels.values()]
class SurfaceFlingerFramesInstrument(FramesInstrument):
mode = CONTINUOUS
collector_cls = SurfaceFlingerFrameCollector
def _init_channels(self):
for field in SurfaceFlingerFrame._fields:
# remove the "_time" from filed names to avoid duplication
self.add_channel(field[:-5], 'time_us')
self.header = [chan.label for chan in self.channels.values()]

View File

@@ -0,0 +1,80 @@
# Copyright 2017-2018 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 __future__ import division
import re
from devlib.platform.gem5 import Gem5SimulationPlatform
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import TargetError, HostError
from devlib.utils.csvutil import csvwriter
class Gem5PowerInstrument(Instrument):
'''
Instrument enabling power monitoring in gem5
'''
mode = CONTINUOUS
roi_label = 'power_instrument'
site_mapping = {'timestamp': 'sim_seconds'}
def __init__(self, target, power_sites):
'''
Parameter power_sites is a list of gem5 identifiers for power values.
One example of such a field:
system.cluster0.cores0.power_model.static_power
'''
if not isinstance(target.platform, Gem5SimulationPlatform):
raise TargetError('Gem5PowerInstrument requires a gem5 platform')
if not target.has('gem5stats'):
raise TargetError('Gem5StatsModule is not loaded')
super(Gem5PowerInstrument, self).__init__(target)
# power_sites is assumed to be a list later
if isinstance(power_sites, list):
self.power_sites = power_sites
else:
self.power_sites = [power_sites]
self.add_channel('timestamp', 'time')
for field in self.power_sites:
self.add_channel(field, 'power')
self.target.gem5stats.book_roi(self.roi_label)
self.sample_period_ns = 10000000
# Sample rate must remain unset as gem5 does not provide samples
# at regular intervals therefore the reported timestamp should be used.
self.sample_rate_hz = None
self.target.gem5stats.start_periodic_dump(0, self.sample_period_ns)
self._base_stats_dump = 0
def start(self):
self.target.gem5stats.roi_start(self.roi_label)
def stop(self):
self.target.gem5stats.roi_end(self.roi_label)
def get_data(self, outfile):
active_sites = [c.site for c in self.active_channels]
with csvwriter(outfile) as writer:
writer.writerow([c.label for c in self.active_channels]) # headers
sites_to_match = [self.site_mapping.get(s, s) for s in active_sites]
for rec, rois in self.target.gem5stats.match_iter(sites_to_match,
[self.roi_label], self._base_stats_dump):
writer.writerow([rec[s] for s in sites_to_match])
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def reset(self, sites=None, kinds=None, channels=None):
super(Gem5PowerInstrument, self).reset(sites, kinds, channels)
self._base_stats_dump = self.target.gem5stats.next_dump_no()

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2017 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -45,7 +45,7 @@ class HwmonInstrument(Instrument):
measure = self.measure_map.get(ts.kind)[0] measure = self.measure_map.get(ts.kind)[0]
if measure: if measure:
self.logger.debug('\tAdding sensor {}'.format(ts.name)) self.logger.debug('\tAdding sensor {}'.format(ts.name))
self.add_channel(_guess_site(ts), measure, name=ts.name, sensor=ts) self.add_channel(_guess_site(ts), measure, sensor=ts)
else: else:
self.logger.debug('\tSkipping sensor {} (unknown kind "{}")'.format(ts.name, ts.kind)) self.logger.debug('\tSkipping sensor {} (unknown kind "{}")'.format(ts.name, ts.kind))
except ValueError: except ValueError:

View File

@@ -1,23 +1,42 @@
import csv # Copyright 2018 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 os
import signal import signal
import sys
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError from devlib.exception import HostError
from devlib.host import PACKAGE_BIN_DIRECTORY from devlib.host import PACKAGE_BIN_DIRECTORY
from devlib.utils.csvutil import csvwriter
from devlib.utils.misc import which from devlib.utils.misc import which
INSTALL_INSTRUCTIONS=""" INSTALL_INSTRUCTIONS="""
MonsoonInstrument requires the monsoon.py tool, available from AOSP: MonsoonInstrument requires the monsoon.py tool, available from AOSP:
https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py
Download this script and put it in your $PATH (or pass it as the monsoon_bin Download this script and put it in your $PATH (or pass it as the monsoon_bin
parameter to MonsoonInstrument). `pip install gflags pyserial` to install the parameter to MonsoonInstrument). `pip install python-gflags pyserial` to install
dependencies. the dependencies.
""" """
class MonsoonInstrument(Instrument): class MonsoonInstrument(Instrument):
"""Instrument for Monsoon Solutions power monitor """Instrument for Monsoon Solutions power monitor
@@ -81,6 +100,9 @@ class MonsoonInstrument(Instrument):
process.poll() process.poll()
if process.returncode is not None: if process.returncode is not None:
stdout, stderr = process.communicate() stdout, stderr = process.communicate()
if sys.version_info[0] == 3:
stdout = stdout.encode(sys.stdout.encoding)
stderr = stderr.encode(sys.stdout.encoding)
raise HostError( raise HostError(
'Monsoon script exited unexpectedly with exit code {}.\n' 'Monsoon script exited unexpectedly with exit code {}.\n'
'stdout:\n{}\nstderr:\n{}'.format(process.returncode, 'stdout:\n{}\nstderr:\n{}'.format(process.returncode,
@@ -104,8 +126,7 @@ class MonsoonInstrument(Instrument):
stdout, stderr = self.output stdout, stderr = self.output
with open(outfile, 'wb') as f: with csvwriter(outfile) as writer:
writer = csv.writer(f)
active_sites = [c.site for c in self.active_channels] active_sites = [c.site for c in self.active_channels]
# Write column headers # Write column headers
@@ -129,4 +150,4 @@ class MonsoonInstrument(Instrument):
row.append(usb) row.append(usb)
writer.writerow(row) writer.writerow(row)
return MeasurementsCsv(outfile, self.active_channels) return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)

View File

@@ -1,14 +1,30 @@
# Copyright 2018 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 os
import re import re
import csv
import tempfile import tempfile
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
from itertools import izip_longest
from future.moves.itertools import zip_longest
from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
from devlib.exception import TargetError, HostError from devlib.exception import TargetError, HostError
from devlib.utils.android import ApkInfo from devlib.utils.android import ApkInfo
from devlib.utils.csvutil import csvwriter
THIS_DIR = os.path.dirname(__file__) THIS_DIR = os.path.dirname(__file__)
@@ -46,10 +62,9 @@ def netstats_to_measurements(netstats):
def write_measurements_csv(measurements, filepath): def write_measurements_csv(measurements, filepath):
headers = sorted(measurements.keys()) headers = sorted(measurements.keys())
columns = [measurements[h] for h in headers] columns = [measurements[h] for h in headers]
with open(filepath, 'wb') as wfh: with csvwriter(filepath) as writer:
writer = csv.writer(wfh)
writer.writerow(headers) writer.writerow(headers)
writer.writerows(izip_longest(*columns)) writer.writerows(zip_longest(*columns))
class NetstatsInstrument(Instrument): class NetstatsInstrument(Instrument):

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
import logging import logging
from inspect import isclass from inspect import isclass
from past.builtins import basestring
from devlib.utils.misc import walk_modules from devlib.utils.misc import walk_modules
from devlib.utils.types import identifier from devlib.utils.types import identifier
@@ -56,7 +58,7 @@ class Module(object):
def __init__(self, target): def __init__(self, target):
self.target = target self.target = target
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.name)
class HardRestModule(Module): # pylint: disable=R0921 class HardRestModule(Module): # pylint: disable=R0921
@@ -75,7 +77,7 @@ class BootModule(Module): # pylint: disable=R0921
raise NotImplementedError() raise NotImplementedError()
def update(self, **kwargs): def update(self, **kwargs):
for name, value in kwargs.iteritems(): for name, value in kwargs.items():
if not hasattr(self, name): if not hasattr(self, name):
raise ValueError('Unknown parameter "{}" for {}'.format(name, self.name)) raise ValueError('Unknown parameter "{}" for {}'.format(name, self.name))
self.logger.debug('Updating "{}" to "{}"'.format(name, value)) self.logger.debug('Updating "{}" to "{}"'.format(name, value))
@@ -117,6 +119,6 @@ def register_module(mod):
def __load_cache(): def __load_cache():
for module in walk_modules('devlib.module'): for module in walk_modules('devlib.module'):
for obj in vars(module).itervalues(): for obj in vars(module).values():
if isclass(obj) and issubclass(obj, Module) and obj.name: if isclass(obj) and issubclass(obj, Module) and obj.name:
register_module(obj) register_module(obj)

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -63,7 +63,7 @@ class FastbootFlashModule(FlashModule):
image_bundle = expand_path(image_bundle) image_bundle = expand_path(image_bundle)
to_flash = self._bundle_to_images(image_bundle) to_flash = self._bundle_to_images(image_bundle)
to_flash = merge_dicts(to_flash, images or {}, should_normalize=False) to_flash = merge_dicts(to_flash, images or {}, should_normalize=False)
for partition, image_path in to_flash.iteritems(): for partition, image_path in to_flash.items():
self.logger.debug('flashing {}'.format(partition)) self.logger.debug('flashing {}'.format(partition))
self._flash_image(self.target, partition, expand_path(image_path)) self._flash_image(self.target, partition, expand_path(image_path))
fastboot_command('reboot') fastboot_command('reboot')

View File

@@ -1,3 +1,18 @@
# Copyright 2018 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 devlib.module import Module from devlib.module import Module
@@ -44,79 +59,151 @@ class BigLittleModule(Module):
# cpufreq # cpufreq
def list_bigs_frequencies(self): def list_bigs_frequencies(self):
return self.target.cpufreq.list_frequencies(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.list_frequencies(bigs_online[0])
def list_bigs_governors(self): def list_bigs_governors(self):
return self.target.cpufreq.list_governors(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.list_governors(bigs_online[0])
def list_bigs_governor_tunables(self): def list_bigs_governor_tunables(self):
return self.target.cpufreq.list_governor_tunables(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.list_governor_tunables(bigs_online[0])
def list_littles_frequencies(self): def list_littles_frequencies(self):
return self.target.cpufreq.list_frequencies(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.list_frequencies(littles_online[0])
def list_littles_governors(self): def list_littles_governors(self):
return self.target.cpufreq.list_governors(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.list_governors(littles_online[0])
def list_littles_governor_tunables(self): def list_littles_governor_tunables(self):
return self.target.cpufreq.list_governor_tunables(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.list_governor_tunables(littles_online[0])
def get_bigs_governor(self): def get_bigs_governor(self):
return self.target.cpufreq.get_governor(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.get_governor(bigs_online[0])
def get_bigs_governor_tunables(self): def get_bigs_governor_tunables(self):
return self.target.cpufreq.get_governor_tunables(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.get_governor_tunables(bigs_online[0])
def get_bigs_frequency(self): def get_bigs_frequency(self):
return self.target.cpufreq.get_frequency(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.get_frequency(bigs_online[0])
def get_bigs_min_frequency(self): def get_bigs_min_frequency(self):
return self.target.cpufreq.get_min_frequency(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.get_min_frequency(bigs_online[0])
def get_bigs_max_frequency(self): def get_bigs_max_frequency(self):
return self.target.cpufreq.get_max_frequency(self.bigs_online[0]) bigs_online = self.bigs_online
if len(bigs_online) > 0:
return self.target.cpufreq.get_max_frequency(bigs_online[0])
def get_littles_governor(self): def get_littles_governor(self):
return self.target.cpufreq.get_governor(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.get_governor(littles_online[0])
def get_littles_governor_tunables(self): def get_littles_governor_tunables(self):
return self.target.cpufreq.get_governor_tunables(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.get_governor_tunables(littles_online[0])
def get_littles_frequency(self): def get_littles_frequency(self):
return self.target.cpufreq.get_frequency(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.get_frequency(littles_online[0])
def get_littles_min_frequency(self): def get_littles_min_frequency(self):
return self.target.cpufreq.get_min_frequency(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.get_min_frequency(littles_online[0])
def get_littles_max_frequency(self): def get_littles_max_frequency(self):
return self.target.cpufreq.get_max_frequency(self.littles_online[0]) littles_online = self.littles_online
if len(littles_online) > 0:
return self.target.cpufreq.get_max_frequency(littles_online[0])
def set_bigs_governor(self, governor, **kwargs): def set_bigs_governor(self, governor, **kwargs):
self.target.cpufreq.set_governor(self.bigs_online[0], governor, **kwargs) bigs_online = self.bigs_online
if len(bigs_online) > 0:
self.target.cpufreq.set_governor(bigs_online[0], governor, **kwargs)
else:
raise ValueError("All bigs appear to be offline")
def set_bigs_governor_tunables(self, governor, **kwargs): def set_bigs_governor_tunables(self, governor, **kwargs):
self.target.cpufreq.set_governor_tunables(self.bigs_online[0], governor, **kwargs) bigs_online = self.bigs_online
if len(bigs_online) > 0:
self.target.cpufreq.set_governor_tunables(bigs_online[0], governor, **kwargs)
else:
raise ValueError("All bigs appear to be offline")
def set_bigs_frequency(self, frequency, exact=True): def set_bigs_frequency(self, frequency, exact=True):
self.target.cpufreq.set_frequency(self.bigs_online[0], frequency, exact) bigs_online = self.bigs_online
if len(bigs_online) > 0:
self.target.cpufreq.set_frequency(bigs_online[0], frequency, exact)
else:
raise ValueError("All bigs appear to be offline")
def set_bigs_min_frequency(self, frequency, exact=True): def set_bigs_min_frequency(self, frequency, exact=True):
self.target.cpufreq.set_min_frequency(self.bigs_online[0], frequency, exact) bigs_online = self.bigs_online
if len(bigs_online) > 0:
self.target.cpufreq.set_min_frequency(bigs_online[0], frequency, exact)
else:
raise ValueError("All bigs appear to be offline")
def set_bigs_max_frequency(self, frequency, exact=True): def set_bigs_max_frequency(self, frequency, exact=True):
self.target.cpufreq.set_max_frequency(self.bigs_online[0], frequency, exact) bigs_online = self.bigs_online
if len(bigs_online) > 0:
self.target.cpufreq.set_max_frequency(bigs_online[0], frequency, exact)
else:
raise ValueError("All bigs appear to be offline")
def set_littles_governor(self, governor, **kwargs): def set_littles_governor(self, governor, **kwargs):
self.target.cpufreq.set_governor(self.littles_online[0], governor, **kwargs) littles_online = self.littles_online
if len(littles_online) > 0:
self.target.cpufreq.set_governor(littles_online[0], governor, **kwargs)
else:
raise ValueError("All littles appear to be offline")
def set_littles_governor_tunables(self, governor, **kwargs): def set_littles_governor_tunables(self, governor, **kwargs):
self.target.cpufreq.set_governor_tunables(self.littles_online[0], governor, **kwargs) littles_online = self.littles_online
if len(littles_online) > 0:
self.target.cpufreq.set_governor_tunables(littles_online[0], governor, **kwargs)
else:
raise ValueError("All littles appear to be offline")
def set_littles_frequency(self, frequency, exact=True): def set_littles_frequency(self, frequency, exact=True):
self.target.cpufreq.set_frequency(self.littles_online[0], frequency, exact) littles_online = self.littles_online
if len(littles_online) > 0:
self.target.cpufreq.set_frequency(littles_online[0], frequency, exact)
else:
raise ValueError("All littles appear to be offline")
def set_littles_min_frequency(self, frequency, exact=True): def set_littles_min_frequency(self, frequency, exact=True):
self.target.cpufreq.set_min_frequency(self.littles_online[0], frequency, exact) littles_online = self.littles_online
if len(littles_online) > 0:
self.target.cpufreq.set_min_frequency(littles_online[0], frequency, exact)
else:
raise ValueError("All littles appear to be offline")
def set_littles_max_frequency(self, frequency, exact=True): def set_littles_max_frequency(self, frequency, exact=True):
self.target.cpufreq.set_max_frequency(self.littles_online[0], frequency, exact) littles_online = self.littles_online
if len(littles_online) > 0:
self.target.cpufreq.set_max_frequency(littles_online[0], frequency, exact)
else:
raise ValueError("All littles appear to be offline")

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
# #
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
import logging import logging
import re
from collections import namedtuple from collections import namedtuple
from devlib.module import Module from devlib.module import Module
@@ -102,7 +103,7 @@ class Controller(object):
.format(self.kind)) .format(self.kind))
if name not in self._cgroups: if name not in self._cgroups:
self._cgroups[name] = CGroup(self, name, create=False) self._cgroups[name] = CGroup(self, name, create=False)
return self._cgroups[name].existe() return self._cgroups[name].exists()
def list_all(self): def list_all(self):
self.logger.debug('Listing groups for %s controller', self.kind) self.logger.debug('Listing groups for %s controller', self.kind)
@@ -168,7 +169,37 @@ class Controller(object):
if cgroup != dest: if cgroup != dest:
self.move_tasks(cgroup, dest, grep_filters) self.move_tasks(cgroup, dest, grep_filters)
def tasks(self, cgroup): def tasks(self, cgroup,
filter_tid='',
filter_tname='',
filter_tcmdline=''):
"""
Report the tasks that are included in a cgroup. The tasks can be
filtered by their tid, tname or tcmdline if filter_tid, filter_tname or
filter_tcmdline are defined respectively. In this case, the reported
tasks are the ones in the cgroup that match these patterns.
Example of tasks format:
TID,tname,tcmdline
903,cameraserver,/system/bin/cameraserver
:params filter_tid: regexp pattern to filter by TID
:type filter_tid: str
:params filter_tname: regexp pattern to filter by tname
:type filter_tname: str
:params filter_tcmdline: regexp pattern to filter by tcmdline
:type filter_tcmdline: str
:returns: a dictionary in the form: {tid:(tname, tcmdline)}
"""
if not isinstance(filter_tid, str):
raise TypeError('filter_tid should be a str')
if not isinstance(filter_tname, str):
raise TypeError('filter_tname should be a str')
if not isinstance(filter_tcmdline, str):
raise TypeError('filter_tcmdline should be a str')
try: try:
cg = self._cgroups[cgroup] cg = self._cgroups[cgroup]
except KeyError as e: except KeyError as e:
@@ -179,15 +210,24 @@ class Controller(object):
entries = output.splitlines() entries = output.splitlines()
tasks = {} tasks = {}
for task in entries: for task in entries:
tid = task.split(',')[0] fields = task.split(',', 2)
try: nr_fields = len(fields)
tname = task.split(',')[1] if nr_fields < 2:
except: continue continue
try: elif nr_fields == 2:
tcmdline = task.split(',')[2] tid_str, tname = fields
except:
tcmdline = '' tcmdline = ''
tasks[int(tid)] = (tname, tcmdline) else:
tid_str, tname, tcmdline = fields
if not re.search(filter_tid, tid_str):
continue
if not re.search(filter_tname, tname):
continue
if not re.search(filter_tcmdline, tcmdline):
continue
tasks[int(tid_str)] = (tname, tcmdline)
return tasks return tasks
def tasks_count(self, cgroup): def tasks_count(self, cgroup):
@@ -285,7 +325,7 @@ class CGroup(object):
def get_tasks(self): def get_tasks(self):
task_ids = self.target.read_value(self.tasks_file).split() task_ids = self.target.read_value(self.tasks_file).split()
logging.debug('Tasks: %s', task_ids) logging.debug('Tasks: %s', task_ids)
return map(int, task_ids) return list(map(int, task_ids))
def add_task(self, tid): def add_task(self, tid):
self.target.write_value(self.tasks_file, tid, verify=False) self.target.write_value(self.tasks_file, tid, verify=False)
@@ -354,7 +394,7 @@ class CgroupsModule(Module):
def list_subsystems(self): def list_subsystems(self):
subsystems = [] subsystems = []
for line in self.target.execute('{} cat /proc/cgroups'\ for line in self.target.execute('{} cat /proc/cgroups'\
.format(self.target.busybox)).splitlines()[1:]: .format(self.target.busybox), as_root=self.target.is_rooted).splitlines()[1:]:
line = line.strip() line = line.strip()
if not line or line.startswith('#'): if not line or line.startswith('#'):
continue continue
@@ -466,11 +506,11 @@ class CgroupsModule(Module):
if freezer is None: if freezer is None:
raise RuntimeError('freezer cgroup controller not present') raise RuntimeError('freezer cgroup controller not present')
freezer_cg = freezer.cgroup('/DEVLIB_FREEZER') freezer_cg = freezer.cgroup('/DEVLIB_FREEZER')
thawed_cg = freezer.cgroup('/') cmd = 'cgroups_freezer_set_state {{}} {}'.format(freezer_cg.directory)
if thaw: if thaw:
# Restart froozen tasks # Restart froozen tasks
freezer_cg.set(state='THAWED') freezer.target._execute_util(cmd.format('THAWED'), as_root=True)
# Remove all tasks from freezer # Remove all tasks from freezer
freezer.move_all_tasks_to('/') freezer.move_all_tasks_to('/')
return return
@@ -482,7 +522,7 @@ class CgroupsModule(Module):
tasks = freezer.tasks('/') tasks = freezer.tasks('/')
# Freeze all tasks # Freeze all tasks
freezer_cg.set(state='FROZEN') freezer.target._execute_util(cmd.format('FROZEN'), as_root=True)
return tasks return tasks

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@ class CpufreqModule(Module):
return True return True
# Generic CPUFreq support (single policy) # Generic CPUFreq support (single policy)
path = '/sys/devices/system/cpu/cpufreq' path = '/sys/devices/system/cpu/cpufreq/policy0'
if target.file_exists(path): if target.file_exists(path):
return True return True
@@ -150,7 +150,7 @@ class CpufreqModule(Module):
if governor is None: if governor is None:
governor = self.get_governor(cpu) governor = self.get_governor(cpu)
valid_tunables = self.list_governor_tunables(cpu) valid_tunables = self.list_governor_tunables(cpu)
for tunable, value in kwargs.iteritems(): for tunable, value in kwargs.items():
if tunable in valid_tunables: if tunable in valid_tunables:
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable) path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
try: try:
@@ -176,16 +176,41 @@ class CpufreqModule(Module):
try: try:
cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/scaling_available_frequencies'.format(cpu) cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/scaling_available_frequencies'.format(cpu)
output = self.target.execute(cmd) output = self.target.execute(cmd)
available_frequencies = map(int, output.strip().split()) # pylint: disable=E1103 available_frequencies = list(map(int, output.strip().split())) # pylint: disable=E1103
except TargetError: except TargetError:
# On some devices scaling_frequencies is not generated. # On some devices scaling_frequencies is not generated.
# http://adrynalyne-teachtofish.blogspot.co.uk/2011/11/how-to-enable-scalingavailablefrequenci.html # http://adrynalyne-teachtofish.blogspot.co.uk/2011/11/how-to-enable-scalingavailablefrequenci.html
# Fall back to parsing stats/time_in_state # Fall back to parsing stats/time_in_state
cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/stats/time_in_state'.format(cpu) path = '/sys/devices/system/cpu/{}/cpufreq/stats/time_in_state'.format(cpu)
out_iter = iter(self.target.execute(cmd).strip().split()) try:
available_frequencies = map(int, reversed([f for f, _ in zip(out_iter, out_iter)])) out_iter = iter(self.target.read_value(path).split())
except TargetError:
if not self.target.file_exists(path):
# Probably intel_pstate. Can't get available freqs.
return []
raise
available_frequencies = list(map(int, reversed([f for f, _ in zip(out_iter, out_iter)])))
return available_frequencies return available_frequencies
@memoized
def get_max_available_frequency(self, cpu):
"""
Returns the maximum available frequency for a given core or None if
could not be found.
"""
freqs = self.list_frequencies(cpu)
return freqs and max(freqs) or None
@memoized
def get_min_available_frequency(self, cpu):
"""
Returns the minimum available frequency for a given core or None if
could not be found.
"""
freqs = self.list_frequencies(cpu)
return freqs and min(freqs) or None
def get_min_frequency(self, cpu): def get_min_frequency(self, cpu):
""" """
Returns the min frequency currently set for the specified CPU. Returns the min frequency currently set for the specified CPU.
@@ -382,7 +407,9 @@ class CpufreqModule(Module):
'cpufreq_set_all_governors {}'.format(governor), 'cpufreq_set_all_governors {}'.format(governor),
as_root=True) as_root=True)
except TargetError as e: except TargetError as e:
if "echo: I/O error" in str(e): if ("echo: I/O error" in str(e) or
"write error: Invalid argument" in str(e)):
cpus_unsupported = [c for c in self.target.list_online_cpus() cpus_unsupported = [c for c in self.target.list_online_cpus()
if governor not in self.list_governors(c)] if governor not in self.list_governors(c)]
raise TargetError("Governor {} unsupported for CPUs {}".format( raise TargetError("Governor {} unsupported for CPUs {}".format(
@@ -410,10 +437,9 @@ class CpufreqModule(Module):
""" """
return self.target._execute_util('cpufreq_trace_all_frequencies', as_root=True) return self.target._execute_util('cpufreq_trace_all_frequencies', as_root=True)
@memoized def get_affected_cpus(self, cpu):
def get_domain_cpus(self, cpu):
""" """
Get the CPUs that share a frequency domain with the given CPU Get the online CPUs that share a frequency domain with the given CPU
""" """
if isinstance(cpu, int): if isinstance(cpu, int):
cpu = 'cpu{}'.format(cpu) cpu = 'cpu{}'.format(cpu)
@@ -421,3 +447,38 @@ class CpufreqModule(Module):
sysfile = '/sys/devices/system/cpu/{}/cpufreq/affected_cpus'.format(cpu) sysfile = '/sys/devices/system/cpu/{}/cpufreq/affected_cpus'.format(cpu)
return [int(c) for c in self.target.read_value(sysfile).split()] return [int(c) for c in self.target.read_value(sysfile).split()]
@memoized
def get_related_cpus(self, cpu):
"""
Get the CPUs that share a frequency domain with the given CPU
"""
if isinstance(cpu, int):
cpu = 'cpu{}'.format(cpu)
sysfile = '/sys/devices/system/cpu/{}/cpufreq/related_cpus'.format(cpu)
return [int(c) for c in self.target.read_value(sysfile).split()]
@memoized
def get_driver(self, cpu):
"""
Get the name of the driver used by this cpufreq policy.
"""
if isinstance(cpu, int):
cpu = 'cpu{}'.format(cpu)
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_driver'.format(cpu)
return self.target.read_value(sysfile).strip()
def iter_domains(self):
"""
Iterate over the frequency domains in the system
"""
cpus = set(range(self.target.number_of_cpus))
while cpus:
cpu = next(iter(cpus))
domain = self.target.cpufreq.get_related_cpus(cpu)
yield domain
cpus = cpus.difference(domain)

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -13,6 +13,8 @@
# limitations under the License. # limitations under the License.
# #
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
from past.builtins import basestring
from devlib.module import Module from devlib.module import Module
from devlib.utils.misc import memoized from devlib.utils.misc import memoized
from devlib.utils.types import integer, boolean from devlib.utils.types import integer, boolean
@@ -41,51 +43,17 @@ class CpuidleState(object):
raise ValueError('invalid idle state name: "{}"'.format(self.id)) raise ValueError('invalid idle state name: "{}"'.format(self.id))
return int(self.id[i:]) return int(self.id[i:])
def __init__(self, target, index, path): def __init__(self, target, index, path, name, desc, power, latency, residency):
self.target = target self.target = target
self.index = index self.index = index
self.path = path self.path = path
self.name = name
self.desc = desc
self.power = power
self.latency = latency
self.id = self.target.path.basename(self.path) self.id = self.target.path.basename(self.path)
self.cpu = self.target.path.basename(self.target.path.dirname(path)) self.cpu = self.target.path.basename(self.target.path.dirname(path))
@property
@memoized
def desc(self):
return self.get('desc')
@property
@memoized
def name(self):
return self.get('name')
@property
@memoized
def latency(self):
"""Exit latency in uS"""
return self.get('latency')
@property
@memoized
def power(self):
"""Power usage in mW
..note::
This value is not always populated by the kernel and may be garbage.
"""
return self.get('power')
@property
@memoized
def target_residency(self):
"""Target residency in uS
This is the amount of time in the state required to 'break even' on
power - the system should avoid entering the state for less time than
this.
"""
return self.get('residency')
def enable(self): def enable(self):
self.set('disable', 0) self.set('disable', 0)
@@ -126,23 +94,47 @@ class Cpuidle(Module):
def probe(target): def probe(target):
return target.file_exists(Cpuidle.root_path) return target.file_exists(Cpuidle.root_path)
def get_driver(self): def __init__(self, target):
return self.target.read_value(self.target.path.join(self.root_path, 'current_driver')) super(Cpuidle, self).__init__(target)
self._states = {}
def get_governor(self): basepath = '/sys/devices/system/cpu/'
return self.target.read_value(self.target.path.join(self.root_path, 'current_governor_ro')) values_tree = self.target.read_tree_values(basepath, depth=4, check_exit_code=False)
i = 0
cpu_id = 'cpu{}'.format(i)
while cpu_id in values_tree:
cpu_node = values_tree[cpu_id]
if 'cpuidle' in cpu_node:
idle_node = cpu_node['cpuidle']
self._states[cpu_id] = []
j = 0
state_id = 'state{}'.format(j)
while state_id in idle_node:
state_node = idle_node[state_id]
state = CpuidleState(
self.target,
index=j,
path=self.target.path.join(basepath, cpu_id, 'cpuidle', state_id),
name=state_node['name'],
desc=state_node['desc'],
power=int(state_node['power']),
latency=int(state_node['latency']),
residency=int(state_node['residency']) if 'residency' in state_node else None,
)
msg = 'Adding {} state {}: {} {}'
self.logger.debug(msg.format(cpu_id, j, state.name, state.desc))
self._states[cpu_id].append(state)
j += 1
state_id = 'state{}'.format(j)
i += 1
cpu_id = 'cpu{}'.format(i)
@memoized
def get_states(self, cpu=0): def get_states(self, cpu=0):
if isinstance(cpu, int): if isinstance(cpu, int):
cpu = 'cpu{}'.format(cpu) cpu = 'cpu{}'.format(cpu)
states_dir = self.target.path.join(self.target.path.dirname(self.root_path), cpu, 'cpuidle') return self._states.get(cpu, [])
idle_states = []
for state in self.target.list_directory(states_dir):
if state.startswith('state'):
index = int(state[5:])
idle_states.append(CpuidleState(self.target, index, self.target.path.join(states_dir, state)))
return idle_states
def get_state(self, state, cpu=0): def get_state(self, state, cpu=0):
if isinstance(state, int): if isinstance(state, int):
@@ -175,4 +167,9 @@ class Cpuidle(Module):
Momentarily wake each CPU. Ensures cpu_idle events in trace file. Momentarily wake each CPU. Ensures cpu_idle events in trace file.
""" """
output = self.target._execute_util('cpuidle_wake_all_cpus') output = self.target._execute_util('cpuidle_wake_all_cpus')
print(output)
def get_driver(self):
return self.target.read_value(self.target.path.join(self.root_path, 'current_driver'))
def get_governor(self):
return self.target.read_value(self.target.path.join(self.root_path, 'current_governor_ro'))

261
devlib/module/devfreq.py Normal file
View File

@@ -0,0 +1,261 @@
# Copyright 2018 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 devlib.module import Module
from devlib.exception import TargetError
from devlib.utils.misc import memoized
class DevfreqModule(Module):
name = 'devfreq'
@staticmethod
def probe(target):
path = '/sys/class/devfreq/'
if not target.file_exists(path):
return False
# Check that at least one policy is implemented
if not target.list_directory(path):
return False
return True
@memoized
def list_devices(self):
"""Returns a list of devfreq devices supported by the target platform."""
sysfile = '/sys/class/devfreq/'
return self.target.list_directory(sysfile)
@memoized
def list_governors(self, device):
"""Returns a list of governors supported by the device."""
sysfile = '/sys/class/devfreq/{}/available_governors'.format(device)
output = self.target.read_value(sysfile)
return output.strip().split()
def get_governor(self, device):
"""Returns the governor currently set for the specified device."""
if isinstance(device, int):
device = 'device{}'.format(device)
sysfile = '/sys/class/devfreq/{}/governor'.format(device)
return self.target.read_value(sysfile)
def set_governor(self, device, governor):
"""
Set the governor for the specified device.
:param device: The device for which the governor is to be set. This must be
the full name as it appears in sysfs, e.g. "e82c0000.mali".
: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.
:raises: TargetError if governor is not supported by the device, or if,
for some reason, the governor could not be set.
"""
supported = self.list_governors(device)
if governor not in supported:
raise TargetError('Governor {} not supported for device {}'.format(governor, device))
sysfile = '/sys/class/devfreq/{}/governor'.format(device)
self.target.write_value(sysfile, governor)
@memoized
def list_frequencies(self, device):
"""
Returns a list of frequencies supported by the device or an empty list
if could not be found.
"""
cmd = 'cat /sys/class/devfreq/{}/available_frequencies'.format(device)
output = self.target.execute(cmd)
available_frequencies = [int(freq) for freq in output.strip().split()]
return available_frequencies
def get_min_frequency(self, device):
"""
Returns the min frequency currently set for the specified device.
Warning, this method does not check if the device is present or not. It
will try to read the minimum frequency and the following exception will
be raised ::
:raises: TargetError if for some reason the frequency could not be read.
"""
sysfile = '/sys/class/devfreq/{}/min_freq'.format(device)
return self.target.read_int(sysfile)
def set_min_frequency(self, device, frequency, exact=True):
"""
Sets the minimum value for device frequency. Actual frequency will
depend on the thermal 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 list_frequencies() or examining
/sys/class/devfreq/<device_name>/available_frequencies
on the device.
:raises: TargetError if the frequency is not supported by the device, or if, for
some reason, frequency could not be set.
:raises: ValueError if ``frequency`` is not an integer.
"""
available_frequencies = self.list_frequencies(device)
try:
value = int(frequency)
if exact and available_frequencies and value not in available_frequencies:
raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(device,
value,
available_frequencies))
sysfile = '/sys/class/devfreq/{}/min_freq'.format(device)
self.target.write_value(sysfile, value)
except ValueError:
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
def get_frequency(self, device):
"""
Returns the current frequency currently set for the specified device.
Warning, this method does not check if the device is present or not. It
will try to read the current frequency and the following exception will
be raised ::
:raises: TargetError if for some reason the frequency could not be read.
"""
sysfile = '/sys/class/devfreq/{}/cur_freq'.format(device)
return self.target.read_int(sysfile)
def get_max_frequency(self, device):
"""
Returns the max frequency currently set for the specified device.
Warning, this method does not check if the device is online or not. It will
try to read the maximum frequency and the following exception will be
raised ::
:raises: TargetError if for some reason the frequency could not be read.
"""
sysfile = '/sys/class/devfreq/{}/max_freq'.format(device)
return self.target.read_int(sysfile)
def set_max_frequency(self, device, frequency, exact=True):
"""
Sets the maximum value for device 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_frequencies() or examining
/sys/class/devfreq/<device_name>/available_frequencies
on the device.
:raises: TargetError if the frequency is not supported by the device, or
if, for some reason, frequency could not be set.
:raises: ValueError if ``frequency`` is not an integer.
"""
available_frequencies = self.list_frequencies(device)
try:
value = int(frequency)
except ValueError:
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
if exact and value not in available_frequencies:
raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(device,
value,
available_frequencies))
sysfile = '/sys/class/devfreq/{}/max_freq'.format(device)
self.target.write_value(sysfile, value)
def set_governor_for_devices(self, devices, governor):
"""
Set the governor for the specified list of devices.
:param devices: The list of device for which the governor is to be set.
"""
for device in devices:
self.set_governor(device, governor)
def set_all_governors(self, governor):
"""
Set the specified governor for all the (available) devices
"""
try:
return self.target._execute_util(
'devfreq_set_all_governors {}'.format(governor), as_root=True)
except TargetError as e:
if ("echo: I/O error" in str(e) or
"write error: Invalid argument" in str(e)):
devs_unsupported = [d for d in self.target.list_devices()
if governor not in self.list_governors(d)]
raise TargetError("Governor {} unsupported for devices {}".format(
governor, devs_unsupported))
else:
raise
def get_all_governors(self):
"""
Get the current governor for all the (online) CPUs
"""
output = self.target._execute_util(
'devfreq_get_all_governors', as_root=True)
governors = {}
for x in output.splitlines():
kv = x.split(' ')
if kv[0] == '':
break
governors[kv[0]] = kv[1]
return governors
def set_frequency_for_devices(self, devices, freq, exact=False):
"""
Set the frequency for the specified list of devices.
:param devices: The list of device for which the frequency has to be set.
"""
for device in devices:
self.set_max_frequency(device, freq, exact)
self.set_min_frequency(device, freq, exact)
def set_all_frequencies(self, freq):
"""
Set the specified (minimum) frequency for all the (available) devices
"""
return self.target._execute_util(
'devfreq_set_all_frequencies {}'.format(freq),
as_root=True)
def get_all_frequencies(self):
"""
Get the current frequency for all the (available) devices
"""
output = self.target._execute_util(
'devfreq_get_all_frequencies', as_root=True)
frequencies = {}
for x in output.splitlines():
kv = x.split(' ')
if kv[0] == '':
break
frequencies[kv[0]] = kv[1]
return frequencies

254
devlib/module/gem5stats.py Normal file
View File

@@ -0,0 +1,254 @@
# Copyright 2017-2018 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 re
import sys
import logging
import os.path
from collections import defaultdict
import devlib
from devlib.exception import TargetError
from devlib.module import Module
from devlib.platform import Platform
from devlib.platform.gem5 import Gem5SimulationPlatform
from devlib.utils.gem5 import iter_statistics_dump, GEM5STATS_ROI_NUMBER, GEM5STATS_DUMP_TAIL
class Gem5ROI:
def __init__(self, number, target):
self.target = target
self.number = number
self.running = False
self.field = 'ROI::{}'.format(number)
def start(self):
if self.running:
return False
self.target.execute('m5 roistart {}'.format(self.number))
self.running = True
return True
def stop(self):
if not self.running:
return False
self.target.execute('m5 roiend {}'.format(self.number))
self.running = False
return True
class Gem5StatsModule(Module):
'''
Module controlling Region of Interest (ROIs) markers, satistics dump
frequency and parsing statistics log file when using gem5 platforms.
ROIs are identified by user-defined labels and need to be booked prior to
use. The translation of labels into gem5 ROI numbers will be performed
internally in order to avoid conflicts between multiple clients.
'''
name = 'gem5stats'
@staticmethod
def probe(target):
return isinstance(target.platform, Gem5SimulationPlatform)
def __init__(self, target):
super(Gem5StatsModule, self).__init__(target)
self._current_origin = 0
self._stats_file_path = os.path.join(target.platform.gem5_out_dir,
'stats.txt')
self.rois = {}
self._dump_pos_cache = {0: 0}
def book_roi(self, label):
if label in self.rois:
raise KeyError('ROI label {} already used'.format(label))
if len(self.rois) >= GEM5STATS_ROI_NUMBER:
raise RuntimeError('Too many ROIs reserved')
all_rois = set(range(GEM5STATS_ROI_NUMBER))
used_rois = set([roi.number for roi in self.rois.values()])
avail_rois = all_rois - used_rois
self.rois[label] = Gem5ROI(list(avail_rois)[0], self.target)
def free_roi(self, label):
if label not in self.rois:
raise KeyError('ROI label {} not reserved yet'.format(label))
self.rois[label].stop()
del self.rois[label]
def roi_start(self, label):
if label not in self.rois:
raise KeyError('Incorrect ROI label: {}'.format(label))
if not self.rois[label].start():
raise TargetError('ROI {} was already running'.format(label))
def roi_end(self, label):
if label not in self.rois:
raise KeyError('Incorrect ROI label: {}'.format(label))
if not self.rois[label].stop():
raise TargetError('ROI {} was not running'.format(label))
def start_periodic_dump(self, delay_ns=0, period_ns=10000000):
# Default period is 10ms because it's roughly what's needed to have
# accurate power estimations
if delay_ns < 0 or period_ns < 0:
msg = 'Delay ({}) and period ({}) for periodic dumps must be positive'
raise ValueError(msg.format(delay_ns, period_ns))
self.target.execute('m5 dumpresetstats {} {}'.format(delay_ns, period_ns))
def match(self, keys, rois_labels, base_dump=0):
'''
Extract specific values from the statistics log file of gem5
:param keys: a list of key name or regular expression patterns that
will be matched in the fields of the statistics file. ``match()``
returns only the values of fields matching at least one these
keys.
:type keys: list
:param rois_labels: list of ROIs labels. ``match()`` returns the
values of the specified fields only during dumps spanned by at
least one of these ROIs.
:type rois_label: list
:param base_dump: dump number from which ``match()`` should operate. By
specifying a non-zero dump number, one can virtually truncate
the head of the stats file and ignore all dumps before a specific
instant. The value of ``base_dump`` will typically (but not
necessarily) be the result of a previous call to ``next_dump_no``.
Default value is 0.
:type base_dump: int
:returns: a dict indexed by key parameters containing a dict indexed by
ROI labels containing an in-order list of records for the key under
consideration during the active intervals of the ROI.
Example of return value:
* Result of match(['sim_'],['roi_1']):
{
'sim_inst':
{
'roi_1': [265300176, 267975881]
}
'sim_ops':
{
'roi_1': [324395787, 327699419]
}
'sim_seconds':
{
'roi_1': [0.199960, 0.199897]
}
'sim_freq':
{
'roi_1': [1000000000000, 1000000000000]
}
'sim_ticks':
{
'roi_1': [199960234227, 199896897330]
}
}
'''
records = defaultdict(lambda : defaultdict(list))
for record, active_rois in self.match_iter(keys, rois_labels, base_dump):
for key in record:
for roi_label in active_rois:
records[key][roi_label].append(record[key])
return records
def match_iter(self, keys, rois_labels, base_dump=0):
'''
Yield specific values dump-by-dump from the statistics log file of gem5
:param keys: same as ``match()``
:param rois_labels: same as ``match()``
:param base_dump: same as ``match()``
:returns: a pair containing:
1. a dict storing the values corresponding to each of the found keys
2. the list of currently active ROIs among those passed as parameters
Example of return value:
* Result of match_iter(['sim_'],['roi_1', 'roi_2']).next()
(
{
'sim_inst': 265300176,
'sim_ops': 324395787,
'sim_seconds': 0.199960,
'sim_freq': 1000000000000,
'sim_ticks': 199960234227,
},
[ 'roi_1 ' ]
)
'''
for label in rois_labels:
if label not in self.rois:
raise KeyError('Impossible to match ROI label {}'.format(label))
if self.rois[label].running:
self.logger.warning('Trying to match records in statistics file'
' while ROI {} is running'.format(label))
# Construct one large regex that concatenates all keys because
# matching one large expression is more efficient than several smaller
all_keys_re = re.compile('|'.join(keys))
def roi_active(roi_label, dump):
roi = self.rois[roi_label]
return (roi.field in dump) and (int(dump[roi.field]) == 1)
with open(self._stats_file_path, 'r') as stats_file:
self._goto_dump(stats_file, base_dump)
for dump in iter_statistics_dump(stats_file):
active_rois = [l for l in rois_labels if roi_active(l, dump)]
if active_rois:
rec = {k: dump[k] for k in dump if all_keys_re.search(k)}
yield (rec, active_rois)
def next_dump_no(self):
'''
Returns the number of the next dump to be written to the stats file.
For example, if next_dump_no is called while there are 5 (0 to 4) full
dumps in the stats file, it will return 5. This will be usefull to know
from which dump one should match() in the future to get only data from
now on.
'''
with open(self._stats_file_path, 'r') as stats_file:
# _goto_dump reach EOF and returns the total number of dumps + 1
return self._goto_dump(stats_file, sys.maxsize)
def _goto_dump(self, stats_file, target_dump):
if target_dump < 0:
raise HostError('Cannot go to dump {}'.format(target_dump))
# Go to required dump quickly if it was visited before
if target_dump in self._dump_pos_cache:
stats_file.seek(self._dump_pos_cache[target_dump])
return target_dump
# Or start from the closest dump already visited before the required one
prev_dumps = filter(lambda x: x < target_dump, self._dump_pos_cache.keys())
curr_dump = max(prev_dumps)
curr_pos = self._dump_pos_cache[curr_dump]
stats_file.seek(curr_pos)
# And iterate until target_dump
dump_iterator = iter_statistics_dump(stats_file)
while curr_dump < target_dump:
try:
dump = next(dump_iterator)
except StopIteration:
break
# End of passed dump is beginning og next one
curr_pos = stats_file.tell()
curr_dump += 1
self._dump_pos_cache[curr_dump] = curr_pos
return curr_dump

90
devlib/module/gpufreq.py Normal file
View File

@@ -0,0 +1,90 @@
# Copyright 2018 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.
#
# Copyright 2017 Google, 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 re
import json
from devlib.module import Module
from devlib.exception import TargetError
from devlib.utils.misc import memoized
class GpufreqModule(Module):
name = 'gpufreq'
path = ''
def __init__(self, target):
super(GpufreqModule, self).__init__(target)
frequencies_str = self.target.read_value("/sys/kernel/gpu/gpu_freq_table")
self.frequencies = list(map(int, frequencies_str.split(" ")))
self.frequencies.sort()
self.governors = self.target.read_value("/sys/kernel/gpu/gpu_available_governor").split(" ")
@staticmethod
def probe(target):
# kgsl/Adreno
probe_path = '/sys/kernel/gpu/'
if target.file_exists(probe_path):
model = target.read_value(probe_path + "gpu_model")
if re.search('adreno', model, re.IGNORECASE):
return True
return False
def set_governor(self, governor):
if governor not in self.governors:
raise TargetError('Governor {} not supported for gpu {}'.format(governor, cpu))
self.target.write_value("/sys/kernel/gpu/gpu_governor", governor)
def get_frequencies(self):
"""
Returns the list of frequencies that the GPU can have
"""
return self.frequencies
def get_current_frequency(self):
"""
Returns the current frequency currently set for the GPU.
Warning, this method does not check if the gpu is online or not. It will
try to read the current frequency and the following exception will be
raised ::
:raises: TargetError if for some reason the frequency could not be read.
"""
return int(self.target.read_value("/sys/kernel/gpu/gpu_clock"))
@memoized
def get_model_name(self):
"""
Returns the model name reported by the GPU.
"""
try:
return self.target.read_value("/sys/kernel/gpu/gpu_model")
except:
return "unknown"

View File

@@ -1,3 +1,18 @@
# Copyright 2018 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 devlib.module import Module from devlib.module import Module
@@ -21,7 +36,8 @@ class HotplugModule(Module):
return target.path.join(cls.base_path, cpu, 'online') return target.path.join(cls.base_path, cpu, 'online')
def online_all(self): def online_all(self):
self.online(*range(self.target.number_of_cpus)) self.target._execute_util('hotplug_online_all',
as_root=self.target.is_rooted)
def online(self, *args): def online(self, *args):
for cpu in args: for cpu in args:

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -12,9 +12,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
import os
import re import re
from collections import defaultdict from collections import defaultdict
from devlib import TargetError
from devlib.module import Module from devlib.module import Module
from devlib.utils.types import integer from devlib.utils.types import integer
@@ -73,20 +75,19 @@ class HwmonDevice(object):
@property @property
def sensors(self): def sensors(self):
all_sensors = [] all_sensors = []
for sensors_of_kind in self._sensors.itervalues(): for sensors_of_kind in self._sensors.values():
all_sensors.extend(sensors_of_kind.values()) all_sensors.extend(list(sensors_of_kind.values()))
return all_sensors return all_sensors
def __init__(self, target, path): def __init__(self, target, path, name, fields):
self.target = target self.target = target
self.path = path self.path = path
self.name = self.target.read_value(self.target.path.join(self.path, 'name')) self.name = name
self._sensors = defaultdict(dict) self._sensors = defaultdict(dict)
path = self.path path = self.path
if not path.endswith(self.target.path.sep): if not path.endswith(self.target.path.sep):
path += self.target.path.sep path += self.target.path.sep
for entry in self.target.list_directory(path, for entry in fields:
as_root=self.target.is_rooted):
match = HWMON_FILE_REGEX.search(entry) match = HWMON_FILE_REGEX.search(entry)
if match: if match:
kind = match.group('kind') kind = match.group('kind')
@@ -99,7 +100,7 @@ class HwmonDevice(object):
def get(self, kind, number=None): def get(self, kind, number=None):
if number is None: if number is None:
return [s for _, s in sorted(self._sensors[kind].iteritems(), return [s for _, s in sorted(self._sensors[kind].items(),
key=lambda x: x[0])] key=lambda x: x[0])]
else: else:
return self._sensors[kind].get(number) return self._sensors[kind].get(number)
@@ -116,7 +117,12 @@ class HwmonModule(Module):
@staticmethod @staticmethod
def probe(target): def probe(target):
return target.file_exists(HWMON_ROOT) try:
target.list_directory(HWMON_ROOT, as_root=target.is_rooted)
except TargetError:
# Doesn't exist or no permissions
return False
return True
@property @property
def sensors(self): def sensors(self):
@@ -132,11 +138,13 @@ class HwmonModule(Module):
self.scan() self.scan()
def scan(self): def scan(self):
for entry in self.target.list_directory(self.root, values_tree = self.target.read_tree_values(self.root, depth=3)
as_root=self.target.is_rooted): for entry_id, fields in values_tree.items():
if entry.startswith('hwmon'): path = self.target.path.join(self.root, entry_id)
entry_path = self.target.path.join(self.root, entry) name = fields.pop('name', None)
if self.target.file_exists(self.target.path.join(entry_path, 'name')): if name is None:
device = HwmonDevice(self.target, entry_path) continue
self.logger.debug('Adding device {}'.format(name))
device = HwmonDevice(self.target, path, name, fields)
self.devices.append(device) self.devices.append(device)

334
devlib/module/sched.py Normal file
View File

@@ -0,0 +1,334 @@
# Copyright 2018 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.
#
# Copyright 2018 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import re
from devlib.module import Module
from devlib.utils.misc import memoized
from past.builtins import basestring
class SchedProcFSNode(object):
"""
Represents a sched_domain procfs node
:param nodes: Dictionnary view of the underlying procfs nodes
(as returned by devlib.read_tree_values())
:type nodes: dict
Say you want to represent this path/data:
$ cat /proc/sys/kernel/sched_domain/cpu0/domain*/name
MC
DIE
Taking cpu0 as a root, this can be defined as:
>>> data = {"domain0" : {"name" : "MC"}, "domain1" : {"name" : "DIE"}}
>>> repr = SchedProcFSNode(data)
>>> print repr.domains[0].name
MC
The "raw" dict remains available under the `procfs` field:
>>> print repr.procfs["domain0"]["name"]
MC
"""
_re_procfs_node = re.compile(r"(?P<name>.*)(?P<digits>\d+)$")
@staticmethod
def _ends_with_digits(node):
if not isinstance(node, basestring):
return False
return re.search(SchedProcFSNode._re_procfs_node, node) != None
@staticmethod
def _node_digits(node):
"""
:returns: The ending digits of the procfs node
"""
return int(re.search(SchedProcFSNode._re_procfs_node, node).group("digits"))
@staticmethod
def _node_name(node):
"""
:returns: The name of the procfs node
"""
return re.search(SchedProcFSNode._re_procfs_node, node).group("name")
@staticmethod
def _packable(node, entries):
"""
:returns: Whether it makes sense to pack a node into a common entry
"""
return (SchedProcFSNode._ends_with_digits(node) and
any([SchedProcFSNode._ends_with_digits(x) and
SchedProcFSNode._node_digits(x) != SchedProcFSNode._node_digits(node) and
SchedProcFSNode._node_name(x) == SchedProcFSNode._node_name(node)
for x in entries]))
@staticmethod
def _build_directory(node_name, node_data):
if node_name.startswith("domain"):
return SchedDomain(node_data)
else:
return SchedProcFSNode(node_data)
@staticmethod
def _build_entry(node_name, node_data):
value = node_data
# Most nodes just contain numerical data, try to convert
try:
value = int(value)
except ValueError:
pass
return value
@staticmethod
def _build_node(node_name, node_data):
if isinstance(node_data, dict):
return SchedProcFSNode._build_directory(node_name, node_data)
else:
return SchedProcFSNode._build_entry(node_name, node_data)
def __getattr__(self, name):
return self._dyn_attrs[name]
def __init__(self, nodes):
self.procfs = nodes
# First, reduce the procs fields by packing them if possible
# Find which entries can be packed into a common entry
packables = {
node : SchedProcFSNode._node_name(node) + "s"
for node in list(nodes.keys()) if SchedProcFSNode._packable(node, list(nodes.keys()))
}
self._dyn_attrs = {}
for dest in set(packables.values()):
self._dyn_attrs[dest] = {}
# Pack common entries
for key, dest in packables.items():
i = SchedProcFSNode._node_digits(key)
self._dyn_attrs[dest][i] = self._build_node(key, nodes[key])
# Build the other nodes
for key in nodes.keys():
if key in packables:
continue
self._dyn_attrs[key] = self._build_node(key, nodes[key])
class SchedDomain(SchedProcFSNode):
"""
Represents a sched domain as seen through procfs
"""
# Domain flags obtained from include/linux/sched/topology.h on v4.17
# https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux/+/v4.17/include/linux/sched/topology.h#20
SD_LOAD_BALANCE = 0x0001 # Do load balancing on this domain.
SD_BALANCE_NEWIDLE = 0x0002 # Balance when about to become idle
SD_BALANCE_EXEC = 0x0004 # Balance on exec
SD_BALANCE_FORK = 0x0008 # Balance on fork, clone
SD_BALANCE_WAKE = 0x0010 # Balance on wakeup
SD_WAKE_AFFINE = 0x0020 # Wake task to waking CPU
SD_ASYM_CPUCAPACITY = 0x0040 # Groups have different max cpu capacities
SD_SHARE_CPUCAPACITY = 0x0080 # Domain members share cpu capacity
SD_SHARE_POWERDOMAIN = 0x0100 # Domain members share power domain
SD_SHARE_PKG_RESOURCES = 0x0200 # Domain members share cpu pkg resources
SD_SERIALIZE = 0x0400 # Only a single load balancing instance
SD_ASYM_PACKING = 0x0800 # Place busy groups earlier in the domain
SD_PREFER_SIBLING = 0x1000 # Prefer to place tasks in a sibling domain
SD_OVERLAP = 0x2000 # sched_domains of this level overlap
SD_NUMA = 0x4000 # cross-node balancing
# Only defined in Android
# https://android.googlesource.com/kernel/common/+/android-4.14/include/linux/sched/topology.h#29
SD_SHARE_CAP_STATES = 0x8000 # Domain members share capacity state
# Checked to be valid from v4.4
SD_FLAGS_REF_PARTS = (4, 4, 0)
@staticmethod
def check_version(target, logger):
"""
Check the target and see if its kernel version matches our view of the world
"""
parts = target.kernel_version.parts
if parts < SchedDomain.SD_FLAGS_REF_PARTS:
logger.warn(
"Sched domain flags are defined for kernels v{} and up, "
"but target is running v{}".format(SchedDomain.SD_FLAGS_REF_PARTS, parts)
)
def has_flags(self, flags):
"""
:returns: Whether 'flags' are set on this sched domain
"""
return self.flags & flags == flags
class SchedProcFSData(SchedProcFSNode):
"""
Root class for creating & storing SchedProcFSNode instances
"""
_read_depth = 6
sched_domain_root = '/proc/sys/kernel/sched_domain'
@staticmethod
def available(target):
return target.directory_exists(SchedProcFSData.sched_domain_root)
def __init__(self, target, path=None):
if not path:
path = self.sched_domain_root
procfs = target.read_tree_values(path, depth=self._read_depth)
super(SchedProcFSData, self).__init__(procfs)
class SchedModule(Module):
name = 'sched'
cpu_sysfs_root = '/sys/devices/system/cpu'
@staticmethod
def probe(target):
logger = logging.getLogger(SchedModule.name)
SchedDomain.check_version(target, logger)
return SchedProcFSData.available(target)
def get_cpu_sd_info(self, cpu):
"""
:returns: An object view of /proc/sys/kernel/sched_domain/cpu<cpu>/*
"""
path = self.target.path.join(
SchedProcFSData.sched_domain_root,
"cpu{}".format(cpu)
)
return SchedProcFSData(self.target, path)
def get_sd_info(self):
"""
:returns: An object view of /proc/sys/kernel/sched_domain/*
"""
return SchedProcFSData(self.target)
def get_capacity(self, cpu):
"""
:returns: The capacity of 'cpu'
"""
return self.get_capacities()[cpu]
@memoized
def has_em(self, cpu, sd=None):
"""
:returns: Whether energy model data is available for 'cpu'
"""
if not sd:
sd = SchedProcFSData(self.target, cpu)
return sd.procfs["domain0"].get("group0", {}).get("energy", {}).get("cap_states") != None
@memoized
def has_dmips_capacity(self, cpu):
"""
:returns: Whether dmips capacity data is available for 'cpu'
"""
return self.target.file_exists(
self.target.path.join(self.cpu_sysfs_root, 'cpu{}/cpu_capacity'.format(cpu))
)
@memoized
def get_em_capacity(self, cpu, sd=None):
"""
:returns: The maximum capacity value exposed by the EAS energy model
"""
if not sd:
sd = SchedProcFSData(self.target, cpu)
cap_states = sd.domains[0].groups[0].energy.cap_states
return int(cap_states.split('\t')[-2])
@memoized
def get_dmips_capacity(self, cpu):
"""
:returns: The capacity value generated from the capacity-dmips-mhz DT entry
"""
return self.target.read_value(
self.target.path.join(
self.cpu_sysfs_root,
'cpu{}/cpu_capacity'.format(cpu)
),
int
)
@memoized
def get_capacities(self, default=None):
"""
:param default: Default capacity value to find if no data is
found in procfs
:returns: a dictionnary of the shape {cpu : capacity}
:raises RuntimeError: Raised when no capacity information is
found and 'default' is None
"""
cpus = list(range(self.target.number_of_cpus))
capacities = {}
sd_info = self.get_sd_info()
for cpu in cpus:
if self.has_em(cpu, sd_info.cpus[cpu]):
capacities[cpu] = self.get_em_capacity(cpu, sd_info.cpus[cpu])
elif self.has_dmips_capacity(cpu):
capacities[cpu] = self.get_dmips_capacity(cpu)
else:
if default != None:
capacities[cpu] = default
else:
raise RuntimeError('No capacity data for cpu{}'.format(cpu))
return capacities
@memoized
def get_hz(self):
"""
:returns: The scheduler tick frequency on the target
"""
return int(self.target.config.get('CONFIG_HZ', strict=True))

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -61,8 +61,8 @@ class ThermalZone(object):
value = self.target.read_value(self.target.path.join(self.path, 'mode')) value = self.target.read_value(self.target.path.join(self.path, 'mode'))
return value == 'enabled' return value == 'enabled'
def set_mode(self, enable): def set_enabled(self, enabled=True):
value = 'enabled' if enable else 'disabled' value = 'enabled' if enabled else 'disabled'
self.target.write_value(self.target.path.join(self.path, 'mode'), value) self.target.write_value(self.target.path.join(self.path, 'mode'), value)
def get_temperature(self): def get_temperature(self):
@@ -100,5 +100,5 @@ class ThermalModule(Module):
def disable_all_zones(self): def disable_all_zones(self):
"""Disables all the thermal zones in the target""" """Disables all the thermal zones in the target"""
for zone in self.zones: for zone in self.zones.values():
zone.set_mode('disabled') zone.set_enabled(False)

View File

@@ -1,5 +1,5 @@
# #
# Copyright 2015 ARM Limited # Copyright 2015-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ import os
import time import time
import tarfile import tarfile
import shutil import shutil
from subprocess import CalledProcessError
from devlib.module import HardRestModule, BootModule, FlashModule from devlib.module import HardRestModule, BootModule, FlashModule
from devlib.exception import TargetError, HostError from devlib.exception import TargetError, HostError
@@ -25,7 +26,8 @@ from devlib.utils.uefi import UefiMenu, UefiConfig
from devlib.utils.uboot import UbootMenu from devlib.utils.uboot import UbootMenu
AUTOSTART_MESSAGE = 'Press Enter to stop auto boot...' OLD_AUTOSTART_MESSAGE = 'Press Enter to stop auto boot...'
AUTOSTART_MESSAGE = 'Hit any key to stop autoboot:'
POWERUP_MESSAGE = 'Powering up system...' POWERUP_MESSAGE = 'Powering up system...'
DEFAULT_MCC_PROMPT = 'Cmd>' DEFAULT_MCC_PROMPT = 'Cmd>'
@@ -51,7 +53,7 @@ class VexpressDtrHardReset(HardRestModule):
try: try:
if self.target.is_connected: if self.target.is_connected:
self.target.execute('sync') self.target.execute('sync')
except TargetError: except (TargetError, CalledProcessError):
pass pass
with open_serial_connection(port=self.port, with open_serial_connection(port=self.port,
baudrate=self.baudrate, baudrate=self.baudrate,
@@ -136,18 +138,20 @@ class VexpressBootModule(BootModule):
def get_through_early_boot(self, tty): def get_through_early_boot(self, tty):
self.logger.debug('Establishing initial state...') self.logger.debug('Establishing initial state...')
tty.sendline('') tty.sendline('')
i = tty.expect([AUTOSTART_MESSAGE, POWERUP_MESSAGE, self.mcc_prompt]) i = tty.expect([AUTOSTART_MESSAGE, OLD_AUTOSTART_MESSAGE, POWERUP_MESSAGE, self.mcc_prompt])
if i == 2: if i == 3:
self.logger.debug('Saw MCC prompt.') self.logger.debug('Saw MCC prompt.')
time.sleep(self.short_delay) time.sleep(self.short_delay)
tty.sendline('reboot') tty.sendline('reboot')
elif i == 1: elif i == 2:
self.logger.debug('Saw powering up message (assuming soft reboot).') self.logger.debug('Saw powering up message (assuming soft reboot).')
else: else:
self.logger.debug('Saw auto boot message.') self.logger.debug('Saw auto boot message.')
tty.sendline('') tty.sendline('')
time.sleep(self.short_delay) time.sleep(self.short_delay)
# could be either depending on where in the boot we are
tty.sendline('reboot') tty.sendline('reboot')
tty.sendline('reset')
def get_uefi_menu(self, tty): def get_uefi_menu(self, tty):
menu = UefiMenu(tty) menu = UefiMenu(tty)
@@ -247,7 +251,7 @@ class VexpressUBoot(VexpressBootModule):
menu = UbootMenu(tty) menu = UbootMenu(tty)
self.logger.debug('Waiting for U-Boot prompt...') self.logger.debug('Waiting for U-Boot prompt...')
menu.open(timeout=120) menu.open(timeout=120)
for var, value in self.env.iteritems(): for var, value in self.env.items():
menu.setenv(var, value) menu.setenv(var, value)
menu.boot() menu.boot()
@@ -324,7 +328,7 @@ class VersatileExpressFlashModule(FlashModule):
baudrate=self.target.platform.baudrate, baudrate=self.target.platform.baudrate,
timeout=self.timeout, timeout=self.timeout,
init_dtr=0) as tty: init_dtr=0) as tty:
i = tty.expect([self.mcc_prompt, AUTOSTART_MESSAGE]) i = tty.expect([self.mcc_prompt, AUTOSTART_MESSAGE, OLD_AUTOSTART_MESSAGE])
if i: if i:
tty.sendline('') tty.sendline('')
wait_for_vemsd(self.vemsd_mount, tty, self.mcc_prompt, self.short_delay) wait_for_vemsd(self.vemsd_mount, tty, self.mcc_prompt, self.short_delay)
@@ -334,7 +338,7 @@ class VersatileExpressFlashModule(FlashModule):
if images: if images:
self._overlay_images(images) self._overlay_images(images)
os.system('sync') os.system('sync')
except (IOError, OSError), e: except (IOError, OSError) as e:
msg = 'Could not deploy images to {}; got: {}' msg = 'Could not deploy images to {}; got: {}'
raise TargetError(msg.format(self.vemsd_mount, e)) raise TargetError(msg.format(self.vemsd_mount, e))
self.target.boot() self.target.boot()
@@ -348,7 +352,7 @@ class VersatileExpressFlashModule(FlashModule):
tar.extractall(self.vemsd_mount) tar.extractall(self.vemsd_mount)
def _overlay_images(self, images): def _overlay_images(self, images):
for dest, src in images.iteritems(): for dest, src in images.items():
dest = os.path.join(self.vemsd_mount, dest) dest = os.path.join(self.vemsd_mount, dest)
self.logger.debug('Copying {} to {}'.format(src, dest)) self.logger.debug('Copying {} to {}'.format(src, dest))
shutil.copy(src, dest) shutil.copy(src, dest)
@@ -375,7 +379,7 @@ def wait_for_vemsd(vemsd_mount, tty, mcc_prompt=DEFAULT_MCC_PROMPT, short_delay=
path = os.path.join(vemsd_mount, 'config.txt') path = os.path.join(vemsd_mount, 'config.txt')
if os.path.exists(path): if os.path.exists(path):
return return
for _ in xrange(attempts): for _ in range(attempts):
tty.sendline('') # clear any garbage tty.sendline('') # clear any garbage
tty.expect(mcc_prompt, timeout=short_delay) tty.expect(mcc_prompt, timeout=short_delay)
tty.sendline('usb_on') tty.sendline('usb_on')

View File

@@ -1,7 +1,22 @@
# Copyright 2018 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging import logging
BIG_CPUS = ['A15', 'A57', 'A72'] BIG_CPUS = ['A15', 'A57', 'A72', 'A73']
class Platform(object): class Platform(object):

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -14,8 +14,8 @@
# #
from __future__ import division from __future__ import division
import os import os
import sys
import tempfile import tempfile
import csv
import time import time
import pexpect import pexpect
@@ -23,6 +23,7 @@ from devlib.platform import Platform
from devlib.instrument import Instrument, InstrumentChannel, MeasurementsCsv, Measurement, CONTINUOUS, INSTANTANEOUS from devlib.instrument import Instrument, InstrumentChannel, MeasurementsCsv, Measurement, CONTINUOUS, INSTANTANEOUS
from devlib.exception import TargetError, HostError from devlib.exception import TargetError, HostError
from devlib.host import PACKAGE_BIN_DIRECTORY from devlib.host import PACKAGE_BIN_DIRECTORY
from devlib.utils.csvutil import csvreader, csvwriter
from devlib.utils.serial_port import open_serial_connection from devlib.utils.serial_port import open_serial_connection
@@ -33,6 +34,7 @@ class VersatileExpressPlatform(Platform):
core_names=None, core_names=None,
core_clusters=None, core_clusters=None,
big_core=None, big_core=None,
model=None,
modules=None, modules=None,
# serial settings # serial settings
@@ -61,6 +63,7 @@ class VersatileExpressPlatform(Platform):
core_names, core_names,
core_clusters, core_clusters,
big_core, big_core,
model,
modules) modules)
self.serial_port = serial_port self.serial_port = serial_port
self.baudrate = baudrate self.baudrate = baudrate
@@ -86,6 +89,9 @@ class VersatileExpressPlatform(Platform):
def _init_android_target(self, target): def _init_android_target(self, target):
if target.connection_settings.get('device') is None: if target.connection_settings.get('device') is None:
addr = self._get_target_ip_address(target) addr = self._get_target_ip_address(target)
if sys.version_info[0] == 3:
# Convert bytes to string for Python3 compatibility
addr = addr.decode("utf-8")
target.connection_settings['device'] = addr + ':5555' target.connection_settings['device'] = addr + ':5555'
def _init_linux_target(self, target): def _init_linux_target(self, target):
@@ -98,12 +104,14 @@ class VersatileExpressPlatform(Platform):
baudrate=self.baudrate, baudrate=self.baudrate,
timeout=30, timeout=30,
init_dtr=0) as tty: init_dtr=0) as tty:
tty.sendline('') tty.sendline('su') # this is, apprently, required to query network device
# info by name on recent Juno builds...
self.logger.debug('Waiting for the Android shell prompt.') self.logger.debug('Waiting for the Android shell prompt.')
tty.expect(target.shell_prompt) tty.expect(target.shell_prompt)
self.logger.debug('Waiting for IP address...') self.logger.debug('Waiting for IP address...')
wait_start_time = time.time() wait_start_time = time.time()
try:
while True: while True:
tty.sendline('ip addr list eth0') tty.sendline('ip addr list eth0')
time.sleep(1) time.sleep(1)
@@ -114,6 +122,8 @@ class VersatileExpressPlatform(Platform):
pass # We have our own timeout -- see below. pass # We have our own timeout -- see below.
if (time.time() - wait_start_time) > self.ready_timeout: if (time.time() - wait_start_time) > self.ready_timeout:
raise TargetError('Could not acquire IP address.') raise TargetError('Could not acquire IP address.')
finally:
tty.sendline('exit') # exit shell created by "su" call at the start
def _set_hard_reset_method(self, hard_reset_method): def _set_hard_reset_method(self, hard_reset_method):
if hard_reset_method == 'dtr': if hard_reset_method == 'dtr':
@@ -210,22 +220,22 @@ class JunoEnergyInstrument(Instrument):
mode = CONTINUOUS | INSTANTANEOUS mode = CONTINUOUS | INSTANTANEOUS
_channels = [ _channels = [
InstrumentChannel('sys_curr', 'sys', 'current'), InstrumentChannel('sys', 'current'),
InstrumentChannel('a57_curr', 'a57', 'current'), InstrumentChannel('a57', 'current'),
InstrumentChannel('a53_curr', 'a53', 'current'), InstrumentChannel('a53', 'current'),
InstrumentChannel('gpu_curr', 'gpu', 'current'), InstrumentChannel('gpu', 'current'),
InstrumentChannel('sys_volt', 'sys', 'voltage'), InstrumentChannel('sys', 'voltage'),
InstrumentChannel('a57_volt', 'a57', 'voltage'), InstrumentChannel('a57', 'voltage'),
InstrumentChannel('a53_volt', 'a53', 'voltage'), InstrumentChannel('a53', 'voltage'),
InstrumentChannel('gpu_volt', 'gpu', 'voltage'), InstrumentChannel('gpu', 'voltage'),
InstrumentChannel('sys_pow', 'sys', 'power'), InstrumentChannel('sys', 'power'),
InstrumentChannel('a57_pow', 'a57', 'power'), InstrumentChannel('a57', 'power'),
InstrumentChannel('a53_pow', 'a53', 'power'), InstrumentChannel('a53', 'power'),
InstrumentChannel('gpu_pow', 'gpu', 'power'), InstrumentChannel('gpu', 'power'),
InstrumentChannel('sys_cenr', 'sys', 'energy'), InstrumentChannel('sys', 'energy'),
InstrumentChannel('a57_cenr', 'a57', 'energy'), InstrumentChannel('a57', 'energy'),
InstrumentChannel('a53_cenr', 'a53', 'energy'), InstrumentChannel('a53', 'energy'),
InstrumentChannel('gpu_cenr', 'gpu', 'energy'), InstrumentChannel('gpu', 'energy'),
] ]
def __init__(self, target): def __init__(self, target):
@@ -243,9 +253,11 @@ class JunoEnergyInstrument(Instrument):
def setup(self): def setup(self):
self.binary = self.target.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.binary = self.target.install(os.path.join(PACKAGE_BIN_DIRECTORY,
self.target.abi, self.binname)) self.target.abi, self.binname))
self.command = '{} -o {}'.format(self.binary, self.on_target_file)
self.command2 = '{}'.format(self.binary)
def reset(self, sites=None, kinds=None): def reset(self, sites=None, kinds=None, channels=None):
super(JunoEnergyInstrument, self).reset(sites, kinds) super(JunoEnergyInstrument, self).reset(sites, kinds, channels)
self.target.killall(self.binname, as_root=True) self.target.killall(self.binname, as_root=True)
def start(self): def start(self):
@@ -259,9 +271,8 @@ class JunoEnergyInstrument(Instrument):
self.target.pull(self.on_target_file, temp_file) self.target.pull(self.on_target_file, temp_file)
self.target.remove(self.on_target_file) self.target.remove(self.on_target_file)
with open(temp_file, 'rb') as fh: with csvreader(temp_file) as reader:
reader = csv.reader(fh) headings = next(reader)
headings = reader.next()
# Figure out which columns from the collected csv we actually want # Figure out which columns from the collected csv we actually want
select_columns = [] select_columns = []
@@ -271,23 +282,22 @@ class JunoEnergyInstrument(Instrument):
except ValueError: except ValueError:
raise HostError('Channel "{}" is not in {}'.format(chan.name, temp_file)) raise HostError('Channel "{}" is not in {}'.format(chan.name, temp_file))
with open(output_file, 'wb') as wfh: with csvwriter(output_file) as writer:
write_headings = ['{}_{}'.format(c.site, c.kind) write_headings = ['{}_{}'.format(c.site, c.kind)
for c in self.active_channels] for c in self.active_channels]
writer = csv.writer(wfh)
writer.writerow(write_headings) writer.writerow(write_headings)
for row in reader: for row in reader:
write_row = [row[c] for c in select_columns] write_row = [row[c] for c in select_columns]
writer.writerow(write_row) writer.writerow(write_row)
return MeasurementsCsv(output_file, self.active_channels) return MeasurementsCsv(output_file, self.active_channels, sample_rate_hz=10)
def take_measurement(self): def take_measurement(self):
result = [] result = []
output = self.target.execute(self.command2).split() output = self.target.execute(self.command2).split()
reader=csv.reader(output) with csvreader(output) as reader:
headings=reader.next() headings=next(reader)
values = reader.next() values = next(reader)
for chan in self.active_channels: for chan in self.active_channels:
value = values[headings.index(chan.name)] value = values[headings.index(chan.name)]
result.append(Measurement(value, chan)) result.append(Measurement(value, chan))

View File

@@ -1,4 +1,4 @@
# Copyright 2016 ARM Limited # Copyright 2016-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -63,13 +63,12 @@ class Gem5SimulationPlatform(Platform):
# Find the first one that does not exist. Ensures that we do not re-use # Find the first one that does not exist. Ensures that we do not re-use
# the directory used by someone else. # the directory used by someone else.
for i in xrange(sys.maxint): i = 0
directory = os.path.join(self.gem5_interact_dir, "wa_{}".format(i)) directory = os.path.join(self.gem5_interact_dir, "wa_{}".format(i))
try: while os.path.exists(directory):
os.stat(directory) i += 1
continue directory = os.path.join(self.gem5_interact_dir, "wa_{}".format(i))
except OSError:
break
self.gem5_interact_dir = directory self.gem5_interact_dir = directory
self.logger.debug("Using {} as the temporary directory." self.logger.debug("Using {} as the temporary directory."
.format(self.gem5_interact_dir)) .format(self.gem5_interact_dir))
@@ -168,12 +167,17 @@ class Gem5SimulationPlatform(Platform):
while self.gem5_port is None: while self.gem5_port is None:
# Check that gem5 is running! # Check that gem5 is running!
if self.gem5.poll(): if self.gem5.poll():
raise TargetError("The gem5 process has crashed with error code {}!".format(self.gem5.poll())) message = "The gem5 process has crashed with error code {}!\n\tPlease see {} for details."
raise TargetError(message.format(self.gem5.poll(), self.stderr_file.name))
# Open the stderr file # Open the stderr file
with open(self.stderr_filename, 'r') as f: with open(self.stderr_filename, 'r') as f:
for line in f: for line in f:
# Look for two different strings, exact wording depends on
# version of gem5
m = re.search(r"Listening for system connection on port (?P<port>\d+)", line) m = re.search(r"Listening for system connection on port (?P<port>\d+)", line)
if not m:
m = re.search(r"Listening for connections on port (?P<port>\d+)", line)
if m: if m:
port = int(m.group('port')) port = int(m.group('port'))
if port >= 3456 and port < 5900: if port >= 3456 and port < 5900:
@@ -200,8 +204,6 @@ class Gem5SimulationPlatform(Platform):
""" """
Deploy m5 if not yet installed Deploy m5 if not yet installed
""" """
m5_path = target.get_installed('m5')
if m5_path is None:
m5_path = self._deploy_m5(target) m5_path = self._deploy_m5(target)
target.conn.m5_path = m5_path target.conn.m5_path = m5_path
@@ -239,6 +241,11 @@ class Gem5SimulationPlatform(Platform):
if '.bmp' in f: if '.bmp' in f:
screen_caps.append(f) screen_caps.append(f)
if '{ts}' in filepath:
cmd = '{} date -u -Iseconds'
ts = self.target.execute(cmd.format(self.target.busybox)).strip()
filepath = filepath.format(ts=ts)
successful_capture = False successful_capture = False
if len(screen_caps) == 1: if len(screen_caps) == 1:
# Bail out if we do not have image, and resort to the slower, built # Bail out if we do not have image, and resort to the slower, built

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,18 @@
# 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 logging import logging

View File

@@ -1,4 +1,4 @@
# Copyright 2015 ARM Limited # Copyright 2015-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import json
import time import time
import re import re
import subprocess import subprocess
import sys
from devlib.trace import TraceCollector from devlib.trace import TraceCollector
from devlib.host import PACKAGE_BIN_DIRECTORY from devlib.host import PACKAGE_BIN_DIRECTORY
@@ -60,6 +61,7 @@ class FtraceCollector(TraceCollector):
autoview=False, autoview=False,
no_install=False, no_install=False,
strict=False, strict=False,
report_on_target=False,
): ):
super(FtraceCollector, self).__init__(target) super(FtraceCollector, self).__init__(target)
self.events = events if events is not None else DEFAULT_EVENTS self.events = events if events is not None else DEFAULT_EVENTS
@@ -70,7 +72,10 @@ class FtraceCollector(TraceCollector):
self.automark = automark self.automark = automark
self.autoreport = autoreport self.autoreport = autoreport
self.autoview = autoview self.autoview = autoview
self.target_output_file = os.path.join(self.target.working_directory, OUTPUT_TRACE_FILE) self.report_on_target = report_on_target
self.target_output_file = target.path.join(self.target.working_directory, OUTPUT_TRACE_FILE)
text_file_name = target.path.splitext(OUTPUT_TRACE_FILE)[0] + '.txt'
self.target_text_file = target.path.join(self.target.working_directory, text_file_name)
self.target_binary = None self.target_binary = None
self.host_binary = None self.host_binary = None
self.start_time = None self.start_time = None
@@ -93,7 +98,7 @@ class FtraceCollector(TraceCollector):
if not self.target.is_rooted: if not self.target.is_rooted:
raise TargetError('trace-cmd instrument cannot be used on an unrooted device.') raise TargetError('trace-cmd instrument cannot be used on an unrooted device.')
if self.autoreport and self.host_binary is None: if self.autoreport and not self.report_on_target and self.host_binary is None:
raise HostError('trace-cmd binary must be installed on the host if autoreport=True.') raise HostError('trace-cmd binary must be installed on the host if autoreport=True.')
if self.autoview and self.kernelshark is None: if self.autoview and self.kernelshark is None:
raise HostError('kernelshark binary must be installed on the host if autoview=True.') raise HostError('kernelshark binary must be installed on the host if autoview=True.')
@@ -117,7 +122,7 @@ class FtraceCollector(TraceCollector):
_event = '*' + event _event = '*' + event
event_re = re.compile(_event.replace('*', '.*')) event_re = re.compile(_event.replace('*', '.*'))
# Select events matching the required ones # Select events matching the required ones
if len(filter(event_re.match, available_events)) == 0: if len(list(filter(event_re.match, available_events))) == 0:
message = 'Event [{}] not available for tracing'.format(event) message = 'Event [{}] not available for tracing'.format(event)
if strict: if strict:
raise TargetError(message) raise TargetError(message)
@@ -202,20 +207,26 @@ class FtraceCollector(TraceCollector):
def get_trace(self, outfile): def get_trace(self, outfile):
if os.path.isdir(outfile): if os.path.isdir(outfile):
outfile = os.path.join(outfile, os.path.dirname(self.target_output_file)) outfile = os.path.join(outfile, os.path.basename(self.target_output_file))
self.target.execute('{} extract -o {}'.format(self.target_binary, self.target_output_file), self.target.execute('{0} extract -o {1}; chmod 666 {1}'.format(self.target_binary,
self.target_output_file),
timeout=TIMEOUT, as_root=True) timeout=TIMEOUT, as_root=True)
# The size of trace.dat will depend on how long trace-cmd was running. # The size of trace.dat will depend on how long trace-cmd was running.
# Therefore timout for the pull command must also be adjusted # Therefore timout for the pull command must also be adjusted
# accordingly. # accordingly.
pull_timeout = 5 * (self.stop_time - self.start_time) pull_timeout = 10 * (self.stop_time - self.start_time)
self.target.pull(self.target_output_file, outfile, timeout=pull_timeout) self.target.pull(self.target_output_file, outfile, timeout=pull_timeout)
if not os.path.isfile(outfile): if not os.path.isfile(outfile):
self.logger.warning('Binary trace not pulled from device.') self.logger.warning('Binary trace not pulled from device.')
else: else:
if self.autoreport: if self.autoreport:
textfile = os.path.splitext(outfile)[0] + '.txt' textfile = os.path.splitext(outfile)[0] + '.txt'
if self.report_on_target:
self.generate_report_on_target()
self.target.pull(self.target_text_file,
textfile, timeout=pull_timeout)
else:
self.report(outfile, textfile) self.report(outfile, textfile)
if self.autoview: if self.autoview:
self.view(outfile) self.view(outfile)
@@ -266,6 +277,8 @@ class FtraceCollector(TraceCollector):
self.logger.debug(command) self.logger.debug(command)
process = subprocess.Popen(command, stderr=subprocess.PIPE, shell=True) process = subprocess.Popen(command, stderr=subprocess.PIPE, shell=True)
_, error = process.communicate() _, error = process.communicate()
if sys.version_info[0] == 3:
error = error.decode(sys.stdout.encoding, 'replace')
if process.returncode: if process.returncode:
raise TargetError('trace-cmd returned non-zero exit code {}'.format(process.returncode)) raise TargetError('trace-cmd returned non-zero exit code {}'.format(process.returncode))
if error: if error:
@@ -286,6 +299,12 @@ class FtraceCollector(TraceCollector):
except OSError: except OSError:
raise HostError('Could not find trace-cmd. Please make sure it is installed and is in PATH.') raise HostError('Could not find trace-cmd. Please make sure it is installed and is in PATH.')
def generate_report_on_target(self):
command = '{} report {} > {}'.format(self.target_binary,
self.target_output_file,
self.target_text_file)
self.target.execute(command, timeout=TIMEOUT)
def view(self, binfile): def view(self, binfile):
check_output('{} {}'.format(self.kernelshark, binfile), shell=True) check_output('{} {}'.format(self.kernelshark, binfile), shell=True)

73
devlib/trace/logcat.py Normal file
View File

@@ -0,0 +1,73 @@
# Copyright 2018 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 re
import shutil
from devlib.trace import TraceCollector
from devlib.utils.android import LogcatMonitor
class LogcatCollector(TraceCollector):
def __init__(self, target, regexps=None):
super(LogcatCollector, self).__init__(target)
self.regexps = regexps
self._collecting = False
self._prev_log = None
def reset(self):
"""
Clear Collector data but do not interrupt collection
"""
if not self._monitor:
return
if self._collecting:
self._monitor.clear_log()
elif self._prev_log:
os.remove(self._prev_log)
self._prev_log = None
def start(self):
"""
Start collecting logcat lines
"""
self._monitor = LogcatMonitor(self.target, self.regexps)
if self._prev_log:
# Append new data collection to previous collection
self._monitor.start(self._prev_log)
else:
self._monitor.start()
self._collecting = True
def stop(self):
"""
Stop collecting logcat lines
"""
if not self._collecting:
raise RuntimeError('Logcat monitor not running, nothing to stop')
self._monitor.stop()
self._collecting = False
self._prev_log = self._monitor.logfile
def get_trace(self, outfile):
"""
Output collected logcat lines to designated file
"""
# copy self._monitor.logfile to outfile
shutil.copy(self._monitor.logfile, outfile)

View File

@@ -0,0 +1,98 @@
# Copyright 2018 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
import os
import sys
import threading
import time
from devlib.trace import TraceCollector
from devlib.exception import WorkerThreadError
class ScreenCapturePoller(threading.Thread):
def __init__(self, target, period, output_path=None, timeout=30):
super(ScreenCapturePoller, self).__init__()
self.target = target
self.logger = logging.getLogger('screencapture')
self.period = period
self.timeout = timeout
self.stop_signal = threading.Event()
self.lock = threading.Lock()
self.last_poll = 0
self.daemon = True
self.exc = None
self.output_path = output_path
def run(self):
self.logger.debug('Starting screen capture 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())
def stop(self):
self.logger.debug('Stopping screen capture polling')
self.stop_signal.set()
self.join(self.timeout)
if self.is_alive():
self.logger.error('Could not join screen capture poller thread.')
if self.exc:
raise self.exc # pylint: disable=E0702
def poll(self):
self.last_poll = time.time()
self.target.capture_screen(os.path.join(self.output_path, "screencap_{ts}.png"))
class ScreenCaptureCollector(TraceCollector):
def __init__(self, target, output_path=None, period=None):
super(ScreenCaptureCollector, self).__init__(target)
self._collecting = False
self.output_path = output_path
self.period = period
self.target = target
self._poller = ScreenCapturePoller(self.target, self.period,
self.output_path)
def reset(self):
pass
def start(self):
"""
Start collecting the screenshots
"""
self._poller.start()
self._collecting = True
def stop(self):
"""
Stop collecting the screenshots
"""
if not self._collecting:
raise RuntimeError('Screen capture collector is not running, nothing to stop')
self._poller.stop()
self._collecting = False

View File

@@ -0,0 +1,92 @@
# Copyright 2018 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 pexpect.exceptions import TIMEOUT
import shutil
from tempfile import NamedTemporaryFile
from devlib.trace import TraceCollector
from devlib.utils.serial_port import get_connection
class SerialTraceCollector(TraceCollector):
@property
def collecting(self):
return self._collecting
def __init__(self, target, serial_port, baudrate, timeout=20):
super(SerialTraceCollector, self).__init__(target)
self.serial_port = serial_port
self.baudrate = baudrate
self.timeout = timeout
self._serial_target = None
self._conn = None
self._tmpfile = None
self._collecting = False
def reset(self):
if self._collecting:
raise RuntimeError("reset was called whilst collecting")
if self._tmpfile:
self._tmpfile.close()
self._tmpfile = None
def start(self):
if self._collecting:
raise RuntimeError("start was called whilst collecting")
self._tmpfile = NamedTemporaryFile()
self._tmpfile.write("-------- Starting serial logging --------\n")
self._serial_target, self._conn = get_connection(port=self.serial_port,
baudrate=self.baudrate,
timeout=self.timeout,
logfile=self._tmpfile,
init_dtr=0)
self._collecting = True
def stop(self):
if not self._collecting:
raise RuntimeError("stop was called whilst not collecting")
# We expect the below to fail, but we need to get pexpect to
# do something so that it interacts with the serial device,
# and hence updates the logfile.
try:
self._serial_target.expect(".", timeout=1)
except TIMEOUT:
pass
self._serial_target.close()
del self._conn
self._tmpfile.write("-------- Stopping serial logging --------\n")
self._collecting = False
def get_trace(self, outfile):
if self._collecting:
raise RuntimeError("get_trace was called whilst collecting")
self._tmpfile.flush()
shutil.copy(self._tmpfile.name, outfile)
self._tmpfile.close()
self._tmpfile = None

173
devlib/trace/systrace.py Normal file
View File

@@ -0,0 +1,173 @@
# Copyright 2018 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.
#
# Copyright 2018 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 subprocess
from shutil import copyfile
from tempfile import NamedTemporaryFile
from devlib.exception import TargetError, HostError
from devlib.trace import TraceCollector
from devlib.utils.android import platform_tools
from devlib.utils.misc import memoized
DEFAULT_CATEGORIES = [
'gfx',
'view',
'sched',
'freq',
'idle'
]
class SystraceCollector(TraceCollector):
"""
A trace collector based on Systrace
For more details, see https://developer.android.com/studio/command-line/systrace
:param target: Devlib target
:type target: AndroidTarget
:param outdir: Working directory to use on the host
:type outdir: str
:param categories: Systrace categories to trace. See `available_categories`
:type categories: list(str)
:param buffer_size: Buffer size in kb
:type buffer_size: int
:param strict: Raise an exception if any of the requested categories
are not available
:type strict: bool
"""
@property
@memoized
def available_categories(self):
lines = subprocess.check_output([self.systrace_binary, '-l']).splitlines()
categories = []
for line in lines:
categories.append(line.split()[0])
return categories
def __init__(self, target,
categories=None,
buffer_size=None,
strict=False):
super(SystraceCollector, self).__init__(target)
self.categories = categories or DEFAULT_CATEGORIES
self.buffer_size = buffer_size
self._systrace_process = None
self._tmpfile = None
# Try to find a systrace binary
self.systrace_binary = None
systrace_binary_path = os.path.join(platform_tools, 'systrace', 'systrace.py')
if not os.path.isfile(systrace_binary_path):
raise HostError('Could not find any systrace binary under {}'.format(platform_tools))
self.systrace_binary = systrace_binary_path
# Filter the requested categories
for category in self.categories:
if category not in self.available_categories:
message = 'Category [{}] not available for tracing'.format(category)
if strict:
raise TargetError(message)
self.logger.warning(message)
self.categories = list(set(self.categories) & set(self.available_categories))
if not self.categories:
raise TargetError('None of the requested categories are available')
def __del__(self):
self.reset()
def _build_cmd(self):
self._tmpfile = NamedTemporaryFile()
self.systrace_cmd = '{} -o {} -e {}'.format(
self.systrace_binary,
self._tmpfile.name,
self.target.adb_name
)
if self.buffer_size:
self.systrace_cmd += ' -b {}'.format(self.buffer_size)
self.systrace_cmd += ' {}'.format(' '.join(self.categories))
def reset(self):
if self._systrace_process:
self.stop()
if self._tmpfile:
self._tmpfile.close()
self._tmpfile = None
def start(self):
if self._systrace_process:
raise RuntimeError("Tracing is already underway, call stop() first")
self.reset()
self._build_cmd()
self._systrace_process = subprocess.Popen(
self.systrace_cmd,
stdin=subprocess.PIPE,
shell=True
)
def stop(self):
if not self._systrace_process:
raise RuntimeError("No tracing to stop, call start() first")
# Systrace expects <enter> to stop
self._systrace_process.communicate('\n')
self._systrace_process = None
def get_trace(self, outfile):
if self._systrace_process:
raise RuntimeError("Tracing is underway, call stop() first")
if not self._tmpfile:
raise RuntimeError("No tracing data available")
copyfile(self._tmpfile.name, outfile)

315
devlib/utils/android.py Normal file → Executable file
View File

@@ -1,4 +1,4 @@
# Copyright 2013-2015 ARM Limited # Copyright 2013-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -20,25 +20,36 @@ Utility functions for working with Android devices through adb.
""" """
# pylint: disable=E1103 # pylint: disable=E1103
import os import os
import pexpect
import time import time
import subprocess import subprocess
import logging import logging
import re import re
import threading
import tempfile
import queue
import sys
from collections import defaultdict from collections import defaultdict
from devlib.exception import TargetError, HostError, DevlibError from devlib.exception import TargetError, HostError, DevlibError
from devlib.utils.misc import check_output, which, memoized from devlib.utils.misc import check_output, which, memoized, ABI_MAP
from devlib.utils.misc import escape_single_quotes, escape_double_quotes from devlib.utils.misc import escape_single_quotes, escape_double_quotes
from devlib import host
logger = logging.getLogger('android') logger = logging.getLogger('android')
MAX_ATTEMPTS = 5 MAX_ATTEMPTS = 5
AM_START_ERROR = re.compile(r"Error: Activity class {[\w|.|/]*} does not exist") AM_START_ERROR = re.compile(r"Error: Activity.*")
# See: # See:
# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels # http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
ANDROID_VERSION_MAP = { ANDROID_VERSION_MAP = {
28: 'P',
27: 'OREO_MR1',
26: 'OREO',
25: 'NOUGAT_MR1',
24: 'NOUGAT',
23: 'MARSHMALLOW', 23: 'MARSHMALLOW',
22: 'LOLLYPOP_MR1', 22: 'LOLLYPOP_MR1',
21: 'LOLLYPOP', 21: 'LOLLYPOP',
@@ -64,6 +75,12 @@ ANDROID_VERSION_MAP = {
1: 'BASE', 1: 'BASE',
} }
# See https://developer.android.com/reference/android/content/Intent.html#setFlags(int)
INTENT_FLAGS = {
'ACTIVITY_NEW_TASK' : 0x10000000,
'ACTIVITY_CLEAR_TASK' : 0x00008000
}
# Initialized in functions near the botton of the file # Initialized in functions near the botton of the file
android_home = None android_home = None
@@ -83,7 +100,7 @@ class AndroidProperties(object):
self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text)) self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text))
def iteritems(self): def iteritems(self):
return self._properties.iteritems() return iter(self._properties.items())
def __iter__(self): def __iter__(self):
return iter(self._properties) return iter(self._properties)
@@ -116,6 +133,7 @@ class ApkInfo(object):
version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'") version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'")
name_regex = re.compile(r"name='(?P<name>[^']+)'") name_regex = re.compile(r"name='(?P<name>[^']+)'")
permission_regex = re.compile(r"name='(?P<permission>[^']+)'")
def __init__(self, path=None): def __init__(self, path=None):
self.path = path self.path = path
@@ -124,13 +142,21 @@ class ApkInfo(object):
self.label = None self.label = None
self.version_name = None self.version_name = None
self.version_code = None self.version_code = None
self.native_code = None
self.permissions = []
self.parse(path) self.parse(path)
def parse(self, apk_path): def parse(self, apk_path):
_check_env() _check_env()
command = [aapt, 'dump', 'badging', apk_path] command = [aapt, 'dump', 'badging', apk_path]
logger.debug(' '.join(command)) logger.debug(' '.join(command))
output = subprocess.check_output(command) try:
output = subprocess.check_output(command, stderr=subprocess.STDOUT)
if sys.version_info[0] == 3:
output = output.decode(sys.stdout.encoding, 'replace')
except subprocess.CalledProcessError as e:
raise HostError('Error parsing APK file {}. `aapt` says:\n{}'
.format(apk_path, e.output))
for line in output.split('\n'): for line in output.split('\n'):
if line.startswith('application-label:'): if line.startswith('application-label:'):
self.label = line.split(':')[1].strip().replace('\'', '') self.label = line.split(':')[1].strip().replace('\'', '')
@@ -143,6 +169,23 @@ class ApkInfo(object):
elif line.startswith('launchable-activity:'): elif line.startswith('launchable-activity:'):
match = self.name_regex.search(line) match = self.name_regex.search(line)
self.activity = match.group('name') self.activity = match.group('name')
elif line.startswith('native-code'):
apk_abis = [entry.strip() for entry in line.split(':')[1].split("'") if entry.strip()]
mapped_abis = []
for apk_abi in apk_abis:
found = False
for abi, architectures in ABI_MAP.items():
if apk_abi in architectures:
mapped_abis.append(abi)
found = True
break
if not found:
mapped_abis.append(apk_abi)
self.native_code = mapped_abis
elif line.startswith('uses-permission:'):
match = self.permission_regex.search(line)
if match:
self.permissions.append(match.group('permission'))
else: else:
pass # not interested pass # not interested
@@ -159,18 +202,6 @@ class AdbConnection(object):
def name(self): def name(self):
return self.device return self.device
@property
@memoized
def newline_separator(self):
output = adb_command(self.device,
"shell '({}); echo \"\n$?\"'".format(self.ls_command))
if output.endswith('\r\n'):
return '\r\n'
elif output.endswith('\n'):
return '\n'
else:
raise DevlibError("Unknown line ending")
# Again, we need to handle boards where the default output format from ls is # Again, we need to handle boards where the default output format from ls is
# single column *and* boards where the default output is multi-column. # single column *and* boards where the default output is multi-column.
# We need to do this purely because the '-1' option causes errors on older # We need to do this purely because the '-1' option causes errors on older
@@ -178,7 +209,7 @@ class AdbConnection(object):
def _setup_ls(self): def _setup_ls(self):
command = "shell '(ls -1); echo \"\n$?\"'" command = "shell '(ls -1); echo \"\n$?\"'"
try: try:
output = adb_command(self.device, command, timeout=self.timeout) output = adb_command(self.device, command, timeout=self.timeout, adb_server=self.adb_server)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise HostError( raise HostError(
'Failed to set up ls command on Android device. Output:\n' 'Failed to set up ls command on Android device. Output:\n'
@@ -189,13 +220,14 @@ class AdbConnection(object):
self.ls_command = 'ls -1' self.ls_command = 'ls -1'
else: else:
self.ls_command = 'ls' self.ls_command = 'ls'
logger.info("ls command is set to {}".format(self.ls_command)) logger.debug("ls command is set to {}".format(self.ls_command))
def __init__(self, device=None, timeout=None, platform=None): def __init__(self, device=None, timeout=None, platform=None, adb_server=None):
self.timeout = timeout if timeout is not None else self.default_timeout self.timeout = timeout if timeout is not None else self.default_timeout
if device is None: if device is None:
device = adb_get_device(timeout=timeout) device = adb_get_device(timeout=timeout, adb_server=adb_server)
self.device = device self.device = device
self.adb_server = adb_server
adb_connect(self.device) adb_connect(self.device)
AdbConnection.active_connections[self.device] += 1 AdbConnection.active_connections[self.device] += 1
self._setup_ls() self._setup_ls()
@@ -206,7 +238,7 @@ class AdbConnection(object):
command = "push '{}' '{}'".format(source, dest) command = "push '{}' '{}'".format(source, dest)
if not os.path.exists(source): if not os.path.exists(source):
raise HostError('No such file "{}"'.format(source)) raise HostError('No such file "{}"'.format(source))
return adb_command(self.device, command, timeout=timeout) return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
def pull(self, source, dest, timeout=None): def pull(self, source, dest, timeout=None):
if timeout is None: if timeout is None:
@@ -215,18 +247,18 @@ class AdbConnection(object):
if os.path.isdir(dest) and \ if os.path.isdir(dest) and \
('*' in source or '?' in source): ('*' in source or '?' in source):
command = 'shell {} {}'.format(self.ls_command, source) command = 'shell {} {}'.format(self.ls_command, source)
output = adb_command(self.device, command, timeout=timeout) output = adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
for line in output.splitlines(): for line in output.splitlines():
command = "pull '{}' '{}'".format(line.strip(), dest) command = "pull '{}' '{}'".format(line.strip(), dest)
adb_command(self.device, command, timeout=timeout) adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
return return
command = "pull '{}' '{}'".format(source, dest) command = "pull '{}' '{}'".format(source, dest)
return adb_command(self.device, command, timeout=timeout) return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
def execute(self, command, timeout=None, check_exit_code=False, def execute(self, command, timeout=None, check_exit_code=False,
as_root=False, strip_colors=True): as_root=False, strip_colors=True):
return adb_shell(self.device, command, timeout, check_exit_code, return adb_shell(self.device, command, timeout, check_exit_code,
as_root, self.newline_separator) as_root, adb_server=self.adb_server)
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False): def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
return adb_background_shell(self.device, command, stdout, stderr, as_root) return adb_background_shell(self.device, command, stdout, stderr, as_root)
@@ -258,7 +290,7 @@ def fastboot_flash_partition(partition, path_to_image):
fastboot_command(command) fastboot_command(command)
def adb_get_device(timeout=None): def adb_get_device(timeout=None, adb_server=None):
""" """
Returns the serial number of a connected android device. Returns the serial number of a connected android device.
@@ -267,13 +299,17 @@ def adb_get_device(timeout=None):
""" """
# TODO this is a hacky way to issue a adb command to all listed devices # TODO this is a hacky way to issue a adb command to all listed devices
# Ensure server is started so the 'daemon started successfully' message
# doesn't confuse the parsing below
adb_command(None, 'start-server', adb_server=adb_server)
# The output of calling adb devices consists of a heading line then # The output of calling adb devices consists of a heading line then
# a list of the devices sperated by new line # a list of the devices sperated by new line
# The last line is a blank new line. in otherwords, if there is a device found # The last line is a blank new line. in otherwords, if there is a device found
# then the output length is 2 + (1 for each device) # then the output length is 2 + (1 for each device)
start = time.time() start = time.time()
while True: while True:
output = adb_command(None, "devices").splitlines() # pylint: disable=E1103 output = adb_command(None, "devices", adb_server=adb_server).splitlines() # pylint: disable=E1103
output_length = len(output) output_length = len(output)
if output_length == 3: if output_length == 3:
# output[1] is the 2nd line in the output which has the device name # output[1] is the 2nd line in the output which has the device name
@@ -292,15 +328,12 @@ def adb_get_device(timeout=None):
def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS): def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
_check_env() _check_env()
# Connect is required only for ADB-over-IP
if "." not in device:
logger.debug('Device connected via USB, connect not required')
return
tries = 0 tries = 0
output = None output = None
while tries <= attempts: while tries <= attempts:
tries += 1 tries += 1
if device: if device:
if "." in device: # Connect is required only for ADB-over-IP
command = 'adb connect {}'.format(device) command = 'adb connect {}'.format(device)
logger.debug(command) logger.debug(command)
output, _ = check_output(command, shell=True, timeout=timeout) output, _ = check_output(command, shell=True, timeout=timeout)
@@ -329,7 +362,7 @@ def adb_disconnect(device):
def _ping(device): def _ping(device):
_check_env() _check_env()
device_string = ' -s {}'.format(device) if device else '' device_string = ' -s {}'.format(device) if device else ''
command = "adb{} shell \"ls / > /dev/null\"".format(device_string) command = "adb{} shell \"ls /data/local/tmp > /dev/null\"".format(device_string)
logger.debug(command) logger.debug(command)
result = subprocess.call(command, stderr=subprocess.PIPE, shell=True) result = subprocess.call(command, stderr=subprocess.PIPE, shell=True)
if not result: if not result:
@@ -339,11 +372,14 @@ def _ping(device):
def adb_shell(device, command, timeout=None, check_exit_code=False, def adb_shell(device, command, timeout=None, check_exit_code=False,
as_root=False, newline_separator='\r\n'): # NOQA as_root=False, adb_server=None): # NOQA
_check_env() _check_env()
if as_root: if as_root:
command = 'echo \'{}\' | su'.format(escape_single_quotes(command)) command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
device_part = ['-s', device] if device else [] device_part = []
if adb_server:
device_part = ['-H', adb_server]
device_part += ['-s', device] if device else []
# On older combinations of ADB/Android versions, the adb host command always # On older combinations of ADB/Android versions, the adb host command always
# exits with 0 if it was able to run the command on the target, even if the # exits with 0 if it was able to run the command on the target, even if the
@@ -353,12 +389,16 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
adb_shell_command = '({}); echo \"\n$?\"'.format(command) adb_shell_command = '({}); echo \"\n$?\"'.format(command)
actual_command = ['adb'] + device_part + ['shell', adb_shell_command] actual_command = ['adb'] + device_part + ['shell', adb_shell_command]
logger.debug('adb {} shell {}'.format(' '.join(device_part), command)) logger.debug('adb {} shell {}'.format(' '.join(device_part), command))
raw_output, error = check_output(actual_command, timeout, shell=False) try:
raw_output, _ = check_output(actual_command, timeout, shell=False, combined_output=True)
except subprocess.CalledProcessError as e:
raise TargetError(str(e))
if raw_output: if raw_output:
try: try:
output, exit_code, _ = raw_output.rsplit(newline_separator, 2) output, exit_code, _ = raw_output.replace('\r\n', '\n').replace('\r', '\n').rsplit('\n', 2)
except ValueError: except ValueError:
exit_code, _ = raw_output.rsplit(newline_separator, 1) exit_code, _ = raw_output.replace('\r\n', '\n').replace('\r', '\n').rsplit('\n', 1)
output = '' output = ''
else: # raw_output is empty else: # raw_output is empty
exit_code = '969696' # just because exit_code = '969696' # just because
@@ -366,23 +406,24 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
if check_exit_code: if check_exit_code:
exit_code = exit_code.strip() exit_code = exit_code.strip()
re_search = AM_START_ERROR.findall(output)
if exit_code.isdigit(): if exit_code.isdigit():
if int(exit_code): if int(exit_code):
message = ('Got exit code {}\nfrom target command: {}\n' message = ('Got exit code {}\nfrom target command: {}\n'
'STDOUT: {}\nSTDERR: {}') 'OUTPUT: {}')
raise TargetError(message.format(exit_code, command, output, error)) raise TargetError(message.format(exit_code, command, output))
elif AM_START_ERROR.findall(output): elif re_search:
message = 'Could not start activity; got the following:'
message += '\n{}'.format(AM_START_ERROR.findall(output)[0])
raise TargetError(message)
else: # not all digits
if AM_START_ERROR.findall(output):
message = 'Could not start activity; got the following:\n{}' message = 'Could not start activity; got the following:\n{}'
raise TargetError(message.format(AM_START_ERROR.findall(output)[0])) raise TargetError(message.format(re_search[0]))
else: # not all digits
if re_search:
message = 'Could not start activity; got the following:\n{}'
raise TargetError(message.format(re_search[0]))
else: else:
message = 'adb has returned early; did not get an exit code. '\ message = 'adb has returned early; did not get an exit code. '\
'Was kill-server invoked?' 'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\
raise TargetError(message) '-----'
raise TargetError(message.format(raw_output))
return output return output
@@ -401,8 +442,8 @@ def adb_background_shell(device, command,
return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True) return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
def adb_list_devices(): def adb_list_devices(adb_server=None):
output = adb_command(None, 'devices') output = adb_command(None, 'devices',adb_server=adb_server)
devices = [] devices = []
for line in output.splitlines(): for line in output.splitlines():
parts = [p.strip() for p in line.split()] parts = [p.strip() for p in line.split()]
@@ -411,14 +452,39 @@ def adb_list_devices():
return devices return devices
def adb_command(device, command, timeout=None): def get_adb_command(device, command, timeout=None,adb_server=None):
_check_env() _check_env()
device_string = ' -s {}'.format(device) if device else '' device_string = ""
full_command = "adb{} {}".format(device_string, command) if adb_server != None:
device_string = ' -H {}'.format(adb_server)
device_string += ' -s {}'.format(device) if device else ''
return "adb{} {}".format(device_string, command)
def adb_command(device, command, timeout=None,adb_server=None):
full_command = get_adb_command(device, command, timeout, adb_server)
logger.debug(full_command) logger.debug(full_command)
output, _ = check_output(full_command, timeout, shell=True) output, _ = check_output(full_command, timeout, shell=True)
return output return output
def grant_app_permissions(target, package):
"""
Grant an app all the permissions it may ask for
"""
dumpsys = target.execute('dumpsys package {}'.format(package))
permissions = re.search(
'requested permissions:\s*(?P<permissions>(android.permission.+\s*)+)', dumpsys
)
if permissions is None:
return
permissions = permissions.group('permissions').replace(" ", "").splitlines()
for permission in permissions:
try:
target.execute('pm grant {} {}'.format(package, permission))
except TargetError:
logger.debug('Cannot grant {}'.format(permission))
# Messy environment initialisation stuff... # Messy environment initialisation stuff...
@@ -486,3 +552,144 @@ def _check_env():
platform_tools = _env.platform_tools platform_tools = _env.platform_tools
adb = _env.adb adb = _env.adb
aapt = _env.aapt aapt = _env.aapt
class LogcatMonitor(object):
"""
Helper class for monitoring Anroid's logcat
:param target: Android target to monitor
:type target: :class:`AndroidTarget`
:param regexps: List of uncompiled regular expressions to filter on the
device. Logcat entries that don't match any will not be
seen. If omitted, all entries will be sent to host.
:type regexps: list(str)
"""
@property
def logfile(self):
return self._logfile
def __init__(self, target, regexps=None):
super(LogcatMonitor, self).__init__()
self.target = target
self._regexps = regexps
def start(self, outfile=None):
"""
Start logcat and begin monitoring
:param outfile: Optional path to file to store all logcat entries
:type outfile: str
"""
if outfile:
self._logfile = open(outfile, 'w')
else:
self._logfile = tempfile.NamedTemporaryFile()
self.target.clear_logcat()
logcat_cmd = 'logcat'
# Join all requested regexps with an 'or'
if self._regexps:
regexp = '{}'.format('|'.join(self._regexps))
if len(self._regexps) > 1:
regexp = '({})'.format(regexp)
# Logcat on older version of android do not support the -e argument
# so fall back to using grep.
if self.target.get_sdk_version() > 23:
logcat_cmd = '{} -e "{}"'.format(logcat_cmd, regexp)
else:
logcat_cmd = '{} | grep "{}"'.format(logcat_cmd, regexp)
logcat_cmd = get_adb_command(self.target.conn.device, logcat_cmd)
logger.debug('logcat command ="{}"'.format(logcat_cmd))
self._logcat = pexpect.spawn(logcat_cmd, logfile=self._logfile)
def stop(self):
self._logcat.terminate()
self._logfile.close()
def get_log(self):
"""
Return the list of lines found by the monitor
"""
# Unless we tell pexect to 'expect' something, it won't read from
# logcat's buffer or write into our logfile. We'll need to force it to
# read any pending logcat output.
while True:
try:
read_size = 1024 * 8
# This will read up to read_size bytes, but only those that are
# already ready (i.e. it won't block). If there aren't any bytes
# already available it raises pexpect.TIMEOUT.
buf = self._logcat.read_nonblocking(read_size, timeout=0)
# We can't just keep calling read_nonblocking until we get a
# pexpect.TIMEOUT (i.e. until we don't find any available
# bytes), because logcat might be writing bytes the whole time -
# in that case we might never return from this function. In
# fact, we only care about bytes that were written before we
# entered this function. So, if we read read_size bytes (as many
# as we were allowed to), then we'll assume there are more bytes
# that have already been sitting in the output buffer of the
# logcat command. If not, we'll assume we read everything that
# had already been written.
if len(buf) == read_size:
continue
else:
break
except pexpect.TIMEOUT:
# No available bytes to read. No prob, logcat just hasn't
# printed anything since pexpect last read from its buffer.
break
with open(self._logfile.name) as fh:
return [line for line in fh]
def clear_log(self):
with open(self._logfile.name, 'w') as fh:
pass
def search(self, regexp):
"""
Search a line that matches a regexp in the logcat log
Return immediatly
"""
return [line for line in self.get_log() if re.match(regexp, line)]
def wait_for(self, regexp, timeout=30):
"""
Search a line that matches a regexp in the logcat log
Wait for it to appear if it's not found
:param regexp: regexp to search
:type regexp: str
:param timeout: Timeout in seconds, before rasing RuntimeError.
``None`` means wait indefinitely
:type timeout: number
:returns: List of matched strings
"""
log = self.get_log()
res = [line for line in log if re.match(regexp, line)]
# Found some matches, return them
if len(res) > 0:
return res
# Store the number of lines we've searched already, so we don't have to
# re-grep them after 'expect' returns
next_line_num = len(log)
try:
self._logcat.expect(regexp, timeout=timeout)
except pexpect.TIMEOUT:
raise RuntimeError('Logcat monitor timeout ({}s)'.format(timeout))
return [line for line in self.get_log()[next_line_num:]
if re.match(regexp, line)]

100
devlib/utils/csvutil.py Normal file
View File

@@ -0,0 +1,100 @@
# Copyright 2018 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.
#
'''
Due to the change in the nature of "binary mode" when opening files in
Python 3, the way files need to be opened for ``csv.reader`` and ``csv.writer``
is different from Python 2.
The functions in this module are intended to hide these differences allowing
the rest of the code to create csv readers/writers without worrying about which
Python version it is running under.
First up are ``csvwriter`` and ``csvreader`` context mangers that handle the
opening and closing of the underlying file. These are intended to replace the
most common usage pattern
.. code-block:: python
with open(filepath, 'wb') as wfh: # or open(filepath, 'w', newline='') in Python 3
writer = csv.writer(wfh)
writer.writerows(data)
with
.. code-block:: python
with csvwriter(filepath) as writer:
writer.writerows(data)
``csvreader`` works in an analogous way. ``csvreader`` and ``writer`` can take
additional arguments which will be passed directly to the
``csv.reader``/``csv.writer`` calls.
In some cases, it is desirable not to use a context manager (e.g. if the
reader/writer is intended to be returned from the function that creates it. For
such cases, alternative functions, ``create_reader`` and ``create_writer``,
exit. These return a two-tuple, with the created reader/writer as the first
element, and the corresponding ``FileObject`` as the second. It is the
responsibility of the calling code to ensure that the file is closed properly.
'''
import csv
import sys
from contextlib import contextmanager
@contextmanager
def csvwriter(filepath, *args, **kwargs):
if sys.version_info[0] == 3:
wfh = open(filepath, 'w', newline='')
else:
wfh = open(filepath, 'wb')
try:
yield csv.writer(wfh, *args, **kwargs)
finally:
wfh.close()
@contextmanager
def csvreader(filepath, *args, **kwargs):
if sys.version_info[0] == 3:
fh = open(filepath, 'r', newline='')
else:
fh = open(filepath, 'rb')
try:
yield csv.reader(fh, *args, **kwargs)
finally:
fh.close()
def create_writer(filepath, *args, **kwargs):
if sys.version_info[0] == 3:
wfh = open(filepath, 'w', newline='')
else:
wfh = open(filepath, 'wb')
return csv.writer(wfh, *args, **kwargs), wfh
def create_reader(filepath, *args, **kwargs):
if sys.version_info[0] == 3:
fh = open(filepath, 'r', newline='')
else:
fh = open(filepath, 'rb')
return csv.reader(fh, *args, **kwargs), fh

53
devlib/utils/gem5.py Normal file
View File

@@ -0,0 +1,53 @@
# Copyright 2017-2018 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 re
import logging
from devlib.utils.types import numeric
GEM5STATS_FIELD_REGEX = re.compile("^(?P<key>[^- ]\S*) +(?P<value>[^#]+).+$")
GEM5STATS_DUMP_HEAD = '---------- Begin Simulation Statistics ----------'
GEM5STATS_DUMP_TAIL = '---------- End Simulation Statistics ----------'
GEM5STATS_ROI_NUMBER = 8
logger = logging.getLogger('gem5')
def iter_statistics_dump(stats_file):
'''
Yields statistics dumps as dicts. The parameter is assumed to be a stream
reading from the statistics log file.
'''
cur_dump = {}
while True:
line = stats_file.readline()
if not line:
break
if GEM5STATS_DUMP_TAIL in line:
yield cur_dump
cur_dump = {}
else:
res = GEM5STATS_FIELD_REGEX.match(line)
if res:
k = res.group("key")
vtext = res.group("value")
try:
v = list(map(numeric, vtext.split()))
cur_dump[k] = v[0] if len(v)==1 else set(v)
except ValueError:
msg = 'Found non-numeric entry in gem5 stats ({}: {})'
logger.warning(msg.format(k, vtext))

View File

@@ -1,4 +1,4 @@
# Copyright 2013-2015 ARM Limited # Copyright 2013-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -30,18 +30,21 @@ import pkgutil
import logging import logging
import random import random
import ctypes import ctypes
import threading
from operator import itemgetter from operator import itemgetter
from itertools import groupby from itertools import groupby
from functools import partial from functools import partial
import wrapt import wrapt
from past.builtins import basestring
from devlib.exception import HostError, TimeoutError from devlib.exception import HostError, TimeoutError
from functools import reduce
# ABI --> architectures list # ABI --> architectures list
ABI_MAP = { ABI_MAP = {
'armeabi': ['armeabi', 'armv7', 'armv7l', 'armv7el', 'armv7lh'], 'armeabi': ['armeabi', 'armv7', 'armv7l', 'armv7el', 'armv7lh', 'armeabi-v7a'],
'arm64': ['arm64', 'armv8', 'arm64-v8a', 'aarch64'], 'arm64': ['arm64', 'armv8', 'arm64-v8a', 'aarch64'],
} }
@@ -79,9 +82,19 @@ CPU_PART_MAP = {
0xd08: {None: 'A72'}, 0xd08: {None: 'A72'},
0xd09: {None: 'A73'}, 0xd09: {None: 'A73'},
}, },
0x42: { # Broadcom
0x516: {None: 'Vulcan'},
},
0x43: { # Cavium
0x0a1: {None: 'Thunderx'},
0x0a2: {None: 'Thunderx81xx'},
},
0x4e: { # Nvidia 0x4e: { # Nvidia
0x0: {None: 'Denver'}, 0x0: {None: 'Denver'},
}, },
0x50: { # AppliedMicro
0x0: {None: 'xgene'},
},
0x51: { # Qualcomm 0x51: { # Qualcomm
0x02d: {None: 'Scorpion'}, 0x02d: {None: 'Scorpion'},
0x04d: {None: 'MSM8960'}, 0x04d: {None: 'MSM8960'},
@@ -91,6 +104,10 @@ CPU_PART_MAP = {
}, },
0x205: {0x1: 'KryoSilver'}, 0x205: {0x1: 'KryoSilver'},
0x211: {0x1: 'KryoGold'}, 0x211: {0x1: 'KryoGold'},
0x800: {None: 'Falkor'},
},
0x53: { # Samsung LSI
0x001: {0x1: 'MongooseM1'},
}, },
0x56: { # Marvell 0x56: { # Marvell
0x131: { 0x131: {
@@ -121,9 +138,13 @@ def preexec_function():
check_output_logger = logging.getLogger('check_output') check_output_logger = logging.getLogger('check_output')
# Popen is not thread safe. If two threads attempt to call it at the same time,
# one may lock up. See https://bugs.python.org/issue12739.
check_output_lock = threading.Lock()
def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs): def check_output(command, timeout=None, ignore=None, inputtext=None,
combined_output=False, **kwargs):
"""This is a version of subprocess.check_output that adds a timeout parameter to kill """This is a version of subprocess.check_output that adds a timeout parameter to kill
the subprocess if it does not return within the specified time.""" the subprocess if it does not return within the specified time."""
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
@@ -144,9 +165,14 @@ def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs):
except OSError: except OSError:
pass # process may have already terminated. pass # process may have already terminated.
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, with check_output_lock:
stderr = subprocess.STDOUT if combined_output else subprocess.PIPE
process = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=stderr,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
preexec_fn=preexec_function, **kwargs) preexec_fn=preexec_function,
**kwargs)
if timeout: if timeout:
timer = threading.Timer(timeout, callback, [process.pid, ]) timer = threading.Timer(timeout, callback, [process.pid, ])
@@ -154,6 +180,11 @@ def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs):
try: try:
output, error = process.communicate(inputtext) output, error = process.communicate(inputtext)
if sys.version_info[0] == 3:
# Currently errors=replace is needed as 0x8c throws an error
output = output.decode(sys.stdout.encoding, "replace")
if error:
error = error.decode(sys.stderr.encoding, "replace")
finally: finally:
if timeout: if timeout:
timer.cancel() timer.cancel()
@@ -161,9 +192,9 @@ def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs):
retcode = process.poll() retcode = process.poll()
if retcode: if retcode:
if retcode == -9: # killed, assume due to timeout callback if retcode == -9: # killed, assume due to timeout callback
raise TimeoutError(command, output='\n'.join([output, error])) raise TimeoutError(command, output='\n'.join([output or '', error or '']))
elif ignore != 'all' and retcode not in ignore: elif ignore != 'all' and retcode not in ignore:
raise subprocess.CalledProcessError(retcode, command, output='\n'.join([output, error])) raise subprocess.CalledProcessError(retcode, command, output='\n'.join([output or '', error or '']))
return output, error return output, error
@@ -235,8 +266,8 @@ def _merge_two_dicts(base, other, list_duplicates='all', match_types=False, # p
dict_type=dict, should_normalize=True, should_merge_lists=True): dict_type=dict, should_normalize=True, should_merge_lists=True):
"""Merge dicts normalizing their keys.""" """Merge dicts normalizing their keys."""
merged = dict_type() merged = dict_type()
base_keys = base.keys() base_keys = list(base.keys())
other_keys = other.keys() other_keys = list(other.keys())
norm = normalize if should_normalize else lambda x, y: x norm = normalize if should_normalize else lambda x, y: x
base_only = [] base_only = []
@@ -368,7 +399,7 @@ def normalize(value, dict_type=dict):
no surrounding whitespace, underscore-delimited strings.""" no surrounding whitespace, underscore-delimited strings."""
if isinstance(value, dict): if isinstance(value, dict):
normalized = dict_type() normalized = dict_type()
for k, v in value.iteritems(): for k, v in value.items():
key = k.strip().lower().replace(' ', '_') key = k.strip().lower().replace(' ', '_')
normalized[key] = normalize(v, dict_type) normalized[key] = normalize(v, dict_type)
return normalized return normalized
@@ -400,11 +431,16 @@ def escape_double_quotes(text):
return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\"', '\\\"') return re.sub(r'\\("|\')', r'\\\\\1', text).replace('\"', '\\\"')
def escape_spaces(text):
"""Escape spaces in the specified text"""
return text.replace(' ', '\ ')
def getch(count=1): def getch(count=1):
"""Read ``count`` characters from standard input.""" """Read ``count`` characters from standard input."""
if os.name == 'nt': if os.name == 'nt':
import msvcrt # pylint: disable=F0401 import msvcrt # pylint: disable=F0401
return ''.join([msvcrt.getch() for _ in xrange(count)]) return ''.join([msvcrt.getch() for _ in range(count)])
else: # assume Unix else: # assume Unix
import tty # NOQA import tty # NOQA
import termios # NOQA import termios # NOQA
@@ -431,6 +467,19 @@ def as_relative(path):
return path.lstrip(os.sep) return path.lstrip(os.sep)
def commonprefix(file_list, sep=os.sep):
"""
Find the lowest common base folder of a passed list of files.
"""
common_path = os.path.commonprefix(file_list)
cp_split = common_path.split(sep)
other_split = file_list[0].split(sep)
last = len(cp_split) - 1
if cp_split[last] != other_split[last]:
cp_split = cp_split[:-1]
return sep.join(cp_split)
def get_cpu_mask(cores): def get_cpu_mask(cores):
"""Return a string with the hex for the cpu mask for the specified core numbers.""" """Return a string with the hex for the cpu mask for the specified core numbers."""
mask = 0 mask = 0
@@ -460,8 +509,8 @@ def which(name):
return None return None
_bash_color_regex = re.compile('\x1b\\[[0-9;]+m') # This matches most ANSI escape sequences, not just colors
_bash_color_regex = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]')
def strip_bash_colors(text): def strip_bash_colors(text):
return _bash_color_regex.sub('', text) return _bash_color_regex.sub('', text)
@@ -469,7 +518,7 @@ def strip_bash_colors(text):
def get_random_string(length): def get_random_string(length):
"""Returns a random ASCII string of the specified length).""" """Returns a random ASCII string of the specified length)."""
return ''.join(random.choice(string.ascii_letters + string.digits) for _ in xrange(length)) return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
class LoadSyntaxError(Exception): class LoadSyntaxError(Exception):
@@ -486,13 +535,18 @@ class LoadSyntaxError(Exception):
RAND_MOD_NAME_LEN = 30 RAND_MOD_NAME_LEN = 30
BAD_CHARS = string.punctuation + string.whitespace BAD_CHARS = string.punctuation + string.whitespace
if sys.version_info[0] == 3:
TRANS_TABLE = str.maketrans(BAD_CHARS, '_' * len(BAD_CHARS))
else:
TRANS_TABLE = string.maketrans(BAD_CHARS, '_' * len(BAD_CHARS)) TRANS_TABLE = string.maketrans(BAD_CHARS, '_' * len(BAD_CHARS))
def to_identifier(text): def to_identifier(text):
"""Converts text to a valid Python identifier by replacing all """Converts text to a valid Python identifier by replacing all
whitespace and punctuation.""" whitespace and punctuation and adding a prefix if starting with a digit"""
return re.sub('_+', '_', text.translate(TRANS_TABLE)) if text[:1].isdigit():
text = '_' + text
return re.sub('_+', '_', str(text).translate(TRANS_TABLE))
def unique(alist): def unique(alist):
@@ -513,8 +567,8 @@ def ranges_to_list(ranges_string):
values = [] values = []
for rg in ranges_string.split(','): for rg in ranges_string.split(','):
if '-' in rg: if '-' in rg:
first, last = map(int, rg.split('-')) first, last = list(map(int, rg.split('-')))
values.extend(xrange(first, last + 1)) values.extend(range(first, last + 1))
else: else:
values.append(int(rg)) values.append(int(rg))
return values return values
@@ -523,8 +577,8 @@ def ranges_to_list(ranges_string):
def list_to_ranges(values): def list_to_ranges(values):
"""Converts a list, e.g ``[0,2,3,4]``, into a sysfs-style ranges string, e.g. ``"0,2-4"``""" """Converts a list, e.g ``[0,2,3,4]``, into a sysfs-style ranges string, e.g. ``"0,2-4"``"""
range_groups = [] range_groups = []
for _, g in groupby(enumerate(values), lambda (i, x): i - x): for _, g in groupby(enumerate(values), lambda i_x: i_x[0] - i_x[1]):
range_groups.append(map(itemgetter(1), g)) range_groups.append(list(map(itemgetter(1), g)))
range_strings = [] range_strings = []
for group in range_groups: for group in range_groups:
if len(group) == 1: if len(group) == 1:
@@ -547,7 +601,7 @@ def mask_to_list(mask):
"""Converts the specfied integer bitmask into a list of """Converts the specfied integer bitmask into a list of
indexes of bits that are set in the mask.""" indexes of bits that are set in the mask."""
size = len(bin(mask)) - 2 # because of "0b" size = len(bin(mask)) - 2 # because of "0b"
return [size - i - 1 for i in xrange(size) return [size - i - 1 for i in range(size)
if mask & (1 << size - i - 1)] if mask & (1 << size - i - 1)]
@@ -592,7 +646,7 @@ def memoized(wrapped, instance, args, kwargs):
def memoize_wrapper(*args, **kwargs): def memoize_wrapper(*args, **kwargs):
id_string = func_id + ','.join([__get_memo_id(a) for a in args]) id_string = func_id + ','.join([__get_memo_id(a) for a in args])
id_string += ','.join('{}={}'.format(k, v) id_string += ','.join('{}={}'.format(k, v)
for k, v in kwargs.iteritems()) for k, v in kwargs.items())
if id_string not in __memo_cache: if id_string not in __memo_cache:
__memo_cache[id_string] = wrapped(*args, **kwargs) __memo_cache[id_string] = wrapped(*args, **kwargs)
return __memo_cache[id_string] return __memo_cache[id_string]

536
devlib/utils/parse_aep.py Executable file
View File

@@ -0,0 +1,536 @@
#!/usr/bin/env python
# Copyright 2018 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.
#
# Copyright 2018 Linaro 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 getopt
import subprocess
import logging
import signal
import serial
import time
import math
logger = logging.getLogger('aep-parser')
class AepParser(object):
prepared = False
@staticmethod
def topology_from_data(array, topo):
# Extract topology information for the data file
# The header of a data file looks like this ('#' included):
# configuration: <file path>
# config_name: <file name>
# trigger: 0.400000V (hyst 0.200000V) 0.000000W (hyst 0.200000W) 400us
# date: Fri, 10 Jun 2016 11:25:07 +0200
# host: <host name>
#
# CHN_0 Pretty_name_0 PARENT_0 Color0 Class0
# CHN_1 Pretty_name_1 PARENT_1 Color1 Class1
# CHN_2 Pretty_name_2 PARENT_2 Color2 Class2
# CHN_3 Pretty_name_3 PARENT_3 Color3 Class3
# ..
# CHN_N Pretty_name_N PARENT_N ColorN ClassN
#
info = {}
if len(array) == 6:
info['name'] = array[1]
info['parent'] = array[3]
info['pretty'] = array[2]
# add an entry for both name and pretty name in order to not parse
# the whole dict when looking for a parent and the parent of parent
topo[array[1]] = info
topo[array[2]] = info
return topo
@staticmethod
def create_virtual(topo, label, hide, duplicate):
# Create a list of virtual power domain that are the sum of others
# A virtual domain is the parent of several channels but is not sampled by a
# channel
# This can be useful if a power domain is supplied by 2 power rails
virtual = {}
# Create an entry for each virtual parent
for supply in topo.keys():
index = topo[supply]['index']
# Don't care of hidden columns
if hide[index]:
continue
# Parent is in the topology
parent = topo[supply]['parent']
if parent in topo:
continue
if parent not in virtual:
virtual[parent] = { supply : index }
virtual[parent][supply] = index
# Remove parent with 1 child as they don't give more information than their
# child
for supply in list(virtual.keys()):
if len(virtual[supply]) == 1:
del virtual[supply];
for supply in list(virtual.keys()):
# Add label, hide and duplicate columns for virtual domains
hide.append(0)
duplicate.append(1)
label.append(supply)
return virtual
@staticmethod
def get_label(array):
# Get the label of each column
# Remove unit '(X)' from the end of the label
label = [""]*len(array)
unit = [""]*len(array)
label[0] = array[0]
unit[0] = "(S)"
for i in range(1,len(array)):
label[i] = array[i][:-3]
unit[i] = array[i][-3:]
return label, unit
@staticmethod
def filter_column(label, unit, topo):
# Filter columns
# We don't parse Volt and Amper columns: put in hide list
# We don't add in Total a column that is the child of another one: put in duplicate list
# By default we hide all columns
hide = [1] * len(label)
# By default we assume that there is no child
duplicate = [0] * len(label)
for i in range(len(label)):
# We only care about time and Watt
if label[i] == 'time':
hide[i] = 0
continue
if '(W)' not in unit[i]:
continue
hide[i] = 0
#label is pretty name
pretty = label[i]
# We don't add a power domain that is already accounted by its parent
if topo[pretty]['parent'] in topo:
duplicate[i] = 1
# Set index, that will be used by virtual domain
topo[topo[pretty]['name']]['index'] = i
# remove pretty element that is useless now
del topo[pretty]
return hide, duplicate
@staticmethod
def parse_text(array, hide):
data = [0]*len(array)
for i in range(len(array)):
if hide[i]:
continue
try:
data[i] = int(float(array[i])*1000000)
except ValueError:
continue
return data
@staticmethod
def add_virtual_data(data, virtual):
# write virtual domain
for parent in virtual.keys():
power = 0
for child in list(virtual[parent].values()):
try:
power += data[child]
except IndexError:
continue
data.append(power)
return data
@staticmethod
def delta_nrj(array, delta, min, max, hide):
# Compute the energy consumed in this time slice and add it
# delta[0] is used to save the last time stamp
if (delta[0] < 0):
delta[0] = array[0]
time = array[0] - delta[0]
if (time <= 0):
return delta
for i in range(len(array)):
if hide[i]:
continue
try:
data = array[i]
except ValueError:
continue
if (data < min[i]):
min[i] = data
if (data > max[i]):
max[i] = data
delta[i] += time * data
# save last time stamp
delta[0] = array[0]
return delta
def output_label(self, label, hide):
self.fo.write(label[0]+"(uS)")
for i in range(1, len(label)):
if hide[i]:
continue
self.fo.write(" "+label[i]+"(uW)")
self.fo.write("\n")
def output_power(self, array, hide):
#skip partial line. Most probably the last one
if len(array) < len(hide):
return
# write not hidden colums
self.fo.write(str(array[0]))
for i in range(1, len(array)):
if hide[i]:
continue
self.fo.write(" "+str(array[i]))
self.fo.write("\n")
def prepare(self, infile, outfile, summaryfile):
try:
self.fi = open(infile, "r")
except IOError:
logger.warn('Unable to open input file {}'.format(infile))
logger.warn('Usage: parse_arp.py -i <inputfile> [-o <outputfile>]')
sys.exit(2)
self.parse = True
if len(outfile) > 0:
try:
self.fo = open(outfile, "w")
except IOError:
logger.warn('Unable to create {}'.format(outfile))
self.parse = False
else:
self.parse = False
self.summary = True
if len(summaryfile) > 0:
try:
self.fs = open(summaryfile, "w")
except IOError:
logger.warn('Unable to create {}'.format(summaryfile))
self.fs = sys.stdout
else:
self.fs = sys.stdout
self.prepared = True
def unprepare(self):
if not self.prepared:
# nothing has been prepared
return
self.fi.close()
if self.parse:
self.fo.close()
self.prepared = False
def parse_aep(self, start=0, lenght=-1):
# Parse aep data and calculate the energy consumed
begin = 0
label_line = 1
topo = {}
lines = self.fi.readlines()
for myline in lines:
array = myline.split()
if "#" in myline:
# update power topology
topo = self.topology_from_data(array, topo)
continue
if label_line:
label_line = 0
# 1st line not starting with # gives label of each column
label, unit = self.get_label(array)
# hide useless columns and detect channels that are children
# of other channels
hide, duplicate = self.filter_column(label, unit, topo)
# Create virtual power domains
virtual = self.create_virtual(topo, label, hide, duplicate)
if self.parse:
self.output_label(label, hide)
logger.debug('Topology : {}'.format(topo))
logger.debug('Virtual power domain : {}'.format(virtual))
logger.debug('Duplicated power domain : : {}'.format(duplicate))
logger.debug('Name of columns : {}'.format(label))
logger.debug('Hidden columns : {}'.format(hide))
logger.debug('Unit of columns : {}'.format(unit))
# Init arrays
nrj = [0]*len(label)
min = [100000000]*len(label)
max = [0]*len(label)
offset = [0]*len(label)
continue
# convert text to int and unit to micro-unit
data = self.parse_text(array, hide)
# get 1st time stamp
if begin <= 0:
being = data[0]
# skip data before start
if (data[0]-begin) < start:
continue
# stop after lenght
if lenght >= 0 and (data[0]-begin) > (start + lenght):
continue
# add virtual domains
data = self.add_virtual_data(data, virtual)
# extract power figures
self.delta_nrj(data, nrj, min, max, hide)
# write data into new file
if self.parse:
self.output_power(data, hide)
# if there is no data just return
if label_line or len(nrj) == 1:
raise ValueError('No data found in the data file. Please check the Arm Energy Probe')
return
# display energy consumption of each channel and total energy consumption
total = 0
results_table = {}
for i in range(1, len(nrj)):
if hide[i]:
continue
nrj[i] -= offset[i] * nrj[0]
total_nrj = nrj[i]/1000000000000.0
duration = (max[0]-min[0])/1000000.0
channel_name = label[i]
average_power = total_nrj/duration
self.fs.write("Total nrj: %8.3f J for %s -- duration %8.3f sec -- min %8.3f W -- max %8.3f W\n" % (nrj[i]/1000000000000.0, label[i], (max[0]-min[0])/1000000.0, min[i]/1000000.0, max[i]/1000000.0))
# store each AEP channel info except Platform in the results table
results_table[channel_name] = total_nrj, average_power
if (min[i] < offset[i]):
self.fs.write ("!!! Min below offset\n")
if duplicate[i]:
continue
total += nrj[i]
self.fs.write ("Total nrj: %8.3f J for %s -- duration %8.3f sec\n" % (total/1000000000000.0, "Platform ", (max[0]-min[0])/1000000.0))
total_nrj = total/1000000000000.0
duration = (max[0]-min[0])/1000000.0
average_power = total_nrj/duration
# store AEP Platform channel info in the results table
results_table["Platform"] = total_nrj, average_power
return results_table
def topology_from_config(self, topofile):
try:
ft = open(topofile, "r")
except IOError:
logger.warn('Unable to open config file {}'.format(topofile))
return
lines = ft.readlines()
topo = {}
virtual = {}
name = ""
offset = 0
index = 0
#parse config file
for myline in lines:
if myline.startswith("#"):
# skip comment
continue
if myline == "\n":
# skip empty line
continue
if name == "":
# 1st valid line is the config's name
name = myline
continue
if not myline.startswith((' ', '\t')):
# new device path
offset = index
continue
# Get parameters of channel configuration
items = myline.split()
info = {}
info['name'] = items[0]
info['parent'] = items[9]
info['pretty'] = items[8]
info['index'] = int(items[2])+offset
# Add channel
topo[items[0]] = info
# Increase index
index +=1
# Create an entry for each virtual parent
for supply in topo.keys():
# Parent is in the topology
parent = topo[supply]['parent']
if parent in topo:
continue
if parent not in virtual:
virtual[parent] = { supply : topo[supply]['index'] }
virtual[parent][supply] = topo[supply]['index']
# Remove parent with 1 child as they don't give more information than their
# child
for supply in list(virtual.keys()):
if len(virtual[supply]) == 1:
del virtual[supply];
topo_list = ['']*(1+len(topo)+len(virtual))
topo_list[0] = 'time'
for chnl in topo.keys():
topo_list[topo[chnl]['index']] = chnl
for chnl in virtual.keys():
index +=1
topo_list[index] = chnl
ft.close()
return topo_list
def __del__(self):
self.unprepare()
if __name__ == '__main__':
def handleSigTERM(signum, frame):
sys.exit(2)
signal.signal(signal.SIGTERM, handleSigTERM)
signal.signal(signal.SIGINT, handleSigTERM)
logger.setLevel(logging.WARN)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)
infile = ""
outfile = ""
figurefile = ""
start = 0
lenght = -1
try:
opts, args = getopt.getopt(sys.argv[1:], "i:vo:s:l:t:")
except getopt.GetoptError as err:
print(str(err)) # will print something like "option -a not recognized"
sys.exit(2)
for o, a in opts:
if o == "-i":
infile = a
if o == "-v":
logger.setLevel(logging.DEBUG)
if o == "-o":
parse = True
outfile = a
if o == "-s":
start = int(float(a)*1000000)
if o == "-l":
lenght = int(float(a)*1000000)
if o == "-t":
topofile = a
parser = AepParser()
print(parser.topology_from_config(topofile))
exit(0)
parser = AepParser()
parser.prepare(infile, outfile, figurefile)
parser.parse_aep(start, lenght)

272
devlib/utils/rendering.py Normal file
View File

@@ -0,0 +1,272 @@
# Copyright 2018 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
import os
import re
import shutil
import sys
import tempfile
import threading
import time
from collections import namedtuple, OrderedDict
from distutils.version import LooseVersion
from devlib.exception import WorkerThreadError, TargetNotRespondingError, TimeoutError
from devlib.utils.csvutil import csvwriter
logger = logging.getLogger('rendering')
SurfaceFlingerFrame = namedtuple('SurfaceFlingerFrame',
'desired_present_time actual_present_time frame_ready_time')
VSYNC_INTERVAL = 16666667
class FrameCollector(threading.Thread):
def __init__(self, target, period):
super(FrameCollector, self).__init__()
self.target = target
self.period = period
self.stop_signal = threading.Event()
self.frames = []
self.temp_file = None
self.refresh_period = None
self.drop_threshold = None
self.unresponsive_count = 0
self.last_ready_time = None
self.exc = None
self.header = None
def run(self):
logger.debug('Surface flinger frame data collection started.')
try:
self.stop_signal.clear()
fd, self.temp_file = tempfile.mkstemp()
logger.debug('temp file: {}'.format(self.temp_file))
wfh = os.fdopen(fd, 'wb')
try:
while not self.stop_signal.is_set():
self.collect_frames(wfh)
time.sleep(self.period)
finally:
wfh.close()
except (TargetNotRespondingError, TimeoutError): # pylint: disable=W0703
raise
except Exception as e: # pylint: disable=W0703
logger.warning('Exception on collector thread: {}({})'.format(e.__class__.__name__, e))
self.exc = WorkerThreadError(self.name, sys.exc_info())
logger.debug('Surface flinger frame data collection stopped.')
def stop(self):
self.stop_signal.set()
self.join()
if self.unresponsive_count:
message = 'FrameCollector was unrepsonsive {} times.'.format(self.unresponsive_count)
if self.unresponsive_count > 10:
logger.warning(message)
else:
logger.debug(message)
if self.exc:
raise self.exc # pylint: disable=E0702
def process_frames(self, outfile=None):
if not self.temp_file:
raise RuntimeError('Attempting to process frames before running the collector')
with open(self.temp_file) as fh:
self._process_raw_file(fh)
if outfile:
shutil.copy(self.temp_file, outfile)
os.unlink(self.temp_file)
self.temp_file = None
def write_frames(self, outfile, columns=None):
if columns is None:
header = self.header
frames = self.frames
else:
indexes = []
for c in columns:
if c not in self.header:
msg = 'Invalid column "{}"; must be in {}'
raise ValueError(msg.format(c, self.header))
indexes.append(self.header.index(c))
frames = [[f[i] for i in indexes] for f in self.frames]
header = columns
with csvwriter(outfile) as writer:
if header:
writer.writerow(header)
writer.writerows(frames)
def collect_frames(self, wfh):
raise NotImplementedError()
def clear(self):
raise NotImplementedError()
def _process_raw_file(self, fh):
raise NotImplementedError()
class SurfaceFlingerFrameCollector(FrameCollector):
def __init__(self, target, period, view, header=None):
super(SurfaceFlingerFrameCollector, self).__init__(target, period)
self.view = view
self.header = header or SurfaceFlingerFrame._fields
def collect_frames(self, wfh):
for activity in self.list():
if activity == self.view:
wfh.write(self.get_latencies(activity))
def clear(self):
self.target.execute('dumpsys SurfaceFlinger --latency-clear ')
def get_latencies(self, activity):
cmd = 'dumpsys SurfaceFlinger --latency "{}"'
return self.target.execute(cmd.format(activity))
def list(self):
text = self.target.execute('dumpsys SurfaceFlinger --list')
return text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
def _process_raw_file(self, fh):
text = fh.read().replace('\r\n', '\n').replace('\r', '\n')
for line in text.split('\n'):
line = line.strip()
if line:
self._process_trace_line(line)
def _process_trace_line(self, line):
parts = line.split()
if len(parts) == 3:
frame = SurfaceFlingerFrame(*list(map(int, parts)))
if not frame.frame_ready_time:
return # "null" frame
if frame.frame_ready_time <= self.last_ready_time:
return # duplicate frame
if (frame.frame_ready_time - frame.desired_present_time) > self.drop_threshold:
logger.debug('Dropping bogus frame {}.'.format(line))
return # bogus data
self.last_ready_time = frame.frame_ready_time
self.frames.append(frame)
elif len(parts) == 1:
self.refresh_period = int(parts[0])
self.drop_threshold = self.refresh_period * 1000
elif 'SurfaceFlinger appears to be unresponsive, dumping anyways' in line:
self.unresponsive_count += 1
else:
logger.warning('Unexpected SurfaceFlinger dump output: {}'.format(line))
def read_gfxinfo_columns(target):
output = target.execute('dumpsys gfxinfo --list framestats')
lines = iter(output.split('\n'))
for line in lines:
if line.startswith('---PROFILEDATA---'):
break
columns_line = next(lines)
return columns_line.split(',')[:-1] # has a trailing ','
class GfxinfoFrameCollector(FrameCollector):
def __init__(self, target, period, package, header=None):
super(GfxinfoFrameCollector, self).__init__(target, period)
self.package = package
self.header = None
self._init_header(header)
def collect_frames(self, wfh):
cmd = 'dumpsys gfxinfo {} framestats'
wfh.write(self.target.execute(cmd.format(self.package)))
def clear(self):
pass
def _init_header(self, header):
if header is not None:
self.header = header
else:
self.header = read_gfxinfo_columns(self.target)
def _process_raw_file(self, fh):
found = False
try:
last_vsync = 0
while True:
for line in fh:
if line.startswith('---PROFILEDATA---'):
found = True
break
next(fh) # headers
for line in fh:
if line.startswith('---PROFILEDATA---'):
break
entries = list(map(int, line.strip().split(',')[:-1])) # has a trailing ','
if entries[1] <= last_vsync:
continue # repeat frame
last_vsync = entries[1]
self.frames.append(entries)
except StopIteration:
pass
if not found:
logger.warning('Could not find frames data in gfxinfo output')
return
def _file_reverse_iter(fh, buf_size=1024):
fh.seek(0, os.SEEK_END)
offset = 0
file_size = remaining_size = fh.tell()
while remaining_size > 0:
offset = min(file_size, offset + buf_size)
fh.seek(file_size - offset)
buf = fh.read(min(remaining_size, buf_size))
remaining_size -= buf_size
yield buf
def gfxinfo_get_last_dump(filepath):
"""
Return the last gfxinfo dump from the frame collector's raw output.
"""
record = ''
with open(filepath, 'r') as fh:
fh_iter = _file_reverse_iter(fh)
try:
while True:
buf = next(fh_iter)
ix = buf.find('** Graphics')
if ix >= 0:
return buf[ix:] + record
ix = buf.find(' **\n')
if ix >= 0:
buf = next(fh_iter) + buf
ix = buf.find('** Graphics')
if ix < 0:
msg = '"{}" appears to be corrupted'
raise RuntimeError(msg.format(filepath))
return buf[ix:] + record
record = buf + record
except StopIteration:
pass

View File

@@ -1,4 +1,4 @@
# Copyright 2013-2015 ARM Limited # Copyright 2013-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -32,6 +32,14 @@ from pexpect import EOF, TIMEOUT # NOQA pylint: disable=W0611
from devlib.exception import HostError from devlib.exception import HostError
class SerialLogger(Logger):
write = Logger.debug
def flush(self):
pass
def pulse_dtr(conn, state=True, duration=0.1): def pulse_dtr(conn, state=True, duration=0.1):
"""Set the DTR line of the specified serial connection to the specified state """Set the DTR line of the specified serial connection to the specified state
for the specified duration (note: the initial state of the line is *not* checked.""" for the specified duration (note: the initial state of the line is *not* checked."""
@@ -40,19 +48,19 @@ def pulse_dtr(conn, state=True, duration=0.1):
conn.setDTR(not state) conn.setDTR(not state)
def get_connection(timeout, init_dtr=None, logcls=Logger, def get_connection(timeout, init_dtr=None, logcls=SerialLogger,
*args, **kwargs): logfile=None, *args, **kwargs):
if init_dtr is not None: if init_dtr is not None:
kwargs['dsrdtr'] = True kwargs['dsrdtr'] = True
try: try:
conn = serial.Serial(*args, **kwargs) conn = serial.Serial(*args, **kwargs)
except serial.SerialException as e: except serial.SerialException as e:
raise HostError(e.message) raise HostError(str(e))
if init_dtr is not None: if init_dtr is not None:
conn.setDTR(init_dtr) conn.setDTR(init_dtr)
conn.nonblocking() conn.nonblocking()
conn.flushOutput() conn.flushOutput()
target = fdpexpect.fdspawn(conn.fileno(), timeout=timeout) target = fdpexpect.fdspawn(conn.fileno(), timeout=timeout, logfile=logfile)
target.logfile_read = logcls('read') target.logfile_read = logcls('read')
target.logfile_send = logcls('send') target.logfile_send = logcls('send')
@@ -83,7 +91,7 @@ def write_characters(conn, line, delay=0.05):
@contextmanager @contextmanager
def open_serial_connection(timeout, get_conn=False, init_dtr=None, def open_serial_connection(timeout, get_conn=False, init_dtr=None,
logcls=Logger, *args, **kwargs): logcls=SerialLogger, *args, **kwargs):
""" """
Opens a serial connection to a device. Opens a serial connection to a device.

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import threading
import tempfile import tempfile
import shutil import shutil
import socket import socket
import sys
import time import time
import pexpect import pexpect
@@ -34,7 +35,9 @@ else:
from pexpect import EOF, TIMEOUT, spawn from pexpect import EOF, TIMEOUT, spawn
from devlib.exception import HostError, TargetError, TimeoutError from devlib.exception import HostError, TargetError, TimeoutError
from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output from devlib.utils.misc import which, strip_bash_colors, check_output
from devlib.utils.misc import (escape_single_quotes, escape_double_quotes,
escape_spaces)
from devlib.utils.types import boolean from devlib.utils.types import boolean
@@ -160,7 +163,8 @@ class SshConnection(object):
telnet=False, telnet=False,
password_prompt=None, password_prompt=None,
original_prompt=None, original_prompt=None,
platform=None platform=None,
sudo_cmd="sudo -- sh -c '{}'"
): ):
self.host = host self.host = host
self.username = username self.username = username
@@ -169,16 +173,21 @@ class SshConnection(object):
self.port = port self.port = port
self.lock = threading.Lock() self.lock = threading.Lock()
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
self.sudo_cmd = sudo_cmd
logger.debug('Logging in {}@{}'.format(username, host)) logger.debug('Logging in {}@{}'.format(username, host))
timeout = timeout if timeout is not None else self.default_timeout timeout = timeout if timeout is not None else self.default_timeout
self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None) self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None)
def push(self, source, dest, timeout=30): def push(self, source, dest, timeout=30):
dest = '{}@{}:{}'.format(self.username, self.host, dest) dest = '"{}"@"{}":"{}"'.format(escape_double_quotes(self.username),
escape_spaces(escape_double_quotes(self.host)),
escape_spaces(escape_double_quotes(dest)))
return self._scp(source, dest, timeout) return self._scp(source, dest, timeout)
def pull(self, source, dest, timeout=30): def pull(self, source, dest, timeout=30):
source = '{}@{}:{}'.format(self.username, self.host, source) source = '"{}"@"{}":"{}"'.format(escape_double_quotes(self.username),
escape_spaces(escape_double_quotes(self.host)),
escape_spaces(escape_double_quotes(source)))
return self._scp(source, dest, timeout) return self._scp(source, dest, timeout)
def execute(self, command, timeout=None, check_exit_code=True, def execute(self, command, timeout=None, check_exit_code=True,
@@ -212,7 +221,7 @@ class SshConnection(object):
port_string = '-p {}'.format(self.port) if self.port else '' port_string = '-p {}'.format(self.port) if self.port else ''
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else '' keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
if as_root: if as_root:
command = "sudo -- sh -c '{}'".format(command) command = self.sudo_cmd.format(command)
command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command) command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command)
logger.debug(command) logger.debug(command)
if self.password: if self.password:
@@ -228,7 +237,7 @@ class SshConnection(object):
def cancel_running_command(self): def cancel_running_command(self):
# simulate impatiently hitting ^C until command prompt appears # simulate impatiently hitting ^C until command prompt appears
logger.debug('Sending ^C') logger.debug('Sending ^C')
for _ in xrange(self.max_cancel_attempts): for _ in range(self.max_cancel_attempts):
self.conn.sendline(chr(3)) self.conn.sendline(chr(3))
if self.conn.prompt(0.1): if self.conn.prompt(0.1):
return True return True
@@ -240,7 +249,7 @@ class SshConnection(object):
# As we're already root, there is no need to use sudo. # As we're already root, there is no need to use sudo.
as_root = False as_root = False
if as_root: if as_root:
command = "sudo -- sh -c '{}'".format(escape_single_quotes(command)) command = self.sudo_cmd.format(escape_single_quotes(command))
if log: if log:
logger.debug(command) logger.debug(command)
self.conn.sendline(command) self.conn.sendline(command)
@@ -255,6 +264,9 @@ class SshConnection(object):
timed_out = self._wait_for_prompt(timeout) timed_out = self._wait_for_prompt(timeout)
# the regex removes line breaks potential introduced when writing # the regex removes line breaks potential introduced when writing
# command to shell. # command to shell.
if sys.version_info[0] == 3:
output = process_backspaces(self.conn.before.decode(sys.stdout.encoding, 'replace'))
else:
output = process_backspaces(self.conn.before) output = process_backspaces(self.conn.before)
output = re.sub(r'\r([^\n])', r'\1', output) output = re.sub(r'\r([^\n])', r'\1', output)
if '\r\n' in output: # strip the echoed command if '\r\n' in output: # strip the echoed command
@@ -282,16 +294,18 @@ class SshConnection(object):
port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else '' port_string = '-P {}'.format(self.port) if (self.port and self.port != 22) else ''
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else '' keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest) command = '{} -r {} {} {} {}'.format(scp, keyfile_string, port_string, source, dest)
pass_string = '' command_redacted = command
logger.debug(command) logger.debug(command)
if self.password: if self.password:
command = _give_password(self.password, command) command = _give_password(self.password, command)
command_redacted = command.replace(self.password, '<redacted>')
try: try:
check_output(command, timeout=timeout, shell=True) check_output(command, timeout=timeout, shell=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise subprocess.CalledProcessError(e.returncode, e.cmd.replace(pass_string, ''), e.output) raise HostError("Failed to copy file with '{}'. Output:\n{}".format(
command_redacted, e.output))
except TimeoutError as e: except TimeoutError as e:
raise TimeoutError(e.command.replace(pass_string, ''), e.output) raise TimeoutError(command_redacted, e.output)
class TelnetConnection(SshConnection): class TelnetConnection(SshConnection):
@@ -328,6 +342,7 @@ class Gem5Connection(TelnetConnection):
timeout=None, timeout=None,
password_prompt=None, password_prompt=None,
original_prompt=None, original_prompt=None,
strip_echoed_commands=False,
): ):
if host is not None: if host is not None:
host_system = socket.gethostname() host_system = socket.gethostname()
@@ -344,6 +359,8 @@ class Gem5Connection(TelnetConnection):
self.is_rooted = True self.is_rooted = True
self.password = None self.password = None
self.port = None self.port = None
# Flag to indicate whether commands are echoed by the simulated system
self.strip_echoed_commands = strip_echoed_commands
# Long timeouts to account for gem5 being slow # Long timeouts to account for gem5 being slow
# Can be overriden if the given timeout is longer # Can be overriden if the given timeout is longer
self.default_timeout = 3600 self.default_timeout = 3600
@@ -439,25 +456,39 @@ class Gem5Connection(TelnetConnection):
# First check if the connection is set up to interact with gem5 # First check if the connection is set up to interact with gem5
self._check_ready() self._check_ready()
filename = os.path.basename(source) result = self._gem5_shell("ls {}".format(source))
files = strip_bash_colors(result).split()
logger.debug("pull_file {} {}".format(source, filename)) for filename in files:
# We don't check the exit code here because it is non-zero if the source dest_file = os.path.basename(filename)
# and destination are the same. The ls below will cause an error if the logger.debug("pull_file {} {}".format(filename, dest_file))
# file was not where we expected it to be. # writefile needs the file to be copied to be in the current
if os.path.dirname(source) != os.getcwd(): # working directory so if needed, copy to the working directory
self._gem5_shell("cat '{}' > '{}'".format(source, filename)) # We don't check the exit code here because it is non-zero if the
# source and destination are the same. The ls below will cause an
# error if the file was not where we expected it to be.
if os.path.isabs(source):
if os.path.dirname(source) != self.execute('pwd',
check_exit_code=False):
self._gem5_shell("cat '{}' > '{}'".format(filename,
dest_file))
self._gem5_shell("sync") self._gem5_shell("sync")
self._gem5_shell("ls -la {}".format(filename)) self._gem5_shell("ls -la {}".format(dest_file))
logger.debug('Finished the copy in the simulator') logger.debug('Finished the copy in the simulator')
self._gem5_util("writefile {}".format(filename)) self._gem5_util("writefile {}".format(dest_file))
if 'cpu' not in filename: if 'cpu' not in filename:
while not os.path.exists(os.path.join(self.gem5_out_dir, filename)): while not os.path.exists(os.path.join(self.gem5_out_dir,
dest_file)):
time.sleep(1) time.sleep(1)
# Perform the local move # Perform the local move
shutil.move(os.path.join(self.gem5_out_dir, filename), dest) if os.path.exists(os.path.join(dest, dest_file)):
logger.warning(
'Destination file {} already exists!'\
.format(dest_file))
else:
shutil.move(os.path.join(self.gem5_out_dir, dest_file), dest)
logger.debug("Pull complete.") logger.debug("Pull complete.")
def execute(self, command, timeout=1000, check_exit_code=True, def execute(self, command, timeout=1000, check_exit_code=True,
@@ -468,7 +499,9 @@ class Gem5Connection(TelnetConnection):
# First check if the connection is set up to interact with gem5 # First check if the connection is set up to interact with gem5
self._check_ready() self._check_ready()
output = self._gem5_shell(command, as_root=as_root) output = self._gem5_shell(command,
check_exit_code=check_exit_code,
as_root=as_root)
if strip_colors: if strip_colors:
output = strip_bash_colors(output) output = strip_bash_colors(output)
return output return output
@@ -503,6 +536,10 @@ class Gem5Connection(TelnetConnection):
""" """
gem5_logger.info("Gracefully terminating the gem5 simulation.") gem5_logger.info("Gracefully terminating the gem5 simulation.")
try: try:
# Unmount the virtio device BEFORE we kill the
# simulation. This is done to simplify checkpointing at
# the end of a simulation!
self._unmount_virtio()
self._gem5_util("exit") self._gem5_util("exit")
self.gem5simulation.wait() self.gem5simulation.wait()
except EOF: except EOF:
@@ -525,6 +562,19 @@ class Gem5Connection(TelnetConnection):
self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir) self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir)
# Handle the EOF exception raised by pexpect
def _gem5_EOF_handler(self, gem5_simulation, gem5_out_dir, err):
# If we have reached the "EOF", it typically means
# that gem5 crashed and closed the connection. Let's
# check and actually tell the user what happened here,
# rather than spewing out pexpect errors.
if gem5_simulation.poll():
message = "The gem5 process has crashed with error code {}!\n\tPlease see {} for details."
raise TargetError(message.format(gem5_simulation.poll(), gem5_out_dir))
else:
# Let's re-throw the exception in this case.
raise err
# This function connects to the gem5 simulation # This function connects to the gem5 simulation
def connect_gem5(self, port, gem5_simulation, gem5_interact_dir, def connect_gem5(self, port, gem5_simulation, gem5_interact_dir,
gem5_out_dir): gem5_out_dir):
@@ -562,6 +612,8 @@ class Gem5Connection(TelnetConnection):
break break
except pxssh.ExceptionPxssh: except pxssh.ExceptionPxssh:
pass pass
except EOF as err:
self._gem5_EOF_handler(gem5_simulation, gem5_out_dir, err)
else: else:
gem5_simulation.kill() gem5_simulation.kill()
raise TargetError("Failed to connect to the gem5 telnet session.") raise TargetError("Failed to connect to the gem5 telnet session.")
@@ -582,13 +634,18 @@ class Gem5Connection(TelnetConnection):
self._login_to_device() self._login_to_device()
except TIMEOUT: except TIMEOUT:
pass pass
except EOF as err:
self._gem5_EOF_handler(gem5_simulation, gem5_out_dir, err)
try: try:
# Try and force a prompt to be shown # Try and force a prompt to be shown
self.conn.send('\n') self.conn.send('\n')
self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60) self.conn.expect([r'# ', r'\$ ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60)
prompt_found = True prompt_found = True
except TIMEOUT: except TIMEOUT:
pass pass
except EOF as err:
self._gem5_EOF_handler(gem5_simulation, gem5_out_dir, err)
gem5_logger.info("Successfully logged in") gem5_logger.info("Successfully logged in")
gem5_logger.info("Setting unique prompt...") gem5_logger.info("Setting unique prompt...")
@@ -675,6 +732,9 @@ class Gem5Connection(TelnetConnection):
gem5_logger.debug("gem5_shell command: {}".format(command)) gem5_logger.debug("gem5_shell command: {}".format(command))
if as_root:
command = 'echo "{}" | su'.format(escape_double_quotes(command))
# Send the actual command # Send the actual command
self.conn.send("{}\n".format(command)) self.conn.send("{}\n".format(command))
@@ -700,8 +760,8 @@ class Gem5Connection(TelnetConnection):
output = output[command_index + len(command):].strip() output = output[command_index + len(command):].strip()
# It is possible that gem5 will echo the command. Therefore, we need to # If the gem5 system echoes the executed command, we need to remove that too!
# remove that too! if self.strip_echoed_commands:
command_index = output.find(command) command_index = output.find(command)
if command_index != -1: if command_index != -1:
output = output[command_index + len(command):].strip() output = output[command_index + len(command):].strip()
@@ -733,9 +793,33 @@ class Gem5Connection(TelnetConnection):
""" """
gem5_logger.info("Mounting VirtIO device in simulated system") gem5_logger.info("Mounting VirtIO device in simulated system")
self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir)) self._gem5_shell('mkdir -p {}'.format(self.gem5_input_dir), as_root=True)
mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir) mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir)
self._gem5_shell(mount_command) self._gem5_shell(mount_command, as_root=True)
def _unmount_virtio(self):
"""
Unmount the VirtIO device in the simulated system.
"""
gem5_logger.info("Unmounting VirtIO device in simulated system")
unmount_command = "umount {}".format(self.gem5_input_dir)
self._gem5_shell(unmount_command, as_root=True)
def take_checkpoint(self):
"""
Take a checkpoint of the simulated system.
In order to take a checkpoint we first unmount the virtio
device, take then checkpoint, and then remount the device to
allow us to continue the current run. This needs to be done to
ensure that future gem5 simulations are able to utilise the
virtio device (i.e., we need to drop the current state
information that the device has).
"""
self._unmount_virtio()
self._gem5_util("checkpoint")
self._mount_virtio()
def _move_to_temp_dir(self, source): def _move_to_temp_dir(self, source):
""" """

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -26,6 +26,11 @@ is not the best language to use for configuration.
""" """
import math import math
import re
import sys
from functools import total_ordering
from past.builtins import basestring
from devlib.utils.misc import isiterable, to_identifier, ranges_to_list, list_to_mask from devlib.utils.misc import isiterable, to_identifier, ranges_to_list, list_to_mask
@@ -68,6 +73,15 @@ def numeric(value):
""" """
if isinstance(value, int): if isinstance(value, int):
return value return value
if isinstance(value, basestring):
value = value.strip()
if value.endswith('%'):
try:
return float(value.rstrip('%')) / 100
except ValueError:
raise ValueError('Not numeric: {}'.format(value))
try: try:
fvalue = float(value) fvalue = float(value)
except ValueError: except ValueError:
@@ -79,6 +93,7 @@ def numeric(value):
return fvalue return fvalue
@total_ordering
class caseless_string(str): class caseless_string(str):
""" """
Just like built-in Python string except case-insensitive on comparisons. However, the Just like built-in Python string except case-insensitive on comparisons. However, the
@@ -92,12 +107,17 @@ class caseless_string(str):
return self.lower() == other return self.lower() == other
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) if isinstance(other, basestring):
def __cmp__(self, other):
if isinstance(basestring, other):
other = other.lower() other = other.lower()
return cmp(self.lower(), other) return self.lower() != other
def __lt__(self, other):
if isinstance(other, basestring):
other = other.lower()
return self.lower() < other
def __hash__(self):
return hash(self.lower())
def format(self, *args, **kwargs): def format(self, *args, **kwargs):
return caseless_string(super(caseless_string, self).format(*args, **kwargs)) return caseless_string(super(caseless_string, self).format(*args, **kwargs))
@@ -111,3 +131,40 @@ def bitmask(value):
if not isinstance(value, int): if not isinstance(value, int):
raise ValueError(value) raise ValueError(value)
return value return value
regex_type = type(re.compile(''))
if sys.version_info[0] == 3:
def regex(value):
if isinstance(value, regex_type):
if isinstance(value.pattern, str):
return value
return re.compile(value.pattern.decode(),
value.flags | re.UNICODE)
else:
if isinstance(value, bytes):
value = value.decode()
return re.compile(value)
def bytes_regex(value):
if isinstance(value, regex_type):
if isinstance(value.pattern, bytes):
return value
return re.compile(value.pattern.encode(sys.stdout.encoding),
value.flags & ~re.UNICODE)
else:
if isinstance(value, str):
value = value.encode(sys.stdout.encoding)
return re.compile(value)
else:
def regex(value):
if isinstance(value, regex_type):
return value
else:
return re.compile(value)
bytes_regex = regex

View File

@@ -1,4 +1,4 @@
# Copyright 2014-2015 ARM Limited # Copyright 2014-2018 ARM Limited
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -19,6 +19,8 @@ import time
import logging import logging
from copy import copy from copy import copy
from past.builtins import basestring
from devlib.utils.serial_port import write_characters, TIMEOUT from devlib.utils.serial_port import write_characters, TIMEOUT
from devlib.utils.types import boolean from devlib.utils.types import boolean
@@ -193,14 +195,14 @@ class UefiMenu(object):
is not in the current menu, ``LookupError`` will be raised.""" is not in the current menu, ``LookupError`` will be raised."""
if not self.prompt: if not self.prompt:
self.read_menu(timeout) self.read_menu(timeout)
return self.options.items() return list(self.options.items())
def get_option_index(self, text, timeout=default_timeout): def get_option_index(self, text, timeout=default_timeout):
"""Returns the menu index of the specified option text (uses regex matching). If the option """Returns the menu index of the specified option text (uses regex matching). If the option
is not in the current menu, ``LookupError`` will be raised.""" is not in the current menu, ``LookupError`` will be raised."""
if not self.prompt: if not self.prompt:
self.read_menu(timeout) self.read_menu(timeout)
for k, v in self.options.iteritems(): for k, v in self.options.items():
if re.search(text, v): if re.search(text, v):
return k return k
raise LookupError(text) raise LookupError(text)

30
devlib/utils/version.py Normal file
View File

@@ -0,0 +1,30 @@
# Copyright 2018 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 subprocess import Popen, PIPE
def get_commit():
p = Popen(['git', 'rev-parse', 'HEAD'], cwd=os.path.dirname(__file__),
stdout=PIPE, stderr=PIPE)
std, _ = p.communicate()
p.wait()
if p.returncode:
return None
if sys.version_info[0] == 3:
return std[:8].decode(sys.stdout.encoding, 'replace')
else:
return std[:8]

View File

@@ -99,19 +99,19 @@ Connection Types
``adb`` is part of the Android SDK (though stand-alone versions are also ``adb`` is part of the Android SDK (though stand-alone versions are also
available). available).
:param device: The name of the adb divice. This is usually a unique hex :param device: The name of the adb device. This is usually a unique hex
string for USB-connected devices, or an ip address/port string for USB-connected devices, or an ip address/port
combination. To see connected devices, you can run ``adb combination. To see connected devices, you can run ``adb
devices`` on the host. devices`` on the host.
:param timeout: Connection timeout in seconds. If a connection to the device :param timeout: Connection timeout in seconds. If a connection to the device
is not esblished within this period, :class:`HostError` is not established within this period, :class:`HostError`
is raised. is raised.
.. class:: SshConnection(host, username, password=None, keyfile=None, port=None,\ .. class:: SshConnection(host, username, password=None, keyfile=None, port=None,\
timeout=None, password_prompt=None) timeout=None, password_prompt=None)
A connectioned to a device on the network over SSH. A connection to a device on the network over SSH.
:param host: SSH host to which to connect :param host: SSH host to which to connect
:param username: username for SSH login :param username: username for SSH login
@@ -126,21 +126,21 @@ Connection Types
.. note:: ``keyfile`` and ``password`` can't be specified .. note:: ``keyfile`` and ``password`` can't be specified
at the same time. at the same time.
:param port: TCP port on which SSH server is litening on the remoted device. :param port: TCP port on which SSH server is listening on the remote device.
Omit to use the default port. Omit to use the default port.
:param timeout: Timeout for the connection in seconds. If a connection :param timeout: Timeout for the connection in seconds. If a connection
cannot be established within this time, an error will be cannot be established within this time, an error will be
raised. raised.
:param password_prompt: A string with the password prompt used by :param password_prompt: A string with the password prompt used by
``sshpass``. Set this if your version of ``sshpass`` ``sshpass``. Set this if your version of ``sshpass``
uses somethin other than ``"[sudo] password"``. uses something other than ``"[sudo] password"``.
.. class:: TelnetConnection(host, username, password=None, port=None,\ .. class:: TelnetConnection(host, username, password=None, port=None,\
timeout=None, password_prompt=None,\ timeout=None, password_prompt=None,\
original_prompt=None) original_prompt=None)
A connectioned to a device on the network over Telenet. A connection to a device on the network over Telenet.
.. note:: Since Telenet protocol is does not support file transfer, scp is .. note:: Since Telenet protocol is does not support file transfer, scp is
used for that purpose. used for that purpose.
@@ -153,19 +153,19 @@ Connection Types
``sshpass`` utility must be installed on the ``sshpass`` utility must be installed on the
system. system.
:param port: TCP port on which SSH server is litening on the remoted device. :param port: TCP port on which SSH server is listening on the remote device.
Omit to use the default port. Omit to use the default port.
:param timeout: Timeout for the connection in seconds. If a connection :param timeout: Timeout for the connection in seconds. If a connection
cannot be established within this time, an error will be cannot be established within this time, an error will be
raised. raised.
:param password_prompt: A string with the password prompt used by :param password_prompt: A string with the password prompt used by
``sshpass``. Set this if your version of ``sshpass`` ``sshpass``. Set this if your version of ``sshpass``
uses somethin other than ``"[sudo] password"``. uses something other than ``"[sudo] password"``.
:param original_prompt: A regex for the shell prompted presented in the Telenet :param original_prompt: A regex for the shell prompted presented in the Telenet
connection (the prompt will be reset to a connection (the prompt will be reset to a
randomly-generated pattern for the duration of the randomly-generated pattern for the duration of the
connection to reduce the possibility of clashes). connection to reduce the possibility of clashes).
This paramer is ignored for SSH connections. This parameter is ignored for SSH connections.
.. class:: LocalConnection(keep_password=True, unrooted=False, password=None) .. class:: LocalConnection(keep_password=True, unrooted=False, password=None)
@@ -189,7 +189,7 @@ Connection Types
A connection to a gem5 simulation using a local Telnet connection. A connection to a gem5 simulation using a local Telnet connection.
.. note:: Some of the following input parameters are optional and will be ignored during .. note:: Some of the following input parameters are optional and will be ignored during
initialisation. They were kept to keep the anology with a :class:`TelnetConnection` initialisation. They were kept to keep the analogy with a :class:`TelnetConnection`
(i.e. ``host``, `username``, ``password``, ``port``, (i.e. ``host``, `username``, ``password``, ``port``,
``password_prompt`` and ``original_promp``) ``password_prompt`` and ``original_promp``)
@@ -220,7 +220,7 @@ Connection Types
There are two classes that inherit from :class:`Gem5Connection`: There are two classes that inherit from :class:`Gem5Connection`:
:class:`AndroidGem5Connection` and :class:`LinuxGem5Connection`. :class:`AndroidGem5Connection` and :class:`LinuxGem5Connection`.
They inherit *almost* all methods from the parent class, without altering them. They inherit *almost* all methods from the parent class, without altering them.
The only methods discussed belows are those that will be overwritten by the The only methods discussed below are those that will be overwritten by the
:class:`LinuxGem5Connection` and :class:`AndroidGem5Connection` respectively. :class:`LinuxGem5Connection` and :class:`AndroidGem5Connection` respectively.
.. class:: LinuxGem5Connection .. class:: LinuxGem5Connection

View File

@@ -0,0 +1,221 @@
Derived Measurements
=====================
The ``DerivedMeasurements`` API provides a consistent way of performing post
processing on a provided :class:`MeasurementCsv` file.
Example
-------
The following example shows how to use an implementation of a
:class:`DerivedMeasurement` to obtain a list of calculated ``DerivedMetric``'s.
.. code-block:: ipython
# Import the relevant derived measurement module
# in this example the derived energy module is used.
In [1]: from devlib import DerivedEnergyMeasurements
# Obtain a MeasurementCsv file from an instrument or create from
# existing .csv file. In this example an existing csv file is used which was
# created with a sampling rate of 100Hz
In [2]: from devlib import MeasurementsCsv
In [3]: measurement_csv = MeasurementsCsv('/example/measurements.csv', sample_rate_hz=100)
# Process the file and obtain a list of the derived measurements
In [4]: derived_measurements = DerivedEnergyMeasurements.process(measurement_csv)
In [5]: derived_measurements
Out[5]: [device_energy: 239.1854075 joules, device_power: 5.5494089227 watts]
API
---
Derived Measurements
~~~~~~~~~~~~~~~~~~~~
.. class:: DerivedMeasurements
The ``DerivedMeasurements`` class provides an API for post-processing
instrument output offline (i.e. without a connection to the target device) to
generate additional metrics.
.. method:: DerivedMeasurements.process(measurement_csv)
Process a :class:`MeasurementsCsv`, returning a list of
:class:`DerivedMetric` and/or :class:`MeasurementsCsv` objects that have been
derived from the input. The exact nature and ordering of the list members
is specific to individual 'class'`DerivedMeasurements` implementations.
.. method:: DerivedMeasurements.process_raw(\*args)
Process raw output from an instrument, returning a list :class:`DerivedMetric`
and/or :class:`MeasurementsCsv` objects that have been derived from the
input. The exact nature and ordering of the list members is specific to
individual 'class'`DerivedMeasurements` implementations.
The arguments to this method should be paths to raw output files generated by
an instrument. The number and order of expected arguments is specific to
particular implementations.
Derived Metric
~~~~~~~~~~~~~~
.. class:: DerivedMetric
Represents a metric derived from previously collected ``Measurement``s.
Unlike, a ``Measurement``, this was not measured directly from the target.
.. attribute:: DerivedMetric.name
The name of the derived metric. This uniquely defines a metric -- two
``DerivedMetric`` objects with the same ``name`` represent to instances of
the same metric (e.g. computed from two different inputs).
.. attribute:: DerivedMetric.value
The ``numeric`` value of the metric that has been computed for a particular
input.
.. attribute:: DerivedMetric.measurement_type
The ``MeasurementType`` of the metric. This indicates which conceptual
category the metric falls into, its units, and conversions to other
measurement types.
.. attribute:: DerivedMetric.units
The units in which the metric's value is expressed.
Available Derived Measurements
-------------------------------
.. note:: If a method of the API is not documented for a particular
implementation, that means that it s not overridden by that
implementation. It is still safe to call it -- an empty list will be
returned.
Energy
~~~~~~
.. class:: DerivedEnergyMeasurements
The ``DerivedEnergyMeasurements`` class is used to calculate average power and
cumulative energy for each site if the required data is present.
The calculation of cumulative energy can occur in 3 ways. If a
``site`` contains ``energy`` results, the first and last measurements are extracted
and the delta calculated. If not, a ``timestamp`` channel will be used to calculate
the energy from the power channel, failing back to using the sample rate attribute
of the :class:`MeasurementCsv` file if timestamps are not available. If neither
timestamps or a sample rate are available then an error will be raised.
.. method:: DerivedEnergyMeasurements.process(measurement_csv)
This will return total cumulative energy for each energy channel, and the
average power for each power channel in the input CSV. The output will contain
all energy metrics followed by power metrics. The ordering of both will match
the ordering of channels in the input. The metrics will by named based on the
sites of the corresponding channels according to the following patters:
``"<site>_total_energy"`` and ``"<site>_average_power"``.
FPS / Rendering
~~~~~~~~~~~~~~~
.. class:: DerivedGfxInfoStats(drop_threshold=5, suffix='-fps', filename=None, outdir=None)
Produces FPS (frames-per-second) and other derived statistics from
:class:`GfxInfoFramesInstrument` output. This takes several optional
parameters in creation:
:param drop_threshold: FPS in an application, such as a game, which this
processor is primarily targeted at, cannot reasonably
drop to a very low value. This is specified to this
threshold. If an FPS for a frame is computed to be
lower than this threshold, it will be dropped on the
assumption that frame rendering was suspended by the
system (e.g. when idling), or there was some sort of
error, and therefore this should be used in
performance calculations. defaults to ``5``.
:param suffix: The name of the generated per-frame FPS csv file will be
derived from the input frames csv file by appending this
suffix. This cannot be specified at the same time as
a ``filename``.
:param filename: As an alternative to the suffix, a complete file name for
FPS csv can be specified. This cannot be used at the same
time as the ``suffix``.
:param outdir: By default, the FPS csv file will be placed in the same
directory as the input frames csv file. This can be changed
by specifying an alternate directory here
.. warning:: Specifying both ``filename`` and ``oudir`` will mean that exactly
the same file will be used for FPS output on each invocation of
``process()`` (even for different inputs) resulting in previous
results being overwritten.
.. method:: DerivedGfxInfoStats.process(measurement_csv)
Process the fames csv generated by :class:`GfxInfoFramesInstrument` and
returns a list containing exactly three entries: :class:`DerivedMetric`\ s
``fps`` and ``total_frames``, followed by a :class:`MeasurentCsv` containing
per-frame FPSs values.
.. method:: DerivedGfxInfoStats.process_raw(gfxinfo_frame_raw_file)
As input, this takes a single argument, which should be the path to the raw
output file of :class:`GfxInfoFramesInstrument`. The returns stats
accumulated by gfxinfo. At the time of writing, the stats (in order) are:
``janks``, ``janks_pc`` (percentage of all frames),
``render_time_50th_ptile`` (50th percentile, or median, for time to render a
frame), ``render_time_90th_ptile``, ``render_time_95th_ptile``,
``render_time_99th_ptile``, ``missed_vsync``, ``hight_input_latency``,
``slow_ui_thread``, ``slow_bitmap_uploads``, ``slow_issue_draw_commands``.
Please see the `gfxinfo documentation`_ for details.
.. _gfxinfo documentation: https://developer.android.com/training/testing/performance.html
.. class:: DerivedSurfaceFlingerStats(drop_threshold=5, suffix='-fps', filename=None, outdir=None)
Produces FPS (frames-per-second) and other derived statistics from
:class:`SurfaceFlingerFramesInstrument` output. This takes several optional
parameters in creation:
:param drop_threshold: FPS in an application, such as a game, which this
processor is primarily targeted at, cannot reasonably
drop to a very low value. This is specified to this
threshold. If an FPS for a frame is computed to be
lower than this threshold, it will be dropped on the
assumption that frame rendering was suspended by the
system (e.g. when idling), or there was some sort of
error, and therefore this should be used in
performance calculations. defaults to ``5``.
:param suffix: The name of the generated per-frame FPS csv file will be
derived from the input frames csv file by appending this
suffix. This cannot be specified at the same time as
a ``filename``.
:param filename: As an alternative to the suffix, a complete file name for
FPS csv can be specified. This cannot be used at the same
time as the ``suffix``.
:param outdir: By default, the FPS csv file will be placed in the same
directory as the input frames csv file. This can be changed
by specifying an alternate directory here
.. warning:: Specifying both ``filename`` and ``oudir`` will mean that exactly
the same file will be used for FPS output on each invocation of
``process()`` (even for different inputs) resulting in previous
results being overwritten.
.. method:: DerivedSurfaceFlingerStats.process(measurement_csv)
Process the fames csv generated by :class:`SurfaceFlingerFramesInstrument` and
returns a list containing exactly three entries: :class:`DerivedMetric`\ s
``fps`` and ``total_frames``, followed by a :class:`MeasurentCsv` containing
per-frame FPSs values, followed by ``janks`` ``janks_pc``, and
``missed_vsync`` metrics.

View File

@@ -19,6 +19,7 @@ Contents:
target target
modules modules
instrumentation instrumentation
derived_measurements
platform platform
connection connection

View File

@@ -65,8 +65,8 @@ Instrument
:INSTANTANEOUS: The instrument supports taking a single sample via :INSTANTANEOUS: The instrument supports taking a single sample via
``take_measurement()``. ``take_measurement()``.
:CONTINUOUS: The instrument supports collecting measurements over a :CONTINUOUS: The instrument supports collecting measurements over a
period of time via ``start()``, ``stop()``, and period of time via ``start()``, ``stop()``, ``get_data()``,
``get_data()`` methods. and (optionally) ``get_raw`` methods.
.. note:: It's possible for one instrument to support more than a single .. note:: It's possible for one instrument to support more than a single
mode. mode.
@@ -99,14 +99,21 @@ Instrument
``teardown()`` has been called), but see documentation for the instrument ``teardown()`` has been called), but see documentation for the instrument
you're interested in. you're interested in.
.. method:: Instrument.reset([sites, [kinds]]) .. method:: Instrument.reset(sites=None, kinds=None, channels=None)
This is used to configure an instrument for collection. This must be invoked This is used to configure an instrument for collection. This must be invoked
before ``start()`` is called to begin collection. ``sites`` and ``kinds`` before ``start()`` is called to begin collection. This methods sets the
parameters may be used to specify which channels measurements should be ``active_channels`` attribute of the ``Instrument``.
collected from (if omitted, then measurements will be collected for all
available sites/kinds). This methods sets the ``active_channels`` attribute If ``channels`` is provided, it is a list of names of channels to enable and
of the ``Instrument``. ``sites`` and ``kinds`` must both be ``None``.
Otherwise, if one of ``sites`` or ``kinds`` is provided, all channels
matching the given sites or kinds are enabled. If both are provided then all
channels of the given kinds at the given sites are enabled.
If none of ``sites``, ``kinds`` or ``channels`` are provided then all
available channels are enabled.
.. method:: Instrument.take_measurment() .. method:: Instrument.take_measurment()
@@ -114,14 +121,14 @@ Instrument
:class:`Measurement` objects (one for each active channel). :class:`Measurement` objects (one for each active channel).
.. note:: This method is only implemented by :class:`Instrument`\ s that .. note:: This method is only implemented by :class:`Instrument`\ s that
support ``INSTANTANEOUS`` measurment. support ``INSTANTANEOUS`` measurement.
.. method:: Instrument.start() .. method:: Instrument.start()
Starts collecting measurements from ``active_channels``. Starts collecting measurements from ``active_channels``.
.. note:: This method is only implemented by :class:`Instrument`\ s that .. note:: This method is only implemented by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment. support ``CONTINUOUS`` measurement.
.. method:: Instrument.stop() .. method:: Instrument.stop()
@@ -129,29 +136,44 @@ Instrument
:func:`start()`. :func:`start()`.
.. note:: This method is only implemented by :class:`Instrument`\ s that .. note:: This method is only implemented by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment. support ``CONTINUOUS`` measurement.
.. method:: Instrument.get_data(outfile) .. method:: Instrument.get_data(outfile)
Write collected data into ``outfile``. Must be called after :func:`stop()`. Write collected data into ``outfile``. Must be called after :func:`stop()`.
Data will be written in CSV format with a column for each channel and a row Data will be written in CSV format with a column for each channel and a row
for each sample. Column heading will be channel, labels in the form for each sample. Column heading will be channel, labels in the form
``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the coluns ``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the columns
will be the same as the order of channels in ``Instrument.active_channels``. will be the same as the order of channels in ``Instrument.active_channels``.
If reporting timestamps, one channel must have a ``site`` named ``"timestamp"``
and a ``kind`` of a :class:`MeasurmentType` of an appropriate time unit which will
be used, if appropriate, during any post processing.
.. note:: Currently supported time units are seconds, milliseconds and
microseconds, other units can also be used if an appropriate
conversion is provided.
This returns a :class:`MeasurementCsv` instance associated with the outfile This returns a :class:`MeasurementCsv` instance associated with the outfile
that can be used to stream :class:`Measurement`\ s lists (similar to what is that can be used to stream :class:`Measurement`\ s lists (similar to what is
returned by ``take_measurement()``. returned by ``take_measurement()``.
.. note:: This method is only implemented by :class:`Instrument`\ s that .. note:: This method is only implemented by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment. support ``CONTINUOUS`` measurement.
.. method:: Instrument.get_raw()
Returns a list of paths to files containing raw output from the underlying
source(s) that is used to produce the data CSV. If now raw output is
generated or saved, an empty list will be returned. The format of the
contents of the raw files is entirely source-dependent.
.. attribute:: Instrument.sample_rate_hz .. attribute:: Instrument.sample_rate_hz
Sample rate of the instrument in Hz. Assumed to be the same for all channels. Sample rate of the instrument in Hz. Assumed to be the same for all channels.
.. note:: This attribute is only provided by :class:`Instrument`\ s that .. note:: This attribute is only provided by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment. support ``CONTINUOUS`` measurement.
Instrument Channel Instrument Channel
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
@@ -163,16 +185,16 @@ Instrument Channel
``site`` and a ``measurement_type``. ``site`` and a ``measurement_type``.
A ``site`` indicates where on the target a measurement is collected from A ``site`` indicates where on the target a measurement is collected from
(e.g. a volage rail or location of a sensor). (e.g. a voltage rail or location of a sensor).
A ``measurement_type`` is an instance of :class:`MeasurmentType` that A ``measurement_type`` is an instance of :class:`MeasurmentType` that
describes what sort of measurment this is (power, temperature, etc). Each describes what sort of measurement this is (power, temperature, etc). Each
mesurement type has a standard unit it is reported in, regardless of an measurement type has a standard unit it is reported in, regardless of an
instrument used to collect it. instrument used to collect it.
A channel (i.e. site/measurement_type combination) is unique per instrument, A channel (i.e. site/measurement_type combination) is unique per instrument,
however there may be more than one channel associated with one site (e.g. for however there may be more than one channel associated with one site (e.g. for
both volatage and power). both voltage and power).
It should not be assumed that any site/measurement_type combination is valid. It should not be assumed that any site/measurement_type combination is valid.
The list of available channels can queried with The list of available channels can queried with
@@ -180,22 +202,22 @@ Instrument Channel
.. attribute:: InstrumentChannel.site .. attribute:: InstrumentChannel.site
The name of the "site" from which the measurments are collected (e.g. voltage The name of the "site" from which the measurements are collected (e.g. voltage
rail, sensor, etc). rail, sensor, etc).
.. attribute:: InstrumentChannel.kind .. attribute:: InstrumentChannel.kind
A string indingcating the type of measrument that will be collted. This is A string indicating the type of measurement that will be collected. This is
the ``name`` of the :class:`MeasurmentType` associated with this channel. the ``name`` of the :class:`MeasurmentType` associated with this channel.
.. attribute:: InstrumentChannel.units .. attribute:: InstrumentChannel.units
Units in which measurment will be reported. this is determined by the Units in which measurement will be reported. this is determined by the
underlying :class:`MeasurmentType`. underlying :class:`MeasurmentType`.
.. attribute:: InstrumentChannel.label .. attribute:: InstrumentChannel.label
A label that can be attached to measurments associated with with channel. A label that can be attached to measurements associated with with channel.
This is constructed with :: This is constructed with ::
'{}_{}'.format(self.site, self.kind) '{}_{}'.format(self.site, self.kind)
@@ -211,27 +233,33 @@ be reported as "power" in Watts, and never as "pwr" in milliWatts. Currently
defined measurement types are defined measurement types are
+-------------+---------+---------------+ +-------------+-------------+---------------+
| name | units | category | | name | units | category |
+=============+=========+===============+ +=============+=============+===============+
| time | seconds | | | count | count | |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| temperature | degrees | | | percent | percent | |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| time_us | microseconds| time |
+-------------+-------------+---------------+
| time_ms | milliseconds| time |
+-------------+-------------+---------------+
| temperature | degrees | thermal |
+-------------+-------------+---------------+
| power | watts | power/energy | | power | watts | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| voltage | volts | power/energy | | voltage | volts | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| current | amps | power/energy | | current | amps | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| energy | joules | power/energy | | energy | joules | power/energy |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| tx | bytes | data transfer | | tx | bytes | data transfer |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| rx | bytes | data transfer | | rx | bytes | data transfer |
+-------------+---------+---------------+ +-------------+-------------+---------------+
| tx/rx | bytes | data transfer | | tx/rx | bytes | data transfer |
+-------------+---------+---------------+ +-------------+-------------+---------------+
.. instruments: .. instruments:

View File

@@ -106,11 +106,20 @@ policies (governors). The ``devlib`` module exposes the following interface
target.cpufreq.set_min_frequency(cpu, frequency[, exact=True]) target.cpufreq.set_min_frequency(cpu, frequency[, exact=True])
target.cpufreq.set_max_frequency(cpu, frequency[, exact=True]) target.cpufreq.set_max_frequency(cpu, frequency[, exact=True])
Get and set min and max frequencies on the specified CPU. "set" functions are Get the currently set, or set new min and max frequencies for the specified
available with all governors other than ``userspace``. CPU. "set" functions are available with all governors other than
``userspace``.
:param cpu: The cpu; could be a numeric or the corresponding string (e.g. :param cpu: The cpu; could be a numeric or the corresponding string (e.g.
``1`` or ``"cpu1"``). ``1`` or ``"cpu1"``).
.. method:: target.cpufreq.get_min_available_frequency(cpu)
target.cpufreq.get_max_available_frequency(cpu)
Retrieve the min or max DVFS frequency that is supported (as opposed to
currently enforced) for a given CPU. Returns an int or None if could not be
determined.
:param frequency: Frequency to set. :param frequency: Frequency to set.
.. method:: target.cpufreq.get_frequency(cpu) .. method:: target.cpufreq.get_frequency(cpu)
@@ -126,7 +135,7 @@ policies (governors). The ``devlib`` module exposes the following interface
cpuidle cpuidle
------- -------
``cpufreq`` is the kernel subsystem for managing CPU low power (idle) states. ``cpuidle`` is the kernel subsystem for managing CPU low power (idle) states.
.. method:: target.cpuidle.get_driver() .. method:: target.cpuidle.get_driver()
@@ -182,7 +191,7 @@ Every module (ultimately) derives from :class:`Module` class. A module must
define the following class attributes: define the following class attributes:
:name: A unique name for the module. This cannot clash with any of the existing :name: A unique name for the module. This cannot clash with any of the existing
names and must be a valid Python identifier, but is otherwise free-from. names and must be a valid Python identifier, but is otherwise free-form.
:kind: This identifies the type of functionality a module implements, which in :kind: This identifies the type of functionality a module implements, which in
turn determines the interface implemented by the module (all modules of turn determines the interface implemented by the module (all modules of
the same kind must expose a consistent interface). This must be a valid the same kind must expose a consistent interface). This must be a valid

View File

@@ -74,13 +74,13 @@ This sets up the target for ``devlib`` interaction. This includes creating
working directories, deploying busybox, etc. It's usually enough to do this once working directories, deploying busybox, etc. It's usually enough to do this once
for a new device, as the changes this makes will persist across reboots. for a new device, as the changes this makes will persist across reboots.
However, there is no issue with calling this multiple times, so, to be on the However, there is no issue with calling this multiple times, so, to be on the
safe site, it's a good idea to call this once at the beginning of your scripts. safe side, it's a good idea to call this once at the beginning of your scripts.
Command Execution Command Execution
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
There are several ways to execute a command on the target. In each case, a There are several ways to execute a command on the target. In each case, a
:class:`TargetError` will be raised if something goes wrong. In very case, it is :class:`TargetError` will be raised if something goes wrong. In each case, it is
also possible to specify ``as_root=True`` if the specified command should be also possible to specify ``as_root=True`` if the specified command should be
executed as root. executed as root.
@@ -154,7 +154,7 @@ Process Control
# kill all running instances of a process. # kill all running instances of a process.
t.killall('badexe', signal=signal.SIGKILL) t.killall('badexe', signal=signal.SIGKILL)
# List processes running on the target. This retruns a list of parsed # List processes running on the target. This returns a list of parsed
# PsEntry records. # PsEntry records.
entries = t.ps() entries = t.ps()
# e.g. print virtual memory sizes of all running sshd processes: # e.g. print virtual memory sizes of all running sshd processes:
@@ -173,7 +173,7 @@ Super User Privileges
It is not necessary for the account logged in on the target to have super user It is not necessary for the account logged in on the target to have super user
privileges, however the functionality will obviously be diminished, if that is privileges, however the functionality will obviously be diminished, if that is
not the case. ``devilib`` will determine if the logged in user has root not the case. ``devlib`` will determine if the logged in user has root
privileges and the correct way to invoke it. You should avoid including "sudo" privileges and the correct way to invoke it. You should avoid including "sudo"
directly in your commands, instead, specify ``as_root=True`` where needed. This directly in your commands, instead, specify ``as_root=True`` where needed. This
will make your scripts portable across multiple devices and OS's. will make your scripts portable across multiple devices and OS's.

View File

@@ -24,11 +24,11 @@ it was not specified explicitly by the user.
assumed to be in a cluster). assumed to be in a cluster).
:param big_core: The name of the big core in a big.LITTLE system. If this is :param big_core: The name of the big core in a big.LITTLE system. If this is
not specified it will be inferred (on systems with exactly not specified it will be inferred (on systems with exactly
two clasters). two clusters).
:param model: Model name of the hardware system. If this is not specified it :param model: Model name of the hardware system. If this is not specified it
will be queried at run time. will be queried at run time.
:param modules: Modules with additional functionality supported by the :param modules: Modules with additional functionality supported by the
platfrom (e.g. for handling flashing, rebooting, etc). These platform (e.g. for handling flashing, rebooting, etc). These
would be added to the Target's modules. (See :ref:`modules`\ ). would be added to the Target's modules. (See :ref:`modules`\ ).
@@ -38,13 +38,13 @@ Versatile Express
The generic platform may be extended to support hardware- or The generic platform may be extended to support hardware- or
infrastructure-specific functionality. Platforms exist for ARM infrastructure-specific functionality. Platforms exist for ARM
VersatileExpress-based :class:`Juno` and :class:`TC2` development boards. In VersatileExpress-based :class:`Juno` and :class:`TC2` development boards. In
addition to the standard :class:`Platform` parameters above, these platfroms addition to the standard :class:`Platform` parameters above, these platforms
support additional configuration: support additional configuration:
.. class:: VersatileExpressPlatform .. class:: VersatileExpressPlatform
Normally, this would be instatiated via one of its derived classes Normally, this would be instantiated via one of its derived classes
(:class:`Juno` or :class:`TC2`) that set appropriate defaults for some of (:class:`Juno` or :class:`TC2`) that set appropriate defaults for some of
the parameters. the parameters.
@@ -63,7 +63,7 @@ support additional configuration:
mounted on the host system. mounted on the host system.
:param hard_reset_method: Specifies the method for hard-resetting the devices :param hard_reset_method: Specifies the method for hard-resetting the devices
(e.g. if it becomes unresponsive and normal reboot (e.g. if it becomes unresponsive and normal reboot
method doesn not work). Currently supported methods method doesn't not work). Currently supported methods
are: are:
:dtr: reboot by toggling DTR line on the serial :dtr: reboot by toggling DTR line on the serial
@@ -80,7 +80,7 @@ support additional configuration:
The following values are currently supported: The following values are currently supported:
:uefi: Boot via UEFI menu, by selecting the entry :uefi: Boot via UEFI menu, by selecting the entry
specified by ``uefi_entry`` paramter. If this specified by ``uefi_entry`` parameter. If this
entry does not exist, it will be automatically entry does not exist, it will be automatically
created based on values provided for ``image``, created based on values provided for ``image``,
``initrd``, ``fdt``, and ``bootargs`` parameters. ``initrd``, ``fdt``, and ``bootargs`` parameters.

View File

@@ -2,7 +2,7 @@ Target
====== ======
.. class:: Target(connection_settings=None, platform=None, working_directory=None, executables_directory=None, connect=True, modules=None, load_default_modules=True, shell_prompt=DEFAULT_SHELL_PROMPT) .. class:: Target(connection_settings=None, platform=None, working_directory=None, executables_directory=None, connect=True, modules=None, load_default_modules=True, shell_prompt=DEFAULT_SHELL_PROMPT, conn_cls=None)
:class:`Target` is the primary interface to the remote device. All interactions :class:`Target` is the primary interface to the remote device. All interactions
with the device are performed via a :class:`Target` instance, either with the device are performed via a :class:`Target` instance, either
@@ -38,7 +38,7 @@ Target
by the connection's account). This location will be created, by the connection's account). This location will be created,
if necessary, during ``setup()``. if necessary, during ``setup()``.
This location does *not* to be same as the system's executables This location does *not* need to be same as the system's executables
location. In fact, to prevent devlib from overwriting system's defaults, location. In fact, to prevent devlib from overwriting system's defaults,
it better if this is a separate location, if possible. it better if this is a separate location, if possible.
@@ -68,6 +68,9 @@ Target
prompted on the target. This may be used by some modules that establish prompted on the target. This may be used by some modules that establish
auxiliary connections to a target over UART. auxiliary connections to a target over UART.
:param conn_cls: This is the type of connection that will be used to communicate
with the device.
.. attribute:: Target.core_names .. attribute:: Target.core_names
This is a list containing names of CPU cores on the target, in the order in This is a list containing names of CPU cores on the target, in the order in
@@ -83,12 +86,12 @@ Target
.. attribute:: Target.big_core .. attribute:: Target.big_core
This is the name of the cores that the "big"s in an ARM big.LITTLE This is the name of the cores that are the "big"s in an ARM big.LITTLE
configuration. This is obtained via the underlying :class:`Platform`. configuration. This is obtained via the underlying :class:`Platform`.
.. attribute:: Target.little_core .. attribute:: Target.little_core
This is the name of the cores that the "little"s in an ARM big.LITTLE This is the name of the cores that are the "little"s in an ARM big.LITTLE
configuration. This is obtained via the underlying :class:`Platform`. configuration. This is obtained via the underlying :class:`Platform`.
.. attribute:: Target.is_connected .. attribute:: Target.is_connected
@@ -199,21 +202,23 @@ Target
operations during reboot process to detect if the reboot has failed and operations during reboot process to detect if the reboot has failed and
the device has hung. the device has hung.
.. method:: Target.push(source, dest [, timeout]) .. method:: Target.push(source, dest [,as_root , timeout])
Transfer a file from the host machine to the target device. Transfer a file from the host machine to the target device.
:param source: path of to the file on the host :param source: path of to the file on the host
:param dest: path of to the file on the target :param dest: path of to the file on the target
:param as_root: whether root is required. Defaults to false.
:param timeout: timeout (in seconds) for the transfer; if the transfer does :param timeout: timeout (in seconds) for the transfer; if the transfer does
not complete within this period, an exception will be raised. not complete within this period, an exception will be raised.
.. method:: Target.pull(source, dest [, timeout]) .. method:: Target.pull(source, dest [, as_root, timeout])
Transfer a file from the target device to the host machine. Transfer a file from the target device to the host machine.
:param source: path of to the file on the target :param source: path of to the file on the target
:param dest: path of to the file on the host :param dest: path of to the file on the host
:param as_root: whether root is required. Defaults to false.
:param timeout: timeout (in seconds) for the transfer; if the transfer does :param timeout: timeout (in seconds) for the transfer; if the transfer does
not complete within this period, an exception will be raised. not complete within this period, an exception will be raised.
@@ -265,6 +270,24 @@ Target
:param timeout: If this is specified and invocation does not terminate within this number :param timeout: If this is specified and invocation does not terminate within this number
of seconds, an exception will be raised. of seconds, an exception will be raised.
.. method:: Target.background_invoke(binary [, args [, in_directory [, on_cpus [, as_root ]]]])
Execute the specified binary on target (must already be installed) as a background
task, under the specified conditions and return the :class:`subprocess.Popen`
instance for the command.
:param binary: binary to execute. Must be present and executable on the device.
:param args: arguments to be passed to the binary. The can be either a list or
a string.
:param in_directory: execute the binary in the specified directory. This must
be an absolute path.
:param on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
case, it will be interpreted as the mask), a list of ``ints``, in which
case this will be interpreted as the list of cpus, or string, which
will be interpreted as a comma-separated list of cpu ranges, e.g.
``"0,4-7"``.
:param as_root: Specify whether the command should be run as root
.. method:: Target.kick_off(command [, as_root]) .. method:: Target.kick_off(command [, as_root])
Kick off the specified command on the target and return immediately. Unlike Kick off the specified command on the target and return immediately. Unlike
@@ -288,11 +311,11 @@ Target
.. method:: Target.read_int(self, path) .. method:: Target.read_int(self, path)
Equivalent to ``Target.read_value(path, kind=devlab.utils.types.integer)`` Equivalent to ``Target.read_value(path, kind=devlib.utils.types.integer)``
.. method:: Target.read_bool(self, path) .. method:: Target.read_bool(self, path)
Equivalent to ``Target.read_value(path, kind=devlab.utils.types.boolean)`` Equivalent to ``Target.read_value(path, kind=devlib.utils.types.boolean)``
.. method:: Target.write_value(path, value [, verify]) .. method:: Target.write_value(path, value [, verify])
@@ -306,6 +329,32 @@ Target
some sysfs entries silently failing to set the written value without some sysfs entries silently failing to set the written value without
returning an error code. returning an error code.
.. method:: Target.read_tree_values(path, depth=1, dictcls=dict):
Read values of all sysfs (or similar) file nodes under ``path``, traversing
up to the maximum depth ``depth``.
Returns a nested structure of dict-like objects (``dict``\ s by default) that
follows the structure of the scanned sub-directory tree. The top-level entry
has a single item who's key is ``path``. If ``path`` points to a single file,
the value of the entry is the value ready from that file node. Otherwise, the
value is a dict-line object with a key for every entry under ``path``
mapping onto its value or further dict-like objects as appropriate.
:param path: sysfs path to scan
:param depth: maximum depth to descend
:param dictcls: a dict-like type to be used for each level of the hierarchy.
.. method:: Target.read_tree_values_flat(path, depth=1):
Read values of all sysfs (or similar) file nodes under ``path``, traversing
up to the maximum depth ``depth``.
Returns a dict mapping paths of file nodes to corresponding values.
:param path: sysfs path to scan
:param depth: maximum depth to descend
.. method:: Target.reset() .. method:: Target.reset()
Soft reset the target. Typically, this means executing ``reboot`` on the Soft reset the target. Typically, this means executing ``reboot`` on the
@@ -392,7 +441,9 @@ Target
.. method:: Target.capture_screen(filepath) .. method:: Target.capture_screen(filepath)
Take a screenshot on the device and save it to the specified file on the Take a screenshot on the device and save it to the specified file on the
host. This may not be supported by the target. host. This may not be supported by the target. You can optionally insert a
``{ts}`` tag into the file name, in which case it will be substituted with
on-target timestamp of the screen shot in ISO8601 format.
.. method:: Target.install(filepath[, timeout[, with_name]]) .. method:: Target.install(filepath[, timeout[, with_name]])
@@ -402,6 +453,17 @@ Target
:param timeout: Optional timeout (in seconds) for the installation :param timeout: Optional timeout (in seconds) for the installation
:param with_name: This may be used to rename the executable on the target :param with_name: This may be used to rename the executable on the target
.. method:: Target.install_if_needed(host_path, search_system_binaries=True)
Check to see if the binary is already installed on the device and if not,
install it.
:param host_path: path to the executable on the host
:param search_system_binaries: Specify whether to search the devices PATH
when checking to see if the executable is installed, otherwise only check
user installed binaries.
.. method:: Target.uninstall(name) .. method:: Target.uninstall(name)
Uninstall the specified executable from the target Uninstall the specified executable from the target
@@ -422,13 +484,163 @@ Target
.. method:: Target.extract(path, dest=None) .. method:: Target.extract(path, dest=None)
Extracts the specified archive/file and returns the path to the extrated Extracts the specified archive/file and returns the path to the extracted
contents. The extraction method is determined based on the file extension. contents. The extraction method is determined based on the file extension.
``zip``, ``tar``, ``gzip``, and ``bzip2`` are supported. ``zip``, ``tar``, ``gzip``, and ``bzip2`` are supported.
:param dest: Specified an on-target destination directory (which must exist) :param dest: Specified an on-target destination directory (which must exist)
for the extrated contents. for the extracted contents.
Returns the path to the extracted contents. In case of files (gzip and Returns the path to the extracted contents. In case of files (gzip and
bzip2), the path to the decompressed file is returned; for archives, the bzip2), the path to the decompressed file is returned; for archives, the
path to the directory with the archive's contents is returned. path to the directory with the archive's contents is returned.
.. method:: Target.is_network_connected()
Checks for internet connectivity on the device. This doesn't actually
guarantee that the internet connection is "working" (which is rather
nebulous), it's intended just for failing early when definitively _not_
connected to the internet.
:returns: ``True`` if internet seems available, ``False`` otherwise.
Android Target
---------------
.. class:: AndroidTarget(connection_settings=None, platform=None, working_directory=None, executables_directory=None, connect=True, modules=None, load_default_modules=True, shell_prompt=DEFAULT_SHELL_PROMPT, conn_cls=AdbConnection, package_data_directory="/data/data")
:class:`AndroidTarget` is a subclass of :class:`Target` with additional features specific to a device running Android.
:param package_data_directory: This is the location of the data stored
for installed Android packages on the device.
.. method:: AndroidTarget.set_rotation(rotation)
Specify an integer representing the desired screen rotation with the
following mappings: Natural: ``0``, Rotated Left: ``1``, Inverted : ``2``
and Rotated Right : ``3``.
.. method:: AndroidTarget.get_rotation(rotation)
Returns an integer value representing the orientation of the devices
screen. ``0`` : Natural, ``1`` : Rotated Left, ``2`` : Inverted
and ``3`` : Rotated Right.
.. method:: AndroidTarget.set_natural_rotation()
Sets the screen orientation of the device to its natural (0 degrees)
orientation.
.. method:: AndroidTarget.set_left_rotation()
Sets the screen orientation of the device to 90 degrees.
.. method:: AndroidTarget.set_inverted_rotation()
Sets the screen orientation of the device to its inverted (180 degrees)
orientation.
.. method:: AndroidTarget.set_right_rotation()
Sets the screen orientation of the device to 270 degrees.
.. method:: AndroidTarget.set_auto_rotation(autorotate)
Specify a boolean value for whether the devices auto-rotation should
be enabled.
.. method:: AndroidTarget.get_auto_rotation()
Returns ``True`` if the targets auto rotation is currently enabled and
``False`` otherwise.
.. method:: AndroidTarget.set_airplane_mode(mode)
Specify a boolean value for whether the device should be in airplane mode.
.. note:: Requires the device to be rooted if the device is running Android 7+.
.. method:: AndroidTarget.get_airplane_mode()
Returns ``True`` if the target is currently in airplane mode and
``False`` otherwise.
.. method:: AndroidTarget.set_brightness(value)
Sets the devices screen brightness to a specified integer between ``0`` and
``255``.
.. method:: AndroidTarget.get_brightness()
Returns an integer between ``0`` and ``255`` representing the devices
current screen brightness.
.. method:: AndroidTarget.set_auto_brightness(auto_brightness)
Specify a boolean value for whether the devices auto brightness
should be enabled.
.. method:: AndroidTarget.get_auto_brightness()
Returns ``True`` if the targets auto brightness is currently
enabled and ``False`` otherwise.
.. method:: AndroidTarget.ensure_screen_is_off()
Checks if the devices screen is on and if so turns it off.
.. method:: AndroidTarget.ensure_screen_is_on()
Checks if the devices screen is off and if so turns it on.
.. method:: AndroidTarget.is_screen_on()
Returns ``True`` if the targets screen is currently on and ``False``
otherwise.
.. method:: AndroidTarget.homescreen()
Returns the device to its home screen.
.. method:: AndroidTarget.swipe_to_unlock(direction="diagonal")
Performs a swipe input on the device to try and unlock the device.
A direction of ``"horizontal"``, ``"vertical"`` or ``"diagonal"``
can be supplied to specify in which direction the swipe should be
performed. By default ``"diagonal"`` will be used to try and
support the majority of newer devices.
ChromeOS Target
---------------
.. class:: ChromeOsTarget(connection_settings=None, platform=None, working_directory=None, executables_directory=None, android_working_directory=None, android_executables_directory=None, connect=True, modules=None, load_default_modules=True, shell_prompt=DEFAULT_SHELL_PROMPT, package_data_directory="/data/data")
:class:`ChromeOsTarget` is a subclass of :class:`LinuxTarget` with
additional features specific to a device running ChromeOS for example,
if supported, its own android container which can be accessed via the
``android_container`` attribute. When making calls to or accessing
properties and attributes of the ChromeOS target, by default they will
be applied to Linux target as this is where the majority of device
configuration will be performed and if not available, will fall back to
using the android container if available. This means that all the
available methods from
:class:`LinuxTarget` and :class:`AndroidTarget` are available for
:class:`ChromeOsTarget` if the device supports android otherwise only the
:class:`LinuxTarget` methods will be available.
:param working_directory: This is the location of the working
directory to be used for the Linux target container. If not specified will
default to ``"/mnt/stateful_partition/devlib-target"``.
:param android_working_directory: This is the location of the working
directory to be used for the android container. If not specified it will
use the working directory default for :class:`AndroidTarget.`.
:param android_executables_directory: This is the location of the
executables directory to be used for the android container. If not
specified will default to a ``bin`` subfolder in the
``android_working_directory.``
:param package_data_directory: This is the location of the data stored
for installed Android packages on the device.

View File

@@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
# #
import imp
import os import os
import sys import sys
import warnings import warnings
@@ -20,8 +21,10 @@ from itertools import chain
try: try:
from setuptools import setup from setuptools import setup
from setuptools.command.sdist import sdist as orig_sdist
except ImportError: except ImportError:
from distutils.core import setup from distutils.core import setup
from distutils.command.sdist import sdist as orig_sdist
devlib_dir = os.path.join(os.path.dirname(__file__), 'devlib') devlib_dir = os.path.join(os.path.dirname(__file__), 'devlib')
@@ -37,6 +40,26 @@ try:
except OSError: except OSError:
pass pass
with open(os.path.join(devlib_dir, '__init__.py')) as fh:
# Extract the version by parsing the text of the file,
# as may not be able to load as a module yet.
for line in fh:
if '__version__' in line:
parts = line.split("'")
__version__ = parts[1]
break
else:
raise RuntimeError('Did not see __version__')
vh_path = os.path.join(devlib_dir, 'utils', 'version.py')
# can load this, as it does not have any devlib imports
version_helper = imp.load_source('version_helper', vh_path)
commit = version_helper.get_commit()
if commit:
__version__ = '{}+{}'.format(__version__, commit)
packages = [] packages = []
data_files = {} data_files = {}
source_dir = os.path.dirname(__file__) source_dir = os.path.dirname(__file__)
@@ -59,10 +82,10 @@ for root, dirs, files in os.walk(devlib_dir):
params = dict( params = dict(
name='devlib', name='devlib',
description='A framework for automating workload execution and measurment collection on ARM devices.', description='A framework for automating workload execution and measurment collection on ARM devices.',
version='0.0.4', version=__version__,
packages=packages, packages=packages,
package_data=data_files, package_data=data_files,
url='N/A', url='https://github.com/ARM-software/devlib',
license='Apache v2', license='Apache v2',
maintainer='ARM Ltd.', maintainer='ARM Ltd.',
install_requires=[ install_requires=[
@@ -70,10 +93,12 @@ params = dict(
'pexpect>=3.3', # Send/recieve to/from device 'pexpect>=3.3', # Send/recieve to/from device
'pyserial', # Serial port interface 'pyserial', # Serial port interface
'wrapt', # Basic for construction of decorator functions 'wrapt', # Basic for construction of decorator functions
'future', # Python 2-3 compatibility
], ],
extras_require={ extras_require={
'daq': ['daqpower'], 'daq': ['daqpower'],
'doc': ['sphinx'], 'doc': ['sphinx'],
'monsoon': ['python-gflags'],
}, },
# https://pypi.python.org/pypi?%3Aaction=list_classifiers # https://pypi.python.org/pypi?%3Aaction=list_classifiers
classifiers=[ classifiers=[
@@ -84,7 +109,28 @@ params = dict(
], ],
) )
all_extras = list(chain(params['extras_require'].itervalues())) all_extras = list(chain(iter(params['extras_require'].values())))
params['extras_require']['full'] = all_extras params['extras_require']['full'] = all_extras
class sdist(orig_sdist):
user_options = orig_sdist.user_options + [
('strip-commit', 's',
"Strip git commit hash from package version ")
]
def initialize_options(self):
orig_sdist.initialize_options(self)
self.strip_commit = False
def run(self):
if self.strip_commit:
self.distribution.get_version = lambda : __version__.split('+')[0]
orig_sdist.run(self)
params['cmdclass'] = {'sdist': sdist}
setup(**params) setup(**params)

View File

@@ -114,7 +114,7 @@ struct reading
double sys_enm_ch0_gpu; double sys_enm_ch0_gpu;
}; };
inline uint64_t join_64bit_register(uint32_t *buffer, int index) static inline uint64_t join_64bit_register(uint32_t *buffer, int index)
{ {
uint64_t result = 0; uint64_t result = 0;
result |= buffer[index]; result |= buffer[index];
@@ -254,10 +254,10 @@ void emeter_init(struct emeter *this, char *outfile)
} }
if(this->out) { if(this->out) {
fprintf(this->out, "sys_curr,a57_curr,a53_curr,gpu_curr," fprintf(this->out, "sys_current,a57_current,a53_current,gpu_current,"
"sys_volt,a57_volt,a53_volt,gpu_volt," "sys_voltage,a57_voltage,a53_voltage,gpu_voltage,"
"sys_pow,a57_pow,a53_pow,gpu_pow," "sys_power,a57_power,a53_power,gpu_power,"
"sys_cenr,a57_cenr,a53_cenr,gpu_cenr\n"); "sys_energy,a57_energy,a53_energy,gpu_energy\n");
} }
} }