adb_profile_chrome: Refactor into multiple modules and add tests

The adb_profile_chrome tool has grown quite a bit since its inception,
so now seems to be a good time to split it into smaller modules and add
some rudimentary tests.

BUG=375754
TEST=build/android/chrome_profiler/run_tests
NOTRY=true

Review URL: https://codereview.chromium.org/290013006

git-svn-id: http://src.chromium.org/svn/trunk/src/build@272946 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
This commit is contained in:
skyostil@chromium.org 2014-05-27 11:26:27 +00:00
Родитель 71545b826c
Коммит 00187128e2
14 изменённых файлов: 766 добавлений и 464 удалений

Просмотреть файл

@ -4,472 +4,10 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import gzip
import json
import logging
import optparse
import os
import re
import select
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
from pylib.device import device_utils
_TRACE_VIEWER_ROOT = os.path.join(constants.DIR_SOURCE_ROOT,
'third_party', 'trace-viewer')
sys.path.append(_TRACE_VIEWER_ROOT)
from trace_viewer.build import trace2html # pylint: disable=F0401
_DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
def _GetTraceTimestamp():
return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
class ChromeTracingController(object):
def __init__(self, device, package_info, categories, ring_buffer):
self._device = device
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._device.old_interface.StartMonitoringLogcat(clear=False)
def __str__(self):
return 'chrome trace'
@staticmethod
def GetCategories(device, package_info):
device.old_interface.BroadcastIntent(
package_info.package, 'GPU_PROFILER_LIST_CATEGORIES')
try:
json_category_list = device.old_interface.WaitForLogMatch(
re.compile(r'{"traceCategoriesList(.*)'), None, timeout=5).group(0)
except pexpect.TIMEOUT:
raise RuntimeError('Performance trace category list marker not found. '
'Is the correct version of the browser running?')
record_categories = []
disabled_by_default_categories = []
json_data = json.loads(json_category_list)['traceCategoriesList']
for item in json_data:
if item.startswith('disabled-by-default'):
disabled_by_default_categories.append(item)
else:
record_categories.append(item)
return record_categories, disabled_by_default_categories
def StartTracing(self, interval):
self._trace_interval = interval
self._device.old_interface.SyncLogCat()
self._device.old_interface.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._device.old_interface.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?')
def StopTracing(self):
self._device.old_interface.BroadcastIntent(
self._package_info.package,
'GPU_PROFILER_STOP')
self._trace_file = self._device.old_interface.WaitForLogMatch(
self._trace_finish_re, None, timeout=120).group(1)
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._device.old_interface.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, device, categories, ring_buffer):
self._device = device
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(device):
return device.old_interface.RunShellCommand('atrace --list_categories')
def StartTracing(self, _):
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):
# TODO(jbudorick) can this be made work with DeviceUtils?
# We use a separate interface to adb because the one from AndroidCommands
# isn't re-entrant.
device_param = (['-s', self._device.old_interface.GetDevice()]
if self._device.old_interface.GetDevice() else [])
cmd = ['adb'] + device_param + ['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 _PackageTracesAsHtml(trace_files, html_file):
with open(html_file, 'w') as f:
trace2html.WriteHTMLForTracesToFile(trace_files, f)
for trace_file in trace_files:
os.unlink(trace_file)
def _PrintMessage(heading, eol='\n'):
sys.stdout.write('%s%s' % (heading, eol))
sys.stdout.flush()
def _WaitForEnter(timeout):
select.select([sys.stdin], [], [], timeout)
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_json):
_PrintMessage('Downloading...', eol='')
trace_files = []
for controller in controllers:
trace_files.append(controller.PullTrace())
if not write_json:
html_file = os.path.splitext(trace_files[0])[0] + '.html'
_PackageTracesAsHtml(trace_files, html_file)
trace_files = [html_file]
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]
_PrintMessage('done')
_PrintMessage('Trace written to file://%s' % os.path.abspath(result))
return result
def _CaptureAndPullTrace(controllers, interval, output, compress, write_json):
trace_type = ' + '.join(map(str, controllers))
try:
_StartTracing(controllers, interval)
if interval:
_PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
(interval, trace_type), eol='')
_WaitForEnter(interval)
else:
_PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='')
raw_input()
finally:
_StopTracing(controllers)
if interval:
_PrintMessage('done')
return _PullTraces(controllers, output, compress, write_json)
def _ComputeChromeCategories(options):
categories = []
if options.trace_frame_viewer:
categories.append('disabled-by-default-cc.debug')
if options.trace_ubercompositor:
categories.append('disabled-by-default-cc.debug*')
if options.trace_gpu:
categories.append('disabled-by-default-gpu.debug*')
if options.trace_flow:
categories.append('disabled-by-default-toplevel.flow')
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=\'\'". Use "list" to see '
'the available 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='Deprecated, use --trace-frame-viewer.',
action='store_true')
categories.add_option('--trace-frame-viewer',
help='Enable enough trace categories for '
'compositor frame viewing.', action='store_true')
categories.add_option('--trace-ubercompositor',
help='Enable enough trace categories for '
'ubercompositor frame data.', action='store_true')
categories.add_option('--trace-gpu', help='Enable extra trace categories for '
'GPU data.', action='store_true')
categories.add_option('--trace-flow', help='Enable extra trace categories '
'for IPC message flows.', 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('--json', help='Save trace as raw JSON instead of '
'HTML.', 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.trace_cc:
parser.parse_error("""--trace-cc is deprecated.
For basic jank busting uses, use --trace-frame-viewer
For detailed study of ubercompositor, pass --trace-ubercompositor.
When in doubt, just try out --trace-frame-viewer.
""")
if options.verbose:
logging.getLogger().setLevel(logging.DEBUG)
devices = android_commands.GetAttachedDevices()
if len(devices) != 1:
parser.error('Exactly 1 device much be attached.')
device = device_utils.DeviceUtils(devices[0])
package_info = _GetSupportedBrowsers()[options.browser]
if options.chrome_categories in ['list', 'help']:
_PrintMessage('Collecting record categories list...', eol='')
record_categories = []
disabled_by_default_categories = []
record_categories, disabled_by_default_categories = \
ChromeTracingController.GetCategories(device, package_info)
_PrintMessage('done')
_PrintMessage('Record Categories:')
_PrintMessage('\n'.join('\t%s' % item \
for item in sorted(record_categories)))
_PrintMessage('\nDisabled by Default Categories:')
_PrintMessage('\n'.join('\t%s' % item \
for item in sorted(disabled_by_default_categories)))
return 0
if options.systrace_categories in ['list', 'help']:
_PrintMessage('\n'.join(SystraceController.GetCategories(device)))
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)
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(device,
package_info,
chrome_categories,
options.ring_buffer))
if systrace_categories:
controllers.append(SystraceController(device,
systrace_categories,
options.ring_buffer))
if not controllers:
_PrintMessage('No trace categories enabled.')
return 1
if options.output:
options.output = os.path.expanduser(options.output)
result = _CaptureAndPullTrace(controllers,
options.time if not options.continuous else 0,
options.output,
options.compress,
options.json)
if options.view:
if sys.platform == 'darwin':
os.system('/usr/bin/open %s' % os.path.abspath(result))
else:
webbrowser.open(result)
from chrome_profiler import main
if __name__ == '__main__':
sys.exit(main())
sys.exit(main.main())

