Home | History | Annotate | Download | only in scripts
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (C) 2015 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License");
      6 # you may not use this file except in compliance with the License.
      7 # You may obtain a copy of the License at
      8 #
      9 #      http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS,
     13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14 # See the License for the specific language governing permissions and
     15 # limitations under the License.
     16 #
     17 
     18 """Simpleperf gui reporter: provide gui interface for simpleperf report command.
     19 
     20 There are two ways to use gui reporter. One way is to pass it a report file
     21 generated by simpleperf report command, and reporter will display it. The
     22 other ways is to pass it any arguments you want to use when calling
     23 simpleperf report command. The reporter will call `simpleperf report` to
     24 generate report file, and display it.
     25 """
     26 
     27 import os
     28 import os.path
     29 import re
     30 import subprocess
     31 import sys
     32 
     33 try:
     34     from tkinter import *
     35     from tkinter.font import Font
     36     from tkinter.ttk import *
     37 except ImportError:
     38     from Tkinter import *
     39     from tkFont import Font
     40     from ttk import *
     41 
     42 from utils import *
     43 
     44 PAD_X = 3
     45 PAD_Y = 3
     46 
     47 
     48 class CallTreeNode(object):
     49 
     50   """Representing a node in call-graph."""
     51 
     52   def __init__(self, percentage, function_name):
     53     self.percentage = percentage
     54     self.call_stack = [function_name]
     55     self.children = []
     56 
     57   def add_call(self, function_name):
     58     self.call_stack.append(function_name)
     59 
     60   def add_child(self, node):
     61     self.children.append(node)
     62 
     63   def __str__(self):
     64     strs = self.dump()
     65     return '\n'.join(strs)
     66 
     67   def dump(self):
     68     strs = []
     69     strs.append('CallTreeNode percentage = %.2f' % self.percentage)
     70     for function_name in self.call_stack:
     71       strs.append(' %s' % function_name)
     72     for child in self.children:
     73       child_strs = child.dump()
     74       strs.extend(['  ' + x for x in child_strs])
     75     return strs
     76 
     77 
     78 class ReportItem(object):
     79 
     80   """Representing one item in report, may contain a CallTree."""
     81 
     82   def __init__(self, raw_line):
     83     self.raw_line = raw_line
     84     self.call_tree = None
     85 
     86   def __str__(self):
     87     strs = []
     88     strs.append('ReportItem (raw_line %s)' % self.raw_line)
     89     if self.call_tree is not None:
     90       strs.append('%s' % self.call_tree)
     91     return '\n'.join(strs)
     92 
     93 class EventReport(object):
     94 
     95   """Representing report for one event attr."""
     96 
     97   def __init__(self, common_report_context):
     98     self.context = common_report_context[:]
     99     self.title_line = None
    100     self.report_items = []
    101 
    102 
    103 def parse_event_reports(lines):
    104   # Parse common report context
    105   common_report_context = []
    106   line_id = 0
    107   while line_id < len(lines):
    108     line = lines[line_id]
    109     if not line or line.find('Event:') == 0:
    110       break
    111     common_report_context.append(line)
    112     line_id += 1
    113 
    114   event_reports = []
    115   in_report_context = True
    116   cur_event_report = EventReport(common_report_context)
    117   cur_report_item = None
    118   call_tree_stack = {}
    119   vertical_columns = []
    120   last_node = None
    121 
    122   has_skipped_callgraph = False
    123 
    124   for line in lines[line_id:]:
    125     if not line:
    126       in_report_context = not in_report_context
    127       if in_report_context:
    128         cur_event_report = EventReport(common_report_context)
    129       continue
    130 
    131     if in_report_context:
    132       cur_event_report.context.append(line)
    133       if line.find('Event:') == 0:
    134         event_reports.append(cur_event_report)
    135       continue
    136 
    137     if cur_event_report.title_line is None:
    138       cur_event_report.title_line = line
    139     elif not line[0].isspace():
    140       cur_report_item = ReportItem(line)
    141       cur_event_report.report_items.append(cur_report_item)
    142       # Each report item can have different column depths.
    143       vertical_columns = []
    144     else:
    145       for i in range(len(line)):
    146         if line[i] == '|':
    147           if not vertical_columns or vertical_columns[-1] < i:
    148             vertical_columns.append(i)
    149 
    150       if not line.strip('| \t'):
    151         continue
    152       if 'skipped in brief callgraph mode' in line:
    153         has_skipped_callgraph = True
    154         continue
    155 
    156       if line.find('-') == -1:
    157         line = line.strip('| \t')
    158         function_name = line
    159         last_node.add_call(function_name)
    160       else:
    161         pos = line.find('-')
    162         depth = -1
    163         for i in range(len(vertical_columns)):
    164           if pos >= vertical_columns[i]:
    165             depth = i
    166         assert depth != -1
    167 
    168         line = line.strip('|- \t')
    169         m = re.search(r'^([\d\.]+)%[-\s]+(.+)$', line)
    170         if m:
    171           percentage = float(m.group(1))
    172           function_name = m.group(2)
    173         else:
    174           percentage = 100.0
    175           function_name = line
    176 
    177         node = CallTreeNode(percentage, function_name)
    178         if depth == 0:
    179           cur_report_item.call_tree = node
    180         else:
    181           call_tree_stack[depth - 1].add_child(node)
    182         call_tree_stack[depth] = node
    183         last_node = node
    184 
    185   if has_skipped_callgraph:
    186       log_warning('some callgraphs are skipped in brief callgraph mode')
    187 
    188   return event_reports
    189 
    190 
    191 class ReportWindow(object):
    192 
    193   """A window used to display report file."""
    194 
    195   def __init__(self, master, report_context, title_line, report_items):
    196     frame = Frame(master)
    197     frame.pack(fill=BOTH, expand=1)
    198 
    199     font = Font(family='courier', size=12)
    200 
    201     # Report Context
    202     for line in report_context:
    203       label = Label(frame, text=line, font=font)
    204       label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
    205 
    206     # Space
    207     label = Label(frame, text='', font=font)
    208     label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
    209 
    210     # Title
    211     label = Label(frame, text='  ' + title_line, font=font)
    212     label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
    213 
    214     # Report Items
    215     report_frame = Frame(frame)
    216     report_frame.pack(fill=BOTH, expand=1)
    217 
    218     yscrollbar = Scrollbar(report_frame)
    219     yscrollbar.pack(side=RIGHT, fill=Y)
    220     xscrollbar = Scrollbar(report_frame, orient=HORIZONTAL)
    221     xscrollbar.pack(side=BOTTOM, fill=X)
    222 
    223     tree = Treeview(report_frame, columns=[title_line], show='')
    224     tree.pack(side=LEFT, fill=BOTH, expand=1)
    225     tree.tag_configure('set_font', font=font)
    226 
    227     tree.config(yscrollcommand=yscrollbar.set)
    228     yscrollbar.config(command=tree.yview)
    229     tree.config(xscrollcommand=xscrollbar.set)
    230     xscrollbar.config(command=tree.xview)
    231 
    232     self.display_report_items(tree, report_items)
    233 
    234   def display_report_items(self, tree, report_items):
    235     for report_item in report_items:
    236       prefix_str = '+ ' if report_item.call_tree is not None else '  '
    237       id = tree.insert(
    238           '',
    239           'end',
    240           None,
    241           values=[
    242               prefix_str +
    243               report_item.raw_line],
    244           tag='set_font')
    245       if report_item.call_tree is not None:
    246         self.display_call_tree(tree, id, report_item.call_tree, 1)
    247 
    248   def display_call_tree(self, tree, parent_id, node, indent):
    249     id = parent_id
    250     indent_str = '    ' * indent
    251 
    252     if node.percentage != 100.0:
    253       percentage_str = '%.2f%% ' % node.percentage
    254     else:
    255       percentage_str = ''
    256 
    257     for i in range(len(node.call_stack)):
    258       s = indent_str
    259       s += '+ ' if node.children and i == len(node.call_stack) - 1 else '  '
    260       s += percentage_str if i == 0 else ' ' * len(percentage_str)
    261       s += node.call_stack[i]
    262       child_open = False if i == len(node.call_stack) - 1 and indent > 1 else True
    263       id = tree.insert(id, 'end', None, values=[s], open=child_open,
    264                        tag='set_font')
    265 
    266     for child in node.children:
    267       self.display_call_tree(tree, id, child, indent + 1)
    268 
    269 
    270 def display_report_file(report_file, self_kill_after_sec):
    271     fh = open(report_file, 'r')
    272     lines = fh.readlines()
    273     fh.close()
    274 
    275     lines = [x.rstrip() for x in lines]
    276     event_reports = parse_event_reports(lines)
    277 
    278     if event_reports:
    279         root = Tk()
    280         for i in range(len(event_reports)):
    281             report = event_reports[i]
    282             parent = root if i == 0 else Toplevel(root)
    283             ReportWindow(parent, report.context, report.title_line, report.report_items)
    284         if self_kill_after_sec:
    285             root.after(self_kill_after_sec * 1000, lambda: root.destroy())
    286         root.mainloop()
    287 
    288 
    289 def call_simpleperf_report(args, show_gui, self_kill_after_sec):
    290     simpleperf_path = get_host_binary_path('simpleperf')
    291     if not show_gui:
    292         subprocess.check_call([simpleperf_path, 'report'] + args)
    293     else:
    294         report_file = 'perf.report'
    295         subprocess.check_call([simpleperf_path, 'report', '--full-callgraph'] + args +
    296                               ['-o', report_file])
    297         display_report_file(report_file, self_kill_after_sec=self_kill_after_sec)
    298 
    299 
    300 def get_simpleperf_report_help_msg():
    301     simpleperf_path = get_host_binary_path('simpleperf')
    302     args = [simpleperf_path, 'report', '-h']
    303     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
    304     (stdoutdata, _) = proc.communicate()
    305     return stdoutdata[stdoutdata.find('\n') + 1:]
    306 
    307 
    308 def main():
    309     self_kill_after_sec = 0
    310     args = sys.argv[1:]
    311     if args and args[0] == "--self-kill-for-testing":
    312         self_kill_after_sec = 1
    313         args = args[1:]
    314     if len(args) == 1 and os.path.isfile(args[0]):
    315         display_report_file(args[0], self_kill_after_sec=self_kill_after_sec)
    316 
    317     i = 0
    318     args_for_report_cmd = []
    319     show_gui = False
    320     while i < len(args):
    321         if args[i] == '-h' or args[i] == '--help':
    322             print('report.py   A python wrapper for simpleperf report command.')
    323             print('Options supported by simpleperf report command:')
    324             print(get_simpleperf_report_help_msg())
    325             print('\nOptions supported by report.py:')
    326             print('--gui   Show report result in a gui window.')
    327             print('\nIt also supports showing a report generated by simpleperf report cmd:')
    328             print('\n  python report.py report_file')
    329             sys.exit(0)
    330         elif args[i] == '--gui':
    331             show_gui = True
    332             i += 1
    333         else:
    334             args_for_report_cmd.append(args[i])
    335             i += 1
    336 
    337     call_simpleperf_report(args_for_report_cmd, show_gui, self_kill_after_sec)
    338 
    339 
    340 if __name__ == '__main__':
    341     main()
    342