diff --git a/devlib/trace/perf.py b/devlib/trace/perf.py index 17c2009ef..3d2394d51 100644 --- a/devlib/trace/perf.py +++ b/devlib/trace/perf.py @@ -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']: + 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): + 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 diff --git a/devlib/utils/cli.py b/devlib/utils/cli.py new file mode 100644 index 000000000..2bbb6defe --- /dev/null +++ b/devlib/utils/cli.py @@ -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:: + + + + where + + - `` is the command name or path (used as-is); + - `` are space-separated flags with a leading `-` (single + character flag) or `--` (multiple characters); + - `` 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; + - `` are space-separated options (used as-is); + - `` is a character sequence understood by `` + as meaning the end of the options (typically `--`); + - `` 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)