From 1edd0e1be33e0d8332d4e10bbd06e1158f54d98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= Date: Fri, 3 May 2019 21:15:50 +0100 Subject: [PATCH 1/6] utils/cli: Introduce class to build CLI commands Introduce a class allowing to programmatically generate CLI commands in a clear, reliable and robust way for any calling code manipulating raw command strings. --- devlib/utils/cli.py | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 devlib/utils/cli.py diff --git a/devlib/utils/cli.py b/devlib/utils/cli.py new file mode 100644 index 000000000..afdea5915 --- /dev/null +++ b/devlib/utils/cli.py @@ -0,0 +1,80 @@ +# 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: + """Provides an abstraction for manipulating CLI commands + """ + # 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): + """ + NB: if ``None`` in ``flags``, ``kwflags``, ``options``, replace with ``''`` + empty flags are ignored + empty kwflag values are kept but striped + NB: caller responsible for escaping args as a single string + """ #TODO + # pylint: disable=too-many-arguments + these = lambda x: (x if isinstance(x, collections.abc.Iterable) + and not isinstance(x, str) else [x]) + + self.command = shlex.split(command) + self.flags = map(str, filter(None, these(flags))) + self.kwflags_sep = kwflags_sep + self.kwflags_join = kwflags_join + self.kwflags = {} if kwflags is None else { + key: ['' if x is None else str(x) for x in these(values)] + for key, values in kwflags.items() + } + self.options = [] if options is None else [ + '' if x is None else str(x) for x in 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): + filepipe = lambda f: (f if isinstance(f, str) and f.startswith('&') + else shlex.quote(f)) + quoted = itertools.chain( + self.command, + map(self._flagged, self.flags), + ('{}{}{}'.format(self._flagged(k), + self.kwflags_sep, + self.kwflags_join.join(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(filepipe(self.stdout))) + if self.stderr: + words.append('2>{}'.format(filepipe(self.stderr))) + return ' '.join(words) + + @classmethod + def _flagged(cls, flag): + return '{}{}'.format('--' if len(flag) > 1 else '-', str(flag).strip()) From 30ac951a6a012e2f52eafbe3e50ec77df1336244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= Date: Wed, 1 May 2019 11:47:59 +0100 Subject: [PATCH 2/6] trace/perf: Introduce a new, generic collector Introduce a perf collector that is more generic than the previous one and which is expected to be able to handle all potential calls to perf (irrespective of the subcommand, flags, options or arguments being used). --- devlib/trace/perf.py | 201 +++++++++++++++++++++---------------------- 1 file changed, 100 insertions(+), 101 deletions(-) diff --git a/devlib/trace/perf.py b/devlib/trace/perf.py index 17c2009ef..60abf53b6 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,130 @@ # 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 - - -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*$') -DEFAULT_EVENTS = [ - 'migrations', - 'cs', -] - - -class PerfCollector(TraceCollector): - """ - Perf is a Linux profiling with performance counters. +if sys.version_info >= (3, 0): + from shlex import quote +else: + from pipes import quote - 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. +class PerfCommandDict(collections.OrderedDict): - Events must be provided as a list that contains them and they will look like - this :: + 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) - perf_events = ['migrations', 'cs'] + def stat_commands(self): + return {label: self[label] for label in self._stat_command_labels} - Events can be obtained by typing the following in the command line on the - device :: + def as_strings(self): + return {label: str(cmd) for label, cmd in self.items()} - 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 +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`. """ - - 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 kick_off(self, command, label=None): + directory = quote(self.working_directory(label or 'default')) + return self.target.kick_off('mkdir -p {0} && cd {0} && {1} {2}' + .format(directory, self.binary, command), + as_root=self.target.is_rooted) + + def execute(self, command, label=None): + directory = quote(self.working_directory(label or 'default')) + return self.target.execute('mkdir -p {0} && cd {0} && {1} {2}' + .format(directory, self.binary, command), + 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 From e65256175458ea3a19c402d34f5c3bd700174e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= Date: Tue, 11 Jun 2019 15:13:59 +0100 Subject: [PATCH 3/6] fixup! trace/perf: Introduce a new, generic collector --- devlib/trace/perf.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/devlib/trace/perf.py b/devlib/trace/perf.py index 60abf53b6..abff51c4b 100644 --- a/devlib/trace/perf.py +++ b/devlib/trace/perf.py @@ -46,12 +46,6 @@ def __init__(self, yaml_dict): if 'stat'in parameters['command']: self._stat_command_labels.add(label) - def stat_commands(self): - return {label: self[label] for label in self._stat_command_labels} - - def as_strings(self): - return {label: str(cmd) for label, cmd in self.items()} - class PerfCollector(TraceCollector): """Perf is a Linux profiling tool based on performance counters. From fa67be2249bc73cfd7dfd146f1db13bd7182442d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= Date: Tue, 11 Jun 2019 15:51:37 +0100 Subject: [PATCH 4/6] fixup! trace/perf: Introduce a new, generic collector --- devlib/utils/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devlib/utils/cli.py b/devlib/utils/cli.py index afdea5915..104143909 100644 --- a/devlib/utils/cli.py +++ b/devlib/utils/cli.py @@ -17,7 +17,7 @@ import itertools import shlex -class Command: +class Command(dict): """Provides an abstraction for manipulating CLI commands """ # pylint: disable=too-many-instance-attributes @@ -75,6 +75,9 @@ def __str__(self): words.append('2>{}'.format(filepipe(self.stderr))) return ' '.join(words) + def __getitem__(self, key): + return self.__dict__[key] + @classmethod def _flagged(cls, flag): return '{}{}'.format('--' if len(flag) > 1 else '-', str(flag).strip()) From b4d852b4bfea83d34cf44a2af034e60098818f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= Date: Tue, 25 Jun 2019 20:42:58 +0100 Subject: [PATCH 5/6] fixup! utils/cli: Introduce class to build CLI commands --- devlib/utils/cli.py | 100 +++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/devlib/utils/cli.py b/devlib/utils/cli.py index 104143909..2bbb6defe 100644 --- a/devlib/utils/cli.py +++ b/devlib/utils/cli.py @@ -17,34 +17,73 @@ import itertools import shlex -class Command(dict): + +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): + 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`); """ - NB: if ``None`` in ``flags``, ``kwflags``, ``options``, replace with ``''`` - empty flags are ignored - empty kwflag values are kept but striped - NB: caller responsible for escaping args as a single string - """ #TODO # pylint: disable=too-many-arguments - these = lambda x: (x if isinstance(x, collections.abc.Iterable) - and not isinstance(x, str) else [x]) - - self.command = shlex.split(command) - self.flags = map(str, filter(None, these(flags))) + # 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 None else { - key: ['' if x is None else str(x) for x in these(values)] - for key, values in kwflags.items() - } + 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 these(options)] + '' 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): @@ -55,14 +94,12 @@ def __init__(self, command, flags=None, kwflags=None, kwflags_sep=' ', self.stderr = stderr def __str__(self): - filepipe = lambda f: (f if isinstance(f, str) and f.startswith('&') - else shlex.quote(f)) quoted = itertools.chain( - self.command, + shlex.split(self.command), map(self._flagged, self.flags), ('{}{}{}'.format(self._flagged(k), self.kwflags_sep, - self.kwflags_join.join(v)) + self.kwflags_join.join(self._these(v))) for k, v in self.kwflags.items()), self.options ) @@ -70,14 +107,27 @@ def __str__(self): if self.args: words.append(str(self.args)) if self.stdout: - words.append('1>{}'.format(filepipe(self.stdout))) + words.append('1>{}'.format(self._filepipe(self.stdout))) if self.stderr: - words.append('2>{}'.format(filepipe(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): - return '{}{}'.format('--' if len(flag) > 1 else '-', str(flag).strip()) + flag = str(flag).strip() + return '{}{}'.format('--' if len(flag) > 1 else '-', flag) From 1520575948e82dde74c4e68bca8dabbac3478ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-Cl=C3=A9ment=20Tosi?= Date: Tue, 25 Jun 2019 20:43:31 +0100 Subject: [PATCH 6/6] fixup! trace/perf: Introduce a new, generic collector --- devlib/trace/perf.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/devlib/trace/perf.py b/devlib/trace/perf.py index abff51c4b..3d2394d51 100644 --- a/devlib/trace/perf.py +++ b/devlib/trace/perf.py @@ -109,17 +109,21 @@ def stop(self): 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): - directory = quote(self.working_directory(label or 'default')) - return self.target.kick_off('mkdir -p {0} && cd {0} && {1} {2}' - .format(directory, self.binary, command), - as_root=self.target.is_rooted) + 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): - directory = quote(self.working_directory(label or 'default')) - return self.target.execute('mkdir -p {0} && cd {0} && {1} {2}' - .format(directory, self.binary, command), - as_root=self.target.is_rooted) + 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,