exclude PluginsFieldTrialTest.NoPrefLeftBehind from valgrind bot
[chromium-blink-merge.git] / tools / cygprofile / profile_android_startup.py
blob29fad7484afd274c4af3baf15d397df12a1daa5d
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.PushChangedFiles([(self._cygprofile_tests, device_path)])
222 try:
223 self._device.RunShellCommand(device_path, check_return=True)
224 except device_errors.CommandFailedError:
225 # TODO(jbudorick): Let the exception propagate up once clients can
226 # handle it.
227 logging.exception('Failure while running cygprofile_unittests:')
228 return 1
229 return 0
231 def CollectProfile(self, apk, package_info):
232 """Run a profile and collect the log files.
234 Args:
235 apk: The location of the chrome apk to profile.
236 package_info: A PackageInfo structure describing the chrome apk,
237 as from pylib/constants.
238 Returns:
239 A list of cygprofile data files.
240 Raises:
241 NoCyglogDataError: No data was found on the device.
243 self._Install(apk)
244 try:
245 changer = self._SetChromeFlags(package_info)
246 self._SetUpDeviceFolders()
247 # Start up chrome once with a blank page, just to get the one-off
248 # activities out of the way such as apk resource extraction and profile
249 # creation.
250 self._StartChrome(package_info, 'about:blank')
251 time.sleep(15)
252 self._KillChrome(package_info)
253 self._SetUpDeviceFolders()
254 with WprManager(self._WPR_ARCHIVE, self._device,
255 package_info.cmdline_file):
256 self._StartChrome(package_info, self._TEST_URL)
257 time.sleep(90)
258 self._KillChrome(package_info)
259 finally:
260 self._RestoreChromeFlags(changer)
262 data = self._PullCyglogData()
263 self._DeleteDeviceData()
264 return data
266 def Cleanup(self):
267 """Delete all local and device files left over from profiling. """
268 self._DeleteDeviceData()
269 self._DeleteHostData()
271 def _Install(self, apk):
272 """Installs Chrome.apk on the device.
273 Args:
274 apk: The location of the chrome apk to profile.
275 package_info: A PackageInfo structure describing the chrome apk,
276 as from pylib/constants.
278 print 'Installing apk...'
279 self._device.Install(apk)
281 def _SetUpDevice(self):
282 """When profiling, files are output to the disk by every process. This
283 means running without sandboxing enabled.
285 # We need to have adb root in order to pull cyglog data
286 try:
287 print 'Enabling root...'
288 self._device.EnableRoot()
289 # SELinux need to be in permissive mode, otherwise the process cannot
290 # write the log files.
291 print 'Putting SELinux in permissive mode...'
292 self._device.RunShellCommand(['setenforce' '0'], check_return=True)
293 except device_errors.CommandFailedError as e:
294 # TODO(jbudorick) Handle this exception appropriately once interface
295 # conversions are finished.
296 logging.error(str(e))
298 def _SetChromeFlags(self, package_info):
299 print 'Setting Chrome flags...'
300 changer = flag_changer.FlagChanger(
301 self._device, package_info.cmdline_file)
302 changer.AddFlags(['--no-sandbox', '--disable-fre'])
303 return changer
305 def _RestoreChromeFlags(self, changer):
306 print 'Restoring Chrome flags...'
307 if changer:
308 changer.Restore()
310 def _SetUpDeviceFolders(self):
311 """Creates folders on the device to store cyglog data. """
312 print 'Setting up device folders...'
313 self._DeleteDeviceData()
314 self._device.RunShellCommand(
315 ['mkdir', '-p', str(self._DEVICE_CYGLOG_DIR)],
316 check_return=True)
318 def _DeleteDeviceData(self):
319 """Clears out cyglog storage locations on the device. """
320 self._device.RunShellCommand(
321 ['rm', '-rf', str(self._DEVICE_CYGLOG_DIR)],
322 check_return=True)
324 def _StartChrome(self, package_info, url):
325 print 'Launching chrome...'
326 self._device.StartActivity(
327 intent.Intent(package=package_info.package,
328 activity=package_info.activity,
329 data=url,
330 extras={'create_new_tab' : True}),
331 blocking=True, force_stop=True)
333 def _KillChrome(self, package_info):
334 self._device.KillAll(package_info.package)
336 def _DeleteHostData(self):
337 """Clears out cyglog storage locations on the host."""
338 shutil.rmtree(self._host_cyglog_dir, ignore_errors=True)
340 def _SetUpHostFolders(self):
341 self._DeleteHostData()
342 os.mkdir(self._host_cyglog_dir)
344 def _PullCyglogData(self):
345 """Pull the cyglog data off of the device.
347 Returns:
348 A list of cyglog data files which were pulled.
349 Raises:
350 NoCyglogDataError: No data was found on the device.
352 print 'Pulling cyglog data...'
353 self._SetUpHostFolders()
354 self._device.PullFile(
355 self._DEVICE_CYGLOG_DIR, self._host_cyglog_dir)
356 files = os.listdir(self._host_cyglog_dir)
358 if len(files) == 0:
359 raise NoCyglogDataError('No cyglog data was collected')
361 return [os.path.join(self._host_cyglog_dir, x) for x in files]