OSDN Git Service

ade3e741e50f2fb18c49843e85bdce095dc12e90
[android-x86/system-extras.git] / simpleperf / inferno / inferno.py
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
32 from scripts.simpleperf_report_lib import *
33 import argparse
34 from data_types import *
35 from svg_renderer import *
36 import datetime
37 import webbrowser
38 from adb_non_root import AdbNonRoot
39 from adb_root import AdbRoot
40
41 def create_process(adb_client, args):
42     """ Retrieves target process pid and create a process contained.
43
44     :param args: Argument as parsed by argparse
45     :return: Process objectk
46     """
47     process_id = adb_client.get_process_pid(args.process_name)
48     process = Process(args.process_name, process_id)
49     return process
50
51
52 def collect_data(adb_client, process):
53     """ Start simpleperf on device and collect data. Pull perf.data into cwd.
54
55     :param process:  Process object
56     :return: Populated Process object
57     """
58
59     if process.args.dwarf_unwinding:
60         unwinding_parameter = "-g"
61         print "Unwinding with dwarf."
62     else:
63         unwinding_parameter = "--call-graph fp"
64         print "Unwinding with frame pointers."
65
66     process.cmd = "./simpleperf record \
67     -o /data/local/tmp/perf.data \
68     %s \
69     -p %s \
70     --duration %s \
71     -f %s" % (
72         unwinding_parameter,
73         process.pid,
74         process.args.capture_duration,
75         process.args.sample_frequency)
76
77     # TODO Add arg to configure what events to listen on:
78     # -e instructions
79     # -e cpu-cycles
80     # -e cache-references
81     # -e cache-misses
82     # -e branch-instructions
83     # -e branch-misses
84     # Also add the granularity with -c 100000
85     print("Process '%s' PID = %d" % (process.name, process.pid))
86
87     if process.args.skip_collection:
88        print("Skipping data collection, expecting perf.data in folder")
89        return True
90
91     print("Sampling for %s seconds..." % process.args.capture_duration)
92
93
94     adb_client.delete_previous_data()
95
96     success = adb_client.collect_data(process)
97     if not success:
98         return False
99
100     err = adb_client.pull_data()
101     if err:
102         return False
103
104     return True
105
106
107 def parse_samples(process, args):
108     """ read record_file, and print each sample"""
109
110     record_file = args.record_file
111     symfs_dir = args.symfs
112     kallsyms_file = args.kallsyms
113
114     lib = ReportLib()
115
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)
123
124     while True:
125         sample = lib.GetNextSample()
126         if sample is None:
127             lib.Close()
128             break
129         symbol = lib.GetSymbolOfCurrentSample()
130         callchain = lib.GetCallChainOfCurrentSample()
131         process.get_thread(sample.tid).add_callchain(callchain, symbol, sample)
132         process.num_samples += 1
133
134     print("Parsed %s callchains." % process.num_samples)
135
136
137 def collapse_callgraphs(process):
138     """
139     For each thread, collapse all callgraph into one flamegraph.
140     :param process:  Process object
141     :return: None
142     """
143     for _, thread in process.threads.items():
144         thread.collapse_flamegraph()
145
146
147 def get_local_asset_content(local_path):
148     """
149     Retrieves local package text content
150     :param local_path: str, filename of local asset
151     :return: str, the content of local_path
152     """
153     f = open(os.path.join(os.path.dirname(__file__), local_path), 'r')
154     content = f.read()
155     f.close()
156     return content
157
158
159 def output_report(process):
160     """
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
164     """
165     f = open('report.html', 'w')
166     filepath = os.path.realpath(f.name)
167     f.write("<html>")
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"))
173     f.write('"/>')
174     f.write("<div style='display:inline-block;'> \
175     <font size='8'>\
176     Inferno Flamegraph Report</font><br/><br/> \
177     Process : %s (%d)<br/>\
178     Date&nbsp;&nbsp;&nbsp;&nbsp;: %s<br/>\
179     Threads : %d <br/>\
180     Samples : %d</br>\
181     Duration: %s seconds<br/>\
182     Machine : %s (%s) by %s<br/>\
183     Capture : %s<br/><br/></div>"
184             % (
185                 process.name,process.pid,
186                 datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"),
187                 len(process.threads),
188                 process.num_samples,
189                 process.args.capture_duration,
190                 process.props["ro.product.model"], process.props["ro.product.name"],
191                 process.props["ro.product.manufacturer"],
192                 process.cmd))
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"))
195
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)
201
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)
206
207     f.write("</body>")
208     f.write("</html>")
209     f.close()
210     return "file://" + filepath
211
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)
218
219
220 def generate_threads_offsets(process):
221     for _, thread in process.threads.items():
222         generate_flamegraph_offsets(thread.flamegraph)
223
224
225 def collect_machine_info(adb_client, process):
226     process.props = adb_client.get_props()
227
228
229 def setup_adb():
230     err = subprocess.call(["adb", "root"])
231     if err == 0:
232         return AdbRoot()
233     else:
234         return AdbNonRoot()
235
236 def open_report_in_browser(report_path):
237     # Try to open the report with Chrome
238     browser_key = ""
239     for key, value in webbrowser._browsers.items():
240         if key.find("chrome") != -1:
241            browser_key = key
242     browser = webbrowser.get(browser_key)
243     browser.open(report_path, new=0, autoraise=True)
244
245
246
247 def main():
248
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()
263
264     # Since we may attempt to sample privileged process, let's try to be root.
265     adb_client = setup_adb()
266
267     # Create a process object
268     process = create_process(adb_client, args)
269     if process.pid == 0:
270         print("Unable to retrive pid for process '%s'. Terminating." % process.name)
271         return
272     process.args = args
273
274
275     print("Starting data collection stage for process '%s'." % args.process_name)
276     success = collect_data(adb_client, process)
277     if not success:
278         print "Unable to collect data"
279         return
280
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)
287
288     print "Report generated at '%s'." % report_path
289
290 if __name__ == "__main__":
291     main()