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