# # Copyright (C) 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # """ Inferno is a tool to generate flamegraphs for android programs. It was originally written to profile surfaceflinger (Android compositor) but it can be used for other C++ program. It uses simpleperf to collect data. Programs have to be compiled with frame pointers which excludes ART based programs for the time being. Here is how it works: 1/ Data collection is started via simpleperf and pulled locally as "perf.data". 2/ The raw format is parsed, callstacks are merged to form a flamegraph data structure. 3/ The data structure is used to generate a SVG embedded into an HTML page. 4/ Javascript is injected to allow flamegraph navigation, search, coloring model. """ from scripts.simpleperf_report_lib import * import argparse from data_types import * from svg_renderer import * import datetime import webbrowser from adb_non_root import AdbNonRoot from adb_root import AdbRoot def create_process(adb_client, args): """ Retrieves target process pid and create a process contained. :param args: Argument as parsed by argparse :return: Process objectk """ process_id = adb_client.get_process_pid(args.process_name) process = Process(args.process_name, process_id) return process def collect_data(adb_client, process): """ Start simpleperf on device and collect data. Pull perf.data into cwd. :param process: Process object :return: Populated Process object """ if process.args.dwarf_unwinding: unwinding_parameter = "-g" print "Unwinding with dwarf." else: unwinding_parameter = "--call-graph fp" print "Unwinding with frame pointers." # Check whether sampling will be frequency based or event based. sampling_parameter = "-f %s" % process.args.sample_frequency if process.args.events: tokens = process.args.events.split(" ") if len(tokens) == 2: num_events = tokens[0] event_name = tokens[1] sampling_parameter = "-c %s -e '%s'" % (num_events, event_name) else: print "Event format string not recognized. Expected \"requency event_name\"." print "Got : [" + ",".join(tokens) + "]" return False print "Using event sampling (%s)." % sampling_parameter else: print "Using frequency sampling (%s)." % sampling_parameter process.cmd = "./simpleperf record \ -o /data/local/tmp/perf.data \ %s \ -p %s \ --duration %s \ %s" % ( unwinding_parameter, process.pid, process.args.capture_duration, sampling_parameter) print("Process '%s' PID = %d" % (process.name, process.pid)) if process.args.skip_collection: print("Skipping data collection, expecting perf.data in folder") return True print("Sampling for %s seconds..." % process.args.capture_duration) adb_client.delete_previous_data() success = adb_client.collect_data(process) if not success: return False err = adb_client.pull_data() if err: return False return True def parse_samples(process, args): """ read record_file, and print each sample""" record_file = args.record_file symfs_dir = args.symfs kallsyms_file = args.kallsyms lib = ReportLib() lib.ShowIpForUnknownSymbol() if symfs_dir is not None: lib.SetSymfs(symfs_dir) if record_file is not None: lib.SetRecordFile(record_file) if kallsyms_file is not None: lib.SetKallsymsFile(kallsyms_file) while True: sample = lib.GetNextSample() if sample is None: lib.Close() break symbol = lib.GetSymbolOfCurrentSample() callchain = lib.GetCallChainOfCurrentSample() process.get_thread(sample.tid).add_callchain(callchain, symbol, sample) process.num_samples += 1 print("Parsed %s callchains." % process.num_samples) def collapse_callgraphs(process): """ For each thread, collapse all callgraph into one flamegraph. :param process: Process object :return: None """ for _, thread in process.threads.items(): thread.collapse_flamegraph() def get_local_asset_content(local_path): """ Retrieves local package text content :param local_path: str, filename of local asset :return: str, the content of local_path """ f = open(os.path.join(os.path.dirname(__file__), local_path), 'r') content = f.read() f.close() return content def output_report(process): """ Generates a HTML report representing the result of simpleperf sampling as flamegraph :param process: Process object :return: str, absolute path to the file """ f = open('report.html', 'w') filepath = os.path.realpath(f.name) f.write("") f.write("") f.write('') f.write('') f.write('Embedded Image') f.write("
\ \ Inferno Flamegraph Report

