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