Init
This commit is contained in:
@ -0,0 +1,137 @@
|
||||
#
|
||||
# Copyright (C) 2016 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.
|
||||
#
|
||||
|
||||
|
||||
class CallSite(object):
|
||||
|
||||
def __init__(self, method, dso):
|
||||
self.method = method
|
||||
self.dso = dso
|
||||
|
||||
|
||||
class Thread(object):
|
||||
|
||||
def __init__(self, tid, pid):
|
||||
self.tid = tid
|
||||
self.pid = pid
|
||||
self.name = ""
|
||||
self.samples = []
|
||||
self.flamegraph = FlameGraphCallSite("root", "", 0)
|
||||
self.num_samples = 0
|
||||
self.num_events = 0
|
||||
|
||||
def add_callchain(self, callchain, symbol, sample):
|
||||
self.name = sample.thread_comm
|
||||
self.num_samples += 1
|
||||
self.num_events += sample.period
|
||||
chain = []
|
||||
for j in range(callchain.nr):
|
||||
entry = callchain.entries[callchain.nr - j - 1]
|
||||
if entry.ip == 0:
|
||||
continue
|
||||
chain.append(CallSite(entry.symbol.symbol_name, entry.symbol.dso_name))
|
||||
|
||||
chain.append(CallSite(symbol.symbol_name, symbol.dso_name))
|
||||
self.flamegraph.add_callchain(chain, sample.period)
|
||||
|
||||
|
||||
class Process(object):
|
||||
|
||||
def __init__(self, name, pid):
|
||||
self.name = name
|
||||
self.pid = pid
|
||||
self.threads = {}
|
||||
self.cmd = ""
|
||||
self.props = {}
|
||||
# num_samples is the count of samples recorded in the profiling file.
|
||||
self.num_samples = 0
|
||||
# num_events is the count of events contained in all samples. Each sample contains a
|
||||
# count of events happened since last sample. If we use cpu-cycles event, the count
|
||||
# shows how many cpu-cycles have happened during recording.
|
||||
self.num_events = 0
|
||||
|
||||
def get_thread(self, tid, pid):
|
||||
thread = self.threads.get(tid)
|
||||
if thread is None:
|
||||
thread = self.threads[tid] = Thread(tid, pid)
|
||||
return thread
|
||||
|
||||
def add_sample(self, sample, symbol, callchain):
|
||||
thread = self.get_thread(sample.tid, sample.pid)
|
||||
thread.add_callchain(callchain, symbol, sample)
|
||||
self.num_samples += 1
|
||||
# sample.period is the count of events happened since last sample.
|
||||
self.num_events += sample.period
|
||||
|
||||
|
||||
class FlameGraphCallSite(object):
|
||||
|
||||
callsite_counter = 0
|
||||
@classmethod
|
||||
def _get_next_callsite_id(cls):
|
||||
cls.callsite_counter += 1
|
||||
return cls.callsite_counter
|
||||
|
||||
def __init__(self, method, dso, callsite_id):
|
||||
# map from (dso, method) to FlameGraphCallSite. Used to speed up add_callchain().
|
||||
self.child_dict = {}
|
||||
self.children = []
|
||||
self.method = method
|
||||
self.dso = dso
|
||||
self.num_events = 0
|
||||
self.offset = 0 # Offset allows position nodes in different branches.
|
||||
self.id = callsite_id
|
||||
|
||||
def weight(self):
|
||||
return float(self.num_events)
|
||||
|
||||
def add_callchain(self, chain, num_events):
|
||||
self.num_events += num_events
|
||||
current = self
|
||||
for callsite in chain:
|
||||
current = current.get_child(callsite)
|
||||
current.num_events += num_events
|
||||
|
||||
def get_child(self, callsite):
|
||||
key = (callsite.dso, callsite.method)
|
||||
child = self.child_dict.get(key)
|
||||
if child is None:
|
||||
child = self.child_dict[key] = FlameGraphCallSite(callsite.method, callsite.dso,
|
||||
self._get_next_callsite_id())
|
||||
return child
|
||||
|
||||
def trim_callchain(self, min_num_events, max_depth, depth=0):
|
||||
""" Remove call sites with num_events < min_num_events in the subtree.
|
||||
Remaining children are collected in a list.
|
||||
"""
|
||||
if depth <= max_depth:
|
||||
for key in self.child_dict:
|
||||
child = self.child_dict[key]
|
||||
if child.num_events >= min_num_events:
|
||||
child.trim_callchain(min_num_events, max_depth, depth + 1)
|
||||
self.children.append(child)
|
||||
# Relese child_dict since it will not be used.
|
||||
self.child_dict = None
|
||||
|
||||
def get_max_depth(self):
|
||||
return max([c.get_max_depth() for c in self.children]) + 1 if self.children else 1
|
||||
|
||||
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.num_events
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2016 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.
|
||||
#
|
||||
|
||||
"""
|
||||
Inferno is a tool to generate flamegraphs for android programs. It was originally written
|
||||
to profile surfaceflinger (Android compositor) but it can be used for other C++ program.
|
||||
It uses simpleperf to collect data. Programs have to be compiled with frame pointers which
|
||||
excludes ART based programs for the time being.
|
||||
|
||||
Here is how it works:
|
||||
|
||||
1/ Data collection is started via simpleperf and pulled locally as "perf.data".
|
||||
2/ The raw format is parsed, callstacks are merged to form a flamegraph data structure.
|
||||
3/ The data structure is used to generate a SVG embedded into an HTML page.
|
||||
4/ Javascript is injected to allow flamegraph navigation, search, coloring model.
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# fmt: off
|
||||
# pylint: disable=wrong-import-position
|
||||
SCRIPTS_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
sys.path.append(SCRIPTS_PATH)
|
||||
from simpleperf_report_lib import ReportLib
|
||||
from simpleperf_utils import (log_exit, log_fatal, AdbHelper, open_report_in_browser,
|
||||
BaseArgumentParser)
|
||||
|
||||
from data_types import Process
|
||||
from svg_renderer import get_proper_scaled_time_string, render_svg
|
||||
# fmt: on
|
||||
|
||||
|
||||
def collect_data(args):
|
||||
""" Run app_profiler.py to generate record file. """
|
||||
app_profiler_args = [sys.executable, os.path.join(SCRIPTS_PATH, "app_profiler.py"), "-nb"]
|
||||
if args.app:
|
||||
app_profiler_args += ["-p", args.app]
|
||||
elif args.native_program:
|
||||
app_profiler_args += ["-np", args.native_program]
|
||||
elif args.pid != -1:
|
||||
app_profiler_args += ['--pid', str(args.pid)]
|
||||
elif args.system_wide:
|
||||
app_profiler_args += ['--system_wide']
|
||||
else:
|
||||
log_exit("Please set profiling target with -p, -np, --pid or --system_wide option.")
|
||||
if args.compile_java_code:
|
||||
app_profiler_args.append("--compile_java_code")
|
||||
if args.disable_adb_root:
|
||||
app_profiler_args.append("--disable_adb_root")
|
||||
record_arg_str = ""
|
||||
if args.dwarf_unwinding:
|
||||
record_arg_str += "-g "
|
||||
else:
|
||||
record_arg_str += "--call-graph fp "
|
||||
if args.events:
|
||||
tokens = args.events.split()
|
||||
if len(tokens) == 2:
|
||||
num_events = tokens[0]
|
||||
event_name = tokens[1]
|
||||
record_arg_str += "-c %s -e %s " % (num_events, event_name)
|
||||
else:
|
||||
log_exit("Event format string of -e option cann't be recognized.")
|
||||
logging.info("Using event sampling (-c %s -e %s)." % (num_events, event_name))
|
||||
else:
|
||||
record_arg_str += "-f %d " % args.sample_frequency
|
||||
logging.info("Using frequency sampling (-f %d)." % args.sample_frequency)
|
||||
record_arg_str += "--duration %d " % args.capture_duration
|
||||
app_profiler_args += ["-r", record_arg_str]
|
||||
returncode = subprocess.call(app_profiler_args)
|
||||
return returncode == 0
|
||||
|
||||
|
||||
def parse_samples(process, args, sample_filter_fn):
|
||||
"""Read samples from record file.
|
||||
process: Process object
|
||||
args: arguments
|
||||
sample_filter_fn: if not None, is used to modify and filter samples.
|
||||
It returns false for samples should be filtered out.
|
||||
"""
|
||||
|
||||
record_file = args.record_file
|
||||
symfs_dir = args.symfs
|
||||
kallsyms_file = args.kallsyms
|
||||
|
||||
lib = ReportLib()
|
||||
|
||||
lib.ShowIpForUnknownSymbol()
|
||||
if symfs_dir:
|
||||
lib.SetSymfs(symfs_dir)
|
||||
if record_file:
|
||||
lib.SetRecordFile(record_file)
|
||||
if kallsyms_file:
|
||||
lib.SetKallsymsFile(kallsyms_file)
|
||||
lib.SetReportOptions(args.report_lib_options)
|
||||
process.cmd = lib.GetRecordCmd()
|
||||
product_props = lib.MetaInfo().get("product_props")
|
||||
if product_props:
|
||||
manufacturer, model, name = product_props.split(':')
|
||||
process.props['ro.product.manufacturer'] = manufacturer
|
||||
process.props['ro.product.model'] = model
|
||||
process.props['ro.product.name'] = name
|
||||
if lib.MetaInfo().get('trace_offcpu') == 'true':
|
||||
process.props['trace_offcpu'] = True
|
||||
if args.one_flamegraph:
|
||||
log_exit("It doesn't make sense to report with --one-flamegraph for perf.data " +
|
||||
"recorded with --trace-offcpu.""")
|
||||
else:
|
||||
process.props['trace_offcpu'] = False
|
||||
|
||||
while True:
|
||||
sample = lib.GetNextSample()
|
||||
if sample is None:
|
||||
lib.Close()
|
||||
break
|
||||
symbol = lib.GetSymbolOfCurrentSample()
|
||||
callchain = lib.GetCallChainOfCurrentSample()
|
||||
if sample_filter_fn and not sample_filter_fn(sample, symbol, callchain):
|
||||
continue
|
||||
process.add_sample(sample, symbol, callchain)
|
||||
|
||||
if process.pid == 0:
|
||||
main_threads = [thread for thread in process.threads.values() if thread.tid == thread.pid]
|
||||
if main_threads:
|
||||
process.name = main_threads[0].name
|
||||
process.pid = main_threads[0].pid
|
||||
|
||||
for thread in process.threads.values():
|
||||
min_event_count = thread.num_events * args.min_callchain_percentage * 0.01
|
||||
thread.flamegraph.trim_callchain(min_event_count, args.max_callchain_depth)
|
||||
|
||||
logging.info("Parsed %s callchains." % process.num_samples)
|
||||
|
||||
|
||||
def get_local_asset_content(local_path):
|
||||
"""
|
||||
Retrieves local package text content
|
||||
:param local_path: str, filename of local asset
|
||||
:return: str, the content of local_path
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), local_path), 'r') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def output_report(process, args):
|
||||
"""
|
||||
Generates a HTML report representing the result of simpleperf sampling as flamegraph
|
||||
:param process: Process object
|
||||
:return: str, absolute path to the file
|
||||
"""
|
||||
f = open(args.report_path, 'w')
|
||||
filepath = os.path.realpath(f.name)
|
||||
if not args.embedded_flamegraph:
|
||||
f.write("<html><body>")
|
||||
f.write("<div id='flamegraph_id' style='font-family: Monospace; %s'>" % (
|
||||
"display: none;" if args.embedded_flamegraph else ""))
|
||||
f.write("""<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;}
|
||||
</style>""")
|
||||
f.write('<style type="text/css"> .t:hover { cursor:pointer; } </style>')
|
||||
f.write('<img height="180" alt = "Embedded Image" src ="data')
|
||||
f.write(get_local_asset_content("inferno.b64"))
|
||||
f.write('"/>')
|
||||
process_entry = ("Process : %s (%d)<br/>" % (process.name, process.pid)) if process.pid else ""
|
||||
thread_entry = '' if args.one_flamegraph else ('Threads: %d<br/>' % len(process.threads))
|
||||
if process.props['trace_offcpu']:
|
||||
event_entry = 'Total time: %s<br/>' % get_proper_scaled_time_string(process.num_events)
|
||||
else:
|
||||
event_entry = 'Event count: %s<br/>' % ("{:,}".format(process.num_events))
|
||||
# TODO: collect capture duration info from perf.data.
|
||||
duration_entry = ("Duration: %s seconds<br/>" % args.capture_duration
|
||||
) if args.capture_duration else ""
|
||||
f.write("""<div style='display:inline-block;'>
|
||||
<font size='8'>
|
||||
Inferno Flamegraph Report%s</font><br/><br/>
|
||||
%s
|
||||
Date : %s<br/>
|
||||
%s
|
||||
Samples : %d<br/>
|
||||
%s
|
||||
%s""" % ((': ' + args.title) if args.title else '',
|
||||
process_entry,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
|
||||
thread_entry,
|
||||
process.num_samples,
|
||||
event_entry,
|
||||
duration_entry))
|
||||
if 'ro.product.model' in process.props:
|
||||
f.write(
|
||||
"Machine : %s (%s) by %s<br/>" %
|
||||
(process.props["ro.product.model"],
|
||||
process.props["ro.product.name"],
|
||||
process.props["ro.product.manufacturer"]))
|
||||
if process.cmd:
|
||||
f.write("Capture : %s<br/><br/>" % process.cmd)
|
||||
f.write("</div>")
|
||||
f.write("""<br/><br/>
|
||||
<div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>""")
|
||||
f.write("<script>%s</script>" % get_local_asset_content("script.js"))
|
||||
if not args.embedded_flamegraph:
|
||||
f.write("<script>document.addEventListener('DOMContentLoaded', flamegraphInit);</script>")
|
||||
|
||||
# Sort threads by the event count in a thread.
|
||||
for thread in sorted(process.threads.values(), key=lambda x: x.num_events, reverse=True):
|
||||
thread_name = 'One flamegraph' if args.one_flamegraph else ('Thread %d (%s)' %
|
||||
(thread.tid, thread.name))
|
||||
f.write("<br/><br/><b>%s (%d samples):</b><br/>\n\n\n\n" %
|
||||
(thread_name, thread.num_samples))
|
||||
render_svg(process, thread.flamegraph, f, args.color)
|
||||
|
||||
f.write("</div>")
|
||||
if not args.embedded_flamegraph:
|
||||
f.write("</body></html")
|
||||
f.close()
|
||||
return "file://" + filepath
|
||||
|
||||
|
||||
def generate_threads_offsets(process):
|
||||
for thread in process.threads.values():
|
||||
thread.flamegraph.generate_offset(0)
|
||||
|
||||
|
||||
def collect_machine_info(process):
|
||||
adb = AdbHelper()
|
||||
process.props = {}
|
||||
process.props['ro.product.model'] = adb.get_property('ro.product.model')
|
||||
process.props['ro.product.name'] = adb.get_property('ro.product.name')
|
||||
process.props['ro.product.manufacturer'] = adb.get_property('ro.product.manufacturer')
|
||||
|
||||
|
||||
def main():
|
||||
# Allow deep callchain with length >1000.
|
||||
sys.setrecursionlimit(1500)
|
||||
parser = BaseArgumentParser(description="""Report samples in perf.data. Default option
|
||||
is: "-np surfaceflinger -f 6000 -t 10".""")
|
||||
record_group = parser.add_argument_group('Record options')
|
||||
record_group.add_argument('-du', '--dwarf_unwinding', action='store_true', help="""Perform
|
||||
unwinding using dwarf instead of fp.""")
|
||||
record_group.add_argument('-e', '--events', default="", help="""Sample based on event
|
||||
occurences instead of frequency. Format expected is
|
||||
"event_counts event_name". e.g: "10000 cpu-cyles". A few examples
|
||||
of event_name: cpu-cycles, cache-references, cache-misses,
|
||||
branch-instructions, branch-misses""")
|
||||
record_group.add_argument('-f', '--sample_frequency', type=int, default=6000, help="""Sample
|
||||
frequency""")
|
||||
record_group.add_argument('--compile_java_code', action='store_true',
|
||||
help="""On Android N and Android O, we need to compile Java code
|
||||
into native instructions to profile Java code. Android O
|
||||
also needs wrap.sh in the apk to use the native
|
||||
instructions.""")
|
||||
record_group.add_argument('-np', '--native_program', default="surfaceflinger", help="""Profile
|
||||
a native program. The program should be running on the device.
|
||||
Like -np surfaceflinger.""")
|
||||
record_group.add_argument('-p', '--app', help="""Profile an Android app, given the package
|
||||
name. Like -p com.example.android.myapp.""")
|
||||
record_group.add_argument('--pid', type=int, default=-1, help="""Profile a native program
|
||||
with given pid, the pid should exist on the device.""")
|
||||
record_group.add_argument('--record_file', default='perf.data', help='Default is perf.data.')
|
||||
record_group.add_argument('-sc', '--skip_collection', action='store_true', help="""Skip data
|
||||
collection""")
|
||||
record_group.add_argument('--system_wide', action='store_true', help='Profile system wide.')
|
||||
record_group.add_argument('-t', '--capture_duration', type=int, default=10, help="""Capture
|
||||
duration in seconds.""")
|
||||
|
||||
report_group = parser.add_argument_group('Report options')
|
||||
report_group.add_argument('-c', '--color', default='hot', choices=['hot', 'dso', 'legacy'],
|
||||
help="""Color theme: hot=percentage of samples, dso=callsite DSO
|
||||
name, legacy=brendan style""")
|
||||
report_group.add_argument('--embedded_flamegraph', action='store_true', help="""Generate
|
||||
embedded flamegraph.""")
|
||||
report_group.add_argument('--kallsyms', help='Set the path to find kernel symbols.')
|
||||
report_group.add_argument('--min_callchain_percentage', default=0.01, type=float, help="""
|
||||
Set min percentage of callchains shown in the report.
|
||||
It is used to limit nodes shown in the flamegraph. For example,
|
||||
when set to 0.01, only callchains taking >= 0.01%% of the event
|
||||
count of the owner thread are collected in the report.""")
|
||||
report_group.add_argument('--max_callchain_depth', default=1000000000, type=int, help="""
|
||||
Set maximum depth of callchains shown in the report. It is used
|
||||
to limit the nodes shown in the flamegraph and avoid processing
|
||||
limits. For example, when set to 10, callstacks will be cut after
|
||||
the tenth frame.""")
|
||||
report_group.add_argument('--no_browser', action='store_true', help="""Don't open report
|
||||
in browser.""")
|
||||
report_group.add_argument('-o', '--report_path', default='report.html', help="""Set report
|
||||
path.""")
|
||||
report_group.add_argument('--one-flamegraph', action='store_true', help="""Generate one
|
||||
flamegraph instead of one for each thread.""")
|
||||
report_group.add_argument('--symfs', help="""Set the path to find binaries with symbols and
|
||||
debug info.""")
|
||||
report_group.add_argument('--title', help='Show a title in the report.')
|
||||
parser.add_report_lib_options(
|
||||
report_group, sample_filter_group=report_group, sample_filter_with_pid_shortcut=False)
|
||||
|
||||
debug_group = parser.add_argument_group('Debug options')
|
||||
debug_group.add_argument('--disable_adb_root', action='store_true', help="""Force adb to run
|
||||
in non root mode.""")
|
||||
args = parser.parse_args()
|
||||
process = Process("", 0)
|
||||
|
||||
if not args.skip_collection:
|
||||
if args.pid != -1:
|
||||
process.pid = args.pid
|
||||
args.native_program = ''
|
||||
if args.system_wide:
|
||||
process.pid = -1
|
||||
args.native_program = ''
|
||||
|
||||
if args.system_wide:
|
||||
process.name = 'system_wide'
|
||||
else:
|
||||
process.name = args.app or args.native_program or ('Process %d' % args.pid)
|
||||
logging.info("Starting data collection stage for '%s'." % process.name)
|
||||
if not collect_data(args):
|
||||
log_exit("Unable to collect data.")
|
||||
if process.pid == 0:
|
||||
result, output = AdbHelper().run_and_return_output(['shell', 'pidof', process.name])
|
||||
if result:
|
||||
try:
|
||||
process.pid = int(output)
|
||||
except ValueError:
|
||||
process.pid = 0
|
||||
collect_machine_info(process)
|
||||
else:
|
||||
args.capture_duration = 0
|
||||
|
||||
sample_filter_fn = None
|
||||
if args.one_flamegraph:
|
||||
def filter_fn(sample, _symbol, _callchain):
|
||||
sample.pid = sample.tid = process.pid
|
||||
return True
|
||||
sample_filter_fn = filter_fn
|
||||
if not args.title:
|
||||
args.title = ''
|
||||
args.title += '(One Flamegraph)'
|
||||
|
||||
try:
|
||||
parse_samples(process, args, sample_filter_fn)
|
||||
generate_threads_offsets(process)
|
||||
report_path = output_report(process, args)
|
||||
if not args.no_browser:
|
||||
open_report_in_browser(report_path)
|
||||
except RuntimeError as r:
|
||||
if 'maximum recursion depth' in r.__str__():
|
||||
log_fatal("Recursion limit exceeded (%s), try --max_callchain_depth." % r)
|
||||
raise r
|
||||
|
||||
logging.info("Flamegraph generated at '%s'." % report_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
function flamegraphInit() {
|
||||
let flamegraph = document.getElementById('flamegraph_id');
|
||||
let svgs = flamegraph.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; ++i) {
|
||||
createZoomHistoryStack(svgs[i]);
|
||||
adjust_text_size(svgs[i]);
|
||||
}
|
||||
|
||||
function throttle(callback) {
|
||||
let running = false;
|
||||
return function() {
|
||||
if (!running) {
|
||||
running = true;
|
||||
window.requestAnimationFrame(function () {
|
||||
callback();
|
||||
running = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
window.addEventListener('resize', throttle(function() {
|
||||
let flamegraph = document.getElementById('flamegraph_id');
|
||||
let svgs = flamegraph.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; ++i) {
|
||||
adjust_text_size(svgs[i]);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Create a stack add the root svg element in it.
|
||||
function createZoomHistoryStack(svgElement) {
|
||||
svgElement.zoomStack = [svgElement.getElementById(svgElement.attributes['rootid'].value)];
|
||||
}
|
||||
|
||||
function adjust_node_text_size(x, svgWidth) {
|
||||
let title = x.getElementsByTagName('title')[0];
|
||||
let text = x.getElementsByTagName('text')[0];
|
||||
let rect = x.getElementsByTagName('rect')[0];
|
||||
|
||||
let width = parseFloat(rect.attributes['width'].value) * svgWidth * 0.01;
|
||||
|
||||
// Don't even bother trying to find a best fit. The area is too small.
|
||||
if (width < 28) {
|
||||
text.textContent = '';
|
||||
return;
|
||||
}
|
||||
// Remove dso and #samples which are here only for mouseover purposes.
|
||||
let methodName = title.textContent.split(' | ')[0];
|
||||
|
||||
let numCharacters;
|
||||
for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) {
|
||||
// Avoid reflow by using hard-coded estimate instead of
|
||||
// text.getSubStringLength(0, numCharacters).
|
||||
if (numCharacters * 7.5 <= width) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (numCharacters == methodName.length) {
|
||||
text.textContent = methodName;
|
||||
return;
|
||||
}
|
||||
|
||||
text.textContent = methodName.substring(0, numCharacters-2) + '..';
|
||||
}
|
||||
|
||||
function adjust_text_size(svgElement) {
|
||||
let svgWidth = window.innerWidth;
|
||||
let x = svgElement.getElementsByTagName('g');
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
adjust_node_text_size(x[i], svgWidth);
|
||||
}
|
||||
}
|
||||
|
||||
function zoom(e) {
|
||||
let svgElement = e.ownerSVGElement;
|
||||
let zoomStack = svgElement.zoomStack;
|
||||
zoomStack.push(e);
|
||||
displaySVGElement(svgElement);
|
||||
select(e);
|
||||
|
||||
// Show zoom out button.
|
||||
svgElement.getElementById('zoom_rect').style.display = 'block';
|
||||
svgElement.getElementById('zoom_text').style.display = 'block';
|
||||
}
|
||||
|
||||
function displaySVGElement(svgElement) {
|
||||
let zoomStack = svgElement.zoomStack;
|
||||
let e = zoomStack[zoomStack.length - 1];
|
||||
let clicked_rect = e.getElementsByTagName('rect')[0];
|
||||
let clicked_origin_x;
|
||||
let clicked_origin_y = clicked_rect.attributes['oy'].value;
|
||||
let clicked_origin_width;
|
||||
|
||||
if (zoomStack.length == 1) {
|
||||
// Show all nodes when zoomStack only contains the root node.
|
||||
// This is needed to show flamegraph containing more than one node at the root level.
|
||||
clicked_origin_x = 0;
|
||||
clicked_origin_width = 100;
|
||||
} else {
|
||||
clicked_origin_x = clicked_rect.attributes['ox'].value;
|
||||
clicked_origin_width = clicked_rect.attributes['owidth'].value;
|
||||
}
|
||||
|
||||
|
||||
let svgBox = svgElement.getBoundingClientRect();
|
||||
let svgBoxHeight = svgBox.height;
|
||||
let svgBoxWidth = 100;
|
||||
let scaleFactor = svgBoxWidth / clicked_origin_width;
|
||||
|
||||
let callsites = svgElement.getElementsByTagName('g');
|
||||
for (let i = 0; i < callsites.length; i++) {
|
||||
let text = callsites[i].getElementsByTagName('text')[0];
|
||||
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||||
|
||||
let rect_o_x = parseFloat(rect.attributes['ox'].value);
|
||||
let rect_o_y = parseFloat(rect.attributes['oy'].value);
|
||||
|
||||
// Avoid multiple forced reflow by hiding nodes.
|
||||
if (rect_o_y > clicked_origin_y) {
|
||||
rect.style.display = 'none';
|
||||
text.style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
rect.style.display = 'block';
|
||||
text.style.display = 'block';
|
||||
|
||||
let newrec_x = rect.attributes['x'].value = (rect_o_x - clicked_origin_x) * scaleFactor +
|
||||
'%';
|
||||
let newrec_y = rect.attributes['y'].value = rect_o_y + (svgBoxHeight - clicked_origin_y
|
||||
- 17 - 2);
|
||||
|
||||
text.attributes['y'].value = newrec_y + 12;
|
||||
text.attributes['x'].value = newrec_x;
|
||||
|
||||
rect.attributes['width'].value = (rect.attributes['owidth'].value * scaleFactor) + '%';
|
||||
}
|
||||
|
||||
adjust_text_size(svgElement);
|
||||
}
|
||||
|
||||
function unzoom(e) {
|
||||
let svgOwner = e.ownerSVGElement;
|
||||
let stack = svgOwner.zoomStack;
|
||||
|
||||
// Unhighlight whatever was selected.
|
||||
if (selected) {
|
||||
selected.classList.remove('s');
|
||||
}
|
||||
|
||||
// Stack management: Never remove the last element which is the flamegraph root.
|
||||
if (stack.length > 1) {
|
||||
let previouslySelected = stack.pop();
|
||||
select(previouslySelected);
|
||||
}
|
||||
|
||||
// Hide zoom out button.
|
||||
if (stack.length == 1) {
|
||||
svgOwner.getElementById('zoom_rect').style.display = 'none';
|
||||
svgOwner.getElementById('zoom_text').style.display = 'none';
|
||||
}
|
||||
|
||||
displaySVGElement(svgOwner);
|
||||
}
|
||||
|
||||
function search(e) {
|
||||
let term = prompt('Search for:', '');
|
||||
let callsites = e.ownerSVGElement.getElementsByTagName('g');
|
||||
|
||||
if (!term) {
|
||||
for (let i = 0; i < callsites.length; i++) {
|
||||
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||||
rect.attributes['fill'].value = rect.attributes['ofill'].value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < callsites.length; i++) {
|
||||
let title = callsites[i].getElementsByTagName('title')[0];
|
||||
let rect = callsites[i].getElementsByTagName('rect')[0];
|
||||
if (title.textContent.indexOf(term) != -1) {
|
||||
rect.attributes['fill'].value = 'rgb(230,100,230)';
|
||||
} else {
|
||||
rect.attributes['fill'].value = rect.attributes['ofill'].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let selected;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!selected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let nav = selected.attributes['nav'].value.split(',');
|
||||
let navigation_index;
|
||||
switch (e.keyCode) {
|
||||
// case 38: // ARROW UP
|
||||
case 87: navigation_index = 0; break; // W
|
||||
|
||||
// case 32 : // ARROW LEFT
|
||||
case 65: navigation_index = 1; break; // A
|
||||
|
||||
// case 43: // ARROW DOWN
|
||||
case 68: navigation_index = 3; break; // S
|
||||
|
||||
// case 39: // ARROW RIGHT
|
||||
case 83: navigation_index = 2; break; // D
|
||||
|
||||
case 32: zoom(selected); return false; // SPACE
|
||||
|
||||
case 8: // BACKSPACE
|
||||
unzoom(selected); return false;
|
||||
default: return true;
|
||||
}
|
||||
|
||||
if (nav[navigation_index] == '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target_element = selected.ownerSVGElement.getElementById(nav[navigation_index]);
|
||||
select(target_element);
|
||||
return false;
|
||||
});
|
||||
|
||||
function select(e) {
|
||||
if (selected) {
|
||||
selected.classList.remove('s');
|
||||
}
|
||||
selected = e;
|
||||
selected.classList.add('s');
|
||||
|
||||
// Update info bar
|
||||
let titleElement = selected.getElementsByTagName('title')[0];
|
||||
let text = titleElement.textContent;
|
||||
|
||||
// Parse title
|
||||
let method_and_info = text.split(' | ');
|
||||
let methodName = method_and_info[0];
|
||||
let info = method_and_info[1];
|
||||
|
||||
// Parse info
|
||||
// '/system/lib64/libhwbinder.so (4 events: 0.28%)'
|
||||
let regexp = /(.*) \((.*)\)/g;
|
||||
let match = regexp.exec(info);
|
||||
if (match.length > 2) {
|
||||
let percentage = match[2];
|
||||
// Write percentage
|
||||
let percentageTextElement = selected.ownerSVGElement.getElementById('percent_text');
|
||||
percentageTextElement.textContent = percentage;
|
||||
// console.log("'" + percentage + "'")
|
||||
}
|
||||
|
||||
// Set fields
|
||||
let barTextElement = selected.ownerSVGElement.getElementById('info_text');
|
||||
barTextElement.textContent = methodName;
|
||||
}
|
||||
@ -0,0 +1,204 @@
|
||||
#
|
||||
# Copyright (C) 2016 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.
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
SVG_NODE_HEIGHT = 17
|
||||
FONT_SIZE = 12
|
||||
|
||||
UNZOOM_NODE_ORIGIN_X = 10
|
||||
UNZOOM_NODE_WIDTH = 80
|
||||
INFO_NODE_ORIGIN_X = 120
|
||||
INFO_NODE_WIDTH = 800
|
||||
PERCENT_NODE_ORIGIN_X = 930
|
||||
PERCENT_NODE_WIDTH = 250
|
||||
SEARCH_NODE_ORIGIN_X = 1190
|
||||
SEARCH_NODE_WIDTH = 80
|
||||
RECT_TEXT_PADDING = 10
|
||||
|
||||
|
||||
def hash_to_float(string):
|
||||
return hash(string) / float(sys.maxsize)
|
||||
|
||||
|
||||
def get_legacy_color(method):
|
||||
r = 175 + int(50 * hash_to_float(reversed(method)))
|
||||
g = 60 + int(180 * hash_to_float(method))
|
||||
b = 60 + int(55 * hash_to_float(reversed(method)))
|
||||
return (r, g, b)
|
||||
|
||||
|
||||
def get_dso_color(method):
|
||||
r = 170 + int(80 * hash_to_float(reversed(method)))
|
||||
g = 180 + int(70 * hash_to_float((method)))
|
||||
b = 170 + int(80 * hash_to_float(reversed(method)))
|
||||
return (r, g, b)
|
||||
|
||||
|
||||
def get_heat_color(callsite, total_weight):
|
||||
r = 245 + 10 * (1 - callsite.weight() / total_weight)
|
||||
g = 110 + 105 * (1 - callsite.weight() / total_weight)
|
||||
b = 100
|
||||
return (r, g, b)
|
||||
|
||||
def get_proper_scaled_time_string(value):
|
||||
if value >= 1e9:
|
||||
return '%.3f s' % (value / 1e9)
|
||||
if value >= 1e6:
|
||||
return '%.3f ms' % (value / 1e6)
|
||||
if value >= 1e3:
|
||||
return '%.3f us' % (value / 1e3)
|
||||
return '%.0f ns' % value
|
||||
|
||||
def create_svg_node(process, callsite, depth, f, total_weight, height, color_scheme, nav):
|
||||
x = float(callsite.offset) / total_weight * 100
|
||||
y = height - (depth + 1) * SVG_NODE_HEIGHT
|
||||
width = callsite.weight() / total_weight * 100
|
||||
|
||||
method = callsite.method.replace(">", ">").replace("<", "<")
|
||||
if width <= 0:
|
||||
return
|
||||
|
||||
if color_scheme == "dso":
|
||||
r, g, b = get_dso_color(callsite.dso)
|
||||
elif color_scheme == "legacy":
|
||||
r, g, b = get_legacy_color(method)
|
||||
else:
|
||||
r, g, b = get_heat_color(callsite, total_weight)
|
||||
|
||||
r_border, g_border, b_border = [max(0, color - 50) for color in [r, g, b]]
|
||||
|
||||
if process.props['trace_offcpu']:
|
||||
weight_str = get_proper_scaled_time_string(callsite.weight())
|
||||
else:
|
||||
weight_str = "{:,}".format(int(callsite.weight())) + ' events'
|
||||
|
||||
f.write(
|
||||
"""<g id=%d class="n" onclick="zoom(this);" onmouseenter="select(this);" nav="%s">
|
||||
<title>%s | %s (%s: %3.2f%%)</title>
|
||||
<rect x="%f%%" y="%f" ox="%f" oy="%f" width="%f%%" owidth="%f" height="15.0"
|
||||
ofill="rgb(%d,%d,%d)" fill="rgb(%d,%d,%d)" style="stroke:rgb(%d,%d,%d)"/>
|
||||
<text x="%f%%" y="%f" font-size="%d" font-family="Monospace"></text>
|
||||
</g>""" %
|
||||
(callsite.id,
|
||||
','.join(str(x) for x in nav),
|
||||
method,
|
||||
callsite.dso,
|
||||
weight_str,
|
||||
callsite.weight() / total_weight * 100,
|
||||
x,
|
||||
y,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
width,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
r_border,
|
||||
g_border,
|
||||
b_border,
|
||||
x,
|
||||
y + 12,
|
||||
FONT_SIZE))
|
||||
|
||||
|
||||
def render_svg_nodes(process, flamegraph, depth, f, total_weight, height, color_scheme):
|
||||
for i, child in enumerate(flamegraph.children):
|
||||
# Prebuild navigation target for wasd
|
||||
|
||||
if i == 0:
|
||||
left_index = 0
|
||||
else:
|
||||
left_index = flamegraph.children[i - 1].id
|
||||
|
||||
if i == len(flamegraph.children) - 1:
|
||||
right_index = 0
|
||||
else:
|
||||
right_index = flamegraph.children[i + 1].id
|
||||
|
||||
up_index = max(child.children, key=lambda x: x.weight()).id if child.children else 0
|
||||
|
||||
# up, left, down, right
|
||||
nav = [up_index, left_index, flamegraph.id, right_index]
|
||||
|
||||
create_svg_node(process, child, depth, f, total_weight, height, color_scheme, nav)
|
||||
# Recurse down
|
||||
render_svg_nodes(process, child, depth + 1, f, total_weight, height, color_scheme)
|
||||
|
||||
|
||||
def render_search_node(f):
|
||||
f.write(
|
||||
"""<rect id="search_rect" style="stroke:rgb(0,0,0);" onclick="search(this);" class="t"
|
||||
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)""/>
|
||||
<text id="search_text" class="t" x="%d" y="30" onclick="search(this);">Search</text>
|
||||
""" % (SEARCH_NODE_ORIGIN_X, SEARCH_NODE_WIDTH, SEARCH_NODE_ORIGIN_X + RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def render_unzoom_node(f):
|
||||
f.write(
|
||||
"""<rect id="zoom_rect" style="display:none;stroke:rgb(0,0,0);" class="t"
|
||||
onclick="unzoom(this);" rx="10" ry="10" x="%d" y="10" width="%d" height="30"
|
||||
fill="rgb(255,255,255)"/>
|
||||
<text id="zoom_text" style="display:none;" class="t" x="%d" y="30"
|
||||
onclick="unzoom(this);">Zoom out</text>
|
||||
""" % (UNZOOM_NODE_ORIGIN_X, UNZOOM_NODE_WIDTH, UNZOOM_NODE_ORIGIN_X + RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def render_info_node(f):
|
||||
f.write(
|
||||
"""<clipPath id="info_clip_path"> <rect id="info_rect" style="stroke:rgb(0,0,0);"
|
||||
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
|
||||
</clipPath>
|
||||
<rect id="info_rect" style="stroke:rgb(0,0,0);"
|
||||
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
|
||||
<text clip-path="url(#info_clip_path)" id="info_text" x="%d" y="30"></text>
|
||||
""" % (INFO_NODE_ORIGIN_X, INFO_NODE_WIDTH, INFO_NODE_ORIGIN_X, INFO_NODE_WIDTH,
|
||||
INFO_NODE_ORIGIN_X + RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def render_percent_node(f):
|
||||
f.write(
|
||||
"""<rect id="percent_rect" style="stroke:rgb(0,0,0);"
|
||||
rx="10" ry="10" x="%d" y="10" width="%d" height="30" fill="rgb(255,255,255)"/>
|
||||
<text id="percent_text" text-anchor="end" x="%d" y="30">100.00%%</text>
|
||||
""" % (PERCENT_NODE_ORIGIN_X, PERCENT_NODE_WIDTH,
|
||||
PERCENT_NODE_ORIGIN_X + PERCENT_NODE_WIDTH - RECT_TEXT_PADDING))
|
||||
|
||||
|
||||
def render_svg(process, flamegraph, f, color_scheme):
|
||||
height = (flamegraph.get_max_depth() + 2) * SVG_NODE_HEIGHT
|
||||
f.write("""<div class="flamegraph_block" style="width:100%%; height:%dpx;">
|
||||
""" % height)
|
||||
f.write("""<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
|
||||
width="100%%" height="100%%" style="border: 1px solid black;"
|
||||
rootid="%d">
|
||||
""" % (flamegraph.children[0].id))
|
||||
f.write("""<defs > <linearGradient id="background_gradiant" y1="0" y2="1" x1="0" x2="0" >
|
||||
<stop stop-color="#eeeeee" offset="5%" /> <stop stop-color="#efefb1" offset="90%" />
|
||||
</linearGradient> </defs>""")
|
||||
f.write("""<rect x="0.0" y="0" width="100%" height="100%" fill="url(#background_gradiant)" />
|
||||
""")
|
||||
render_svg_nodes(process, flamegraph, 0, f, flamegraph.weight(), height, color_scheme)
|
||||
render_search_node(f)
|
||||
render_unzoom_node(f)
|
||||
render_info_node(f)
|
||||
render_percent_node(f)
|
||||
f.write("</svg></div><br/>\n\n")
|
||||
Reference in New Issue
Block a user