1 #!/usr/bin/env python 2 # 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 5 # found in the LICENSE file. 6 7 import base64 8 import gzip 9 import logging 10 import optparse 11 import os 12 import re 13 import shutil 14 import sys 15 import threading 16 import time 17 import webbrowser 18 import zipfile 19 import zlib 20 21 from pylib import android_commands 22 from pylib import cmd_helper 23 from pylib import constants 24 from pylib import pexpect 25 26 27 _TRACE_VIEWER_TEMPLATE = """<!DOCTYPE html> 28 <html> 29 <head> 30 <title>%(title)s</title> 31 <style> 32 %(timeline_css)s 33 </style> 34 <style> 35 .view { 36 overflow: hidden; 37 position: absolute; 38 top: 0; 39 bottom: 0; 40 left: 0; 41 right: 0; 42 } 43 </style> 44 <script> 45 %(timeline_js)s 46 </script> 47 <script> 48 document.addEventListener('DOMContentLoaded', function() { 49 var trace_data = window.atob('%(trace_data_base64)s'); 50 var m = new tracing.TraceModel(trace_data); 51 var timelineViewEl = document.querySelector('.view'); 52 ui.decorate(timelineViewEl, tracing.TimelineView); 53 timelineViewEl.model = m; 54 timelineViewEl.tabIndex = 1; 55 timelineViewEl.timeline.focusElement = timelineViewEl; 56 }); 57 </script> 58 </head> 59 <body> 60 <div class="view"></view> 61 </body> 62 </html>""" 63 64 _DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES' 65 66 67 def _GetTraceTimestamp(): 68 return time.strftime('%Y-%m-%d-%H%M%S', time.localtime()) 69 70 71 def _PackageTraceAsHtml(trace_file_name, html_file_name): 72 trace_viewer_root = os.path.join(constants.DIR_SOURCE_ROOT, 73 'third_party', 'trace-viewer') 74 build_dir = os.path.join(trace_viewer_root, 'build') 75 src_dir = os.path.join(trace_viewer_root, 'src') 76 if not build_dir in sys.path: 77 sys.path.append(build_dir) 78 generate = __import__('generate', {}, {}) 79 parse_deps = __import__('parse_deps', {}, {}) 80 81 basename = os.path.splitext(trace_file_name)[0] 82 load_sequence = parse_deps.calc_load_sequence( 83 ['tracing/standalone_timeline_view.js'], [src_dir]) 84 85 with open(trace_file_name) as trace_file: 86 trace_data = base64.b64encode(trace_file.read()) 87 with open(html_file_name, 'w') as html_file: 88 html = _TRACE_VIEWER_TEMPLATE % { 89 'title': os.path.basename(os.path.splitext(trace_file_name)[0]), 90 'timeline_js': generate.generate_js(load_sequence), 91 'timeline_css': generate.generate_css(load_sequence), 92 'trace_data_base64': trace_data 93 } 94 html_file.write(html) 95 96 97 class ChromeTracingController(object): 98 def __init__(self, adb, package_info, categories, ring_buffer): 99 self._adb = adb 100 self._package_info = package_info 101 self._categories = categories 102 self._ring_buffer = ring_buffer 103 self._trace_file = None 104 self._trace_interval = None 105 self._trace_start_re = \ 106 re.compile(r'Logging performance trace to file: (.*)') 107 self._trace_finish_re = \ 108 re.compile(r'Profiler finished[.] Results are in (.*)[.]') 109 self._adb.StartMonitoringLogcat(clear=False) 110 111 def __str__(self): 112 return 'chrome trace' 113 114 def StartTracing(self, interval): 115 self._trace_interval = interval 116 self._adb.SyncLogCat() 117 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START', 118 '-e categories "%s"' % ','.join(self._categories), 119 '-e continuous' if self._ring_buffer else '') 120 # Chrome logs two different messages related to tracing: 121 # 122 # 1. "Logging performance trace to file [...]" 123 # 2. "Profiler finished. Results are in [...]" 124 # 125 # The first one is printed when tracing starts and the second one indicates 126 # that the trace file is ready to be pulled. 127 try: 128 self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re, 129 None, 130 timeout=5).group(1) 131 except pexpect.TIMEOUT: 132 raise RuntimeError('Trace start marker not found. Is the correct version ' 133 'of the browser running?') 134 135 def StopTracing(self): 136 if not self._trace_file: 137 return 138 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP') 139 self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120) 140 141 def PullTrace(self): 142 # Wait a bit for the browser to finish writing the trace file. 143 time.sleep(self._trace_interval / 4 + 1) 144 145 trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/') 146 host_file = os.path.join(os.path.curdir, os.path.basename(trace_file)) 147 self._adb.PullFileFromDevice(trace_file, host_file) 148 return host_file 149 150 151 _SYSTRACE_OPTIONS = [ 152 # Compress the trace before sending it over USB. 153 '-z', 154 # Use a large trace buffer to increase the polling interval. 155 '-b', '16384' 156 ] 157 158 # Interval in seconds for sampling systrace data. 159 _SYSTRACE_INTERVAL = 15 160 161 162 class SystraceController(object): 163 def __init__(self, adb, categories, ring_buffer): 164 self._adb = adb 165 self._categories = categories 166 self._ring_buffer = ring_buffer 167 self._done = threading.Event() 168 self._thread = None 169 self._trace_data = None 170 171 def __str__(self): 172 return 'systrace' 173 174 @staticmethod 175 def GetCategories(adb): 176 return adb.RunShellCommand('atrace --list_categories') 177 178 def StartTracing(self, interval): 179 self._thread = threading.Thread(target=self._CollectData) 180 self._thread.start() 181 182 def StopTracing(self): 183 self._done.set() 184 185 def PullTrace(self): 186 self._thread.join() 187 self._thread = None 188 if self._trace_data: 189 output_name = 'systrace-%s' % _GetTraceTimestamp() 190 with open(output_name, 'w') as out: 191 out.write(self._trace_data) 192 return output_name 193 194 def _RunATraceCommand(self, command): 195 # We use a separate interface to adb because the one from AndroidCommands 196 # isn't re-entrant. 197 device = ['-s', self._adb.GetDevice()] if self._adb.GetDevice() else [] 198 cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \ 199 _SYSTRACE_OPTIONS + self._categories 200 return cmd_helper.GetCmdOutput(cmd) 201 202 def _CollectData(self): 203 trace_data = [] 204 self._RunATraceCommand('async_start') 205 try: 206 while not self._done.is_set(): 207 self._done.wait(_SYSTRACE_INTERVAL) 208 if not self._ring_buffer or self._done.is_set(): 209 trace_data.append( 210 self._DecodeTraceData(self._RunATraceCommand('async_dump'))) 211 finally: 212 trace_data.append( 213 self._DecodeTraceData(self._RunATraceCommand('async_stop'))) 214 self._trace_data = ''.join([zlib.decompress(d) for d in trace_data]) 215 216 @staticmethod 217 def _DecodeTraceData(trace_data): 218 try: 219 trace_start = trace_data.index('TRACE:') 220 except ValueError: 221 raise RuntimeError('Systrace start marker not found') 222 trace_data = trace_data[trace_start + 6:] 223 224 # Collapse CRLFs that are added by adb shell. 225 if trace_data.startswith('\r\n'): 226 trace_data = trace_data.replace('\r\n', '\n') 227 228 # Skip the initial newline. 229 return trace_data[1:] 230 231 232 def _GetSupportedBrowsers(): 233 # Add aliases for backwards compatibility. 234 supported_browsers = { 235 'stable': constants.PACKAGE_INFO['chrome_stable'], 236 'beta': constants.PACKAGE_INFO['chrome_beta'], 237 'dev': constants.PACKAGE_INFO['chrome_dev'], 238 'build': constants.PACKAGE_INFO['chrome'], 239 } 240 supported_browsers.update(constants.PACKAGE_INFO) 241 unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser'] 242 for browser in unsupported_browsers: 243 del supported_browsers[browser] 244 return supported_browsers 245 246 247 def _CompressFile(host_file, output): 248 with gzip.open(output, 'wb') as out: 249 with open(host_file, 'rb') as input_file: 250 out.write(input_file.read()) 251 os.unlink(host_file) 252 253 254 def _ArchiveFiles(host_files, output): 255 with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z: 256 for host_file in host_files: 257 z.write(host_file) 258 os.unlink(host_file) 259 260 261 def _PrintMessage(heading, eol='\n'): 262 sys.stdout.write('%s%s' % (heading, eol)) 263 sys.stdout.flush() 264 265 266 def _StartTracing(controllers, interval): 267 for controller in controllers: 268 controller.StartTracing(interval) 269 270 271 def _StopTracing(controllers): 272 for controller in controllers: 273 controller.StopTracing() 274 275 276 def _PullTraces(controllers, output, compress, write_html): 277 _PrintMessage('Downloading...', eol='') 278 trace_files = [] 279 for controller in controllers: 280 trace_files.append(controller.PullTrace()) 281 282 if compress and len(trace_files) == 1: 283 result = output or trace_files[0] + '.gz' 284 _CompressFile(trace_files[0], result) 285 elif len(trace_files) > 1: 286 result = output or 'chrome-combined-trace-%s.zip' % _GetTraceTimestamp() 287 _ArchiveFiles(trace_files, result) 288 elif output: 289 result = output 290 shutil.move(trace_files[0], result) 291 else: 292 result = trace_files[0] 293 294 if write_html: 295 result, trace_file = os.path.splitext(result)[0] + '.html', result 296 _PackageTraceAsHtml(trace_file, result) 297 if trace_file != result: 298 os.unlink(trace_file) 299 300 _PrintMessage('done') 301 _PrintMessage('Trace written to %s' % os.path.abspath(result)) 302 return result 303 304 305 def _CaptureAndPullTrace(controllers, interval, output, compress, write_html): 306 trace_type = ' + '.join(map(str, controllers)) 307 try: 308 _StartTracing(controllers, interval) 309 if interval: 310 _PrintMessage('Capturing %d-second %s. Press Ctrl-C to stop early...' % \ 311 (interval, trace_type), eol='') 312 time.sleep(interval) 313 else: 314 _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='') 315 raw_input() 316 except KeyboardInterrupt: 317 _PrintMessage('\nInterrupted...', eol='') 318 finally: 319 _StopTracing(controllers) 320 if interval: 321 _PrintMessage('done') 322 323 return _PullTraces(controllers, output, compress, write_html) 324 325 326 def _ComputeChromeCategories(options): 327 categories = [] 328 if options.trace_cc: 329 categories.append('disabled-by-default-cc.debug*') 330 if options.trace_gpu: 331 categories.append('disabled-by-default-gpu.debug*') 332 if options.chrome_categories: 333 categories += options.chrome_categories.split(',') 334 return categories 335 336 337 def _ComputeSystraceCategories(options): 338 if not options.systrace_categories: 339 return [] 340 return options.systrace_categories.split(',') 341 342 343 def main(): 344 parser = optparse.OptionParser(description='Record about://tracing profiles ' 345 'from Android browsers. See http://dev.' 346 'chromium.org/developers/how-tos/trace-event-' 347 'profiling-tool for detailed instructions for ' 348 'profiling.') 349 350 timed_options = optparse.OptionGroup(parser, 'Timed tracing') 351 timed_options.add_option('-t', '--time', help='Profile for N seconds and ' 352 'download the resulting trace.', metavar='N', 353 type='float') 354 parser.add_option_group(timed_options) 355 356 cont_options = optparse.OptionGroup(parser, 'Continuous tracing') 357 cont_options.add_option('--continuous', help='Profile continuously until ' 358 'stopped.', action='store_true') 359 cont_options.add_option('--ring-buffer', help='Use the trace buffer as a ' 360 'ring buffer and save its contents when stopping ' 361 'instead of appending events into one long trace.', 362 action='store_true') 363 parser.add_option_group(cont_options) 364 365 categories = optparse.OptionGroup(parser, 'Trace categories') 366 categories.add_option('-c', '--categories', help='Select Chrome tracing ' 367 'categories with comma-delimited wildcards, ' 368 'e.g., "*", "cat1*,-cat1a". Omit this option to trace ' 369 'Chrome\'s default categories. Chrome tracing can be ' 370 'disabled with "--categories=\'\'".', 371 metavar='CHROME_CATEGORIES', dest='chrome_categories', 372 default=_DEFAULT_CHROME_CATEGORIES) 373 categories.add_option('-s', '--systrace', help='Capture a systrace with the ' 374 'chosen comma-delimited systrace categories. You can ' 375 'also capture a combined Chrome + systrace by enabling ' 376 'both types of categories. Use "list" to see the ' 377 'available categories. Systrace is disabled by ' 378 'default.', metavar='SYS_CATEGORIES', 379 dest='systrace_categories', default='') 380 categories.add_option('--trace-cc', help='Enable extra trace categories for ' 381 'compositor frame viewer data.', action='store_true') 382 categories.add_option('--trace-gpu', help='Enable extra trace categories for ' 383 'GPU data.', action='store_true') 384 parser.add_option_group(categories) 385 386 output_options = optparse.OptionGroup(parser, 'Output options') 387 output_options.add_option('-o', '--output', help='Save trace output to file.') 388 output_options.add_option('--html', help='Package trace into a standalone ' 389 'html file.', action='store_true') 390 output_options.add_option('--view', help='Open resulting trace file in a ' 391 'browser.', action='store_true') 392 parser.add_option_group(output_options) 393 394 browsers = sorted(_GetSupportedBrowsers().keys()) 395 parser.add_option('-b', '--browser', help='Select among installed browsers. ' 396 'One of ' + ', '.join(browsers) + ', "stable" is used by ' 397 'default.', type='choice', choices=browsers, 398 default='stable') 399 parser.add_option('-v', '--verbose', help='Verbose logging.', 400 action='store_true') 401 parser.add_option('-z', '--compress', help='Compress the resulting trace ' 402 'with gzip. ', action='store_true') 403 options, args = parser.parse_args() 404 405 if options.verbose: 406 logging.getLogger().setLevel(logging.DEBUG) 407 408 adb = android_commands.AndroidCommands() 409 if options.systrace_categories in ['list', 'help']: 410 _PrintMessage('\n'.join(SystraceController.GetCategories(adb))) 411 return 0 412 413 if not options.time and not options.continuous: 414 _PrintMessage('Time interval or continuous tracing should be specified.') 415 return 1 416 417 chrome_categories = _ComputeChromeCategories(options) 418 systrace_categories = _ComputeSystraceCategories(options) 419 package_info = _GetSupportedBrowsers()[options.browser] 420 421 if chrome_categories and 'webview' in systrace_categories: 422 logging.warning('Using the "webview" category in systrace together with ' 423 'Chrome tracing results in duplicate trace events.') 424 425 controllers = [] 426 if chrome_categories: 427 controllers.append(ChromeTracingController(adb, 428 package_info, 429 chrome_categories, 430 options.ring_buffer)) 431 if systrace_categories: 432 controllers.append(SystraceController(adb, 433 systrace_categories, 434 options.ring_buffer)) 435 436 if not controllers: 437 _PrintMessage('No trace categories enabled.') 438 return 1 439 440 result = _CaptureAndPullTrace(controllers, 441 options.time if not options.continuous else 0, 442 options.output, 443 options.compress, 444 options.html) 445 if options.view: 446 webbrowser.open(result) 447 448 449 if __name__ == '__main__': 450 sys.exit(main()) 451