Просмотреть файл

@ -0,0 +1,3 @@
# Copyright 2014 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.

Просмотреть файл

@ -0,0 +1,90 @@
# Copyright 2014 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 json
import os
import re
import time
from chrome_profiler import controllers
from pylib import pexpect
class ChromeTracingController(controllers.BaseController):
def __init__(self, device, package_info, categories, ring_buffer):
controllers.BaseController.__init__(self)
self._device = device
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._device.old_interface.StartMonitoringLogcat(clear=False)
def __repr__(self):
return 'chrome trace'
@staticmethod
def GetCategories(device, package_info):
device.old_interface.BroadcastIntent(
package_info.package, 'GPU_PROFILER_LIST_CATEGORIES')
try:
json_category_list = device.old_interface.WaitForLogMatch(
re.compile(r'{"traceCategoriesList(.*)'), None, timeout=5).group(0)
except pexpect.TIMEOUT:
raise RuntimeError('Performance trace category list marker not found. '
'Is the correct version of the browser running?')
record_categories = []
disabled_by_default_categories = []
json_data = json.loads(json_category_list)['traceCategoriesList']
for item in json_data:
if item.startswith('disabled-by-default'):
disabled_by_default_categories.append(item)
else:
record_categories.append(item)
return record_categories, disabled_by_default_categories
def StartTracing(self, interval):
self._trace_interval = interval
self._device.old_interface.SyncLogCat()
self._device.old_interface.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._device.old_interface.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?')
def StopTracing(self):
self._device.old_interface.BroadcastIntent(
self._package_info.package,
'GPU_PROFILER_STOP')
self._trace_file = self._device.old_interface.WaitForLogMatch(
self._trace_finish_re, None, timeout=120).group(1)
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._device.old_interface.PullFileFromDevice(trace_file, host_file)
return host_file

