Home | History | Annotate | Download | only in analysis
      1 # SPDX-License-Identifier: Apache-2.0
      2 #
      3 # Copyright (C) 2015, ARM Limited and contributors.
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
      6 # not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 # http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 
     18 """ Tasks Analysis Module """
     19 
     20 import matplotlib.gridspec as gridspec
     21 import matplotlib.pyplot as plt
     22 import numpy as np
     23 import pylab as pl
     24 import re
     25 
     26 from analysis_module import AnalysisModule
     27 from devlib.utils.misc import memoized
     28 from trappy.utils import listify
     29 
     30 
     31 class TasksAnalysis(AnalysisModule):
     32     """
     33     Support for Tasks signals analysis.
     34 
     35     :param trace: input Trace object
     36     :type trace: :mod:`libs.utils.Trace`
     37     """
     38 
     39     def __init__(self, trace):
     40         super(TasksAnalysis, self).__init__(trace)
     41 
     42 
     43 ###############################################################################
     44 # DataFrame Getter Methods
     45 ###############################################################################
     46 
     47     def _dfg_top_big_tasks(self, min_samples=100, min_utilization=None):
     48         """
     49         Tasks which had 'utilization' samples bigger than the specified
     50         threshold
     51 
     52         :param min_samples: minumum number of samples over the min_utilization
     53         :type min_samples: int
     54 
     55         :param min_utilization: minimum utilization used to filter samples
     56             default: capacity of a little cluster
     57         :type min_utilization: int
     58         """
     59         if not self._trace.hasEvents('sched_load_avg_task'):
     60             self._log.warning('Events [sched_load_avg_task] not found')
     61             return None
     62 
     63         if min_utilization is None:
     64             min_utilization = self._little_cap
     65 
     66         # Get utilization samples >= min_utilization
     67         df = self._dfg_trace_event('sched_load_avg_task')
     68         big_tasks_events = df[df.util_avg > min_utilization]
     69         if not len(big_tasks_events):
     70             self._log.warning('No tasks with with utilization samples > %d',
     71                               min_utilization)
     72             return None
     73 
     74         # Report the number of tasks which match the min_utilization condition
     75         big_tasks = big_tasks_events.pid.unique()
     76         self._log.info('%5d tasks with samples of utilization > %d',
     77                        len(big_tasks), min_utilization)
     78 
     79         # Compute number of samples above threshold
     80         big_tasks_stats = big_tasks_events.groupby('pid')\
     81                             .describe(include=['object'])
     82         big_tasks_stats = big_tasks_stats.unstack()['comm']\
     83                             .sort_values(by=['count'], ascending=False)
     84 
     85         # Filter for number of occurrences
     86         big_tasks_stats = big_tasks_stats[big_tasks_stats['count'] > min_samples]
     87         if not len(big_tasks_stats):
     88             self._log.warning('      but none with more than %d samples',
     89                               min_samples)
     90             return None
     91 
     92         self._log.info('      %d with more than %d samples',
     93                        len(big_tasks_stats), min_samples)
     94 
     95         # Add task name column
     96         big_tasks_stats['comm'] = big_tasks_stats.index.map(
     97             lambda pid: self._trace.getTaskByPid(pid))
     98 
     99         # Filter columns of interest
    100         big_tasks_stats = big_tasks_stats[['count', 'comm']]
    101         big_tasks_stats.rename(columns={'count': 'samples'}, inplace=True)
    102 
    103         return big_tasks_stats
    104 
    105     def _dfg_top_wakeup_tasks(self, min_wakeups=100):
    106         """
    107         Tasks which wakeup more frequently than a specified threshold.
    108 
    109         :param min_wakeups: minimum number of wakeups
    110         :type min_wakeups: int
    111         """
    112         if not self._trace.hasEvents('sched_wakeup'):
    113             self._log.warning('Events [sched_wakeup] not found')
    114             return None
    115 
    116         df = self._dfg_trace_event('sched_wakeup')
    117 
    118         # Compute number of wakeups above threshold
    119         wkp_tasks_stats = df.groupby('pid').describe(include=['object'])
    120         wkp_tasks_stats = wkp_tasks_stats.unstack()['comm']\
    121                           .sort_values(by=['count'], ascending=False)
    122 
    123         # Filter for number of occurrences
    124         wkp_tasks_stats = wkp_tasks_stats[
    125             wkp_tasks_stats['count'] > min_wakeups]
    126         if not len(df):
    127             self._log.warning('No tasks with more than %d wakeups',
    128                               len(wkp_tasks_stats))
    129             return None
    130         self._log.info('%5d tasks with more than %d wakeups',
    131                        len(df), len(wkp_tasks_stats))
    132 
    133         # Add task name column
    134         wkp_tasks_stats['comm'] = wkp_tasks_stats.index.map(
    135             lambda pid: self._trace.getTaskByPid(pid))
    136 
    137         # Filter columns of interest
    138         wkp_tasks_stats = wkp_tasks_stats[['count', 'comm']]
    139         wkp_tasks_stats.rename(columns={'count': 'samples'}, inplace=True)
    140 
    141         return wkp_tasks_stats
    142 
    143     def _dfg_rt_tasks(self, min_prio=100):
    144         """
    145         Tasks with RT priority
    146 
    147         NOTE: priorities uses scheduler values, thus: the lower the value the
    148               higher is the task priority.
    149               RT   Priorities: [  0..100]
    150               FAIR Priorities: [101..120]
    151 
    152         :param min_prio: minumum priority
    153         :type min_prio: int
    154         """
    155         if not self._trace.hasEvents('sched_switch'):
    156             self._log.warning('Events [sched_switch] not found')
    157             return None
    158 
    159         df = self._dfg_trace_event('sched_switch')
    160 
    161         # Filters tasks which have a priority bigger than threshold
    162         df = df[df.next_prio <= min_prio]
    163 
    164         # Filter columns of interest
    165         rt_tasks = df[['next_pid', 'next_prio']]
    166 
    167         # Remove all duplicateds
    168         rt_tasks = rt_tasks.drop_duplicates()
    169 
    170         # Order by priority
    171         rt_tasks.sort_values(by=['next_prio', 'next_pid'], ascending=True,
    172                              inplace=True)
    173         rt_tasks.rename(columns={'next_pid': 'pid', 'next_prio': 'prio'},
    174                         inplace=True)
    175 
    176         # Set PID as index
    177         rt_tasks.set_index('pid', inplace=True)
    178 
    179         # Add task name column
    180         rt_tasks['comm'] = rt_tasks.index.map(
    181             lambda pid: self._trace.getTaskByPid(pid))
    182 
    183         return rt_tasks
    184 
    185 
    186 ###############################################################################
    187 # Plotting Methods
    188 ###############################################################################
    189 
    190     def plotTasks(self, tasks, signals=None):
    191         """
    192         Generate a common set of useful plots for each of the specified tasks
    193 
    194         This method allows to filter which signals should be plot, if data are
    195         available in the input trace. The list of signals supported are:
    196         Tasks signals plot:
    197                 load_avg, util_avg, boosted_util, sched_overutilized
    198         Tasks residencies on CPUs:
    199                 residencies, sched_overutilized
    200         Tasks PELT signals:
    201                 load_sum, util_sum, period_contrib, sched_overutilized
    202 
    203         At least one of the previous signals must be specified to get a valid
    204         plot.
    205 
    206         Addidional custom signals can be specified and they will be represented
    207         in the "Task signals plots" if they represent valid keys of the task
    208         load/utilization trace event (e.g. sched_load_avg_task).
    209 
    210         Note:
    211             sched_overutilized: enable the plotting of overutilization bands on
    212                                 top of each subplot
    213             residencies: enable the generation of the CPUs residencies plot
    214 
    215         :param tasks: the list of task names and/or PIDs to plot.
    216                       Numerical PIDs and string task names can be mixed
    217                       in the same list.
    218         :type tasks: list(str) or list(int)
    219 
    220         :param signals: list of signals (and thus plots) to generate
    221                         default: all the plots and signals available in the
    222                         current trace
    223         :type signals: list(str)
    224         """
    225         if not signals:
    226             signals = ['load_avg', 'util_avg', 'boosted_util',
    227                        'sched_overutilized',
    228                        'load_sum', 'util_sum', 'period_contrib',
    229                        'residencies']
    230 
    231         # Check for the minimum required signals to be available
    232         if not self._trace.hasEvents('sched_load_avg_task'):
    233             self._log.warning('Events [sched_load_avg_task] not found, '
    234                               'plot DISABLED!')
    235             return
    236 
    237         # Defined list of tasks to plot
    238         if tasks and \
    239             not isinstance(tasks, str) and \
    240             not isinstance(tasks, list):
    241             raise ValueError('Wrong format for tasks parameter')
    242 
    243         if tasks:
    244             tasks_to_plot = listify(tasks)
    245         else:
    246             raise ValueError('No tasks to plot specified')
    247 
    248         # Compute number of plots to produce
    249         plots_count = 0
    250         plots_signals = [
    251                 # Fist plot: task's utilization
    252                 {'load_avg', 'util_avg', 'boosted_util'},
    253                 # Second plot: task residency
    254                 {'residencies'},
    255                 # Third plot: tasks's load
    256                 {'load_sum', 'util_sum', 'period_contrib'}
    257         ]
    258         hr = []
    259         ysize = 0
    260         for plot_id, signals_to_plot in enumerate(plots_signals):
    261             signals_to_plot = signals_to_plot.intersection(signals)
    262             if len(signals_to_plot):
    263                 plots_count = plots_count + 1
    264                 # Use bigger size only for the first plot
    265                 hr.append(3 if plot_id == 0 else 1)
    266                 ysize = ysize + (8 if plot_id else 4)
    267 
    268         # Grid
    269         gs = gridspec.GridSpec(plots_count, 1, height_ratios=hr)
    270         gs.update(wspace=0.1, hspace=0.1)
    271 
    272         # Build list of all PIDs for each task_name to plot
    273         pids_to_plot = []
    274         for task in tasks_to_plot:
    275             # Add specified PIDs to the list
    276             if isinstance(task, int):
    277                 pids_to_plot.append(task)
    278                 continue
    279             # Otherwise: add all the PIDs for task with the specified name
    280             pids_to_plot.extend(self._trace.getTaskByName(task))
    281 
    282         for tid in pids_to_plot:
    283             savefig = False
    284 
    285             task_name = self._trace.getTaskByPid(tid)
    286             self._log.info('Plotting [%d:%s]...', tid, task_name)
    287             plot_id = 0
    288 
    289             # For each task create a figure with plots_count plots
    290             plt.figure(figsize=(16, ysize))
    291             plt.suptitle('Task Signals',
    292                          y=.94, fontsize=16, horizontalalignment='center')
    293 
    294             # Plot load and utilization
    295             signals_to_plot = {'load_avg', 'util_avg', 'boosted_util'}
    296             signals_to_plot = list(signals_to_plot.intersection(signals))
    297             if len(signals_to_plot) > 0:
    298                 axes = plt.subplot(gs[plot_id, 0])
    299                 axes.set_title('Task [{0:d}:{1:s}] Signals'
    300                                .format(tid, task_name))
    301                 plot_id = plot_id + 1
    302                 is_last = (plot_id == plots_count)
    303                 self._plotTaskSignals(axes, tid, signals, is_last)
    304                 savefig = True
    305 
    306             # Plot CPUs residency
    307             signals_to_plot = {'residencies'}
    308             signals_to_plot = list(signals_to_plot.intersection(signals))
    309             if len(signals_to_plot) > 0:
    310                 axes = plt.subplot(gs[plot_id, 0])
    311                 axes.set_title(
    312                     'Task [{0:d}:{1:s}] Residency (green: LITTLE, red: big)'
    313                     .format(tid, task_name)
    314                 )
    315                 plot_id = plot_id + 1
    316                 is_last = (plot_id == plots_count)
    317                 if 'sched_overutilized' in signals:
    318                     signals_to_plot.append('sched_overutilized')
    319                 self._plotTaskResidencies(axes, tid, signals_to_plot, is_last)
    320                 savefig = True
    321 
    322             # Plot PELT signals
    323             signals_to_plot = {'load_sum', 'util_sum', 'period_contrib'}
    324             signals_to_plot = list(signals_to_plot.intersection(signals))
    325             if len(signals_to_plot) > 0:
    326                 axes = plt.subplot(gs[plot_id, 0])
    327                 axes.set_title('Task [{0:d}:{1:s}] PELT Signals'
    328                                .format(tid, task_name))
    329                 plot_id = plot_id + 1
    330                 if 'sched_overutilized' in signals:
    331                     signals_to_plot.append('sched_overutilized')
    332                 self._plotTaskPelt(axes, tid, signals_to_plot)
    333                 savefig = True
    334 
    335             if not savefig:
    336                 self._log.warning('Nothing to plot for %s', task_name)
    337                 continue
    338 
    339             # Save generated plots into datadir
    340             if isinstance(task_name, list):
    341                 task_name = re.sub('[:/]', '_', task_name[0])
    342             else:
    343                 task_name = re.sub('[:/]', '_', task_name)
    344             figname = '{}/{}task_util_{}_{}.png'\
    345                       .format(self._trace.plots_dir, self._trace.plots_prefix,
    346                               tid, task_name)
    347             pl.savefig(figname, bbox_inches='tight')
    348 
    349     def plotBigTasks(self, max_tasks=10, min_samples=100,
    350                      min_utilization=None):
    351         """
    352         For each big task plot utilization and show the smallest cluster
    353         capacity suitable for accommodating task utilization.
    354 
    355         :param max_tasks: maximum number of tasks to consider
    356         :type max_tasks: int
    357 
    358         :param min_samples: minumum number of samples over the min_utilization
    359         :type min_samples: int
    360 
    361         :param min_utilization: minimum utilization used to filter samples
    362             default: capacity of a little cluster
    363         :type min_utilization: int
    364         """
    365 
    366         # Get PID of big tasks
    367         big_frequent_task_df = self._dfg_top_big_tasks(
    368             min_samples, min_utilization)
    369         if max_tasks > 0:
    370             big_frequent_task_df = big_frequent_task_df.head(max_tasks)
    371         big_frequent_task_pids = big_frequent_task_df.index.values
    372 
    373         big_frequent_tasks_count = len(big_frequent_task_pids)
    374         if big_frequent_tasks_count == 0:
    375             self._log.warning('No big/frequent tasks to plot')
    376             return
    377 
    378         # Get the list of events for all big frequent tasks
    379         df = self._dfg_trace_event('sched_load_avg_task')
    380         big_frequent_tasks_events = df[df.pid.isin(big_frequent_task_pids)]
    381 
    382         # Define axes for side-by-side plottings
    383         fig, axes = plt.subplots(big_frequent_tasks_count, 1,
    384                                  figsize=(16, big_frequent_tasks_count*4))
    385         plt.subplots_adjust(wspace=0.1, hspace=0.2)
    386 
    387         plot_idx = 0
    388         for pid, group in big_frequent_tasks_events.groupby('pid'):
    389 
    390             # # Build task names (there could be multiple, during the task lifetime)
    391             task_name = 'Task [%d:%s]'.format(pid, self._trace.getTaskByPid(pid))
    392 
    393             # Plot title
    394             if big_frequent_tasks_count == 1:
    395                 ax = axes
    396             else:
    397                 ax = axes[plot_idx]
    398             ax.set_title(task_name)
    399 
    400             # Left axis: utilization
    401             ax = group.plot(y=['util_avg', 'min_cluster_cap'],
    402                             style=['r.', '-b'],
    403                             drawstyle='steps-post',
    404                             linewidth=1,
    405                             ax=ax)
    406             ax.set_xlim(self._trace.x_min, self._trace.x_max)
    407             ax.set_ylim(0, 1100)
    408             ax.set_ylabel('util_avg')
    409             ax.set_xlabel('')
    410             ax.grid(True)
    411             self._trace.analysis.status.plotOverutilized(ax)
    412 
    413             plot_idx += 1
    414 
    415         ax.set_xlabel('Time [s]')
    416 
    417         self._log.info('Tasks which have been a "utilization" of %d for at least %d samples',
    418                        self._little_cap, min_samples)
    419 
    420     def plotWakeupTasks(self, max_tasks=10, min_wakeups=0, per_cluster=False):
    421         """
    422         Show waking up tasks over time and newly forked tasks in two separate
    423         plots.
    424 
    425         :param max_tasks: maximum number of tasks to consider
    426         :param max_tasks: int
    427 
    428         :param min_wakeups: minimum number of wakeups of each task
    429         :type min_wakeups: int
    430 
    431         :param per_cluster: if True get per-cluster wakeup events
    432         :type per_cluster: bool
    433         """
    434         if per_cluster is True and \
    435            not self._trace.hasEvents('sched_wakeup_new'):
    436             self._log.warning('Events [sched_wakeup_new] not found, '
    437                               'plots DISABLED!')
    438             return
    439         elif  not self._trace.hasEvents('sched_wakeup') and \
    440               not self._trace.hasEvents('sched_wakeup_new'):
    441             self._log.warning('Events [sched_wakeup, sched_wakeup_new] not found, '
    442                               'plots DISABLED!')
    443             return
    444 
    445         # Define axes for side-by-side plottings
    446         fig, axes = plt.subplots(2, 1, figsize=(14, 5))
    447         plt.subplots_adjust(wspace=0.2, hspace=0.3)
    448 
    449         if per_cluster:
    450 
    451             # Get per cluster wakeup events
    452             df = self._dfg_trace_event('sched_wakeup_new')
    453             big_frequent = df.target_cpu.isin(self._big_cpus)
    454             ntbc = df[big_frequent]
    455             ntbc_count = len(ntbc)
    456             little_frequent = df.target_cpu.isin(self._little_cpus)
    457             ntlc = df[little_frequent];
    458             ntlc_count = len(ntlc)
    459 
    460             self._log.info('%5d tasks forked on big cluster    (%3.1f %%)',
    461                            ntbc_count,
    462                            100. * ntbc_count / (ntbc_count + ntlc_count))
    463             self._log.info('%5d tasks forked on LITTLE cluster (%3.1f %%)',
    464                            ntlc_count,
    465                            100. * ntlc_count / (ntbc_count + ntlc_count))
    466 
    467             ax = axes[0]
    468             ax.set_title('Tasks Forks on big CPUs');
    469             ntbc.pid.plot(style=['g.'], ax=ax);
    470             ax.set_xlim(self._trace.x_min, self._trace.x_max);
    471             ax.set_xticklabels([])
    472             ax.set_xlabel('')
    473             ax.grid(True)
    474             self._trace.analysis.status.plotOverutilized(ax)
    475 
    476             ax = axes[1]
    477             ax.set_title('Tasks Forks on LITTLE CPUs');
    478             ntlc.pid.plot(style=['g.'], ax=ax);
    479             ax.set_xlim(self._trace.x_min, self._trace.x_max);
    480             ax.grid(True)
    481             self._trace.analysis.status.plotOverutilized(ax)
    482 
    483             return
    484 
    485         # Keep events of defined big tasks
    486         wkp_task_pids = self._dfg_top_wakeup_tasks(min_wakeups)
    487         if len(wkp_task_pids):
    488             wkp_task_pids = wkp_task_pids.index.values[:max_tasks]
    489             self._log.info('Plotting %d frequent wakeup tasks',
    490                            len(wkp_task_pids))
    491 
    492         ax = axes[0]
    493         ax.set_title('Tasks WakeUps Events')
    494         df = self._dfg_trace_event('sched_wakeup')
    495         if len(df):
    496             df = df[df.pid.isin(wkp_task_pids)]
    497             df.pid.astype(int).plot(style=['b.'], ax=ax)
    498             ax.set_xlim(self._trace.x_min, self._trace.x_max)
    499             ax.set_xticklabels([])
    500             ax.set_xlabel('')
    501             ax.grid(True)
    502             self._trace.analysis.status.plotOverutilized(ax)
    503 
    504         ax = axes[1]
    505         ax.set_title('Tasks Forks Events')
    506         df = self._dfg_trace_event('sched_wakeup_new')
    507         if len(df):
    508             df = df[df.pid.isin(wkp_task_pids)]
    509             df.pid.astype(int).plot(style=['r.'], ax=ax)
    510             ax.set_xlim(self._trace.x_min, self._trace.x_max)
    511             ax.grid(True)
    512             self._trace.analysis.status.plotOverutilized(ax)
    513 
    514     def plotBigTasksVsCapacity(self, min_samples=1,
    515                                min_utilization=None, big_cluster=True):
    516         """
    517         Draw a plot that shows whether tasks are placed on the correct cluster
    518         based on their utilization and cluster capacity. Green dots mean the
    519         task was placed on the correct cluster, Red means placement was wrong
    520 
    521         :param min_samples: minumum number of samples over the min_utilization
    522         :type min_samples: int
    523 
    524         :param min_utilization: minimum utilization used to filter samples
    525             default: capacity of a little cluster
    526         :type min_utilization: int
    527 
    528         :param big_cluster:
    529         :type big_cluster: bool
    530         """
    531 
    532         if not self._trace.hasEvents('sched_load_avg_task'):
    533             self._log.warning('Events [sched_load_avg_task] not found')
    534             return
    535         if not self._trace.hasEvents('cpu_frequency'):
    536             self._log.warning('Events [cpu_frequency] not found')
    537             return
    538 
    539         if big_cluster:
    540             cluster_correct = 'big'
    541             cpus = self._big_cpus
    542         else:
    543             cluster_correct = 'LITTLE'
    544             cpus = self._little_cpus
    545 
    546         # Get all utilization update events
    547         df = self._dfg_trace_event('sched_load_avg_task')
    548 
    549         # Keep events of defined big tasks
    550         big_task_pids = self._dfg_top_big_tasks(
    551             min_samples, min_utilization)
    552         if big_task_pids is not None:
    553             big_task_pids = big_task_pids.index.values
    554             df = df[df.pid.isin(big_task_pids)]
    555         if not df.size:
    556             self._log.warning('No events for tasks with more then %d utilization '
    557                               'samples bigger than %d, plots DISABLED!')
    558             return
    559 
    560         fig, axes = plt.subplots(2, 1, figsize=(14, 5))
    561         plt.subplots_adjust(wspace=0.2, hspace=0.3)
    562 
    563         # Add column of expected cluster depending on:
    564         # a) task utilization value
    565         # b) capacity of the selected cluster
    566         bu_bc = ((df['util_avg'] > self._little_cap) &
    567                  (df['cpu'].isin(self._big_cpus)))
    568         su_lc = ((df['util_avg'] <= self._little_cap) &
    569                  (df['cpu'].isin(self._little_cpus)))
    570 
    571         # The Cluster CAPacity Matches the UTILization (ccap_mutil) iff:
    572         # - tasks with util_avg  > little_cap are running on a BIG cpu
    573         # - tasks with util_avg <= little_cap are running on a LITTLe cpu
    574         df.loc[:,'ccap_mutil'] = np.select([(bu_bc | su_lc)], [True], False)
    575 
    576         df_freq = self._dfg_trace_event('cpu_frequency')
    577         df_freq = df_freq[df_freq.cpu == cpus[0]]
    578 
    579         ax = axes[0]
    580         ax.set_title('Tasks Utilization vs Allocation')
    581         for ucolor, umatch in zip('gr', [True, False]):
    582             cdata = df[df['ccap_mutil'] == umatch]
    583             if len(cdata) > 0:
    584                 cdata['util_avg'].plot(ax=ax,
    585                         style=[ucolor+'.'], legend=False)
    586         ax.set_xlim(self._trace.x_min, self._trace.x_max)
    587         ax.set_xticklabels([])
    588         ax.set_xlabel('')
    589         ax.grid(True)
    590         self._trace.analysis.status.plotOverutilized(ax)
    591 
    592         ax = axes[1]
    593         ax.set_title('Frequencies on "{}" cluster'.format(cluster_correct))
    594         df_freq['frequency'].plot(style=['-b'], ax=ax, drawstyle='steps-post')
    595         ax.set_xlim(self._trace.x_min, self._trace.x_max);
    596         ax.grid(True)
    597         self._trace.analysis.status.plotOverutilized(ax)
    598 
    599         legend_y = axes[0].get_ylim()[1]
    600         axes[0].annotate('Utilization-Capacity Matches',
    601                          xy=(0, legend_y),
    602                          xytext=(-50, 45), textcoords='offset points',
    603                          fontsize=18)
    604         axes[0].annotate('Task schduled (green) or not (red) on min cluster',
    605                          xy=(0, legend_y),
    606                          xytext=(-50, 25), textcoords='offset points',
    607                          fontsize=14)
    608 
    609 
    610 ###############################################################################
    611 # Utility Methods
    612 ###############################################################################
    613 
    614     def _plotTaskSignals(self, axes, tid, signals, is_last=False):
    615         """
    616         For task with ID `tid` plot the specified signals.
    617 
    618         :param axes: axes over which to generate the plot
    619         :type axes: :mod:`matplotlib.axes.Axes`
    620 
    621         :param tid: task ID
    622         :type tid: int
    623 
    624         :param signals: signals to be plot
    625         :param signals: list(str)
    626 
    627         :param is_last: if True this is the last plot
    628         :type is_last: bool
    629         """
    630         # Get dataframe for the required task
    631         util_df = self._dfg_trace_event('sched_load_avg_task')
    632 
    633         # Plot load and util
    634         signals_to_plot = set(signals).difference({'boosted_util'})
    635         for signal in signals_to_plot:
    636             if signal not in util_df.columns:
    637                 continue
    638             data = util_df[util_df.pid == tid][signal]
    639             data.plot(ax=axes, drawstyle='steps-post', legend=True)
    640 
    641         # Plot boost utilization if available
    642         if 'boosted_util' in signals and \
    643            self._trace.hasEvents('sched_boost_task'):
    644             boost_df = self._dfg_trace_event('sched_boost_task')
    645             data = boost_df[boost_df.pid == tid][['boosted_util']]
    646             if len(data):
    647                 data.plot(ax=axes, style=['y-'], drawstyle='steps-post')
    648             else:
    649                 task_name = self._trace.getTaskByPid(tid)
    650                 self._log.warning('No "boosted_util" data for task [%d:%s]',
    651                                   tid, task_name)
    652 
    653         # Add Capacities data if avilable
    654         if 'nrg_model' in self._platform:
    655             nrg_model = self._platform['nrg_model']
    656             max_lcap = nrg_model['little']['cpu']['cap_max']
    657             max_bcap = nrg_model['big']['cpu']['cap_max']
    658             tip_lcap = 0.8 * max_lcap
    659             tip_bcap = 0.8 * max_bcap
    660             self._log.debug(
    661                 'LITTLE capacity tip/max: %d/%d, big capacity tip/max: %d/%d',
    662                 tip_lcap, max_lcap, tip_bcap, max_bcap
    663             )
    664             axes.axhline(tip_lcap, color='y', linestyle=':', linewidth=2)
    665             axes.axhline(max_lcap, color='y', linestyle='--', linewidth=2)
    666             axes.axhline(tip_bcap, color='r', linestyle=':', linewidth=2)
    667             axes.axhline(max_bcap, color='r', linestyle='--', linewidth=2)
    668 
    669         axes.set_ylim(0, 1100)
    670         axes.set_xlim(self._trace.x_min, self._trace.x_max)
    671         axes.grid(True)
    672         if not is_last:
    673             axes.set_xticklabels([])
    674             axes.set_xlabel('')
    675         if 'sched_overutilized' in signals:
    676             self._trace.analysis.status.plotOverutilized(axes)
    677 
    678     def _plotTaskResidencies(self, axes, tid, signals, is_last=False):
    679         """
    680         For task with ID `tid` plot residency information.
    681 
    682         :param axes: axes over which to generate the plot
    683         :type axes: :mod:`matplotlib.axes.Axes`
    684 
    685         :param tid: task ID
    686         :type tid: int
    687 
    688         :param signals: signals to be plot
    689         :param signals: list(str)
    690 
    691         :param is_last: if True this is the last plot
    692         :type is_last: bool
    693         """
    694         util_df = self._dfg_trace_event('sched_load_avg_task')
    695 
    696         if 'cluster' in util_df:
    697             data = util_df[util_df.pid == tid][['cluster', 'cpu']]
    698             for ccolor, clabel in zip('gr', ['LITTLE', 'big']):
    699                 cdata = data[data.cluster == clabel]
    700                 if len(cdata) > 0:
    701                     cdata.plot(ax=axes, style=[ccolor+'+'], legend=False)
    702 
    703         # Y Axis - placeholders for legend, acutal CPUs. topmost empty lane
    704         cpus = [str(n) for n in range(self._platform['cpus_count'])]
    705         ylabels = [''] + cpus
    706         axes.set_yticklabels(ylabels)
    707         axes.set_ylim(-1, len(cpus))
    708         axes.set_ylabel('CPUs')
    709         # X Axis
    710         axes.set_xlim(self._trace.x_min, self._trace.x_max)
    711 
    712         axes.grid(True)
    713         if not is_last:
    714             axes.set_xticklabels([])
    715             axes.set_xlabel('')
    716         if 'sched_overutilized' in signals:
    717             self._trace.analysis.status.plotOverutilized(axes)
    718 
    719     def _plotTaskPelt(self, axes, tid, signals):
    720         """
    721         For task with ID `tid` plot PELT-related signals.
    722 
    723         :param axes: axes over which to generate the plot
    724         :type axes: :mod:`matplotlib.axes.Axes`
    725 
    726         :param tid: task ID
    727         :type tid: int
    728 
    729         :param signals: signals to be plot
    730         :param signals: list(str)
    731         """
    732         util_df = self._dfg_trace_event('sched_load_avg_task')
    733         data = util_df[util_df.pid == tid][['load_sum',
    734                                             'util_sum',
    735                                             'period_contrib']]
    736         data.plot(ax=axes, drawstyle='steps-post')
    737         axes.set_xlim(self._trace.x_min, self._trace.x_max)
    738         axes.ticklabel_format(style='scientific', scilimits=(0, 0),
    739                               axis='y', useOffset=False)
    740         axes.grid(True)
    741         if 'sched_overutilized' in signals:
    742             self._trace.analysis.status.plotOverutilized(axes)
    743 
    744 # vim :set tabstop=4 shiftwidth=4 expandtab
    745