-
Notifications
You must be signed in to change notification settings - Fork 78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add generic perf trace instrument #388
Closed
Closed
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
1edd0e1
utils/cli: Introduce class to build CLI commands
pietos01-arm 30ac951
trace/perf: Introduce a new, generic collector
pietos01-arm e652561
fixup! trace/perf: Introduce a new, generic collector
pietos01-arm fa67be2
fixup! trace/perf: Introduce a new, generic collector
pietos01-arm b4d852b
fixup! utils/cli: Introduce class to build CLI commands
pietos01-arm 1520575
fixup! trace/perf: Introduce a new, generic collector
pietos01-arm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
# Copyright 2018 ARM Limited | ||
# Copyright 2018-2019 ARM Limited | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
|
@@ -11,131 +11,128 @@ | |
# 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=missing-docstring | ||
|
||
import collections | ||
import os | ||
import re | ||
from past.builtins import basestring, zip | ||
import sys | ||
|
||
from devlib.utils.cli import Command | ||
from devlib.host import PACKAGE_BIN_DIRECTORY | ||
from devlib.trace import TraceCollector | ||
from devlib.utils.misc import ensure_file_directory_exists as _f | ||
|
||
if sys.version_info >= (3, 0): | ||
from shlex import quote | ||
else: | ||
from pipes import quote | ||
|
||
PERF_COMMAND_TEMPLATE = '{} stat {} {} sleep 1000 > {} 2>&1 ' | ||
|
||
PERF_COUNT_REGEX = re.compile(r'^(CPU\d+)?\s*(\d+)\s*(.*?)\s*(\[\s*\d+\.\d+%\s*\])?\s*$') | ||
class PerfCommandDict(collections.OrderedDict): | ||
|
||
DEFAULT_EVENTS = [ | ||
'migrations', | ||
'cs', | ||
] | ||
def __init__(self, yaml_dict): | ||
super().__init__() | ||
self._stat_command_labels = set() | ||
if isinstance(yaml_dict, self.__class__): | ||
for key, val in yaml_dict.items(): | ||
self[key] = val | ||
return | ||
yaml_dict_copy = yaml_dict.copy() | ||
for label, parameters in yaml_dict_copy.items(): | ||
self[label] = Command(kwflags_join=',', | ||
kwflags_sep='=', | ||
end_of_options='--', | ||
**parameters) | ||
if 'stat'in parameters['command']: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing space before |
||
self._stat_command_labels.add(label) | ||
|
||
|
||
class PerfCollector(TraceCollector): | ||
"""Perf is a Linux profiling tool based on performance counters. | ||
|
||
Performance counters are typically CPU hardware registers (found in the | ||
Performance Monitoring Unit) that count hardware events such as | ||
instructions executed, cache-misses suffered, or branches mispredicted. | ||
Because each ``event`` corresponds to a hardware counter, the maximum | ||
number of events that can be tracked is imposed by the available hardware. | ||
|
||
By extension, performance counters, in the context of ``perf``, also refer | ||
to so-called "software counters" representing events that can be tracked by | ||
the OS kernel (e.g. context switches). As these are software events, the | ||
counters are kept in RAM and the hardware virtually imposes no limit on the | ||
number that can be used. | ||
|
||
This collector calls ``perf`` ``commands`` to capture a run of a workload. | ||
The ``pre_commands`` and ``post_commands`` are provided to suit those | ||
``perf`` commands that don't actually capture data (``list``, ``config``, | ||
``report``, ...). | ||
|
||
``pre_commands``, ``commands`` and ``post_commands`` are instances of | ||
:class:`PerfCommandDict`. | ||
""" | ||
Perf is a Linux profiling with performance counters. | ||
|
||
Performance counters are CPU hardware registers that count hardware events | ||
such as instructions executed, cache-misses suffered, or branches | ||
mispredicted. They form a basis for profiling applications to trace dynamic | ||
control flow and identify hotspots. | ||
|
||
pref accepts options and events. If no option is given the default '-a' is | ||
used. For events, the default events are migrations and cs. They both can | ||
be specified in the config file. | ||
|
||
Events must be provided as a list that contains them and they will look like | ||
this :: | ||
|
||
perf_events = ['migrations', 'cs'] | ||
|
||
Events can be obtained by typing the following in the command line on the | ||
device :: | ||
|
||
perf list | ||
|
||
Whereas options, they can be provided as a single string as following :: | ||
|
||
perf_options = '-a -i' | ||
|
||
Options can be obtained by running the following in the command line :: | ||
|
||
man perf-stat | ||
""" | ||
|
||
def __init__(self, target, | ||
events=None, | ||
optionstring=None, | ||
labels=None, | ||
force_install=False): | ||
def __init__(self, target, force_install=False, pre_commands=None, | ||
commands=None, post_commands=None): | ||
# pylint: disable=too-many-arguments | ||
super(PerfCollector, self).__init__(target) | ||
self.events = events if events else DEFAULT_EVENTS | ||
self.force_install = force_install | ||
self.labels = labels | ||
|
||
# Validate parameters | ||
if isinstance(optionstring, list): | ||
self.optionstrings = optionstring | ||
else: | ||
self.optionstrings = [optionstring] | ||
if self.events and isinstance(self.events, basestring): | ||
self.events = [self.events] | ||
if not self.labels: | ||
self.labels = ['perf_{}'.format(i) for i in range(len(self.optionstrings))] | ||
if len(self.labels) != len(self.optionstrings): | ||
raise ValueError('The number of labels must match the number of optstrings provided for perf.') | ||
self.pre_commands = pre_commands or PerfCommandDict({}) | ||
self.commands = commands or PerfCommandDict({}) | ||
self.post_commands = post_commands or PerfCommandDict({}) | ||
|
||
self.binary = self.target.get_installed('perf') | ||
if self.force_install or not self.binary: | ||
self.binary = self._deploy_perf() | ||
if force_install or not self.binary: | ||
host_binary = os.path.join(PACKAGE_BIN_DIRECTORY, | ||
self.target.abi, 'perf') | ||
self.binary = self.target.install(host_binary) | ||
|
||
self.commands = self._build_commands() | ||
self.kill_sleep = False | ||
|
||
def reset(self): | ||
super(PerfCollector, self).reset() | ||
self.target.remove(self.working_directory()) | ||
self.target.killall('perf', as_root=self.target.is_rooted) | ||
for label in self.labels: | ||
filepath = self._get_target_outfile(label) | ||
self.target.remove(filepath) | ||
|
||
def start(self): | ||
for command in self.commands: | ||
self.target.kick_off(command) | ||
super(PerfCollector, self).start() | ||
for label, command in self.pre_commands.items(): | ||
self.execute(str(command), label) | ||
for label, command in self.commands.items(): | ||
self.kick_off(str(command), label) | ||
if 'sleep' in str(command): | ||
self.kill_sleep = True | ||
|
||
def stop(self): | ||
super(PerfCollector, self).stop() | ||
self.target.killall('perf', signal='SIGINT', | ||
as_root=self.target.is_rooted) | ||
# perf doesn't transmit the signal to its sleep call so handled here: | ||
self.target.killall('sleep', as_root=self.target.is_rooted) | ||
# NB: we hope that no other "important" sleep is on-going | ||
|
||
# pylint: disable=arguments-differ | ||
def get_trace(self, outdir): | ||
for label in self.labels: | ||
target_file = self._get_target_outfile(label) | ||
host_relpath = os.path.basename(target_file) | ||
host_file = _f(os.path.join(outdir, host_relpath)) | ||
self.target.pull(target_file, host_file) | ||
|
||
def _deploy_perf(self): | ||
host_executable = os.path.join(PACKAGE_BIN_DIRECTORY, | ||
self.target.abi, 'perf') | ||
return self.target.install(host_executable) | ||
|
||
def _build_commands(self): | ||
commands = [] | ||
for opts, label in zip(self.optionstrings, self.labels): | ||
commands.append(self._build_perf_command(opts, self.events, label)) | ||
return commands | ||
|
||
def _get_target_outfile(self, label): | ||
return self.target.get_workpath('{}.out'.format(label)) | ||
|
||
def _build_perf_command(self, options, events, label): | ||
event_string = ' '.join(['-e {}'.format(e) for e in events]) | ||
command = PERF_COMMAND_TEMPLATE.format(self.binary, | ||
options or '', | ||
event_string, | ||
self._get_target_outfile(label)) | ||
return command | ||
if self.kill_sleep: | ||
self.target.killall('sleep', as_root=self.target.is_rooted) | ||
for label, command in self.post_commands.items(): | ||
self.execute(str(command), label) | ||
|
||
def _target_runnable_command(self, command, label=None): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "Private" methods should go after the "normal" methods. |
||
cmd = '{} {}'.format(self.binary, command) | ||
if label is None: | ||
return cmd | ||
directory = quote(self.working_directory(label)) | ||
cwd = 'mkdir -p {0} && cd {0}'.format(directory) | ||
return '{cwd} && {cmd}'.format(cwd=cwd, cmd=cmd) | ||
|
||
def kick_off(self, command, label=None): | ||
cmd = self._target_runnable_command(command, label) | ||
return self.target.kick_off(cmd, as_root=self.target.is_rooted) | ||
|
||
def execute(self, command, label=None): | ||
cmd = self._target_runnable_command(command, label) | ||
return self.target.execute(cmd, as_root=self.target.is_rooted) | ||
|
||
def working_directory(self, label=None): | ||
wdir = self.target.path.join(self.target.working_directory, | ||
'instrument', 'perf') | ||
return wdir if label is None else self.target.path.join(wdir, label) | ||
|
||
def get_traces(self, host_outdir): | ||
self.target.pull(self.working_directory(), host_outdir, | ||
as_root=self.target.is_rooted) | ||
|
||
def get_trace(self, outfile): | ||
raise NotImplementedError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# Copyright 2019 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 collections | ||
import itertools | ||
import shlex | ||
|
||
|
||
class Command(dict): # inherit from dict for JSON serializability | ||
"""Provides an abstraction for manipulating CLI commands | ||
|
||
The expected format of the abstracted command is as follows:: | ||
|
||
<command> <flags> <kwflags> <options> <end_of_options> <args> | ||
|
||
where | ||
|
||
- `<command>` is the command name or path (used as-is); | ||
- `<flags>` are space-separated flags with a leading `-` (single | ||
character flag) or `--` (multiple characters); | ||
- `<kwflags>` are space-separated key-value flag pairs with a leading | ||
`-` (single character flag) or `--` (multiple characters), a | ||
key-value separator (typically `=`) and, if required, a CLI-compliant | ||
escaped value; | ||
- `<options>` are space-separated options (used as-is); | ||
- `<end_of_options>` is a character sequence understood by `<command>` | ||
as meaning the end of the options (typically `--`); | ||
- `<args>` are the arguments to the command and could potentially | ||
themselves be a valid command (_e.g._ POSIX `time`); | ||
|
||
If allowed by the CLI, redirecting the output streams of the command | ||
(potentially between themselves) may be done through this abstraciton. | ||
""" | ||
# pylint: disable=too-many-instance-attributes | ||
# pylint: disable=too-few-public-methods | ||
def __init__(self, command, flags=None, kwflags=None, kwflags_sep=' ', | ||
kwflags_join=',', options=None, end_of_options=None, | ||
args=None, stdout=None, stderr=None): | ||
""" | ||
Parameters: | ||
command command name or path | ||
flags ``str`` or list of ``str`` without the leading `-`/`--`. | ||
Flags that evaluate as falsy are ignored; | ||
kwflags mapping giving the key-value pairs. The key and value of | ||
the pair are separated by a `kwflags_sep`. If the value | ||
is a list, it is joined with `kwflags_join`. If a value | ||
evaluates as falsy, it is replaced by the empty string; | ||
kwflags_sep Key-value separator for `kwflags`; | ||
kwflags_join Separator for lists of values in `kwflags`; | ||
options same as `flags` but nothing is prepended to the options; | ||
args ``str`` or mapping holding keys which are valid | ||
arguments to this constructor, for recursive | ||
instantiation; | ||
stdout file for redirection of ``stdout``. This is passed to | ||
the CLI so non-file expressions may be used (*e.g.* | ||
`&2`); | ||
stderr file for redirection of ``stderr``. This is passed to | ||
the CLI so non-file expressions may be used (*e.g.* | ||
`&1`); | ||
""" | ||
# pylint: disable=too-many-arguments | ||
# pylint: disable=super-init-not-called | ||
self.command = ' '.join(shlex.split(command)) | ||
self.flags = map(str, filter(None, self._these(flags))) | ||
self.kwflags_sep = kwflags_sep | ||
self.kwflags_join = kwflags_join | ||
self.kwflags = {} | ||
if kwflags is not None: | ||
for k in kwflags: | ||
v = ['' if x is None else str(x) | ||
for x in self._these(kwflags[k])] | ||
self.kwflags[k] = v[0] if len(v) == 1 else v | ||
self.options = [] if options is None else [ | ||
'' if x is None else str(x) for x in self._these(options)] | ||
if end_of_options: | ||
self.options.append(str(end_of_options).strip()) | ||
if isinstance(args, collections.Mapping): | ||
self.args = Command(**args) | ||
else: | ||
self.args = None if args is None else str(args) | ||
self.stdout = stdout | ||
self.stderr = stderr | ||
|
||
def __str__(self): | ||
quoted = itertools.chain( | ||
shlex.split(self.command), | ||
map(self._flagged, self.flags), | ||
('{}{}{}'.format(self._flagged(k), | ||
self.kwflags_sep, | ||
self.kwflags_join.join(self._these(v))) | ||
for k, v in self.kwflags.items()), | ||
self.options | ||
) | ||
words = [shlex.quote(word) for word in quoted] | ||
if self.args: | ||
words.append(str(self.args)) | ||
if self.stdout: | ||
words.append('1>{}'.format(self._filepipe(self.stdout))) | ||
if self.stderr: | ||
words.append('2>{}'.format(self._filepipe(self.stderr))) | ||
return ' '.join(words) | ||
|
||
def __getitem__(self, key): | ||
return self.__dict__[key] | ||
|
||
@staticmethod | ||
def _these(x): | ||
if isinstance(x, str) or not isinstance(x, collections.abc.Iterable): | ||
return [x] | ||
return x | ||
|
||
@staticmethod | ||
def _filepipe(f): | ||
if isinstance(f, str) and f.startswith('&'): | ||
return f | ||
return shlex.quote(f) | ||
|
||
@classmethod | ||
def _flagged(cls, flag): | ||
flag = str(flag).strip() | ||
return '{}{}'.format('--' if len(flag) > 1 else '-', flag) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implies that compatibility with Python2.7 is attempted to be retained however is not present elsewhere in this PR. Is this aimed to be merged before the next release aka supporting Python 2.7 or to be held until after support has been dropped?