diff --git a/lisa/analysis/rta.py b/lisa/analysis/rta.py index dc58c1e871944024d427d46dc8e251ab5afe4228..ae26702ab539243a5addf3370ce850b37979d7c5 100644 --- a/lisa/analysis/rta.py +++ b/lisa/analysis/rta.py @@ -21,6 +21,8 @@ import glob import pandas as pd +from lisa.utils import memoized + from lisa.utils import Loggable from lisa.analysis.base import AnalysisHelpers @@ -38,7 +40,7 @@ class PerfAnalysis(AnalysisHelpers): RTA_LOG_PATTERN = 'rt-app-{task}.log' "Filename pattern matching RTApp log files" - def __init__(self, task_log_map): + def __init__(self, task_log_map, bundle=None): """ Load peformance data of an rt-app workload """ @@ -47,6 +49,15 @@ class PerfAnalysis(AnalysisHelpers): if not task_log_map: raise ValueError('No tasks in the task log mapping') + # When a bundle is provided, sanity check that all logfiles are in its res_dir + if bundle: + resdir = getattr(bundle, 'res_dir', None) + if not resdir: + return None + if not all([logfile.startswith(resdir) + for _, logfile in task_log_map.items()]): + return None + for task_name, logfile in task_log_map.items(): logger.debug('rt-app task [{}] logfile: {}'.format( task_name, logfile @@ -59,12 +70,28 @@ class PerfAnalysis(AnalysisHelpers): } for task_name, logfile in task_log_map.items() } + self._bundle = bundle @classmethod - def from_log_files(cls, rta_logs): + def _log_dir_from_bundle(cls, bundle, default=None): + if bundle: + rtapp_profile = getattr(bundle, 'rtapp_profile', None) + if not rtapp_profile: + return None + log_dir = getattr(bundle, 'res_dir', None) + if log_dir: + return log_dir + return default + + @classmethod + def from_log_files(cls, rta_logs, bundle=None): """ Build a :class:`PerfAnalysis` from a sequence of RTApp log files + If a :class:`ResultBundle` is provided as the `bundle` parameter, the + `bundle`'s `log_dir` is used to ensure the specified log files are from + that folder. + :param rta_logs: sequence of path to log files :type rta_logs: list(str) """ @@ -81,32 +108,54 @@ class PerfAnalysis(AnalysisHelpers): find_task_name(logfile): logfile for logfile in rta_logs } - return cls(task_log_map) + return cls(task_log_map, bundle) @classmethod - def from_dir(cls, log_dir): + def from_dir(cls, log_dir=None, bundle=None): """ - Build a :class:`PerfAnalysis` from a folder path + Build a :class:`PerfAnalysis` from a folder path or :class:ResultBundle + + One among the `log_dir` or the `bundle` parameter must be provided. + If a :class:`ResultBundle` is provided as the `bundle` parameter, the + `bundle`'s `log_dir` is used for log files search. :param log_dir: Folder containing RTApp log files :type log_dir: str + + :param bundle: A :class:ResultBundle containing information on the log folder + :type bundle: str """ + log_dir = cls._log_dir_from_bundle(bundle, log_dir) + if not log_dir: + return None + rta_logs = glob.glob(os.path.join( log_dir, cls.RTA_LOG_PATTERN.format(task='*'), )) - return cls.from_log_files(rta_logs) + return cls.from_log_files(rta_logs, bundle) @classmethod - def from_task_names(cls, task_names, log_dir): + def from_task_names(cls, task_names, log_dir=None, bundle=None): """ - Build a :class:`PerfAnalysis` from a list of task names + Build a :class:`PerfAnalysis` from a list of task names and a log_dir or :class:Result. + + One among the `log_dir` or the `bundle` parameter must be provided. + If a :class:`ResultBundle` is provided as the `bundle` parameter, the + `bundle`'s `log_dir` is used for log files search. :param task_names: List of task names to look for :type task_names: list(str) :param log_dir: Folder containing RTApp log files :type log_dir: str + + :param bundle: A :class:ResultBundle containing information on the log folder + :type bundle: str """ + log_dir = cls._log_dir_from_bundle(bundle, log_dir) + if not log_dir: + return None + def find_log_file(task_name, log_dir): log_file = os.path.join(log_dir, cls.RTA_LOG_PATTERN.format(task_name)) if not os.path.isfile(log_file): @@ -119,7 +168,20 @@ class PerfAnalysis(AnalysisHelpers): task_name: find_log_file(task, log_dir) for task_name in tasks } - return cls(task_log_map) + return cls(task_log_map, bundle) + + @classmethod + def from_bundle(cls, bundle): + """ + Build a :class:`PerfAnalysis` from a :class:`ResultBundle` + + A :class:`ResultBundle` obtained from the execution of an + :class:`RTApp` workload can be used to get access to the rtapp + generated logfile information. + + + """ + return cls.from_dir(bundle=bundle) @staticmethod def _parse_df(logfile): @@ -160,15 +222,40 @@ class PerfAnalysis(AnalysisHelpers): """ return self.perf_data[task]['logfile'] - def get_df(self, task): + @memoized + def _start_time_from_bundle(self): + if not self._bundle: + return None + trace = getattr(self._bundle, 'trace', None) + if not trace: + return None + return trace.start + + @memoized + def get_df(self, task, start_time=None): """ Return the pandas dataframe with the performance data for the - specified task + specified task. A start time can be specified as a reference for the + first event, for example to align events to a given (see :class:`TraceView`). :param task: Name of the task that we want the performance dataframe of. :type task: str + + :param start_time: The first event time in seconds + :type start_time: float """ - return self.perf_data[task]['df'] + task_df = self.perf_data[task]['df'] + + if start_time is None: + start_time = self._start_time_from_bundle() + if not start_time: + return task_df + + # Let's keep a copy so that we can still access the original one + task_df = task_df.reset_index() + task_df['Time'] = task_df['Time'] + start_time + + return task_df.set_index('Time') def save_plot(self, figure, filepath=None, img_format=None): # If all logfiles are located in the same folder, use that folder @@ -183,6 +270,135 @@ class PerfAnalysis(AnalysisHelpers): default_dir = dirnames.pop() return self._save_plot(figure, default_dir, filepath, img_format) + @memoized + def df_activations(self, task, start_time=None): + """ + Get activation events + + :param task: Name of the task that we want the performance dataframe of. + :type task: str + + :param start_time: The first activation time in seconds + :type start_time: float + + :returns: A :class:`pandas.DataFrame` with index representing the start + time of an activation and these columns: + + * ``Run``: the running time of the activation + * ``End``: the end time of the activation + * ``Slack``: the slack of the activation + * ``WKPLatency``: the wakeup latency of the activation + * ``PerfIndex``: the performance index of the activation + """ + activations_df = self.get_df(task, start_time)[[ + 'Run', 'Slack', 'WKPLatency', 'PerfIndex']].copy() + activations_df['End'] = activations_df.index + activations_df['Run'] / 1e6 + + # Reorder to keep "End" as the second column + return activations_df[['Run', 'End', 'Slack', 'WKPLatency', 'PerfIndex']] + + def plot_activations(self, task, start_time=None, filepath=None, axis=None): + """ + Draw the task's activations colored bands + + :param task: Name of the task that we want the performance dataframe of. + :type task: str + + :param start_time: The first event time in seconds + :type start_time: float + + .. seealso:: :meth:`lisa.analysis.base.AnalysisHelpers.do_plot` + """ + activations_df = self.df_activations(task, start_time) + + # Compute intervals in which the task was running + bands = [(t, activations_df['End'][t]) for t in activations_df.index] + + def plotter(axis, local_fig): + label = 'Activations' + for (start, end) in bands: + axis.axvspan(start, end, alpha=0.1, facecolor='r', label=label) + if label: + label = None + + axis.legend() + + if local_fig: + axis.set_title("Task [{}] activations".format(task)) + + return self.do_plot(plotter, filepath, axis) + + @memoized + def df_phases(self, task, start_time=None): + """ + Get phases actual start times and durations + + :param task: Name of the task that we want the phases dataframe of + :type task: def __str__(self): + + :param start_time: The first activation time in seconds + :type start_time: float + + :returns: A :class:`pandas.DataFrame` with index representing the + start time of a phase and these column: + + * ``Duration``: the measured phase duration. + * ``CPeriod``: the configured activation periods for the phase + * ``CRun``: the configured activation run time for the phase + + """ + activation_df = self.get_df(task, start_time) + phase_df = activation_df[['CRun', 'CPeriod']].copy() + + # Shift Run and Periods for phase changes detection + phase_df['PRun'] = phase_df['CRun'].shift(1) + phase_df['PPeriod'] = phase_df['CPeriod'].shift(1) + + # Keep only new phase start events + phase_df = phase_df[(phase_df.CRun != phase_df.PRun) | + (phase_df.CPeriod != phase_df.PPeriod)] + + # Compute duration of each phase, last one requires a "special" treatment + durations = list(phase_df.index[1:] - phase_df.index[:-1]) + last_phase_duration = activation_df.index[-1] - phase_df.index[-1] + \ + activation_df.iloc[-1].Period / 1e6 + + # Compute phase durations + phase_df.loc[:,'Duration'] = durations + [last_phase_duration] + + return phase_df[['Duration', 'CPeriod', 'CRun']] + + def plot_phases(self, task, start_time=None, filepath=None, axis=None): + """ + Draw the task's phases colored bands + + :param task: Name of the task that we want the performance dataframe of. + :type task: str + + :param start_time: The first event time in seconds + :type start_time: float + + .. seealso:: :meth:`lisa.analysis.base.AnalysisHelpers.do_plot` + """ + phases_df = self.df_phases(task, start_time) + + # Compute phases intervals + bands = [(t, t + phases_df['Duration'][t]) for t in phases_df.index] + + def plotter(axis, local_fig): + for idx, (start, end) in enumerate(bands): + color = self.get_next_color(axis) + label = 'Phase_{}:{}'.format(int(phases_df.iloc[idx].CRun /1e3), + int(phases_df.iloc[idx].CPeriod / 1e3)) + axis.axvspan(start, end, alpha=0.1, facecolor=color, label=label) + + axis.legend() + + if local_fig: + axis.set_title("Task [{}] phases".format(task)) + + return self.do_plot(plotter, filepath, axis) + def plot_perf(self, task, **kwargs): """ Plot the performance Index