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