diff --git a/libs/utils/analysis/frequency_analysis.py b/libs/utils/analysis/frequency_analysis.py index e9fce893f91ae5f51bf5cebc3976e382ee832760..a48b9a315cc51340ca0621f782a7565988242c4f 100644 --- a/libs/utils/analysis/frequency_analysis.py +++ b/libs/utils/analysis/frequency_analysis.py @@ -446,7 +446,6 @@ class FrequencyAnalysis(AnalysisModule): # Utility Methods ############################################################################### - @memoized def _getFrequencyResidency(self, cluster): """ Get a DataFrame with per cluster frequency residency, i.e. amount of @@ -479,7 +478,8 @@ class FrequencyAnalysis(AnalysisModule): self._log.warning('Cluster frequency is NOT coherent,' 'cannot compute residency!') return None - cluster_freqs = freq_df[freq_df.cpu == _cluster[0]] + cluster_freqs = freq_df[freq_df.cpu == _cluster[0]].frequency + cluster_freqs = self._trace._cropToXTimeRange(cluster_freqs) # Compute TOTAL Time time_intervals = cluster_freqs.index[1:] - cluster_freqs.index[:-1] @@ -490,7 +490,8 @@ class FrequencyAnalysis(AnalysisModule): total_time = total_time.groupby(['frequency']).sum() # Compute ACTIVE Time - cluster_active = self._trace.getClusterActiveSignal(_cluster) + cluster_active = self._getClusterActiveSignal(_cluster) + cluster_active = self._trace._cropToXTimeRange(cluster_active) # In order to compute the active time spent at each frequency we # multiply 2 square waves: diff --git a/libs/utils/trace.py b/libs/utils/trace.py index 80b6133d7fa959dea58dc3268ce677eff4807e72..0a2110b2c308c5666e5751e35346997650b7df34 100644 --- a/libs/utils/trace.py +++ b/libs/utils/trace.py @@ -85,9 +85,6 @@ class Trace(object): # Trace format self.trace_format = trace_format - # The time window used to limit trace parsing to - self.window = window - # Dynamically registered TRAPpy events self.trappy_cls = {} @@ -130,14 +127,13 @@ class Trace(object): trace_format) self.__computeTimeSpan() - # Minimum and Maximum x_time to use for all plots - self.x_min = 0 - self.x_max = self.time_range + # The time window used to limit trace parsing to a user-defined plot + # window + self.window = self.__computePlotWindow(window, normalize_time) - # Reset x axis time range to full scale - t_min = self.window[0] - t_max = self.window[1] - self.setXTimeRange(t_min, t_max) + # Reset x axis time range to plot window scale + self.x_min = self.window[0] + self.x_max = self.window[1] self.data_frame = TraceData() self._registerDataFrameGetters(self) @@ -171,14 +167,31 @@ class Trace(object): :param t_max: upper bound :type t_max: int or float """ - if t_min is None: - self.x_min = 0 - else: - self.x_min = t_min - if t_max is None: - self.x_max = self.time_range - else: - self.x_max = t_max + self.x_min = self.window[0] + self.x_max = self.window[1] + + if t_min is not None: + if t_min < self.x_min: + self._log.warning('t_min out of range: ' + 'capping to trace minimum %.6f [s]', + self.x_min) + elif t_min > self.x_max: + raise ValueError('t_min out of range: ' + 'trace boundaries are ({:.6f}, {:.6f}) [s]'\ + .format(self.x_min, self.x_max)) + else: + self.x_min = t_min + if t_max is not None: + if t_max > self.x_max: + self._log.warning('t_max out of range: ' + 'capping to trace maximum %.6f [s]', + self.x_max) + elif t_max < self.x_min: + raise ValueError('t_max out of range: ' + 'trace boundaries are ({:.6f}, {:.6f}) [s]'\ + .format(self.window[0], self.x_max)) + else: + self.x_max = t_max self._log.debug('Set plots time range to (%.6f, %.6f)[s]', self.x_min, self.x_max) @@ -260,16 +273,6 @@ class Trace(object): self._sanitize_SchedOverutilized() self._sanitize_CpuFrequency() - # Compute plot window - if not normalize_time: - start = self.window[0] - if self.window[1]: - duration = min(self.ftrace.get_duration(), self.window[1]) - else: - duration = self.ftrace.get_duration() - self.window = (self.ftrace.basetime + start, - self.ftrace.basetime + duration) - def __checkAvailableEvents(self, key=""): """ Internal method used to build a list of available events. @@ -319,18 +322,7 @@ class Trace(object): """ Compute time axis range, considering all the parsed events. """ - ts = sys.maxint - te = 0 - - for events in self.available_events: - df = self._dfg_trace_event(events) - if len(df) == 0: - continue - if (df.index[0]) < ts: - ts = df.index[0] - if (df.index[-1]) > te: - te = df.index[-1] - self.time_range = te - ts + self.time_range = self.ftrace.get_duration() self._log.debug('Collected events spans a %.3f [s] time interval', self.time_range) @@ -344,6 +336,28 @@ class Trace(object): self._log.debug('Overutilized time: %.6f [s] (%.3f%% of trace time)', self.overutilized_time, self.overutilized_prc) + def __computePlotWindow(self, window, normalize_time): + """ + Compute boundaries for the plot window. + + :param window: time window considered when trace was parsed + :type window: tuple(int or float, int or float) + + :param normalize_time: normalize trace time stamps + :type normalize_time: bool + """ + start = window[0] + if window[1]: + end = min(self.time_range, window[1]) + else: + end = self.time_range + + if not normalize_time: + start += self.ftrace.basetime + end += self.ftrace.basetime + + return (start, end) + def _scanTasks(self, df, name_key='comm', pid_key='pid'): """ Extract tasks names and PIDs from the input data frame. The data frame @@ -857,6 +871,43 @@ class Trace(object): return cluster_active + def _cropToXTimeRange(self, data): + """ + Crop a series of FTrace events to the X time range specified by the + user through setXTimeRange(). + + When cropping a series it might be necessary to include a first and + last element with timestamps equal to the boundaries of the X time + range. + + :param data: series to be cropped + :type data: :mod:`pandas.Series` + """ + if not isinstance(data, pd.Series): + msg = 'Cropping supported only for pandas.Series objects!' + raise ValueError(msg) + + first = None + # Avoid duplicate indexes in case the user specifies an x_min that + # corresponds to the time stamp of an existing event. + if self.x_min not in data.index: + # data[data.index < self.x_min] may return an empty series if x_min + # is lower than the lowest index of the data series. This will + # raise an exception that can be caught and leave first to None, as + # in this case a first element is not needed + try: + first = pd.Series([data[data.index < self.x_min].iloc[-1]], + index=[self.x_min]) + except: pass + last = None + if self.x_max not in data.index: + try: + last = pd.Series([data[data.index <= self.x_max].iloc[-1]], + index=[self.x_max]) + except: pass + data = data.loc[self.x_min:self.x_max] + return pd.concat([first, data, last]) + class TraceData: """ A DataFrame collector exposed to Trace's clients """ diff --git a/tests/lisa/test_trace.py b/tests/lisa/test_trace.py index 30885e91c5f7b846fe3c5a5fc6a1544aa4c1902b..174e3a87ad1d0582b6d06de0b8f4e2e926f8e411 100644 --- a/tests/lisa/test_trace.py +++ b/tests/lisa/test_trace.py @@ -78,3 +78,53 @@ class TestTrace(TestCase): self.assertEqual(trace.getTaskByName('father'), [1234]) os.remove(self.test_trace) + + def test_setXTimeRange(self): + """ + TestTrace: setXTimeRange() properly update user-specified trace time + boundaries. + """ + # Test min and max values out of range + t_min = self.trace.window[1] + 10 + with self.assertRaises(ValueError): + self.trace.setXTimeRange(t_min=t_min) + + t_max = self.trace.window[0] - 10 + with self.assertRaises(ValueError): + self.trace.setXTimeRange(t_max=t_max) + + # Test min/max set to valid values in the range + # [trace.window[0], trace.window[1]] + t_min = self.trace.window[0] + 1 + t_max = self.trace.window[1] - 1 + self.trace.setXTimeRange(t_min, t_max) + self.assertEqual(self.trace.x_min, t_min) + self.assertEqual(self.trace.x_max, t_max) + + # Test min/max reset to trace.window[0]/trace.window[1] + self.trace.setXTimeRange() + self.assertEqual(self.trace.x_min, self.trace.window[0]) + self.assertEqual(self.trace.x_max, self.trace.window[1]) + + # Test min value and max capped to trace minimum and maximum + # respectively + t_min = self.trace.window[0] - 10 + t_max = self.trace.window[1] + 10 + self.trace.setXTimeRange(t_min, t_max) + self.assertEqual(self.trace.x_min, self.trace.window[0]) + self.assertEqual(self.trace.x_max, self.trace.window[1]) + + def test_cropToXTimeRange(self): + """ + TestTrace: cropToXTimeRange() properly crops the data frame to the X + time range specified by the user through setXTimeRange(). + """ + series = self.trace.data_frame.trace_event('sched_switch').prev_state + + t_min = self.trace.window[0] + 2 + t_max = self.trace.window[1] - 2 + self.trace.setXTimeRange(t_min, t_max) + + cropped_series = self.trace._cropToXTimeRange(series) + self.assertTrue(cropped_series.index[0] >= t_min) + self.assertTrue(cropped_series.index[-1] <= t_max)