#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright 2019 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """One-line documentation for perf_diff module. A detailed description of perf_diff. """ from __future__ import print_function __author__ = 'asharif@google.com (Ahmad Sharif)' import argparse import functools import re import sys import misc import tabulator ROWS_TO_SHOW = 'Rows_to_show_in_the_perf_table' TOTAL_EVENTS = 'Total_events_of_this_profile' def GetPerfDictFromReport(report_file): output = {} perf_report = PerfReport(report_file) for k, v in perf_report.sections.items(): if k not in output: output[k] = {} output[k][ROWS_TO_SHOW] = 0 output[k][TOTAL_EVENTS] = 0 for function in v.functions: out_key = '%s' % (function.name) output[k][out_key] = function.count output[k][TOTAL_EVENTS] += function.count if function.percent > 1: output[k][ROWS_TO_SHOW] += 1 return output def _SortDictionaryByValue(d): l = d.items() def GetFloat(x): if misc.IsFloat(x): return float(x) else: return x sorted_l = sorted(l, key=lambda x: GetFloat(x[1])) sorted_l.reverse() return [f[0] for f in sorted_l] class Tabulator(object): """Make tables.""" def __init__(self, all_dicts): self._all_dicts = all_dicts def PrintTable(self): for dicts in self._all_dicts: self.PrintTableHelper(dicts) def PrintTableHelper(self, dicts): """Transfrom dicts to tables.""" fields = {} for d in dicts: for f in d.keys(): if f not in fields: fields[f] = d[f] else: fields[f] = max(fields[f], d[f]) table = [] header = ['name'] for i in range(len(dicts)): header.append(i) table.append(header) sorted_fields = _SortDictionaryByValue(fields) for f in sorted_fields: row = [f] for d in dicts: if f in d: row.append(d[f]) else: row.append('0') table.append(row) print(tabulator.GetSimpleTable(table)) class Function(object): """Function for formatting.""" def __init__(self): self.count = 0 self.name = '' self.percent = 0 class Section(object): """Section formatting.""" def __init__(self, contents): self.name = '' self.raw_contents = contents self._ParseSection() def _ParseSection(self): matches = re.findall(r'Events: (\w+)\s+(.*)', self.raw_contents) assert len(matches) <= 1, 'More than one event found in 1 section' if not matches: return match = matches[0] self.name = match[1] self.count = misc.UnitToNumber(match[0]) self.functions = [] for line in self.raw_contents.splitlines(): if not line.strip(): continue if '%' not in line: continue if not line.startswith('#'): fields = [f for f in line.split(' ') if f] function = Function() function.percent = float(fields[0].strip('%')) function.count = int(fields[1]) function.name = ' '.join(fields[2:]) self.functions.append(function) class PerfReport(object): """Get report from raw report.""" def __init__(self, perf_file): self.perf_file = perf_file self._ReadFile() self.sections = {} self.metadata = {} self._section_contents = [] self._section_header = '' self._SplitSections() self._ParseSections() self._ParseSectionHeader() def _ParseSectionHeader(self): """Parse a header of a perf report file.""" # The "captured on" field is inaccurate - this actually refers to when the # report was generated, not when the data was captured. for line in self._section_header.splitlines(): line = line[2:] if ':' in line: key, val = line.strip().split(':', 1) key = key.strip() val = val.strip() self.metadata[key] = val def _ReadFile(self): self._perf_contents = open(self.perf_file).read() def _ParseSections(self): self.event_counts = {} self.sections = {} for section_content in self._section_contents: section = Section(section_content) section.name = self._GetHumanReadableName(section.name) self.sections[section.name] = section # TODO(asharif): Do this better. def _GetHumanReadableName(self, section_name): if not 'raw' in section_name: return section_name raw_number = section_name.strip().split(' ')[-1] for line in self._section_header.splitlines(): if raw_number in line: name = line.strip().split(' ')[5] return name def _SplitSections(self): self._section_contents = [] indices = [m.start() for m in re.finditer('# Events:', self._perf_contents)] indices.append(len(self._perf_contents)) for i in range(len(indices) - 1): section_content = self._perf_contents[indices[i]:indices[i + 1]] self._section_contents.append(section_content) self._section_header = '' if indices: self._section_header = self._perf_contents[0:indices[0]] class PerfDiffer(object): """Perf differ class.""" def __init__(self, reports, num_symbols, common_only): self._reports = reports self._num_symbols = num_symbols self._common_only = common_only self._common_function_names = {} def DoDiff(self): """The function that does the diff.""" section_names = self._FindAllSections() filename_dicts = [] summary_dicts = [] for report in self._reports: d = {} filename_dicts.append({'file': report.perf_file}) for section_name in section_names: if section_name in report.sections: d[section_name] = report.sections[section_name].count summary_dicts.append(d) all_dicts = [filename_dicts, summary_dicts] for section_name in section_names: function_names = self._GetTopFunctions(section_name, self._num_symbols) self._FindCommonFunctions(section_name) dicts = [] for report in self._reports: d = {} if section_name in report.sections: section = report.sections[section_name] # Get a common scaling factor for this report. common_scaling_factor = self._GetCommonScalingFactor(section) for function in section.functions: if function.name in function_names: key = '%s %s' % (section.name, function.name) d[key] = function.count # Compute a factor to scale the function count by in common_only # mode. if self._common_only and ( function.name in self._common_function_names[section.name]): d[key + ' scaled'] = common_scaling_factor * function.count dicts.append(d) all_dicts.append(dicts) mytabulator = Tabulator(all_dicts) mytabulator.PrintTable() def _FindAllSections(self): sections = {} for report in self._reports: for section in report.sections.values(): if section.name not in sections: sections[section.name] = section.count else: sections[section.name] = max(sections[section.name], section.count) return _SortDictionaryByValue(sections) def _GetCommonScalingFactor(self, section): unique_count = self._GetCount( section, lambda x: x in self._common_function_names[section.name]) return 100.0 / unique_count def _GetCount(self, section, filter_fun=None): total_count = 0 for function in section.functions: if not filter_fun or filter_fun(function.name): total_count += int(function.count) return total_count def _FindCommonFunctions(self, section_name): function_names_list = [] for report in self._reports: if section_name in report.sections: section = report.sections[section_name] function_names = {f.name for f in section.functions} function_names_list.append(function_names) self._common_function_names[section_name] = ( functools.reduce(set.intersection, function_names_list)) def _GetTopFunctions(self, section_name, num_functions): all_functions = {} for report in self._reports: if section_name in report.sections: section = report.sections[section_name] for f in section.functions[:num_functions]: if f.name in all_functions: all_functions[f.name] = max(all_functions[f.name], f.count) else: all_functions[f.name] = f.count # FIXME(asharif): Don't really need to sort these... return _SortDictionaryByValue(all_functions) def _GetFunctionsDict(self, section, function_names): d = {} for function in section.functions: if function.name in function_names: d[function.name] = function.count return d def Main(argv): """The entry of the main.""" parser = argparse.ArgumentParser() parser.add_argument( '-n', '--num_symbols', dest='num_symbols', default='5', help='The number of symbols to show.') parser.add_argument( '-c', '--common_only', dest='common_only', action='store_true', default=False, help='Diff common symbols only.') options, args = parser.parse_known_args(argv) try: reports = [] for report in args[1:]: report = PerfReport(report) reports.append(report) pd = PerfDiffer(reports, int(options.num_symbols), options.common_only) pd.DoDiff() finally: pass return 0 if __name__ == '__main__': sys.exit(Main(sys.argv))