| #!/usr/bin/env python |
| # |
| # Copyright 2013 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import base64 |
| import gzip |
| import logging |
| import optparse |
| import os |
| import re |
| import shutil |
| import sys |
| import threading |
| import time |
| import webbrowser |
| import zipfile |
| import zlib |
| |
| from pylib import android_commands |
| from pylib import cmd_helper |
| from pylib import constants |
| from pylib import pexpect |
| |
| |
| _TRACE_VIEWER_TEMPLATE = """<!DOCTYPE html> |
| <html> |
| <head> |
| <title>%(title)s</title> |
| <style> |
| %(timeline_css)s |
| </style> |
| <style> |
| .view { |
| overflow: hidden; |
| position: absolute; |
| top: 0; |
| bottom: 0; |
| left: 0; |
| right: 0; |
| } |
| </style> |
| <script> |
| %(timeline_js)s |
| </script> |
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| var trace_data = window.atob('%(trace_data_base64)s'); |
| var m = new tracing.TraceModel(trace_data); |
| var timelineViewEl = document.querySelector('.view'); |
| ui.decorate(timelineViewEl, tracing.TimelineView); |
| timelineViewEl.model = m; |
| timelineViewEl.tabIndex = 1; |
| timelineViewEl.timeline.focusElement = timelineViewEl; |
| }); |
| </script> |
| </head> |
| <body> |
| <div class="view"></view> |
| </body> |
| </html>""" |
| |
| _DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES' |
| |
| |
| def _GetTraceTimestamp(): |
| return time.strftime('%Y-%m-%d-%H%M%S', time.localtime()) |
| |
| |
| def _PackageTraceAsHtml(trace_file_name, html_file_name): |
| trace_viewer_root = os.path.join(constants.DIR_SOURCE_ROOT, |
| 'third_party', 'trace-viewer') |
| build_dir = os.path.join(trace_viewer_root, 'build') |
| src_dir = os.path.join(trace_viewer_root, 'src') |
| if not build_dir in sys.path: |
| sys.path.append(build_dir) |
| generate = __import__('generate', {}, {}) |
| parse_deps = __import__('parse_deps', {}, {}) |
| |
| basename = os.path.splitext(trace_file_name)[0] |
| load_sequence = parse_deps.calc_load_sequence( |
| ['tracing/standalone_timeline_view.js'], [src_dir]) |
| |
| with open(trace_file_name) as trace_file: |
| trace_data = base64.b64encode(trace_file.read()) |
| with open(html_file_name, 'w') as html_file: |
| html = _TRACE_VIEWER_TEMPLATE % { |
| 'title': os.path.basename(os.path.splitext(trace_file_name)[0]), |
| 'timeline_js': generate.generate_js(load_sequence), |
| 'timeline_css': generate.generate_css(load_sequence), |
| 'trace_data_base64': trace_data |
| } |
| html_file.write(html) |
| |
| |
| class ChromeTracingController(object): |
| def __init__(self, adb, package_info, categories, ring_buffer): |
| self._adb = adb |
| self._package_info = package_info |
| self._categories = categories |
| self._ring_buffer = ring_buffer |
| self._trace_file = None |
| self._trace_interval = None |
| self._trace_start_re = \ |
| re.compile(r'Logging performance trace to file: (.*)') |
| self._trace_finish_re = \ |
| re.compile(r'Profiler finished[.] Results are in (.*)[.]') |
| self._adb.StartMonitoringLogcat(clear=False) |
| |
| def __str__(self): |
| return 'chrome trace' |
| |
| def StartTracing(self, interval): |
| self._trace_interval = interval |
| self._adb.SyncLogCat() |
| self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START', |
| '-e categories "%s"' % ','.join(self._categories), |
| '-e continuous' if self._ring_buffer else '') |
| # Chrome logs two different messages related to tracing: |
| # |
| # 1. "Logging performance trace to file [...]" |
| # 2. "Profiler finished. Results are in [...]" |
| # |
| # The first one is printed when tracing starts and the second one indicates |
| # that the trace file is ready to be pulled. |
| try: |
| self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re, |
| None, |
| timeout=5).group(1) |
| except pexpect.TIMEOUT: |
| raise RuntimeError('Trace start marker not found. Is the correct version ' |
| 'of the browser running?') |
| |
| def StopTracing(self): |
| if not self._trace_file: |
| return |
| self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP') |
| self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120) |
| |
| def PullTrace(self): |
| # Wait a bit for the browser to finish writing the trace file. |
| time.sleep(self._trace_interval / 4 + 1) |
| |
| trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/') |
| host_file = os.path.join(os.path.curdir, os.path.basename(trace_file)) |
| self._adb.PullFileFromDevice(trace_file, host_file) |
| return host_file |
| |
| |
| _SYSTRACE_OPTIONS = [ |
| # Compress the trace before sending it over USB. |
| '-z', |
| # Use a large trace buffer to increase the polling interval. |
| '-b', '16384' |
| ] |
| |
| # Interval in seconds for sampling systrace data. |
| _SYSTRACE_INTERVAL = 15 |
| |
| |
| class SystraceController(object): |
| def __init__(self, adb, categories, ring_buffer): |
| self._adb = adb |
| self._categories = categories |
| self._ring_buffer = ring_buffer |
| self._done = threading.Event() |
| self._thread = None |
| self._trace_data = None |
| |
| def __str__(self): |
| return 'systrace' |
| |
| @staticmethod |
| def GetCategories(adb): |
| return adb.RunShellCommand('atrace --list_categories') |
| |
| def StartTracing(self, interval): |
| self._thread = threading.Thread(target=self._CollectData) |
| self._thread.start() |
| |
| def StopTracing(self): |
| self._done.set() |
| |
| def PullTrace(self): |
| self._thread.join() |
| self._thread = None |
| if self._trace_data: |
| output_name = 'systrace-%s' % _GetTraceTimestamp() |
| with open(output_name, 'w') as out: |
| out.write(self._trace_data) |
| return output_name |
| |
| def _RunATraceCommand(self, command): |
| # We use a separate interface to adb because the one from AndroidCommands |
| # isn't re-entrant. |
| device = ['-s', self._adb.GetDevice()] if self._adb.GetDevice() else [] |
| cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \ |
| _SYSTRACE_OPTIONS + self._categories |
| return cmd_helper.GetCmdOutput(cmd) |
| |
| def _CollectData(self): |
| trace_data = [] |
| self._RunATraceCommand('async_start') |
| try: |
| while not self._done.is_set(): |
| self._done.wait(_SYSTRACE_INTERVAL) |
| if not self._ring_buffer or self._done.is_set(): |
| trace_data.append( |
| self._DecodeTraceData(self._RunATraceCommand('async_dump'))) |
| finally: |
| trace_data.append( |
| self._DecodeTraceData(self._RunATraceCommand('async_stop'))) |
| self._trace_data = ''.join([zlib.decompress(d) for d in trace_data]) |
| |
| @staticmethod |
| def _DecodeTraceData(trace_data): |
| try: |
| trace_start = trace_data.index('TRACE:') |
| except ValueError: |
| raise RuntimeError('Systrace start marker not found') |
| trace_data = trace_data[trace_start + 6:] |
| |
| # Collapse CRLFs that are added by adb shell. |
| if trace_data.startswith('\r\n'): |
| trace_data = trace_data.replace('\r\n', '\n') |
| |
| # Skip the initial newline. |
| return trace_data[1:] |
| |
| |
| def _GetSupportedBrowsers(): |
| # Add aliases for backwards compatibility. |
| supported_browsers = { |
| 'stable': constants.PACKAGE_INFO['chrome_stable'], |
| 'beta': constants.PACKAGE_INFO['chrome_beta'], |
| 'dev': constants.PACKAGE_INFO['chrome_dev'], |
| 'build': constants.PACKAGE_INFO['chrome'], |
| } |
| supported_browsers.update(constants.PACKAGE_INFO) |
| unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser'] |
| for browser in unsupported_browsers: |
| del supported_browsers[browser] |
| return supported_browsers |
| |
| |
| def _CompressFile(host_file, output): |
| with gzip.open(output, 'wb') as out: |
| with open(host_file, 'rb') as input_file: |
| out.write(input_file.read()) |
| os.unlink(host_file) |
| |
| |
| def _ArchiveFiles(host_files, output): |
| with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z: |
| for host_file in host_files: |
| z.write(host_file) |
| os.unlink(host_file) |
| |
| |
| def _PrintMessage(heading, eol='\n'): |
| sys.stdout.write('%s%s' % (heading, eol)) |
| sys.stdout.flush() |
| |
| |
| def _StartTracing(controllers, interval): |
| for controller in controllers: |
| controller.StartTracing(interval) |
| |
| |
| def _StopTracing(controllers): |
| for controller in controllers: |
| controller.StopTracing() |
| |
| |
| def _PullTraces(controllers, output, compress, write_html): |
| _PrintMessage('Downloading...', eol='') |
| trace_files = [] |
| for controller in controllers: |
| trace_files.append(controller.PullTrace()) |
| |
| if compress and len(trace_files) == 1: |
| result = output or trace_files[0] + '.gz' |
| _CompressFile(trace_files[0], result) |
| elif len(trace_files) > 1: |
| result = output or 'chrome-combined-trace-%s.zip' % _GetTraceTimestamp() |
| _ArchiveFiles(trace_files, result) |
| elif output: |
| result = output |
| shutil.move(trace_files[0], result) |
| else: |
| result = trace_files[0] |
| |
| if write_html: |
| result, trace_file = os.path.splitext(result)[0] + '.html', result |
| _PackageTraceAsHtml(trace_file, result) |
| if trace_file != result: |
| os.unlink(trace_file) |
| |
| _PrintMessage('done') |
| _PrintMessage('Trace written to %s' % os.path.abspath(result)) |
| return result |
| |
| |
| def _CaptureAndPullTrace(controllers, interval, output, compress, write_html): |
| trace_type = ' + '.join(map(str, controllers)) |
| try: |
| _StartTracing(controllers, interval) |
| if interval: |
| _PrintMessage('Capturing %d-second %s. Press Ctrl-C to stop early...' % \ |
| (interval, trace_type), eol='') |
| time.sleep(interval) |
| else: |
| _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='') |
| raw_input() |
| except KeyboardInterrupt: |
| _PrintMessage('\nInterrupted...', eol='') |
| finally: |
| _StopTracing(controllers) |
| if interval: |
| _PrintMessage('done') |
| |
| return _PullTraces(controllers, output, compress, write_html) |
| |
| |
| def _ComputeChromeCategories(options): |
| categories = [] |
| if options.trace_cc: |
| categories.append('disabled-by-default-cc.debug*') |
| if options.trace_gpu: |
| categories.append('disabled-by-default-gpu.debug*') |
| if options.chrome_categories: |
| categories += options.chrome_categories.split(',') |
| return categories |
| |
| |
| def _ComputeSystraceCategories(options): |
| if not options.systrace_categories: |
| return [] |
| return options.systrace_categories.split(',') |
| |
| |
| def main(): |
| parser = optparse.OptionParser(description='Record about://tracing profiles ' |
| 'from Android browsers. See http://dev.' |
| 'chromium.org/developers/how-tos/trace-event-' |
| 'profiling-tool for detailed instructions for ' |
| 'profiling.') |
| |
| timed_options = optparse.OptionGroup(parser, 'Timed tracing') |
| timed_options.add_option('-t', '--time', help='Profile for N seconds and ' |
| 'download the resulting trace.', metavar='N', |
| type='float') |
| parser.add_option_group(timed_options) |
| |
| cont_options = optparse.OptionGroup(parser, 'Continuous tracing') |
| cont_options.add_option('--continuous', help='Profile continuously until ' |
| 'stopped.', action='store_true') |
| cont_options.add_option('--ring-buffer', help='Use the trace buffer as a ' |
| 'ring buffer and save its contents when stopping ' |
| 'instead of appending events into one long trace.', |
| action='store_true') |
| parser.add_option_group(cont_options) |
| |
| categories = optparse.OptionGroup(parser, 'Trace categories') |
| categories.add_option('-c', '--categories', help='Select Chrome tracing ' |
| 'categories with comma-delimited wildcards, ' |
| 'e.g., "*", "cat1*,-cat1a". Omit this option to trace ' |
| 'Chrome\'s default categories. Chrome tracing can be ' |
| 'disabled with "--categories=\'\'".', |
| metavar='CHROME_CATEGORIES', dest='chrome_categories', |
| default=_DEFAULT_CHROME_CATEGORIES) |
| categories.add_option('-s', '--systrace', help='Capture a systrace with the ' |
| 'chosen comma-delimited systrace categories. You can ' |
| 'also capture a combined Chrome + systrace by enabling ' |
| 'both types of categories. Use "list" to see the ' |
| 'available categories. Systrace is disabled by ' |
| 'default.', metavar='SYS_CATEGORIES', |
| dest='systrace_categories', default='') |
| categories.add_option('--trace-cc', help='Enable extra trace categories for ' |
| 'compositor frame viewer data.', action='store_true') |
| categories.add_option('--trace-gpu', help='Enable extra trace categories for ' |
| 'GPU data.', action='store_true') |
| parser.add_option_group(categories) |
| |
| output_options = optparse.OptionGroup(parser, 'Output options') |
| output_options.add_option('-o', '--output', help='Save trace output to file.') |
| output_options.add_option('--html', help='Package trace into a standalone ' |
| 'html file.', action='store_true') |
| output_options.add_option('--view', help='Open resulting trace file in a ' |
| 'browser.', action='store_true') |
| parser.add_option_group(output_options) |
| |
| browsers = sorted(_GetSupportedBrowsers().keys()) |
| parser.add_option('-b', '--browser', help='Select among installed browsers. ' |
| 'One of ' + ', '.join(browsers) + ', "stable" is used by ' |
| 'default.', type='choice', choices=browsers, |
| default='stable') |
| parser.add_option('-v', '--verbose', help='Verbose logging.', |
| action='store_true') |
| parser.add_option('-z', '--compress', help='Compress the resulting trace ' |
| 'with gzip. ', action='store_true') |
| options, args = parser.parse_args() |
| |
| if options.verbose: |
| logging.getLogger().setLevel(logging.DEBUG) |
| |
| adb = android_commands.AndroidCommands() |
| if options.systrace_categories in ['list', 'help']: |
| _PrintMessage('\n'.join(SystraceController.GetCategories(adb))) |
| return 0 |
| |
| if not options.time and not options.continuous: |
| _PrintMessage('Time interval or continuous tracing should be specified.') |
| return 1 |
| |
| chrome_categories = _ComputeChromeCategories(options) |
| systrace_categories = _ComputeSystraceCategories(options) |
| package_info = _GetSupportedBrowsers()[options.browser] |
| |
| if chrome_categories and 'webview' in systrace_categories: |
| logging.warning('Using the "webview" category in systrace together with ' |
| 'Chrome tracing results in duplicate trace events.') |
| |
| controllers = [] |
| if chrome_categories: |
| controllers.append(ChromeTracingController(adb, |
| package_info, |
| chrome_categories, |
| options.ring_buffer)) |
| if systrace_categories: |
| controllers.append(SystraceController(adb, |
| systrace_categories, |
| options.ring_buffer)) |
| |
| if not controllers: |
| _PrintMessage('No trace categories enabled.') |
| return 1 |
| |
| result = _CaptureAndPullTrace(controllers, |
| options.time if not options.continuous else 0, |
| options.output, |
| options.compress, |
| options.html) |
| if options.view: |
| webbrowser.open(result) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |