Home | History | Annotate | Download | only in android
      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