| Index: build/android/adb_profile_chrome.py
|
| diff --git a/build/android/adb_profile_chrome.py b/build/android/adb_profile_chrome.py
|
| index b328e47ca979fc6ecd5fc817f4ed5c6394617caf..c8cea89ceb2406430293fed702220fae239ea433 100755
|
| --- a/build/android/adb_profile_chrome.py
|
| +++ b/build/android/adb_profile_chrome.py
|
| @@ -10,13 +10,156 @@ import optparse
|
| import os
|
| import re
|
| import sys
|
| +import threading
|
| import time
|
| +import zipfile
|
| +import zlib
|
|
|
| from pylib import android_commands
|
| +from pylib import cmd_helper
|
| from pylib import constants
|
| from pylib import pexpect
|
|
|
|
|
| +def _GetTraceTimestamp():
|
| + return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
|
| +
|
| +
|
| +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 = {
|
| @@ -32,111 +175,93 @@ def _GetSupportedBrowsers():
|
| return supported_browsers
|
|
|
|
|
| -def _StartTracing(adb, package_info, categories, continuous):
|
| - adb.BroadcastIntent(package_info.package, 'GPU_PROFILER_START',
|
| - '-e categories "%s"' % categories,
|
| - '-e continuous' if continuous else '')
|
| -
|
| -
|
| -def _StopTracing(adb, package_info):
|
| - adb.BroadcastIntent(package_info.package, 'GPU_PROFILER_STOP')
|
| +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 _GetLatestTraceFileName(adb, check_for_multiple_traces=True):
|
| - # 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 downloaded.
|
| - #
|
| - # We have to look for both of these messages to make sure we get the results
|
| - # from the latest trace and that the trace file is complete.
|
| - trace_start_re = re.compile(r'Logging performance trace to file')
|
| - trace_finish_re = re.compile(r'Profiler finished[.] Results are in (.*)[.]')
|
| +def _ArchiveFiles(host_files, output):
|
| + with zipfile.ZipFile(output, 'w') as z:
|
| + for host_file in host_files:
|
| + z.write(host_file)
|
| + os.unlink(host_file)
|
|
|
| - start_timeout = 5
|
| - start_match = None
|
| - finish_match = None
|
|
|
| - while True:
|
| - try:
|
| - start_match = adb.WaitForLogMatch(trace_start_re, None,
|
| - timeout=start_timeout)
|
| - except pexpect.TIMEOUT:
|
| - if start_match:
|
| - break
|
| - raise RuntimeError('Trace start marker not found. Is the correct version '
|
| - 'of the browser running?')
|
| - finish_match = adb.WaitForLogMatch(trace_finish_re, None, timeout=120)
|
| - if not check_for_multiple_traces:
|
| - break
|
| - # Now that we've found one trace file, switch to polling for the rest of the
|
| - # log to see if there are more.
|
| - start_timeout = 1
|
| - return finish_match.group(1)
|
| +def _PrintMessage(heading, eol='\n'):
|
| + sys.stdout.write('%s%s' % (heading, eol))
|
| + sys.stdout.flush()
|
|
|
|
|
| -def _DownloadTrace(adb, trace_file):
|
| - trace_file = trace_file.replace('/storage/emulated/0/', '/sdcard/')
|
| - host_file = os.path.abspath(os.path.basename(trace_file))
|
| - adb.PullFileFromDevice(trace_file, host_file)
|
| - return host_file
|
| +def _StartTracing(controllers, interval):
|
| + for controller in controllers:
|
| + controller.StartTracing(interval)
|
|
|
|
|
| -def _CompressFile(host_file):
|
| - compressed_file = host_file + '.gz'
|
| - with gzip.open(compressed_file, 'wb') as out:
|
| - with open(host_file, 'rb') as input_file:
|
| - out.write(input_file.read())
|
| - os.unlink(host_file)
|
| - return compressed_file
|
| +def _StopTracing(controllers):
|
| + for controller in controllers:
|
| + controller.StopTracing()
|
|
|
|
|
| -def _PrintMessage(heading, eol='\n'):
|
| - sys.stdout.write(heading + eol)
|
| - sys.stdout.flush()
|
| +def _PullTraces(controllers, output, compress):
|
| + _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
|
| + os.rename(trace_files[0], result)
|
| + else:
|
| + result = trace_files[0]
|
|
|
| -def _DownloadLatestTrace(adb, compress, check_for_multiple_traces=True):
|
| - _PrintMessage('Downloading trace...', eol='')
|
| - trace_file = _GetLatestTraceFileName(adb, check_for_multiple_traces)
|
| - host_file = _DownloadTrace(adb, trace_file)
|
| - if compress:
|
| - host_file = _CompressFile(host_file)
|
| _PrintMessage('done')
|
| - _PrintMessage('Trace written to %s' % host_file)
|
| -
|
| + _PrintMessage('Trace written to %s' % os.path.abspath(result))
|
|
|
| -def _CaptureAndDownloadTimedTrace(adb, package_info, categories, interval,
|
| - continuous, compress):
|
| - adb.StartMonitoringLogcat(clear=False)
|
| - adb.SyncLogCat()
|
|
|
| +def _CaptureAndPullTrace(controllers, interval, output, compress):
|
| + trace_type = ' + '.join(map(str, controllers))
|
| try:
|
| - _PrintMessage('Capturing %d-second trace. Press Ctrl-C to stop early...' \
|
| - % interval, eol='')
|
| - _StartTracing(adb, package_info, categories, continuous)
|
| - time.sleep(interval)
|
| + _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, stopping...', eol='')
|
| - _StopTracing(adb, package_info)
|
| - _PrintMessage('done')
|
| + _PrintMessage('\nInterrupted...', eol='')
|
| + finally:
|
| + _StopTracing(controllers)
|
| + if interval:
|
| + _PrintMessage('done')
|
|
|
| - # Wait a bit for the browser to finish writing the trace file.
|
| - time.sleep(interval / 4 + 1)
|
| + _PullTraces(controllers, output, compress)
|
|
|
| - _DownloadLatestTrace(adb, compress, check_for_multiple_traces=False)
|
|
|
| -
|
| -def _ComputeCategories(options):
|
| - categories = [options.categories]
|
| +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*')
|
| - return ",".join(categories)
|
| + 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():
|
| @@ -145,36 +270,40 @@ def main():
|
| 'chromium.org/developers/how-tos/trace-event-'
|
| 'profiling-tool for detailed instructions for '
|
| 'profiling.')
|
| - manual_options = optparse.OptionGroup(parser, 'Manual tracing')
|
| - manual_options.add_option('--start', help='Start tracing.',
|
| - action='store_true')
|
| - manual_options.add_option('--stop', help='Stop tracing.',
|
| - action='store_true')
|
| - manual_options.add_option('-d', '--download', help='Download latest trace.',
|
| - action='store_true')
|
| - parser.add_option_group(manual_options)
|
| -
|
| - auto_options = optparse.OptionGroup(parser, 'Automated tracing')
|
| - auto_options.add_option('-t', '--time', help='Profile for N seconds and '
|
| +
|
| + 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(auto_options)
|
| + 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 categories to trace '
|
| 'with comma-delimited wildcards, e.g., '
|
| - '"*", "cat1*,-cat1a". Default is "*".', default='*')
|
| + '"*", "cat1*,-cat1a". Default is "*".', default='*',
|
| + dest='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.', 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)
|
|
|
| - parser.add_option('-o', '--output', help='Save profile output to file. '
|
| - 'Default is "/sdcard/Download/chrome-profile-results-*".')
|
| - parser.add_option('--continuous', help='Using the trace buffer as a ring '
|
| - 'buffer, continuously profile until stopped.',
|
| - action='store_true')
|
| + parser.add_option('-o', '--output', help='Save profile output to file.')
|
| browsers = sorted(_GetSupportedBrowsers().keys())
|
| parser.add_option('-b', '--browser', help='Select among installed browsers. '
|
| 'One of ' + ', '.join(browsers) + ', "stable" is used by '
|
| @@ -186,26 +315,45 @@ def main():
|
| 'with gzip. ', action='store_true')
|
| options, args = parser.parse_args()
|
|
|
| - if not any([options.start, options.stop, options.time, options.download]):
|
| - _PrintMessage('One of start/stop/download/time should be specified.')
|
| - return 1
|
| -
|
| if options.verbose:
|
| logging.getLogger().setLevel(logging.DEBUG)
|
|
|
| adb = android_commands.AndroidCommands()
|
| - categories = _ComputeCategories(options)
|
| + 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 options.start:
|
| - _StartTracing(adb, package_info, categories, options.continuous)
|
| - elif options.stop:
|
| - _StopTracing(adb, package_info)
|
| - elif options.download:
|
| - _DownloadLatestTrace(adb, options.compress)
|
| - elif options.time:
|
| - _CaptureAndDownloadTimedTrace(adb, package_info, categories, options.time,
|
| - options.continuous, options.compress)
|
| + 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
|
| +
|
| + _CaptureAndPullTrace(controllers,
|
| + options.time if not options.continuous else 0,
|
| + options.output,
|
| + options.compress)
|
|
|
|
|
| if __name__ == '__main__':
|
|
|