def get_trace_cpu_util(self): """ Get the per-phase average CPU utilization read from the trace :returns: A dict of the shape {cpu : {phase_id : trace_util}} """ cpu_util = { cpu: {phase_id: 0 for phase_id in range(self.nr_phases)} for cpu in self.cpus } df = self.trace.analysis.load_tracking.df_cpus_signal('util') phase_start = self.trace.start for phase in range(self.nr_phases): # Start looking at signals once they should've converged start = phase_start + UTIL_AVG_CONVERGENCE_TIME_S # Trim the end a bit, otherwise we could have one or two events # from the next phase end = phase_start + self.phases_durations[phase] * .9 phase_df = df[start:end] phase_duration = end - start for cpu in self.cpus: util = phase_df[phase_df.cpu == cpu].util cpu_util[cpu][phase] = area_under_curve(util) / ( phase_duration) phase_start += self.phases_durations[phase] return cpu_util
def _test_task_signal(self, signal_name, allowed_error_pct, trace, cpu, task_name, capacity): # Use utilization signal for both load and util, since they should be # proportionnal in the test environment we setup exp_signal = self.get_expected_util_avg(trace, cpu, task_name, capacity) signal_df = self.get_task_sched_signal(trace, cpu, task_name, signal_name) signal = signal_df[UTIL_AVG_CONVERGENCE_TIME_S:][signal_name] signal_mean = area_under_curve(signal) / (signal.index[-1] - signal.index[0]) # Since load is now CPU invariant in recent kernel versions, we don't # rescale it back. To match the old behavior, that line is # needed: # exp_signal /= (self.plat_info['cpu-capacities'][cpu] / UTIL_SCALE) kernel_version = self.plat_info['kernel']['version'] if (signal_name == 'load' and kernel_version.parts[:2] < (5, 1)): self.get_logger().warning( 'Load signal is assumed to be CPU invariant, which is true for recent mainline kernels, but may be wrong for {}' .format(kernel_version, )) ok = self.is_almost_equal(exp_signal, signal_mean, allowed_error_pct) return ok, exp_signal, signal_mean
def _test_task_placement(self, experiment, tasks): """ Test that task placement was energy-efficient Use :meth:get_expected_power_df and :meth:get_power_df to estimate optimal and observed power usage for task placements of the experiment's workload. Assert that the observed power does not exceed the optimal power by more than :attr:energy_est_threshold_pct percents. """ exp_power = self.get_expected_power_df(experiment) est_power = self.get_power_df(experiment) exp_energy = area_under_curve(exp_power.sum(axis=1), method='rect') est_energy = area_under_curve(est_power.sum(axis=1), method='rect') msg = 'Estimated {} bogo-Joules to run workload, expected {}'.format( est_energy, exp_energy) threshold = exp_energy * (1 + (self.energy_est_threshold_pct / 100.)) self.assertLess(est_energy, threshold, msg=msg)
def _test_task_placement(self, experiment, tasks): """ Test that task placement was energy-efficient Use :meth:get_expected_power_df and :meth:get_power_df to estimate optimal and observed power usage for task placements of the experiment's workload. Assert that the observed power does not exceed the optimal power by more than :attr:energy_est_threshold_pct percents. """ exp_power = self.get_expected_power_df(experiment) est_power = self.get_power_df(experiment) exp_energy = area_under_curve(exp_power.sum(axis=1), method='rect') est_energy = area_under_curve(est_power.sum(axis=1), method='rect') msg = 'Estimated {} bogo-Joules to run workload, expected {}'.format( est_energy, exp_energy) threshold = exp_energy * (1 + (self.energy_est_threshold_pct / 100.)) self.assertLess(est_energy, threshold, msg=msg)
def get_signal_mean(self, experiment, signal, ignore_first_s=UTIL_AVG_CONVERGENCE_TIME): """ Get the mean of a scheduler signal for the experiment's task Ignore the first `ignore_first_s` seconds of the signal. """ (wload_start, wload_end) = self.get_window(experiment) window = (wload_start + ignore_first_s, wload_end) signal = self.get_sched_task_signals(experiment, [signal])[signal] signal = select_window(signal, window) return area_under_curve(signal) / (window[1] - window[0])
def test_task_placement(self, energy_est_threshold_pct=5, nrg_model: EnergyModel = None, capacity_margin_pct=20) -> ResultBundle: """ Test that task placement was energy-efficient :param nrg_model: Allow using an alternate EnergyModel instead of ``nrg_model``` :type nrg_model: EnergyModel :param energy_est_threshold_pct: Allowed margin for estimated vs optimal task placement energy cost :type energy_est_threshold_pct: int Compute optimal energy consumption (energy-optimal task placement) and compare to energy consumption estimated from the trace. Check that the estimated energy does not exceed the optimal energy by more than ``energy_est_threshold_pct``` percents. """ nrg_model = nrg_model or self.nrg_model exp_power = self._get_expected_power_df(nrg_model, capacity_margin_pct) est_power = self._get_estimated_power_df(nrg_model) exp_energy = area_under_curve(exp_power.sum(axis=1), method='rect') est_energy = area_under_curve(est_power.sum(axis=1), method='rect') msg = 'Estimated {} bogo-Joules to run workload, expected {}'.format( est_energy, exp_energy) threshold = exp_energy * (1 + (energy_est_threshold_pct / 100)) passed = est_energy < threshold res = ResultBundle.from_bool(passed) res.add_metric("estimated energy", est_energy, 'bogo-joules') res.add_metric("energy threshold", threshold, 'bogo-joules') return res
def avg_cpu_freq(cls): """ To calculate average frequency, first obtain the area under the series of frequency transitions for the period of interest. We do this by recomputing the index on the frequency series derived from the cpu_frequency trace objects and then using area_under_curve from bart. Once we have the area, we can divide it by the time span to obtain average frequency. """ df = cls.ftrace_obj.cpu_frequency.data_frame cpu_df = df[df.cpu == cls.test_cpu] freq_s = cpu_df.frequency old_index = list(freq_s[cls.task_start_time:cls.task_end_time].index) # the new index should run from the start to the end time, including # any events which occurred in between new_index = [ cls.task_start_time ] + old_index + [ cls.task_end_time ] windowed_freq_s = freq_s.reindex(index=new_index, method='pad') # avg_freq is area / time for the window of interest area = area_under_curve(windowed_freq_s, method='rect') return int((area + 0.5) / (cls.task_end_time - cls.task_start_time))
def report(self, out_dir, out_energy='energy.json', out_samples='samples.csv'): self._instrument.stop() csv_path = os.path.join(out_dir, out_samples) csv_data = self._instrument.get_data(csv_path) with open(csv_path) as f: # Each column in the CSV will be headed with 'SITE_measure' # (e.g. 'BAT_power'). Convert that to a list of ('SITE', 'measure') # tuples, then pass that as the `names` parameter to read_csv to get # a nested column index. None of devlib's standard measurement types # have '_' in the name so this use of rsplit should be fine. exp_headers = [c.label for c in csv_data.channels] headers = f.readline().strip().split(',') if set(headers) != set(exp_headers): raise ValueError( 'Unexpected headers in CSV from devlib instrument. ' 'Expected {}, found {}'.format(sorted(headers), sorted(exp_headers))) columns = [tuple(h.rsplit('_', 1)) for h in headers] # Passing `names` means read_csv doesn't expect to find headers in # the CSV (i.e. expects every line to hold data). This works because # we have already consumed the first line of `f`. df = pd.read_csv(f, names=columns) sample_period = 1. / self._instrument.sample_rate_hz df.index = np.linspace(0, sample_period * len(df), num=len(df)) if df.empty: raise RuntimeError('No energy data collected') channels_nrg = {} for site, measure in df: if measure == 'power': channels_nrg[site] = area_under_curve(df[site]['power']) # Dump data as JSON file nrg_file = '{}/{}'.format(out_dir, out_energy) with open(nrg_file, 'w') as ofile: json.dump(channels_nrg, ofile, sort_keys=True, indent=4) return EnergyReport(channels_nrg, nrg_file, df)
def conditional_compare(self, condition, **kwargs): """Conditionally compare two signals The conditional comparison of signals has two components: - **Value Coefficient** :math:`\\alpha_{v}` which measures the difference in values of of the two signals when the condition is true: .. math:: \\alpha_{v} = \\frac{area\_under\_curve(S_A\ |\ C(t)\ is\ true)} {area\_under\_curve(S_B\ |\ C(t)\ is\ true)} \\\\ \\alpha_{v} = \\frac{\int S_A(\{t\ |\ C(t)\})dt}{\int S_B(\{t\ |\ C(t)\})dt} - **Time Coefficient** :math:`\\alpha_{t}` which measures the time during which the condition holds true. .. math:: \\alpha_{t} = \\frac{T_{valid}}{T_{total}} :param condition: A condition that returns a truth value and obeys the grammar syntax :: "event_x:sig_a > event_x:sig_b" :type condition: str :param method: The method for area calculation. This can be any of the integration methods supported in `numpy` or `rect` :type param: str :param step: The step behaviour for area and time summation calculation :type step: str Consider the two signals A and B as follows: .. code:: A = [0, 0, 0, 3, 3, 0, 0, 0] B = [0, 0, 2, 2, 2, 2, 1, 1] .. code:: A = xxxx 3 *xxxx*xxxx+ B = ---- | | 2 *----*----*----+ | | | 1 | | *----*----+ | | | 0 *x-x-*x-x-+xxxx+ +xxxx*xxxx+ 0 1 2 3 4 5 6 7 The condition: .. math:: A > B is valid between T=3 and T=5. Therefore, .. math:: \\alpha_v=1.5 \\\\ \\alpha_t=\\frac{2}{7} :returns: There are two cases: - **Pivoted Signals** :: { "pivot_name" : { "pval_1" : (v1,t1), "pval_2" : (v2, t2) } } - **Non Pivoted Signals** The tuple of :math:`(\\alpha_v, \\alpha_t)` """ if self._pivot: result = {self._pivot: {}} mask = self._parser.solve(condition) step = kwargs.get("step", "post") for pivot_val in self._pivot_vals: a_piv = self._a_data[pivot_val] b_piv = self._b_data[pivot_val] area = area_under_curve(a_piv[mask[pivot_val]], **kwargs) try: area /= area_under_curve(b_piv[mask[pivot_val]], **kwargs) except ZeroDivisionError: area = float("nan") duration = min(a_piv.last_valid_index(), b_piv.last_valid_index()) duration -= max(a_piv.first_valid_index(), b_piv.first_valid_index()) duration = interval_sum(mask[pivot_val], step=step) / duration if self._pivot: result[self._pivot][pivot_val] = area, duration else: result = area, duration return result
def conditional_compare(self, condition, **kwargs): """Conditionally compare two signals The conditional comparison of signals has two components: - **Value Coefficient** :math:`\\alpha_{v}` which measures the difference in values of of the two signals when the condition is true: .. math:: \\alpha_{v} = \\frac{area\_under\_curve(S_A\ |\ C(t)\ is\ true)} {area\_under\_curve(S_B\ |\ C(t)\ is\ true)} \\\\ \\alpha_{v} = \\frac{\int S_A(\{t\ |\ C(t)\})dt}{\int S_B(\{t\ |\ C(t)\})dt} - **Time Coefficient** :math:`\\alpha_{t}` which measures the time during which the condition holds true. .. math:: \\alpha_{t} = \\frac{T_{valid}}{T_{total}} :param condition: A condition that returns a truth value and obeys the grammar syntax :: "event_x:sig_a > event_x:sig_b" :type condition: str :param method: The method for area calculation. This can be any of the integration methods supported in `numpy` or `rect` :type param: str :param step: The step behaviour for area and time summation calculation :type step: str Consider the two signals A and B as follows: .. code:: A = [0, 0, 0, 3, 3, 0, 0, 0] B = [0, 0, 2, 2, 2, 2, 1, 1] .. code:: A = xxxx 3 *xxxx*xxxx+ B = ---- | | 2 *----*----*----+ | | | 1 | | *----*----+ | | | 0 *x-x-*x-x-+xxxx+ +xxxx*xxxx+ 0 1 2 3 4 5 6 7 The condition: .. math:: A > B is valid between T=3 and T=5. Therefore, .. math:: \\alpha_v=1.5 \\\\ \\alpha_t=\\frac{2}{7} :returns: There are two cases: - **Pivoted Signals** :: { "pivot_name" : { "pval_1" : (v1,t1), "pval_2" : (v2, t2) } } - **Non Pivoted Signals** The tuple of :math:`(\\alpha_v, \\alpha_t)` """ if self._pivot: result = {self._pivot: {}} mask = self._parser.solve(condition) step = kwargs.get("step", "post") for pivot_val in self._pivot_vals: a_piv = self._a_data[pivot_val] b_piv = self._b_data[pivot_val] area = area_under_curve(a_piv[mask[pivot_val]], **kwargs) try: area /= area_under_curve(b_piv[mask[pivot_val]], **kwargs) except ZeroDivisionError: area = float("nan") duration = min(a_piv.last_valid_index(), b_piv.last_valid_index()) duration -= max(a_piv.first_valid_index(), b_piv.first_valid_index()) duration = interval_sum(mask[pivot_val], step=step) / duration if self._pivot: result[self._pivot][pivot_val] = area, duration else: result = area, duration return result
def _compute_energy(self, df): channels_nrg = {} for site, measure in df: if measure == 'power': channels_nrg[site] = area_under_curve(df[site]['power']) return channels_nrg
def _test_group_util(self, group): if 'sched_load_se' not in self.trace.available_events: raise ValueError('No sched_load_se events. ' 'Does the kernel support them?') if 'sched_load_cfs_rq' not in self.trace.available_events: raise ValueError('No sched_load_cfs_rq events. ' 'Does the kernel support them?') if 'sched_switch' not in self.trace.available_events: raise ValueError('No sched_switch events. ' 'Does the kernel support them?') task_util_df = self.trace.data_frame.trace_event('sched_load_se') tg_util_df = self.trace.data_frame.trace_event('sched_load_cfs_rq') sw_df = self.trace.data_frame.trace_event('sched_switch') tg = None for se in self.root_group.iter_nodes(): if se.name == group: tg = se if tg is None: raise ValueError('{} taskgroup does not exist.'.format(group)) # Only consider the time interval where the signal should be stable tasks_names = [se.name for se in tg.iter_nodes() if se.is_task] tasks_sw_df = sw_df[sw_df.next_comm.isin(tasks_names)] start = tasks_sw_df.index[0] + UTIL_AVG_CONVERGENCE_TIME end = tasks_sw_df.index[-1] - UTIL_AVG_CONVERGENCE_TIME task_util_df = task_util_df[start:end] tg_util_df = tg_util_df[start:end] # Compute mean util of the taskgroup and its children util_tg = tg_util_df[(tg_util_df.path == group) & (tg_util_df.cpu == self.target_cpu)].util util_mean_tg = area_under_curve(util_tg) / (end - start) msg = 'Saw util {} for {} cgroup, expected {}' expected_trace_util = 0.0 for child in tg.children: if child.is_task: util_s = task_util_df[task_util_df.comm == child.name].util else: util_s = tg_util_df[(tg_util_df.path == child.name) & (tg_util_df.cpu == self.target_cpu)].util util_mean = area_under_curve(util_s) / (end - start) # Make sure the trace utilization of children entities matches the # expected utilization (i.e. duty cycle for tasks, sum of utils for # taskgroups) expected = child.get_expected_util() error_margin = expected * (ERROR_MARGIN_PCT / 100.) self.assertAlmostEqual(util_mean, expected, delta=error_margin, msg=msg.format(util_mean, child.name, expected)) expected_trace_util += util_mean error_margin = expected_trace_util * self.allowed_util_margin self.assertAlmostEqual(util_mean_tg, expected_trace_util, delta=error_margin, msg=msg.format(util_mean_tg, group, expected_trace_util))
def _get_trace_metrics(self, trace_path): """ Parse a trace (or used cached results) and extract extra metrics from it Returns a DataFrame with columns: metric,value,units """ cache_path = os.path.join(os.path.dirname(trace_path), 'lisa_trace_metrics.csv') if self.use_cached_trace_metrics and os.path.exists(cache_path): return pd.read_csv(cache_path) # I wonder if this should go in LISA itself? Probably. metrics = [] events = ['irq_handler_entry', 'cpu_frequency', 'nohz_kick', 'sched_switch', 'sched_load_cfs_rq', 'sched_load_avg_task', 'thermal_temperature'] trace = Trace(self.platform, trace_path, events) metrics.append(('cpu_wakeup_count', len(trace.data_frame.cpu_wakeups()), None)) # Helper to get area under curve of multiple CPU active signals def get_cpu_time(trace, cpus): df = pd.DataFrame([trace.getCPUActiveSignal(cpu) for cpu in cpus]) return df.sum(axis=1).sum(axis=0) clusters = trace.platform.get('clusters') if clusters: for cluster in clusters.values(): name = '-'.join(str(c) for c in cluster) df = trace.data_frame.cluster_frequency_residency(cluster) if df is None or df.empty: self._log.warning("Can't get cluster freq residency from %s", trace.data_dir) else: df = df.reset_index() avg_freq = (df.frequency * df.time).sum() / df.time.sum() metric = 'avg_freq_cluster_{}'.format(name) metrics.append((metric, avg_freq, 'MHz')) df = trace.data_frame.trace_event('cpu_frequency') df = df[df.cpu == cluster[0]] metrics.append(('freq_transition_count_{}'.format(name), len(df), None)) active_time = area_under_curve(trace.getClusterActiveSignal(cluster)) metrics.append(('active_time_cluster_{}'.format(name), active_time, 'seconds')) metrics.append(('cpu_time_cluster_{}'.format(name), get_cpu_time(trace, cluster), 'cpu-seconds')) metrics.append(('cpu_time_total', get_cpu_time(trace, range(trace.platform['cpus_count'])), 'cpu-seconds')) event = None if trace.hasEvents('sched_load_cfs_rq'): event = 'sched_load_cfs_rq' row_filter = lambda r: r.path == '/' column = 'util' elif trace.hasEvents('sched_load_avg_cpu'): event = 'sched_load_avg_cpu' row_filter = lambda r: True column = 'util_avg' if event: df = trace.data_frame.trace_event(event) util_sum = (handle_duplicate_index(df)[row_filter] .pivot(columns='cpu')[column].ffill().sum(axis=1)) avg_util_sum = area_under_curve(util_sum) / (util_sum.index[-1] - util_sum.index[0]) metrics.append(('avg_util_sum', avg_util_sum, None)) if trace.hasEvents('thermal_temperature'): df = trace.data_frame.trace_event('thermal_temperature') for zone, zone_df in df.groupby('thermal_zone'): metrics.append(('tz_{}_start_temp'.format(zone), zone_df.iloc[0]['temp_prev'], 'milliCelcius')) if len(zone_df == 1): # Avoid division by 0 avg_tmp = zone_df['temp'].iloc[0] else: avg_tmp = (area_under_curve(zone_df['temp']) / (zone_df.index[-1] - zone_df.index[0])) metrics.append(('tz_{}_avg_temp'.format(zone), avg_tmp, 'milliCelcius')) ret = pd.DataFrame(metrics, columns=['metric', 'value', 'units']) ret.to_csv(cache_path, index=False) return ret
def plotCPUFrequencies(self, cpus=None): """ Plot frequency for the specified CPUs (or all if not specified). If sched_overutilized events are available, the plots will also show the intervals of time where the system was overutilized. The generated plots are also saved as PNG images under the folder specified by the `plots_dir` parameter of :class:`Trace`. :param cpus: the list of CPUs to plot, if None it generate a plot for each available CPU :type cpus: int or list(int) :return: a dictionary of average frequency for each CPU. """ if not self._trace.hasEvents('cpu_frequency'): self._log.warning('Events [cpu_frequency] not found, plot DISABLED!') return df = self._dfg_trace_event('cpu_frequency') if cpus is None: # Generate plots only for available CPUs cpus = range(df.cpu.max()+1) else: # Generate plots only specified CPUs cpus = listify(cpus) chained_assignment = pd.options.mode.chained_assignment pd.options.mode.chained_assignment = None freq = {} for cpu_id in listify(cpus): # Extract CPUs' frequencies and scale them to [MHz] _df = df[df.cpu == cpu_id] if _df.empty: self._log.warning('No [cpu_frequency] events for CPU%d, ' 'plot DISABLED!', cpu_id) continue _df['frequency'] = _df.frequency / 1e3 # Compute AVG frequency for this CPU avg_freq = 0 if len(_df) > 1: timespan = _df.index[-1] - _df.index[0] avg_freq = area_under_curve(_df['frequency'], method='rect') / timespan # Store DF for plotting freq[cpu_id] = { 'df' : _df, 'avg' : avg_freq, } pd.options.mode.chained_assignment = chained_assignment plots_count = len(freq) if not plots_count: return # Setup CPUs plots fig, pltaxes = plt.subplots(len(freq), 1, figsize=(16, 4 * plots_count)) avg_freqs = {} for plot_idx, cpu_id in enumerate(freq): # CPU frequencies and average value _df = freq[cpu_id]['df'] _avg = freq[cpu_id]['avg'] # Plot average frequency try: axes = pltaxes[plot_idx] except TypeError: axes = pltaxes axes.set_title('CPU{:2d} Frequency'.format(cpu_id)) axes.axhline(_avg, color='r', linestyle='--', linewidth=2) # Set plot limit based on CPU min/max frequencies if 'clusters' in self._platform: for cluster,cpus in self._platform['clusters'].iteritems(): if cpu_id not in cpus: continue freqs = self._platform['freqs'][cluster] break else: freqs = df['frequency'].unique() axes.set_ylim((min(freqs) - 100000) / 1e3, (max(freqs) + 100000) / 1e3) # Plot CPU frequency transitions _df['frequency'].plot(style=['r-'], ax=axes, drawstyle='steps-post', alpha=0.4) # Plot overutilzied regions (if signal available) self._trace.analysis.status.plotOverutilized(axes) # Finalize plot axes.set_xlim(self._trace.x_min, self._trace.x_max) axes.set_ylabel('MHz') axes.grid(True) if plot_idx + 1 < plots_count: axes.set_xticklabels([]) axes.set_xlabel('') avg_freqs[cpu_id] = _avg/1e3 self._log.info('CPU%02d average frequency: %.3f GHz', cpu_id, avg_freqs[cpu_id]) # Save generated plots into datadir figname = '{}/{}cpus_freqs.png'\ .format(self._trace.plots_dir, self._trace.plots_prefix) pl.savefig(figname, bbox_inches='tight') return avg_freqs
def get_trace_metrics(self, trace_path): cache_path = os.path.join(os.path.dirname(trace_path), 'lisa_trace_metrics.csv') if self.use_cached_trace_metrics and os.path.exists(cache_path): return pd.read_csv(cache_path) # I wonder if this should go in LISA itself? Probably. metrics = [] events = [ 'irq_handler_entry', 'cpu_frequency', 'nohz_kick', 'sched_switch', 'sched_load_cfs_rq', 'sched_load_avg_task' ] trace = Trace(self.platform, trace_path, events) if hasattr(trace.data_frame, 'cpu_wakeups'): # Not merged in LISA yet metrics.append(('cpu_wakeup_count', len(trace.data_frame.cpu_wakeups()), None)) # Helper to get area under curve of multiple CPU active signals def get_cpu_time(trace, cpus): df = pd.DataFrame([trace.getCPUActiveSignal(cpu) for cpu in cpus]) return df.sum(axis=1).sum(axis=0) clusters = trace.platform.get('clusters') if clusters: for cluster in clusters.values(): name = '-'.join(str(c) for c in cluster) df = trace.data_frame.cluster_frequency_residency(cluster) if df is None or df.empty: print "Can't get cluster freq residency from {}".format( trace.data_dir) else: df = df.reset_index() avg_freq = (df.frequency * df.time).sum() / df.time.sum() metric = 'avg_freq_cluster_{}'.format(name) metrics.append((metric, avg_freq, 'MHz')) df = trace.data_frame.trace_event('cpu_frequency') df = df[df.cpu == cluster[0]] metrics.append( ('freq_transition_count_{}'.format(name), len(df), None)) active_time = area_under_curve( trace.getClusterActiveSignal(cluster)) metrics.append(('active_time_cluster_{}'.format(name), active_time, 'seconds')) metrics.append(('cpu_time_cluster_{}'.format(name), get_cpu_time(trace, cluster), 'cpu-seconds')) metrics.append( ('cpu_time_total', get_cpu_time(trace, range(trace.platform['cpus_count'])), 'cpu-seconds')) event = None if trace.hasEvents('sched_load_cfs_rq'): event = 'sched_load_cfs_rq' row_filter = lambda r: r.path == '/' column = 'util' elif trace.hasEvents('sched_load_avg_cpu'): event = 'sched_load_avg_cpu' row_filter = lambda r: True column = 'util_avg' if event: df = trace.data_frame.trace_event(event) util_sum = (handle_duplicate_index(df)[row_filter].pivot( columns='cpu')[column].ffill().sum(axis=1)) avg_util_sum = area_under_curve(util_sum) / (util_sum.index[-1] - util_sum.index[0]) metrics.append(('avg_util_sum', avg_util_sum, None)) if trace.hasEvents('nohz_kick'): metrics.append( ('nohz_kick_count', len(trace.data_frame.trace_event('nohz_kick')), None)) ret = pd.DataFrame(metrics, columns=['metric', 'value', 'units']) if self.use_cached_trace_metrics: ret.to_csv(cache_path) return ret
def plotCPUFrequencies(self, cpus=None): """ Plot frequency for the specified CPUs (or all if not specified). If sched_overutilized events are available, the plots will also show the intervals of time where the system was overutilized. The generated plots are also saved as PNG images under the folder specified by the `plots_dir` parameter of :class:`Trace`. :param cpus: the list of CPUs to plot, if None it generate a plot for each available CPU :type cpus: int or list(int) :return: a dictionary of average frequency for each CPU. """ if not self._trace.hasEvents('cpu_frequency'): self._log.warning('Events [cpu_frequency] not found, plot DISABLED!') return df = self._dfg_trace_event('cpu_frequency') if cpus is None: # Generate plots only for available CPUs cpus = range(df.cpu.max()+1) else: # Generate plots only specified CPUs cpus = listify(cpus) chained_assignment = pd.options.mode.chained_assignment pd.options.mode.chained_assignment = None freq = {} for cpu_id in listify(cpus): # Extract CPUs' frequencies and scale them to [MHz] _df = df[df.cpu == cpu_id] if _df.empty: self._log.warning('No [cpu_frequency] events for CPU%d, ' 'plot DISABLED!', cpu_id) continue _df['frequency'] = _df.frequency / 1e3 # Compute AVG frequency for this CPU avg_freq = 0 if len(_df) > 1: timespan = _df.index[-1] - _df.index[0] avg_freq = area_under_curve(_df['frequency'], method='rect') / timespan # Store DF for plotting freq[cpu_id] = { 'df' : _df, 'avg' : avg_freq, } pd.options.mode.chained_assignment = chained_assignment plots_count = len(freq) if not plots_count: return # Setup CPUs plots fig, pltaxes = plt.subplots(len(freq), 1, figsize=(16, 4 * plots_count)) avg_freqs = {} for plot_idx, cpu_id in enumerate(freq): # CPU frequencies and average value _df = freq[cpu_id]['df'] _avg = freq[cpu_id]['avg'] # Plot average frequency try: axes = pltaxes[plot_idx] except TypeError: axes = pltaxes axes.set_title('CPU{:2d} Frequency'.format(cpu_id)) axes.axhline(_avg, color='r', linestyle='--', linewidth=2) # Set plot limit based on CPU min/max frequencies if 'clusters' in self._platform: for cluster,cpus in self._platform['clusters'].iteritems(): if cpu_id not in cpus: continue freqs = self._platform['freqs'][cluster] break else: freqs = df['frequency'].unique() axes.set_ylim((min(freqs) - 100000) / 1e3, (max(freqs) + 100000) / 1e3) # Plot CPU frequency transitions _df['frequency'].plot(style=['r-'], ax=axes, drawstyle='steps-post', alpha=0.4) # Plot overutilzied regions (if signal available) self._trace.analysis.status.plotOverutilized(axes) # Finalize plot axes.set_xlim(self._trace.x_min, self._trace.x_max) axes.set_ylabel('MHz') axes.grid(True) if plot_idx + 1 < plots_count: axes.set_xticklabels([]) axes.set_xlabel('') avg_freqs[cpu_id] = _avg/1e3 self._log.info('CPU%02d average frequency: %.3f GHz', cpu_id, avg_freqs[cpu_id]) # Save generated plots into datadir figname = '{}/{}cpus_freqs.png'\ .format(self._trace.plots_dir, self._trace.plots_prefix) pl.savefig(figname, bbox_inches='tight') return avg_freqs
def _get_trace_metrics(self, trace_path): """ Parse a trace (or used cached results) and extract extra metrics from it Returns a DataFrame with columns: metric,value,units """ cache_path = os.path.join(os.path.dirname(trace_path), 'lisa_trace_metrics.csv') if self.use_cached_trace_metrics and os.path.exists(cache_path): return pd.read_csv(cache_path) # I wonder if this should go in LISA itself? Probably. metrics = [] events = [ 'irq_handler_entry', 'cpu_frequency', 'nohz_kick', 'sched_switch', 'sched_load_cfs_rq', 'sched_load_avg_task', 'thermal_temperature' ] trace = Trace(self.platform, trace_path, events) metrics.append( ('cpu_wakeup_count', len(trace.data_frame.cpu_wakeups()), None)) # Helper to get area under curve of multiple CPU active signals def get_cpu_time(trace, cpus): df = pd.DataFrame([trace.getCPUActiveSignal(cpu) for cpu in cpus]) return df.sum(axis=1).sum(axis=0) clusters = trace.platform.get('clusters') if clusters: for cluster in clusters.values(): name = '-'.join(str(c) for c in cluster) df = trace.data_frame.cluster_frequency_residency(cluster) if df is None or df.empty: self._log.warning( "Can't get cluster freq residency from %s", trace.data_dir) else: df = df.reset_index() avg_freq = (df.frequency * df.time).sum() / df.time.sum() metric = 'avg_freq_cluster_{}'.format(name) metrics.append((metric, avg_freq, 'MHz')) df = trace.data_frame.trace_event('cpu_frequency') df = df[df.cpu == cluster[0]] metrics.append( ('freq_transition_count_{}'.format(name), len(df), None)) active_time = area_under_curve( trace.getClusterActiveSignal(cluster)) metrics.append(('active_time_cluster_{}'.format(name), active_time, 'seconds')) metrics.append(('cpu_time_cluster_{}'.format(name), get_cpu_time(trace, cluster), 'cpu-seconds')) metrics.append( ('cpu_time_total', get_cpu_time(trace, range(trace.platform['cpus_count'])), 'cpu-seconds')) event = None if trace.hasEvents('sched_load_cfs_rq'): event = 'sched_load_cfs_rq' row_filter = lambda r: r.path == '/' column = 'util' elif trace.hasEvents('sched_load_avg_cpu'): event = 'sched_load_avg_cpu' row_filter = lambda r: True column = 'util_avg' if event: df = trace.data_frame.trace_event(event) util_sum = (handle_duplicate_index(df)[row_filter].pivot( columns='cpu')[column].ffill().sum(axis=1)) avg_util_sum = area_under_curve(util_sum) / (util_sum.index[-1] - util_sum.index[0]) metrics.append(('avg_util_sum', avg_util_sum, None)) if trace.hasEvents('thermal_temperature'): df = trace.data_frame.trace_event('thermal_temperature') for zone, zone_df in df.groupby('thermal_zone'): metrics.append(('tz_{}_start_temp'.format(zone), zone_df.iloc[0]['temp_prev'], 'milliCelcius')) if len(zone_df == 1): # Avoid division by 0 avg_tmp = zone_df['temp'].iloc[0] else: avg_tmp = (area_under_curve(zone_df['temp']) / (zone_df.index[-1] - zone_df.index[0])) metrics.append( ('tz_{}_avg_temp'.format(zone), avg_tmp, 'milliCelcius')) ret = pd.DataFrame(metrics, columns=['metric', 'value', 'units']) ret.to_csv(cache_path, index=False) return ret
def _compute_energy(self, df): channels_nrg = {} for site, measure in df: if measure == 'power': channels_nrg[site] = area_under_curve(df[site]['power']) return channels_nrg