Index: build/android/adb_profile_chrome.py |
diff --git a/build/android/adb_profile_chrome.py b/build/android/adb_profile_chrome.py |
index ba5b1ed554340a1c88aa46f925d0b38fd75732a5..7b0f2c69a7c57355ee24d403026c49374cc934a3 100755 |
--- a/build/android/adb_profile_chrome.py |
+++ b/build/android/adb_profile_chrome.py |
@@ -10,13 +10,145 @@ import optparse |
import os |
import pexpect |
import re |
+import subprocess |
import sys |
+import threading |
import time |
+import zipfile |
+import zlib |
from pylib import android_commands |
from pylib import constants |
+def _GetTraceTimestamp(): |
+ return time.strftime('%Y-%m-%d-%H%M%S', time.localtime()) |
+ |
+ |
+class ChromeTracingController(object): |
+ def __init__(self, adb, package_info, categories, continuous): |
+ self._adb = adb |
+ self._package_info = package_info |
+ self._categories = categories |
+ self._continuous = continuous |
+ 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._adb.SyncLogCat() |
+ self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START', |
+ '-e categories "%s"' % ','.join(self._categories), |
+ '-e continuous' if self._continuous 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 downloaded. |
bulach
2013/10/04 14:30:11
nit: how about s/Download/Pull/g ?
I think we norm
|
+ try: |
+ self._adb.WaitForLogMatch(self._trace_start_re, None, timeout=5) |
+ except pexpect.TIMEOUT: |
+ raise RuntimeError('Trace start marker not found. Is the correct version ' |
+ 'of the browser running?') |
+ self._trace_interval = interval |
+ |
+ def StopTracing(self): |
+ if not self._trace_interval: |
+ return |
+ self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP') |
+ self._trace_file = self._adb.WaitForLogMatch(self._trace_finish_re, None, |
+ timeout=120).group(1) |
+ |
+ def DownloadTrace(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' |
+] |
+ |
+class SystraceController(object): |
+ def __init__(self, device, categories): |
+ self._device = device |
+ self._categories = categories |
+ 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 DownloadTrace(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): |
+ device = ['-s', self._device] if self._device else [] |
+ cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \ |
+ _SYSTRACE_OPTIONS + self._categories |
+ return subprocess.check_output(cmd) |
bulach
2013/10/04 14:30:11
comment from tonyg :)
there's a pylib/cmd_helper.p
|
+ |
+ def _CollectData(self): |
+ trace_data = [] |
+ self._RunATraceCommand('async_start') |
+ try: |
+ while not self._done.is_set(): |
+ self._done.wait(15) |
+ 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 +164,88 @@ 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 _DownloadTraces(controllers, output, compress): |
+ _PrintMessage('Downloading...', eol='') |
+ trace_files = [] |
+ for controller in controllers: |
+ trace_files.append(controller.DownloadTrace()) |
+ 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 _CaptureAndDownloadTimedTrace(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) |
+ _StartTracing(controllers, interval) |
+ _PrintMessage('Capturing %d-second %s. Press Ctrl-C to stop early...' % \ |
+ (interval, trace_type), eol='') |
time.sleep(interval) |
except KeyboardInterrupt: |
- _PrintMessage('\nInterrupted, stopping...', eol='') |
- _StopTracing(adb, package_info) |
+ _PrintMessage('\nInterrupted...', eol='') |
+ finally: |
+ _StopTracing(controllers) |
_PrintMessage('done') |
- # Wait a bit for the browser to finish writing the trace file. |
- time.sleep(interval / 4 + 1) |
+ _DownloadTraces(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,16 +254,8 @@ 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 = optparse.OptionGroup(parser, 'Timed tracing') |
auto_options.add_option('-t', '--time', help='Profile for N seconds and ' |
'download the resulting trace.', metavar='N', |
type='float') |
@@ -163,15 +264,21 @@ def main(): |
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('-o', '--output', help='Save profile output to file.') |
parser.add_option('--continuous', help='Using the trace buffer as a ring ' |
'buffer, continuously profile until stopped.', |
action='store_true') |
@@ -186,26 +293,41 @@ 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: |
bulach
2013/10/04 14:30:11
how does this play with --continuous? should we re
|
+ _PrintMessage('Trace time interval 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.continuous)) |
+ if systrace_categories: |
+ controllers.append(SystraceController(adb.GetDevice(), systrace_categories)) |
+ |
+ if not controllers: |
+ _PrintMessage('No trace categories enabled.') |
+ return 1 |
+ |
+ _CaptureAndDownloadTimedTrace(controllers, options.time, options.output, |
+ options.compress) |
if __name__ == '__main__': |