From f5ebef9c0507bf3168c12dfc08381ebe15b38d0e Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Thu, 23 Jun 2016 16:14:33 +0100 Subject: [PATCH 1/4] submodules: pull updates for memoized Latest updates on devlib memoized preserve functions signature, docstring, path to the file where the function is implemented, and so on. Signed-off-by: Michele Di Giorgio --- libs/devlib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/devlib b/libs/devlib index cf791d1e6..fa20e7c28 160000 --- a/libs/devlib +++ b/libs/devlib @@ -1 +1 @@ -Subproject commit cf791d1e6444ad2e8b0899e08ca93708a5ed59a2 +Subproject commit fa20e7c28d9b004e2feedd7cb472a542bf6d481a -- GitLab From 2e7b1c9fcd405f24b69742e100097ee56308387d Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Fri, 17 Jun 2016 20:02:35 +0100 Subject: [PATCH 2/4] libs/utils/trace_analysis: compute cluster and CPU active signals It is useful to have a signal that tells when a cluster or a CPU is active (i.e. non-idle) or idle. Signed-off-by: Michele Di Giorgio --- libs/utils/trace_analysis.py | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/libs/utils/trace_analysis.py b/libs/utils/trace_analysis.py index e63a20beb..a58a35b7f 100644 --- a/libs/utils/trace_analysis.py +++ b/libs/utils/trace_analysis.py @@ -25,10 +25,14 @@ import pylab as pl import re import sys import trappy +import operator +from devlib.utils.misc import memoized # Configure logging import logging +NON_IDLE_STATE = 4294967295 + class TraceAnalysis(object): def __init__(self, trace, tasks=None, plotsdir=None, prefix=''): @@ -839,3 +843,66 @@ class TraceAnalysis(object): # Save generated plots into datadir figname = '{}/{}schedtune_conf.png'.format(self.plotsdir, self.prefix) pl.savefig(figname, bbox_inches='tight') + + @memoized + def getCPUActiveSignal(self, cpu): + """ + Build a square wave representing the active (i.e. non-idle) CPU time, + i.e.: + cpu_active[t] == 1 if at least one CPU is reported to be + non-idle by CPUFreq at time t + cpu_active[t] == 0 otherwise + + :param cpu: CPU ID + :type cpu: int + """ + if not self.trace.hasEvents('cpu_idle'): + logging.warn('Events [cpu_idle] not found, '\ + 'cannot compute CPU active signal!') + return None + + idle_df = self.trace.df('cpu_idle') + cpu_df = idle_df[idle_df.cpu_id == cpu] + + cpu_active = cpu_df.state.apply( + lambda s: 1 if s == NON_IDLE_STATE else 0 + ) + + start_time = 0.0 + if not self.trace.ftrace.normalized_time: + start_time = self.trace.ftrace.basetime + if cpu_active.index[0] != start_time: + entry_0 = pd.Series(cpu_active.iloc[0] ^ 1, index=[start_time]) + cpu_active = pd.concat([entry_0, cpu_active]) + + return cpu_active + + @memoized + def getClusterActiveSignal(self, cluster): + """ + Build a square wave representing the active (i.e. non-idle) cluster + time, i.e.: + cluster_active[t] == 1 if at least one CPU is reported to be + non-idle by CPUFreq at time t + cluster_active[t] == 0 otherwise + + :param cluster: list of CPU IDs belonging to a cluster + :type cluster: list(int) + """ + cpu_active = {} + for cpu in cluster: + cpu_active[cpu] = self.getCPUActiveSignal(cpu) + + active = pd.DataFrame(cpu_active) + active.fillna(method='ffill', inplace=True) + + # Cluster active is the OR between the actives on each CPU + # belonging to that specific cluster + cluster_active = reduce( + operator.or_, + [cpu_active.astype(int) for _, cpu_active in + active.iteritems()] + ) + + return cluster_active + -- GitLab From 31c1c49cd6eec3bca6aee51b38b7042a884ceb61 Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Fri, 17 Jun 2016 20:04:59 +0100 Subject: [PATCH 3/4] libs/utils/trace_analysis: compute per-cluster and per-CPU frequency residency Frequency residency is the time spent by a cluster or a CPU at a certain frequency. Both active (i.e. non-idle) and total frequency residencies are computed. Signed-off-by: Michele Di Giorgio --- libs/utils/trace_analysis.py | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/libs/utils/trace_analysis.py b/libs/utils/trace_analysis.py index a58a35b7f..0eefba183 100644 --- a/libs/utils/trace_analysis.py +++ b/libs/utils/trace_analysis.py @@ -26,13 +26,17 @@ import re import sys import trappy import operator +from trappy.utils import listify from devlib.utils.misc import memoized +from collections import namedtuple # Configure logging import logging NON_IDLE_STATE = 4294967295 +ResidencyTime = namedtuple('ResidencyTime', ['total', 'active']) + class TraceAnalysis(object): def __init__(self, trace, tasks=None, plotsdir=None, prefix=''): @@ -906,3 +910,117 @@ class TraceAnalysis(object): return cluster_active + def _integrate_square_wave(self, sq_wave): + """ + Compute the integral of a square wave time series. + + :param sq_wave: square wave assuming only 1.0 and 0.0 values + :type sq_wave: :mod:`pandas.Series` + """ + sq_wave.iloc[-1] = 0.0 + # Compact signal to obtain only 1-0-1-0 sequences + comp_sig = sq_wave.loc[sq_wave.shift() != sq_wave] + # First value for computing the difference must be a 1 + if comp_sig.iloc[0] == 0.0: + return sum(comp_sig.iloc[2::2].index - comp_sig.iloc[1:-1:2].index) + else: + return sum(comp_sig.iloc[1::2].index - comp_sig.iloc[:-1:2].index) + + @memoized + def getClusterFrequencyResidency(self, cluster): + """ + Get a DataFrame with per cluster frequency residency, i.e. amount of + time spent at a given frequency in each cluster. + + :param cluster: this can be either a single CPU ID or a list of CPU IDs + belonging to a cluster or the cluster name as specified in the + platform description + :type cluster: str or int or list(int) + + :returns: namedtuple(ResidencyTime) - tuple of total and active time + dataframes + + :raises: KeyError + """ + if not self.trace.hasEvents('cpu_frequency'): + logging.warn('Events [cpu_frequency] not found, '\ + 'frequency residency computation not possible!') + return None + if not self.trace.hasEvents('cpu_idle'): + logging.warn('Events [cpu_idle] not found, '\ + 'frequency residency computation not possible!') + return None + + if isinstance(cluster, str): + try: + _cluster = self.platform['clusters'][cluster.lower()] + except KeyError: + logging.warn('%s cluster not found!', cluster) + return None + else: + _cluster = listify(cluster) + + freq_df = self.trace.df('cpu_frequency') + # Assumption: all CPUs in a cluster run at the same frequency, i.e. the + # frequency is scaled per-cluster not per-CPU. Hence, we can limit the + # cluster frequencies data to a single CPU. This assumption is verified + # by the Trace module when parsing the trace. + if len(_cluster) > 1 and not self.trace.freq_coherency: + logging.warn('Cluster frequency is NOT coherent,'\ + 'cannot compute residency!') + return None + cluster_freqs = freq_df[freq_df.cpu == _cluster[0]] + + ### Compute TOTAL Time ### + time_intervals = cluster_freqs.index[1:] - cluster_freqs.index[:-1] + total_time = pd.DataFrame({ + 'time' : time_intervals, + 'frequency' : [f/1000 for f in cluster_freqs.iloc[:-1].frequency] + }) + total_time = total_time.groupby(['frequency']).sum() + + ### Compute ACTIVE Time ### + cluster_active = self.getClusterActiveSignal(_cluster) + + # In order to compute the active time spent at each frequency we + # multiply 2 square waves: + # - cluster_active, a square wave of the form: + # cluster_active[t] == 1 if at least one CPU is reported to be + # non-idle by CPUFreq at time t + # cluster_active[t] == 0 otherwise + # - freq_active, square wave of the form: + # freq_active[t] == 1 if at time t the frequency is f + # freq_active[t] == 0 otherwise + available_freqs = sorted(cluster_freqs.frequency.unique()) + new_idx = sorted(cluster_freqs.index.tolist() + \ + cluster_active.index.tolist()) + cluster_freqs = cluster_freqs.reindex(new_idx, method='ffill') + cluster_active = cluster_active.reindex(new_idx, method='ffill') + nonidle_time = [] + for f in available_freqs: + freq_active = cluster_freqs.frequency.apply( + lambda x: 1 if x == f else 0 + ) + active_t = cluster_active * freq_active + # Compute total time by integrating the square wave + nonidle_time.append(self._integrate_square_wave(active_t)) + + active_time = pd.DataFrame({'time' : nonidle_time}, + index=[f/1000 for f in available_freqs]) + active_time.index.name = 'frequency' + return ResidencyTime(total_time, active_time) + + def getCPUFrequencyResidency(self, cpu): + """ + Get a DataFrame with per-CPU frequency residency, i.e. amount of + time CPU `cpu` spent at each frequency. Both total and active times + will be computed. + + :param cpu: CPU ID + :type cpu: int + + :returns: namedtuple(ResidencyTime) - tuple of total and active time + dataframes + """ + return self.getClusterFrequencyResidency(cpu) + -- GitLab From 354b56b4f5f237ad0cae16b73750038d6cbec1ca Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Fri, 17 Jun 2016 20:07:40 +0100 Subject: [PATCH 4/4] libs/utils/trace_analysis: add plots for per-CPU and per-cluster frequency residency Two plot modes are available thanks to the `pct` boolean argument. By default, `pct` is set to False, so total and active residencies are plotted in terms of time spent at a given frequency. By setting `pct` to True, total or active residencies for each frequency are plotted as a percentage relative to the sum of the time intervals computed by the getFrequencyResidency() functions. For example, suppose total_time is: In [1]: total_time Out [1]: time frequency 450 20 800 45 950 35 The sum of the time intervals is 100 and we would get a plot showing that 20% of the time is spent at 450 MHz, 45% at 800 MHz and 35% at 950 MHz. In case of percentage plots, it is possible to plot either ACTIVE or TOTAL residencies by properly setting the `active` argument. Signed-off-by: Michele Di Giorgio --- libs/utils/trace_analysis.py | 279 +++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/libs/utils/trace_analysis.py b/libs/utils/trace_analysis.py index 0eefba183..afb444f7c 100644 --- a/libs/utils/trace_analysis.py +++ b/libs/utils/trace_analysis.py @@ -36,6 +36,7 @@ import logging NON_IDLE_STATE = 4294967295 ResidencyTime = namedtuple('ResidencyTime', ['total', 'active']) +ResidencyData = namedtuple('ResidencyData', ['label', 'residency']) class TraceAnalysis(object): @@ -1024,3 +1025,281 @@ class TraceAnalysis(object): """ return self.getClusterFrequencyResidency(cpu) + def _plotFrequencyResidencyAbs(self, axes, residency, n_plots, + is_first, is_last, xmax, title=''): + """ + Private method to generate frequency residency plots. + + :param axes: axes over which to generate the plot + :type axes: matplotlib.axes.Axes + + :param residency: tuple of total and active time dataframes + :type residency: namedtuple(ResidencyTime) + + :param n_plots: total number of plots + :type n_plots: int + + :param is_first: if True this is the first plot + :type is_first: bool + + :param is_first: if True this is the last plot + :type is_first: bool + + :param xmax: x-axes higher bound + :param xmax: double + + :param title: title of this subplot + :type title: str + """ + yrange = 0.4 * max(6, len(residency.total)) * n_plots + residency.total.plot.barh(ax = axes, color='g', + legend=False, figsize=(16,yrange)) + residency.active.plot.barh(ax = axes, color='r', + legend=False, figsize=(16,yrange)) + + axes.set_xlim(0, 1.05*xmax) + axes.set_ylabel('Frequency [MHz]') + axes.set_title(title) + axes.grid(True) + if is_last: + axes.set_xlabel('Time [s]') + else: + axes.set_xticklabels([]) + + if is_first: + # Put title on top of the figure. As of now there is no clean way + # to make the title appear always in the same position in the + # figure because figure heights may vary between different + # platforms (different number of OPPs). Hence, we use annotation + legend_y = axes.get_ylim()[1] + axes.annotate('OPP Residency Time', xy=(0, legend_y), + xytext=(-50, 45), textcoords='offset points', + fontsize=18) + axes.annotate('GREEN: Total', xy=(0, legend_y), + xytext=(-50, 25), textcoords='offset points', + color='g', fontsize=14) + axes.annotate('RED: Active', xy=(0, legend_y), + xytext=(50, 25), textcoords='offset points', + color='r', fontsize=14) + + def _plotFrequencyResidencyPct(self, axes, residency_df, label, + n_plots, is_first, is_last, res_type): + """ + Private method to generate PERCENTAGE frequency residency plots. + + :param axes: axes over which to generate the plot + :type axes: matplotlib.axes.Axes + + :param residency_df: residency time dataframe + :type residency_df: :mod:`pandas.DataFrame` + + :param label: label to be used for percentage residency dataframe + :type label: str + + :param n_plots: total number of plots + :type n_plots: int + + :param is_first: if True this is the first plot + :type is_first: bool + + :param is_first: if True this is the last plot + :type is_first: bool + + :param res_type: type of residency, either TOTAL or ACTIVE + :type title: str + """ + # Compute sum of the time intervals + duration = residency_df.time.sum() + residency_pct = pd.DataFrame( + {label : residency_df.time.apply(lambda x: x*100/duration)}, + index=residency_df.index + ) + yrange = 3 * n_plots + residency_pct.T.plot.barh(ax=axes, stacked=True, figsize=(16, yrange)) + + axes.legend(loc='lower center', ncol=7) + axes.set_xlim(0, 100) + axes.grid(True) + if is_last: + axes.set_xlabel('Residency [%]') + else: + axes.set_xticklabels([]) + if is_first: + legend_y = axes.get_ylim()[1] + axes.annotate('OPP {} Residency Time'.format(res_type), + xy=(0, legend_y), xytext=(-50, 35), + textcoords='offset points', fontsize=18) + + def _plotFrequencyResidency(self, residencies, entity_name, xmax, + pct, active): + """ + Generate Frequency residency plots for the given entities. + + :param residencies: + :type residencies: namedtuple(ResidencyData) - tuple containing: + 1) as first element, a label to be used as subplot title + 2) as second element, a namedtuple(ResidencyTime) + + :param entity_name: name of the entity ('cpu' or 'cluster') used in the + figure name + :type entity_name: str + + :param xmax: upper bound of x-axes + :type xmax: double + + :param pct: plot residencies in percentage + :type pct: bool + + :param active: for percentage plot specify whether to plot active or + total time. Default is TOTAL time + :type active: bool + """ + n_plots = len(residencies) + gs = gridspec.GridSpec(n_plots, 1) + fig = plt.figure() + + figtype = "" + for idx, data in enumerate(residencies): + label = data[0] + r = data[1] + if r is None: + plt.close(fig) + return + + axes = fig.add_subplot(gs[idx]) + is_first = idx == 0 + is_last = idx+1 == n_plots + if pct and active: + self._plotFrequencyResidencyPct(axes, data.residency.active, + data.label, n_plots, + is_first, is_last, + 'ACTIVE') + figtype = "_pct_active" + continue + if pct: + self._plotFrequencyResidencyPct(axes, data.residency.total, + data.label, n_plots, + is_first, is_last, + 'TOTAL') + figtype = "_pct_total" + continue + + self._plotFrequencyResidencyAbs(axes, data.residency, + n_plots, is_first, + is_last, xmax, + title=data.label) + + figname = '{}/{}{}_freq_residency{}.png'\ + .format(self.plotsdir, self.prefix, entity_name, figtype) + + pl.savefig(figname, bbox_inches='tight') + + def plotCPUFrequencyResidency(self, cpus=None, pct=False, active=False): + """ + Plot per-CPU frequency residency. big CPUs are plotted first and then + LITTLEs. + + Requires the following trace events: + - cpu_frequency + - cpu_idle + + :param cpus: List of cpus. By default plot all CPUs + :type cpus: list(str) + + :param pct: plot residencies in percentage + :type pct: bool + + :param active: for percentage plot specify whether to plot active or + total time. Default is TOTAL time + :type active: bool + """ + if not self.trace.hasEvents('cpu_frequency'): + logging.warn('Events [cpu_frequency] not found, plot DISABLED!') + return + if not self.trace.hasEvents('cpu_idle'): + logging.warn('Events [cpu_idle] not found, plot DISABLED!') + return + + if cpus is None: + # Generate plots only for available CPUs + cpufreq_data = self.trace.df('cpu_frequency') + _cpus = range(cpufreq_data.cpu.max()+1) + else: + _cpus = listify(cpus) + + # Split between big and LITTLE CPUs ordered from higher to lower ID + _cpus.reverse() + big_cpus = [c for c in _cpus if c in self.platform['clusters']['big']] + little_cpus = [c for c in _cpus if c in + self.platform['clusters']['little']] + _cpus = big_cpus + little_cpus + + # Precompute active and total time for each CPU + residencies = [] + xmax = 0.0 + for c in _cpus: + r = self.getCPUFrequencyResidency(c) + residencies.append(ResidencyData('CPU{}'.format(c), r)) + + max_time = r.total.max().values[0] + if xmax < max_time: + xmax = max_time + + self._plotFrequencyResidency(residencies, 'cpu', xmax, pct, active) + + def plotClusterFrequencyResidency(self, clusters=None, + pct=False, active=False): + """ + Plot the frequency residency in a given cluster, i.e. the amount of + time cluster `cluster` spent at frequency `f_i`. By default, both 'big' + and 'LITTLE' clusters data are plotted. + + Requires the following trace events: + - cpu_frequency + - cpu_idle + + :param clusters: name of the clusters to be plotted (all of them by + default) + :type clusters: str ot list(str) + + :param pct: plot residencies in percentage + :type pct: bool + + :param active: for percentage plot specify whether to plot active or + total time. Default is TOTAL time + :type active: bool + """ + if not self.trace.hasEvents('cpu_frequency'): + logging.warn('Events [cpu_frequency] not found, plot DISABLED!') + return + if not self.trace.hasEvents('cpu_idle'): + logging.warn('Events [cpu_idle] not found, plot DISABLED!') + return + + # Assumption: all CPUs in a cluster run at the same frequency, i.e. the + # frequency is scaled per-cluster not per-CPU. Hence, we can limit the + # cluster frequencies data to a single CPU + if not self.trace.freq_coherency: + logging.warn('Cluster frequency is not coherent, plot DISABLED!') + return + + # Sanitize clusters + if clusters is None: + _clusters = self.platform['clusters'].keys() + else: + _clusters = listify(clusters) + + # Precompute active and total time for each cluster + residencies = [] + xmax = 0.0 + for c in _clusters: + r = self.getClusterFrequencyResidency( + self.platform['clusters'][c.lower()]) + residencies.append(ResidencyData('{} Cluster'.format(c), r)) + + max_time = r.total.max().values[0] + if xmax < max_time: + xmax = max_time + + self._plotFrequencyResidency(residencies, 'cluster', xmax, pct, active) + -- GitLab