Home | History | Annotate | Download | only in inferno
      1 #
      2 # Copyright (C) 2016 The Android Open Source Project
      3 #
      4 # Licensed under the Apache License, Version 2.0 (the "License");
      5 # you may not use this file except in compliance with the License.
      6 # You may obtain a copy of the License at
      7 #
      8 #      http://www.apache.org/licenses/LICENSE-2.0
      9 #
     10 # Unless required by applicable law or agreed to in writing, software
     11 # distributed under the License is distributed on an "AS IS" BASIS,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 #
     16 
     17 """
     18     Inferno is a tool to generate flamegraphs for android programs. It was originally written
     19     to profile surfaceflinger (Android compositor) but it can be used for other C++ program.
     20     It uses simpleperf to collect data. Programs have to be compiled with frame pointers which
     21     excludes ART based programs for the time being.
     22 
     23     Here is how it works:
     24 
     25     1/ Data collection is started via simpleperf and pulled locally as "perf.data".
     26     2/ The raw format is parsed, callstacks are merged to form a flamegraph data structure.
     27     3/ The data structure is used to generate a SVG embedded into an HTML page.
     28     4/ Javascript is injected to allow flamegraph navigation, search, coloring model.
     29 
     30 """
     31 
     32 import argparse
     33 import datetime
     34 import os
     35 import subprocess
     36 import sys
     37 
     38 scripts_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
     39 sys.path.append(scripts_path)
     40 from simpleperf_report_lib import ReportLib
     41 from utils import log_exit, log_info, AdbHelper, open_report_in_browser
     42 
     43 from data_types import *
     44 from svg_renderer import *
     45 
     46 
     47 def collect_data(args):
     48     """ Run app_profiler.py to generate record file. """
     49     app_profiler_args = [sys.executable, os.path.join(scripts_path, "app_profiler.py"), "-nb"]
     50     if args.app:
     51         app_profiler_args += ["-p", args.app]
     52     elif args.native_program:
     53         app_profiler_args += ["-np", args.native_program]
     54     else:
     55         log_exit("Please set profiling target with -p or -np option.")
     56     if args.skip_recompile:
     57         app_profiler_args.append("-nc")
     58     if args.disable_adb_root:
     59         app_profiler_args.append("--disable_adb_root")
     60     record_arg_str = ""
     61     if args.dwarf_unwinding:
     62         record_arg_str += "-g "
     63     else:
     64         record_arg_str += "--call-graph fp "
     65     if args.events:
     66         tokens = args.events.split()
     67         if len(tokens) == 2:
     68             num_events = tokens[0]
     69             event_name = tokens[1]
     70             record_arg_str += "-c %s -e %s " % (num_events, event_name)
     71         else:
     72             log_exit("Event format string of -e option cann't be recognized.")
     73         log_info("Using event sampling (-c %s -e %s)." % (num_events, event_name))
     74     else:
     75         record_arg_str += "-f %d " % args.sample_frequency
     76         log_info("Using frequency sampling (-f %d)." % args.sample_frequency)
     77     record_arg_str += "--duration %d " % args.capture_duration
     78     app_profiler_args += ["-r", record_arg_str]
     79     returncode = subprocess.call(app_profiler_args)
     80     return returncode == 0
     81 
     82 
     83 def parse_samples(process, args, sample_filter_fn):
     84     """Read samples from record file.
     85         process: Process object
     86         args: arguments
     87         sample_filter_fn: if not None, is used to modify and filter samples.
     88                           It returns false for samples should be filtered out.
     89     """
     90 
     91     record_file = args.record_file
     92     symfs_dir = args.symfs
     93     kallsyms_file = args.kallsyms
     94 
     95     lib = ReportLib()
     96 
     97     lib.ShowIpForUnknownSymbol()
     98     if symfs_dir:
     99         lib.SetSymfs(symfs_dir)
    100     if record_file:
    101         lib.SetRecordFile(record_file)
    102     if kallsyms_file:
    103         lib.SetKallsymsFile(kallsyms_file)
    104     process.cmd = lib.GetRecordCmd()
    105     product_props = lib.MetaInfo().get("product_props")
    106     if product_props:
    107         tuple = product_props.split(':')
    108         process.props['ro.product.manufacturer'] = tuple[0]
    109         process.props['ro.product.model'] = tuple[1]
    110         process.props['ro.product.name'] = tuple[2]
    111     if lib.MetaInfo().get('trace_offcpu') == 'true':
    112         process.props['trace_offcpu'] = True
    113         if args.one_flamegraph:
    114             log_exit("It doesn't make sense to report with --one-flamegraph for perf.data " +
    115                      "recorded with --trace-offcpu.""")
    116     else:
    117         process.props['trace_offcpu'] = False
    118 
    119     while True:
    120         sample = lib.GetNextSample()
    121         if sample is None:
    122             lib.Close()
    123             break
    124         symbol = lib.GetSymbolOfCurrentSample()
    125         callchain = lib.GetCallChainOfCurrentSample()
    126         if sample_filter_fn and not sample_filter_fn(sample, symbol, callchain):
    127             continue
    128         process.add_sample(sample, symbol, callchain)
    129 
    130     if process.pid == 0:
    131         main_threads = [thread for thread in process.threads.values() if thread.tid == thread.pid]
    132         if main_threads:
    133             process.name = main_threads[0].name
    134             process.pid = main_threads[0].pid
    135 
    136     for thread in process.threads.values():
    137         min_event_count = thread.num_events * args.min_callchain_percentage * 0.01
    138         thread.flamegraph.trim_callchain(min_event_count)
    139 
    140     log_info("Parsed %s callchains." % process.num_samples)
    141 
    142 
    143 def get_local_asset_content(local_path):
    144     """
    145     Retrieves local package text content
    146     :param local_path: str, filename of local asset
    147     :return: str, the content of local_path
    148     """
    149     with open(os.path.join(os.path.dirname(__file__), local_path), 'r') as f:
    150         return f.read()
    151 
    152 
    153 def output_report(process, args):
    154     """
    155     Generates a HTML report representing the result of simpleperf sampling as flamegraph
    156     :param process: Process object
    157     :return: str, absolute path to the file
    158     """
    159     f = open(args.report_path, 'w')
    160     filepath = os.path.realpath(f.name)
    161     if not args.embedded_flamegraph:
    162         f.write("<html><body>")
    163     f.write("<div id='flamegraph_id' style='font-family: Monospace; %s'>" % (
    164             "display: none;" if args.embedded_flamegraph else ""))
    165     f.write("""<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;}
    166             </style>""")
    167     f.write('<style type="text/css"> .t:hover { cursor:pointer; } </style>')
    168     f.write('<img height="180" alt = "Embedded Image" src ="data')
    169     f.write(get_local_asset_content("inferno.b64"))
    170     f.write('"/>')
    171     process_entry = ("Process : %s (%d)<br/>" % (process.name, process.pid)) if process.pid else ""
    172     if process.props['trace_offcpu']:
    173         event_entry = 'Total time: %s<br/>' % get_proper_scaled_time_string(process.num_events)
    174     else:
    175         event_entry = 'Event count: %s<br/>' % ("{:,}".format(process.num_events))
    176     # TODO: collect capture duration info from perf.data.
    177     duration_entry = ("Duration: %s seconds<br/>" % args.capture_duration
    178                       ) if args.capture_duration else ""
    179     f.write("""<div style='display:inline-block;'>
    180                   <font size='8'>
    181                   Inferno Flamegraph Report%s</font><br/><br/>
    182                   %s
    183                   Date&nbsp;&nbsp;&nbsp;&nbsp;: %s<br/>
    184                   Threads : %d <br/>
    185                   Samples : %d<br/>
    186                   %s
    187                   %s""" % (
    188         (': ' + args.title) if args.title else '',
    189         process_entry,
    190         datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
    191         len(process.threads),
    192         process.num_samples,
    193         event_entry,
    194         duration_entry))
    195     if 'ro.product.model' in process.props:
    196         f.write(
    197             "Machine : %s (%s) by %s<br/>" %
    198             (process.props["ro.product.model"],
    199              process.props["ro.product.name"],
    200              process.props["ro.product.manufacturer"]))
    201     if process.cmd:
    202         f.write("Capture : %s<br/><br/>" % process.cmd)
    203     f.write("</div>")
    204     f.write("""<br/><br/>
    205             <div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>""")
    206     f.write("<script>%s</script>" % get_local_asset_content("script.js"))
    207     if not args.embedded_flamegraph:
    208         f.write("<script>document.addEventListener('DOMContentLoaded', flamegraphInit);</script>")
    209 
    210     # Sort threads by the event count in a thread.
    211     for thread in sorted(process.threads.values(), key=lambda x: x.num_events, reverse=True):
    212         f.write("<br/><br/><b>Thread %d (%s) (%d samples):</b><br/>\n\n\n\n" % (
    213                 thread.tid, thread.name, thread.num_samples))
    214         renderSVG(process, thread.flamegraph, f, args.color)
    215 
    216     f.write("</div>")
    217     if not args.embedded_flamegraph:
    218         f.write("</body></html")
    219     f.close()
    220     return "file://" + filepath
    221 
    222 
    223 def generate_threads_offsets(process):
    224     for thread in process.threads.values():
    225        thread.flamegraph.generate_offset(0)
    226 
    227 
    228 def collect_machine_info(process):
    229     adb = AdbHelper()
    230     process.props = {}
    231     process.props['ro.product.model'] = adb.get_property('ro.product.model')
    232     process.props['ro.product.name'] = adb.get_property('ro.product.name')
    233     process.props['ro.product.manufacturer'] = adb.get_property('ro.product.manufacturer')
    234 
    235 
    236 def main():
    237     # Allow deep callchain with length >1000.
    238     sys.setrecursionlimit(1500)
    239     parser = argparse.ArgumentParser(description="""Report samples in perf.data. Default option
    240                                                     is: "-np surfaceflinger -f 6000 -t 10".""")
    241     record_group = parser.add_argument_group('Record options')
    242     record_group.add_argument('-du', '--dwarf_unwinding', action='store_true', help="""Perform
    243                               unwinding using dwarf instead of fp.""")
    244     record_group.add_argument('-e', '--events', default="", help="""Sample based on event
    245                               occurences instead of frequency. Format expected is
    246                               "event_counts event_name". e.g: "10000 cpu-cyles". A few examples
    247                               of event_name: cpu-cycles, cache-references, cache-misses,
    248                               branch-instructions, branch-misses""")
    249     record_group.add_argument('-f', '--sample_frequency', type=int, default=6000, help="""Sample
    250                               frequency""")
    251     record_group.add_argument('-nc', '--skip_recompile', action='store_true', help="""When
    252                               profiling an Android app, by default we recompile java bytecode to
    253                               native instructions to profile java code. It takes some time. You
    254                               can skip it if the code has been compiled or you don't need to
    255                               profile java code.""")
    256     record_group.add_argument('-np', '--native_program', default="surfaceflinger", help="""Profile
    257                               a native program. The program should be running on the device.
    258                               Like -np surfaceflinger.""")
    259     record_group.add_argument('-p', '--app', help="""Profile an Android app, given the package
    260                               name. Like -p com.example.android.myapp.""")
    261     record_group.add_argument('--record_file', default='perf.data', help='Default is perf.data.')
    262     record_group.add_argument('-sc', '--skip_collection', action='store_true', help="""Skip data
    263                               collection""")
    264     record_group.add_argument('-t', '--capture_duration', type=int, default=10, help="""Capture
    265                               duration in seconds.""")
    266 
    267     report_group = parser.add_argument_group('Report options')
    268     report_group.add_argument('-c', '--color', default='hot', choices=['hot', 'dso', 'legacy'],
    269                               help="""Color theme: hot=percentage of samples, dso=callsite DSO
    270                                       name, legacy=brendan style""")
    271     report_group.add_argument('--embedded_flamegraph', action='store_true', help="""Generate
    272                               embedded flamegraph.""")
    273     report_group.add_argument('--kallsyms', help='Set the path to find kernel symbols.')
    274     report_group.add_argument('--min_callchain_percentage', default=0.01, type=float, help="""
    275                               Set min percentage of callchains shown in the report.
    276                               It is used to limit nodes shown in the flamegraph. For example,
    277                               when set to 0.01, only callchains taking >= 0.01%% of the event
    278                               count of the owner thread are collected in the report.""")
    279     report_group.add_argument('--no_browser', action='store_true', help="""Don't open report
    280                               in browser.""")
    281     report_group.add_argument('-o', '--report_path', default='report.html', help="""Set report
    282                               path.""")
    283     report_group.add_argument('--one-flamegraph', action='store_true', help="""Generate one
    284                               flamegraph instead of one for each thread.""")
    285     report_group.add_argument('--symfs', help="""Set the path to find binaries with symbols and
    286                               debug info.""")
    287     report_group.add_argument('--title', help='Show a title in the report.')
    288 
    289     debug_group = parser.add_argument_group('Debug options')
    290     debug_group.add_argument('--disable_adb_root', action='store_true', help="""Force adb to run
    291                              in non root mode.""")
    292     args = parser.parse_args()
    293     process = Process("", 0)
    294 
    295     if not args.skip_collection:
    296         process.name = args.app or args.native_program
    297         log_info("Starting data collection stage for process '%s'." % process.name)
    298         if not collect_data(args):
    299             log_exit("Unable to collect data.")
    300         result, output = AdbHelper().run_and_return_output(['shell', 'pidof', process.name])
    301         if result:
    302             try:
    303                 process.pid = int(output)
    304             except:
    305                 process.pid = 0
    306         collect_machine_info(process)
    307     else:
    308         args.capture_duration = 0
    309 
    310     sample_filter_fn = None
    311     if args.one_flamegraph:
    312         def filter_fn(sample, symbol, callchain):
    313             sample.pid = sample.tid = process.pid
    314             return True
    315         sample_filter_fn = filter_fn
    316         if not args.title:
    317             args.title = ''
    318         args.title += '(One Flamegraph)'
    319 
    320     parse_samples(process, args, sample_filter_fn)
    321     generate_threads_offsets(process)
    322     report_path = output_report(process, args)
    323     if not args.no_browser:
    324         open_report_in_browser(report_path)
    325 
    326     log_info("Flamegraph generated at '%s'." % report_path)
    327 
    328 if __name__ == "__main__":
    329     main()
    330