Просмотреть файл

@ -0,0 +1,46 @@
# Copyright 2014 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 os
import json
from chrome_profiler import chrome_controller
from chrome_profiler import controllers_unittest
class ChromeControllerTest(controllers_unittest.BaseControllerTest):
def testGetCategories(self):
# Not supported on stable yet.
# TODO(skyostil): Remove this once category queries roll into stable.
if self.browser == 'stable':
return
categories = \
chrome_controller.ChromeTracingController.GetCategories(
self.device, self.package_info)
self.assertEquals(len(categories), 2)
self.assertTrue(categories[0])
self.assertTrue(categories[1])
def testTracing(self):
categories = '*'
ring_buffer = False
controller = chrome_controller.ChromeTracingController(self.device,
self.package_info,
categories,
ring_buffer)
interval = 1
try:
controller.StartTracing(interval)
finally:
controller.StopTracing()
result = controller.PullTrace()
try:
with open(result) as f:
json.loads(f.read())
finally:
os.remove(result)

Просмотреть файл

@ -0,0 +1,16 @@
# Copyright 2014 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 exceptions
# pylint: disable=R0201
class BaseController(object):
def StartTracing(self, _):
raise exceptions.NotImplementError
def StopTracing(self):
raise exceptions.NotImplementError
def PullTrace(self):
raise exceptions.NotImplementError

Просмотреть файл

@ -0,0 +1,23 @@
# Copyright 2014 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 unittest
from chrome_profiler import profiler
from pylib import android_commands
from pylib.device import device_utils
class BaseControllerTest(unittest.TestCase):
def setUp(self):
devices = android_commands.GetAttachedDevices()
self.browser = 'stable'
self.package_info = profiler.GetSupportedBrowsers()[self.browser]
self.device = device_utils.DeviceUtils(devices[0])
adb = android_commands.AndroidCommands(devices[0])
adb.StartActivity(self.package_info.package,
self.package_info.activity,
wait_for_completion=True)

211
android/chrome_profiler/main.py Executable file
Просмотреть файл

