1#    Copyright 2015-2017 ARM Limited
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15
16
17# pylint can't see any of the dynamically allocated classes of FTrace
18# pylint: disable=no-member
19
20import itertools
21import json
22import os
23import re
24import pandas as pd
25import hashlib
26import shutil
27import warnings
28
29from trappy.bare_trace import BareTrace
30from trappy.utils import listify
31
32class FTraceParseError(Exception):
33    pass
34
35def _plot_freq_hists(allfreqs, what, axis, title):
36    """Helper function for plot_freq_hists
37
38    allfreqs is the output of a Cpu*Power().get_all_freqs() (for
39    example, CpuInPower.get_all_freqs()).  what is a string: "in" or
40    "out"
41
42    """
43    import trappy.plot_utils
44
45    for ax, actor in zip(axis, allfreqs):
46        this_title = "freq {} {}".format(what, actor)
47        this_title = trappy.plot_utils.normalize_title(this_title, title)
48        xlim = (0, allfreqs[actor].max())
49
50        trappy.plot_utils.plot_hist(allfreqs[actor], ax, this_title, "KHz", 20,
51                             "Frequency", xlim, "default")
52
53SPECIAL_FIELDS_RE = re.compile(
54                        r"^\s*(?P<comm>.*)-(?P<pid>\d+)\s+\(?(?P<tgid>.*?)?\)"\
55                        r"?\s*\[(?P<cpu>\d+)\](?:\s+....)?\s+"\
56                        r"(?P<timestamp>[0-9]+(?P<us>\.[0-9]+)?): (\w+:\s+)+(?P<data>.+)"
57)
58
59class GenericFTrace(BareTrace):
60    """Generic class to parse output of FTrace.  This class is meant to be
61subclassed by FTrace (for parsing FTrace coming from trace-cmd) and SysTrace."""
62
63    thermal_classes = {}
64
65    sched_classes = {}
66
67    dynamic_classes = {}
68
69    disable_cache = True
70
71    def _trace_cache_path(self):
72        trace_file = self.trace_path
73        cache_dir  = '.' +  os.path.basename(trace_file) + '.cache'
74        tracefile_dir = os.path.dirname(os.path.abspath(trace_file))
75        cache_path = os.path.join(tracefile_dir, cache_dir)
76        return cache_path
77
78    def _check_trace_cache(self, params):
79        cache_path = self._trace_cache_path()
80        md5file = os.path.join(cache_path, 'md5sum')
81        basetime_path = os.path.join(cache_path, 'basetime')
82        params_path = os.path.join(cache_path, 'params.json')
83
84        for path in [cache_path, md5file, params_path]:
85            if not os.path.exists(path):
86                return False
87
88        with open(md5file) as f:
89            cache_md5sum = f.read()
90        with open(basetime_path) as f:
91            self.basetime = float(f.read())
92        with open(self.trace_path, 'rb') as f:
93            trace_md5sum = hashlib.md5(f.read()).hexdigest()
94        with open(params_path) as f:
95            cache_params = json.dumps(json.load(f))
96
97        # Convert to a json string for comparison
98        params = json.dumps(params)
99
100        # check if cache is valid
101        if cache_md5sum != trace_md5sum or cache_params != params:
102            shutil.rmtree(cache_path)
103            return False
104        return True
105
106    def _create_trace_cache(self, params):
107        cache_path = self._trace_cache_path()
108        md5file = os.path.join(cache_path, 'md5sum')
109        basetime_path = os.path.join(cache_path, 'basetime')
110        params_path = os.path.join(cache_path, 'params.json')
111
112        if os.path.exists(cache_path):
113            shutil.rmtree(cache_path)
114        os.mkdir(cache_path)
115
116        md5sum = hashlib.md5(open(self.trace_path, 'rb').read()).hexdigest()
117        with open(md5file, 'w') as f:
118            f.write(md5sum)
119
120        with open(basetime_path, 'w') as f:
121            f.write(str(self.basetime))
122
123        with open(params_path, 'w') as f:
124            json.dump(params, f)
125
126    def _get_csv_path(self, trace_class):
127        path = self._trace_cache_path()
128        return os.path.join(path, trace_class.__class__.__name__ + '.csv')
129
130    def __init__(self, name="", normalize_time=True, scope="all",
131                 events=[], window=(0, None), abs_window=(0, None)):
132        super(GenericFTrace, self).__init__(name)
133
134        self.class_definitions.update(self.dynamic_classes.items())
135        self.__add_events(listify(events))
136
137        if scope == "thermal":
138            self.class_definitions.update(self.thermal_classes.items())
139        elif scope == "sched":
140            self.class_definitions.update(self.sched_classes.items())
141        elif scope != "custom":
142            self.class_definitions.update(self.thermal_classes.items() +
143                                          self.sched_classes.items())
144
145        for attr, class_def in self.class_definitions.iteritems():
146            trace_class = class_def()
147            setattr(self, attr, trace_class)
148            self.trace_classes.append(trace_class)
149
150        # save parameters to complete init later
151        self.normalize_time = normalize_time
152        self.window = window
153        self.abs_window = abs_window
154
155    @classmethod
156    def register_parser(cls, cobject, scope):
157        """Register the class as an Event. This function
158        can be used to register a class which is associated
159        with an FTrace unique word.
160
161        .. seealso::
162
163            :mod:`trappy.dynamic.register_dynamic_ftrace` :mod:`trappy.dynamic.register_ftrace_parser`
164
165        """
166
167        if not hasattr(cobject, "name"):
168            cobject.name = cobject.unique_word.split(":")[0]
169
170        # Add the class to the classes dictionary
171        if scope == "all":
172            cls.dynamic_classes[cobject.name] = cobject
173        else:
174            getattr(cls, scope + "_classes")[cobject.name] = cobject
175
176    @classmethod
177    def unregister_parser(cls, cobject):
178        """Unregister a parser
179
180        This is the opposite of FTrace.register_parser(), it removes a class
181        from the list of classes that will be parsed on the trace
182
183        """
184
185        # TODO: scopes should not be hardcoded (nor here nor in the FTrace object)
186        all_scopes = [cls.thermal_classes, cls.sched_classes,
187                      cls.dynamic_classes]
188        known_events = ((n, c, sc) for sc in all_scopes for n, c in sc.items())
189
190        for name, obj, scope_classes in known_events:
191            if cobject == obj:
192                del scope_classes[name]
193
194    def _do_parse(self):
195        params = {'window': self.window, 'abs_window': self.abs_window}
196        if not self.__class__.disable_cache and self._check_trace_cache(params):
197            # Read csv into frames
198            for trace_class in self.trace_classes:
199                try:
200                    csv_file = self._get_csv_path(trace_class)
201                    trace_class.read_csv(csv_file)
202                    trace_class.cached = True
203                except:
204                    warnstr = "TRAPpy: Couldn't read {} from cache, reading it from trace".format(trace_class)
205                    warnings.warn(warnstr)
206
207        if all([c.cached for c in self.trace_classes]):
208            if self.normalize_time:
209                self._normalize_time()
210            return
211
212        self.__parse_trace_file(self.trace_path)
213
214        self.finalize_objects()
215
216        if not self.__class__.disable_cache:
217            try:
218                # Recreate basic cache directories only if nothing cached
219                if not any([c.cached for c in self.trace_classes]):
220                    self._create_trace_cache(params)
221
222                # Write out only events that weren't cached before
223                for trace_class in self.trace_classes:
224                    if trace_class.cached:
225                        continue
226                    csv_file = self._get_csv_path(trace_class)
227                    trace_class.write_csv(csv_file)
228            except OSError as err:
229                warnings.warn(
230                    "TRAPpy: Cache not created due to OS error: {0}".format(err))
231
232        if self.normalize_time:
233            self._normalize_time()
234
235    def __add_events(self, events):
236        """Add events to the class_definitions
237
238        If the events are known to trappy just add that class to the
239        class definitions list.  Otherwise, register a class to parse
240        that event
241
242        """
243
244        from trappy.dynamic import DynamicTypeFactory, default_init
245        from trappy.base import Base
246
247        # TODO: scopes should not be hardcoded (nor here nor in the FTrace object)
248        all_scopes = [self.thermal_classes, self.sched_classes,
249                      self.dynamic_classes]
250        known_events = {k: v for sc in all_scopes for k, v in sc.iteritems()}
251
252        for event_name in events:
253            for cls in known_events.itervalues():
254                if (event_name == cls.unique_word) or \
255                   (event_name + ":" == cls.unique_word):
256                    self.class_definitions[event_name] = cls
257                    break
258            else:
259                kwords = {
260                    "__init__": default_init,
261                    "unique_word": event_name + ":",
262                    "name": event_name,
263                }
264                trace_class = DynamicTypeFactory(event_name, (Base,), kwords)
265                self.class_definitions[event_name] = trace_class
266
267    def __get_trace_class(self, line, cls_word):
268        trace_class = None
269        for unique_word, cls in cls_word.iteritems():
270            if unique_word in line:
271                trace_class = cls
272                if not cls.fallback:
273                    return trace_class
274        return trace_class
275
276    def __populate_data(self, fin, cls_for_unique_word):
277        """Append to trace data from a txt trace"""
278
279        actual_trace = itertools.dropwhile(self.trace_hasnt_started(), fin)
280        actual_trace = itertools.takewhile(self.trace_hasnt_finished(),
281                                           actual_trace)
282
283        for line in actual_trace:
284            trace_class = self.__get_trace_class(line, cls_for_unique_word)
285            if not trace_class:
286                self.lines += 1
287                continue
288
289            line = line[:-1]
290
291            fields_match = SPECIAL_FIELDS_RE.match(line)
292            if not fields_match:
293                raise FTraceParseError("Couldn't match fields in '{}'".format(line))
294            comm = fields_match.group('comm')
295            pid = int(fields_match.group('pid'))
296            cpu = int(fields_match.group('cpu'))
297            tgid = fields_match.group('tgid')
298            tgid = -1 if (not tgid or '-' in tgid) else int(tgid)
299
300            # The timestamp, depending on the trace_clock configuration, can be
301            # reported either in [s].[us] or [ns] format. Let's ensure that we
302            # always generate DF which have the index expressed in:
303            #    [s].[decimals]
304            timestamp = float(fields_match.group('timestamp'))
305            if not fields_match.group('us'):
306                timestamp /= 1e9
307            data_str = fields_match.group('data')
308
309            if not self.basetime:
310                self.basetime = timestamp
311
312            if (timestamp < self.window[0] + self.basetime) or \
313               (timestamp < self.abs_window[0]):
314                self.lines += 1
315                continue
316
317            if (self.window[1] and timestamp > self.window[1] + self.basetime) or \
318               (self.abs_window[1] and timestamp > self.abs_window[1]):
319                return
320
321            # Remove empty arrays from the trace
322            if "={}" in data_str:
323                data_str = re.sub(r"[A-Za-z0-9_]+=\{\} ", r"", data_str)
324
325            trace_class.append_data(timestamp, comm, pid, tgid, cpu, self.lines, data_str)
326            self.lines += 1
327
328    def trace_hasnt_started(self):
329        """Return a function that accepts a line and returns true if this line
330is not part of the trace.
331
332        Subclasses of GenericFTrace may override this to skip the
333        beginning of a file that is not part of the trace.  The first
334        time the returned function returns False it will be considered
335        the beginning of the trace and this function will never be
336        called again (because once it returns False, the trace has
337        started).
338
339        """
340        return lambda line: not SPECIAL_FIELDS_RE.match(line)
341
342    def trace_hasnt_finished(self):
343        """Return a function that accepts a line and returns true if this line
344is part of the trace.
345
346        This function is called with each line of the file *after*
347        trace_hasnt_started() returns True so the first line it sees
348        is part of the trace.  The returned function should return
349        True as long as the line it receives is part of the trace.  As
350        soon as this function returns False, the rest of the file will
351        be dropped.  Subclasses of GenericFTrace may override this to
352        stop processing after the end of the trace is found to skip
353        parsing the end of the file if it contains anything other than
354        trace.
355
356        """
357        return lambda x: True
358
359    def __parse_trace_file(self, trace_file):
360        """parse the trace and create a pandas DataFrame"""
361
362        # Memoize the unique words to speed up parsing the trace file
363        cls_for_unique_word = {}
364        for trace_name in self.class_definitions.iterkeys():
365            trace_class = getattr(self, trace_name)
366            if trace_class.cached:
367                continue
368
369            unique_word = trace_class.unique_word
370            cls_for_unique_word[unique_word] = trace_class
371
372        if len(cls_for_unique_word) == 0:
373            return
374
375        try:
376            with open(trace_file) as fin:
377                self.lines = 0
378                self.__populate_data(
379                    fin, cls_for_unique_word)
380        except FTraceParseError as e:
381            raise ValueError('Failed to parse ftrace file {}:\n{}'.format(
382                trace_file, str(e)))
383
384    # TODO: Move thermal specific functionality
385
386    def get_all_freqs_data(self, map_label):
387        """get an array of tuple of names and DataFrames suitable for the
388        allfreqs plot"""
389
390        cpu_in_freqs = self.cpu_in_power.get_all_freqs(map_label)
391        cpu_out_freqs = self.cpu_out_power.get_all_freqs(map_label)
392
393        ret = []
394        for label in map_label.values():
395            in_label = label + "_freq_in"
396            out_label = label + "_freq_out"
397
398            cpu_inout_freq_dict = {in_label: cpu_in_freqs[label],
399                                   out_label: cpu_out_freqs[label]}
400            dfr = pd.DataFrame(cpu_inout_freq_dict).fillna(method="pad")
401            ret.append((label, dfr))
402
403        try:
404            gpu_freq_in_data = self.devfreq_in_power.get_all_freqs()
405            gpu_freq_out_data = self.devfreq_out_power.get_all_freqs()
406        except KeyError:
407            gpu_freq_in_data = gpu_freq_out_data = None
408
409        if gpu_freq_in_data is not None:
410            inout_freq_dict = {"gpu_freq_in": gpu_freq_in_data["freq"],
411                               "gpu_freq_out": gpu_freq_out_data["freq"]
412                           }
413            dfr = pd.DataFrame(inout_freq_dict).fillna(method="pad")
414            ret.append(("GPU", dfr))
415
416        return ret
417
418    def apply_callbacks(self, fn_map, *kwarg):
419        """
420        Apply callback functions to trace events in chronological order.
421
422        This method iterates over a user-specified subset of the available trace
423        event dataframes, calling different user-specified functions for each
424        event type. These functions are passed a dictionary mapping 'Index' and
425        the column names to their values for that row.
426
427        For example, to iterate over trace t, applying your functions callback_fn1
428        and callback_fn2 to each sched_switch and sched_wakeup event respectively:
429
430        t.apply_callbacks({
431            "sched_switch": callback_fn1,
432            "sched_wakeup": callback_fn2
433        })
434        """
435        dfs = {event: getattr(self, event).data_frame for event in fn_map.keys()}
436        events = [event for event in fn_map.keys() if not dfs[event].empty]
437        iters = {event: dfs[event].itertuples() for event in events}
438        next_rows = {event: iterator.next() for event,iterator in iters.iteritems()}
439
440        # Column names beginning with underscore will not be preserved in tuples
441        # due to constraints on namedtuple field names, so store mappings from
442        # column name to column number for each trace event.
443        col_idxs = {event: {
444            name: idx for idx, name in enumerate(
445                ['Index'] + dfs[event].columns.tolist()
446            )
447        } for event in events}
448
449        def getLine(event):
450            line_col_idx = col_idxs[event]['__line']
451            return next_rows[event][line_col_idx]
452
453        while events:
454            event_name = min(events, key=getLine)
455            event_tuple = next_rows[event_name]
456
457            event_dict = {
458                col: event_tuple[idx] for col, idx in col_idxs[event_name].iteritems()
459            }
460
461            if kwarg:
462                fn_map[event_name](event_dict, kwarg)
463            else:
464                fn_map[event_name](event_dict)
465
466            event_row = next(iters[event_name], None)
467            if event_row:
468                next_rows[event_name] = event_row
469            else:
470                events.remove(event_name)
471
472    def plot_freq_hists(self, map_label, ax):
473        """Plot histograms for each actor input and output frequency
474
475        ax is an array of axis, one for the input power and one for
476        the output power
477
478        """
479
480        in_base_idx = len(ax) / 2
481
482        try:
483            devfreq_out_all_freqs = self.devfreq_out_power.get_all_freqs()
484            devfreq_in_all_freqs = self.devfreq_in_power.get_all_freqs()
485        except KeyError:
486            devfreq_out_all_freqs = None
487            devfreq_in_all_freqs = None
488
489        out_allfreqs = (self.cpu_out_power.get_all_freqs(map_label),
490                        devfreq_out_all_freqs, ax[0:in_base_idx])
491        in_allfreqs = (self.cpu_in_power.get_all_freqs(map_label),
492                       devfreq_in_all_freqs, ax[in_base_idx:])
493
494        for cpu_allfreqs, devfreq_freqs, axis in (out_allfreqs, in_allfreqs):
495            if devfreq_freqs is not None:
496                devfreq_freqs.name = "GPU"
497                allfreqs = pd.concat([cpu_allfreqs, devfreq_freqs], axis=1)
498            else:
499                allfreqs = cpu_allfreqs
500
501            allfreqs.fillna(method="pad", inplace=True)
502            _plot_freq_hists(allfreqs, "out", axis, self.name)
503
504    def plot_load(self, mapping_label, title="", width=None, height=None,
505                  ax=None):
506        """plot the load of all the clusters, similar to how compare runs did it
507
508        the mapping_label has to be a dict whose keys are the cluster
509        numbers as found in the trace and values are the names that
510        will appear in the legend.
511
512        """
513        import trappy.plot_utils
514
515        load_data = self.cpu_in_power.get_load_data(mapping_label)
516        try:
517            gpu_data = pd.DataFrame({"GPU":
518                                     self.devfreq_in_power.data_frame["load"]})
519            load_data = pd.concat([load_data, gpu_data], axis=1)
520        except KeyError:
521            pass
522
523        load_data = load_data.fillna(method="pad")
524        title = trappy.plot_utils.normalize_title("Utilization", title)
525
526        if not ax:
527            ax = trappy.plot_utils.pre_plot_setup(width=width, height=height)
528
529        load_data.plot(ax=ax)
530
531        trappy.plot_utils.post_plot_setup(ax, title=title)
532
533    def plot_normalized_load(self, mapping_label, title="", width=None,
534                             height=None, ax=None):
535        """plot the normalized load of all the clusters, similar to how compare runs did it
536
537        the mapping_label has to be a dict whose keys are the cluster
538        numbers as found in the trace and values are the names that
539        will appear in the legend.
540
541        """
542        import trappy.plot_utils
543
544        load_data = self.cpu_in_power.get_normalized_load_data(mapping_label)
545        if "load" in self.devfreq_in_power.data_frame:
546            gpu_dfr = self.devfreq_in_power.data_frame
547            gpu_max_freq = max(gpu_dfr["freq"])
548            gpu_load = gpu_dfr["load"] * gpu_dfr["freq"] / gpu_max_freq
549
550            gpu_data = pd.DataFrame({"GPU": gpu_load})
551            load_data = pd.concat([load_data, gpu_data], axis=1)
552
553        load_data = load_data.fillna(method="pad")
554        title = trappy.plot_utils.normalize_title("Normalized Utilization", title)
555
556        if not ax:
557            ax = trappy.plot_utils.pre_plot_setup(width=width, height=height)
558
559        load_data.plot(ax=ax)
560
561        trappy.plot_utils.post_plot_setup(ax, title=title)
562
563    def plot_allfreqs(self, map_label, width=None, height=None, ax=None):
564        """Do allfreqs plots similar to those of CompareRuns
565
566        if ax is not none, it must be an array of the same size as
567        map_label.  Each plot will be done in each of the axis in
568        ax
569
570        """
571        import trappy.plot_utils
572
573        all_freqs = self.get_all_freqs_data(map_label)
574
575        setup_plot = False
576        if ax is None:
577            ax = [None] * len(all_freqs)
578            setup_plot = True
579
580        for this_ax, (label, dfr) in zip(ax, all_freqs):
581            this_title = trappy.plot_utils.normalize_title("allfreqs " + label,
582                                                        self.name)
583
584            if setup_plot:
585                this_ax = trappy.plot_utils.pre_plot_setup(width=width,
586                                                        height=height)
587
588            dfr.plot(ax=this_ax)
589            trappy.plot_utils.post_plot_setup(this_ax, title=this_title)
590
591class FTrace(GenericFTrace):
592    """A wrapper class that initializes all the classes of a given run
593
594    - The FTrace class can receive the following optional parameters.
595
596    :param path: Path contains the path to the trace file.  If no path is given, it
597        uses the current directory by default.  If path is a file, and ends in
598        .dat, it's run through "trace-cmd report".  If it doesn't end in
599        ".dat", then it must be the output of a trace-cmd report run.  If path
600        is a directory that contains a trace.txt, that is assumed to be the
601        output of "trace-cmd report".  If path is a directory that doesn't
602        have a trace.txt but has a trace.dat, it runs trace-cmd report on the
603        trace.dat, saves it in trace.txt and then uses that.
604
605    :param name: is a string describing the trace.
606
607    :param normalize_time: is used to make all traces start from time 0 (the
608        default).  If normalize_time is False, the trace times are the same as
609        in the trace file.
610
611    :param scope: can be used to limit the parsing done on the trace.  The default
612        scope parses all the traces known to trappy.  If scope is thermal, only
613        the thermal classes are parsed.  If scope is sched, only the sched
614        classes are parsed.
615
616    :param events: A list of strings containing the name of the trace
617        events that you want to include in this FTrace object.  The
618        string must correspond to the event name (what you would pass
619        to "trace-cmd -e", i.e. 4th field in trace.txt)
620
621    :param window: a tuple indicating a time window.  The first
622        element in the tuple is the start timestamp and the second one
623        the end timestamp.  Timestamps are relative to the first trace
624        event that's parsed.  If you want to trace until the end of
625        the trace, set the second element to None.  If you want to use
626        timestamps extracted from the trace file use "abs_window". The
627        window is inclusive: trace events exactly matching the start
628        or end timestamps will be included.
629
630    :param abs_window: a tuple indicating an absolute time window.
631        This parameter is similar to the "window" one but its values
632        represent timestamps that are not normalized, (i.e. the ones
633        you find in the trace file). The window is inclusive.
634
635
636    :type path: str
637    :type name: str
638    :type normalize_time: bool
639    :type scope: str
640    :type events: list
641    :type window: tuple
642    :type abs_window: tuple
643
644    This is a simple example:
645    ::
646
647        import trappy
648        trappy.FTrace("trace_dir")
649
650    """
651
652    def __init__(self, path=".", name="", normalize_time=True, scope="all",
653                 events=[], window=(0, None), abs_window=(0, None)):
654        super(FTrace, self).__init__(name, normalize_time, scope, events,
655                                     window, abs_window)
656        self.raw_events = []
657        self.trace_path = self.__process_path(path)
658        self.__populate_metadata()
659        self._do_parse()
660
661    def __warn_about_txt_trace_files(self, trace_dat, raw_txt, formatted_txt):
662        self.__get_raw_event_list()
663        warn_text = ( "You appear to be parsing both raw and formatted "
664                      "trace files. TRAPpy now uses a unified format. "
665                      "If you have the {} file, remove the .txt files "
666                      "and try again. If not, you can manually move "
667                      "lines with the following events from {} to {} :"
668                      ).format(trace_dat, raw_txt, formatted_txt)
669        for raw_event in self.raw_events:
670            warn_text = warn_text+" \"{}\"".format(raw_event)
671
672        raise RuntimeError(warn_text)
673
674    def __process_path(self, basepath):
675        """Process the path and return the path to the trace text file"""
676
677        if os.path.isfile(basepath):
678            trace_name = os.path.splitext(basepath)[0]
679        else:
680            trace_name = os.path.join(basepath, "trace")
681
682        trace_txt = trace_name + ".txt"
683        trace_raw_txt = trace_name + ".raw.txt"
684        trace_dat = trace_name + ".dat"
685
686        if os.path.isfile(trace_dat):
687            # Warn users if raw.txt files are present
688            if os.path.isfile(trace_raw_txt):
689                self.__warn_about_txt_trace_files(trace_dat, trace_raw_txt, trace_txt)
690            # TXT traces must always be generated
691            if not os.path.isfile(trace_txt):
692                self.__run_trace_cmd_report(trace_dat)
693            # TXT traces must match the most recent binary trace
694            elif os.path.getmtime(trace_txt) < os.path.getmtime(trace_dat):
695                self.__run_trace_cmd_report(trace_dat)
696
697        return trace_txt
698
699    def __get_raw_event_list(self):
700        self.raw_events = []
701        # Generate list of events which need to be parsed in raw format
702        for event_class in (self.thermal_classes, self.sched_classes, self.dynamic_classes):
703            for trace_class in event_class.itervalues():
704                raw = getattr(trace_class, 'parse_raw', None)
705                if raw:
706                    name = getattr(trace_class, 'name', None)
707                    if name:
708                        self.raw_events.append(name)
709
710    def __run_trace_cmd_report(self, fname):
711        """Run "trace-cmd report [ -r raw_event ]* fname > fname.txt"
712
713        The resulting trace is stored in files with extension ".txt". If
714        fname is "my_trace.dat", the trace is stored in "my_trace.txt". The
715        contents of the destination file is overwritten if it exists.
716        Trace events which require unformatted output (raw_event == True)
717        are added to the command line with one '-r <event>' each event and
718        trace-cmd then prints those events without formatting.
719
720        """
721        from subprocess import check_output
722
723        cmd = ["trace-cmd", "report"]
724
725        if not os.path.isfile(fname):
726            raise IOError("No such file or directory: {}".format(fname))
727
728        trace_output = os.path.splitext(fname)[0] + ".txt"
729        # Ask for the raw event list and request them unformatted
730        self.__get_raw_event_list()
731        for raw_event in self.raw_events:
732            cmd.extend([ '-r', raw_event ])
733
734        cmd.append(fname)
735
736        with open(os.devnull) as devnull:
737            try:
738                out = check_output(cmd, stderr=devnull)
739            except OSError as exc:
740                if exc.errno == 2 and not exc.filename:
741                    raise OSError(2, "trace-cmd not found in PATH, is it installed?")
742                else:
743                    raise
744        with open(trace_output, "w") as fout:
745            fout.write(out)
746
747
748    def __populate_metadata(self):
749        """Populates trace metadata"""
750
751        # Meta Data as expected to be found in the parsed trace header
752        metadata_keys = ["version", "cpus"]
753
754        for key in metadata_keys:
755            setattr(self, "_" + key, None)
756
757        with open(self.trace_path) as fin:
758            for line in fin:
759                if not metadata_keys:
760                    return
761
762                metadata_pattern = r"^\b(" + "|".join(metadata_keys) + \
763                                   r")\b\s*=\s*([0-9]+)"
764                match = re.search(metadata_pattern, line)
765                if match:
766                    setattr(self, "_" + match.group(1), match.group(2))
767                    metadata_keys.remove(match.group(1))
768
769                if SPECIAL_FIELDS_RE.match(line):
770                    # Reached a valid trace line, abort metadata population
771                    return
772