OSDN Git Service

Simpleperf: add a simple gui interface for report command.
authorYabin Cui <yabinc@google.com>
Tue, 6 Oct 2015 21:14:39 +0000 (14:14 -0700)
committerYabin Cui <yabinc@google.com>
Thu, 8 Oct 2015 20:35:38 +0000 (13:35 -0700)
Change-Id: Ie3293a3f223117bd2dc2b398d0a96f9df8cf484b

simpleperf/report.py [new file with mode: 0644]

diff --git a/simpleperf/report.py b/simpleperf/report.py
new file mode 100644 (file)
index 0000000..225998a
--- /dev/null
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Simpleperf gui reporter: provide gui interface for simpleperf report command.
+
+There are two ways to use gui reporter. One way is to pass it a report file
+generated by simpleperf report command, and reporter will display it. The
+other ways is to pass it any arguments you want to use when calling
+simpleperf report command. The reporter will call `simpleperf report` to
+generate report file, and display it.
+"""
+
+import os.path
+import re
+import subprocess
+import sys
+from tkFont import *
+from Tkinter import *
+from ttk import *
+
+PAD_X = 3
+PAD_Y = 3
+
+
+class CallTreeNode(object):
+
+  """Representing a node in call-graph."""
+
+  def __init__(self, percentage, function_name):
+    self.percentage = percentage
+    self.call_stack = [function_name]
+    self.children = []
+
+  def add_call(self, function_name):
+    self.call_stack.append(function_name)
+
+  def add_child(self, node):
+    self.children.append(node)
+
+  def __str__(self):
+    strs = self.dump()
+    return '\n'.join(strs)
+
+  def dump(self):
+    strs = []
+    strs.append('CallTreeNode percentage = %.2f' % self.percentage)
+    for function_name in self.call_stack:
+      strs.append(' %s' % function_name)
+    for child in self.children:
+      child_strs = child.dump()
+      strs.extend(['  ' + x for x in child_strs])
+    return strs
+
+
+class ReportItem(object):
+
+  """Representing one item in report, may contain a CallTree."""
+
+  def __init__(self, raw_line):
+    self.raw_line = raw_line
+    self.call_tree = None
+
+  def __str__(self):
+    strs = []
+    strs.append('ReportItem (raw_line %s)' % self.raw_line)
+    if self.call_tree is not None:
+      strs.append('%s' % self.call_tree)
+    return '\n'.join(strs)
+
+
+def parse_report_items(lines):
+  report_items = []
+  cur_report_item = None
+  call_tree_stack = {}
+  vertical_columns = []
+  last_node = None
+
+  for line in lines:
+    if not line:
+      continue
+    if not line[0].isspace():
+      cur_report_item = ReportItem(line)
+      report_items.append(cur_report_item)
+      # Each report item can have different column depths.
+      vertical_columns = []
+    else:
+      for i in range(len(line)):
+        if line[i] == '|':
+          if not vertical_columns or vertical_columns[-1] < i:
+            vertical_columns.append(i)
+
+      if not line.strip('| \t'):
+        continue
+      if line.find('-') == -1:
+        line = line.strip('| \t')
+        function_name = line
+        last_node.add_call(function_name)
+      else:
+        pos = line.find('-')
+        depth = -1
+        for i in range(len(vertical_columns)):
+          if pos >= vertical_columns[i]:
+            depth = i
+        assert depth != -1
+
+        line = line.strip('|- \t')
+        m = re.search(r'^([\d\.]+)%[-\s]+(.+)$', line)
+        if m:
+          percentage = float(m.group(1))
+          function_name = m.group(2)
+        else:
+          percentage = 100.0
+          function_name = line
+
+        node = CallTreeNode(percentage, function_name)
+        if depth == 0:
+          cur_report_item.call_tree = node
+        else:
+          call_tree_stack[depth - 1].add_child(node)
+        call_tree_stack[depth] = node
+        last_node = node
+
+  return report_items
+
+
+class ReportWindow(object):
+
+  """A window used to display report file."""
+
+  def __init__(self, master, report_context, title_line, report_items):
+    frame = Frame(master)
+    frame.pack(fill=BOTH, expand=1)
+
+    font = Font(family='courier', size=10)
+
+    # Report Context
+    for line in report_context:
+      label = Label(frame, text=line, font=font)
+      label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
+
+    # Space
+    label = Label(frame, text='', font=font)
+    label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
+
+    # Title
+    label = Label(frame, text='  ' + title_line, font=font)
+    label.pack(anchor=W, padx=PAD_X, pady=PAD_Y)
+
+    # Report Items
+    report_frame = Frame(frame)
+    report_frame.pack(fill=BOTH, expand=1)
+
+    yscrollbar = Scrollbar(report_frame)
+    yscrollbar.pack(side=RIGHT, fill=Y)
+    xscrollbar = Scrollbar(report_frame, orient=HORIZONTAL)
+    xscrollbar.pack(side=BOTTOM, fill=X)
+
+    tree = Treeview(report_frame, columns=[title_line], show='')
+    tree.pack(side=LEFT, fill=BOTH, expand=1)
+    tree.tag_configure('set_font', font=font)
+
+    tree.config(yscrollcommand=yscrollbar.set)
+    yscrollbar.config(command=tree.yview)
+    tree.config(xscrollcommand=xscrollbar.set)
+    xscrollbar.config(command=tree.xview)
+
+    self.display_report_items(tree, report_items)
+
+  def display_report_items(self, tree, report_items):
+    for report_item in report_items:
+      prefix_str = '+ ' if report_item.call_tree is not None else '  '
+      id = tree.insert(
+          '',
+          'end',
+          None,
+          values=[
+              prefix_str +
+              report_item.raw_line],
+          tag='set_font')
+      if report_item.call_tree is not None:
+        self.display_call_tree(tree, id, report_item.call_tree, 1)
+
+  def display_call_tree(self, tree, parent_id, node, indent):
+    id = parent_id
+    indent_str = '  ' * indent
+
+    if node.percentage != 100.0:
+      percentage_str = '%.2f%%' % node.percentage
+    else:
+      percentage_str = ''
+    first_open = True if node.percentage == 100.0 else False
+
+    for i in range(len(node.call_stack)):
+      s = indent_str
+      s += '+ ' if node.children else '  '
+      s += percentage_str if i == 0 else ' ' * len(percentage_str)
+      s += node.call_stack[i]
+      child_open = first_open if i == 0 else True
+      id = tree.insert(id, 'end', None, values=[s], open=child_open,
+                       tag='set_font')
+
+    for child in node.children:
+      self.display_call_tree(tree, id, child, indent + 1)
+
+
+def display_report_file(report_file):
+  fh = open(report_file, 'r')
+  lines = fh.readlines()
+  fh.close()
+
+  lines = [x.rstrip() for x in lines]
+
+  blank_line_index = -1
+  for i in range(len(lines)):
+    if not lines[i]:
+      blank_line_index = i
+      break
+  assert blank_line_index != -1
+  assert blank_line_index + 1 < len(lines)
+
+  report_context = lines[:blank_line_index]
+  title_line = lines[blank_line_index + 1]
+  report_items = parse_report_items(lines[blank_line_index + 2:])
+
+  root = Tk()
+  ReportWindow(root, report_context, title_line, report_items)
+  root.mainloop()
+
+
+def call_simpleperf_report(args, report_file):
+  output_fh = open(report_file, 'w')
+  args = ['simpleperf', 'report'] + args
+  subprocess.check_call(args, stdout=output_fh)
+  output_fh.close()
+
+
+def main():
+  if len(sys.argv) == 2 and os.path.isfile(sys.argv[1]):
+    display_report_file(sys.argv[1])
+  else:
+    call_simpleperf_report(sys.argv[1:], 'perf.report')
+    display_report_file('perf.report')
+
+
+if __name__ == '__main__':
+  main()