diff --git a/lisa/_kmod.py b/lisa/_kmod.py index 36c45fe5db2125fa4a407da04624704c38e53e08..1351a780f798a81537ef9cdc6551d50fce5720c5 100644 --- a/lisa/_kmod.py +++ b/lisa/_kmod.py @@ -132,6 +132,7 @@ from shlex import quote from io import BytesIO from collections.abc import Mapping import typing +import fnmatch from elftools.elf.elffile import ELFFile @@ -2263,8 +2264,12 @@ class LISAFtraceDynamicKmod(FtraceDynamicKmod): **kwargs, ) - @classmethod - def _event_features(cls, events): - return set(f'event__{event}' for event in events) + def _event_features(self, events): + all_events = self.defined_events + return set( + f'event__{event}' + for pattern in events + for event in fnmatch.filter(all_events, pattern) + ) # vim :set tabstop=4 shiftwidth=4 expandtab textwidth=80 diff --git a/lisa/target.py b/lisa/target.py index d4f3c30c3fc7a14355d5047fd6ecd8e7061ed4f5..43cf6d90aaa487b05f2273cdcd3f0be181ad29e3 100644 --- a/lisa/target.py +++ b/lisa/target.py @@ -42,7 +42,7 @@ from devlib.exception import TargetStableError from devlib.utils.misc import which from devlib.platform.gem5 import Gem5SimulationPlatform -from lisa.utils import Loggable, HideExekallID, resolve_dotted_name, get_subclasses, import_all_submodules, LISA_HOME, RESULT_DIR, LATEST_LINK, setup_logging, ArtifactPath, nullcontext, ExekallTaggable, memoized, destroyablecontextmanager, ContextManagerExit +from lisa.utils import Loggable, HideExekallID, resolve_dotted_name, get_subclasses, import_all_submodules, LISA_HOME, RESULT_DIR, LATEST_LINK, setup_logging, ArtifactPath, nullcontext, ExekallTaggable, memoized, destroyablecontextmanager, ContextManagerExit, update_params_from from lisa._assets import ASSETS_PATH from lisa.conf import SimpleMultiSrcConf, KeyDesc, LevelKeyDesc, TopLevelKeyDesc, Configurable, DelegatedLevelKeyDesc from lisa._kmod import _KernelBuildEnv, DynamicKmod, _KernelBuildEnvConf @@ -270,35 +270,13 @@ class Target(Loggable, HideExekallID, ExekallTaggable, Configurable): wait_boot=True, wait_boot_timeout=10, kernel_src=None, kmod_build_env=None, kmod_make_vars=None, kmod_overlay_backend=None, devlib_max_async=None, ): - # Set it temporarily to avoid breaking __getattr__ - self._devlib_loadable_modules = set() - - # pylint: disable=dangerous-default-value - super().__init__() - logger = self.logger - self.name = name - - res_dir = res_dir if res_dir else self._get_res_dir( - root=os.path.join(LISA_HOME, RESULT_DIR), - relative='', - name=f'{self.__class__.__qualname__}-{self.name}', - append_time=True, - symlink=True - ) - - self._res_dir = res_dir - os.makedirs(self._res_dir, exist_ok=True) - if os.listdir(self._res_dir): - raise ValueError(f'res_dir must be empty: {self._res_dir}') - # Determine file transfer method. Currently avaliable options # are 'sftp' and 'scp', defaults to sftp. if devlib_file_xfer and devlib_file_xfer not in ('scp', 'sftp'): raise ValueError(f'Invalid file transfer method: {devlib_file_xfer}') use_scp = devlib_file_xfer == 'scp' - self._installed_tools = set() - self.target = self._init_target( + target = self._init_target( kind=kind, name=name, workdir=workdir, @@ -315,6 +293,50 @@ class Target(Loggable, HideExekallID, ExekallTaggable, Configurable): wait_boot_timeout=wait_boot_timeout, max_async=devlib_max_async, ) + self._init_post_devlib( + name=name, res_dir=res_dir, + target=target, tools=tools, plat_info=plat_info, + lazy_platinfo=lazy_platinfo, + devlib_excluded_modules=devlib_excluded_modules, + kernel_src=kernel_src, kmod_build_env=kmod_build_env, + kmod_make_vars=kmod_make_vars, + kmod_overlay_backend=kmod_overlay_backend, + ) + + @classmethod + def _from_devlib_target(cls, target, **kwargs): + self = cls.__new__(cls) + self._init_post_devlib(target=target, **kwargs) + return self + + @update_params_from(__init__) + def _init_post_devlib(self, *, name, res_dir, target, + tools, plat_info, lazy_platinfo, devlib_excluded_modules, kernel_src, + kmod_build_env, kmod_make_vars, kmod_overlay_backend, + ): + # Set it temporarily to avoid breaking __getattr__ + self._devlib_loadable_modules = set() + + # pylint: disable=dangerous-default-value + super().__init__() + logger = self.logger + self.name = name + + res_dir = res_dir if res_dir else self._get_res_dir( + root=os.path.join(LISA_HOME, RESULT_DIR), + relative='', + name=f'{self.__class__.__qualname__}-{self.name}', + append_time=True, + symlink=True + ) + + self._res_dir = res_dir + os.makedirs(self._res_dir, exist_ok=True) + if os.listdir(self._res_dir): + raise ValueError(f'res_dir must be empty: {self._res_dir}') + + self._installed_tools = set() + self.target = target devlib_excluded_modules = set(devlib_excluded_modules) # Sorry, can't let you do that. Messing with cgroups in a systemd @@ -336,7 +358,7 @@ class Target(Loggable, HideExekallID, ExekallTaggable, Configurable): self._init_plat_info(plat_info, name, deferred=lazy_platinfo, fallback=True) logger.info(f'Effective platform information:\n{self.plat_info}') - cache_dir = Path(res_dir).resolve() / '.lisa' / 'cache' + cache_dir = Path(self._res_dir).resolve() / '.lisa' / 'cache' cache_dir.mkdir(parents=True) self._cache_dir = cache_dir @@ -766,14 +788,15 @@ class Target(Loggable, HideExekallID, ExekallTaggable, Configurable): return custom_args, cls.from_conf(conf=target_conf, plat_info=platform_info, res_dir=args.res_dir) - def _init_target(self, kind, name, workdir, device, host, + @classmethod + def _init_target(cls, kind, name, workdir, device, host, port, username, password, keyfile, strict_host_check, use_scp, devlib_platform, wait_boot, wait_boot_timeout, max_async, ): """ Initialize the Target """ - logger = self.logger + logger = cls.get_logger() conn_settings = {} resolved_username = username or 'root' @@ -789,7 +812,7 @@ class Target(Loggable, HideExekallID, ExekallTaggable, Configurable): if device: pass elif host: - port = port or self.ADB_PORT_DEFAULT + port = port or cls.ADB_PORT_DEFAULT device = f'{host}:{port}' else: device = 'DEFAULT' @@ -803,7 +826,7 @@ class Target(Loggable, HideExekallID, ExekallTaggable, Configurable): devlib_target_cls = devlib.LinuxTarget conn_settings.update( username=resolved_username, - port=port or self.SSH_PORT_DEFAULT, + port=port or cls.SSH_PORT_DEFAULT, host=host, strict_host_check=True if strict_host_check is None else strict_host_check, use_scp=False if use_scp is None else use_scp, diff --git a/lisa/utils.py b/lisa/utils.py index 585b5eba65b1579af017a6db1755fcc132e246b0..ada664ea32b112fc99a4060d46d1bcd6cc4aa146 100644 --- a/lisa/utils.py +++ b/lisa/utils.py @@ -1898,6 +1898,60 @@ def optional_kwargs(func): return wrapper +def update_params_from(f, ignore=None): + """ + Decorator to update the signature of the decorated function using + annotation and default values from the specified ``f`` function. + + + If the parameter already has a default value, it will be used instead of + copied-over. Same goes for annotations. + """ + ignore = set(ignore or []) + + def fixup_param(existing, new): + default = new.default if existing.default == existing.empty else existing.default + annotation = new.annotation if existing.annotation == existing.empty else existing.annotation + return existing.replace( + default=default, + annotation=annotation + ) + + def fixup_sig(decorated, f): + f_sig = inspect.signature(f) + sig = inspect.signature(decorated) + parameters = [ + ( + fixup_param( + existing=spec, + new=f_sig.parameters.get(name, spec), + ) + if name not in ignore else + spec + ) + for name, spec in sig.parameters.items() + ] + return sig.replace(parameters=parameters) + + + def decorator(decorated): + sig = fixup_sig(decorated, f) + + @functools.wraps(decorated) + def wrapper(*args, **kwargs): + # use bind_partial() to leave the missing arguments errors to the + # decorated function itself. + bound = sig.bind_partial(*args, **kwargs) + bound.apply_defaults() + return decorated(*bound.args, **bound.kwargs) + + + wrapper.__signature__ = sig + return wrapper + + return decorator + + def kwargs_forwarded_to(f, ignore=None): """ Similar to :func:`functools.wraps`, except that it will only fixup the diff --git a/lisa/wa.py b/lisa/wa/__init__.py similarity index 100% rename from lisa/wa.py rename to lisa/wa/__init__.py diff --git a/lisa/wa/plugins/_kmod.py b/lisa/wa/plugins/_kmod.py new file mode 100644 index 0000000000000000000000000000000000000000..e55abb955947af23aea4d41289ea704258004ba9 --- /dev/null +++ b/lisa/wa/plugins/_kmod.py @@ -0,0 +1,268 @@ +# Copyright 2022 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 functools +from weakref import WeakKeyDictionary +from contextlib import contextmanager, nullcontext +import threading + +from wa import Instrument, Parameter +from wa.framework.instrument import very_slow, very_fast +from wa.utils.types import list_of_strings + +from lisa.target import Target as LISATarget +from lisa._kmod import LISAFtraceDynamicKmod +from lisa.utils import get_nested_key + + +class _Default: + pass + + +_DEFAULT = _Default() + + +class _AttrStore: + def __init__(self): + self._lock = threading.Lock() + self._store = WeakKeyDictionary() + + def get(self, obj, attr, setdefault=_DEFAULT): + with self._lock: + attrs = self._store.setdefault(obj, {}) + try: + return attrs[attr] + except KeyError: + if setdefault is _DEFAULT: + raise + else: + attrs[attr] = setdefault + return setdefault + + def set(self, obj, attr, val): + with self._lock: + self._store.setdefault(obj, {})[attr] = val + + +class _AttrProxy: + """ + Proxy to another object that redirects ``__setattr__`` to a different + backing storage so that the original object is not modified. + """ + _STORE = _AttrStore() + + def __init__(self, obj, subkey=None): + super().__setattr__('_obj', obj) + super().__setattr__('_subkey', subkey) + + def __getattr__(self, attr): + try: + return self._STORE.get(self._obj, self._subkey)[attr] + except KeyError: + return getattr(self._obj, attr) + + def __setattr__(self, attr, val): + attrs = self._STORE.get(self._obj, self._subkey, setdefault={}) + attrs[attr] = val + + +class LisaKmodInstrument(Instrument): + name = 'lisa-kmod' + description = """ + Compile and load LISA kernel module when the trace-cmd instrument is used. + + The events to enable are taken from the trace-cmd configuration. Disabling + the trace-cmd instrument will also disable that instrument. + + Example config: + + .. code:: yaml + + lisa-kmod: + kernel_src: /path/to/linux/kernel/tree/sources + build_env: + # Using "build-env: alpine" will use an Alpine Linux chroot, + # removing the need to have your own toolchain installed. + build-env: host + build-env-settings: + host: + # Extra entry to PATH when running the toolchain in the "host" build-env + toolchain-path: /foobar + """ + + parameters = [ + Parameter('kernel_src', kind=str, default=None, + description=""" + Path to kernel sources + """), + + Parameter('build_env', kind=dict, default={'build-env': 'host'}, + description=""" + Configuration of the build environment + """), + + Parameter('ftrace_events', kind=list_of_strings, default=[], + description=""" + List of ftrace events that should be enabled in the kernel + module. Events specified in the trace-cmd instrument config + will also be used. + """), + ] + + def __init__(self, target, kernel_src, build_env, ftrace_events, **kwargs): + super().__init__(target, **kwargs) + self._lisa_target = LISATarget._from_devlib_target( + target=target, + lazy_platinfo=True, + kernel_src=kernel_src, + kmod_build_env=build_env, + ) + self._ftrace_events = set(ftrace_events) + self._kmod = None + self._cm = None + self._features = set() + + # Add a new attribute to the devlib target so we can find ourselves + # from the monkey-patched methods. + _AttrProxy(self.target).kmod_instrument = self + + @classmethod + def _monkey_patch(cls, instrument): + patch = dict( + initialize=cls._initialize_cm, + setup=cls._setup_cm, + start=cls._start_cm, + stop=cls._stop_cm, + ) + + def make_wrapper(orig, f): + @very_slow + @functools.wraps(orig) + def wrapper(self, context, *args, **kwargs): + target = _AttrProxy(self.target) + def bind(): + try: + instr = target.kmod_instrument + except AttributeError: + return nullcontext + else: + return f.__get__(instr, type(instr)) + + # Only enable the instrument for jobs that use our + # augmentation. + job = context.current_job + if job is None: + _f = bind() + else: + # Track the state per job and per patched function name. + # This way, we will only run the code once per job at each + # stage of the setup, even if multiple monkey patched + # instruments are used simultaneously + job = _AttrProxy(job, subkey=orig.__name__) + + # Only run the kmod code once per job and per patched + # function. If multiple instruments are monkey patched, we + # will be triggered more than once per job. + has_run = getattr(job, 'kmod_has_run', False) + job.kmod_has_run = True + + if has_run: + _f = nullcontext + elif cls.name in job.spec.augmentations: + _f = bind() + else: + _f = nullcontext + + with _f(context, *args, **kwargs): + return orig.__get__(self, type(self))(context, *args, **kwargs) + + return wrapper + + for attr, f in patch.items(): + setattr( + instrument, + attr, + make_wrapper( + getattr( + instrument, + attr, + # No-op method, since getattr() already crawls the MRO + # there is truly nothing to do if the method does not + # exist. + lambda *args, **kwargs: None + ), + f + ) + ) + + def _all_ftrace_events(self, context): + try: + trace_cmd_events = get_nested_key( + context.cm.run_config.augmentations, + ['trace-cmd', 'events'], + ) + except KeyError: + trace_cmd_events = [] + + return set(trace_cmd_events) | set(self._ftrace_events) + + def _run(self): + features = sorted(self._features) + self.logger.info(f'Enabling LISA kmod features {", ".join(features)}') + return self._kmod.run( + kmod_params={ + 'features': features, + } + ) + + @contextmanager + def _initialize_cm(self, context): + # Note that this function will be ran for each monkey patched + # instrument, unlike the other methods ran in job context. + events = self._all_ftrace_events(context) + kmod = self._lisa_target.get_kmod(LISAFtraceDynamicKmod) + self._features = set(kmod._event_features(events)) + self._kmod = kmod + + # Load the module while running the instrument's initialize so that the + # events are visible in the kernel at that point. + with self._run(): + yield + + @contextmanager + def _setup_cm(self, context): + self._cm = self._run() + yield + + @contextmanager + def _start_cm(self, context): + self.logger.info(f'Loading LISA kmod') + self._cm.__enter__() + yield + + @contextmanager + def _stop_cm(self, context): + self.logger.info(f'Unloading LISA kmod') + self._cm.__exit__(None, None, None) + yield + + +# Monkey-patch the trace-cmd instrument in order to reliably load the +# module before the detection of kernel events. Otherwise, the random +# ordering would lead to ftrace events being detected by the +# FtraceCollector before loading the kernel module, and the events +# would therefore be disabled by devlib with a warning. +from wa.instruments.trace_cmd import TraceCmdInstrument +LisaKmodInstrument._monkey_patch(TraceCmdInstrument) diff --git a/shell/lisa_shell b/shell/lisa_shell index a2e89feb21884bcb6238791e074f142386831897..a7df3f2ceef295010b12f062c327fbe9752f21a8 100755 --- a/shell/lisa_shell +++ b/shell/lisa_shell @@ -66,6 +66,9 @@ export EXEKALL_ARTIFACT_ROOT=${EXEKALL_ARTIFACT_ROOT:-$LISA_RESULT_ROOT} # Add our man pages export MANPATH="$MANPATH:$LISA_HOME/doc/" +# Add workload-automation LISA plugin folder +export WA_PLUGIN_PATHS=$WA_PLUGIN_PATHS:$LISA_HOME/lisa/wa/plugins + # make sure it is unset if the folder does not exist, as some other tools will # not handle that well [[ ! -d "$ANDROID_HOME" ]] && unset ANDROID_HOME