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
32import argparse
33import datetime
34import os
35import subprocess
36import sys
37
38scripts_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
39sys.path.append(scripts_path)
40from simpleperf_report_lib import ReportLib
41from utils import log_exit, log_info, AdbHelper, open_report_in_browser
42
43from data_types import *
44from svg_renderer import *
45
46
47def 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
83def 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
143def 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
153def 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
223def generate_threads_offsets(process):
224    for thread in process.threads.values():
225       thread.flamegraph.generate_offset(0)
226
227
228def 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
236def 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
328if __name__ == "__main__":
329    main()
330