summaryrefslogtreecommitdiff
path: root/perfprofd/scripts/perf_proto_stack_sqlite_flame.py
diff options
context:
space:
mode:
Diffstat (limited to 'perfprofd/scripts/perf_proto_stack_sqlite_flame.py')
-rw-r--r--perfprofd/scripts/perf_proto_stack_sqlite_flame.py272
1 files changed, 272 insertions, 0 deletions
diff --git a/perfprofd/scripts/perf_proto_stack_sqlite_flame.py b/perfprofd/scripts/perf_proto_stack_sqlite_flame.py
new file mode 100644
index 00000000..3eb2379a
--- /dev/null
+++ b/perfprofd/scripts/perf_proto_stack_sqlite_flame.py
@@ -0,0 +1,272 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2018 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.
+
+# Make sure that simpleperf's inferno is on the PYTHONPATH, e.g., run as
+# PYTHONPATH=$PYTHONPATH:$ANDROID_BUILD_TOP/system/extras/simpleperf/scripts/inferno python ..
+
+import argparse
+import itertools
+import sqlite3
+
+
+class Callsite(object):
+ def __init__(self, dso_id, sym_id):
+ self.dso_id = dso_id
+ self.sym_id = sym_id
+ self.count = 0
+ self.child_map = {}
+ self.id = self._get_next_callsite_id()
+
+ def add(self, dso_id, sym_id):
+ if (dso_id, sym_id) in self.child_map:
+ return self.child_map[(dso_id, sym_id)]
+ new_callsite = Callsite(dso_id, sym_id)
+ self.child_map[(dso_id, sym_id)] = new_callsite
+ return new_callsite
+
+ def child_count_to_self(self):
+ self.count = reduce(lambda x, y: x + y[1].count, self.child_map.iteritems(), 0)
+
+ def trim(self, local_threshold_in_percent, global_threshold):
+ local_threshold = local_threshold_in_percent * 0.01 * self.count
+ threshold = max(local_threshold, global_threshold)
+ for k, v in self.child_map.items():
+ if v.count < threshold:
+ del self.child_map[k]
+ for _, v in self.child_map.iteritems():
+ v.trim(local_threshold_in_percent, global_threshold)
+
+ def _get_str(self, id, m):
+ if id in m:
+ return m[id]
+ return str(id)
+
+ def print_callsite_ascii(self, depth, indent, dsos, syms):
+
+ print ' ' * indent + "%s (%s) [%d]" % (self._get_str(self.sym_id, syms),
+ self._get_str(self.dso_id, dsos),
+ self.count)
+ if depth == 0:
+ return
+ for v in sorted(self.child_map.itervalues, key=lambda x: x.count, reverse=True):
+ v.print_callsite_ascii(depth - 1, indent + 1, dsos, syms)
+
+ # Functions for flamegraph compatibility.
+
+ callsite_counter = 0
+
+ @classmethod
+ def _get_next_callsite_id(cls):
+ cls.callsite_counter += 1
+ return cls.callsite_counter
+
+ def create_children_list(self):
+ self.children = sorted(self.child_map.itervalues(), key=lambda x: x.count, reverse=True)
+
+ def generate_offset(self, start_offset):
+ self.offset = start_offset
+ child_offset = start_offset
+ for child in self.children:
+ child_offset = child.generate_offset(child_offset)
+ return self.offset + self.count
+
+ def svgrenderer_compat(self, dsos, syms):
+ self.create_children_list()
+ self.method = self._get_str(self.sym_id, syms)
+ self.dso = self._get_str(self.dso_id, dsos)
+ self.offset = 0
+ for c in self.children:
+ c.svgrenderer_compat(dsos, syms)
+
+ def weight(self):
+ return float(self.count)
+
+ def get_max_depth(self):
+ if self.child_map:
+ return max([c.get_max_depth() for c in self.child_map.itervalues()]) + 1
+ return 1
+
+
+class SqliteReader(object):
+ def __init__(self):
+ self.root = Callsite("root", "root")
+ self.dsos = {}
+ self.syms = {}
+
+ def open(self, f):
+ self._conn = sqlite3.connect(f)
+ self._c = self._conn.cursor()
+
+ def close(self):
+ self._conn.close()
+
+ def read(self, local_threshold_in_percent, global_threshold_in_percent, limit,
+ skip_simpleperf):
+ # Read aux tables first, as we need to find the kernel symbols.
+ def read_table(name, dest_table):
+ self._c.execute('select id, name from %s' % (name))
+ while True:
+ rows = self._c.fetchmany(100)
+ if not rows:
+ break
+ for row in rows:
+ dest_table[row[0]] = row[1]
+
+ print 'Reading DSOs'
+ read_table('dsos', self.dsos)
+
+ print 'Reading symbol strings'
+ read_table('syms', self.syms)
+
+ kernel_sym_id = None
+ for i, v in self.syms.iteritems():
+ if v == '[kernel]':
+ kernel_sym_id = i
+ break
+
+ skip_query_str = ""
+ if skip_simpleperf:
+ self._c.execute('select id from pids where name = "simpleperf"')
+ pid_row = self._c.fetchone()
+ if pid_row:
+ skip_query_join = "as st join samples sa on st.sample_id = sa.id "
+ skip_query_str = skip_query_join + "where sa.pid_id != %d" % (pid_row[0])
+
+ query_prefix = 'select sample_id, depth, dso_id, sym_id from stacks '
+ query_suffix = ' order by sample_id asc, depth desc'
+
+ print 'Reading samples'
+ self._c.execute(query_prefix + skip_query_str + query_suffix)
+
+ last_sample_id = None
+ chain = None
+ count = 0
+ while True:
+ rows = self._c.fetchmany(100)
+
+ if not rows:
+ break
+ for row in rows:
+ if row[3] == kernel_sym_id and row[1] == 0:
+ # Skip kernel.
+ continue
+ if row[0] != last_sample_id:
+ last_sample_id = row[0]
+ chain = self.root
+ chain = chain.add(row[2], row[3])
+ chain.count = chain.count + 1
+ count = count + len(rows)
+ if limit is not None and count >= limit:
+ print 'Breaking as limit is reached'
+ break
+
+ self.root.child_count_to_self()
+ global_threshold = global_threshold_in_percent * 0.01 * self.root.count
+ self.root.trim(local_threshold_in_percent, global_threshold)
+
+ def print_data_ascii(self, depth):
+ self.root.print_callsite_ascii(depth, 0, self.dsos, self.syms)
+
+ def get_script_js(self):
+ # Try to get the data directly (if packaged with embedded_loader).
+ import os.path
+ import sys
+ if '__loader__' in globals():
+ try:
+ js = __loader__.get_data(os.path.join(os.path.dirname(__file__), "script.js"))
+ if js is not None and len(js) > 0:
+ return js
+ except:
+ pass
+ # See if we can find it another way.
+ rel_paths = [
+ # Maybe we're being run packaged.
+ "script.js",
+ # Maybe we're being run directly.
+ "../../simpleperf/scripts/inferno/script.js",
+ ]
+ for rel_path in rel_paths:
+ script_js = os.path.join(os.path.dirname(__file__), rel_path)
+ if os.path.exists(script_js):
+ with open(script_js, 'r') as script_f:
+ return script_f.read()
+ return None
+
+ def print_svg(self, filename, depth):
+ from svg_renderer import render_svg
+ self.root.svgrenderer_compat(self.dsos, self.syms)
+ self.root.generate_offset(0)
+ f = open(filename, 'w')
+ f.write('''
+<html>
+<body>
+<div id='flamegraph_id' style='font-family: Monospace;'>
+<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;} </style>
+<style type="text/css"> .t:hover { cursor:pointer; } </style>
+''')
+
+ class FakeProcess:
+ def __init__(self):
+ self.props = {'trace_offcpu': False}
+ fake_process = FakeProcess()
+ render_svg(fake_process, self.root, f, 'hot')
+
+ f.write('''
+</div>
+''')
+
+ # Emit script.js, if we can find it.
+ script_data = self.get_script_js()
+ if script_data is not None:
+ f.write('<script>\n')
+ f.write(script_data)
+ f.write('''
+</script>
+<br/><br/>
+<div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>
+<script>document.addEventListener('DOMContentLoaded', flamegraphInit);</script>
+</body>
+</html>
+''')
+ f.close()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='''Translate a perfprofd database into a flame
+ representation''')
+
+ parser.add_argument('file', help='the sqlite database to use', metavar='file', type=str)
+
+ parser.add_argument('--html-out', help='output file for HTML flame graph', type=str)
+ parser.add_argument('--threshold', help='child threshold in percent', type=float, default=5)
+ parser.add_argument('--global-threshold', help='global threshold in percent', type=float,
+ default=.1)
+ parser.add_argument('--depth', help='depth to print to', type=int, default=10)
+ parser.add_argument('--limit', help='limit to given number of stack trace entries', type=int)
+ parser.add_argument('--skip-simpleperf', help='skip simpleperf samples', action='store_const',
+ const=True)
+
+ args = parser.parse_args()
+ if args is not None:
+ sql_out = SqliteReader()
+ sql_out.open(args.file)
+ sql_out.read(args.threshold, args.global_threshold, args.limit,
+ args.skip_simpleperf is not None)
+ if args.html_out is None:
+ sql_out.print_data_ascii(args.depth)
+ else:
+ sql_out.print_svg(args.html_out, args.depth)
+ sql_out.close()