2 # Copyright (C) 2016 The Android Open Source Project
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
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.
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.
32 from scripts.simpleperf_report_lib import *
34 from data_types import *
35 from svg_renderer import *
38 from adb_non_root import AdbNonRoot
39 from adb_root import AdbRoot
41 def create_process(adb_client, args):
42 """ Retrieves target process pid and create a process contained.
44 :param args: Argument as parsed by argparse
45 :return: Process objectk
47 process_id = adb_client.get_process_pid(args.process_name)
48 process = Process(args.process_name, process_id)
52 def collect_data(adb_client, process):
53 """ Start simpleperf on device and collect data. Pull perf.data into cwd.
55 :param process: Process object
56 :return: Populated Process object
59 if process.args.dwarf_unwinding:
60 unwinding_parameter = "-g"
61 print "Unwinding with dwarf."
63 unwinding_parameter = "--call-graph fp"
64 print "Unwinding with frame pointers."
66 process.cmd = "./simpleperf record \
67 -o /data/local/tmp/perf.data \
74 process.args.capture_duration,
75 process.args.sample_frequency)
77 # TODO Add arg to configure what events to listen on:
82 # -e branch-instructions
84 # Also add the granularity with -c 100000
85 print("Process '%s' PID = %d" % (process.name, process.pid))
87 if process.args.skip_collection:
88 print("Skipping data collection, expecting perf.data in folder")
91 print("Sampling for %s seconds..." % process.args.capture_duration)
94 adb_client.delete_previous_data()
96 success = adb_client.collect_data(process)
100 err = adb_client.pull_data()
107 def parse_samples(process, args):
108 """ read record_file, and print each sample"""
110 record_file = args.record_file
111 symfs_dir = args.symfs
112 kallsyms_file = args.kallsyms
116 lib.ShowIpForUnknownSymbol()
117 if symfs_dir is not None:
118 lib.SetSymfs(symfs_dir)
119 if record_file is not None:
120 lib.SetRecordFile(record_file)
121 if kallsyms_file is not None:
122 lib.SetKallsymsFile(kallsyms_file)
125 sample = lib.GetNextSample()
129 symbol = lib.GetSymbolOfCurrentSample()
130 callchain = lib.GetCallChainOfCurrentSample()
131 process.get_thread(sample.tid).add_callchain(callchain, symbol, sample)
132 process.num_samples += 1
134 print("Parsed %s callchains." % process.num_samples)
137 def collapse_callgraphs(process):
139 For each thread, collapse all callgraph into one flamegraph.
140 :param process: Process object
143 for _, thread in process.threads.items():
144 thread.collapse_flamegraph()
147 def get_local_asset_content(local_path):
149 Retrieves local package text content
150 :param local_path: str, filename of local asset
151 :return: str, the content of local_path
153 f = open(os.path.join(os.path.dirname(__file__), local_path), 'r')
159 def output_report(process):
161 Generates a HTML report representing the result of simpleperf sampling as flamegraph
162 :param process: Process object
163 :return: str, absolute path to the file
165 f = open('report.html', 'w')
166 filepath = os.path.realpath(f.name)
168 f.write("<body style='font-family: Monospace;' onload='init()'>")
169 f.write('<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;} </style>')
170 f.write('<style type="text/css"> .t:hover { cursor:pointer; } </style>')
171 f.write('<img height="180" alt = "Embedded Image" src ="data')
172 f.write(get_local_asset_content("inferno.b64"))
174 f.write("<div style='display:inline-block;'> \
176 Inferno Flamegraph Report</font><br/><br/> \
177 Process : %s (%d)<br/>\
178 Date : %s<br/>\
181 Duration: %s seconds<br/>\
182 Machine : %s (%s) by %s<br/>\
183 Capture : %s<br/><br/></div>"
185 process.name,process.pid,
186 datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
187 len(process.threads),
189 process.args.capture_duration,
190 process.props["ro.product.model"], process.props["ro.product.name"],
191 process.props["ro.product.manufacturer"],
193 f.write("<br/><br/><div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>")
194 f.write(get_local_asset_content("script.js"))
196 # Output tid == pid Thread first.
197 main_thread = [x for _, x in process.threads.items() if x.tid == process.pid]
198 for thread in main_thread:
199 f.write("<br/><br/><b>Main Thread %d (%d samples):</b><br/>\n\n\n\n" % (thread.tid, thread.num_samples))
200 renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width)
202 other_threads = [x for _, x in process.threads.items() if x.tid != process.pid]
203 for thread in other_threads:
204 f.write("<br/><br/><b>Thread %d (%d samples):</b><br/>\n\n\n\n" % (thread.tid, thread.num_samples))
205 renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width)
210 return "file://" + filepath
212 def generate_flamegraph_offsets(flamegraph):
213 rover = flamegraph.offset
214 for callsite in flamegraph.callsites:
215 callsite.offset = rover
216 rover += callsite.num_samples
217 generate_flamegraph_offsets(callsite)
220 def generate_threads_offsets(process):
221 for _, thread in process.threads.items():
222 generate_flamegraph_offsets(thread.flamegraph)
225 def collect_machine_info(adb_client, process):
226 process.props = adb_client.get_props()
230 err = subprocess.call(["adb", "root"])
236 def open_report_in_browser(report_path):
237 # Try to open the report with Chrome
239 for key, value in webbrowser._browsers.items():
240 if key.find("chrome") != -1:
242 browser = webbrowser.get(browser_key)
243 browser.open(report_path, new=0, autoraise=True)
249 parser = argparse.ArgumentParser(description='Report samples in perf.data.')
250 parser.add_argument('--symfs', help='Set the path to find binaries with symbols and debug info.')
251 parser.add_argument('--kallsyms', help='Set the path to find kernel symbols.')
252 parser.add_argument('--record_file', default='perf.data', help='Default is perf.data.')
253 parser.add_argument('-t', '--capture_duration', default=10, help='Capture duration in seconds.')
254 parser.add_argument('-p', '--process_name', default='surfaceflinger', help='Default is surfaceflinger.')
255 parser.add_argument('-c', '--color', default='hot', choices=['hot', 'dso', 'legacy'],
256 help='Color theme: hot=percentage of samples, dso=callsite DSO name, legacy=brendan style')
257 parser.add_argument('-sc','--skip_collection', default=False, help='Skip data collection', action="store_true")
258 parser.add_argument('-f', '--sample_frequency', default=6000, help='Sample frequency')
259 parser.add_argument('-w', '--svg_width', type=int, default=1124)
260 parser.add_argument('-sb', '--skip_push_binary', help='Skip pushing simpleperf before profiling', default=False, action="store_true")
261 parser.add_argument('-du', '--dwarf_unwinding', help='Perform unwinding using dwarf instead of fp.', default=False, action='store_true')
262 args = parser.parse_args()
264 # Since we may attempt to sample privileged process, let's try to be root.
265 adb_client = setup_adb()
267 # Create a process object
268 process = create_process(adb_client, args)
270 print("Unable to retrive pid for process '%s'. Terminating." % process.name)
275 print("Starting data collection stage for process '%s'." % args.process_name)
276 success = collect_data(adb_client, process)
278 print "Unable to collect data"
281 collect_machine_info(adb_client, process)
282 parse_samples(process, args)
283 collapse_callgraphs(process)
284 generate_threads_offsets(process)
285 report_path = output_report(process)
286 open_report_in_browser(report_path)
288 print "Report generated at '%s'." % report_path
290 if __name__ == "__main__":