def test_get_task_id(self): for name, pid in [('watchdog/0', 12), ('jbd2/sda2-8', 1383)]: task_id = TaskID(pid=pid, comm=name) task_id2 = TaskID(pid=pid, comm=None) task_id3 = TaskID(pid=None, comm=name) task_id_tuple = TaskID(pid=None, comm=name) for x in (pid, name, task_id, task_id2, task_id3, task_id_tuple): self.assertEqual(self.trace.get_task_id(x), task_id) with self.assertRaises(ValueError): for x in ('sh', 'sshd', 1639, 1642, 1702, 1717, 1718): self.trace.get_task_id(x)
def plot_tasks_activation(self, hide_tasks: TypedList[TaskID] = None, which_cpu: bool = True, **kwargs): """ Plot all tasks activations, in a style similar to kernelshark. :param hide_tasks: PIDs to hide. Note that PID 0 (idle task) will always be hidden. :type hide_tasks: list(TaskID) or None :Variable keyword arguments: Forwarded to :meth:`plot_task_activation`. """ trace = self.trace hide_tasks = hide_tasks or [] hidden_pids = { trace.get_task_id(task, update=True).pid for task in hide_tasks } # Hide idle task hidden_pids.add(0) # Plot per-PID, to avoid quirks around task renaming for pid, comms in self.trace.get_tasks().items(): if pid in hidden_pids: continue self.plot_task_activation(TaskID(pid=pid, comm=None), which_cpu=which_cpu, **kwargs)
def _get_rtapp_tasks(self): task_ids = set() for evt in self.trace.available_events: if not evt.startswith('rtapp_'): continue df = self.trace.df_events(evt) for pid, name in df[['__pid', '__comm']].drop_duplicates().values: task_ids.add(TaskID(pid, name)) return sorted(task_ids)
def get_first_switch(row): comm, pid, _ = row.name start_time = row['Time'] task = TaskID(comm=comm, pid=pid) start_swdf = df_filter_task_ids(swdf, [task], pid_col='next_pid', comm_col='next_comm') pre_phase_swdf = start_swdf[start_swdf.index < start_time] # The task with that comm and PID was never switched-in, which # means it was still on the current CPU when it was renamed, so we # just report phase-start. if pre_phase_swdf.empty: return start_time # Otherwise, we return the timestamp of the switch else: return pre_phase_swdf.index[-1]
def f(df): pid, comm = df.name task_id = TaskID(pid=pid, comm=comm) task, nr_json_phases = task_map[task_id] df = df.copy(deep=False) phase_nr = df['thread_loop'] * nr_json_phases + df['phase'] def add_info(get): return phase_nr.map(_dict({ i: get(phase) for i, phase in enumerate(task.phases) })) df['phase'] = add_info(lambda phase: phase.get('name')) df['properties'] = add_info(lambda phase: dict(phase.properties)) return df
def rtapp_tasks(self): """ List of :class:`lisa.trace.TaskID` of the ``rt-app`` tasks present in the trace. """ task_ids = set() for event in self.RTAPP_USERSPACE_EVENTS: try: df = self.trace.df_events(event) except MissingTraceEventError: continue else: for pid, name in df[['__pid', '__comm']].drop_duplicates().values: task_ids.add(TaskID(pid, name)) return sorted(task_ids)
def rtapp_tasks(self): """ List of :class:`lisa.trace.TaskID` of the ``rt-app`` tasks present in the trace. """ task_ids = set() for event in self.RTAPP_USERSPACE_EVENTS: try: df = self.trace.df_event(event) except MissingTraceEventError: continue else: task_ids.update(df[[ '__pid', '__comm' ]].drop_duplicates().apply( lambda row: TaskID(pid=row['__pid'], comm=row['__comm']), axis=1, )) return sorted(task_ids)
def plot_tasks_activation(self, hide_tasks: TypedList[TaskID] = None, which_cpu: bool = True, **kwargs): """ Plot all tasks activations, in a style similar to kernelshark. :param hide_tasks: PIDs to hide. Note that PID 0 (idle task) will always be hidden. :type hide_tasks: list(TaskID) or None :Variable keyword arguments: Forwarded to :meth:`plot_task_activation`. """ trace = self.trace hide_tasks = hide_tasks or [] hidden_pids = { trace.get_task_id(task, update=True).pid for task in hide_tasks } # Hide idle task hidden_pids.add(0) # Plot per-PID, to avoid quirks around task renaming for pid, comms in self.trace.get_tasks().items(): if pid in hidden_pids: continue try: self.plot_task_activation(TaskID(pid=pid, comm=None), which_cpu=which_cpu, **kwargs) except ValueError: # The task might not be present in that slice, or might only be # visible through events that are not used for the task state # (such as PELT events). continue
class RTATestBundle(FtraceTestBundle, DmesgTestBundle): """ Abstract Base Class for :class:`lisa.wlgen.rta.RTA`-powered TestBundles .. seealso: :class:`lisa.tests.base.FtraceTestBundleMeta` for default ``ftrace_conf`` content. """ TASK_PERIOD_MS = 16 """ A task period you can re-use for your :class:`lisa.wlgen.rta.RTATask` definitions. """ NOISE_ACCOUNTING_THRESHOLDS = { # Idle task - ignore completely # note: since it has multiple comms, we need to ignore them TaskID(pid=0, comm=None): 100, # Feeble boards like Juno/TC2 spend a while in sugov r"^sugov:\d+$": 5, # The mailbox controller (MHU), now threaded, creates work that sometimes # exceeds the 1% threshold. r"^irq/\d+-mhu_link$": 1.5 } """ PID/comm specific tuning for :meth:`test_noisy_tasks` * **keys** can be PIDs, comms, or regexps for comms. * **values** are noisiness thresholds (%), IOW below that runtime threshold the associated task will be ignored in the noise accounting. """ @RTAEventsAnalysis.df_rtapp_phases_start.used_events @RTAEventsAnalysis.df_rtapp_phases_end.used_events @requires_events('sched_switch') def trace_window(self, trace): """ The time window to consider for this :class:`RTATestBundle` :returns: a (start, stop) tuple Since we're using rt-app profiles, we know the name of tasks we are interested in, so we can trim our trace scope to filter out the setup/teardown events we don't care about. Override this method if you need a different trace trimming. .. warning:: Calling ``self.trace`` here will raise an :exc:`AttributeError` exception, to avoid entering infinite recursion. """ swdf = trace.df_events('sched_switch') def get_first_switch(row): comm, pid, _ = row.name start_time = row['Time'] task = TaskID(comm=comm, pid=pid) start_swdf = df_filter_task_ids(swdf, [task], pid_col='next_pid', comm_col='next_comm') pre_phase_swdf = start_swdf[start_swdf.index < start_time] # The task with that comm and PID was never switched-in, which # means it was still on the current CPU when it was renamed, so we # just report phase-start. if pre_phase_swdf.empty: return start_time # Otherwise, we return the timestamp of the switch else: return pre_phase_swdf.index[-1] # Find when the first rtapp phase starts, and take the associated # sched_switch that is immediately preceding rta_start = trace.analysis.rta.df_rtapp_phases_start().apply( get_first_switch, axis=1).min() # Find when the last rtapp phase ends rta_stop = trace.analysis.rta.df_rtapp_phases_end()['Time'].max() return (rta_start, rta_stop) @property def rtapp_profile(self): """ Compute the RTapp profile based on ``plat_info``. """ return self.get_rtapp_profile(self.plat_info) @property def rtapp_tasks(self): """ The rtapp task names as found from the trace in this bundle. :return: the list of actual trace task names """ return sorted( itertools.chain.from_iterable(self.rtapp_tasks_map.values())) @property @requires_events('sched_switch') @memoized def rtapp_tasks_map(self): """ Mapping of task names as specified in the rtapp profile to list of task names found in the trace. If the task forked, the list will contain more than one item. """ trace = self.get_trace(events=['sched_switch']) prefix_regexps = { prefix: re.compile(r"^{}(-[0-9]+)*$".format(re.escape(prefix))) for prefix in self.rtapp_profile.keys() } comms = set(itertools.chain.from_iterable(trace.get_tasks().values())) task_map = { prefix: sorted(comm for comm in comms if re.match(regexp, comm)) for prefix, regexp in prefix_regexps.items() } missing = sorted(prefix for prefix, comms in task_map.items() if not comms) if missing: raise RuntimeError( "Missing tasks matching the following rt-app profile names: {}" .format(', '.join(missing))) return task_map @property def cgroup_configuration(self): """ Compute the cgroup configuration based on ``plat_info`` """ return self.get_cgroup_configuration(self.plat_info) @non_recursive_property @memoized def trace(self): """ :returns: a :class:`lisa.trace.TraceView` cropped to fit the ``rt-app`` tasks. All events specified in ``ftrace_conf`` are parsed from the trace, so it is suitable for direct use in methods. Having the trace as a property lets us defer the loading of the actual trace to when it is first used. Also, this prevents it from being serialized when calling :meth:`lisa.utils.Serializable.to_path` and allows updating the underlying path before it is actually loaded to match a different folder structure. """ trace = self.get_trace(events=self.ftrace_conf["events"]) return trace.get_view(self.trace_window(trace)) @TasksAnalysis.df_tasks_runtime.used_events def test_noisy_tasks(self, noise_threshold_pct=None, noise_threshold_ms=None): """ Test that no non-rtapp ("noisy") task ran for longer than the specified thresholds :param noise_threshold_pct: The maximum allowed runtime for noisy tasks in percentage of the total rt-app execution time :type noise_threshold_pct: float :param noise_threshold_ms: The maximum allowed runtime for noisy tasks in ms :type noise_threshold_ms: float If both are specified, the smallest threshold (in seconds) will be used. """ if noise_threshold_pct is None and noise_threshold_ms is None: raise ValueError('Both "{}" and "{}" cannot be None'.format( "noise_threshold_pct", "noise_threshold_ms")) # No task can run longer than the recorded duration threshold_s = self.trace.time_range if noise_threshold_pct is not None: threshold_s = noise_threshold_pct * self.trace.time_range / 100 if noise_threshold_ms is not None: threshold_s = min(threshold_s, noise_threshold_ms * 1e3) df = self.trace.analysis.tasks.df_tasks_runtime() # We don't want to account the test tasks ignored_ids = list(map(self.trace.get_task_id, self.rtapp_tasks)) def compute_duration_pct(row): return row.runtime * 100 / self.trace.time_range df["runtime_pct"] = df.apply(compute_duration_pct, axis=1) df['pid'] = df.index # Figure out which PIDs to exclude from the thresholds for key, threshold in self.NOISE_ACCOUNTING_THRESHOLDS.items(): # Find out which task(s) this threshold is about if isinstance(key, str): comms = [ comm for comm in df.comm.values if re.match(key, comm) ] task_ids = [self.trace.get_task_id(comm) for comm in comms] else: # Use update=False to let None fields propagate, as they are # used to indicate a "dont care" value task_ids = [self.trace.get_task_id(key, update=False)] # For those tasks, check the threshold ignored_ids.extend( task_id for task_id in task_ids if df_filter_task_ids( df, [task_id]).iloc[0].runtime_pct <= threshold) self.get_logger().info( "Ignored PIDs for noise contribution: {}".format(", ".join( map(str, ignored_ids)))) # Filter out unwanted tasks (rt-app tasks + thresholds) df_noise = df_filter_task_ids(df, ignored_ids, invert=True) if df_noise.empty: return ResultBundle.from_bool(True) pid = df_noise.index[0] comm = df_noise.comm.values[0] duration_s = df_noise.runtime.values[0] duration_pct = duration_s * 100 / self.trace.time_range res = ResultBundle.from_bool(duration_s < threshold_s) metric = { "pid": pid, "comm": comm, "duration (abs)": TestMetric(duration_s, "s"), "duration (rel)": TestMetric(duration_pct, "%") } res.add_metric("noisiest task", metric) return res @classmethod #pylint: disable=unused-argument def check_noisy_tasks(cls, noise_threshold_pct=None, noise_threshold_ms=None): """ Decorator that applies :meth:`test_noisy_tasks` to the trace of the :class:`TestBundle` returned by the underlying method. The :class:`Result` will be changed to :attr:`Result.UNDECIDED` if that test fails. We also expose :meth:`test_noisy_tasks` parameters to the decorated function. """ def decorator(func): @update_wrapper_doc( func, added_by= ':meth:`lisa.tests.base.RTATestBundle.test_noisy_tasks`', description=textwrap.dedent(""" The returned ``ResultBundle.result`` will be changed to :attr:`~lisa.tests.base.Result.UNDECIDED` if the environment was too noisy: {} """).strip().format(inspect.getdoc(cls.test_noisy_tasks))) @cls.test_noisy_tasks.used_events def wrapper(self, *args, noise_threshold_pct=noise_threshold_pct, noise_threshold_ms=noise_threshold_ms, **kwargs): res = func(self, *args, **kwargs) noise_res = self.test_noisy_tasks(noise_threshold_pct, noise_threshold_ms) res.metrics.update(noise_res.metrics) if not noise_res: res.result = Result.UNDECIDED return res return wrapper return decorator @classmethod def unscaled_utilization(cls, plat_info, cpu, utilization_pct): """ Convert utilization scaled to a CPU to a 'raw', unscaled one. :param capacity: The CPU against which ``utilization_pct``` is scaled :type capacity: int :param utilization_pct: The scaled utilization in % :type utilization_pct: int """ if "nrg-model" in plat_info: capacity_scale = plat_info["nrg-model"].capacity_scale else: capacity_scale = 1024 return int((plat_info["cpu-capacities"][cpu] / capacity_scale) * utilization_pct) @classmethod @abc.abstractmethod def get_rtapp_profile(cls, plat_info): """ :returns: a :class:`dict` with task names as keys and :class:`lisa.wlgen.rta.RTATask` as values This is the method you want to override to specify what is your synthetic workload. """ pass @classmethod def get_cgroup_configuration(cls, plat_info): """ :returns: a :class:`dict` representing the configuration of a particular cgroup. This is a method you may optionally override to configure a cgroup for the synthetic workload. Example of return value:: { 'name': 'lisa_test', 'controller': 'schedtune', 'attributes' : { 'prefer_idle' : 1, 'boost': 50 } } """ return {} @classmethod def _target_configure_cgroup(cls, target, cfg): if not cfg: return None kind = cfg['controller'] if kind not in target.cgroups.controllers: raise CannotCreateError( '"{}" cgroup controller unavailable'.format(kind)) ctrl = target.cgroups.controllers[kind] cg = ctrl.cgroup(cfg['name']) cg.set(**cfg['attributes']) return '/' + cg.name @classmethod def run_rtapp(cls, target, res_dir, profile=None, ftrace_coll=None, cg_cfg=None, wipe_run_dir=True): """ Run the given RTA profile on the target, and collect an ftrace trace. :param target: target to execute the workload on. :type target: lisa.target.Target :param res_dir: Artifact folder where the artifacts will be stored. :type res_dir: str or lisa.utils.ArtifactPath :param profile: ``rt-app`` profile, as a dictionary of ``dict(task_name, RTATask)``. If ``None``, :meth:`~lisa.tests.base.RTATestBundle.get_rtapp_profile` is called with ``target.plat_info``. :type profile: dict(str, lisa.wlgen.rta.RTATask) :param ftrace_coll: Ftrace collector to use to record the trace. This allows recording extra events compared to the default one, which is based on the ``ftrace_conf`` class attribute. :type ftrace_coll: lisa.trace.FtraceCollector :param cg_cfg: CGroup configuration dictionary. If ``None``, :meth:`lisa.tests.base.RTATestBundle.get_cgroup_configuration` is called with ``target.plat_info``. :type cg_cfg: dict :param wipe_run_dir: Remove the run directory on the target after execution of the workload. :type wipe_run_dir: bool """ trace_path = ArtifactPath.join(res_dir, cls.TRACE_PATH) dmesg_path = ArtifactPath.join(res_dir, cls.DMESG_PATH) ftrace_coll = ftrace_coll or FtraceCollector.from_conf( target, cls.ftrace_conf) dmesg_coll = DmesgCollector(target) profile = profile or cls.get_rtapp_profile(target.plat_info) cg_cfg = cg_cfg or cls.get_cgroup_configuration(target.plat_info) trace_events = [ event.replace('rtapp_', '') for event in ftrace_coll.events if event.startswith("rtapp_") ] wload = RTA.by_profile(target, "rta_{}".format(cls.__name__.lower()), profile, res_dir=res_dir, trace_events=trace_events) cgroup = cls._target_configure_cgroup(target, cg_cfg) as_root = cgroup is not None wload_cm = wload if wipe_run_dir else nullcontext(wload) # Pre-hit the calibration information, in case this is a lazy value. # This avoids polluting the trace and the dmesg output with the # calibration tasks. Since we know that rt-app will always need it for # anything useful, it's reasonable to do it here. target.plat_info['rtapp']['calib'] with wload_cm, dmesg_coll, ftrace_coll, target.freeze_userspace(): wload.run(cgroup=cgroup, as_root=as_root) ftrace_coll.get_trace(trace_path) dmesg_coll.get_trace(dmesg_path) return trace_path # Keep compat with existing code @classmethod def _run_rtapp(cls, *args, **kwargs): """ Has been renamed to :meth:`~lisa.tests.base.RTATestBundle.run_rtapp`, as it really is part of the public API. """ return cls.run_rtapp(*args, **kwargs) @classmethod def _from_target(cls, target: Target, *, res_dir: ArtifactPath, ftrace_coll: FtraceCollector = None) -> 'RTATestBundle': """ Factory method to create a bundle using a live target This will execute the rt-app workload described in :meth:`~lisa.tests.base.RTATestBundle.get_rtapp_profile` """ cls.run_rtapp(target, res_dir, ftrace_coll=ftrace_coll) plat_info = target.plat_info return cls(res_dir, plat_info)
def _df_tasks_states(self, tasks=None, return_one_df=False): """ Compute tasks states for all tasks. :param tasks: If specified, states of these tasks only will be yielded. The :class:`lisa.trace.TaskID` must have a ``pid`` field specified, since the task state is per-PID. :type tasks: list(lisa.trace.TaskID) or list(int) :param return_one_df: If ``True``, a single dataframe is returned with new extra columns. If ``False``, a generator is returned that yields tuples of ``(TaskID, task_df)``. Each ``task_df`` contains the new columns. :type return_one_df: bool """ ###################################################### # A) Assemble the sched_switch and sched_wakeup events ###################################################### wk_df = self.trace.df_event('sched_wakeup') sw_df = self.trace.df_event('sched_switch') try: wkn_df = self.trace.df_event('sched_wakeup_new') except MissingTraceEventError: pass else: wk_df = pd.concat([wk_df, wkn_df]) wk_df = wk_df[["pid", "comm", "target_cpu", "__cpu"]].copy(deep=False) wk_df["curr_state"] = TaskState.TASK_WAKING prev_sw_df = sw_df[["__cpu", "prev_pid", "prev_state", "prev_comm"]].copy() next_sw_df = sw_df[["__cpu", "next_pid", "next_comm"]].copy() prev_sw_df.rename(columns={ "prev_pid": "pid", "prev_state": "curr_state", "prev_comm": "comm", }, inplace=True) next_sw_df["curr_state"] = TaskState.TASK_ACTIVE next_sw_df.rename(columns={ 'next_pid': 'pid', 'next_comm': 'comm' }, inplace=True) all_sw_df = prev_sw_df.append(next_sw_df, sort=False) # Integer values are prefered here, otherwise the whole column # is converted to float64 all_sw_df['target_cpu'] = -1 df = all_sw_df.append(wk_df, sort=False) df.sort_index(inplace=True) df.rename(columns={'__cpu': 'cpu'}, inplace=True) # Restrict the set of data we will process to a given set of tasks if tasks is not None: def resolve_task(task): """ Get a TaskID for each task, and only update existing TaskID if they lack a PID field, since that's what we care about in that function. """ try: do_update = task.pid is None except AttributeError: do_update = False return self.trace.get_task_id(task, update=do_update) tasks = list(map(resolve_task, tasks)) df = df_filter_task_ids(df, tasks) # Return a unique dataframe with new columns added if return_one_df: df.sort_index(inplace=True) df.index.name = 'Time' df.reset_index(inplace=True) # Since sched_switch is split in two df (next and prev), we end up with # duplicated indices. Avoid that by incrementing them by the minimum # amount possible. df = df_update_duplicates(df, col='Time', inplace=True) grouped = df.groupby('pid', observed=True, sort=False) new_columns = dict( next_state=grouped['curr_state'].shift( -1, fill_value=TaskState.TASK_UNKNOWN), # GroupBy.transform() will run the function on each group, and # concatenate the resulting series to create a new column. # Note: We actually need transform() to chain 2 operations on # the group, otherwise the first operation returns a final # Series, and the 2nd is not applied on groups delta=grouped['Time'].transform( lambda time: time.diff().shift(-1)), ) df = df.assign(**new_columns) df.set_index('Time', inplace=True) return df # Return a generator yielding (TaskID, task_df) tuples else: def make_pid_df(pid_df): # Even though the initial dataframe contains duplicated indices due to # using both prev_pid and next_pid in sched_switch event, we should # never end up with prev_pid == next_pid, so task-specific dataframes # are expected to be free from duplicated timestamps. # assert not df.index.duplicated().any() # Copy the df to add new columns pid_df = pid_df.copy(deep=False) # For each PID, add the time it spent in each state pid_df['delta'] = pid_df.index.to_series().diff().shift(-1) pid_df['next_state'] = pid_df['curr_state'].shift( -1, fill_value=TaskState.TASK_UNKNOWN) return pid_df signals = df_split_signals(df, ['pid']) return ((TaskID(pid=col['pid'], comm=None), make_pid_df(pid_df)) for col, pid_df in signals)
class RTATestBundle(FtraceTestBundle): """ Abstract Base Class for :class:`lisa.wlgen.rta.RTA`-powered TestBundles .. seealso: :class:`lisa.tests.base.FtraceTestBundleMeta` for default ``ftrace_conf`` content. """ DMESG_PATH = 'dmesg.log' """ Path to the dmesg log in the result directory. """ TASK_PERIOD_MS = 16 """ A task period you can re-use for your :class:`lisa.wlgen.rta.RTATask` definitions. """ NOISE_ACCOUNTING_THRESHOLDS = { # Idle task - ignore completely # note: since it has multiple comms, we need to ignore them TaskID(pid=0, comm=None): 100, # Feeble boards like Juno/TC2 spend a while in sugov r"^sugov:\d+$": 5, # The mailbox controller (MHU), now threaded, creates work that sometimes # exceeds the 1% threshold. r"^irq/\d+-mhu_link$": 1.5 } """ PID/comm specific tuning for :meth:`test_noisy_tasks` * **keys** can be PIDs, comms, or regexps for comms. * **values** are noisiness thresholds (%), IOW below that runtime threshold the associated task will be ignored in the noise accounting. """ @requires_events('sched_switch') def trace_window(self, trace): """ The time window to consider for this :class:`RTATestBundle` :returns: a (start, stop) tuple Since we're using rt-app profiles, we know the name of tasks we are interested in, so we can trim our trace scope to filter out the setup/teardown events we don't care about. Override this method if you need a different trace trimming. .. warning:: Calling ``self.trace`` here will raise an :exc:`AttributeError` exception, to avoid entering infinite recursion. """ sdf = trace.df_events('sched_switch') # Find when the first task starts running rta_start = sdf[sdf.next_comm.isin(self.rtapp_tasks)].index[0] # Find when the last task stops running rta_stop = sdf[sdf.prev_comm.isin(self.rtapp_tasks)].index[-1] return (rta_start, rta_stop) @property def rtapp_profile(self): """ Compute the RTapp profile based on ``plat_info``. """ return self.get_rtapp_profile(self.plat_info) @property def rtapp_tasks(self): """ Sorted list of rtapp task names, as defined in ``rtapp_profile`` attribute. """ return sorted(self.rtapp_profile.keys()) @property def cgroup_configuration(self): """ Compute the cgroup configuration based on ``plat_info`` """ return self.get_cgroup_configuration(self.plat_info) def get_trace(self, **kwargs): """ :returns: a :class:`lisa.trace.TraceView` cropped to fit the ``rt-app`` tasks. :Keyword arguments: forwarded to :class:`lisa.trace.Trace`. """ trace = Trace(self.trace_path, self.plat_info, **kwargs) return trace.get_view(self.trace_window(trace)) @TasksAnalysis.df_tasks_runtime.used_events def test_noisy_tasks(self, noise_threshold_pct=None, noise_threshold_ms=None): """ Test that no non-rtapp ("noisy") task ran for longer than the specified thresholds :param noise_threshold_pct: The maximum allowed runtime for noisy tasks in percentage of the total rt-app execution time :type noise_threshold_pct: float :param noise_threshold_ms: The maximum allowed runtime for noisy tasks in ms :type noise_threshold_ms: float If both are specified, the smallest threshold (in seconds) will be used. """ if noise_threshold_pct is None and noise_threshold_ms is None: raise ValueError('Both "{}" and "{}" cannot be None'.format( "noise_threshold_pct", "noise_threshold_ms")) # No task can run longer than the recorded duration threshold_s = self.trace.time_range if noise_threshold_pct is not None: threshold_s = noise_threshold_pct * self.trace.time_range / 100 if noise_threshold_ms is not None: threshold_s = min(threshold_s, noise_threshold_ms * 1e3) df = self.trace.analysis.tasks.df_tasks_runtime() # We don't want to account the test tasks ignored_ids = list(map(self.trace.get_task_id, self.rtapp_tasks)) def compute_duration_pct(row): return row.runtime * 100 / self.trace.time_range df["runtime_pct"] = df.apply(compute_duration_pct, axis=1) df['pid'] = df.index # Figure out which PIDs to exclude from the thresholds for key, threshold in self.NOISE_ACCOUNTING_THRESHOLDS.items(): # Find out which task(s) this threshold is about if isinstance(key, str): comms = [ comm for comm in df.comm.values if re.match(key, comm) ] task_ids = [self.trace.get_task_id(comm) for comm in comms] else: # Use update=False to let None fields propagate, as they are # used to indicate a "dont care" value task_ids = [self.trace.get_task_id(key, update=False)] # For those tasks, check the threshold ignored_ids.extend( task_id for task_id in task_ids if df_filter_task_ids( df, [task_id]).iloc[0].runtime_pct <= threshold) self.get_logger().info( "Ignored PIDs for noise contribution: {}".format(", ".join( map(str, ignored_ids)))) # Filter out unwanted tasks (rt-app tasks + thresholds) df_noise = df_filter_task_ids(df, ignored_ids, invert=True) if df_noise.empty: return ResultBundle.from_bool(True) pid = df_noise.index[0] comm = df_noise.comm.values[0] duration_s = df_noise.runtime.values[0] duration_pct = duration_s * 100 / self.trace.time_range res = ResultBundle.from_bool(duration_s < threshold_s) metric = { "pid": pid, "comm": comm, "duration (abs)": TestMetric(duration_s, "s"), "duration (rel)": TestMetric(duration_pct, "%") } res.add_metric("noisiest task", metric) return res @classmethod #pylint: disable=unused-argument def check_noisy_tasks(cls, noise_threshold_pct=None, noise_threshold_ms=None): """ Decorator that applies :meth:`test_noisy_tasks` to the trace of the :class:`TestBundle` returned by the underlying method. The :class:`Result` will be changed to :attr:`Result.UNDECIDED` if that test fails. We also expose :meth:`test_noisy_tasks` parameters to the decorated function. """ def decorator(func): @update_wrapper_doc( func, added_by= ':meth:`lisa.tests.base.RTATestBundle.test_noisy_tasks`', description=textwrap.dedent(""" The returned ``ResultBundle.result`` will be changed to :attr:`~lisa.tests.base.Result.UNDECIDED` if the environment was too noisy: {} """).strip().format(inspect.getdoc(cls.test_noisy_tasks))) @cls.test_noisy_tasks.used_events def wrapper(self, *args, noise_threshold_pct=noise_threshold_pct, noise_threshold_ms=noise_threshold_ms, **kwargs): res = func(self, *args, **kwargs) noise_res = self.test_noisy_tasks(noise_threshold_pct, noise_threshold_ms) res.metrics.update(noise_res.metrics) if not noise_res: res.result = Result.UNDECIDED return res return wrapper return decorator @classmethod def unscaled_utilization(cls, plat_info, cpu, utilization_pct): """ Convert utilization scaled to a CPU to a 'raw', unscaled one. :param capacity: The CPU against which ``utilization_pct``` is scaled :type capacity: int :param utilization_pct: The scaled utilization in % :type utilization_pct: int """ if "nrg-model" in plat_info: capacity_scale = plat_info["nrg-model"].capacity_scale else: capacity_scale = 1024 return int((plat_info["cpu-capacities"][cpu] / capacity_scale) * utilization_pct) @classmethod @abc.abstractmethod def get_rtapp_profile(cls, plat_info): """ :returns: a :class:`dict` with task names as keys and :class:`lisa.wlgen.rta.RTATask` as values This is the method you want to override to specify what is your synthetic workload. """ pass @classmethod def get_cgroup_configuration(cls, plat_info): """ :returns: a :class:`dict` representing the configuration of a particular cgroup. This is a method you may optionally override to configure a cgroup for the synthetic workload. Example of return value:: { 'name': 'lisa_test', 'controller': 'schedtune', 'attributes' : { 'prefer_idle' : 1, 'boost': 50 } } """ return None @classmethod def _target_configure_cgroup(cls, target, cfg): if not cfg: return None kind = cfg['controller'] if kind not in target.cgroups.controllers: raise CannotCreateError( '"{}" cgroup controller unavailable'.format(kind)) ctrl = target.cgroups.controllers[kind] cg = ctrl.cgroup(cfg['name']) cg.set(**cfg['attributes']) return '/' + cg.name @classmethod def _run_rtapp(cls, target, res_dir, profile, ftrace_coll=None, cg_cfg=None): wload = RTA.by_profile(target, "rta_{}".format(cls.__name__.lower()), profile, res_dir=res_dir) trace_path = ArtifactPath.join(res_dir, cls.TRACE_PATH) dmesg_path = ArtifactPath.join(res_dir, cls.DMESG_PATH) ftrace_coll = ftrace_coll or FtraceCollector.from_conf( target, cls.ftrace_conf) dmesg_coll = DmesgCollector(target) cgroup = cls._target_configure_cgroup(target, cg_cfg) as_root = cgroup is not None with dmesg_coll, ftrace_coll, target.freeze_userspace(): wload.run(cgroup=cgroup, as_root=as_root) ftrace_coll.get_trace(trace_path) dmesg_coll.get_trace(dmesg_path) return trace_path @classmethod def _from_target(cls, target: Target, *, res_dir: ArtifactPath = None, ftrace_coll: FtraceCollector = None) -> 'RTATestBundle': """ Factory method to create a bundle using a live target This will execute the rt-app workload described in :meth:`~lisa.tests.base.RTATestBundle.get_rtapp_profile` """ plat_info = target.plat_info rtapp_profile = cls.get_rtapp_profile(plat_info) cgroup_config = cls.get_cgroup_configuration(plat_info) cls._run_rtapp(target, res_dir, rtapp_profile, ftrace_coll, cgroup_config) return cls(res_dir, plat_info)
def plot_tasks_activation(self, tasks: TypedList[TaskID]=None, hide_tasks: TypedList[TaskID]=None, which_cpu: bool=True, overlay: bool=False, **kwargs): """ Plot all tasks activations, in a style similar to kernelshark. :param tasks: Tasks to plot. If ``None``, all tasks in the trace will be used. :type tasks: list(TaskID) or None :param hide_tasks: Tasks to hide. Note that PID 0 (idle task) will always be hidden. :type hide_tasks: list(TaskID) or None :param alpha: transparency level of the plot. :type task: float :param overlay: If ``True``, adjust the transparency and plot activations on a separate hidden scale so existing scales are not modified. :type task: bool :param duration: Plot the duration of each sleep/activation. :type duration: bool :param duty_cycle: Plot the duty cycle of each pair of sleep/activation. :type duty_cycle: bool :param which_cpu: If ``True``, plot the activations on each CPU in a separate row like kernelshark does. :type which_cpu: bool :param height_duty_cycle: Height of each activation's rectangle is proportional to the duty cycle during that activation. :type height_duty_cycle: bool .. seealso:: :meth:`df_task_activation` """ trace = self.trace hidden = set(itertools.chain.from_iterable( trace.get_task_ids(task) for task in (hide_tasks or []) )) if tasks: best_effort = False task_ids = list(itertools.chain.from_iterable( map(trace.get_task_ids, tasks) )) else: best_effort = True task_ids = trace.task_ids full_task_ids = sorted( task for task in task_ids if ( task not in hidden and task.pid != 0 ) ) # Only consider the PIDs in order to: # * get the same color for the same PID during its whole life # * avoid potential issues around task renaming # Note: The task comm will still be displayed in the hover tool task_ids = [ TaskID(pid=pid, comm=None) for pid in sorted(set(x.pid for x in full_task_ids)) ] #TODO: Re-enable the CPU "lanes" once this bug is solved: # https://github.com/holoviz/holoviews/issues/4979 if False and which_cpu and not overlay: # Add horizontal lines to delimitate each CPU "lane" in the plot cpu_lanes = [ hv.HLine(y - offset).options( color='grey', alpha=0.2, ).options( backend='bokeh', line_width=0.5, ).options( backend='matplotlib', linewidth=0.5, ) for y in range(trace.cpus_count + 1) for offset in ((0.5, -0.5) if y == 0 else (0.5,)) ] else: cpu_lanes = [] title = 'Activations of ' + ', '.join( map(str, full_task_ids) ) if len(title) > 50: title = 'Task activations' return self._plot_tasks_activation( tasks=task_ids, which_cpu=which_cpu, overlay=overlay, best_effort=best_effort, **kwargs ).options( title=title )