\ Process : %s (%d)
\ Date    : %s
\ Threads : %d
\ Samples : %d
\ Duration: %s seconds
\ Machine : %s (%s) by %s
\ Capture : %s

" % ( process.name,process.pid, datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"), len(process.threads), process.num_samples, process.args.capture_duration, process.props["ro.product.model"], process.props["ro.product.name"], process.props["ro.product.manufacturer"], process.cmd)) f.write("

Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.
") f.write(get_local_asset_content("script.js")) # Output tid == pid Thread first. main_thread = [x for _, x in process.threads.items() if x.tid == process.pid] for thread in main_thread: f.write("

Main Thread %d (%d samples):
\n\n\n\n" % (thread.tid, thread.num_samples)) renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width) other_threads = [x for _, x in process.threads.items() if x.tid != process.pid] for thread in other_threads: f.write("

Thread %d (%d samples):
\n\n\n\n" % (thread.tid, thread.num_samples)) renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width) f.write("") f.write("") f.close() return "file://" + filepath def generate_flamegraph_offsets(flamegraph): rover = flamegraph.offset for callsite in flamegraph.callsites: callsite.offset = rover rover += callsite.num_samples generate_flamegraph_offsets(callsite) def generate_threads_offsets(process): for _, thread in process.threads.items(): generate_flamegraph_offsets(thread.flamegraph) def collect_machine_info(adb_client, process): process.props = adb_client.get_props() def setup_adb(): err = subprocess.call(["adb", "root"]) if err == 0: return AdbRoot() else: return AdbNonRoot() def open_report_in_browser(report_path): # Try to open the report with Chrome browser_key = "" for key, value in webbrowser._browsers.items(): if key.find("chrome") != -1: browser_key = key browser = webbrowser.get(browser_key) browser.open(report_path, new=0, autoraise=True) def main(): parser = argparse.ArgumentParser(description='Report samples in perf.data.') parser.add_argument('--symfs', help='Set the path to find binaries with symbols and debug info.') parser.add_argument('--kallsyms', help='Set the path to find kernel symbols.') parser.add_argument('--record_file', default='perf.data', help='Default is perf.data.') parser.add_argument('-t', '--capture_duration', default=10, help='Capture duration in seconds.') parser.add_argument('-p', '--process_name', default='surfaceflinger', help='Default is surfaceflinger.') parser.add_argument('-c', '--color', default='hot', choices=['hot', 'dso', 'legacy'], help='Color theme: hot=percentage of samples, dso=callsite DSO name, legacy=brendan style') parser.add_argument('-sc','--skip_collection', default=False, help='Skip data collection', action="store_true") parser.add_argument('-f', '--sample_frequency', default=6000, help='Sample frequency') parser.add_argument('-w', '--svg_width', type=int, default=1124) parser.add_argument('-sb', '--skip_push_binary', help='Skip pushing simpleperf before profiling', default=False, action="store_true") parser.add_argument('-du', '--dwarf_unwinding', help='Perform unwinding using dwarf instead of fp.', default=False, action='store_true') parser.add_argument('-e', '--events', help='Sample based on event occurences instead of frequency. ' 'Format expected is "event_counts event_name". e.g: "10000 cpu-cyles". A few examples of \ nmames: cpu-cycles, cache-references, cache-misses, branch-instructions, branch-misses', default="") args = parser.parse_args() # Since we may attempt to sample privileged process, let's try to be root. adb_client = setup_adb() # Create a process object process = create_process(adb_client, args) if process.pid == 0: print("Unable to retrive pid for process '%s'. Terminating." % process.name) return process.args = args print("Starting data collection stage for process '%s'." % args.process_name) success = collect_data(adb_client, process) if not success: print "Unable to collect data" return collect_machine_info(adb_client, process) parse_samples(process, args) collapse_callgraphs(process) generate_threads_offsets(process) report_path = output_report(process) open_report_in_browser(report_path) print "Report generated at '%s'." % report_path if __name__ == "__main__": main()