| # |
| # 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("<html>") |
| f.write("<body style='font-family: Monospace;' onload='init()'>") |
| f.write('<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;} </style>') |
| f.write('<style type="text/css"> .t:hover { cursor:pointer; } </style>') |
| f.write('<img height="180" alt = "Embedded Image" src ="data') |
| f.write(get_local_asset_content("inferno.b64")) |
| f.write('"/>') |
| f.write("<div style='display:inline-block;'> \ |
| <font size='8'>\ |
| Inferno Flamegraph Report</font><br/><br/> \ |
| Process : %s (%d)<br/>\ |
| Date : %s<br/>\ |
| Threads : %d <br/>\ |
| Samples : %d</br>\ |
| Duration: %s seconds<br/>\ |
| Machine : %s (%s) by %s<br/>\ |
| Capture : %s<br/><br/></div>" |
| % ( |
| 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("<br/><br/><div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>") |
| 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("<br/><br/><b>Main Thread %d (%d samples):</b><br/>\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("<br/><br/><b>Thread %d (%d samples):</b><br/>\n\n\n\n" % (thread.tid, thread.num_samples)) |
| renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width) |
| |
| f.write("</body>") |
| f.write("</html>") |
| 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() |