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