OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # | 2 # |
3 # Copyright 2013 The Chromium Authors. All rights reserved. | 3 # Copyright 2013 The Chromium Authors. All rights reserved. |
4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 import gzip | 7 import gzip |
8 import logging | 8 import logging |
9 import optparse | 9 import optparse |
10 import os | 10 import os |
11 import re | 11 import re |
12 import sys | 12 import sys |
| 13 import threading |
13 import time | 14 import time |
| 15 import zipfile |
| 16 import zlib |
14 | 17 |
15 from pylib import android_commands | 18 from pylib import android_commands |
| 19 from pylib import cmd_helper |
16 from pylib import constants | 20 from pylib import constants |
17 from pylib import pexpect | 21 from pylib import pexpect |
18 | 22 |
19 | 23 |
| 24 def _GetTraceTimestamp(): |
| 25 return time.strftime('%Y-%m-%d-%H%M%S', time.localtime()) |
| 26 |
| 27 |
| 28 class ChromeTracingController(object): |
| 29 def __init__(self, adb, package_info, categories, ring_buffer): |
| 30 self._adb = adb |
| 31 self._package_info = package_info |
| 32 self._categories = categories |
| 33 self._ring_buffer = ring_buffer |
| 34 self._trace_file = None |
| 35 self._trace_interval = None |
| 36 self._trace_start_re = \ |
| 37 re.compile(r'Logging performance trace to file: (.*)') |
| 38 self._trace_finish_re = \ |
| 39 re.compile(r'Profiler finished[.] Results are in (.*)[.]') |
| 40 self._adb.StartMonitoringLogcat(clear=False) |
| 41 |
| 42 def __str__(self): |
| 43 return 'chrome trace' |
| 44 |
| 45 def StartTracing(self, interval): |
| 46 self._trace_interval = interval |
| 47 self._adb.SyncLogCat() |
| 48 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START', |
| 49 '-e categories "%s"' % ','.join(self._categories), |
| 50 '-e continuous' if self._ring_buffer else '') |
| 51 # Chrome logs two different messages related to tracing: |
| 52 # |
| 53 # 1. "Logging performance trace to file [...]" |
| 54 # 2. "Profiler finished. Results are in [...]" |
| 55 # |
| 56 # The first one is printed when tracing starts and the second one indicates |
| 57 # that the trace file is ready to be pulled. |
| 58 try: |
| 59 self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re, |
| 60 None, |
| 61 timeout=5).group(1) |
| 62 except pexpect.TIMEOUT: |
| 63 raise RuntimeError('Trace start marker not found. Is the correct version ' |
| 64 'of the browser running?') |
| 65 |
| 66 def StopTracing(self): |
| 67 if not self._trace_file: |
| 68 return |
| 69 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP') |
| 70 self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120) |
| 71 |
| 72 def PullTrace(self): |
| 73 # Wait a bit for the browser to finish writing the trace file. |
| 74 time.sleep(self._trace_interval / 4 + 1) |
| 75 |
| 76 trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/') |
| 77 host_file = os.path.join(os.path.curdir, os.path.basename(trace_file)) |
| 78 self._adb.PullFileFromDevice(trace_file, host_file) |
| 79 return host_file |
| 80 |
| 81 |
| 82 _SYSTRACE_OPTIONS = [ |
| 83 # Compress the trace before sending it over USB. |
| 84 '-z', |
| 85 # Use a large trace buffer to increase the polling interval. |
| 86 '-b', '16384' |
| 87 ] |
| 88 |
| 89 # Interval in seconds for sampling systrace data. |
| 90 _SYSTRACE_INTERVAL = 15 |
| 91 |
| 92 |
| 93 class SystraceController(object): |
| 94 def __init__(self, adb, categories, ring_buffer): |
| 95 self._adb = adb |
| 96 self._categories = categories |
| 97 self._ring_buffer = ring_buffer |
| 98 self._done = threading.Event() |
| 99 self._thread = None |
| 100 self._trace_data = None |
| 101 |
| 102 def __str__(self): |
| 103 return 'systrace' |
| 104 |
| 105 @staticmethod |
| 106 def GetCategories(adb): |
| 107 return adb.RunShellCommand('atrace --list_categories') |
| 108 |
| 109 def StartTracing(self, interval): |
| 110 self._thread = threading.Thread(target=self._CollectData) |
| 111 self._thread.start() |
| 112 |
| 113 def StopTracing(self): |
| 114 self._done.set() |
| 115 |
| 116 def PullTrace(self): |
| 117 self._thread.join() |
| 118 self._thread = None |
| 119 if self._trace_data: |
| 120 output_name = 'systrace-%s' % _GetTraceTimestamp() |
| 121 with open(output_name, 'w') as out: |
| 122 out.write(self._trace_data) |
| 123 return output_name |
| 124 |
| 125 def _RunATraceCommand(self, command): |
| 126 # We use a separate interface to adb because the one from AndroidCommands |
| 127 # isn't re-entrant. |
| 128 device = ['-s', self._adb.GetDevice()] if self._adb.GetDevice() else [] |
| 129 cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \ |
| 130 _SYSTRACE_OPTIONS + self._categories |
| 131 return cmd_helper.GetCmdOutput(cmd) |
| 132 |
| 133 def _CollectData(self): |
| 134 trace_data = [] |
| 135 self._RunATraceCommand('async_start') |
| 136 try: |
| 137 while not self._done.is_set(): |
| 138 self._done.wait(_SYSTRACE_INTERVAL) |
| 139 if not self._ring_buffer or self._done.is_set(): |
| 140 trace_data.append( |
| 141 self._DecodeTraceData(self._RunATraceCommand('async_dump'))) |
| 142 finally: |
| 143 trace_data.append( |
| 144 self._DecodeTraceData(self._RunATraceCommand('async_stop'))) |
| 145 self._trace_data = ''.join([zlib.decompress(d) for d in trace_data]) |
| 146 |
| 147 @staticmethod |
| 148 def _DecodeTraceData(trace_data): |
| 149 try: |
| 150 trace_start = trace_data.index('TRACE:') |
| 151 except ValueError: |
| 152 raise RuntimeError('Systrace start marker not found') |
| 153 trace_data = trace_data[trace_start + 6:] |
| 154 |
| 155 # Collapse CRLFs that are added by adb shell. |
| 156 if trace_data.startswith('\r\n'): |
| 157 trace_data = trace_data.replace('\r\n', '\n') |
| 158 |
| 159 # Skip the initial newline. |
| 160 return trace_data[1:] |
| 161 |
| 162 |
20 def _GetSupportedBrowsers(): | 163 def _GetSupportedBrowsers(): |
21 # Add aliases for backwards compatibility. | 164 # Add aliases for backwards compatibility. |
22 supported_browsers = { | 165 supported_browsers = { |
23 'stable': constants.PACKAGE_INFO['chrome_stable'], | 166 'stable': constants.PACKAGE_INFO['chrome_stable'], |
24 'beta': constants.PACKAGE_INFO['chrome_beta'], | 167 'beta': constants.PACKAGE_INFO['chrome_beta'], |
25 'dev': constants.PACKAGE_INFO['chrome_dev'], | 168 'dev': constants.PACKAGE_INFO['chrome_dev'], |
26 'build': constants.PACKAGE_INFO['chrome'], | 169 'build': constants.PACKAGE_INFO['chrome'], |
27 } | 170 } |
28 supported_browsers.update(constants.PACKAGE_INFO) | 171 supported_browsers.update(constants.PACKAGE_INFO) |
29 unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser'] | 172 unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser'] |
30 for browser in unsupported_browsers: | 173 for browser in unsupported_browsers: |
31 del supported_browsers[browser] | 174 del supported_browsers[browser] |
32 return supported_browsers | 175 return supported_browsers |
33 | 176 |
34 | 177 |
35 def _StartTracing(adb, package_info, categories, continuous): | 178 def _CompressFile(host_file, output): |
36 adb.BroadcastIntent(package_info.package, 'GPU_PROFILER_START', | 179 with gzip.open(output, 'wb') as out: |
37 '-e categories "%s"' % categories, | |
38 '-e continuous' if continuous else '') | |
39 | |
40 | |
41 def _StopTracing(adb, package_info): | |
42 adb.BroadcastIntent(package_info.package, 'GPU_PROFILER_STOP') | |
43 | |
44 | |
45 def _GetLatestTraceFileName(adb, check_for_multiple_traces=True): | |
46 # Chrome logs two different messages related to tracing: | |
47 # | |
48 # 1. "Logging performance trace to file [...]" | |
49 # 2. "Profiler finished. Results are in [...]" | |
50 # | |
51 # The first one is printed when tracing starts and the second one indicates | |
52 # that the trace file is ready to be downloaded. | |
53 # | |
54 # We have to look for both of these messages to make sure we get the results | |
55 # from the latest trace and that the trace file is complete. | |
56 trace_start_re = re.compile(r'Logging performance trace to file') | |
57 trace_finish_re = re.compile(r'Profiler finished[.] Results are in (.*)[.]') | |
58 | |
59 start_timeout = 5 | |
60 start_match = None | |
61 finish_match = None | |
62 | |
63 while True: | |
64 try: | |
65 start_match = adb.WaitForLogMatch(trace_start_re, None, | |
66 timeout=start_timeout) | |
67 except pexpect.TIMEOUT: | |
68 if start_match: | |
69 break | |
70 raise RuntimeError('Trace start marker not found. Is the correct version ' | |
71 'of the browser running?') | |
72 finish_match = adb.WaitForLogMatch(trace_finish_re, None, timeout=120) | |
73 if not check_for_multiple_traces: | |
74 break | |
75 # Now that we've found one trace file, switch to polling for the rest of the | |
76 # log to see if there are more. | |
77 start_timeout = 1 | |
78 return finish_match.group(1) | |
79 | |
80 | |
81 def _DownloadTrace(adb, trace_file): | |
82 trace_file = trace_file.replace('/storage/emulated/0/', '/sdcard/') | |
83 host_file = os.path.abspath(os.path.basename(trace_file)) | |
84 adb.PullFileFromDevice(trace_file, host_file) | |
85 return host_file | |
86 | |
87 | |
88 def _CompressFile(host_file): | |
89 compressed_file = host_file + '.gz' | |
90 with gzip.open(compressed_file, 'wb') as out: | |
91 with open(host_file, 'rb') as input_file: | 180 with open(host_file, 'rb') as input_file: |
92 out.write(input_file.read()) | 181 out.write(input_file.read()) |
93 os.unlink(host_file) | 182 os.unlink(host_file) |
94 return compressed_file | 183 |
| 184 |
| 185 def _ArchiveFiles(host_files, output): |
| 186 with zipfile.ZipFile(output, 'w') as z: |
| 187 for host_file in host_files: |
| 188 z.write(host_file) |
| 189 os.unlink(host_file) |
95 | 190 |
96 | 191 |
97 def _PrintMessage(heading, eol='\n'): | 192 def _PrintMessage(heading, eol='\n'): |
98 sys.stdout.write(heading + eol) | 193 sys.stdout.write('%s%s' % (heading, eol)) |
99 sys.stdout.flush() | 194 sys.stdout.flush() |
100 | 195 |
101 | 196 |
102 def _DownloadLatestTrace(adb, compress, check_for_multiple_traces=True): | 197 def _StartTracing(controllers, interval): |
103 _PrintMessage('Downloading trace...', eol='') | 198 for controller in controllers: |
104 trace_file = _GetLatestTraceFileName(adb, check_for_multiple_traces) | 199 controller.StartTracing(interval) |
105 host_file = _DownloadTrace(adb, trace_file) | |
106 if compress: | |
107 host_file = _CompressFile(host_file) | |
108 _PrintMessage('done') | |
109 _PrintMessage('Trace written to %s' % host_file) | |
110 | 200 |
111 | 201 |
112 def _CaptureAndDownloadTimedTrace(adb, package_info, categories, interval, | 202 def _StopTracing(controllers): |
113 continuous, compress): | 203 for controller in controllers: |
114 adb.StartMonitoringLogcat(clear=False) | 204 controller.StopTracing() |
115 adb.SyncLogCat() | |
116 | |
117 try: | |
118 _PrintMessage('Capturing %d-second trace. Press Ctrl-C to stop early...' \ | |
119 % interval, eol='') | |
120 _StartTracing(adb, package_info, categories, continuous) | |
121 time.sleep(interval) | |
122 except KeyboardInterrupt: | |
123 _PrintMessage('\nInterrupted, stopping...', eol='') | |
124 _StopTracing(adb, package_info) | |
125 _PrintMessage('done') | |
126 | |
127 # Wait a bit for the browser to finish writing the trace file. | |
128 time.sleep(interval / 4 + 1) | |
129 | |
130 _DownloadLatestTrace(adb, compress, check_for_multiple_traces=False) | |
131 | 205 |
132 | 206 |
133 def _ComputeCategories(options): | 207 def _PullTraces(controllers, output, compress): |
134 categories = [options.categories] | 208 _PrintMessage('Downloading...', eol='') |
| 209 trace_files = [] |
| 210 for controller in controllers: |
| 211 trace_files.append(controller.PullTrace()) |
| 212 |
| 213 if compress and len(trace_files) == 1: |
| 214 result = output or trace_files[0] + '.gz' |
| 215 _CompressFile(trace_files[0], result) |
| 216 elif len(trace_files) > 1: |
| 217 result = output or 'chrome-combined-trace-%s.zip' % _GetTraceTimestamp() |
| 218 _ArchiveFiles(trace_files, result) |
| 219 elif output: |
| 220 result = output |
| 221 os.rename(trace_files[0], result) |
| 222 else: |
| 223 result = trace_files[0] |
| 224 |
| 225 _PrintMessage('done') |
| 226 _PrintMessage('Trace written to %s' % os.path.abspath(result)) |
| 227 |
| 228 |
| 229 def _CaptureAndPullTrace(controllers, interval, output, compress): |
| 230 trace_type = ' + '.join(map(str, controllers)) |
| 231 try: |
| 232 _StartTracing(controllers, interval) |
| 233 if interval: |
| 234 _PrintMessage('Capturing %d-second %s. Press Ctrl-C to stop early...' % \ |
| 235 (interval, trace_type), eol='') |
| 236 time.sleep(interval) |
| 237 else: |
| 238 _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='') |
| 239 raw_input() |
| 240 except KeyboardInterrupt: |
| 241 _PrintMessage('\nInterrupted...', eol='') |
| 242 finally: |
| 243 _StopTracing(controllers) |
| 244 if interval: |
| 245 _PrintMessage('done') |
| 246 |
| 247 _PullTraces(controllers, output, compress) |
| 248 |
| 249 |
| 250 def _ComputeChromeCategories(options): |
| 251 categories = [] |
135 if options.trace_cc: | 252 if options.trace_cc: |
136 categories.append('disabled-by-default-cc.debug*') | 253 categories.append('disabled-by-default-cc.debug*') |
137 if options.trace_gpu: | 254 if options.trace_gpu: |
138 categories.append('disabled-by-default-gpu.debug*') | 255 categories.append('disabled-by-default-gpu.debug*') |
139 return ",".join(categories) | 256 if options.chrome_categories: |
| 257 categories += options.chrome_categories.split(',') |
| 258 return categories |
| 259 |
| 260 |
| 261 def _ComputeSystraceCategories(options): |
| 262 if not options.systrace_categories: |
| 263 return [] |
| 264 return options.systrace_categories.split(',') |
140 | 265 |
141 | 266 |
142 def main(): | 267 def main(): |
143 parser = optparse.OptionParser(description='Record about://tracing profiles ' | 268 parser = optparse.OptionParser(description='Record about://tracing profiles ' |
144 'from Android browsers. See http://dev.' | 269 'from Android browsers. See http://dev.' |
145 'chromium.org/developers/how-tos/trace-event-' | 270 'chromium.org/developers/how-tos/trace-event-' |
146 'profiling-tool for detailed instructions for ' | 271 'profiling-tool for detailed instructions for ' |
147 'profiling.') | 272 'profiling.') |
148 manual_options = optparse.OptionGroup(parser, 'Manual tracing') | |
149 manual_options.add_option('--start', help='Start tracing.', | |
150 action='store_true') | |
151 manual_options.add_option('--stop', help='Stop tracing.', | |
152 action='store_true') | |
153 manual_options.add_option('-d', '--download', help='Download latest trace.', | |
154 action='store_true') | |
155 parser.add_option_group(manual_options) | |
156 | 273 |
157 auto_options = optparse.OptionGroup(parser, 'Automated tracing') | 274 timed_options = optparse.OptionGroup(parser, 'Timed tracing') |
158 auto_options.add_option('-t', '--time', help='Profile for N seconds and ' | 275 timed_options.add_option('-t', '--time', help='Profile for N seconds and ' |
159 'download the resulting trace.', metavar='N', | 276 'download the resulting trace.', metavar='N', |
160 type='float') | 277 type='float') |
161 parser.add_option_group(auto_options) | 278 parser.add_option_group(timed_options) |
| 279 |
| 280 cont_options = optparse.OptionGroup(parser, 'Continuous tracing') |
| 281 cont_options.add_option('--continuous', help='Profile continuously until ' |
| 282 'stopped.', action='store_true') |
| 283 cont_options.add_option('--ring-buffer', help='Use the trace buffer as a ' |
| 284 'ring buffer and save its contents when stopping ' |
| 285 'instead of appending events into one long trace.', |
| 286 action='store_true') |
| 287 parser.add_option_group(cont_options) |
162 | 288 |
163 categories = optparse.OptionGroup(parser, 'Trace categories') | 289 categories = optparse.OptionGroup(parser, 'Trace categories') |
164 categories.add_option('-c', '--categories', help='Select categories to trace ' | 290 categories.add_option('-c', '--categories', help='Select categories to trace ' |
165 'with comma-delimited wildcards, e.g., ' | 291 'with comma-delimited wildcards, e.g., ' |
166 '"*", "cat1*,-cat1a". Default is "*".', default='*') | 292 '"*", "cat1*,-cat1a". Default is "*".', default='*', |
| 293 dest='chrome_categories') |
| 294 categories.add_option('-s', '--systrace', help='Capture a systrace with the ' |
| 295 'chosen comma-delimited systrace categories. You can ' |
| 296 'also capture a combined Chrome + systrace by enabling ' |
| 297 'both types of categories. Use "list" to see the ' |
| 298 'available categories.', metavar='SYS_CATEGORIES', |
| 299 dest='systrace_categories', default='') |
167 categories.add_option('--trace-cc', help='Enable extra trace categories for ' | 300 categories.add_option('--trace-cc', help='Enable extra trace categories for ' |
168 'compositor frame viewer data.', action='store_true') | 301 'compositor frame viewer data.', action='store_true') |
169 categories.add_option('--trace-gpu', help='Enable extra trace categories for ' | 302 categories.add_option('--trace-gpu', help='Enable extra trace categories for ' |
170 'GPU data.', action='store_true') | 303 'GPU data.', action='store_true') |
171 parser.add_option_group(categories) | 304 parser.add_option_group(categories) |
172 | 305 |
173 parser.add_option('-o', '--output', help='Save profile output to file. ' | 306 parser.add_option('-o', '--output', help='Save profile output to file.') |
174 'Default is "/sdcard/Download/chrome-profile-results-*".') | |
175 parser.add_option('--continuous', help='Using the trace buffer as a ring ' | |
176 'buffer, continuously profile until stopped.', | |
177 action='store_true') | |
178 browsers = sorted(_GetSupportedBrowsers().keys()) | 307 browsers = sorted(_GetSupportedBrowsers().keys()) |
179 parser.add_option('-b', '--browser', help='Select among installed browsers. ' | 308 parser.add_option('-b', '--browser', help='Select among installed browsers. ' |
180 'One of ' + ', '.join(browsers) + ', "stable" is used by ' | 309 'One of ' + ', '.join(browsers) + ', "stable" is used by ' |
181 'default.', type='choice', choices=browsers, | 310 'default.', type='choice', choices=browsers, |
182 default='stable') | 311 default='stable') |
183 parser.add_option('-v', '--verbose', help='Verbose logging.', | 312 parser.add_option('-v', '--verbose', help='Verbose logging.', |
184 action='store_true') | 313 action='store_true') |
185 parser.add_option('-z', '--compress', help='Compress the resulting trace ' | 314 parser.add_option('-z', '--compress', help='Compress the resulting trace ' |
186 'with gzip. ', action='store_true') | 315 'with gzip. ', action='store_true') |
187 options, args = parser.parse_args() | 316 options, args = parser.parse_args() |
188 | 317 |
189 if not any([options.start, options.stop, options.time, options.download]): | |
190 _PrintMessage('One of start/stop/download/time should be specified.') | |
191 return 1 | |
192 | |
193 if options.verbose: | 318 if options.verbose: |
194 logging.getLogger().setLevel(logging.DEBUG) | 319 logging.getLogger().setLevel(logging.DEBUG) |
195 | 320 |
196 adb = android_commands.AndroidCommands() | 321 adb = android_commands.AndroidCommands() |
197 categories = _ComputeCategories(options) | 322 if options.systrace_categories in ['list', 'help']: |
| 323 _PrintMessage('\n'.join(SystraceController.GetCategories(adb))) |
| 324 return 0 |
| 325 |
| 326 if not options.time and not options.continuous: |
| 327 _PrintMessage('Time interval or continuous tracing should be specified.') |
| 328 return 1 |
| 329 |
| 330 chrome_categories = _ComputeChromeCategories(options) |
| 331 systrace_categories = _ComputeSystraceCategories(options) |
198 package_info = _GetSupportedBrowsers()[options.browser] | 332 package_info = _GetSupportedBrowsers()[options.browser] |
199 | 333 |
200 if options.start: | 334 if chrome_categories and 'webview' in systrace_categories: |
201 _StartTracing(adb, package_info, categories, options.continuous) | 335 logging.warning('Using the "webview" category in systrace together with ' |
202 elif options.stop: | 336 'Chrome tracing results in duplicate trace events.') |
203 _StopTracing(adb, package_info) | 337 |
204 elif options.download: | 338 controllers = [] |
205 _DownloadLatestTrace(adb, options.compress) | 339 if chrome_categories: |
206 elif options.time: | 340 controllers.append(ChromeTracingController(adb, |
207 _CaptureAndDownloadTimedTrace(adb, package_info, categories, options.time, | 341 package_info, |
208 options.continuous, options.compress) | 342 chrome_categories, |
| 343 options.ring_buffer)) |
| 344 if systrace_categories: |
| 345 controllers.append(SystraceController(adb, |
| 346 systrace_categories, |
| 347 options.ring_buffer)) |
| 348 |
| 349 if not controllers: |
| 350 _PrintMessage('No trace categories enabled.') |
| 351 return 1 |
| 352 |
| 353 _CaptureAndPullTrace(controllers, |
| 354 options.time if not options.continuous else 0, |
| 355 options.output, |
| 356 options.compress) |
209 | 357 |
210 | 358 |
211 if __name__ == '__main__': | 359 if __name__ == '__main__': |
212 sys.exit(main()) | 360 sys.exit(main()) |
OLD | NEW |