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.
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
.core
import webpagereplay
31 sys
.path
.append(os
.path
.join(sys
.path
[0], '..', '..',
32 'third_party', 'webpagereplay'))
33 import adb_install_cert
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
__()
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
)
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
64 _WPR_BUCKET
= 'chrome-partner-telemetry'
66 def __init__(self
, wpr_archive
, device
, cmdline_file
):
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
80 """Set up the device and host for WPR."""
82 #TODO(azarchs): make self._InstallTestCa() work
84 self
._StartForwarder
()
87 """Clean up the device and host's WPR setup."""
90 #TODO(azarchs): make self._RemoveTestCa() work
95 def __exit__(self
, unused_exc_type
, unused_exc_val
, unused_exc_tb
):
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
:
119 if self
._is
_test
_ca
_installed
:
121 self
._device
_cert
_util
.remove_cert()
123 # Best effort cleanup - show the error and continue.
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]
149 """ Stop the WPR and forwarder. """
150 print 'Stopping WPR on host...'
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
159 if not self
._wpr
_server
:
160 logging
.warning('No host WPR server to forward to.')
162 print 'Starting device forwarder...'
163 forwarder
.Forwarder
.Map([(0, self
._host
_http
_port
),
164 (0, self
._host
_https
_port
)],
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')
211 def RunCygprofileTests(self
):
212 """Run the cygprofile unit tests suite on the device.
215 path_to_tests: The location on the host machine with the compiled
216 cygprofile test binary.
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
)
224 self
._device
.old_interface
.GetShellCommandStatusAndOutput(
225 command
=device_path
, log_result
=True))
228 def CollectProfile(self
, apk
, package_info
):
229 """Run a profile and collect the log files.
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.
236 A list of cygprofile data files.
238 NoCyglogDataError: No data was found on the device.
240 self
._Install
(apk
, package_info
)
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
248 self
._StartChrome
(package_info
, 'about:blank')
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
)
256 self
._KillChrome
(package_info
)
258 self
._RestoreChromeFlags
(changer
)
260 data
= self
._PullCyglogData
()
261 self
._DeleteDeviceData
()
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.
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
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'])
303 def _RestoreChromeFlags(self
, changer
):
304 print 'Restoring Chrome flags...'
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
,
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.
344 A list of cyglog data files which were pulled.
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
)
355 raise NoCyglogDataError('No cyglog data was collected')
357 return [os
.path
.join(self
._host
_cyglog
_dir
, x
) for x
in files
]