def plotter(axes, local_fig): for cpu in cpus: axis = axes[cpu] if len(cpus) > 1 else axes # Add CPU utilization axis.set_title('CPU{}'.format(cpu)) for signal in signals: df = self.df_cpus_signal(signal) df = df[df['cpu'] == cpu] df = df_refit_index(df, start, end) df[signal].plot(ax=axis, drawstyle='steps-post', alpha=0.4) self.trace.analysis.cpus.plot_orig_capacity(cpu, axis=axis) # Add capacities data if available if self.trace.has_events('cpu_capacity'): df = self.trace.df_events('cpu_capacity') df = df[df["__cpu"] == cpu] if len(df): data = df[['capacity', 'tip_capacity']] data = df_refit_index(data, start, end) data.plot(ax=axis, style=['m', '--y'], drawstyle='steps-post') # Add overutilized signal to the plot plot_overutilized = self.trace.analysis.status.plot_overutilized if self.trace.has_events(plot_overutilized.used_events): plot_overutilized(axis=axis) axis.set_ylim(0, 1100) axis.set_xlim(start, end) axis.legend()
def plotter(axes, local_fig): axes = axes if len(cpus) > 1 else itertools.repeat(axes) for cpu, axis in zip(cpus, axes): # Add CPU utilization axis.set_title(f'CPU{cpu}') for signal in signals: df = self.df_cpus_signal(signal, cpus=[cpu]) df = df_refit_index(df, window=window) df[signal].plot(ax=axis, drawstyle='steps-post', alpha=0.4) self.trace.analysis.cpus.plot_orig_capacity(cpu, axis=axis) # Add capacities data if available try: df = self.df_cpus_signal('capacity', cpus=[cpu]) except MissingTraceEventError: pass else: if len(df): data = df[['capacity']] data = df_refit_index(data, window=window) data.plot(ax=axis, style=['m', '--y'], drawstyle='steps-post') # Add overutilized signal to the plot plot_overutilized = self.trace.analysis.status.plot_overutilized if self.trace.has_events(plot_overutilized.used_events): plot_overutilized(axis=axis) axis.set_ylim(0, 1100) axis.legend()
def plot_tasks_wakeups_heatmap(self, xbins=100, colormap=None, axis=None, local_fig=None, **kwargs): """ Plot tasks wakeups heatmap :param xbins: Number of x-axis bins, i.e. in how many slices should time be arranged :type xbins: int :param colormap: The name of a colormap (see https://matplotlib.org/users/colormaps.html), or a Colormap object :type colormap: str or matplotlib.colors.Colormap """ df = self.trace.df_events("sched_wakeup") df = df_refit_index(df, self.trace.start, self.trace.end) fig, axis = self._plot_cpu_heatmap( df.index, df.target_cpu, xbins, "Number of wakeups", cmap=colormap, **kwargs, ) axis.set_title("Tasks wakeups over time") return axis
def plot_cpu_cooling_states(self, cpu: CPU, axis, local_fig): """ Plot the state evolution of a cpufreq cooling device :param cpu: The CPU. Whole clusters can be controlled as a single cooling device, they will be plotted as long this CPU belongs to the cluster. :type cpu: int """ window = self.trace.window df = self.df_cpufreq_cooling_state([cpu]) df = df_refit_index(df, window=window) cdev_name = f"CPUs {mask_to_list(df.cpus.unique()[0])}" series = series_refit_index(df['cdev_state'], window=window) series.plot(drawstyle="steps-post", ax=axis, label=f"\"{cdev_name}\"") axis.legend() if local_fig: axis.grid(True) axis.set_title("cpufreq cooling devices status") axis.yaxis.set_major_locator(MaxNLocator(integer=True)) axis.grid(axis='y')
def plot_thermal_zone_temperature(self, thermal_zone_id: int, axis, local_fig): """ Plot temperature of thermal zones (all by default) :param thermal_zone_id: ID of the zone :type thermal_zone_id: int """ window = self.trace.window df = self.df_thermal_zones_temperature() df = df[df.id == thermal_zone_id] df = df_refit_index(df, window=window) tz_name = df.thermal_zone.unique()[0] series = series_refit_index(df['temp'], window=window) series.plot(drawstyle="steps-post", ax=axis, label=f"Thermal zone \"{tz_name}\"") axis.legend() if local_fig: axis.grid(True) axis.set_title("Temperature evolution") axis.set_ylabel("Temperature (°C.10e3)")
def plot_thermal_zone_temperature(self, thermal_zone_id, axis, local_fig): """ Plot temperature of thermal zones (all by default) :param thermal_zone_id: ID of the zone :type thermal_zone_id: int """ start = self.trace.start end = self.trace.end df = self.df_thermal_zones_temperature() df = df[df.id == thermal_zone_id] df = df_refit_index(df, start, end) tz_name = df.thermal_zone.unique()[0] df.temp.plot(drawstyle="steps-post", ax=axis, label="Thermal zone \"{}\"".format(tz_name)) axis.legend() if local_fig: axis.grid(True) axis.set_title("Temperature evolution") axis.set_ylabel("Temperature (°C.10e3)") axis.set_xlim(start, end)
def plot_overutilized(self, axis, local_fig): """ Draw the system's overutilized status as colored bands """ df = self.df_overutilized() if not df.empty: df = df_refit_index(df, window=self.trace.window) # Compute intervals in which the system is reported to be overutilized bands = [(t, df['len'][t], df['overutilized'][t]) for t in df.index] color = self.get_next_color(axis) label = "Overutilized" for (start, delta, overutilized) in bands: if not overutilized: continue end = start + delta axis.axvspan(start, end, alpha=0.2, facecolor=color, label=label) if label: label = None axis.legend() if local_fig: axis.set_title("System-wide overutilized status")
def plot_task_required_capacity(self, task, **kwargs): """ Plot the minimum required capacity of a task :param task: The name or PID of the task :type task: str or int """ pid = self.trace.get_task_pid(task) start = self.trace.start end = self.trace.end df = self.df_tasks_signal('required_capacity') df = df[df.pid == pid] df = df_refit_index(df, start, end) # Build task names (there could be multiple, during the task lifetime) task_name = 'Task ({}:{})'.format(pid, self.trace.get_task_by_pid(pid)) def plotter(axis, local_fig): df["required_capacity"].plot(drawstyle='steps-post', ax=axis) axis.legend() axis.grid(True) if local_fig: axis.set_title(task_name) axis.set_ylim(0, 1100) axis.set_xlim(start, end) axis.set_ylabel('Utilization') axis.set_xlabel('Time (s)') return self.do_plot(plotter, height=8, **kwargs)
def _get_trace_df(self): task = self.rtapp_task_ids_map['task'][0] # There is no CPU selection when we're going back from preemption. # Setting preempted_value=1 ensures that it won't count as a new # activation. df = self.trace.ana.tasks.df_task_activation(task, preempted_value=1) df = df_refit_index(df, window=self.trace.window) df = df[['active', 'cpu']] df['activation_start'] = df['active'] == 1 df_freq = self.trace.ana.frequency.df_cpus_frequency() df_freq = df_freq[['cpu', 'frequency']] df_freq = df_freq.pivot(index=None, columns='cpu', values='frequency') df_freq.reset_index(inplace=True) df_freq.set_index('Time', inplace=True) df = df.merge(df_freq, how='outer', left_index=True, right_index=True) # Merge with df_freq will bring NaN in the activation column. We do not # want to ffill() them. df['activation_start'].fillna(value=False, inplace=True) # Ensures that frequency values are propogated through the entire # DataFrame, as it is possible that no frequency event occur # during a phase. df.ffill(inplace=True) return df
def plot_task_signals(self, task, axis, local_fig, signals=['util', 'load']): """ Plot the task-related load-tracking signals :param task: The name or PID of the task :type task: str or int :param signals: List of signals to plot. :type signals: list(str) """ pid = self.trace.get_task_pid(task) start = self.trace.start end = self.trace.end for signal in signals: df = self.df_tasks_signal(signal) df = df[df.pid == pid] df = df_refit_index(df, start, end) df[signal].plot(ax=axis, drawstyle='steps-post', alpha=0.4) plot_overutilized = self.trace.analysis.status.plot_overutilized if self.trace.has_events(plot_overutilized.used_events): plot_overutilized(axis=axis) axis.set_title('Load-tracking signals of task "{}"'.format(task)) axis.legend() axis.grid(True) axis.set_xlim(start, end)
def make_fig(name, df_getter, label): df = df_getter(task) if df.empty: self.logger.warning(f"No data to plot for {name}") else: df = df_refit_index(df, window=self.trace.window) return self._plot_markers(df, label)
def plot_dev_freq_cooling_states(self, device, axis, local_fig): """ Plot the state evolution of a devfreq cooling device :param device: The devfreq devices to consider :type device: str """ start = self.trace.start end = self.trace.end df = self.df_devfreq_cooling_state([device]) df = df_refit_index(df, start, end) df.cdev_state.plot(drawstyle="steps-post", ax=axis, label="Device \"{}\"".format(device)) axis.legend() if local_fig: axis.grid(True) axis.set_title("devfreq cooling devices status") axis.yaxis.set_major_locator(MaxNLocator(integer=True)) axis.grid(axis='y') axis.set_xlim(start, end)
def df_tasks_runtime(self): """ DataFrame of the time each task spent in TASK_ACTIVE (:class:`TaskState`) :returns: a :class:`pandas.DataFrame` with: * PIDs as index * A ``comm`` column (the name of the task) * A ``runtime`` column (the time that task spent running) """ runtimes = {} for task, pid_df in self._df_tasks_states(): pid = task.pid # Make sure to only look at the relevant portion of the dataframe # with the window, since we are going to make a time-based sum pid_df = df_refit_index(pid_df, window=self.trace.window) pid_df = df_add_delta(pid_df) # Resolve the comm to the last name of the PID in that window comms = pid_df['comm'].unique() comm = comms[-1] pid_df = pid_df[pid_df['curr_state'] == TaskState.TASK_ACTIVE] runtimes[pid] = (pid_df['delta'].sum(skipna=True), comm) df = pd.DataFrame.from_dict(runtimes, orient="index", columns=["runtime", 'comm']) df.index.name = "pid" df.sort_values(by="runtime", ascending=False, inplace=True) return df
def plot_cpu_cooling_states(self, cpu, axis, local_fig): """ Plot the state evolution of a cpufreq cooling device :param cpu: The CPU. Whole clusters can be controlled as a single cooling device, they will be plotted as long this CPU belongs to the cluster. :type cpu: int """ start = self.trace.start end = self.trace.end df = self.df_cpufreq_cooling_state([cpu]) df = df_refit_index(df, start, end) cdev_name = "CPUs {}".format(mask_to_list(df.cpus.unique()[0])) df.cdev_state.plot(drawstyle="steps-post", ax=axis, label="\"{}\"".format(cdev_name)) axis.legend() if local_fig: axis.grid(True) axis.set_title("cpufreq cooling devices status") axis.yaxis.set_major_locator(MaxNLocator(integer=True)) axis.grid(axis='y') axis.set_xlim(start, end)
def plot_task_required_capacity(self, task: TaskID, axis=None, **kwargs): """ Plot the minimum required capacity of a task :param task: The name or PID of the task, or a tuple ``(pid, comm)`` :type task: str or int or tuple """ window = self.trace.window task_ids = self.trace.get_task_ids(task) df = self.df_tasks_signal('required_capacity') df = df_filter_task_ids(df, task_ids) df = df_refit_index(df, window=window) # Build task names (there could be multiple, during the task lifetime) task_name = f"Task ({', '.join(map(str, task_ids))})" def plotter(axis, local_fig): df["required_capacity"].plot(drawstyle='steps-post', ax=axis) axis.legend() axis.grid(True) if local_fig: axis.set_title(task_name) axis.set_ylim(0, 1100) axis.set_ylabel('Utilization') axis.set_xlabel('Time (s)') return self.do_plot(plotter, height=8, axis=axis, **kwargs)
def plot_task_signals(self, task: TaskID, axis, local_fig, signals: TypedList[str] = ['util', 'load']): """ Plot the task-related load-tracking signals :param task: The name or PID of the task, or a tuple ``(pid, comm)`` :type task: str or int or tuple :param signals: List of signals to plot. :type signals: list(str) """ window = self.trace.window task = self.trace.get_task_id(task, update=False) for signal in signals: df = self.df_task_signal(task, signal) df = df_refit_index(df, window=window) df[signal].plot(ax=axis, drawstyle='steps-post', alpha=0.4) plot_overutilized = self.trace.analysis.status.plot_overutilized if self.trace.has_events(plot_overutilized.used_events): plot_overutilized(axis=axis) axis.set_title(f'Load-tracking signals of task {task}') axis.legend() axis.grid(True)
def _get_task_cpu_df(self): """ Get a DataFrame mapping task names to the CPU they ran on Use the sched_switch trace event to find which CPU each task ran on. Does not reflect idleness - tasks not running are shown as running on the last CPU they woke on. :returns: A Pandas DataFrame with a column for each task, showing the CPU that the task was "on" at each moment in time """ def task_cpu(task): return task.comm, self.trace.ana.tasks.df_task_activation( task=task)['cpu'] df = pd.DataFrame( dict( task_cpu(task_ids[0]) for task, task_ids in self.rtapp_task_ids_map.items())) df.fillna(method='ffill', inplace=True) df.dropna(inplace=True) df = df_deduplicate(df, consecutives=True, keep='first') # Ensure the index is refitted so that integrals work as expected df = df_refit_index(df, window=self.trace.window) return df
def df_cpu_idle_state_residency(self, cpu): """ Compute time spent by a given CPU in each idle state. :param cpu: CPU ID :type cpu: int :returns: a :class:`pandas.DataFrame` with: * Idle states as index * A ``time`` column (The time spent in the idle state) """ idle_df = self.df_cpu_idle(cpu) # Ensure accurate time-based sum of state deltas idle_df = df_refit_index(idle_df, window=self.trace.window) # For each state, sum the time spent in it idle_df = df_add_delta(idle_df) residency = { cols['state']: state_df['delta'].sum() for cols, state_df in df_split_signals(idle_df, ['state']) } df = pd.DataFrame.from_dict(residency, orient='index', columns=['time']) df.index.name = 'idle_state' return df
def _plot_signal(signal): df = self.df_task_signal(task, signal) df = df_refit_index(df, window=window) return plot_signal( df[signal], name=signal, ).options(dict(Curve=dict(alpha=0.5)), )
def _get_expected_task_utils_df(self): """ Get a DataFrame with the *expected* utilization of each task over time. :param nrg_model: EnergyModel used to computed the expected utilization :type nrg_model: EnergyModel :returns: A Pandas DataFrame with a column for each task, showing how the utilization of that task varies over time .. note:: The timestamps to match the beginning and end of each rtapp phase are taken from the trace. """ tasks_map = self.rtapp_tasks_map rtapp_profile = self.rtapp_profile def task_util(task, wlgen_task): task_list = tasks_map[task] assert len(task_list) == 1 task = task_list[0] df = self.trace.analysis.rta.df_phases(task, wlgen_profile=rtapp_profile) df = df[df['properties'].transform(lambda phase: phase['meta']['from_test'])] def get_phase_max_util(phase): wload = phase['wload'] # Take into account the duty cycle of the phase avg = wload.unscaled_duty_cycle_pct( plat_info=self.plat_info, ) * PELT_SCALE / 100 # Also take into account the period and the swing of PELT # around its "average" swing = pelt_swing( period=wload.period, duty_cycle=wload.duty_cycle_pct / 100, kind='above', ) return avg + swing phases_util = { phase.get('name'): get_phase_max_util(phase) for phase in wlgen_task.phases if phase['meta']['from_test'] } expected_util = df['phase'].map(phases_util) return task, expected_util cols = dict( task_util(task, wlgen_task) for task, wlgen_task in rtapp_profile.items() ) df = pd.DataFrame(cols) df.fillna(method='ffill', inplace=True) df.dropna(inplace=True) # Ensure the index is refitted so that integrals work as expected df = df_refit_index(df, window=self.trace.window) return df
def plotter(axis, local_fig): freq_axis, state_axis = axis freq_axis.get_figure().suptitle('Peripheral frequency', y=.97, fontsize=16, horizontalalignment='center') freq = self.df_peripheral_clock_effective_rate(clk) freq = df_refit_index(freq, start, end) # Plot frequency information (set rate) freq_axis.set_title("Clock frequency for " + clk) set_rate = freq['rate'].dropna() rate_axis_lib = 0 if len(set_rate) > 0: rate_axis_lib = set_rate.max() set_rate.plot(style=['b--'], ax=freq_axis, drawstyle='steps-post', alpha=0.4, label="clock_set_rate value") freq_axis.hlines(set_rate.iloc[-1], set_rate.index[-1], end, linestyle='--', color='b', alpha=0.4) else: logger.warning('No clock_set_rate events to plot') # Plot frequency information (effective rate) eff_rate = freq['effective_rate'].dropna() if len(eff_rate) > 0 and eff_rate.max() > 0: rate_axis_lib = max(rate_axis_lib, eff_rate.max()) eff_rate.plot(style=['b-'], ax=freq_axis, drawstyle='steps-post', alpha=1.0, label="Effective rate (with on/off)") freq_axis.hlines(eff_rate.iloc[-1], eff_rate.index[-1], end, linestyle='-', color='b', alpha=1.0) else: logger.warning('No effective frequency events to plot') freq_axis.set_ylim(0, rate_axis_lib * 1.1) freq_axis.set_xlim(start, end) freq_axis.set_xlabel('') freq_axis.grid(True) freq_axis.legend() def mhz(x, pos): return '{:1.2f} MHz'.format(x*1e-6) freq_axis.get_yaxis().set_major_formatter(FuncFormatter(mhz)) on = freq[freq.state == 1] state_axis.hlines([0] * len(on), on['start'], on['start'] + on['len'], linewidth = 10.0, label='clock on', color='green') off = freq[freq.state == 0] state_axis.hlines([0] * len(off), off['start'], off['start'] + off['len'], linewidth = 10.0, label='clock off', color='red') # Plot time period that the clock state was unknown from the trace indeterminate = pd.concat([on, off]).sort_index() if indeterminate.empty: indet_range_max = end else: indet_range_max = indeterminate.index[0] state_axis.hlines(0, 0, indet_range_max, linewidth = 1.0, label='indeterminate clock state', linestyle='--') state_axis.legend(bbox_to_anchor=(0., 1.02, 1., 0.102), loc=3, ncol=3, mode='expand') state_axis.set_yticks([]) state_axis.set_xlabel('seconds') state_axis.set_xlim(start, end)
def ensure_last_rectangle(df): # Make sure we will draw the last rectangle, which could be # critical for tasks that are never sleeping if df.empty: return df else: return df_refit_index(df, window=(None, df.index[-1] + df['duration'].iat[-1]))
def plot_bands(df, column, label): df = df_refit_index(df, self.trace.start, self.trace.end) bands = [(t, df[column][t]) for t in df.index] color = self.get_next_color(axis) for idx, (start, duration) in enumerate(bands): if idx > 0: label = None end = start + duration axis.axvspan(start, end, facecolor=color, alpha=0.5, label=label)
def plot_cpu_frequencies(self, cpu: CPU, axis, local_fig, average: bool = True): """ Plot frequency for the specified CPU :param cpu: The CPU for which to plot frequencies :type cpus: int :param average: If ``True``, add a horizontal line which is the frequency average. :type average: bool If ``sched_overutilized`` events are available, the plots will also show the intervals of time where the system was overutilized. """ logger = self.get_logger() df = self.df_cpu_frequency(cpu) if "freqs" in self.trace.plat_info: frequencies = self.trace.plat_info['freqs'][cpu] else: logger.info("Estimating CPU{} frequencies from trace".format(cpu)) frequencies = sorted(list(df.frequency.unique())) logger.debug("Estimated frequencies: {}".format(frequencies)) avg = self.get_average_cpu_frequency(cpu) logger.info("Average frequency for CPU{} : {:.3f} GHz".format( cpu, avg / 1e6)) df = df_refit_index(df, window=self.trace.window) df['frequency'].plot(ax=axis, drawstyle='steps-post') if average and avg > 0: axis.axhline(avg, color=self.get_next_color(axis), linestyle='--', label="average") plot_overutilized = self.trace.analysis.status.plot_overutilized if self.trace.has_events(plot_overutilized.used_events): plot_overutilized(axis=axis) axis.set_ylabel('Frequency (Hz)') axis.set_ylim(frequencies[0] * 0.9, frequencies[-1] * 1.1) axis.legend() if local_fig: axis.set_xlabel('Time') axis.set_title('Frequency of CPU{}'.format(cpu)) axis.grid(True)
def _get_expected_task_utils_df(self): """ Get a DataFrame with the *expected* utilization of each task over time. :param nrg_model: EnergyModel used to computed the expected utilization :type nrg_model: EnergyModel :returns: A Pandas DataFrame with a column for each task, showing how the utilization of that task varies over time .. note:: The timestamps to match the beginning and end of each rtapp phase are taken from the trace. """ tasks_map = self.rtapp_tasks_map def task_util(task, wlgen_task): task_list = tasks_map[task] assert len(task_list) == 1 task = task_list[0] df = self.trace.analysis.rta.df_phases(task) phases = wlgen_task.phases duty_cycles = { i: phase.duty_cycle_pct for i, phase in enumerate(phases) } # TODO: remove that once we have named phases to skip the buffer phase # Shift by one to take into account the buffer phase duty_cycles = { i+1: duty_cycle for i, duty_cycle in duty_cycles.items() } # The buffer phase inherits its duty cycle from the next phase duty_cycles[0] = duty_cycles[1] expected_util = df['phase'].map(duty_cycles) expected_util *= PELT_SCALE / 100 return task, expected_util cols = dict( task_util(task, wlgen_task) for task, wlgen_task in self.rtapp_profile.items() ) df = pd.DataFrame(cols) df.fillna(method='ffill', inplace=True) df.dropna(inplace=True) # Ensure the index is refitted so that integrals work as expected df = df_refit_index(df, window=self.trace.window) return df
def df_cluster_idle_state_residency(self, cluster): """ Compute time spent by a given cluster in each idle state. :param cluster: list of CPU IDs :type cluster: list(int) :returns: a :class:`pandas.DataFrame` with: * Idle states as index * A ``time`` column (The time spent in the idle state) """ idle_df = self.df_cpu_idle() # Create a dataframe with a column per CPU cols = { cpu: group['state'] for cpu, group in idle_df.groupby( 'cpu', sort=False, observed=True, ) if cpu in cluster } cpus_df = pd.DataFrame(cols, index=idle_df.index) cpus_df.fillna(method='ffill', inplace=True) # Ensure accurate time-based sum of state deltas. This will extrapolate # the known cluster_state both to the left and the right. cpus_df = df_refit_index(cpus_df, window=self.trace.window) # Each core in a cluster can be in a different idle state, but the # cluster lies in the idle state with lowest ID, that is the shallowest # idle state among the idle states of its CPUs cluster_state = cpus_df.min(axis='columns') cluster_state.name = 'cluster_state' df = cluster_state.to_frame() # For each state transition, sum the time spent in it df_add_delta(df, inplace=True) # For each cluster state, take the sum of the delta column. # The resulting dataframe is indexed by group keys (cluster_state). residency = df.groupby('cluster_state', sort=False, observed=True)['delta'].sum() residency.name = 'time' residency = residency.to_frame() residency.index.name = 'idle_state' return residency
def plot_dev_freq_cooling_states(self, device: str): """ Plot the state evolution of a devfreq cooling device :param device: The devfreq devices to consider :type device: str """ df = self.df_devfreq_cooling_state([device]) df = df_refit_index(df, window=self.trace.window) return plot_signal( df['cdev_state'], name=f'Device "{device}"', ).options( title='devfreq cooling devices status' )
def plot_runtimes(self, task: TaskID): """ Plot the :meth:`lisa.analysis.latency.LatencyAnalysis.df_runtimes` of a task :param task: The task's name or PID :type task: int or str or tuple(int, str) """ df = self.df_runtimes(task) df = df_refit_index(df, window=self.trace.window) name = f'Per-activation runtimes of task {task}' return ( self._plot_markers(df, name) * self._plot_overutilized() ).options( title=name, )
def plot_bands(df, column, label): df = df_refit_index(df, window=self.trace.window) if df.empty: return _hv_neutral() return hv.Overlay( [ hv.VSpan( start, start + duration, label=label, ).options( alpha=0.5, ) for start, duration in df[[column]].itertuples() ] )
def plot_runtimes(self, task: TaskID, axis, local_fig): """ Plot the :meth:`lisa.analysis.latency.LatencyAnalysis.df_runtimes` of a task :param task: The task's name or PID :type task: int or str or tuple(int, str) """ df = self.df_runtimes(task) df = df_refit_index(df, window=self.trace.window) df.plot(style='+', ax=axis) plot_overutilized = self.trace.analysis.status.plot_overutilized if self.trace.has_events(plot_overutilized.used_events): plot_overutilized(axis=axis) axis.set_title('Per-activation runtimes of task "{}"'.format(task))