1#!/usr/bin/env python
2
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
5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7from __future__ import absolute_import
8
9import json
10import os
11import re
12import shutil
13import subprocess
14import sys
15import tempfile
16import traceback
17from abc import ABCMeta, abstractmethod
18
19import mozinfo
20import mozprocess
21import mozproxy.utils as mpu
22import mozversion
23import six
24
25from mozprofile import create_profile
26from mozproxy import get_playback
27
28# need this so raptor imports work both from /raptor and via mach
29here = os.path.abspath(os.path.dirname(__file__))
30paths = [here]
31
32webext_dir = os.path.join(here, "..", "webext")
33paths.append(webext_dir)
34
35for path in paths:
36    if not os.path.exists(path):
37        raise IOError("%s does not exist. " % path)
38    sys.path.insert(0, path)
39
40from cmdline import FIREFOX_ANDROID_APPS
41from condprof.client import get_profile, ProfileNotFoundError
42from condprof.util import get_current_platform
43from logger.logger import RaptorLogger
44from gecko_profile import GeckoProfile
45from results import RaptorResultsHandler
46
47LOG = RaptorLogger(component="raptor-perftest")
48
49# - mozproxy.utils LOG displayed INFO messages even when LOG.error() was used in mitm.py
50mpu.LOG = RaptorLogger(component="raptor-mitmproxy")
51
52try:
53    from mozbuild.base import MozbuildObject
54
55    build = MozbuildObject.from_environment(cwd=here)
56except ImportError:
57    build = None
58
59POST_DELAY_CONDPROF = 1000
60POST_DELAY_DEBUG = 3000
61POST_DELAY_DEFAULT = 30000
62
63
64@six.add_metaclass(ABCMeta)
65class Perftest(object):
66    """Abstract base class for perftests that execute via a subharness,
67    either Raptor or browsertime."""
68
69    def __init__(
70        self,
71        app,
72        binary,
73        run_local=False,
74        noinstall=False,
75        obj_path=None,
76        profile_class=None,
77        installerpath=None,
78        gecko_profile=False,
79        gecko_profile_interval=None,
80        gecko_profile_entries=None,
81        gecko_profile_extra_threads=None,
82        gecko_profile_threads=None,
83        gecko_profile_features=None,
84        symbols_path=None,
85        host=None,
86        power_test=False,
87        cpu_test=False,
88        cold=False,
89        memory_test=False,
90        live_sites=False,
91        is_release_build=False,
92        debug_mode=False,
93        post_startup_delay=POST_DELAY_DEFAULT,
94        interrupt_handler=None,
95        e10s=True,
96        enable_webrender=False,
97        results_handler_class=RaptorResultsHandler,
98        device_name=None,
99        disable_perf_tuning=False,
100        conditioned_profile=None,
101        chimera=False,
102        extra_prefs={},
103        environment={},
104        project="mozilla-central",
105        verbose=False,
106        **kwargs
107    ):
108        self._dirs_to_remove = []
109        self.verbose = verbose
110
111        # Override the magic --host HOST_IP with the value of the environment variable.
112        if host == "HOST_IP":
113            host = os.environ["HOST_IP"]
114
115        self.config = {
116            "app": app,
117            "binary": binary,
118            "platform": mozinfo.os,
119            "processor": mozinfo.processor,
120            "run_local": run_local,
121            "obj_path": obj_path,
122            "gecko_profile": gecko_profile,
123            "gecko_profile_interval": gecko_profile_interval,
124            "gecko_profile_entries": gecko_profile_entries,
125            "gecko_profile_extra_threads": gecko_profile_extra_threads,
126            "gecko_profile_threads": gecko_profile_threads,
127            "gecko_profile_features": gecko_profile_features,
128            "symbols_path": symbols_path,
129            "host": host,
130            "power_test": power_test,
131            "memory_test": memory_test,
132            "cpu_test": cpu_test,
133            "cold": cold,
134            "live_sites": live_sites,
135            "is_release_build": is_release_build,
136            "enable_control_server_wait": memory_test or cpu_test,
137            "e10s": e10s,
138            "enable_webrender": enable_webrender,
139            "device_name": device_name,
140            "enable_fission": extra_prefs.get("fission.autostart", False),
141            "disable_perf_tuning": disable_perf_tuning,
142            "conditioned_profile": conditioned_profile,
143            "chimera": chimera,
144            "extra_prefs": extra_prefs,
145            "environment": environment,
146            "project": project,
147            "verbose": verbose,
148        }
149
150        self.firefox_android_apps = FIREFOX_ANDROID_APPS
151
152        # We are deactivating the conditioned profiles for:
153        # - win10-aarch64 : no support for geckodriver see 1582757
154        # - reference browser: no conditioned profiles created see 1606767
155        if (
156            self.config["platform"] == "win" and self.config["processor"] == "aarch64"
157        ) or self.config["binary"] == "org.mozilla.reference.browser.raptor":
158            self.config["conditioned_profile"] = None
159
160        if self.config["conditioned_profile"]:
161            LOG.info("Using a conditioned profile.")
162        else:
163            LOG.info("Using an empty profile.")
164
165        # To differentiate between chrome/firefox failures, we
166        # set an app variable in the logger which prefixes messages
167        # with the app name
168        if self.config["app"] in ("chrome", "chrome-m", "chromium"):
169            LOG.set_app(self.config["app"])
170
171        self.browser_name = None
172        self.browser_version = None
173
174        self.raptor_venv = os.path.join(os.getcwd(), "raptor-venv")
175        self.installerpath = installerpath
176        self.playback = None
177        self.benchmark = None
178        self.gecko_profiler = None
179        self.device = None
180        self.runtime_error = None
181        self.profile_class = profile_class or app
182        self.conditioned_profile_dir = None
183        self.interrupt_handler = interrupt_handler
184        self.results_handler = results_handler_class(**self.config)
185
186        self.browser_name, self.browser_version = self.get_browser_meta()
187
188        browser_name, browser_version = self.get_browser_meta()
189        self.results_handler.add_browser_meta(self.config["app"], browser_version)
190
191        # debug mode is currently only supported when running locally
192        self.run_local = self.config["run_local"]
193        self.debug_mode = debug_mode if self.run_local else False
194
195        # For the post startup delay, we want to max it to 1s when using the
196        # conditioned profiles.
197        if self.config.get("conditioned_profile"):
198            self.post_startup_delay = min(post_startup_delay, POST_DELAY_CONDPROF)
199        else:
200            # if running debug-mode reduce the pause after browser startup
201            if self.debug_mode:
202                self.post_startup_delay = min(post_startup_delay, POST_DELAY_DEBUG)
203            else:
204                self.post_startup_delay = post_startup_delay
205
206        if self.config["enable_webrender"]:
207            self.config["environment"]["MOZ_WEBRENDER"] = "1"
208        else:
209            self.config["environment"]["MOZ_WEBRENDER"] = "0"
210
211        LOG.info("Post startup delay set to %d ms" % self.post_startup_delay)
212        LOG.info("main raptor init, config is: %s" % str(self.config))
213        self.build_browser_profile()
214
215        # Crashes counter
216        self.crashes = 0
217
218    def _get_temp_dir(self):
219        tempdir = tempfile.mkdtemp()
220        self._dirs_to_remove.append(tempdir)
221        return tempdir
222
223    @property
224    def is_localhost(self):
225        return self.config.get("host") in ("localhost", "127.0.0.1")
226
227    @property
228    def conditioned_profile_copy(self):
229        """Returns a copy of the original conditioned profile that was created."""
230        condprof_copy = os.path.join(self._get_temp_dir(), "profile")
231        shutil.copytree(
232            self.conditioned_profile_dir,
233            condprof_copy,
234            ignore=shutil.ignore_patterns("lock"),
235        )
236        LOG.info("Created a conditioned-profile copy: %s" % condprof_copy)
237        return condprof_copy
238
239    def build_conditioned_profile(self):
240        # Late import so python-test doesn't import it
241        import asyncio
242        from condprof.runner import Runner
243
244        # The following import patchs an issue with invalid
245        # content-type, see bug 1655869
246        from condprof import patch  # noqa
247
248        if not getattr(self, "browsertime"):
249            raise Exception(
250                "Building conditioned profiles within a test is only supported "
251                "when using Browsertime."
252            )
253
254        geckodriver = getattr(self, "browsertime_geckodriver", None)
255        if not geckodriver:
256            geckodriver = (
257                sys.platform.startswith("win") and "geckodriver.exe" or "geckodriver"
258            )
259
260        scenario = self.config.get("conditioned_profile")
261        runner = Runner(
262            profile=None,
263            firefox=self.config.get("binary"),
264            geckodriver=geckodriver,
265            archive=None,
266            device_name=self.config.get("device_name"),
267            strict=True,
268            visible=True,
269            force_new=True,
270            skip_logs=True,
271        )
272
273        if self.config.get("is_release_build", False):
274            # Enable non-local connections for building the conditioned profile
275            self.enable_non_local_connections()
276
277        if scenario == "settled-youtube":
278            runner.prepare(scenario, "youtube")
279
280            loop = asyncio.get_event_loop()
281            loop.run_until_complete(runner.one_run(scenario, "youtube"))
282        else:
283            runner.prepare(scenario, "default")
284
285            loop = asyncio.get_event_loop()
286            loop.run_until_complete(runner.one_run(scenario, "default"))
287
288        if self.config.get("is_release_build", False):
289            self.disable_non_local_connections()
290
291        self.conditioned_profile_dir = runner.env.profile
292        return self.conditioned_profile_copy
293
294    def get_conditioned_profile(self, binary=None):
295        """Downloads a platform-specific conditioned profile, using the
296        condprofile client API; returns a self.conditioned_profile_dir"""
297        if self.conditioned_profile_dir:
298            # We already have a directory, so provide a copy that
299            # will get deleted after it's done with
300            return self.conditioned_profile_copy
301
302        # Build the conditioned profile before the test
303        if not self.config.get("conditioned_profile").startswith("artifact:"):
304            return self.build_conditioned_profile()
305
306        # create a temp file to help ensure uniqueness
307        temp_download_dir = self._get_temp_dir()
308        LOG.info(
309            "Making temp_download_dir from inside get_conditioned_profile {}".format(
310                temp_download_dir
311            )
312        )
313        # call condprof's client API to yield our platform-specific
314        # conditioned-profile binary
315        if isinstance(self, PerftestAndroid):
316            android_app = self.config["binary"].split("org.mozilla.")[-1]
317            device_name = self.config.get("device_name")
318            if device_name is None:
319                device_name = "g5"
320            platform = "%s-%s" % (device_name, android_app)
321        else:
322            platform = get_current_platform()
323
324        LOG.info("Platform used: %s" % platform)
325
326        # when running under mozharness, the --project value
327        # is set to match the project (try, mozilla-central, etc.)
328        # By default it's mozilla-central, even on local runs.
329        # We use it to prioritize conditioned profiles indexed
330        # into the same project when it runs on the CI
331        repo = self.config["project"]
332
333        # we fall back to mozilla-central in all cases. If it
334        # was already mozilla-central, we fall back to try
335        alternate_repo = "mozilla-central" if repo != "mozilla-central" else "try"
336        LOG.info("Getting profile from project %s" % repo)
337
338        profile_scenario = self.config.get("conditioned_profile").replace(
339            "artifact:", ""
340        )
341        try:
342            cond_prof_target_dir = get_profile(
343                temp_download_dir, platform, profile_scenario, repo=repo
344            )
345        except ProfileNotFoundError:
346            cond_prof_target_dir = get_profile(
347                temp_download_dir, platform, profile_scenario, repo=alternate_repo
348            )
349        except Exception:
350            # any other error is a showstopper
351            LOG.critical("Could not get the conditioned profile")
352            traceback.print_exc()
353            raise
354
355        # now get the full directory path to our fetched conditioned profile
356        self.conditioned_profile_dir = os.path.join(
357            temp_download_dir, cond_prof_target_dir
358        )
359        if not os.path.exists(cond_prof_target_dir):
360            LOG.critical(
361                "Can't find target_dir {}, from get_profile()"
362                "temp_download_dir {}, platform {}, scenario {}".format(
363                    cond_prof_target_dir, temp_download_dir, platform, profile_scenario
364                )
365            )
366            raise OSError
367
368        LOG.info(
369            "Original self.conditioned_profile_dir is now set: {}".format(
370                self.conditioned_profile_dir
371            )
372        )
373        return self.conditioned_profile_copy
374
375    def build_browser_profile(self):
376        if (
377            self.config["app"] in ["chrome", "chromium", "chrome-m"]
378            or self.config.get("conditioned_profile") is None
379        ):
380            self.profile = create_profile(self.profile_class)
381        else:
382            # use mozprofile to create a profile for us, from our conditioned profile's path
383            self.profile = create_profile(
384                self.profile_class, profile=self.get_conditioned_profile()
385            )
386        # Merge extra profile data from testing/profiles
387        with open(os.path.join(self.profile_data_dir, "profiles.json"), "r") as fh:
388            base_profiles = json.load(fh)["raptor"]
389
390        for profile in base_profiles:
391            path = os.path.join(self.profile_data_dir, profile)
392            LOG.info("Merging profile: {}".format(path))
393            self.profile.merge(path)
394
395        if self.config["extra_prefs"].get("fission.autostart", False):
396            LOG.info("Enabling fission via browser preferences")
397            LOG.info("Browser preferences: {}".format(self.config["extra_prefs"]))
398        self.profile.set_preferences(self.config["extra_prefs"])
399
400        # share the profile dir with the config and the control server
401        self.config["local_profile_dir"] = self.profile.profile
402        LOG.info("Local browser profile: {}".format(self.profile.profile))
403
404    @property
405    def profile_data_dir(self):
406        if "MOZ_DEVELOPER_REPO_DIR" in os.environ:
407            return os.path.join(
408                os.environ["MOZ_DEVELOPER_REPO_DIR"], "testing", "profiles"
409            )
410        if build:
411            return os.path.join(build.topsrcdir, "testing", "profiles")
412        return os.path.join(here, "profile_data")
413
414    @property
415    def artifact_dir(self):
416        artifact_dir = os.getcwd()
417        if self.config.get("run_local", False):
418            if "MOZ_DEVELOPER_REPO_DIR" in os.environ:
419                artifact_dir = os.path.join(
420                    os.environ["MOZ_DEVELOPER_REPO_DIR"],
421                    "testing",
422                    "mozharness",
423                    "build",
424                )
425            else:
426                artifact_dir = here
427        elif os.getenv("MOZ_UPLOAD_DIR"):
428            artifact_dir = os.getenv("MOZ_UPLOAD_DIR")
429        return artifact_dir
430
431    @abstractmethod
432    def run_test_setup(self, test):
433        LOG.info("starting test: %s" % test["name"])
434
435        # if 'alert_on' was provided in the test INI, add to our config for results/output
436        self.config["subtest_alert_on"] = test.get("alert_on")
437
438        if test.get("playback") is not None and self.playback is None:
439            self.start_playback(test)
440
441        if test.get("preferences") is not None:
442            self.set_browser_test_prefs(test["preferences"])
443
444    @abstractmethod
445    def setup_chrome_args(self):
446        pass
447
448    @abstractmethod
449    def get_browser_meta(self):
450        pass
451
452    def run_tests(self, tests, test_names):
453        try:
454            for test in tests:
455                try:
456                    self.run_test(test, timeout=int(test.get("page_timeout")))
457                except RuntimeError as e:
458                    # Check for crashes before showing the timeout error.
459                    self.check_for_crashes()
460                    if self.crashes == 0:
461                        LOG.critical(e)
462                    os.sys.exit(1)
463                finally:
464                    self.run_test_teardown(test)
465            return self.process_results(tests, test_names)
466        finally:
467            self.clean_up()
468
469    @abstractmethod
470    def run_test(self, test, timeout):
471        raise NotImplementedError()
472
473    @abstractmethod
474    def run_test_teardown(self, test):
475        self.check_for_crashes()
476
477    def process_results(self, tests, test_names):
478        # when running locally output results in build/raptor.json; when running
479        # in production output to a local.json to be turned into tc job artifact
480        raptor_json_path = os.path.join(self.artifact_dir, "raptor.json")
481        if not self.config.get("run_local", False):
482            raptor_json_path = os.path.join(os.getcwd(), "local.json")
483
484        self.config["raptor_json_path"] = raptor_json_path
485        self.config["artifact_dir"] = self.artifact_dir
486        res = self.results_handler.summarize_and_output(self.config, tests, test_names)
487
488        # gecko profiling symbolication
489        if self.config["gecko_profile"]:
490            self.gecko_profiler.symbolicate()
491            # clean up the temp gecko profiling folders
492            LOG.info("cleaning up after gecko profiling")
493            self.gecko_profiler.clean()
494
495        return res
496
497    @abstractmethod
498    def set_browser_test_prefs(self):
499        pass
500
501    @abstractmethod
502    def check_for_crashes(self):
503        pass
504
505    def clean_up(self):
506        for dir_to_rm in self._dirs_to_remove:
507            if not os.path.exists(dir_to_rm):
508                continue
509            LOG.info("Removing temporary directory: {}".format(dir_to_rm))
510            shutil.rmtree(dir_to_rm, ignore_errors=True)
511        self._dirs_to_remove = []
512
513    def get_page_timeout_list(self):
514        return self.results_handler.page_timeout_list
515
516    def get_recording_paths(self, test):
517        recordings = test.get("playback_recordings")
518
519        if recordings:
520            recording_paths = []
521            proxy_dir = self.playback.mozproxy_dir
522
523            for recording in recordings.split():
524                if not recording:
525                    continue
526                recording_paths.append(os.path.join(proxy_dir, recording))
527
528            return recording_paths
529
530    def log_recording_dates(self, test):
531        _recording_paths = self.get_recording_paths(test)
532        if _recording_paths is None:
533            LOG.info(
534                "No playback recordings specified in the test; so not getting recording info"
535            )
536            return
537
538        for r in _recording_paths:
539            json_path = "{}.json".format(r.split(".")[0])
540
541            if os.path.exists(json_path):
542                with open(json_path) as f:
543                    recording_date = json.loads(f.read()).get("recording_date")
544
545                    if recording_date is not None:
546                        LOG.info(
547                            "Playback recording date: {} ".format(
548                                recording_date.split(" ")[0]
549                            )
550                        )
551                    else:
552                        LOG.info("Playback recording date not available")
553            else:
554                LOG.info("Playback recording information not available")
555
556    def delete_proxy_settings_from_profile(self):
557        # Must delete the proxy settings from the profile if running
558        # the test with a host different from localhost.
559        userjspath = os.path.join(self.profile.profile, "user.js")
560        with open(userjspath) as userjsfile:
561            prefs = userjsfile.readlines()
562        prefs = [pref for pref in prefs if "network.proxy" not in pref]
563        with open(userjspath, "w") as userjsfile:
564            userjsfile.writelines(prefs)
565
566    def start_playback(self, test):
567        # creating the playback tool
568
569        playback_dir = os.path.join(here, "tooltool-manifests", "playback")
570
571        self.config.update(
572            {
573                "playback_tool": test.get("playback"),
574                "playback_version": test.get("playback_version", "4.0.4"),
575                "playback_files": [
576                    os.path.join(playback_dir, test.get("playback_pageset_manifest"))
577                ],
578            }
579        )
580
581        LOG.info("test uses playback tool: %s " % self.config["playback_tool"])
582
583        self.playback = get_playback(self.config)
584
585        # let's start it!
586        self.playback.start()
587
588        self.log_recording_dates(test)
589
590    def _init_gecko_profiling(self, test):
591        LOG.info("initializing gecko profiler")
592        upload_dir = os.getenv("MOZ_UPLOAD_DIR")
593        if not upload_dir:
594            LOG.critical("Profiling ignored because MOZ_UPLOAD_DIR was not set")
595        else:
596            self.gecko_profiler = GeckoProfile(upload_dir, self.config, test)
597
598    def disable_non_local_connections(self):
599        # For Firefox we need to set MOZ_DISABLE_NONLOCAL_CONNECTIONS=1 env var before startup
600        # when testing release builds from mozilla-beta/release. This is because of restrictions
601        # on release builds that require webextensions to be signed unless this env var is set
602        LOG.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=1")
603        os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
604
605    def enable_non_local_connections(self):
606        # pageload tests need to be able to access non-local connections via mitmproxy
607        LOG.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=0")
608        os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "0"
609
610
611class PerftestAndroid(Perftest):
612    """Mixin class for Android-specific Perftest subclasses."""
613
614    def setup_chrome_args(self, test):
615        """Sets up chrome/chromium cmd-line arguments.
616
617        Needs to be "implemented" here to deal with Python 2
618        unittest failures.
619        """
620        raise NotImplementedError
621
622    def get_browser_meta(self):
623        """Returns the browser name and version in a tuple (name, version).
624
625        Uses mozversion as the primary method to get this meta data and for
626        android this is the only method which exists to get this data. With android,
627        we use the installerpath attribute to determine this and this only works
628        with Firefox browsers.
629        """
630        browser_name = None
631        browser_version = None
632
633        if self.config["app"] in self.firefox_android_apps:
634            try:
635                meta = mozversion.get_version(binary=self.installerpath)
636                browser_name = meta.get("application_name")
637                browser_version = meta.get("application_version")
638            except Exception as e:
639                LOG.warning(
640                    "Failed to get android browser meta data through mozversion: %s-%s"
641                    % (e.__class__.__name__, e)
642                )
643
644        if self.config["app"] == "chrome-m":
645            # We absolutely need to determine the chrome
646            # version here so that we can select the correct
647            # chromedriver for browsertime
648            from mozdevice import ADBDeviceFactory
649
650            device = ADBDeviceFactory(verbose=True)
651            binary = "com.android.chrome"
652
653            pkg_info = device.shell_output("dumpsys package %s" % binary)
654            version_matcher = re.compile(r".*versionName=([\d.]+)")
655            for line in pkg_info.split("\n"):
656                match = version_matcher.match(line)
657                if match:
658                    browser_version = match.group(1)
659                    browser_name = self.config["app"]
660                    # First one found is the non-system
661                    # or latest version.
662                    break
663
664            if not browser_version:
665                raise Exception(
666                    "Could not determine version for Google Chrome for Android"
667                )
668
669        if not browser_name:
670            LOG.warning("Could not find a browser name")
671        else:
672            LOG.info("Browser name: %s" % browser_name)
673
674        if not browser_version:
675            LOG.warning("Could not find a browser version")
676        else:
677            LOG.info("Browser version: %s" % browser_version)
678
679        return (browser_name, browser_version)
680
681    def set_reverse_port(self, port):
682        tcp_port = "tcp:{}".format(port)
683        self.device.create_socket_connection("reverse", tcp_port, tcp_port)
684
685    def set_reverse_ports(self):
686        if self.is_localhost:
687
688            # only raptor-webext uses the control server
689            if self.config.get("browsertime", False) is False:
690                LOG.info("making the raptor control server port available to device")
691                self.set_reverse_port(self.control_server.port)
692
693            if self.playback:
694                LOG.info("making the raptor playback server port available to device")
695                self.set_reverse_port(self.playback.port)
696
697            if self.benchmark:
698                LOG.info("making the raptor benchmarks server port available to device")
699                self.set_reverse_port(int(self.benchmark.port))
700        else:
701            LOG.info("Reverse port forwarding is used only on local devices")
702
703    def build_browser_profile(self):
704        super(PerftestAndroid, self).build_browser_profile()
705
706        # Merge in the Android profile.
707        path = os.path.join(self.profile_data_dir, "raptor-android")
708        LOG.info("Merging profile: {}".format(path))
709        self.profile.merge(path)
710        self.profile.set_preferences(
711            {"browser.tabs.remote.autostart": self.config["e10s"]}
712        )
713
714    def clear_app_data(self):
715        LOG.info("clearing %s app data" % self.config["binary"])
716        self.device.shell("pm clear %s" % self.config["binary"])
717
718    def set_debug_app_flag(self):
719        # required so release apks will read the android config.yml file
720        LOG.info("setting debug-app flag for %s" % self.config["binary"])
721        self.device.shell("am set-debug-app --persistent %s" % self.config["binary"])
722
723    def copy_profile_to_device(self):
724        """Copy the profile to the device, and update permissions of all files."""
725        if not self.device.is_app_installed(self.config["binary"]):
726            raise Exception("%s is not installed" % self.config["binary"])
727
728        try:
729            LOG.info("copying profile to device: %s" % self.remote_profile)
730            self.device.rm(self.remote_profile, force=True, recursive=True)
731            self.device.push(self.profile.profile, self.remote_profile)
732            self.device.chmod(self.remote_profile, recursive=True)
733
734        except Exception:
735            LOG.error("Unable to copy profile to device.")
736            raise
737
738    def turn_on_android_app_proxy(self):
739        # for geckoview/android pageload playback we can't use a policy to turn on the
740        # proxy; we need to set prefs instead; note that the 'host' may be different
741        # than '127.0.0.1' so we must set the prefs accordingly
742        proxy_prefs = {}
743        proxy_prefs["network.proxy.type"] = 1
744        proxy_prefs["network.proxy.http"] = self.playback.host
745        proxy_prefs["network.proxy.http_port"] = self.playback.port
746        proxy_prefs["network.proxy.ssl"] = self.playback.host
747        proxy_prefs["network.proxy.ssl_port"] = self.playback.port
748        proxy_prefs["network.proxy.no_proxies_on"] = self.config["host"]
749
750        LOG.info(
751            "setting profile prefs to turn on the android app proxy: {}".format(
752                proxy_prefs
753            )
754        )
755        self.profile.set_preferences(proxy_prefs)
756
757
758class PerftestDesktop(Perftest):
759    """Mixin class for Desktop-specific Perftest subclasses"""
760
761    def __init__(self, *args, **kwargs):
762        super(PerftestDesktop, self).__init__(*args, **kwargs)
763        if self.config["enable_webrender"]:
764            self.config["environment"]["MOZ_ACCELERATED"] = "1"
765
766    def setup_chrome_args(self, test):
767        """Sets up chrome/chromium cmd-line arguments.
768
769        Needs to be "implemented" here to deal with Python 2
770        unittest failures.
771        """
772        raise NotImplementedError
773
774    def desktop_chrome_args(self, test):
775        """Returns cmd line options required to run pageload tests on Desktop Chrome
776        and Chromium. Also add the cmd line options to turn on the proxy and
777        ignore security certificate errors if using host localhost, 127.0.0.1.
778        """
779        chrome_args = ["--use-mock-keychain", "--no-default-browser-check"]
780
781        if test.get("playback", False):
782            pb_args = [
783                "--proxy-server=%s:%d" % (self.playback.host, self.playback.port),
784                "--proxy-bypass-list=localhost;127.0.0.1",
785                "--ignore-certificate-errors",
786            ]
787
788            if not self.is_localhost:
789                pb_args[0] = pb_args[0].replace("127.0.0.1", self.config["host"])
790
791            chrome_args.extend(pb_args)
792
793        if self.debug_mode:
794            chrome_args.extend(["--auto-open-devtools-for-tabs"])
795
796        return chrome_args
797
798    def get_browser_meta(self):
799        """Returns the browser name and version in a tuple (name, version).
800
801        On desktop, we use mozversion but a fallback method also exists
802        for non-firefox browsers, where mozversion is known to fail. The
803        methods are OS-specific, with windows being the outlier.
804        """
805        browser_name = None
806        browser_version = None
807
808        try:
809            meta = mozversion.get_version(binary=self.config["binary"])
810            browser_name = meta.get("application_name")
811            browser_version = meta.get("application_version")
812        except Exception as e:
813            LOG.warning(
814                "Failed to get browser meta data through mozversion: %s-%s"
815                % (e.__class__.__name__, e)
816            )
817            LOG.info("Attempting to get version through fallback method...")
818
819            # Fall-back method to get browser version on desktop
820            try:
821                if (
822                    "linux" in self.config["platform"]
823                    or "mac" in self.config["platform"]
824                ):
825                    command = [self.config["binary"], "--version"]
826                    proc = mozprocess.ProcessHandler(command)
827                    proc.run(timeout=10, outputTimeout=10)
828                    proc.wait()
829
830                    bmeta = proc.output
831                    meta_re = re.compile(r"([A-z\s]+)\s+([\w.]*)")
832                    if len(bmeta) != 0:
833                        match = meta_re.match(bmeta[0].decode("utf-8"))
834                        if match:
835                            browser_name = self.config["app"]
836                            browser_version = match.group(2)
837                    else:
838                        LOG.info("Couldn't get browser version and name")
839                else:
840                    # On windows we need to use wimc to get the version
841                    command = r'wmic datafile where name="{0}"'.format(
842                        self.config["binary"].replace("\\", r"\\")
843                    )
844                    bmeta = subprocess.check_output(command)
845
846                    meta_re = re.compile(r"\s+([\d.a-z]+)\s+")
847                    match = meta_re.findall(bmeta.decode("utf-8"))
848                    if len(match) > 0:
849                        browser_name = self.config["app"]
850                        browser_version = match[-1]
851                    else:
852                        LOG.info("Couldn't get browser version and name")
853            except Exception as e:
854                LOG.warning(
855                    "Failed to get browser meta data through fallback method: %s-%s"
856                    % (e.__class__.__name__, e)
857                )
858
859        if not browser_name:
860            LOG.warning("Could not find a browser name")
861        else:
862            LOG.info("Browser name: %s" % browser_name)
863
864        if not browser_version:
865            LOG.warning("Could not find a browser version")
866        else:
867            LOG.info("Browser version: %s" % browser_version)
868
869        return (browser_name, browser_version)
870