Commit ad57a6c1 authored by skyostil@chromium.org's avatar skyostil@chromium.org

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: svn://svn.chromium.org/chrome/trunk/src@272946 0039d316-1c4b-4281-b951-d872f2087c98
parent 31df59b1
This diff is collapsed.
# 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.
# 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
# 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)
# 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
# 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)
#!/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())
# 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)
# 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)
#!/bin/sh
cd $(dirname $0)/..
exec python -m unittest discover chrome_profiler '*_unittest.py'
# 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:]
# 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)
# 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
# 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())
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment