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