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