@ -0,0 +1,211 @@
#!/usr/bin/env python
#
# Copyright 2014 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 logging
import optparse
import os
import sys
import webbrowser
from chrome_profiler import chrome_controller
from chrome_profiler import profiler
from chrome_profiler import systrace_controller
from chrome_profiler import ui
from pylib import android_commands
from pylib.device import device_utils
_DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
def _ComputeChromeCategories(options):
categories = []
if options.trace_frame_viewer:
categories.append('disabled-by-default-cc.debug')
if options.trace_ubercompositor:
categories.append('disabled-by-default-cc.debug*')
if options.trace_gpu:
categories.append('disabled-by-default-gpu.debug*')
if options.trace_flow:
categories.append('disabled-by-default-toplevel.flow')
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 _CreateOptionParser():
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)
chrome_opts = optparse.OptionGroup(parser, 'Chrome tracing options')
chrome_opts.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=\'\'". Use "list" to '
'see the available categories.',
metavar='CHROME_CATEGORIES', dest='chrome_categories',
default=_DEFAULT_CHROME_CATEGORIES)
chrome_opts.add_option('--trace-cc',
help='Deprecated, use --trace-frame-viewer.',
action='store_true')
chrome_opts.add_option('--trace-frame-viewer',
help='Enable enough trace categories for '
'compositor frame viewing.', action='store_true')
chrome_opts.add_option('--trace-ubercompositor',
help='Enable enough trace categories for '
'ubercompositor frame data.', action='store_true')
chrome_opts.add_option('--trace-gpu', help='Enable extra trace categories '
'for GPU data.', action='store_true')
chrome_opts.add_option('--trace-flow', help='Enable extra trace categories '
'for IPC message flows.', action='store_true')
parser.add_option_group(chrome_opts)
systrace_opts = optparse.OptionGroup(parser, 'Systrace tracing options')
systrace_opts.add_option('-s', '--systrace', help='Capture a systrace with '
'the chosen comma-delimited systrace categories. You '
'can also capture a combined Chrome + systrace by '
'enable both types of categories. Use "list" to see '
'the available categories. Systrace is disabled by '
'default.', metavar='SYS_CATEGORIES',
dest='systrace_categories', default='')
parser.add_option_group(systrace_opts)
output_options = optparse.OptionGroup(parser, 'Output options')
output_options.add_option('-o', '--output', help='Save trace output to file.')
output_options.add_option('--json', help='Save trace as raw JSON instead of '
'HTML.', 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(profiler.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')
return parser
def main():
parser = _CreateOptionParser()
options, _args = parser.parse_args()
if options.trace_cc:
parser.parse_error("""--trace-cc is deprecated.
For basic jank busting uses, use --trace-frame-viewer
For detailed study of ubercompositor, pass --trace-ubercompositor.
When in doubt, just try out --trace-frame-viewer.
""")
if options.verbose:
logging.getLogger().setLevel(logging.DEBUG)
devices = android_commands.GetAttachedDevices()
if len(devices) != 1:
parser.error('Exactly 1 device must be attached.')
device = device_utils.DeviceUtils(devices[0])
package_info = profiler.GetSupportedBrowsers()[options.browser]
if options.chrome_categories in ['list', 'help']:
ui.PrintMessage('Collecting record categories list...', eol='')
record_categories = []
disabled_by_default_categories = []
record_categories, disabled_by_default_categories = \
chrome_controller.ChromeTracingController.GetCategories(
device, package_info)
ui.PrintMessage('done')
ui.PrintMessage('Record Categories:')
ui.PrintMessage('\n'.join('\t%s' % item \
for item in sorted(record_categories)))
ui.PrintMessage('\nDisabled by Default Categories:')
ui.PrintMessage('\n'.join('\t%s' % item \
for item in sorted(disabled_by_default_categories)))
return 0
if options.systrace_categories in ['list', 'help']:
ui.PrintMessage('\n'.join(
systrace_controller.SystraceController.GetCategories(device)))
return 0
if not options.time and not options.continuous:
ui.PrintMessage('Time interval or continuous tracing should be specified.')
return 1
chrome_categories = _ComputeChromeCategories(options)
systrace_categories = _ComputeSystraceCategories(options)
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.')
enabled_controllers = []
if chrome_categories:
enabled_controllers.append(
chrome_controller.ChromeTracingController(device,
package_info,
chrome_categories,
options.ring_buffer))
if systrace_categories:
enabled_controllers.append(
systrace_controller.SystraceController(device,
systrace_categories,
options.ring_buffer))
if not enabled_controllers:
ui.PrintMessage('No trace categories enabled.')
return 1
if options.output:
options.output = os.path.expanduser(options.output)
result = profiler.CaptureProfile(
enabled_controllers,
options.time if not options.continuous else 0,
output=options.output,
compress=options.compress,
write_json=options.json)
if options.view:
if sys.platform == 'darwin':
os.system('/usr/bin/open %s' % os.path.abspath(result))
else:
webbrowser.open(result)
if __name__ == '__main__':
sys.exit(main())

Просмотреть файл

@ -0,0 +1,130 @@
# Copyright 2014 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 gzip
import os
import shutil
import sys
import zipfile
from chrome_profiler import ui
from chrome_profiler import util
from pylib import constants
sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT,
'third_party',
'trace-viewer'))
# pylint: disable=F0401
from trace_viewer.build import trace2html
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 _PackageTracesAsHtml(trace_files, html_file):
with open(html_file, 'w') as f:
trace2html.WriteHTMLForTracesToFile(trace_files, f)
for trace_file in trace_files:
os.unlink(trace_file)
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_json):
ui.PrintMessage('Downloading...', eol='')
trace_files = []
for controller in controllers:
trace_files.append(controller.PullTrace())
if not write_json:
html_file = os.path.splitext(trace_files[0])[0] + '.html'
_PackageTracesAsHtml(trace_files, html_file)
trace_files = [html_file]
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' % util.GetTraceTimestamp()
_ArchiveFiles(trace_files, result)
elif output:
result = output
shutil.move(trace_files[0], result)
else:
result = trace_files[0]
ui.PrintMessage('done')
ui.PrintMessage('Trace written to file://%s' % os.path.abspath(result))
return result
def GetSupportedBrowsers():
"""Returns the package names of all supported browsers."""
# 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 CaptureProfile(controllers, interval, output=None, compress=False,
write_json=False):
"""Records a profiling trace saves the result to a file.
Args:
controllers: List of tracing controllers.
interval: Time interval to capture in seconds. An interval of None (or 0)
continues tracing until stopped by the user.
output: Output file name or None to use an automatically generated name.
compress: If True, the result will be compressed either with gzip or zip
depending on the number of captured subtraces.
write_json: If True, prefer JSON output over HTML.
Returns:
Path to saved profile.
"""
trace_type = ' + '.join(map(str, controllers))
try:
_StartTracing(controllers, interval)
if interval:
ui.PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
(interval, trace_type), eol='')
ui.WaitForEnter(interval)
else:
ui.PrintMessage('Capturing %s. Press Enter to stop...' % \
trace_type, eol='')
raw_input()
finally:
_StopTracing(controllers)
if interval:
ui.PrintMessage('done')
return _PullTraces(controllers, output, compress, write_json)

