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
20There are two ways to use gui reporter. One way is to pass it a report file
21generated by simpleperf report command, and reporter will display it. The
22other ways is to pass it any arguments you want to use when calling
23simpleperf report command. The reporter will call `simpleperf report` to
24generate report file, and display it.
25"""
26
27import os
28import os.path
29import re
30import subprocess
31import sys
32
33try:
34    from tkinter import *
35    from tkinter.font import Font
36    from tkinter.ttk import *
37except ImportError:
38    from Tkinter import *
39    from tkFont import Font
40    from ttk import *
41
42from utils import *
43
44PAD_X = 3
45PAD_Y = 3
46
47
48class 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
78class 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
93class 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
103def 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
191class ReportWindow(object):
192
193  """A window used to display report file."""
194
195  def __init__(self, main, report_context, title_line, report_items):
196    frame = Frame(main)
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
270def 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
289def 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
300def 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    stdoutdata = bytes_to_str(stdoutdata)
306    return stdoutdata[stdoutdata.find('\n') + 1:]
307
308
309def main():
310    self_kill_after_sec = 0
311    args = sys.argv[1:]
312    if args and args[0] == "--self-kill-for-testing":
313        self_kill_after_sec = 1
314        args = args[1:]
315    if len(args) == 1 and os.path.isfile(args[0]):
316        display_report_file(args[0], self_kill_after_sec=self_kill_after_sec)
317
318    i = 0
319    args_for_report_cmd = []
320    show_gui = False
321    while i < len(args):
322        if args[i] == '-h' or args[i] == '--help':
323            print('report.py   A python wrapper for simpleperf report command.')
324            print('Options supported by simpleperf report command:')
325            print(get_simpleperf_report_help_msg())
326            print('\nOptions supported by report.py:')
327            print('--gui   Show report result in a gui window.')
328            print('\nIt also supports showing a report generated by simpleperf report cmd:')
329            print('\n  python report.py report_file')
330            sys.exit(0)
331        elif args[i] == '--gui':
332            show_gui = True
333            i += 1
334        else:
335            args_for_report_cmd.append(args[i])
336            i += 1
337
338    call_simpleperf_report(args_for_report_cmd, show_gui, self_kill_after_sec)
339
340
341if __name__ == '__main__':
342    main()
343