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