android: Reimplement adb_profile_chrome in Python

adb_profile_chrome is used to capture and download traces from Chrome on
Android. To prepare for future feature improvements and reduce the
maintenance burden, rewrite this bash script in Python.

This reimplementation keeps all the features and command line options of
the original and adds these new ones:

  --trace-cc          Enable extra trace categories for compositor frame
                      viewer data.
  --trace-gpu         Enable extra trace categories for GPU data.
  -z, --compress      Compress the resulting trace with gzip.

BUG=299822,294883

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

git-svn-id: http://src.chromium.org/svn/trunk/src/build@226259 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
This commit is contained in:
skyostil@chromium.org 2013-10-01 17:45:20 +00:00
Родитель 9fa86cb4e9
Коммит a33e294a3c
4 изменённых файлов: 255 добавлений и 170 удалений

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

@ -5,169 +5,4 @@
# found in the LICENSE file.
#
# Start / stop profiling in chrome.
# The profiling data is saved to directory /sdcard/Download. The files
# are named beginning chrome-profile-results-
#
# Assumes you have sourced the android build environment script
# (e.g. 'adb' is on your path).
set -e
usage() {
echo "adb_profile_chrome [--start [-o file] [-c C]|--stop|-d|-t N] [-v N]"
echo "See http://dev.chromium.org/developers/how-tos/trace-event-profiling-tool for detailed instructions on profiling."
echo ""
echo " --start Start profiling."
echo " --output|-o file Save profile output to file. "
echo " (Default is /sdcard/Download/chrome-profile-results-*)"
echo " --categories|-c C Select categories to trace with comma-delimited wildcards."
echo " e.g. '*', 'cat1*,-cat1a'. Default is '*'."
echo " --continuous Using the trace buffer as a ring buffer, continuously"
echo " profile until stopped."
echo " --stop Stop profiling."
echo " --download|-d Download latest trace."
echo " --time|-t N Profile for N seconds and download the resulting trace."
echo " --version|v N Select among installed browsers."
echo " One of stable (default), beta, dev, build"
echo ""
echo "Profiling data is saved to the device."
exit 0
}
send_intent() {
local PACKAGE=$1
local INTENT=$2
shift
shift
adb shell am broadcast -a $PACKAGE.$INTENT $*
}
download_latest_trace() {
(adb logcat -d | grep -q "Logging performance trace to file") || {
echo "WARNING: Trace start marker not found. Is the correct version of Chrome running?"
}
local ITERATION=0
while true; do
# 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. This is done by
# first looking for the last instance of the first message and then checking
# for the second message in the remaining part of the log.
TRACE_FILE=$(adb logcat -d | \
tac | \
grep --max-count=1 --before-context=100000 "Logging performance trace to file" | \
tac | \
grep "Profiler finished[.] Results are in " | \
perl -pi -e "s{.*/storage/emulated/.+/([^\r]+)[.].*}{/sdcard/Download/\\1}g")
if [ -n "$TRACE_FILE" ]; then
break
fi
if [ $ITERATION -eq 0 ]; then
echo -n "Waiting for Chrome to finish tracing..."
else
echo -n "."
fi
let ITERATION=ITERATION+1
if [ $ITERATION -eq 60 ]; then
echo "Timed out"
exit 1
fi
sleep 1
done
adb pull $TRACE_FILE 2> /dev/null
LOCAL_TRACE_FILE=$(basename $TRACE_FILE)
if [ ! -f "$LOCAL_TRACE_FILE" ]; then
echo "Unable to download trace file"
exit 1
fi
}
do_timed_capture() {
local PACKAGE=$1
local INTERVAL=$2
shift
shift
echo -n "Capturing trace..."
send_intent ${PACKAGE} "GPU_PROFILER_START" $* > /dev/null
sleep ${INTERVAL}
send_intent ${PACKAGE} "GPU_PROFILER_STOP" > /dev/null
echo "done"
echo -n "Downloading trace..."
sleep $[${INTERVAL} / 4 + 1]
download_latest_trace
echo "done"
echo "Trace written to ${PWD}/${LOCAL_TRACE_FILE}"
}
PACKAGE=${DEFAULT_PACKAGE:-com.android.chrome}
while test -n "$1"; do
case "$1" in
-v|--version)
if [[ -z "$2" ]] ; then
usage
fi
shift
case "$1" in
stable) PACKAGE="com.android.chrome" ;;
beta) PACKAGE="com.chrome.beta" ;;
dev) PACKAGE="com.google.android.apps.chrome_dev" ;;
build) PACKAGE="com.google.android.apps.chrome" ;;
*) usage ;;
esac
;;
--start) FUNCTION="GPU_PROFILER_START" ;;
--stop) FUNCTION="GPU_PROFILER_STOP" ;;
-o|--output)
if [ -z "$2" ] ; then
usage
fi
OUTPUT="-e file '$2'"
shift
;;
-c|--categories)
if [ -z "$2" ]; then
usage
fi
CATEGORIES="-e categories '$2'"
shift
;;
--continuous) CONTINUOUS="-e continuous ." ;;
-t|--time)
shift
if [ -z "$1" ] ; then
usage
fi
INTERVAL="$1"
;;
-d|--download)
shift
download_latest_trace
echo "Trace written to ${PWD}/${LOCAL_TRACE_FILE}"
;;
*) usage ;;
esac
shift
done
if [ -z "${INTERVAL}" ] ; then
if [ -z "${FUNCTION}" ] ; then
usage
else
send_intent ${PACKAGE} ${FUNCTION} ${OUTPUT} ${CATEGORIES} ${CONTINUOUS}
fi
else
do_timed_capture ${PACKAGE} ${INTERVAL} ${CATEGORIES} ${CONTINUOUS}
fi
exit 0
exec $(dirname $0)/adb_profile_chrome.py $@

