profile_android_startup.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. #!/usr/bin/env vpython3
  2. # Copyright (c) 2015 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Utility library for running a startup profile on an Android device.
  6. Sets up a device for cygprofile, disables sandboxing permissions, and sets up
  7. support for web page replay, device forwarding, and fake certificate authority
  8. to make runs repeatable.
  9. """
  10. import argparse
  11. import logging
  12. import os
  13. import shutil
  14. import subprocess
  15. import sys
  16. import time
  17. _SRC_PATH = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)
  18. sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'catapult', 'devil'))
  19. from devil.android import apk_helper
  20. from devil.android import device_errors
  21. from devil.android import device_utils
  22. from devil.android import flag_changer
  23. from devil.android import forwarder
  24. from devil.android.sdk import intent
  25. sys.path.append(os.path.join(_SRC_PATH, 'build', 'android'))
  26. import devil_chromium
  27. from pylib import constants
  28. sys.path.append(os.path.join(_SRC_PATH, 'tools', 'perf'))
  29. from core import path_util
  30. sys.path.append(path_util.GetTelemetryDir())
  31. from telemetry.internal.util import webpagereplay_go_server
  32. from telemetry.internal.util import binary_manager
  33. class NoProfileDataError(Exception):
  34. """An error used to indicate that no profile data was collected."""
  35. def __init__(self, value):
  36. super().__init__()
  37. self.value = value
  38. def __str__(self):
  39. return repr(self.value)
  40. def _DownloadFromCloudStorage(bucket, sha1_file_name):
  41. """Download the given file based on a hash file."""
  42. cmd = ['download_from_google_storage', '--no_resume',
  43. '--bucket', bucket, '-s', sha1_file_name]
  44. print('Executing command ' + ' '.join(cmd))
  45. process = subprocess.Popen(cmd)
  46. process.wait()
  47. if process.returncode != 0:
  48. raise Exception('Exception executing command %s' % ' '.join(cmd))
  49. def _SimulateSwipe(device, x1, y1, x2, y2):
  50. """Simulates a swipe on a device from (x1, y1) to (x2, y2).
  51. Coordinates are in (device dependent) pixels, and the origin is at the upper
  52. left corner.
  53. The simulated swipe will take 300ms.
  54. Args:
  55. device: (device_utils.DeviceUtils) device to run the command on.
  56. x1, y1, x2, y2: (int) Coordinates.
  57. """
  58. args = [str(x) for x in (x1, y1, x2, y2)]
  59. device.RunShellCommand(['input', 'swipe'] + args)
  60. class WprManager:
  61. """A utility to download a WPR archive, host it, and forward device ports to
  62. it.
  63. """
  64. _WPR_BUCKET = 'chrome-partner-telemetry'
  65. def __init__(self, wpr_archive, device, cmdline_file, package):
  66. self._device = device
  67. self._wpr_archive = wpr_archive
  68. self._wpr_archive_hash = wpr_archive + '.sha1'
  69. self._cmdline_file = cmdline_file
  70. self._wpr_server = None
  71. self._host_http_port = None
  72. self._host_https_port = None
  73. self._flag_changer = None
  74. self._package = package
  75. def Start(self):
  76. """Set up the device and host for WPR."""
  77. self.Stop()
  78. self._BringUpWpr()
  79. self._StartForwarder()
  80. def Stop(self):
  81. """Clean up the device and host's WPR setup."""
  82. self._StopForwarder()
  83. self._StopWpr()
  84. def __enter__(self):
  85. self.Start()
  86. def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
  87. self.Stop()
  88. def _BringUpWpr(self):
  89. """Start the WPR server on the host and the forwarder on the device."""
  90. print('Starting WPR on host...')
  91. _DownloadFromCloudStorage(self._WPR_BUCKET, self._wpr_archive_hash)
  92. if binary_manager.NeedsInit():
  93. binary_manager.InitDependencyManager([])
  94. self._wpr_server = webpagereplay_go_server.ReplayServer(
  95. self._wpr_archive, '127.0.0.1', 0, 0, replay_options=[])
  96. ports = self._wpr_server.StartServer()
  97. self._host_http_port = ports['http']
  98. self._host_https_port = ports['https']
  99. def _StopWpr(self):
  100. """ Stop the WPR and forwarder."""
  101. print('Stopping WPR on host...')
  102. if self._wpr_server:
  103. self._wpr_server.StopServer()
  104. self._wpr_server = None
  105. def _StartForwarder(self):
  106. """Sets up forwarding of device ports to the host, and configures chrome
  107. to use those ports.
  108. """
  109. if not self._wpr_server:
  110. logging.warning('No host WPR server to forward to.')
  111. return
  112. print('Starting device forwarder...')
  113. forwarder.Forwarder.Map([(0, self._host_http_port),
  114. (0, self._host_https_port)],
  115. self._device)
  116. device_http = forwarder.Forwarder.DevicePortForHostPort(
  117. self._host_http_port)
  118. device_https = forwarder.Forwarder.DevicePortForHostPort(
  119. self._host_https_port)
  120. self._flag_changer = flag_changer.FlagChanger(
  121. self._device, self._cmdline_file)
  122. self._flag_changer.AddFlags([
  123. '--host-resolver-rules=MAP * 127.0.0.1,EXCLUDE localhost',
  124. '--testing-fixed-http-port=%s' % device_http,
  125. '--testing-fixed-https-port=%s' % device_https,
  126. # Allows to selectively avoid certificate errors in Chrome. Unlike
  127. # --ignore-certificate-errors this allows exercising the HTTP disk cache
  128. # and avoids re-establishing socket connections. The value is taken from
  129. # the WprGo documentation at:
  130. # https://github.com/catapult-project/catapult/blob/master/web_page_replay_go/README.md
  131. '--ignore-certificate-errors-spki-list=' +
  132. 'PhrPvGIaAMmd29hj8BCZOq096yj7uMpRNHpn5PDxI6I=',
  133. # The flag --ignore-certificate-errors-spki-list (above) requires
  134. # specifying the profile directory, otherwise it is silently ignored.
  135. '--user-data-dir=/data/data/{}'.format(self._package)])
  136. def _StopForwarder(self):
  137. """Shuts down the port forwarding service."""
  138. if self._flag_changer:
  139. print('Restoring flags while stopping forwarder, but why?...')
  140. self._flag_changer.Restore()
  141. self._flag_changer = None
  142. print('Stopping device forwarder...')
  143. forwarder.Forwarder.UnmapAllDevicePorts(self._device)
  144. class AndroidProfileTool:
  145. """A utility for generating orderfile profile data for chrome on android.
  146. Runs cygprofile_unittest found in output_directory, does profiling runs,
  147. and pulls the data to the local machine in output_directory/profile_data.
  148. """
  149. _DEVICE_PROFILE_DIR = '/data/local/tmp/chrome/orderfile'
  150. # Old profile data directories that used to be used. These are cleaned up in
  151. # order to keep devices tidy.
  152. _LEGACY_PROFILE_DIRS = ['/data/local/tmp/chrome/cyglog']
  153. TEST_URL = 'http://en.m.wikipedia.org/wiki/Science'
  154. _WPR_ARCHIVE = os.path.join(
  155. os.path.dirname(__file__), 'memory_top_10_mobile_000.wprgo')
  156. def __init__(self, output_directory, host_profile_dir, use_wpr, urls,
  157. simulate_user, device, debug=False):
  158. """Constructor.
  159. Args:
  160. output_directory: (str) Chrome build directory.
  161. host_profile_dir: (str) Where to store the profiles on the host.
  162. use_wpr: (bool) Whether to use Web Page Replay.
  163. urls: (str) URLs to load. Have to be contained in the WPR archive if
  164. use_wpr is True.
  165. simulate_user: (bool) Whether to simulate a user.
  166. device: (DeviceUtils) Android device selected to be used to
  167. generate orderfile.
  168. debug: (bool) Use simpler, non-representative debugging profile.
  169. """
  170. assert device, 'Expected a valid device'
  171. self._device = device
  172. self._cygprofile_tests = os.path.join(
  173. output_directory, 'cygprofile_unittests')
  174. self._host_profile_dir = host_profile_dir
  175. self._use_wpr = use_wpr
  176. self._urls = urls
  177. self._simulate_user = simulate_user
  178. self._debug = debug
  179. self._SetUpDevice()
  180. self._pregenerated_profiles = None
  181. def SetPregeneratedProfiles(self, files):
  182. """Set pregenerated profiles.
  183. The pregenerated files will be returned as profile data instead of running
  184. an actual profiling step.
  185. Args:
  186. files: ([str]) List of pregenerated files.
  187. """
  188. logging.info('Using pregenerated profiles')
  189. self._pregenerated_profiles = files
  190. def RunCygprofileTests(self):
  191. """Run the cygprofile unit tests suite on the device.
  192. Returns:
  193. The exit code for the tests.
  194. """
  195. device_path = '/data/local/tmp/cygprofile_unittests'
  196. self._device.PushChangedFiles([(self._cygprofile_tests, device_path)])
  197. try:
  198. self._device.RunShellCommand(device_path, check_return=True)
  199. except (device_errors.CommandFailedError,
  200. device_errors.DeviceUnreachableError):
  201. # TODO(jbudorick): Let the exception propagate up once clients can
  202. # handle it.
  203. logging.exception('Failure while running cygprofile_unittests:')
  204. return 1
  205. return 0
  206. def CollectProfile(self, apk, package_info):
  207. """Run a profile and collect the log files.
  208. Args:
  209. apk: The location of the chrome apk to profile.
  210. package_info: A PackageInfo structure describing the chrome apk,
  211. as from pylib/constants.
  212. Returns:
  213. A list of cygprofile data files.
  214. Raises:
  215. NoProfileDataError: No data was found on the device.
  216. """
  217. if self._pregenerated_profiles:
  218. logging.info('Using pregenerated profiles instead of running profile')
  219. logging.info('Profile files:\n%s', '\n'.join(self._pregenerated_profiles))
  220. return self._pregenerated_profiles
  221. self._device.adb.Logcat(clear=True)
  222. self._Install(apk)
  223. try:
  224. changer = self._SetChromeFlags(package_info)
  225. self._SetUpDeviceFolders()
  226. if self._use_wpr:
  227. with WprManager(self._WPR_ARCHIVE, self._device,
  228. package_info.cmdline_file, package_info.package):
  229. self._RunProfileCollection(package_info, self._simulate_user)
  230. else:
  231. self._RunProfileCollection(package_info, self._simulate_user)
  232. except device_errors.CommandFailedError as exc:
  233. logging.error('Exception %s; dumping logcat', exc)
  234. for logcat_line in self._device.adb.Logcat(dump=True):
  235. logging.error(logcat_line)
  236. raise
  237. finally:
  238. self._RestoreChromeFlags(changer)
  239. data = self._PullProfileData()
  240. self._DeleteDeviceData()
  241. return data
  242. def CollectSystemHealthProfile(self, apk):
  243. """Run the orderfile system health benchmarks and collect log files.
  244. Args:
  245. apk: The location of the chrome apk file to profile.
  246. Returns:
  247. A list of cygprofile data files.
  248. Raises:
  249. NoProfileDataError: No data was found on the device.
  250. """
  251. if self._pregenerated_profiles:
  252. logging.info('Using pregenerated profiles instead of running '
  253. 'system health profile')
  254. logging.info('Profile files: %s', '\n'.join(self._pregenerated_profiles))
  255. return self._pregenerated_profiles
  256. logging.info('Running system health profile')
  257. profile_benchmark = 'orderfile_generation.training'
  258. if self._debug:
  259. logging.info('Using reduced debugging profile')
  260. profile_benchmark = 'orderfile_generation.debugging'
  261. self._SetUpDeviceFolders()
  262. self._RunCommand(['tools/perf/run_benchmark',
  263. '--device={}'.format(self._device.serial),
  264. '--browser=exact',
  265. '--browser-executable={}'.format(apk),
  266. profile_benchmark])
  267. data = self._PullProfileData()
  268. self._DeleteDeviceData()
  269. return data
  270. @classmethod
  271. def _RunCommand(cls, command):
  272. """Run a command from current build directory root.
  273. Args:
  274. command: A list of command strings.
  275. Returns:
  276. The process's return code.
  277. """
  278. root = constants.DIR_SOURCE_ROOT
  279. print('Executing {} in {}'.format(' '.join(command), root))
  280. process = subprocess.Popen(command, cwd=root, env=os.environ)
  281. process.wait()
  282. return process.returncode
  283. def _RunProfileCollection(self, package_info, simulate_user):
  284. """Runs the profile collection tasks.
  285. If |simulate_user| is True, then try to simulate a real user, with swiping.
  286. Also do a first load of the page instead of about:blank, in order to
  287. exercise the cache. This is not desirable with a page that only contains
  288. cachable resources, as in this instance the network code will not be called.
  289. Args:
  290. package_info: Which Chrome package to use.
  291. simulate_user: (bool) Whether to try to simulate a user interacting with
  292. the browser.
  293. """
  294. initial_url = self._urls[0] if simulate_user else 'about:blank'
  295. # Start up chrome once with a page, just to get the one-off
  296. # activities out of the way such as apk resource extraction and profile
  297. # creation.
  298. self._StartChrome(package_info, initial_url)
  299. time.sleep(15)
  300. self._KillChrome(package_info)
  301. self._SetUpDeviceFolders()
  302. for url in self._urls:
  303. self._StartChrome(package_info, url)
  304. time.sleep(15)
  305. if simulate_user:
  306. # Down, down, up, up.
  307. _SimulateSwipe(self._device, 200, 700, 200, 300)
  308. _SimulateSwipe(self._device, 200, 700, 200, 300)
  309. _SimulateSwipe(self._device, 200, 700, 200, 1000)
  310. _SimulateSwipe(self._device, 200, 700, 200, 1000)
  311. time.sleep(30)
  312. self._AssertRunning(package_info)
  313. self._KillChrome(package_info)
  314. def Cleanup(self):
  315. """Delete all local and device files left over from profiling. """
  316. self._DeleteDeviceData()
  317. self._DeleteHostData()
  318. def _Install(self, apk):
  319. """Installs Chrome.apk on the device.
  320. Args:
  321. apk: The location of the chrome apk to profile.
  322. """
  323. print('Installing apk...')
  324. self._device.Install(apk)
  325. def _SetUpDevice(self):
  326. """When profiling, files are output to the disk by every process. This
  327. means running without sandboxing enabled.
  328. """
  329. # We need to have adb root in order to pull profile data
  330. try:
  331. print('Enabling root...')
  332. self._device.EnableRoot()
  333. # SELinux need to be in permissive mode, otherwise the process cannot
  334. # write the log files.
  335. print('Putting SELinux in permissive mode...')
  336. self._device.RunShellCommand(['setenforce', '0'], check_return=True)
  337. except device_errors.CommandFailedError as e:
  338. # TODO(jbudorick) Handle this exception appropriately once interface
  339. # conversions are finished.
  340. logging.error(str(e))
  341. def _SetChromeFlags(self, package_info):
  342. print('Setting Chrome flags...')
  343. changer = flag_changer.FlagChanger(
  344. self._device, package_info.cmdline_file)
  345. changer.AddFlags(['--no-sandbox', '--disable-fre'])
  346. return changer
  347. def _RestoreChromeFlags(self, changer):
  348. print('Restoring Chrome flags...')
  349. if changer:
  350. changer.Restore()
  351. def _SetUpDeviceFolders(self):
  352. """Creates folders on the device to store profile data."""
  353. print('Setting up device folders...')
  354. self._DeleteDeviceData()
  355. self._device.RunShellCommand(['mkdir', '-p', self._DEVICE_PROFILE_DIR],
  356. check_return=True)
  357. def _DeleteDeviceData(self):
  358. """Clears out profile storage locations on the device. """
  359. for profile_dir in [self._DEVICE_PROFILE_DIR] + self._LEGACY_PROFILE_DIRS:
  360. self._device.RunShellCommand(
  361. ['rm', '-rf', str(profile_dir)],
  362. check_return=True)
  363. def _StartChrome(self, package_info, url):
  364. print('Launching chrome...')
  365. self._device.StartActivity(
  366. intent.Intent(package=package_info.package,
  367. activity=package_info.activity,
  368. data=url,
  369. extras={'create_new_tab': True}),
  370. blocking=True, force_stop=True)
  371. def _AssertRunning(self, package_info):
  372. assert self._device.GetApplicationPids(package_info.package), (
  373. 'Expected at least one pid associated with {} but found none'.format(
  374. package_info.package))
  375. def _KillChrome(self, package_info):
  376. self._device.ForceStop(package_info.package)
  377. def _DeleteHostData(self):
  378. """Clears out profile storage locations on the host."""
  379. shutil.rmtree(self._host_profile_dir, ignore_errors=True)
  380. def _SetUpHostFolders(self):
  381. self._DeleteHostData()
  382. os.mkdir(self._host_profile_dir)
  383. def _PullProfileData(self):
  384. """Pulls the profile data off of the device.
  385. Returns:
  386. A list of profile data files which were pulled.
  387. Raises:
  388. NoProfileDataError: No data was found on the device.
  389. """
  390. print('Pulling profile data...')
  391. self._SetUpHostFolders()
  392. self._device.PullFile(self._DEVICE_PROFILE_DIR, self._host_profile_dir,
  393. timeout=300)
  394. # Temporary workaround/investigation: if (for unknown reason) 'adb pull' of
  395. # the directory 'orderfile' '.../Release/profile_data' produces
  396. # '...profile_data/orderfile/files' instead of the usual
  397. # '...profile_data/files', list the files deeper in the tree.
  398. files = []
  399. redundant_dir_root = os.path.basename(self._DEVICE_PROFILE_DIR)
  400. for root_file in os.listdir(self._host_profile_dir):
  401. if root_file == redundant_dir_root:
  402. profile_dir = os.path.join(self._host_profile_dir, root_file)
  403. files.extend(os.path.join(profile_dir, f)
  404. for f in os.listdir(profile_dir))
  405. else:
  406. files.append(os.path.join(self._host_profile_dir, root_file))
  407. if len(files) == 0:
  408. raise NoProfileDataError('No profile data was collected')
  409. return files
  410. def AddProfileCollectionArguments(parser):
  411. """Adds the profiling collection arguments to |parser|."""
  412. parser.add_argument(
  413. '--no-wpr', action='store_true', help='Don\'t use WPR.')
  414. parser.add_argument('--urls', type=str, help='URLs to load.',
  415. default=[AndroidProfileTool.TEST_URL],
  416. nargs='+')
  417. parser.add_argument(
  418. '--simulate-user', action='store_true', help='More realistic collection.')
  419. def CreateArgumentParser():
  420. """Creates and return the argument parser."""
  421. parser = argparse.ArgumentParser()
  422. parser.add_argument(
  423. '--adb-path', type=os.path.realpath,
  424. help='adb binary')
  425. parser.add_argument(
  426. '--apk-path', type=os.path.realpath, required=True,
  427. help='APK to profile')
  428. parser.add_argument(
  429. '--output-directory', type=os.path.realpath, required=True,
  430. help='Chromium output directory (e.g. out/Release)')
  431. parser.add_argument(
  432. '--trace-directory', type=os.path.realpath,
  433. help='Directory in which profile traces will be stored. '
  434. 'Defaults to <output-directory>/profile_data')
  435. AddProfileCollectionArguments(parser)
  436. return parser
  437. def main():
  438. parser = CreateArgumentParser()
  439. args = parser.parse_args()
  440. devil_chromium.Initialize(
  441. output_directory=args.output_directory, adb_path=args.adb_path)
  442. apk = apk_helper.ApkHelper(args.apk_path)
  443. package_info = None
  444. for p in constants.PACKAGE_INFO.items():
  445. if p.package == apk.GetPackageName():
  446. package_info = p
  447. break
  448. else:
  449. raise Exception('Unable to determine package info for %s' % args.apk_path)
  450. trace_directory = args.trace_directory
  451. if not trace_directory:
  452. trace_directory = os.path.join(args.output_directory, 'profile_data')
  453. devices = device_utils.DeviceUtils.HealthyDevices()
  454. assert devices, 'Expected at least one connected device'
  455. profiler = AndroidProfileTool(
  456. args.output_directory, host_profile_dir=trace_directory,
  457. use_wpr=not args.no_wpr, urls=args.urls, simulate_user=args.simulate_user,
  458. device=devices[0])
  459. profiler.CollectProfile(args.apk_path, package_info)
  460. return 0
  461. if __name__ == '__main__':
  462. sys.exit(main())