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