Просмотреть файл

@ -0,0 +1,78 @@
# Copyright 2014 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 os
import tempfile
import unittest
import zipfile
from chrome_profiler import profiler
from chrome_profiler import ui
class FakeController(object):
def __init__(self, contents='fake-contents'):
self.contents = contents
self.interval = None
self.stopped = False
self.filename = None
def StartTracing(self, interval):
self.interval = interval
def StopTracing(self):
self.stopped = True
def PullTrace(self):
with tempfile.NamedTemporaryFile(delete=False) as f:
self.filename = f.name
f.write(self.contents)
return f.name
def __repr__(self):
return 'faketrace'
class ProfilerTest(unittest.TestCase):
def setUp(self):
ui.EnableTestMode()
def testCaptureBasicProfile(self):
controller = FakeController()
interval = 1.5
result = profiler.CaptureProfile([controller], interval)
try:
self.assertEquals(controller.interval, interval)
self.assertTrue(controller.stopped)
self.assertTrue(os.path.exists(result))
self.assertFalse(os.path.exists(controller.filename))
self.assertTrue(result.endswith('.html'))
finally:
os.remove(result)
def testCaptureJsonProfile(self):
controller = FakeController()
result = profiler.CaptureProfile([controller], 1, write_json=True)
try:
self.assertFalse(result.endswith('.html'))
with open(result) as f:
self.assertEquals(f.read(), controller.contents)
finally:
os.remove(result)
def testCaptureMultipleProfiles(self):
controllers = [FakeController('c1'), FakeController('c2')]
result = profiler.CaptureProfile(controllers, 1, write_json=True)
try:
self.assertTrue(result.endswith('.zip'))
self.assertTrue(zipfile.is_zipfile(result))
with zipfile.ZipFile(result) as f:
self.assertEquals(
f.namelist(),
[controllers[0].filename[1:], controllers[1].filename[1:]])
finally:
os.remove(result)

