Commit a1b64f89 authored by Benoit Lize's avatar Benoit Lize Committed by Commit Bot

tools/cygprofile: Add custom URLs and scrolling to profile_android_startup.

Allows to collect profiling data from several page loads, and to scroll
the screen while doing so.

Bug: 758566
Change-Id: I4f0bb8fc167e69dea349dbd4b5ee733193996e41
Reviewed-on: https://chromium-review.googlesource.com/801016
Commit-Queue: Benoit L <lizeb@chromium.org>
Reviewed-by: default avatarEgor Pasko <pasko@chromium.org>
Reviewed-by: default avatarMatthew Cary <mattcary@chromium.org>
Cr-Commit-Position: refs/heads/master@{#522392}
parent 956ce4c3
...@@ -84,16 +84,19 @@ def PlotResidency(data, output_filename): ...@@ -84,16 +84,19 @@ def PlotResidency(data, output_filename):
data: (dict) As returned by ParseDump(). data: (dict) As returned by ParseDump().
output_filename: (str) Output filename. output_filename: (str) Output filename.
""" """
residency = data['residency']
max_percentage = max((100. * sum(d)) / len(d) for d in residency.values())
logging.info('Max residency = %.2f%%', max_percentage)
start = data['start'] start = data['start']
end = data['end'] end = data['end']
data = data['residency']
fig, ax = plt.subplots(figsize=(20, 10)) fig, ax = plt.subplots(figsize=(20, 10))
timestamps = sorted(data.keys()) timestamps = sorted(residency.keys())
x_max = len(data.values()[0]) * 4096 x_max = len(residency.values()[0]) * 4096
for t in timestamps: for t in timestamps:
offset_ms = (t - timestamps[0]) / 1e6 offset_ms = (t - timestamps[0]) / 1e6
incore = [i * 4096 for (i, x) in enumerate(data[t]) if x] incore = [i * 4096 for (i, x) in enumerate(residency[t]) if x]
outcore = [i * 4096 for (i, x) in enumerate(data[t]) if not x] outcore = [i * 4096 for (i, x) in enumerate(residency[t]) if not x]
percentage = 100. * len(incore) / (len(incore) + len(outcore)) percentage = 100. * len(incore) / (len(incore) + len(outcore))
plt.text(x_max, offset_ms, '%.1f%%' % percentage) plt.text(x_max, offset_ms, '%.1f%%' % percentage)
for (d, color) in ((incore, (.2, .6, .05, 1)), (outcore, (1, 0, 0, 1))): for (d, color) in ((incore, (.2, .6, .05, 1)), (outcore, (1, 0, 0, 1))):
......
...@@ -14,10 +14,10 @@ Example usage: ...@@ -14,10 +14,10 @@ Example usage:
--target-arch=arm --target-arch=arm
""" """
import argparse
import hashlib import hashlib
import json import json
import logging import logging
import optparse
import os import os
import re import re
import shutil import shutil
...@@ -460,8 +460,22 @@ class OrderfileGenerator(object): ...@@ -460,8 +460,22 @@ class OrderfileGenerator(object):
self._BUILD_ROOT, self._options.arch + '_uninstrumented_out') self._BUILD_ROOT, self._options.arch + '_uninstrumented_out')
if options.profile: if options.profile:
output_directory = os.path.join(self._instrumented_out_dir, 'Release')
host_cyglog_dir = os.path.join(output_directory, 'cyglog_data')
# Only override the defaults when using lightweight instrumentation,
# as the regular profiling code is likely too slow for these.
urls = [profile_android_startup.AndroidProfileTool.TEST_URL]
use_wpr = True
simulate_user = False
if options.simulate_user and not options.lightweight_instrumentation:
logging.error(
'--simulate-user required --lightweight-instrumentation, ignoring.')
if options.lightweight_instrumentation:
urls = options.urls
use_wpr = not options.no_wpr
simulate_user = options.simulate_user
self._profiler = profile_android_startup.AndroidProfileTool( self._profiler = profile_android_startup.AndroidProfileTool(
os.path.join(self._instrumented_out_dir, 'Release')) output_directory, host_cyglog_dir, use_wpr, urls, simulate_user)
self._output_data = {} self._output_data = {}
self._step_recorder = StepRecorder(options.buildbot) self._step_recorder = StepRecorder(options.buildbot)
...@@ -742,47 +756,49 @@ class OrderfileGenerator(object): ...@@ -742,47 +756,49 @@ class OrderfileGenerator(object):
return self._output_data return self._output_data
def CreateOptionParser(): def CreateArgumentParser():
parser = optparse.OptionParser() """Creates and returns the argument parser."""
parser.add_option( parser = argparse.ArgumentParser()
parser.add_argument(
'--lightweight-instrumentation', action='store_true', default=False, '--lightweight-instrumentation', action='store_true', default=False,
help='Use the lightweight instrumentation path') help='Use the lightweight instrumentation path')
parser.add_option( parser.add_argument(
'--buildbot', action='store_true', '--buildbot', action='store_true',
help='If true, the script expects to be run on a buildbot') help='If true, the script expects to be run on a buildbot')
parser.add_option( parser.add_argument(
'--verify', action='store_true', '--verify', action='store_true',
help='If true, the script only verifies the current orderfile') help='If true, the script only verifies the current orderfile')
parser.add_option('--target-arch', action='store', dest='arch', parser.add_argument('--target-arch', action='store', dest='arch',
default=cygprofile_utils.DetectArchitecture(), default=cygprofile_utils.DetectArchitecture(),
choices=['arm', 'arm64', 'x86', 'x86_64', 'x64', 'mips'], choices=['arm', 'arm64', 'x86', 'x86_64', 'x64', 'mips'],
help='The target architecture for which to build') help='The target architecture for which to build')
parser.add_option('--output-json', action='store', dest='json_file', parser.add_argument('--output-json', action='store', dest='json_file',
help='Location to save stats in json format') help='Location to save stats in json format')
parser.add_option( parser.add_argument(
'--skip-profile', action='store_false', dest='profile', default=True, '--skip-profile', action='store_false', dest='profile', default=True,
help='Don\'t generate a profile on the device. Only patch from the ' help='Don\'t generate a profile on the device. Only patch from the '
'existing profile.') 'existing profile.')
parser.add_option( parser.add_argument(
'--skip-patch', action='store_false', dest='patch', default=True, '--skip-patch', action='store_false', dest='patch', default=True,
help='Only generate the raw (unpatched) orderfile, don\'t patch it.') help='Only generate the raw (unpatched) orderfile, don\'t patch it.')
parser.add_option( parser.add_argument(
'--netrc', action='store', '--netrc', action='store',
help='A custom .netrc file to use for git checkin. Only used on bots.') help='A custom .netrc file to use for git checkin. Only used on bots.')
parser.add_option( parser.add_argument(
'--branch', action='store', default='master', '--branch', action='store', default='master',
help='When running on buildbot with a netrc, the branch orderfile ' help='When running on buildbot with a netrc, the branch orderfile '
'hashes get checked into.') 'hashes get checked into.')
# Note: -j50 was causing issues on the bot. # Note: -j50 was causing issues on the bot.
parser.add_option( parser.add_argument(
'-j', '--jobs', action='store', default=20, '-j', '--jobs', action='store', default=20,
help='Number of jobs to use for compilation.') help='Number of jobs to use for compilation.')
parser.add_option( parser.add_argument(
'-l', '--max-load', action='store', default=4, help='Max cpu load.') '-l', '--max-load', action='store', default=4, help='Max cpu load.')
parser.add_option('--goma-dir', help='GOMA directory.') parser.add_argument('--goma-dir', help='GOMA directory.')
parser.add_option( parser.add_argument(
'--use-goma', action='store_true', help='Enable GOMA.', default=False) '--use-goma', action='store_true', help='Enable GOMA.', default=False)
parser.add_option('--adb-path', help='Path to the adb binary.') parser.add_argument('--adb-path', help='Path to the adb binary.')
profile_android_startup.AddProfileCollectionArguments(parser)
return parser return parser
...@@ -815,11 +831,11 @@ def CreateOrderfile(options, orderfile_updater_class): ...@@ -815,11 +831,11 @@ def CreateOrderfile(options, orderfile_updater_class):
return False return False
def main(argv): def main():
parser = CreateOptionParser() parser = CreateArgumentParser()
options, _ = parser.parse_args(argv) options = parser.parse_args()
return 0 if CreateOrderfile(options, OrderfileUpdater) else 1 return 0 if CreateOrderfile(options, OrderfileUpdater) else 1
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(main(sys.argv)) sys.exit(main())
...@@ -16,7 +16,6 @@ import os ...@@ -16,7 +16,6 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile
import time import time
_SRC_PATH = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) _SRC_PATH = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
...@@ -61,6 +60,21 @@ def _DownloadFromCloudStorage(bucket, sha1_file_name): ...@@ -61,6 +60,21 @@ def _DownloadFromCloudStorage(bucket, sha1_file_name):
raise Exception('Exception executing command %s' % ' '.join(cmd)) raise Exception('Exception executing command %s' % ' '.join(cmd))
def _SimulateSwipe(device, x1, y1, x2, y2):
"""Simulates a swipe on a device from (x1, y1) to (x2, y2).
Coordinates are in (device dependent) pixels, and the origin is at the upper
left corner.
The simulated swipe will take 300ms.
Args:
device: (device_utils.DeviceUtils) device to run the command on.
x1, y1, x2, y2: (int) Coordinates.
"""
args = [str(x) for x in (x1, y1, x2, y2)]
device.RunShellCommand(['input', 'swipe'] + args)
class WprManager(object): class WprManager(object):
"""A utility to download a WPR archive, host it, and forward device ports to """A utility to download a WPR archive, host it, and forward device ports to
it. it.
...@@ -109,7 +123,7 @@ class WprManager(object): ...@@ -109,7 +123,7 @@ class WprManager(object):
self._host_https_port = ports['https'] self._host_https_port = ports['https']
def _StopWpr(self): def _StopWpr(self):
""" Stop the WPR and forwarder. """ """ Stop the WPR and forwarder."""
print 'Stopping WPR on host...' print 'Stopping WPR on host...'
if self._wpr_server: if self._wpr_server:
self._wpr_server.StopServer() self._wpr_server.StopServer()
...@@ -168,28 +182,35 @@ class AndroidProfileTool(object): ...@@ -168,28 +182,35 @@ class AndroidProfileTool(object):
_DEVICE_CYGLOG_DIR = '/data/local/tmp/chrome/cyglog' _DEVICE_CYGLOG_DIR = '/data/local/tmp/chrome/cyglog'
# TEST_URL must be a url in the WPR_ARCHIVE. TEST_URL = 'https://www.google.com/#hl=en&q=science'
_TEST_URL = 'https://www.google.com/#hl=en&q=science'
_WPR_ARCHIVE = os.path.join( _WPR_ARCHIVE = os.path.join(
os.path.dirname(__file__), 'memory_top_10_mobile_000.wprgo') os.path.dirname(__file__), 'memory_top_10_mobile_000.wprgo')
# TODO(jbudorick): Make host_cyglog_dir mandatory after updating def __init__(self, output_directory, host_cyglog_dir, use_wpr, urls,
# downstream clients. See crbug.com/639831 for context. simulate_user):
def __init__(self, output_directory, host_cyglog_dir=None): """Constructor.
Args:
output_directory: (str) Chrome build directory.
host_cyglog_dir: (str) Where to store the profiles.
use_wpr: (bool) Whether to use Web Page Replay.
urls: (str) URLs to load. Have to be contained in the WPR archive if
use_wpr is True.
simulate_user: (bool) Whether to simulate a user.
"""
devices = device_utils.DeviceUtils.HealthyDevices() devices = device_utils.DeviceUtils.HealthyDevices()
self._device = devices[0] self._device = devices[0]
self._cygprofile_tests = os.path.join( self._cygprofile_tests = os.path.join(
output_directory, 'cygprofile_unittests') output_directory, 'cygprofile_unittests')
self._host_cyglog_dir = host_cyglog_dir or os.path.join( self._host_cyglog_dir = host_cyglog_dir
output_directory, 'cyglog_data') self._use_wpr = use_wpr
self._urls = urls
self._simulate_user = simulate_user
self._SetUpDevice() self._SetUpDevice()
def RunCygprofileTests(self): def RunCygprofileTests(self):
"""Run the cygprofile unit tests suite on the device. """Run the cygprofile unit tests suite on the device.
Args:
path_to_tests: The location on the host machine with the compiled
cygprofile test binary.
Returns: Returns:
The exit code for the tests. The exit code for the tests.
""" """
...@@ -223,18 +244,12 @@ class AndroidProfileTool(object): ...@@ -223,18 +244,12 @@ class AndroidProfileTool(object):
try: try:
changer = self._SetChromeFlags(package_info) changer = self._SetChromeFlags(package_info)
self._SetUpDeviceFolders() self._SetUpDeviceFolders()
# Start up chrome once with a blank page, just to get the one-off if self._use_wpr:
# activities out of the way such as apk resource extraction and profile with WprManager(self._WPR_ARCHIVE, self._device,
# creation. package_info.cmdline_file, package_info.package):
self._StartChrome(package_info, 'about:blank') self._RunProfileCollection(package_info, self._simulate_user)
time.sleep(15) else:
self._KillChrome(package_info) self._RunProfileCollection(package_info, self._simulate_user)
self._SetUpDeviceFolders()
with WprManager(self._WPR_ARCHIVE, self._device,
package_info.cmdline_file, package_info.package):
self._StartChrome(package_info, self._TEST_URL)
time.sleep(90)
self._KillChrome(package_info)
finally: finally:
self._RestoreChromeFlags(changer) self._RestoreChromeFlags(changer)
...@@ -242,6 +257,39 @@ class AndroidProfileTool(object): ...@@ -242,6 +257,39 @@ class AndroidProfileTool(object):
self._DeleteDeviceData() self._DeleteDeviceData()
return data return data
def _RunProfileCollection(self, package_info, simulate_user):
"""Runs the profile collection tasks.
If |simulate_user| is True, then try to simulate a real user, with swiping.
Also do a first load of the page instead of about:blank, in order to
exercise the cache. This is not desirable with a page that only contains
cachable resources, as in this instance the network code will not be called.
Args:
package_info: Which Chrome package to use.
simulate_user: (bool) Whether to try to simulate a user interacting with
the browser.
"""
initial_url = self._urls[0] if simulate_user else 'about:blank'
# Start up chrome once with a page, just to get the one-off
# activities out of the way such as apk resource extraction and profile
# creation.
self._StartChrome(package_info, initial_url)
time.sleep(15)
self._KillChrome(package_info)
self._SetUpDeviceFolders()
for url in self._urls:
self._StartChrome(package_info, url)
time.sleep(15)
if simulate_user:
# Down, down, up, up.
_SimulateSwipe(self._device, 200, 700, 200, 300)
_SimulateSwipe(self._device, 200, 700, 200, 300)
_SimulateSwipe(self._device, 200, 700, 200, 1000)
_SimulateSwipe(self._device, 200, 700, 200, 1000)
time.sleep(30)
self._KillChrome(package_info)
def Cleanup(self): def Cleanup(self):
"""Delete all local and device files left over from profiling. """ """Delete all local and device files left over from profiling. """
self._DeleteDeviceData() self._DeleteDeviceData()
...@@ -249,10 +297,9 @@ class AndroidProfileTool(object): ...@@ -249,10 +297,9 @@ class AndroidProfileTool(object):
def _Install(self, apk): def _Install(self, apk):
"""Installs Chrome.apk on the device. """Installs Chrome.apk on the device.
Args: Args:
apk: The location of the chrome apk to profile. apk: The location of the chrome apk to profile.
package_info: A PackageInfo structure describing the chrome apk,
as from pylib/constants.
""" """
print 'Installing apk...' print 'Installing apk...'
self._device.Install(apk) self._device.Install(apk)
...@@ -287,7 +334,7 @@ class AndroidProfileTool(object): ...@@ -287,7 +334,7 @@ class AndroidProfileTool(object):
changer.Restore() changer.Restore()
def _SetUpDeviceFolders(self): def _SetUpDeviceFolders(self):
"""Creates folders on the device to store cyglog data. """ """Creates folders on the device to store cyglog data."""
print 'Setting up device folders...' print 'Setting up device folders...'
self._DeleteDeviceData() self._DeleteDeviceData()
self._device.RunShellCommand( self._device.RunShellCommand(
...@@ -349,7 +396,19 @@ class AndroidProfileTool(object): ...@@ -349,7 +396,19 @@ class AndroidProfileTool(object):
return [os.path.join(cyglog_dir, x) for x in files] return [os.path.join(cyglog_dir, x) for x in files]
def main(): def AddProfileCollectionArguments(parser):
"""Adds the profiling collection arguments to |parser|."""
parser.add_argument(
'--no-wpr', action='store_true', help='Don\'t use WPR.')
parser.add_argument('--urls', type=str, help='URLs to load.',
default=[AndroidProfileTool.TEST_URL],
nargs='+')
parser.add_argument(
'--simulate-user', action='store_true', help='More realistic collection.')
def CreateArgumentParser():
"""Creates and return the argument parser."""
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'--adb-path', type=os.path.realpath, '--adb-path', type=os.path.realpath,
...@@ -364,7 +423,12 @@ def main(): ...@@ -364,7 +423,12 @@ def main():
'--trace-directory', type=os.path.realpath, '--trace-directory', type=os.path.realpath,
help='Directory in which cyglog traces will be stored. ' help='Directory in which cyglog traces will be stored. '
'Defaults to <output-directory>/cyglog_data') 'Defaults to <output-directory>/cyglog_data')
AddProfileCollectionArguments(parser)
return parser
def main():
parser = CreateArgumentParser()
args = parser.parse_args() args = parser.parse_args()
devil_chromium.Initialize( devil_chromium.Initialize(
...@@ -380,7 +444,8 @@ def main(): ...@@ -380,7 +444,8 @@ def main():
raise Exception('Unable to determine package info for %s' % args.apk_path) raise Exception('Unable to determine package info for %s' % args.apk_path)
profiler = AndroidProfileTool( profiler = AndroidProfileTool(
args.output_directory, host_cyglog_dir=args.trace_directory) args.output_directory, host_cyglog_dir=args.trace_directory,
use_wpr=not args.no_wpr, urls=args.urls, simulate_user=args.simulate_user)
profiler.CollectProfile(args.apk_path, package_info) profiler.CollectProfile(args.apk_path, package_info)
return 0 return 0
......
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