Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / tools / cygprofile / profile_android_startup.py
blob9e49bef0a899887f7e5ab1413366999d472c8e0a
1 # Copyright (c) 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Utility library for running a startup profile on an Android device.
7 Sets up a device for cygprofile, disables sandboxing permissions, and sets up
8 support for web page replay, device forwarding, and fake certificate authority
9 to make runs repeatable.
10 """
12 import logging
13 import os
14 import shutil
15 import subprocess
16 import sys
17 import tempfile
18 import time
20 sys.path.append(os.path.join(sys.path[0], '..', '..', 'build', 'android'))
21 from pylib import constants
22 from pylib import flag_changer
23 from pylib import forwarder
24 from pylib.device import device_errors
25 from pylib.device import device_utils
26 from pylib.device import intent
28 sys.path.append(os.path.join(sys.path[0], '..', '..', 'tools', 'telemetry'))
29 from telemetry.internal.util import webpagereplay
31 sys.path.append(os.path.join(sys.path[0], '..', '..',
32 'third_party', 'webpagereplay'))
33 import adb_install_cert
34 import certutils
37 class NoCyglogDataError(Exception):
38 """An error used to indicate that no cyglog data was collected."""
40 def __init__(self, value):
41 super(NoCyglogDataError, self).__init__()
42 self.value = value
44 def __str__(self):
45 return repr(self.value)
48 def _DownloadFromCloudStorage(bucket, sha1_file_name):
49 """Download the given file based on a hash file."""
50 cmd = ['download_from_google_storage', '--no_resume',
51 '--bucket', bucket, '-s', sha1_file_name]
52 print 'Executing command ' + ' '.join(cmd)
53 process = subprocess.Popen(cmd)
54 process.wait()
55 if process.returncode != 0:
56 raise Exception('Exception executing command %s' % ' '.join(cmd))
59 class WprManager(object):
60 """A utility to download a WPR archive, host it, and forward device ports to
61 it.
62 """
64 _WPR_BUCKET = 'chrome-partner-telemetry'
66 def __init__(self, wpr_archive, device, cmdline_file):
67 self._device = device
68 self._wpr_archive = wpr_archive
69 self._wpr_archive_hash = wpr_archive + '.sha1'
70 self._cmdline_file = cmdline_file
71 self._wpr_server = None
72 self._wpr_ca_cert_path = None
73 self._device_cert_util = None
74 self._host_http_port = None
75 self._host_https_port = None
76 self._is_test_ca_installed = False
77 self._flag_changer = None
79 def Start(self):
80 """Set up the device and host for WPR."""
81 self.Stop()
82 #TODO(azarchs): make self._InstallTestCa() work
83 self._BringUpWpr()
84 self._StartForwarder()
86 def Stop(self):
87 """Clean up the device and host's WPR setup."""
88 self._StopForwarder()
89 self._StopWpr()
90 #TODO(azarchs): make self._RemoveTestCa() work
92 def __enter__(self):
93 self.Start()
95 def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
96 self.Stop()
98 def _InstallTestCa(self):
99 """Generates and deploys a test certificate authority."""
100 print 'Installing test certificate authority on device: %s' % (
101 self._device.adb.GetDeviceSerial())
102 self._wpr_ca_cert_path = os.path.join(tempfile.mkdtemp(), 'testca.pem')
103 certutils.write_dummy_ca_cert(*certutils.generate_dummy_ca_cert(),
104 cert_path=self._wpr_ca_cert_path)
105 self._device_cert_util = adb_install_cert.AndroidCertInstaller(
106 self._device.adb.GetDeviceSerial(), None, self._wpr_ca_cert_path)
107 self._device_cert_util.install_cert(overwrite_cert=True)
108 self._is_test_ca_installed = True
110 def _RemoveTestCa(self):
111 """Remove root CA generated by previous call to InstallTestCa().
113 Removes the test root certificate from both the device and host machine.
115 print 'Cleaning up test CA...'
116 if not self._wpr_ca_cert_path:
117 return
119 if self._is_test_ca_installed:
120 try:
121 self._device_cert_util.remove_cert()
122 except Exception:
123 # Best effort cleanup - show the error and continue.
124 logging.error(
125 'Error while trying to remove certificate authority: %s. '
126 % self._adb.device_serial())
127 self._is_test_ca_installed = False
129 shutil.rmtree(os.path.dirname(self._wpr_ca_cert_path), ignore_errors=True)
130 self._wpr_ca_cert_path = None
131 self._device_cert_util = None
133 def _BringUpWpr(self):
134 """Start the WPR server on the host and the forwarder on the device."""
135 print 'Starting WPR on host...'
136 _DownloadFromCloudStorage(self._WPR_BUCKET, self._wpr_archive_hash)
137 args = ['--use_closest_match']
138 if self._is_test_ca_installed:
139 args.extend(['--should_generate_certs',
140 '--https_root_ca_cert_path=' + self._wpr_ca_cert_path])
141 wpr_server = webpagereplay.ReplayServer(self._wpr_archive,
142 '127.0.0.1', 0, 0, None, args)
143 ports = wpr_server.StartServer()[:-1]
144 self._wpr_server = wpr_server
145 self._host_http_port = ports[0]
146 self._host_https_port = ports[1]
148 def _StopWpr(self):
149 """ Stop the WPR and forwarder. """
150 print 'Stopping WPR on host...'
151 if self._wpr_server:
152 self._wpr_server.StopServer()
153 self._wpr_server = None
155 def _StartForwarder(self):
156 """Sets up forwarding of device ports to the host, and configures chrome
157 to use those ports.
159 if not self._wpr_server:
160 logging.warning('No host WPR server to forward to.')
161 return
162 print 'Starting device forwarder...'
163 forwarder.Forwarder.Map([(0, self._host_http_port),
164 (0, self._host_https_port)],
165 self._device)
166 device_http = forwarder.Forwarder.DevicePortForHostPort(
167 self._host_http_port)
168 device_https = forwarder.Forwarder.DevicePortForHostPort(
169 self._host_https_port)
170 self._flag_changer = flag_changer.FlagChanger(
171 self._device, self._cmdline_file)
172 self._flag_changer.AddFlags([
173 '--host-resolver-rules="MAP * 127.0.0.1,EXCLUDE localhost"',
174 '--testing-fixed-http-port=%s' % device_http,
175 '--testing-fixed-https-port=%s' % device_https])
177 def _StopForwarder(self):
178 """Shuts down the port forwarding service."""
179 print 'Stopping device forwarder...'
180 if self._flag_changer:
181 self._flag_changer.Restore()
182 self._flag_changer = None
183 forwarder.Forwarder.UnmapAllDevicePorts(self._device)
186 class AndroidProfileTool(object):
187 """A utility for generating cygprofile data for chrome on andorid.
189 Runs cygprofile_unittest found in output_directory, does profiling runs,
190 and pulls the data to the local machine in output_directory/cyglog_data.
193 _DEVICE_CYGLOG_DIR = '/data/local/tmp/chrome/cyglog'
195 # TEST_URL must be a url in the WPR_ARCHIVE.
196 _TEST_URL = 'https://www.google.com/#hl=en&q=science'
197 _WPR_ARCHIVE = os.path.join(
198 constants.DIR_SOURCE_ROOT, 'tools', 'perf', 'page_sets', 'data',
199 'top_10_mobile_002.wpr')
202 def __init__(self, output_directory):
203 devices = device_utils.DeviceUtils.HealthyDevices()
204 self._device = devices[0]
205 self._cygprofile_tests = os.path.join(
206 output_directory, 'cygprofile_unittests')
207 self._host_cyglog_dir = os.path.join(
208 output_directory, 'cyglog_data')
209 self._SetUpDevice()
211 def RunCygprofileTests(self):
212 """Run the cygprofile unit tests suite on the device.
214 Args:
215 path_to_tests: The location on the host machine with the compiled
216 cygprofile test binary.
217 Returns:
218 The exit code for the tests.
220 device_path = '/data/local/tmp/cygprofile_unittests'
221 self._device.old_interface.PushIfNeeded(
222 self._cygprofile_tests, device_path)
223 (exit_code, _) = (
224 self._device.old_interface.GetShellCommandStatusAndOutput(
225 command=device_path, log_result=True))
226 return exit_code
228 def CollectProfile(self, apk, package_info):
229 """Run a profile and collect the log files.
231 Args:
232 apk: The location of the chrome apk to profile.
233 package_info: A PackageInfo structure describing the chrome apk,
234 as from pylib/constants.
235 Returns:
236 A list of cygprofile data files.
237 Raises:
238 NoCyglogDataError: No data was found on the device.
240 self._Install(apk, package_info)
242 try:
243 changer = self._SetChromeFlags(package_info)
244 self._SetUpDeviceFolders()
245 # Start up chrome once with a blank page, just to get the one-off
246 # activities out of the way such as apk resource extraction and profile
247 # creation.
248 self._StartChrome(package_info, 'about:blank')
249 time.sleep(15)
250 self._KillChrome(package_info)
251 self._SetUpDeviceFolders()
252 with WprManager(self._WPR_ARCHIVE, self._device,
253 package_info.cmdline_file):
254 self._StartChrome(package_info, self._TEST_URL)
255 time.sleep(90)
256 self._KillChrome(package_info)
257 finally:
258 self._RestoreChromeFlags(changer)
260 data = self._PullCyglogData()
261 self._DeleteDeviceData()
262 return data
264 def Cleanup(self):
265 """Delete all local and device files left over from profiling. """
266 self._DeleteDeviceData()
267 self._DeleteHostData()
269 def _Install(self, apk, package_info):
270 """Installs Chrome.apk on the device.
271 Args:
272 apk: The location of the chrome apk to profile.
273 package_info: A PackageInfo structure describing the chrome apk,
274 as from pylib/constants.
276 print 'Installing apk...'
277 self._device.old_interface.ManagedInstall(apk, package_info.package)
279 def _SetUpDevice(self):
280 """When profiling, files are output to the disk by every process. This
281 means running without sandboxing enabled.
283 # We need to have adb root in order to pull cyglog data
284 try:
285 print 'Enabling root...'
286 self._device.EnableRoot()
287 # SELinux need to be in permissive mode, otherwise the process cannot
288 # write the log files.
289 print 'Putting SELinux in permissive mode...'
290 self._device.old_interface.RunShellCommand('setenforce 0')
291 except device_errors.CommandFailedError as e:
292 # TODO(jbudorick) Handle this exception appropriately once interface
293 # conversions are finished.
294 logging.error(str(e))
296 def _SetChromeFlags(self, package_info):
297 print 'Setting Chrome flags...'
298 changer = flag_changer.FlagChanger(
299 self._device, package_info.cmdline_file)
300 changer.AddFlags(['--no-sandbox', '--disable-fre'])
301 return changer
303 def _RestoreChromeFlags(self, changer):
304 print 'Restoring Chrome flags...'
305 if changer:
306 changer.Restore()
308 def _SetUpDeviceFolders(self):
309 """Creates folders on the device to store cyglog data. """
310 print 'Setting up device folders...'
311 self._DeleteDeviceData()
312 self._device.old_interface.RunShellCommand(
313 'mkdir -p %s' % self._DEVICE_CYGLOG_DIR)
315 def _DeleteDeviceData(self):
316 """Clears out cyglog storage locations on the device. """
317 self._device.old_interface.RunShellCommand(
318 'rm -rf %s' % self._DEVICE_CYGLOG_DIR)
320 def _StartChrome(self, package_info, url):
321 print 'Launching chrome...'
322 self._device.StartActivity(
323 intent.Intent(package=package_info.package,
324 activity=package_info.activity,
325 data=url,
326 extras={'create_new_tab' : True}),
327 blocking=True, force_stop=True)
329 def _KillChrome(self, package_info):
330 self._device.KillAll(package_info.package)
332 def _DeleteHostData(self):
333 """Clears out cyglog storage locations on the host."""
334 shutil.rmtree(self._host_cyglog_dir, ignore_errors=True)
336 def _SetUpHostFolders(self):
337 self._DeleteHostData()
338 os.mkdir(self._host_cyglog_dir)
340 def _PullCyglogData(self):
341 """Pull the cyglog data off of the device.
343 Returns:
344 A list of cyglog data files which were pulled.
345 Raises:
346 NoCyglogDataError: No data was found on the device.
348 print 'Pulling cyglog data...'
349 self._SetUpHostFolders()
350 self._device.old_interface.Adb().Pull(
351 self._DEVICE_CYGLOG_DIR, self._host_cyglog_dir)
352 files = os.listdir(self._host_cyglog_dir)
354 if len(files) == 0:
355 raise NoCyglogDataError('No cyglog data was collected')
357 return [os.path.join(self._host_cyglog_dir, x) for x in files]