212
android/adb_profile_chrome.py Executable file
Просмотреть файл

@ -0,0 +1,212 @@
#!/usr/bin/env python
#
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import gzip
import logging
import optparse
import os
import pexpect
import re
import sys
import time
from pylib import android_commands
from pylib import constants
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 _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 _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 (.*)[.]')
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 _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 _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 _PrintMessage(heading, eol='\n'):
sys.stdout.write(heading + eol)
sys.stdout.flush()
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)
def _CaptureAndDownloadTimedTrace(adb, package_info, categories, interval,
continuous, compress):
adb.StartMonitoringLogcat(clear=False)
adb.SyncLogCat()
try:
_PrintMessage('Capturing %d-second trace. Press Ctrl-C to stop early...' \
% interval, eol='')
_StartTracing(adb, package_info, categories, continuous)
time.sleep(interval)
except KeyboardInterrupt:
_PrintMessage('\nInterrupted, stopping...', eol='')
_StopTracing(adb, package_info)
_PrintMessage('done')
# Wait a bit for the browser to finish writing the trace file.
time.sleep(interval / 4 + 1)
_DownloadLatestTrace(adb, compress, check_for_multiple_traces=False)
def _ComputeCategories(options):
categories = [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)
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.')
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 '
'download the resulting trace.', metavar='N',
type='float')
parser.add_option_group(auto_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='*')
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')
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 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)
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 __name__ == '__main__':
sys.exit(main())

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

@ -724,6 +724,17 @@ class AndroidCommands(object):
start_line = m.group(0)
return GetLogTimestamp(start_line, self.GetDeviceYear())
def BroadcastIntent(self, package, intent, *args):
"""Send a broadcast intent.
Args:
package: Name of package containing the intent.
intent: Name of the intent.
args: Optional extra arguments for the intent.
"""
cmd = 'am broadcast -a %s.%s %s' % (package, intent, ' '.join(args))
self.RunShellCommand(cmd)
def GoHome(self):
"""Tell the device to return to the home screen. Blocks until completion."""
self.RunShellCommand('am start -W '
@ -1144,19 +1155,31 @@ class AndroidCommands(object):
if logfile:
logfile = NewLineNormalizer(logfile)
# Spawn logcat and syncronize with it.
# Spawn logcat and synchronize with it.
for _ in range(4):
self._logcat = pexpect.spawn(constants.ADB_PATH, args, timeout=10,
logfile=logfile)
self.RunShellCommand('log startup_sync')
if self._logcat.expect(['startup_sync', pexpect.EOF,
pexpect.TIMEOUT]) == 0:
if not clear or self.SyncLogCat():
break
self._logcat.close(force=True)
else:
logging.critical('Error reading from logcat: ' + str(self._logcat.match))
sys.exit(1)
def SyncLogCat(self):
"""Synchronize with logcat.
Synchronize with the monitored logcat so that WaitForLogMatch will only
consider new message that are received after this point in time.
Returns:
True if the synchronization succeeded.
"""
assert self._logcat
tag = 'logcat_sync_%s' % time.time()
self.RunShellCommand('log ' + tag)
return self._logcat.expect([tag, pexpect.EOF, pexpect.TIMEOUT]) == 0
def GetMonitoredLogCat(self):
"""Returns an "adb logcat" command as created by pexpected.spawn."""
if not self._logcat:
@ -1510,6 +1533,15 @@ class AndroidCommands(object):
os.makedirs(host_dir)
device_file = '%s/screenshot.png' % self.GetExternalStorage()
self.RunShellCommand('/system/bin/screencap -p %s' % device_file)
self.PullFileFromDevice(device_file, host_file)
def PullFileFromDevice(self, device_file, host_file):
"""Download |device_file| on the device from to |host_file| on the host.
Args:
device_file: Absolute path to the file to retrieve from the device.
host_file: Absolute path to the file to store on the host.
"""
assert self._adb.Pull(device_file, host_file)
assert os.path.exists(host_file)

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

@ -43,6 +43,12 @@ PACKAGE_INFO = {
'/data/local/chrome-command-line',
'chrome_devtools_remote',
None),
'chrome_dev': PackageInfo(
'com.google.android.apps.chrome_dev',
'com.google.android.apps.chrome.Main',
'/data/local/chrome-command-line',
'chrome_devtools_remote',
None),
'legacy_browser': PackageInfo(
'com.google.android.browser',
'com.android.browser.BrowserActivity',