1#!/usr/bin/env python 2# ***** BEGIN LICENSE BLOCK ***** 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this file, 5# You can obtain one at http://mozilla.org/MPL/2.0/. 6# ***** END LICENSE BLOCK ***** 7 8from __future__ import absolute_import 9import copy 10import json 11import time 12import glob 13import os 14import sys 15import posixpath 16import subprocess 17 18# load modules from parent dir 19sys.path.insert(1, os.path.dirname(sys.path[0])) 20 21from mozharness.base.script import BaseScript, PreScriptAction 22from mozharness.mozilla.automation import EXIT_STATUS_DICT, TBPL_RETRY 23from mozharness.mozilla.mozbase import MozbaseMixin 24from mozharness.mozilla.testing.android import AndroidMixin 25from mozharness.mozilla.testing.testbase import TestingMixin, testing_config_options 26 27PAGES = [ 28 "js-input/webkit/PerformanceTests/Speedometer/index.html", 29 "blueprint/sample.html", 30 "blueprint/forms.html", 31 "blueprint/grid.html", 32 "blueprint/elements.html", 33 "js-input/3d-thingy.html", 34 "js-input/crypto-otp.html", 35 "js-input/sunspider/3d-cube.html", 36 "js-input/sunspider/3d-morph.html", 37 "js-input/sunspider/3d-raytrace.html", 38 "js-input/sunspider/access-binary-trees.html", 39 "js-input/sunspider/access-fannkuch.html", 40 "js-input/sunspider/access-nbody.html", 41 "js-input/sunspider/access-nsieve.html", 42 "js-input/sunspider/bitops-3bit-bits-in-byte.html", 43 "js-input/sunspider/bitops-bits-in-byte.html", 44 "js-input/sunspider/bitops-bitwise-and.html", 45 "js-input/sunspider/bitops-nsieve-bits.html", 46 "js-input/sunspider/controlflow-recursive.html", 47 "js-input/sunspider/crypto-aes.html", 48 "js-input/sunspider/crypto-md5.html", 49 "js-input/sunspider/crypto-sha1.html", 50 "js-input/sunspider/date-format-tofte.html", 51 "js-input/sunspider/date-format-xparb.html", 52 "js-input/sunspider/math-cordic.html", 53 "js-input/sunspider/math-partial-sums.html", 54 "js-input/sunspider/math-spectral-norm.html", 55 "js-input/sunspider/regexp-dna.html", 56 "js-input/sunspider/string-base64.html", 57 "js-input/sunspider/string-fasta.html", 58 "js-input/sunspider/string-tagcloud.html", 59 "js-input/sunspider/string-unpack-code.html", 60 "js-input/sunspider/string-validate-input.html", 61] 62 63 64class AndroidProfileRun(TestingMixin, BaseScript, MozbaseMixin, AndroidMixin): 65 """ 66 Mozharness script to generate an android PGO profile using the emulator 67 """ 68 69 config_options = copy.deepcopy(testing_config_options) 70 71 def __init__(self, require_config_file=False): 72 super(AndroidProfileRun, self).__init__( 73 config_options=self.config_options, 74 all_actions=[ 75 "setup-avds", 76 "download", 77 "create-virtualenv", 78 "start-emulator", 79 "verify-device", 80 "install", 81 "run-tests", 82 ], 83 require_config_file=require_config_file, 84 config={ 85 "virtualenv_modules": [], 86 "virtualenv_requirements": [], 87 "require_test_zip": True, 88 "mozbase_requirements": "mozbase_source_requirements.txt", 89 }, 90 ) 91 92 # these are necessary since self.config is read only 93 c = self.config 94 self.installer_path = c.get("installer_path") 95 self.device_serial = "emulator-5554" 96 97 def query_abs_dirs(self): 98 if self.abs_dirs: 99 return self.abs_dirs 100 abs_dirs = super(AndroidProfileRun, self).query_abs_dirs() 101 dirs = {} 102 103 dirs["abs_test_install_dir"] = os.path.join(abs_dirs["abs_src_dir"], "testing") 104 dirs["abs_xre_dir"] = os.path.join(abs_dirs["abs_work_dir"], "hostutils") 105 dirs["abs_blob_upload_dir"] = "/builds/worker/artifacts/blobber_upload_dir" 106 dirs["abs_avds_dir"] = os.path.join(abs_dirs["abs_work_dir"], ".android") 107 108 for key in dirs.keys(): 109 if key not in abs_dirs: 110 abs_dirs[key] = dirs[key] 111 self.abs_dirs = abs_dirs 112 return self.abs_dirs 113 114 ########################################## 115 # Actions for AndroidProfileRun # 116 ########################################## 117 118 def preflight_install(self): 119 # in the base class, this checks for mozinstall, but we don't use it 120 pass 121 122 @PreScriptAction("create-virtualenv") 123 def pre_create_virtualenv(self, action): 124 dirs = self.query_abs_dirs() 125 self.register_virtualenv_module( 126 "marionette", 127 os.path.join(dirs["abs_test_install_dir"], "marionette", "client"), 128 ) 129 130 def download(self): 131 """ 132 Download host utilities 133 """ 134 dirs = self.query_abs_dirs() 135 self.xre_path = self.download_hostutils(dirs["abs_xre_dir"]) 136 137 def install(self): 138 """ 139 Install APKs on the device. 140 """ 141 assert ( 142 self.installer_path is not None 143 ), "Either add installer_path to the config or use --installer-path." 144 self.install_apk(self.installer_path) 145 self.info("Finished installing apps for %s" % self.device_serial) 146 147 def run_tests(self): 148 """ 149 Generate the PGO profile data 150 """ 151 from mozhttpd import MozHttpd 152 from mozprofile import Preferences 153 from mozdevice import ADBDeviceFactory, ADBTimeoutError 154 from six import string_types 155 from marionette_driver.marionette import Marionette 156 157 app = self.query_package_name() 158 159 IP = "10.0.2.2" 160 PORT = 8888 161 162 PATH_MAPPINGS = { 163 "/js-input/webkit/PerformanceTests": "third_party/webkit/PerformanceTests", 164 } 165 166 dirs = self.query_abs_dirs() 167 topsrcdir = dirs["abs_src_dir"] 168 adb = self.query_exe("adb") 169 170 path_mappings = { 171 k: os.path.join(topsrcdir, v) for k, v in PATH_MAPPINGS.items() 172 } 173 httpd = MozHttpd( 174 port=PORT, 175 docroot=os.path.join(topsrcdir, "build", "pgo"), 176 path_mappings=path_mappings, 177 ) 178 httpd.start(block=False) 179 180 profile_data_dir = os.path.join(topsrcdir, "testing", "profiles") 181 with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh: 182 base_profiles = json.load(fh)["profileserver"] 183 184 prefpaths = [ 185 os.path.join(profile_data_dir, profile, "user.js") 186 for profile in base_profiles 187 ] 188 189 prefs = {} 190 for path in prefpaths: 191 prefs.update(Preferences.read_prefs(path)) 192 193 interpolation = {"server": "%s:%d" % httpd.httpd.server_address, "OOP": "false"} 194 for k, v in prefs.items(): 195 if isinstance(v, string_types): 196 v = v.format(**interpolation) 197 prefs[k] = Preferences.cast(v) 198 199 outputdir = self.config.get("output_directory", "/sdcard/pgo_profile") 200 jarlog = posixpath.join(outputdir, "en-US.log") 201 profdata = posixpath.join(outputdir, "default_%p_random_%m.profraw") 202 203 env = {} 204 env["XPCOM_DEBUG_BREAK"] = "warn" 205 env["MOZ_IN_AUTOMATION"] = "1" 206 env["MOZ_JAR_LOG_FILE"] = jarlog 207 env["LLVM_PROFILE_FILE"] = profdata 208 209 if self.query_minidump_stackwalk(): 210 os.environ["MINIDUMP_STACKWALK"] = self.minidump_stackwalk_path 211 os.environ["MINIDUMP_SAVE_PATH"] = self.query_abs_dirs()["abs_blob_upload_dir"] 212 if not self.symbols_path: 213 self.symbols_path = os.environ.get("MOZ_FETCHES_DIR") 214 215 # Force test_root to be on the sdcard for android pgo 216 # builds which fail for Android 4.3 when profiles are located 217 # in /data/local/tmp/test_root with 218 # E AndroidRuntime: FATAL EXCEPTION: Gecko 219 # E AndroidRuntime: java.lang.IllegalArgumentException: \ 220 # Profile directory must be writable if specified: /data/local/tmp/test_root/profile 221 # This occurs when .can-write-sentinel is written to 222 # the profile in 223 # mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java. 224 # This is not a problem on later versions of Android. This 225 # over-ride of test_root should be removed when Android 4.3 is no 226 # longer supported. 227 sdcard_test_root = "/sdcard/test_root" 228 adbdevice = ADBDeviceFactory( 229 adb=adb, device="emulator-5554", test_root=sdcard_test_root 230 ) 231 if adbdevice.test_root != sdcard_test_root: 232 # If the test_root was previously set and shared 233 # the initializer will not have updated the shared 234 # value. Force it to match the sdcard_test_root. 235 adbdevice.test_root = sdcard_test_root 236 adbdevice.mkdir(outputdir, parents=True) 237 238 try: 239 # Run Fennec a first time to initialize its profile 240 driver = Marionette( 241 app="fennec", 242 package_name=app, 243 adb_path=adb, 244 bin="geckoview-androidTest.apk", 245 prefs=prefs, 246 connect_to_running_emulator=True, 247 startup_timeout=1000, 248 env=env, 249 symbols_path=self.symbols_path, 250 ) 251 driver.start_session() 252 253 # Now generate the profile and wait for it to complete 254 for page in PAGES: 255 driver.navigate("http://%s:%d/%s" % (IP, PORT, page)) 256 timeout = 2 257 if "Speedometer/index.html" in page: 258 # The Speedometer test actually runs many tests internally in 259 # javascript, so it needs extra time to run through them. The 260 # emulator doesn't get very far through the whole suite, but 261 # this extra time at least lets some of them process. 262 timeout = 360 263 time.sleep(timeout) 264 265 driver.set_context("chrome") 266 driver.execute_script( 267 """ 268 Components.utils.import("resource://gre/modules/Services.jsm"); 269 let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"] 270 .createInstance(Components.interfaces.nsISupportsPRBool); 271 Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null); 272 return cancelQuit.data; 273 """ 274 ) 275 driver.execute_script( 276 """ 277 Components.utils.import("resource://gre/modules/Services.jsm"); 278 Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit) 279 """ 280 ) 281 282 # There is a delay between execute_script() returning and the profile data 283 # actually getting written out, so poll the device until we get a profile. 284 for i in range(50): 285 if not adbdevice.process_exist(app): 286 break 287 time.sleep(2) 288 else: 289 raise Exception("Android App (%s) never quit" % app) 290 291 # Pull all the profraw files and en-US.log 292 adbdevice.pull(outputdir, "/builds/worker/workspace/") 293 except ADBTimeoutError: 294 self.fatal( 295 "INFRA-ERROR: Failed with an ADBTimeoutError", 296 EXIT_STATUS_DICT[TBPL_RETRY], 297 ) 298 299 profraw_files = glob.glob("/builds/worker/workspace/*.profraw") 300 if not profraw_files: 301 self.fatal("Could not find any profraw files in /builds/worker/workspace") 302 merge_cmd = [ 303 os.path.join(os.environ["MOZ_FETCHES_DIR"], "clang/bin/llvm-profdata"), 304 "merge", 305 "-o", 306 "/builds/worker/workspace/merged.profdata", 307 ] + profraw_files 308 rc = subprocess.call(merge_cmd) 309 if rc != 0: 310 self.fatal( 311 "INFRA-ERROR: Failed to merge profile data. Corrupt profile?", 312 EXIT_STATUS_DICT[TBPL_RETRY], 313 ) 314 315 # tarfile doesn't support xz in this version of Python 316 tar_cmd = [ 317 "tar", 318 "-acvf", 319 "/builds/worker/artifacts/profdata.tar.xz", 320 "-C", 321 "/builds/worker/workspace", 322 "merged.profdata", 323 "en-US.log", 324 ] 325 subprocess.check_call(tar_cmd) 326 327 httpd.stop() 328 329 330if __name__ == "__main__": 331 test = AndroidProfileRun() 332 test.run_and_exit() 333