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.path
28import re
29import subprocess
30import sys
31
32try:
33    from tkinter import *
34    from tkinter.font import Font
35    from tkinter.ttk import *
36except ImportError:
37    from Tkinter import *
38    from tkFont import Font
39    from ttk import *
40
41from utils import *
42
43PAD_X = 3
44PAD_Y = 3
45
46
47class CallTreeNode(object):
48
49  """Representing a node in call-graph."""
50
51  def __init__(self, percentage, function_name):
52    self.percentage = percentage
53    self.call_stack = [function_name]
54    self.children = []
55
56  def add_call(self, function_name):
57    self.call_stack.append(function_name)
58
59  def add_child(self, node):
60    self.children.append(node)
61
62  def __str__(self):
63    strs = self.dump()
64    return '\n'.join(strs)
65
66  def dump(self):
67    strs = []
68    strs.append('CallTreeNode percentage = %.2f' % self.percentage)
69    for function_name in self.call_stack:
70      strs.append(' %s' % function_name)
71    for child in self.children:
72      child_strs = child.dump()
73      strs.extend(['  ' + x for x in child_strs])
74    return strs
75
76
77class ReportItem(object):
78
79  """Representing one item in report, may contain a CallTree."""
80
81  def __init__(self, raw_line):
82    self.raw_line = raw_line
83    self.call_tree = None
84
85  def __str__(self):
86    strs = []
87    strs.append('ReportItem (raw_line %s)' % self.raw_line)
88    if self.call_tree is not None:
89      strs.append('%s' % self.call_tree)
90    return '\n'.join(strs)
91
92class EventReport(object):
93
94  """Representing report for one event attr."""
95
96  def __init__(self, common_report_context):
97    self.context = common_report_context[:]
98    self.title_line = None
99    self.report_items = []
100
101
102def parse_event_reports(lines):
103  # Parse common report context
104  common_report_context = []
105  line_id = 0
106  while line_id < len(lines):
107    line = lines[line_id]
108    if not line or line.find('Event:') == 0:
109      break
110    common_report_context.append(line)
111    line_id += 1
112
113  event_reports = []
114  in_report_context = True
115  cur_event_report = EventReport(common_report_context)
116  cur_report_item = None
117  call_tree_stack = {}
118  vertical_columns = []
119  last_node = None
120
121  has_skipped_callgraph = False
122
123  for line in lines[line_id:]:
124    if not line:
125      in_report_context = not in_report_context
126      if in_report_context:
127        cur_event_report = EventReport(common_report_context)
128      continue
129
130    if in_report_context:
131      cur_event_report.context.append(line)
132      if line.find('Event:') == 0:
133        event_reports.append(cur_event_report)
134      continue
135
136    if cur_event_report.title_line is None:
137      cur_event_report.title_line = line
138    elif not line[0].isspace():
139      cur_report_item = ReportItem(line)
140      cur_event_report.report_items.append(cur_report_item)
141      # Each report item can have different column depths.
142      vertical_columns = []
143    else:
144      for i in range(len(line)):
145        if line[i] == '|':
146          if not vertical_columns or vertical_columns[-1] < i:
147            vertical_columns.append(i)
148
149      if not line.strip('| \t'):
150        continue
151      if line.find('skipped in brief callgraph mode') != -1:
152        has_skipped_callgraph = True
153        continue
154
155      if line.find('-') == -1:
156        line = line.strip('| \t')
157        function_name = line
158        last_node.add_call(function_name)
159      else:
160        pos = line.find('-')
161        depth = -1
162        for i in range(len(vertical_columns)):
163          if pos >= vertical_columns[i]:
164            depth = i
165        assert depth != -1
166
167        line = line.strip('|- \t')
168        m = re.search(r'^([\d\.]+)%[-\s]+(.+)$', line)
169        if m:
170          percentage = float(m.group(1))
171          function_name = m.group(2)
172        else:
173          percentage = 100.0
174          function_name = line
175
176        node = CallTreeNode(percentage, function_name)
177        if depth == 0:
178          cur_report_item.call_tree = node
179        else:
180          call_tree_stack[depth - 1].add_child(node)
181        call_tree_stack[depth] = node
182        last_node = node
183
184  if has_skipped_callgraph:
185      log_warning('some callgraphs are skipped in brief callgraph mode')
186
187  return event_reports
188
189
190class ReportWindow(object):
191
192  """A window used to display report file."""
193
194  def __init__(self, master, report_context, title_line, report_items):
195    frame = Frame(master)
196    frame.pack(fill=BOTH, expand=1)
197
198    font = Font(family='courier', size=12)
199
200    # Report Context
201    for line in report_context:
202      label = Label(frame, text=line, font=font)
203      label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
204
205    # Space
206    label = Label(frame, text='', font=font)
207    label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
208
209    # Title
210    label = Label(frame, text='  ' + title_line, font=font)
211    label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
212
213    # Report Items
214    report_frame = Frame(frame)
215    report_frame.pack(fill=BOTH, expand=1)
216
217    yscrollbar = Scrollbar(report_frame)
218    yscrollbar.pack(side=RIGHT, fill=Y)
219    xscrollbar = Scrollbar(report_frame, orient=HORIZONTAL)
220    xscrollbar.pack(side=BOTTOM, fill=X)
221
222    tree = Treeview(report_frame, columns=[title_line], show='')
223    tree.pack(side=LEFT, fill=BOTH, expand=1)
224    tree.tag_configure('set_font', font=font)
225
226    tree.config(yscrollcommand=yscrollbar.set)
227    yscrollbar.config(command=tree.yview)
228    tree.config(xscrollcommand=xscrollbar.set)
229    xscrollbar.config(command=tree.xview)
230
231    self.display_report_items(tree, report_items)
232
233  def display_report_items(self, tree, report_items):
234    for report_item in report_items:
235      prefix_str = '+ ' if report_item.call_tree is not None else '  '
236      id = tree.insert(
237          '',
238          'end',
239          None,
240          values=[
241              prefix_str +
242              report_item.raw_line],
243          tag='set_font')
244      if report_item.call_tree is not None:
245        self.display_call_tree(tree, id, report_item.call_tree, 1)
246
247  def display_call_tree(self, tree, parent_id, node, indent):
248    id = parent_id
249    indent_str = '    ' * indent
250
251    if node.percentage != 100.0:
252      percentage_str = '%.2f%% ' % node.percentage
253    else:
254      percentage_str = ''
255
256    for i in range(len(node.call_stack)):
257      s = indent_str
258      s += '+ ' if node.children and i == len(node.call_stack) - 1 else '  '
259      s += percentage_str if i == 0 else ' ' * len(percentage_str)
260      s += node.call_stack[i]
261      child_open = False if i == len(node.call_stack) - 1 and indent > 1 else True
262      id = tree.insert(id, 'end', None, values=[s], open=child_open,
263                       tag='set_font')
264
265    for child in node.children:
266      self.display_call_tree(tree, id, child, indent + 1)
267
268
269def display_report_file(report_file):
270  fh = open(report_file, 'r')
271  lines = fh.readlines()
272  fh.close()
273
274  lines = [x.rstrip() for x in lines]
275  event_reports = parse_event_reports(lines)
276
277  if event_reports:
278    root = Tk()
279    for i in range(len(event_reports)):
280      report = event_reports[i]
281      parent = root if i == 0 else Toplevel(root)
282      ReportWindow(parent, report.context, report.title_line, report.report_items)
283    root.mainloop()
284
285
286def call_simpleperf_report(args, report_file):
287  output_fh = open(report_file, 'w')
288  simpleperf_path = get_host_binary_path('simpleperf')
289  args = [simpleperf_path, 'report', '--full-callgraph'] + args
290  subprocess.check_call(args, stdout=output_fh)
291  output_fh.close()
292
293
294def main():
295  if len(sys.argv) == 2 and os.path.isfile(sys.argv[1]):
296    display_report_file(sys.argv[1])
297  else:
298    call_simpleperf_report(sys.argv[1:], 'perf.report')
299    display_report_file('perf.report')
300
301
302if __name__ == '__main__':
303  main()
304