Example #1
0
    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)
Example #2
0
    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)
Example #3
0
 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)
Example #4
0
 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]
Example #5
0
            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
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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
Example #9
0
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)
Example #10
0
    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)
Example #11
0
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)
Example #12
0
    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
        )