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