123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- #!/usr/bin/env vpython3
- # Copyright (c) 2015 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.
- """Utility library for running a startup profile on an Android device.
- Sets up a device for cygprofile, disables sandboxing permissions, and sets up
- support for web page replay, device forwarding, and fake certificate authority
- to make runs repeatable.
- """
- import argparse
- import logging
- import os
- import shutil
- import subprocess
- import sys
- import time
- _SRC_PATH = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
- sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'catapult', 'devil'))
- from devil.android import apk_helper
- from devil.android import device_errors
- from devil.android import device_utils
- from devil.android import flag_changer
- from devil.android import forwarder
- from devil.android.sdk import intent
- sys.path.append(os.path.join(_SRC_PATH, 'build', 'android'))
- import devil_chromium
- from pylib import constants
- sys.path.append(os.path.join(_SRC_PATH, 'tools', 'perf'))
- from core import path_util
- sys.path.append(path_util.GetTelemetryDir())
- from telemetry.internal.util import webpagereplay_go_server
- from telemetry.internal.util import binary_manager
- class NoProfileDataError(Exception):
- """An error used to indicate that no profile data was collected."""
- def __init__(self, value):
- super().__init__()
- self.value = value
- def __str__(self):
- return repr(self.value)
- def _DownloadFromCloudStorage(bucket, sha1_file_name):
- """Download the given file based on a hash file."""
- cmd = ['download_from_google_storage', '--no_resume',
- '--bucket', bucket, '-s', sha1_file_name]
- print('Executing command ' + ' '.join(cmd))
- process = subprocess.Popen(cmd)
- process.wait()
- if process.returncode != 0:
- 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:
- """A utility to download a WPR archive, host it, and forward device ports to
- it.
- """
- _WPR_BUCKET = 'chrome-partner-telemetry'
- def __init__(self, wpr_archive, device, cmdline_file, package):
- self._device = device
- self._wpr_archive = wpr_archive
- self._wpr_archive_hash = wpr_archive + '.sha1'
- self._cmdline_file = cmdline_file
- self._wpr_server = None
- self._host_http_port = None
- self._host_https_port = None
- self._flag_changer = None
- self._package = package
- def Start(self):
- """Set up the device and host for WPR."""
- self.Stop()
- self._BringUpWpr()
- self._StartForwarder()
- def Stop(self):
- """Clean up the device and host's WPR setup."""
- self._StopForwarder()
- self._StopWpr()
- def __enter__(self):
- self.Start()
- def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
- self.Stop()
- def _BringUpWpr(self):
- """Start the WPR server on the host and the forwarder on the device."""
- print('Starting WPR on host...')
- _DownloadFromCloudStorage(self._WPR_BUCKET, self._wpr_archive_hash)
- if binary_manager.NeedsInit():
- binary_manager.InitDependencyManager([])
- self._wpr_server = webpagereplay_go_server.ReplayServer(
- self._wpr_archive, '127.0.0.1', 0, 0, replay_options=[])
- ports = self._wpr_server.StartServer()
- self._host_http_port = ports['http']
- self._host_https_port = ports['https']
- def _StopWpr(self):
- """ Stop the WPR and forwarder."""
- print('Stopping WPR on host...')
- if self._wpr_server:
- self._wpr_server.StopServer()
- self._wpr_server = None
- def _StartForwarder(self):
- """Sets up forwarding of device ports to the host, and configures chrome
- to use those ports.
- """
- if not self._wpr_server:
- logging.warning('No host WPR server to forward to.')
- return
- print('Starting device forwarder...')
- forwarder.Forwarder.Map([(0, self._host_http_port),
- (0, self._host_https_port)],
- self._device)
- device_http = forwarder.Forwarder.DevicePortForHostPort(
- self._host_http_port)
- device_https = forwarder.Forwarder.DevicePortForHostPort(
- self._host_https_port)
- self._flag_changer = flag_changer.FlagChanger(
- self._device, self._cmdline_file)
- self._flag_changer.AddFlags([
- '--host-resolver-rules=MAP * 127.0.0.1,EXCLUDE localhost',
- '--testing-fixed-http-port=%s' % device_http,
- '--testing-fixed-https-port=%s' % device_https,
- # Allows to selectively avoid certificate errors in Chrome. Unlike
- # --ignore-certificate-errors this allows exercising the HTTP disk cache
- # and avoids re-establishing socket connections. The value is taken from
- # the WprGo documentation at:
- # https://github.com/catapult-project/catapult/blob/master/web_page_replay_go/README.md
- '--ignore-certificate-errors-spki-list=' +
- 'PhrPvGIaAMmd29hj8BCZOq096yj7uMpRNHpn5PDxI6I=',
- # The flag --ignore-certificate-errors-spki-list (above) requires
- # specifying the profile directory, otherwise it is silently ignored.
- '--user-data-dir=/data/data/{}'.format(self._package)])
- def _StopForwarder(self):
- """Shuts down the port forwarding service."""
- if self._flag_changer:
- print('Restoring flags while stopping forwarder, but why?...')
- self._flag_changer.Restore()
- self._flag_changer = None
- print('Stopping device forwarder...')
- forwarder.Forwarder.UnmapAllDevicePorts(self._device)
- class AndroidProfileTool:
- """A utility for generating orderfile profile data for chrome on android.
- Runs cygprofile_unittest found in output_directory, does profiling runs,
- and pulls the data to the local machine in output_directory/profile_data.
- """
- _DEVICE_PROFILE_DIR = '/data/local/tmp/chrome/orderfile'
- # Old profile data directories that used to be used. These are cleaned up in
- # order to keep devices tidy.
- _LEGACY_PROFILE_DIRS = ['/data/local/tmp/chrome/cyglog']
- TEST_URL = 'http://en.m.wikipedia.org/wiki/Science'
- _WPR_ARCHIVE = os.path.join(
- os.path.dirname(__file__), 'memory_top_10_mobile_000.wprgo')
- def __init__(self, output_directory, host_profile_dir, use_wpr, urls,
- simulate_user, device, debug=False):
- """Constructor.
- Args:
- output_directory: (str) Chrome build directory.
- host_profile_dir: (str) Where to store the profiles on the host.
- 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.
- device: (DeviceUtils) Android device selected to be used to
- generate orderfile.
- debug: (bool) Use simpler, non-representative debugging profile.
- """
- assert device, 'Expected a valid device'
- self._device = device
- self._cygprofile_tests = os.path.join(
- output_directory, 'cygprofile_unittests')
- self._host_profile_dir = host_profile_dir
- self._use_wpr = use_wpr
- self._urls = urls
- self._simulate_user = simulate_user
- self._debug = debug
- self._SetUpDevice()
- self._pregenerated_profiles = None
- def SetPregeneratedProfiles(self, files):
- """Set pregenerated profiles.
- The pregenerated files will be returned as profile data instead of running
- an actual profiling step.
- Args:
- files: ([str]) List of pregenerated files.
- """
- logging.info('Using pregenerated profiles')
- self._pregenerated_profiles = files
- def RunCygprofileTests(self):
- """Run the cygprofile unit tests suite on the device.
- Returns:
- The exit code for the tests.
- """
- device_path = '/data/local/tmp/cygprofile_unittests'
- self._device.PushChangedFiles([(self._cygprofile_tests, device_path)])
- try:
- self._device.RunShellCommand(device_path, check_return=True)
- except (device_errors.CommandFailedError,
- device_errors.DeviceUnreachableError):
- # TODO(jbudorick): Let the exception propagate up once clients can
- # handle it.
- logging.exception('Failure while running cygprofile_unittests:')
- return 1
- return 0
- def CollectProfile(self, apk, package_info):
- """Run a profile and collect the log files.
- Args:
- apk: The location of the chrome apk to profile.
- package_info: A PackageInfo structure describing the chrome apk,
- as from pylib/constants.
- Returns:
- A list of cygprofile data files.
- Raises:
- NoProfileDataError: No data was found on the device.
- """
- if self._pregenerated_profiles:
- logging.info('Using pregenerated profiles instead of running profile')
- logging.info('Profile files:\n%s', '\n'.join(self._pregenerated_profiles))
- return self._pregenerated_profiles
- self._device.adb.Logcat(clear=True)
- self._Install(apk)
- try:
- changer = self._SetChromeFlags(package_info)
- self._SetUpDeviceFolders()
- if self._use_wpr:
- with WprManager(self._WPR_ARCHIVE, self._device,
- package_info.cmdline_file, package_info.package):
- self._RunProfileCollection(package_info, self._simulate_user)
- else:
- self._RunProfileCollection(package_info, self._simulate_user)
- except device_errors.CommandFailedError as exc:
- logging.error('Exception %s; dumping logcat', exc)
- for logcat_line in self._device.adb.Logcat(dump=True):
- logging.error(logcat_line)
- raise
- finally:
- self._RestoreChromeFlags(changer)
- data = self._PullProfileData()
- self._DeleteDeviceData()
- return data
- def CollectSystemHealthProfile(self, apk):
- """Run the orderfile system health benchmarks and collect log files.
- Args:
- apk: The location of the chrome apk file to profile.
- Returns:
- A list of cygprofile data files.
- Raises:
- NoProfileDataError: No data was found on the device.
- """
- if self._pregenerated_profiles:
- logging.info('Using pregenerated profiles instead of running '
- 'system health profile')
- logging.info('Profile files: %s', '\n'.join(self._pregenerated_profiles))
- return self._pregenerated_profiles
- logging.info('Running system health profile')
- profile_benchmark = 'orderfile_generation.training'
- if self._debug:
- logging.info('Using reduced debugging profile')
- profile_benchmark = 'orderfile_generation.debugging'
- self._SetUpDeviceFolders()
- self._RunCommand(['tools/perf/run_benchmark',
- '--device={}'.format(self._device.serial),
- '--browser=exact',
- '--browser-executable={}'.format(apk),
- profile_benchmark])
- data = self._PullProfileData()
- self._DeleteDeviceData()
- return data
- @classmethod
- def _RunCommand(cls, command):
- """Run a command from current build directory root.
- Args:
- command: A list of command strings.
- Returns:
- The process's return code.
- """
- root = constants.DIR_SOURCE_ROOT
- print('Executing {} in {}'.format(' '.join(command), root))
- process = subprocess.Popen(command, cwd=root, env=os.environ)
- process.wait()
- return process.returncode
- 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._AssertRunning(package_info)
- self._KillChrome(package_info)
- def Cleanup(self):
- """Delete all local and device files left over from profiling. """
- self._DeleteDeviceData()
- self._DeleteHostData()
- def _Install(self, apk):
- """Installs Chrome.apk on the device.
- Args:
- apk: The location of the chrome apk to profile.
- """
- print('Installing apk...')
- self._device.Install(apk)
- def _SetUpDevice(self):
- """When profiling, files are output to the disk by every process. This
- means running without sandboxing enabled.
- """
- # We need to have adb root in order to pull profile data
- try:
- print('Enabling root...')
- self._device.EnableRoot()
- # SELinux need to be in permissive mode, otherwise the process cannot
- # write the log files.
- print('Putting SELinux in permissive mode...')
- self._device.RunShellCommand(['setenforce', '0'], check_return=True)
- except device_errors.CommandFailedError as e:
- # TODO(jbudorick) Handle this exception appropriately once interface
- # conversions are finished.
- logging.error(str(e))
- def _SetChromeFlags(self, package_info):
- print('Setting Chrome flags...')
- changer = flag_changer.FlagChanger(
- self._device, package_info.cmdline_file)
- changer.AddFlags(['--no-sandbox', '--disable-fre'])
- return changer
- def _RestoreChromeFlags(self, changer):
- print('Restoring Chrome flags...')
- if changer:
- changer.Restore()
- def _SetUpDeviceFolders(self):
- """Creates folders on the device to store profile data."""
- print('Setting up device folders...')
- self._DeleteDeviceData()
- self._device.RunShellCommand(['mkdir', '-p', self._DEVICE_PROFILE_DIR],
- check_return=True)
- def _DeleteDeviceData(self):
- """Clears out profile storage locations on the device. """
- for profile_dir in [self._DEVICE_PROFILE_DIR] + self._LEGACY_PROFILE_DIRS:
- self._device.RunShellCommand(
- ['rm', '-rf', str(profile_dir)],
- check_return=True)
- def _StartChrome(self, package_info, url):
- print('Launching chrome...')
- self._device.StartActivity(
- intent.Intent(package=package_info.package,
- activity=package_info.activity,
- data=url,
- extras={'create_new_tab': True}),
- blocking=True, force_stop=True)
- def _AssertRunning(self, package_info):
- assert self._device.GetApplicationPids(package_info.package), (
- 'Expected at least one pid associated with {} but found none'.format(
- package_info.package))
- def _KillChrome(self, package_info):
- self._device.ForceStop(package_info.package)
- def _DeleteHostData(self):
- """Clears out profile storage locations on the host."""
- shutil.rmtree(self._host_profile_dir, ignore_errors=True)
- def _SetUpHostFolders(self):
- self._DeleteHostData()
- os.mkdir(self._host_profile_dir)
- def _PullProfileData(self):
- """Pulls the profile data off of the device.
- Returns:
- A list of profile data files which were pulled.
- Raises:
- NoProfileDataError: No data was found on the device.
- """
- print('Pulling profile data...')
- self._SetUpHostFolders()
- self._device.PullFile(self._DEVICE_PROFILE_DIR, self._host_profile_dir,
- timeout=300)
- # Temporary workaround/investigation: if (for unknown reason) 'adb pull' of
- # the directory 'orderfile' '.../Release/profile_data' produces
- # '...profile_data/orderfile/files' instead of the usual
- # '...profile_data/files', list the files deeper in the tree.
- files = []
- redundant_dir_root = os.path.basename(self._DEVICE_PROFILE_DIR)
- for root_file in os.listdir(self._host_profile_dir):
- if root_file == redundant_dir_root:
- profile_dir = os.path.join(self._host_profile_dir, root_file)
- files.extend(os.path.join(profile_dir, f)
- for f in os.listdir(profile_dir))
- else:
- files.append(os.path.join(self._host_profile_dir, root_file))
- if len(files) == 0:
- raise NoProfileDataError('No profile data was collected')
- return files
- 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.add_argument(
- '--adb-path', type=os.path.realpath,
- help='adb binary')
- parser.add_argument(
- '--apk-path', type=os.path.realpath, required=True,
- help='APK to profile')
- parser.add_argument(
- '--output-directory', type=os.path.realpath, required=True,
- help='Chromium output directory (e.g. out/Release)')
- parser.add_argument(
- '--trace-directory', type=os.path.realpath,
- help='Directory in which profile traces will be stored. '
- 'Defaults to <output-directory>/profile_data')
- AddProfileCollectionArguments(parser)
- return parser
- def main():
- parser = CreateArgumentParser()
- args = parser.parse_args()
- devil_chromium.Initialize(
- output_directory=args.output_directory, adb_path=args.adb_path)
- apk = apk_helper.ApkHelper(args.apk_path)
- package_info = None
- for p in constants.PACKAGE_INFO.items():
- if p.package == apk.GetPackageName():
- package_info = p
- break
- else:
- raise Exception('Unable to determine package info for %s' % args.apk_path)
- trace_directory = args.trace_directory
- if not trace_directory:
- trace_directory = os.path.join(args.output_directory, 'profile_data')
- devices = device_utils.DeviceUtils.HealthyDevices()
- assert devices, 'Expected at least one connected device'
- profiler = AndroidProfileTool(
- args.output_directory, host_profile_dir=trace_directory,
- use_wpr=not args.no_wpr, urls=args.urls, simulate_user=args.simulate_user,
- device=devices[0])
- profiler.CollectProfile(args.apk_path, package_info)
- return 0
- if __name__ == '__main__':
- sys.exit(main())
|