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