Просмотреть файл

@ -0,0 +1,3 @@
#!/bin/sh
cd $(dirname $0)/..
exec python -m unittest discover chrome_profiler '*_unittest.py'

Просмотреть файл

@ -0,0 +1,95 @@
# Copyright 2014 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 threading
import zlib
from chrome_profiler import controllers
from chrome_profiler import util
from pylib import cmd_helper
_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(controllers.BaseController):
def __init__(self, device, categories, ring_buffer):
controllers.BaseController.__init__(self)
self._device = device
self._categories = categories
self._ring_buffer = ring_buffer
self._done = threading.Event()
self._thread = None
self._trace_data = None
def __repr__(self):
return 'systrace'
@staticmethod
def GetCategories(device):
return device.old_interface.RunShellCommand('atrace --list_categories')
def StartTracing(self, _):
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' % util.GetTraceTimestamp()
with open(output_name, 'w') as out:
out.write(self._trace_data)
return output_name
def _RunATraceCommand(self, command):
# TODO(jbudorick) can this be made work with DeviceUtils?
# We use a separate interface to adb because the one from AndroidCommands
# isn't re-entrant.
device_param = (['-s', self._device.old_interface.GetDevice()]
if self._device.old_interface.GetDevice() else [])
cmd = ['adb'] + device_param + ['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:]

Просмотреть файл

@ -0,0 +1,36 @@
# Copyright 2014 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 os
from chrome_profiler import controllers_unittest
from chrome_profiler import systrace_controller
class SystraceControllerTest(controllers_unittest.BaseControllerTest):
def testGetCategories(self):
categories = \
systrace_controller.SystraceController.GetCategories(self.device)
self.assertTrue(categories)
assert 'gfx' in ' '.join(categories)
def testTracing(self):
categories = ['gfx', 'input', 'view']
ring_buffer = False
controller = systrace_controller.SystraceController(self.device,
categories,
ring_buffer)
interval = 1
try:
controller.StartTracing(interval)
finally:
controller.StopTracing()
result = controller.PullTrace()
try:
with open(result) as f:
self.assertTrue('CPU#' in f.read())
finally:
os.remove(result)

Просмотреть файл

@ -0,0 +1,25 @@
# Copyright 2014 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 select
import sys
def PrintMessage(heading, eol='\n'):
sys.stdout.write('%s%s' % (heading, eol))
sys.stdout.flush()
def WaitForEnter(timeout):
select.select([sys.stdin], [], [], timeout)
def EnableTestMode():
def NoOp(*_, **__):
pass
# pylint: disable=W0601
global PrintMessage
global WaitForEnter
PrintMessage = NoOp
WaitForEnter = NoOp

Просмотреть файл

@ -0,0 +1,8 @@
# Copyright 2014 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 time
def GetTraceTimestamp():
return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())