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
.internal
.util
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
.PushChangedFiles([(self
._cygprofile
_tests
, device_path
)])
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
227 logging
.exception('Failure while running cygprofile_unittests:')
231 def CollectProfile(self
, apk
, package_info
):
232 """Run a profile and collect the log files.
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.
239 A list of cygprofile data files.
241 NoCyglogDataError: No data was found on the device.
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
250 self
._StartChrome
(package_info
, 'about:blank')
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
)
258 self
._KillChrome
(package_info
)
260 self
._RestoreChromeFlags
(changer
)
262 data
= self
._PullCyglogData
()
263 self
._DeleteDeviceData
()
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.
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
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'])
305 def _RestoreChromeFlags(self
, changer
):
306 print 'Restoring Chrome flags...'
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
)],
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
)],
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
,
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.
348 A list of cyglog data files which were pulled.
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
)
359 raise NoCyglogDataError('No cyglog data was collected')
361 return [os
.path
.join(self
._host
_cyglog
_dir
, x
) for x
in files
]