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