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 : %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