diff --git a/lisa/analysis/load_tracking.py b/lisa/analysis/load_tracking.py index b72b7c8e376bfccf90fecddab37371455f3c0dd8..8bea43df2f790c317e92fcccab26e3dc2ee14b5e 100644 --- a/lisa/analysis/load_tracking.py +++ b/lisa/analysis/load_tracking.py @@ -20,6 +20,7 @@ import pandas as pd from lisa.analysis.base import TraceAnalysisBase +from lisa.trace import requires_one_event_of class LoadTrackingAnalysis(TraceAnalysisBase): @@ -87,6 +88,7 @@ class LoadTrackingAnalysis(TraceAnalysisBase): raise RuntimeError("Trace is missing one of either events: {}".format(events)) + @requires_one_event_of('sched_load_cfs_rq', 'sched_load_avg_cpu') def df_cpus_signals(self): """ Get the load-tracking signals for the CPUs @@ -95,15 +97,10 @@ class LoadTrackingAnalysis(TraceAnalysisBase): * A ``util`` column (the average utilization of a CPU at time t) * A ``load`` column (the average load of a CPU at time t) - - :Required events: - Either of: - - * ``sched_load_cfs_rq`` - * ``sched_load_avg_cpu`` """ return self._df_either_event(['sched_load_cfs_rq', 'sched_load_avg_cpu']) + @requires_one_event_of('sched_load_se', 'sched_load_avg_task') def df_tasks_signals(self): """ Get the load-tracking signals for the tasks @@ -117,12 +114,6 @@ class LoadTrackingAnalysis(TraceAnalysisBase): * A ``required_capacity`` column (the minimum available CPU capacity required to run this task without being CPU-bound) - - :Required events: - Either of: - - * ``sched_load_se`` - * ``sched_load_avg_task`` """ df = self._df_either_event(['sched_load_se', 'sched_load_avg_task']) @@ -142,6 +133,7 @@ class LoadTrackingAnalysis(TraceAnalysisBase): return df + @df_tasks_signals.used_events def df_top_big_tasks(self, util_threshold, min_samples=100): """ Tasks which had 'utilization' samples bigger than the specified @@ -171,6 +163,7 @@ class LoadTrackingAnalysis(TraceAnalysisBase): return top_df + @df_cpus_signals.used_events def plot_cpus_signals(self, cpus=None, filepath=None): """ Plot the CPU-related load-tracking signals @@ -216,6 +209,7 @@ class LoadTrackingAnalysis(TraceAnalysisBase): self.save_plot(fig, filepath) return axes + @df_tasks_signals.used_events def plot_task_signals(self, task, filepath=None): """ Plot the task-related load-tracking signals @@ -245,6 +239,7 @@ class LoadTrackingAnalysis(TraceAnalysisBase): self.save_plot(fig, filepath) return axis + @df_tasks_signals.used_events def plot_task_required_capacity(self, task, filepath=None, axis=None): """ Plot the minimum required capacity of a task @@ -286,6 +281,7 @@ class LoadTrackingAnalysis(TraceAnalysisBase): return axis + @df_tasks_signals.used_events def plot_task_placement(self, task, filepath=None): """ Plot the CPU placement of the task diff --git a/lisa/exekall_customize.py b/lisa/exekall_customize.py index 5cfdd6e9a97eb88bbadbbfb3317356eabd3e979c..5f6ed5810e505f9db6176d063d97ff9b0a2327ec 100644 --- a/lisa/exekall_customize.py +++ b/lisa/exekall_customize.py @@ -157,48 +157,40 @@ class LISAAdaptor(AdaptorBase): return hidden_op_set def format_expr_list(self, expr_list, verbose=0): - if not self.args.list_trace_events: - return '' + def get_callable_events(callable_): + """ + Recursively unwraps all layers of wrappers, collecting the events + at each stage. That is needed in order to cope with things like + :class:`exekall.engine.UnboundMethod`. + """ + try: + used_events = callable_.used_events + except AttributeError: + events = set() + else: + events = set(used_events.get_all_events()) - def get_trace_events(expr): - events = set() - if issubclass(expr.op.value_type, TestBundle): - try: - ftrace_conf = ExekallFtraceCollector._get_consumer_conf( - expr.op.unwrapped_callable - ) - except Exception: - pass - else: - events = set(ftrace_conf.get('events', [])) + with contextlib.suppress(AttributeError): + events.update(get_callable_events(callable_.__wrapped__)) + + return events + def get_trace_events(expr): + events = get_callable_events(expr.op.callable_) for param_expr in expr.param_map.values(): events.update(get_trace_events(param_expr)) return events - def format_events(events): - if not events: - return '\n\t' - else: - joiner = '\n\t- ' - return joiner + joiner.join(sorted(events)) - - def format_expr(expr): - hidden_callable_set = { - op.callable_ for op in self.hidden_op_set - } - expr_id = expr.get_id( - qual=False, - full_qual=verbose, - hidden_callable_set=hidden_callable_set - ) - events = sorted(get_trace_events(expr)) - return '{}:{}'.format(expr_id, format_events(events)) - - return 'Used trace events:\n' + '\n\n'.join( - format_expr(expr) - for expr in expr_list - ) + events = set() + for expr in expr_list: + events.update(get_trace_events(expr)) + + if events: + joiner = '\n - ' + events_str = joiner + joiner.join(sorted(events)) + else: + events_str = ' ' + return 'Used trace events:{}'.format(events_str) @staticmethod def register_run_param(parser): @@ -211,9 +203,6 @@ class LISAAdaptor(AdaptorBase): default=[], help="Serialized object to inject when building expressions") - parser.add_argument('--list-trace-events', action='store_true', - help="Show the list of trace events collected for each testcase") - # Create an empty TargetConf, so we are able to get the list of tests # as if we were going to execute them using a target. # note: that is only used for generating the documentation. diff --git a/lisa/tests/base.py b/lisa/tests/base.py index 9f14e424a17f793078d8a2df1e136c1e40b07d0b..4df170c5c598e964757d9259b9af5ffd46eb70f3 100644 --- a/lisa/tests/base.py +++ b/lisa/tests/base.py @@ -26,6 +26,8 @@ import re from collections.abc import Mapping from inspect import signature +import inspect +import copy from devlib.trace.dmesg import DmesgCollector @@ -347,9 +349,75 @@ class TestBundle(Serializable, abc.ABC): """ super().to_path(self._filepath(res_dir)) -class RTATestBundle(TestBundle): + +class RTATestBundleMeta(abc.ABCMeta): + """ + Metaclass of :class:`RTATestBundle`. + + This metaclass ensures that each class will get its own copy of + ``ftrace_conf`` attribute, and that the events specified in that + configuration are a superset of what is needed by methods using the + decorator :func:`lisa.trace.requires_events`. This makes sure that the + default set of events is always enough to run all defined methods, without + duplicating that information. + + .. note:: An existing ``ftrace_conf`` attribute is used, with extra + detected events merged-in. + """ + + def __new__(metacls, name, bases, dct, **kwargs): + new_cls = super().__new__(metacls, name, bases, dct, **kwargs) + + # Collect all the events that can be used by all methods available on + # that class. + ftrace_events = set() + for name, obj in inspect.getmembers(new_cls, callable): + try: + used_events = obj.used_events + except AttributeError: + continue + else: + ftrace_events.update(used_events.get_all_events()) + + # Get the ftrace_conf attribute of the class, and make sure it is + # unique to that class (i.e. not shared with any other parent or + # sibling classes) + try: + ftrace_conf = new_cls.ftrace_conf + except AttributeError: + ftrace_conf = FtraceConf(src=new_cls.__qualname__) + else: + # If the ftrace_conf attribute has been defined in a base class, + # make sure that class gets its own copy since we are going to + # modify it + if 'ftrace_conf' not in dct: + ftrace_conf = copy.copy(ftrace_conf) + + new_cls.ftrace_conf = ftrace_conf + + # Merge-in a new source to FtraceConf that contains the events we + # collected + ftrace_conf.add_merged_src( + src='{}(required)'.format(new_cls.__qualname__), + conf={ + 'events': sorted(ftrace_events), + }, + ) + + return new_cls + + +class RTATestBundle(TestBundle, metaclass=RTATestBundleMeta): """ Abstract Base Class for :class:`lisa.wlgen.rta.RTA`-powered TestBundles + + Optionally, an ``ftrace_conf`` class attribute can be defined to hold + additional FTrace configuration used to record a trace while the synthetic + workload is being run. By default, the required events are extracted from + decorated test methods. + + .. seealso: :class:`lisa.tests.base.RTATestBundleMeta` for default + ``ftrace_conf`` content. """ TRACE_PATH = 'trace.dat' @@ -361,17 +429,6 @@ class RTATestBundle(TestBundle): Path to the dmesg log in the result directory. """ - ftrace_conf = FtraceConf({ - "events" : [ - "sched_switch", - "sched_wakeup" - ], - }, __qualname__) - """ - The FTrace configuration used to record a trace while the synthetic workload - is being run. - """ - TASK_PERIOD_MS = 16 """ A task period you can re-use for your :class:`lisa.wlgen.rta.RTATask` diff --git a/lisa/tests/scheduler/load_tracking.py b/lisa/tests/scheduler/load_tracking.py index 261e1428d72359282c71b77c5549e424c88e8475..dd6a3af0e321b21639ebdafa329d09bd95777c16 100644 --- a/lisa/tests/scheduler/load_tracking.py +++ b/lisa/tests/scheduler/load_tracking.py @@ -36,7 +36,8 @@ from lisa.tests.base import ( from lisa.target import Target from lisa.utils import ArtifactPath, groupby from lisa.wlgen.rta import Periodic, RTATask -from lisa.trace import FtraceConf, FtraceCollector +from lisa.trace import FtraceConf, FtraceCollector, requires_events +from lisa.analysis.load_tracking import LoadTrackingAnalysis UTIL_SCALE = 1024 """ @@ -158,17 +159,6 @@ class LoadTrackingBase(RTATestBundle, LoadTrackingHelpers): Base class for shared functionality of load tracking tests """ - ftrace_conf = FtraceConf({ - "events" : [ - "sched_switch", - "sched_load_avg_task", - "sched_load_avg_cpu", - "sched_pelt_se", - "sched_load_se", - "sched_load_cfs_rq", - ], - }, __qualname__) - cpufreq_conf = { "governor" : "performance" } @@ -196,13 +186,14 @@ class LoadTrackingBase(RTATestBundle, LoadTrackingHelpers): return cls(res_dir, plat_info) + @LoadTrackingAnalysis.df_tasks_signals.used_events + @requires_events('sched_switch') def get_task_sched_signals(self, trace, cpu, task_name): """ Get a :class:`pandas.DataFrame` with the sched signals for the workload task - This examines scheduler load tracking trace events, supporting either - sched_load_avg_task or sched_pelt_se. You will need a target kernel that - includes these events. + This examines scheduler load tracking trace events. You will need a + target kernel that includes the required events. :returns: :class:`pandas.DataFrame` with a column for each signal for the workload task @@ -291,6 +282,7 @@ class InvarianceItem(LoadTrackingBase): return cls(res_dir, plat_info, cpu, freq, freq_list) + @requires_events('sched_switch') def get_expected_util_avg(self, trace, cpu, task_name, capacity): """ Examine trace to figure out an expected mean for util_avg @@ -303,6 +295,8 @@ class InvarianceItem(LoadTrackingBase): # Scale the relative CPU/freq capacity return (duty_cycle_pct / 100) * capacity + @LoadTrackingBase.get_task_sched_signals.used_events + @get_expected_util_avg.used_events def _test_task_signal(self, signal_name, allowed_error_pct, trace, cpu, task_name, capacity): # Use utilization signal for both load and util, since they should be @@ -330,6 +324,7 @@ class InvarianceItem(LoadTrackingBase): return ok, exp_signal, signal_mean + @_test_task_signal.used_events def _test_signal(self, signal_name, allowed_error_pct): passed = True expected_data = {} @@ -359,6 +354,7 @@ class InvarianceItem(LoadTrackingBase): bundle.add_metric("Trace signals", trace_data) return bundle + @_test_signal.used_events def test_task_util_avg(self, allowed_error_pct=15) -> ResultBundle: """ Test that the mean of the util_avg signal matched the expected value @@ -378,6 +374,7 @@ class InvarianceItem(LoadTrackingBase): """ return self._test_signal('util', allowed_error_pct) + @_test_signal.used_events def test_task_load_avg(self, allowed_error_pct=15) -> ResultBundle: """ Test that the mean of the load_avg signal matched the expected value. @@ -502,6 +499,7 @@ class Invariance(TestBundle, LoadTrackingHelpers): # Combined version of some other tests, applied on all available # InvarianceItem with the result merged. + @InvarianceItem.test_task_util_avg.used_events def test_task_util_avg(self, allowed_error_pct=15) -> ResultBundle: """ Aggregated version of :meth:`InvarianceItem.test_task_util_avg` @@ -512,6 +510,7 @@ class Invariance(TestBundle, LoadTrackingHelpers): ) return self._test_all_freq(item_test) + @InvarianceItem.test_task_load_avg.used_events def test_task_load_avg(self, allowed_error_pct=15) -> ResultBundle: """ Aggregated version of :meth:`InvarianceItem.test_task_load_avg` @@ -544,6 +543,7 @@ class Invariance(TestBundle, LoadTrackingHelpers): return overall_bundle + @InvarianceItem.test_task_util_avg.used_events def test_cpu_invariance(self) -> ResultBundle: """ Check that items using the max freq on each CPU is passing util avg test. @@ -580,6 +580,7 @@ class Invariance(TestBundle, LoadTrackingHelpers): return res + @InvarianceItem.test_task_util_avg.used_events def test_freq_invariance(self) -> ResultBundle: """ Check that at least one CPU has items passing for all tested frequencies. @@ -682,10 +683,12 @@ class PELTTask(LoadTrackingBase): """ return list(self.rtapp_profile.keys())[0] + @LoadTrackingBase.get_task_sched_signals.used_events def get_task_sched_signals(self, cpu): # We only have one task and one trace, simplify this method a bit return super().get_task_sched_signals(self.trace, cpu, self.task_name) + @requires_events('sched_switch') def get_simulated_pelt(self, cpu, signal_name): """ Get simulated PELT signal and the periodic task used to model it. @@ -714,6 +717,7 @@ class PELTTask(LoadTrackingBase): return peltsim, pelt_task, df + @get_simulated_pelt.used_events def _test_range(self, signal_name, allowed_error_pct): res = ResultBundle.from_bool(True) task = self.rtapp_profile[self.task_name] @@ -756,6 +760,8 @@ class PELTTask(LoadTrackingBase): ax.axhline(avg, label="duty-cycle based average", linestyle="--", color="orange") ax.legend() + @get_simulated_pelt.used_events + @requires_events('sched_switch') def _test_behaviour(self, signal_name, error_margin_pct, allowed_error_pct): res = ResultBundle.from_bool(True) task = self.rtapp_profile[self.task_name] @@ -820,6 +826,7 @@ class PELTTask(LoadTrackingBase): return res + @_test_range.used_events def test_util_avg_range(self, allowed_error_pct=1.5) -> ResultBundle: """ Test that the util_avg value ranges (min, max) are sane @@ -828,6 +835,7 @@ class PELTTask(LoadTrackingBase): """ return self._test_range('util', allowed_error_pct) + @_test_range.used_events def test_load_avg_range(self, allowed_error_pct=1.5) -> ResultBundle: """ Test that the load_avg value ranges (min, max) are sane @@ -836,6 +844,7 @@ class PELTTask(LoadTrackingBase): """ return self._test_range('load', allowed_error_pct) + @_test_behaviour.used_events def test_util_avg_behaviour(self, error_margin_pct=7, allowed_error_pct=5)\ -> ResultBundle: """ @@ -850,6 +859,7 @@ class PELTTask(LoadTrackingBase): """ return self._test_behaviour('util', error_margin_pct, allowed_error_pct) + @_test_behaviour.used_events def test_load_avg_behaviour(self, error_margin_pct=7, allowed_error_pct=5)\ -> ResultBundle: """ @@ -944,6 +954,7 @@ class CPUMigrationBase(LoadTrackingBase): return cpu_util + @LoadTrackingAnalysis.df_cpus_signals.used_events def get_trace_cpu_util(self): """ Get the per-phase average CPU utilization read from the trace @@ -974,6 +985,7 @@ class CPUMigrationBase(LoadTrackingBase): return cpu_util + @get_trace_cpu_util.used_events def test_util_task_migration(self, allowed_error_pct=5) -> ResultBundle: """ Test that a migrated task properly propagates its utilization at the CPU level diff --git a/lisa/tests/scheduler/misfit.py b/lisa/tests/scheduler/misfit.py index 0e23d49f96f08c9d9f26301e58d26081646d095d..7d0211264fd50ea813828230e3e4379bc230e557 100644 --- a/lisa/tests/scheduler/misfit.py +++ b/lisa/tests/scheduler/misfit.py @@ -25,6 +25,7 @@ from lisa.wlgen.rta import Periodic from lisa.tests.base import RTATestBundle, Result, ResultBundle, CannotCreateError, TestMetric from lisa.target import Target from lisa.analysis.tasks import TasksAnalysis, TaskState +from lisa.analysis.idle import IdleAnalysis class MisfitMigrationBase(RTATestBundle): """ @@ -33,14 +34,6 @@ class MisfitMigrationBase(RTATestBundle): This class provides some helpers for features related to Misfit. """ - ftrace_conf = FtraceConf({ - "events" : [ - "sched_switch", - "sched_wakeup", - "cpu_idle" - ] - }, __qualname__) - @classmethod def _has_asym_cpucapacity(cls, target): """ @@ -237,6 +230,7 @@ class StaggeredFinishes(MisfitMigrationBase): return res @memoized + @IdleAnalysis.signal_cpu_active.used_events def _get_active_df(self, cpu): """ :returns: A dataframe that describes the idle status (on/off) of 'cpu' @@ -247,6 +241,7 @@ class StaggeredFinishes(MisfitMigrationBase): self.trace.add_events_deltas(active_df) return active_df + @_get_active_df.used_events def _max_idle_time(self, start, end, cpus): """ :returns: The maximum idle time of 'cpus' in the [start, end] interval @@ -269,6 +264,7 @@ class StaggeredFinishes(MisfitMigrationBase): return max_time, max_cpu + @_max_idle_time.used_events def _test_cpus_busy(self, task_state_dfs, cpus, allowed_idle_time_s): """ Test that for every window in which the tasks are running, :attr:`cpus` @@ -297,6 +293,7 @@ class StaggeredFinishes(MisfitMigrationBase): return res @TasksAnalysis.df_task_states.used_events + @_test_cpus_busy.used_events @RTATestBundle.check_noisy_tasks(noise_threshold_pct=1) def test_throughput(self, allowed_idle_time_s=None) -> ResultBundle: """ diff --git a/lisa/trace.py b/lisa/trace.py index 25777fee502e73e0da5d546630df3dcfc4fbe9b4..a2f8d24860f668571fa1238409481385fa23a023 100644 --- a/lisa/trace.py +++ b/lisa/trace.py @@ -30,6 +30,7 @@ import warnings import operator import logging import webbrowser +import inspect from functools import reduce, wraps from collections.abc import Sequence @@ -1093,6 +1094,16 @@ class TraceEventCheckerBase(abc.ABC, Loggable): """ pass + @abc.abstractmethod + def get_all_events(self): + """ + Return a set of all events that are checked by this checker. + + That may be a superset of events that are strictly required, when the + checker checks a logical OR combination of events for example. + """ + pass + def __call__(self, f): """ Decorator for methods that require some given trace events @@ -1115,11 +1126,27 @@ class TraceEventCheckerBase(abc.ABC, Loggable): else: checker = AndTraceEventChecker([self, used_events]) - @wraps(f) - def wrapper(self, *args, **kwargs): - available_events = set(self.trace.available_events) - checker.check_events(available_events) - return f(self, *args, **kwargs) + sig = inspect.signature(f) + if sig.parameters: + @wraps(f) + def wrapper(self, *args, **kwargs): + try: + trace = self.trace + # If there is no "trace" attribute, silently skip the check. This + # allows using the decorator for documentation and chaining purpose + # without having an actual trace to work on. + except AttributeError: + pass + else: + available_events = set(trace.available_events) + checker.check_events(available_events) + + return f(self, *args, **kwargs) + # If the decorated object takes no parameters, we cannot check anything + else: + @wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) # Set an attribute on the wrapper itself, so it can be e.g. added # to the method documentation @@ -1158,6 +1185,9 @@ class TraceEventChecker(TraceEventCheckerBase): def __init__(self, event): self.event = event + def get_all_events(self): + return {self.event} + def check_events(self, event_set): if self.event not in event_set: raise MissingTraceEventError(self) @@ -1193,6 +1223,12 @@ class AssociativeTraceEventChecker(TraceEventCheckerBase): self.checkers = checker_list self.op_str = op_str + def get_all_events(self): + events = set() + for checker in self.checkers: + events.update(checker.get_all_events()) + return events + @classmethod def from_events(cls, events): """ diff --git a/tools/exekall/exekall/main.py b/tools/exekall/exekall/main.py index 6b7fefff7e9b7b624b4e63c1b4575a942409258a..c34aa6e7093ff1a2382cea020a4c4208e58288b2 100755 --- a/tools/exekall/exekall/main.py +++ b/tools/exekall/exekall/main.py @@ -789,9 +789,9 @@ def do_run(args, parser, run_parser, argv): if verbose >= 2: out(expr.get_structure() + '\n') - formatted_out = adaptor.format_expr_list(expr_list, verbose=verbose) - if formatted_out: - out('\n' + formatted_out + '\n') + formatted_out = adaptor.format_expr_list(expr_list, verbose=verbose) + if formatted_out: + out('\n' + formatted_out + '\n') if only_list: return 0