Beispiel #1
0
    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
Beispiel #2
0
    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
Beispiel #3
0
    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)
Beispiel #4
0
    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)
Beispiel #5
0
    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])
Beispiel #6
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
Beispiel #7
0
 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))
Beispiel #8
0
    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)
Beispiel #9
0
    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
Beispiel #10
0
    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
Beispiel #11
0
 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
Beispiel #12
0
    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))
Beispiel #13
0
    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
Beispiel #14
0
    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
Beispiel #15
0
    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
Beispiel #16
0
    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
Beispiel #18
0
 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