summaryrefslogtreecommitdiffstats
path: root/testing/raptor/raptor/perftest.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/raptor/raptor/perftest.py')
-rw-r--r--testing/raptor/raptor/perftest.py909
1 files changed, 909 insertions, 0 deletions
diff --git a/testing/raptor/raptor/perftest.py b/testing/raptor/raptor/perftest.py
new file mode 100644
index 0000000000..eb46e351af
--- /dev/null
+++ b/testing/raptor/raptor/perftest.py
@@ -0,0 +1,909 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import pathlib
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import traceback
+from abc import ABCMeta, abstractmethod
+
+import mozinfo
+import mozproxy.utils as mpu
+import mozversion
+import six
+from mozprofile import create_profile
+from mozproxy import get_playback
+
+# need this so raptor imports work both from /raptor and via mach
+here = os.path.abspath(os.path.dirname(__file__))
+paths = [here]
+
+for path in paths:
+ if not os.path.exists(path):
+ raise IOError("%s does not exist. " % path)
+ sys.path.insert(0, path)
+
+from chrome_trace import ChromeTrace
+from cmdline import (
+ CHROME_ANDROID_APPS,
+ FIREFOX_ANDROID_APPS,
+ FIREFOX_APPS,
+ GECKO_PROFILER_APPS,
+ TRACE_APPS,
+)
+from condprof.client import ProfileNotFoundError, get_profile
+from condprof.util import get_current_platform
+from gecko_profile import GeckoProfile
+from logger.logger import RaptorLogger
+from results import RaptorResultsHandler
+
+LOG = RaptorLogger(component="raptor-perftest")
+
+# - mozproxy.utils LOG displayed INFO messages even when LOG.error() was used in mitm.py
+mpu.LOG = RaptorLogger(component="raptor-mitmproxy")
+
+try:
+ from mozbuild.base import MozbuildObject
+
+ build = MozbuildObject.from_environment(cwd=here)
+except ImportError:
+ build = None
+
+POST_DELAY_CONDPROF = 1000
+POST_DELAY_MOBILE = 20000
+POST_DELAY_DEBUG = 3000
+POST_DELAY_DEFAULT = 30000
+
+
+@six.add_metaclass(ABCMeta)
+class Perftest(object):
+ """Abstract base class for perftests that execute via a subharness,
+ either Raptor or browsertime."""
+
+ def __init__(
+ self,
+ app,
+ binary,
+ run_local=False,
+ noinstall=False,
+ obj_path=None,
+ profile_class=None,
+ installerpath=None,
+ gecko_profile=False,
+ gecko_profile_interval=None,
+ gecko_profile_entries=None,
+ gecko_profile_extra_threads=None,
+ gecko_profile_threads=None,
+ gecko_profile_features=None,
+ extra_profiler_run=False,
+ symbols_path=None,
+ host=None,
+ cold=False,
+ live_sites=False,
+ is_release_build=False,
+ debug_mode=False,
+ post_startup_delay=POST_DELAY_DEFAULT,
+ interrupt_handler=None,
+ e10s=True,
+ results_handler_class=RaptorResultsHandler,
+ device_name=None,
+ disable_perf_tuning=False,
+ conditioned_profile=None,
+ test_bytecode_cache=False,
+ chimera=False,
+ extra_prefs={},
+ environment={},
+ project="mozilla-central",
+ verbose=False,
+ python=None,
+ fission=True,
+ extra_summary_methods=[],
+ benchmark_repository=None,
+ benchmark_revision=None,
+ benchmark_branch=None,
+ clean=False,
+ **kwargs
+ ):
+ self._remote_test_root = None
+ self._dirs_to_remove = []
+ self.verbose = verbose
+ self.page_count = []
+
+ # Override the magic --host HOST_IP with the value of the environment variable.
+ if host == "HOST_IP":
+ host = os.environ["HOST_IP"]
+
+ self.config = {
+ "app": app,
+ "binary": binary,
+ "platform": mozinfo.os,
+ "processor": mozinfo.processor,
+ "run_local": run_local,
+ "obj_path": obj_path,
+ "gecko_profile": gecko_profile,
+ "gecko_profile_interval": gecko_profile_interval,
+ "gecko_profile_entries": gecko_profile_entries,
+ "gecko_profile_extra_threads": gecko_profile_extra_threads,
+ "gecko_profile_threads": gecko_profile_threads,
+ "gecko_profile_features": gecko_profile_features,
+ "extra_profiler_run": extra_profiler_run,
+ "symbols_path": symbols_path,
+ "host": host,
+ "cold": cold,
+ "live_sites": live_sites,
+ "is_release_build": is_release_build,
+ "e10s": e10s,
+ "device_name": device_name,
+ "fission": fission,
+ "disable_perf_tuning": disable_perf_tuning,
+ "conditioned_profile": conditioned_profile,
+ "test_bytecode_cache": test_bytecode_cache,
+ "chimera": chimera,
+ "extra_prefs": extra_prefs,
+ "environment": environment,
+ "project": project,
+ "verbose": verbose,
+ "extra_summary_methods": extra_summary_methods,
+ "benchmark_repository": benchmark_repository,
+ "benchmark_revision": benchmark_revision,
+ "benchmark_branch": benchmark_branch,
+ "clean": clean,
+ }
+
+ self.firefox_android_apps = FIREFOX_ANDROID_APPS
+
+ # We are deactivating the conditioned profiles for:
+ # - win10-aarch64 : no support for geckodriver see 1582757
+ # - reference browser: no conditioned profiles created see 1606767
+ if (
+ self.config["platform"] == "win" and self.config["processor"] == "aarch64"
+ ) or self.config["binary"] == "org.mozilla.reference.browser.raptor":
+ self.config["conditioned_profile"] = None
+
+ if self.config["conditioned_profile"]:
+ LOG.info("Using a conditioned profile.")
+ else:
+ LOG.info("Using an empty profile.")
+
+ # To differentiate between chrome/firefox failures, we
+ # set an app variable in the logger which prefixes messages
+ # with the app name
+ if self.config["app"] in (
+ "chrome",
+ "chrome-m",
+ "chromium",
+ "custom-car",
+ "cstm-car-m",
+ ):
+ LOG.set_app(self.config["app"])
+
+ self.browser_name = None
+ self.browser_version = None
+
+ self.raptor_venv = os.path.join(os.getcwd(), "raptor-venv")
+ self.installerpath = installerpath
+ self.playback = None
+ self.benchmark = None
+ self.gecko_profiler = None
+ self.chrome_trace = None
+ self.device = None
+ self.runtime_error = None
+ self.profile_class = profile_class or app
+ # Use the `chromium` profile class for custom-car
+ if app in ["custom-car"]:
+ self.profile_class = "chromium"
+ self.conditioned_profile_dir = None
+ self.interrupt_handler = interrupt_handler
+ self.results_handler = results_handler_class(**self.config)
+
+ self.browser_name, self.browser_version = self.get_browser_meta()
+
+ browser_name, browser_version = self.get_browser_meta()
+ self.results_handler.add_browser_meta(self.config["app"], browser_version)
+
+ # debug mode is currently only supported when running locally
+ self.run_local = self.config["run_local"]
+ self.debug_mode = debug_mode if self.run_local else False
+
+ # For the post startup delay, we want to max it to 1s when using the
+ # conditioned profiles.
+ if self.config.get("conditioned_profile"):
+ self.post_startup_delay = min(post_startup_delay, POST_DELAY_CONDPROF)
+ elif (
+ self.debug_mode
+ ): # if running debug-mode reduce the pause after browser startup
+ self.post_startup_delay = min(post_startup_delay, POST_DELAY_DEBUG)
+ else:
+ self.post_startup_delay = post_startup_delay
+
+ if app in CHROME_ANDROID_APPS + FIREFOX_ANDROID_APPS and not self.config.get(
+ "conditioned_profile"
+ ):
+ LOG.info("Mobile non-conditioned profile")
+ self.post_startup_delay = POST_DELAY_MOBILE
+ LOG.info("Post startup delay set to %d ms" % self.post_startup_delay)
+ LOG.info("main raptor init, config is: %s" % str(self.config))
+
+ # TODO: Move this outside of the perftest initialization, it contains
+ # platform-specific code
+ self.build_browser_profile()
+
+ # Crashes counter
+ self.crashes = 0
+
+ def _get_temp_dir(self):
+ tempdir = tempfile.mkdtemp()
+ self._dirs_to_remove.append(tempdir)
+ return tempdir
+
+ @property
+ def is_localhost(self):
+ return self.config.get("host") in ("localhost", "127.0.0.1")
+
+ @property
+ def android_external_storage(self):
+ return "/sdcard/test_root/"
+
+ @property
+ def conditioned_profile_copy(self):
+ """Returns a copy of the original conditioned profile that was created."""
+ condprof_copy = os.path.join(self._get_temp_dir(), "profile")
+ shutil.copytree(
+ self.conditioned_profile_dir,
+ condprof_copy,
+ ignore=shutil.ignore_patterns("lock"),
+ )
+ LOG.info("Created a conditioned-profile copy: %s" % condprof_copy)
+ return condprof_copy
+
+ def build_conditioned_profile(self):
+ # Late import so python-test doesn't import it
+ import asyncio
+
+ # The following import patchs an issue with invalid
+ # content-type, see bug 1655869
+ from condprof import patch # noqa
+ from condprof.runner import Runner
+
+ if not getattr(self, "browsertime"):
+ raise Exception(
+ "Building conditioned profiles within a test is only supported "
+ "when using Browsertime."
+ )
+
+ geckodriver = getattr(self, "browsertime_geckodriver", None)
+ if not geckodriver:
+ geckodriver = (
+ sys.platform.startswith("win") and "geckodriver.exe" or "geckodriver"
+ )
+
+ scenario = self.config.get("conditioned_profile")
+ runner = Runner(
+ profile=None,
+ firefox=self.config.get("binary"),
+ geckodriver=geckodriver,
+ archive=None,
+ device_name=self.config.get("device_name"),
+ strict=True,
+ visible=True,
+ force_new=True,
+ skip_logs=True,
+ remote_test_root=self.android_external_storage,
+ )
+
+ if self.config.get("is_release_build", False):
+ # Enable non-local connections for building the conditioned profile
+ self.enable_non_local_connections()
+
+ if scenario == "settled-youtube":
+ runner.prepare(scenario, "youtube")
+
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(runner.one_run(scenario, "youtube"))
+ elif scenario == "settled-webext":
+ runner.prepare("settled", "webext")
+
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(runner.one_run("settled", "webext"))
+ else:
+ runner.prepare(scenario, "default")
+
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(runner.one_run(scenario, "default"))
+
+ if self.config.get("is_release_build", False):
+ self.disable_non_local_connections()
+
+ self.conditioned_profile_dir = runner.env.profile
+ return self.conditioned_profile_copy
+
+ def get_conditioned_profile(self, binary=None):
+ """Downloads a platform-specific conditioned profile, using the
+ condprofile client API; returns a self.conditioned_profile_dir"""
+ if self.conditioned_profile_dir:
+ # We already have a directory, so provide a copy that
+ # will get deleted after it's done with
+ return self.conditioned_profile_copy
+
+ # Build the conditioned profile before the test
+ if not self.config.get("conditioned_profile").startswith("artifact:"):
+ return self.build_conditioned_profile()
+
+ # create a temp file to help ensure uniqueness
+ temp_download_dir = self._get_temp_dir()
+ LOG.info(
+ "Making temp_download_dir from inside get_conditioned_profile {}".format(
+ temp_download_dir
+ )
+ )
+ # call condprof's client API to yield our platform-specific
+ # conditioned-profile binary
+ if isinstance(self, PerftestAndroid):
+ android_app = self.config["binary"].split("org.mozilla.")[-1]
+ device_name = self.config.get("device_name")
+ if device_name is None:
+ device_name = "g5"
+ platform = "%s-%s" % (device_name, android_app)
+ else:
+ platform = get_current_platform()
+
+ LOG.info("Platform used: %s" % platform)
+
+ # when running under mozharness, the --project value
+ # is set to match the project (try, mozilla-central, etc.)
+ # By default it's mozilla-central, even on local runs.
+ # We use it to prioritize conditioned profiles indexed
+ # into the same project when it runs on the CI
+ repo = self.config["project"]
+
+ # we fall back to mozilla-central in all cases. If it
+ # was already mozilla-central, we fall back to try
+ alternate_repo = "mozilla-central" if repo != "mozilla-central" else "try"
+ LOG.info("Getting profile from project %s" % repo)
+
+ profile_scenario = self.config.get("conditioned_profile").replace(
+ "artifact:", ""
+ )
+ try:
+ cond_prof_target_dir = get_profile(
+ temp_download_dir, platform, profile_scenario, repo=repo
+ )
+ except ProfileNotFoundError:
+ cond_prof_target_dir = get_profile(
+ temp_download_dir, platform, profile_scenario, repo=alternate_repo
+ )
+ except Exception:
+ # any other error is a showstopper
+ LOG.critical("Could not get the conditioned profile")
+ traceback.print_exc()
+ raise
+
+ # now get the full directory path to our fetched conditioned profile
+ self.conditioned_profile_dir = os.path.join(
+ temp_download_dir, cond_prof_target_dir
+ )
+ if not os.path.exists(cond_prof_target_dir):
+ LOG.critical(
+ "Can't find target_dir {}, from get_profile()"
+ "temp_download_dir {}, platform {}, scenario {}".format(
+ cond_prof_target_dir, temp_download_dir, platform, profile_scenario
+ )
+ )
+ raise OSError
+
+ LOG.info(
+ "Original self.conditioned_profile_dir is now set: {}".format(
+ self.conditioned_profile_dir
+ )
+ )
+ return self.conditioned_profile_copy
+
+ def build_browser_profile(self):
+ if self.config["app"] in FIREFOX_APPS:
+ if self.config.get("conditioned_profile") is None:
+ self.profile = create_profile(self.profile_class)
+ else:
+ # use mozprofile to create a profile for us, from our conditioned profile's path
+ self.profile = create_profile(
+ self.profile_class, profile=self.get_conditioned_profile()
+ )
+ else:
+ self.profile = None
+ return
+ # Merge extra profile data from testing/profiles
+ with open(os.path.join(self.profile_data_dir, "profiles.json"), "r") as fh:
+ base_profiles = json.load(fh)["raptor"]
+
+ for profile in base_profiles:
+ path = os.path.join(self.profile_data_dir, profile)
+ LOG.info("Merging profile: {}".format(path))
+ self.profile.merge(path)
+
+ LOG.info("Browser preferences: {}".format(self.config["extra_prefs"]))
+ self.profile.set_preferences(self.config["extra_prefs"])
+
+ # share the profile dir with the config and the control server
+ self.config["local_profile_dir"] = self.profile.profile
+ LOG.info("Local browser profile: {}".format(self.profile.profile))
+
+ @property
+ def profile_data_dir(self):
+ if "MOZ_DEVELOPER_REPO_DIR" in os.environ:
+ return os.path.join(
+ os.environ["MOZ_DEVELOPER_REPO_DIR"], "testing", "profiles"
+ )
+ if build:
+ return os.path.join(build.topsrcdir, "testing", "profiles")
+ return os.path.join(here, "profile_data")
+
+ @property
+ def artifact_dir(self):
+ artifact_dir = os.getcwd()
+ if self.config.get("run_local", False):
+ if "MOZ_DEVELOPER_REPO_DIR" in os.environ:
+ artifact_dir = os.path.join(
+ os.environ["MOZ_DEVELOPER_REPO_DIR"],
+ "testing",
+ "mozharness",
+ "build",
+ )
+ else:
+ artifact_dir = here
+ elif os.getenv("MOZ_UPLOAD_DIR"):
+ artifact_dir = os.getenv("MOZ_UPLOAD_DIR")
+ return artifact_dir
+
+ @abstractmethod
+ def run_test_setup(self, test):
+ LOG.info("starting test: %s" % test["name"])
+
+ # if 'alert_on' was provided in the test INI, add to our config for results/output
+ self.config["subtest_alert_on"] = test.get("alert_on")
+
+ if test.get("playback") is not None and self.playback is None:
+ self.start_playback(test)
+
+ if test.get("preferences") is not None and self.config["app"] in FIREFOX_APPS:
+ self.set_browser_test_prefs(test["preferences"])
+
+ @abstractmethod
+ def setup_chrome_args(self):
+ pass
+
+ @abstractmethod
+ def get_browser_meta(self):
+ pass
+
+ def run_tests(self, tests, test_names):
+ tests_to_run = tests
+ if self.results_handler.existing_results:
+ tests_to_run = []
+ try:
+ for test in tests_to_run:
+ try:
+ self.run_test(test, timeout=int(test.get("page_timeout")))
+ except RuntimeError as e:
+ # Check for crashes before showing the timeout error.
+ self.check_for_crashes()
+ if self.crashes == 0:
+ LOG.critical(e)
+ os.sys.exit(1)
+ finally:
+ self.run_test_teardown(test)
+ return self.process_results(tests, test_names)
+ finally:
+ self.clean_up()
+
+ @abstractmethod
+ def run_test(self, test, timeout):
+ raise NotImplementedError()
+
+ @abstractmethod
+ def run_test_teardown(self, test):
+ self.check_for_crashes()
+
+ def process_results(self, tests, test_names):
+ # when running locally output results in build/raptor.json; when running
+ # in production output to a local.json to be turned into tc job artifact
+ raptor_json_path = os.path.join(self.artifact_dir, "raptor.json")
+ if not self.config.get("run_local", False):
+ raptor_json_path = os.path.join(os.getcwd(), "local.json")
+
+ self.config["raptor_json_path"] = raptor_json_path
+ self.config["artifact_dir"] = self.artifact_dir
+ self.config["page_count"] = self.page_count
+ res = self.results_handler.summarize_and_output(self.config, tests, test_names)
+
+ # Gecko profiling symbolication
+ # We enable the gecko profiler either when the profiler is enabled with
+ # gecko_profile flag form the command line or when an extra profiler-enabled
+ # run is added with extra_profiler_run flag.
+ if self.config["gecko_profile"] or self.config.get("extra_profiler_run"):
+ if self.config["app"] in GECKO_PROFILER_APPS:
+ self.gecko_profiler.symbolicate()
+ # clean up the temp gecko profiling folders
+ LOG.info("cleaning up after gecko profiling")
+ self.gecko_profiler.clean()
+ elif self.config["app"] in TRACE_APPS:
+ self.chrome_trace.output_trace()
+ LOG.info("cleaning up after gathering chrome trace")
+ self.chrome_trace.clean()
+
+ return res
+
+ @abstractmethod
+ def set_browser_test_prefs(self):
+ pass
+
+ @abstractmethod
+ def check_for_crashes(self):
+ pass
+
+ def clean_up_mitmproxy(self):
+ if not self.run_local or self.config["clean"]:
+ mitmproxy_dir = pathlib.Path("~/.mitmproxy").expanduser().resolve()
+ if mitmproxy_dir.exists():
+ shutil.rmtree(mitmproxy_dir, ignore_errors=True)
+
+ def clean_up(self):
+ # Cleanup all of our temporary directories
+ for dir_to_rm in self._dirs_to_remove:
+ if not os.path.exists(dir_to_rm):
+ continue
+ LOG.info("Removing temporary directory: {}".format(dir_to_rm))
+ shutil.rmtree(dir_to_rm, ignore_errors=True)
+ self._dirs_to_remove = []
+
+ # Go through the artifact directory and ensure we
+ # don't have too many JPG/PNG files from a task failure
+ if (
+ not self.run_local
+ and self.results_handler
+ and self.results_handler.result_dir()
+ ):
+ artifact_dir = pathlib.Path(self.artifact_dir)
+ for filetype in ("*.png", "*.jpg"):
+ # Limit the number of images uploaded to the last (newest) 5
+ for file in sorted(artifact_dir.rglob(filetype))[:-5]:
+ try:
+ file.unlink()
+ except FileNotFoundError:
+ pass
+
+ self.clean_up_mitmproxy()
+
+ def get_page_timeout_list(self):
+ return self.results_handler.page_timeout_list
+
+ def delete_proxy_settings_from_profile(self):
+ # Must delete the proxy settings from the profile if running
+ # the test with a host different from localhost.
+ userjspath = os.path.join(self.profile.profile, "user.js")
+ with open(userjspath) as userjsfile:
+ prefs = userjsfile.readlines()
+ prefs = [pref for pref in prefs if "network.proxy" not in pref]
+ with open(userjspath, "w") as userjsfile:
+ userjsfile.writelines(prefs)
+
+ def start_playback(self, test):
+ # creating the playback tool
+ playback_dir = os.path.join(here, "tooltool-manifests", "playback")
+ playback_manifest = test.get("playback_pageset_manifest")
+ playback_manifests = playback_manifest.split(",")
+
+ self.config.update(
+ {
+ "playback_tool": test.get("playback"),
+ "playback_version": test.get("playback_version", "8.1.1"),
+ "playback_files": [
+ os.path.join(playback_dir, manifest)
+ for manifest in playback_manifests
+ ],
+ }
+ )
+
+ LOG.info("test uses playback tool: %s " % self.config["playback_tool"])
+
+ self.clean_up_mitmproxy()
+ self.playback = get_playback(self.config)
+
+ # let's start it!
+ self.playback.start()
+
+ def _init_gecko_profiling(self, test):
+ LOG.info("initializing gecko profiler")
+ upload_dir = os.getenv("MOZ_UPLOAD_DIR")
+ if not upload_dir:
+ LOG.critical("Profiling ignored because MOZ_UPLOAD_DIR was not set")
+ else:
+ self.gecko_profiler = GeckoProfile(upload_dir, self.config, test)
+
+ def _init_chrome_trace(self, test):
+ LOG.info("initializing Chrome Trace handler")
+ upload_dir = os.getenv("MOZ_UPLOAD_DIR")
+ if not upload_dir:
+ LOG.critical("Chrome Trace ignored because MOZ_UPLOAD_DIR was not set")
+ else:
+ self.chrome_trace = ChromeTrace(upload_dir, self.config, test)
+
+ def disable_non_local_connections(self):
+ # For Firefox we need to set MOZ_DISABLE_NONLOCAL_CONNECTIONS=1 env var before startup
+ # when testing release builds from mozilla-beta/release. This is because of restrictions
+ # on release builds that require webextensions to be signed unless this env var is set
+ LOG.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=1")
+ os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
+
+ def enable_non_local_connections(self):
+ # pageload tests need to be able to access non-local connections via mitmproxy
+ LOG.info("setting MOZ_DISABLE_NONLOCAL_CONNECTIONS=0")
+ os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "0"
+
+
+class PerftestAndroid(Perftest):
+ """Mixin class for Android-specific Perftest subclasses."""
+
+ def setup_chrome_args(self, test):
+ """Sets up chrome/chromium cmd-line arguments.
+
+ Needs to be "implemented" here to deal with Python 2
+ unittest failures.
+ """
+ raise NotImplementedError
+
+ def get_browser_meta(self):
+ """Returns the browser name and version in a tuple (name, version).
+
+ Uses mozversion as the primary method to get this meta data and for
+ android this is the only method which exists to get this data. With android,
+ we use the installerpath attribute to determine this and this only works
+ with Firefox browsers.
+ """
+ browser_name = None
+ browser_version = None
+
+ if self.config["app"] in self.firefox_android_apps:
+ try:
+ meta = mozversion.get_version(binary=self.installerpath)
+ browser_name = meta.get("application_name")
+ browser_version = meta.get("application_version")
+ except Exception as e:
+ LOG.warning(
+ "Failed to get android browser meta data through mozversion: %s-%s"
+ % (e.__class__.__name__, e)
+ )
+ elif self.config["app"] in CHROME_ANDROID_APPS or browser_version is None:
+ # We absolutely need to determine the chrome
+ # version here so that we can select the correct
+ # chromedriver for browsertime
+ from mozdevice import ADBDeviceFactory
+
+ device = ADBDeviceFactory(verbose=True)
+
+ # Chrome uses a specific binary that we don't set as a command line option
+ binary = "com.android.chrome"
+ if self.config["app"] not in CHROME_ANDROID_APPS:
+ binary = self.config["binary"]
+
+ pkg_info = device.shell_output("dumpsys package %s" % binary)
+ version_matcher = re.compile(r".*versionName=([\d.]+)")
+ for line in pkg_info.split("\n"):
+ match = version_matcher.match(line)
+ if match:
+ browser_version = match.group(1)
+ browser_name = self.config["app"]
+ # First one found is the non-system
+ # or latest version.
+ break
+
+ if not browser_version:
+ raise Exception("Could not determine version for apk %s" % binary)
+
+ if not browser_name:
+ LOG.warning("Could not find a browser name")
+ else:
+ LOG.info("Browser name: %s" % browser_name)
+
+ if not browser_version:
+ LOG.warning("Could not find a browser version")
+ else:
+ LOG.info("Browser version: %s" % browser_version)
+
+ return (browser_name, browser_version)
+
+ def set_reverse_port(self, port):
+ tcp_port = "tcp:{}".format(port)
+ self.device.create_socket_connection("reverse", tcp_port, tcp_port)
+
+ def set_reverse_ports(self):
+ if self.is_localhost:
+ if self.playback:
+ LOG.info("making the raptor playback server port available to device")
+ self.set_reverse_port(self.playback.port)
+
+ if self.benchmark:
+ LOG.info("making the raptor benchmarks server port available to device")
+ self.set_reverse_port(int(self.benchmark.port))
+ else:
+ LOG.info("Reverse port forwarding is used only on local devices")
+
+ def build_browser_profile(self):
+ super(PerftestAndroid, self).build_browser_profile()
+
+ if self.config["app"] in FIREFOX_ANDROID_APPS:
+ # Merge in the Android profile.
+ path = os.path.join(self.profile_data_dir, "raptor-android")
+ LOG.info("Merging profile: {}".format(path))
+ self.profile.merge(path)
+
+ def clear_app_data(self):
+ LOG.info("clearing %s app data" % self.config["binary"])
+ self.device.shell("pm clear %s" % self.config["binary"])
+
+ def set_debug_app_flag(self):
+ # required so release apks will read the android config.yml file
+ LOG.info("setting debug-app flag for %s" % self.config["binary"])
+ self.device.shell("am set-debug-app --persistent %s" % self.config["binary"])
+
+ def copy_profile_to_device(self):
+ """Copy the profile to the device, and update permissions of all files."""
+ if not self.device.is_app_installed(self.config["binary"]):
+ raise Exception("%s is not installed" % self.config["binary"])
+
+ try:
+ LOG.info("copying profile to device: %s" % self.remote_profile)
+ self.device.rm(self.remote_profile, force=True, recursive=True)
+ self.device.push(self.profile.profile, self.remote_profile)
+ self.device.chmod(self.remote_profile, recursive=True)
+
+ except Exception:
+ LOG.error("Unable to copy profile to device.")
+ raise
+
+ def turn_on_android_app_proxy(self):
+ # for geckoview/android pageload playback we can't use a policy to turn on the
+ # proxy; we need to set prefs instead; note that the 'host' may be different
+ # than '127.0.0.1' so we must set the prefs accordingly
+ proxy_prefs = {}
+ proxy_prefs["network.proxy.type"] = 1
+ proxy_prefs["network.proxy.http"] = self.playback.host
+ proxy_prefs["network.proxy.http_port"] = self.playback.port
+ proxy_prefs["network.proxy.ssl"] = self.playback.host
+ proxy_prefs["network.proxy.ssl_port"] = self.playback.port
+ proxy_prefs["network.proxy.no_proxies_on"] = self.config["host"]
+
+ LOG.info(
+ "setting profile prefs to turn on the android app proxy: {}".format(
+ proxy_prefs
+ )
+ )
+ self.profile.set_preferences(proxy_prefs)
+
+
+class PerftestDesktop(Perftest):
+ """Mixin class for Desktop-specific Perftest subclasses"""
+
+ def __init__(self, *args, **kwargs):
+ super(PerftestDesktop, self).__init__(*args, **kwargs)
+
+ def setup_chrome_args(self, test):
+ """Sets up chrome/chromium cmd-line arguments.
+
+ Needs to be "implemented" here to deal with Python 2
+ unittest failures.
+ """
+ raise NotImplementedError
+
+ def desktop_chrome_args(self, test):
+ """Returns cmd line options required to run pageload tests on Desktop Chrome
+ and Chromium. Also add the cmd line options to turn on the proxy and
+ ignore security certificate errors if using host localhost, 127.0.0.1.
+ """
+ chrome_args = ["--use-mock-keychain", "--no-default-browser-check"]
+
+ if test.get("playback", False):
+ pb_args = [
+ "--proxy-server=%s:%d" % (self.playback.host, self.playback.port),
+ "--proxy-bypass-list=localhost;127.0.0.1",
+ "--ignore-certificate-errors",
+ ]
+
+ if not self.is_localhost:
+ pb_args[0] = pb_args[0].replace("127.0.0.1", self.config["host"])
+
+ chrome_args.extend(pb_args)
+
+ if self.debug_mode:
+ chrome_args.extend(["--auto-open-devtools-for-tabs"])
+
+ return chrome_args
+
+ def get_browser_meta(self):
+ """Returns the browser name and version in a tuple (name, version).
+
+ On desktop, we use mozversion but a fallback method also exists
+ for non-firefox browsers, where mozversion is known to fail. The
+ methods are OS-specific, with windows being the outlier.
+ """
+ browser_name = None
+ browser_version = None
+
+ try:
+ meta = mozversion.get_version(binary=self.config["binary"])
+ browser_name = meta.get("application_name")
+ browser_version = meta.get("application_version")
+ except Exception as e:
+ LOG.warning(
+ "Failed to get browser meta data through mozversion: %s-%s"
+ % (e.__class__.__name__, e)
+ )
+ LOG.info("Attempting to get version through fallback method...")
+
+ # Fall-back method to get browser version on desktop
+ try:
+ if "mac" in self.config["platform"]:
+ import plistlib
+
+ for plist_file in ("version.plist", "Info.plist"):
+ try:
+ binary_path = pathlib.Path(self.config["binary"])
+ plist_path = binary_path.parent.parent.joinpath(plist_file)
+ with plist_path.open("rb") as plist_file_content:
+ plist = plistlib.load(plist_file_content)
+ except FileNotFoundError:
+ pass
+ browser_name = self.config["app"]
+ browser_version = plist.get("CFBundleShortVersionString")
+ elif "linux" in self.config["platform"]:
+ command = [self.config["binary"], "--version"]
+ proc = subprocess.run(
+ command, timeout=10, capture_output=True, text=True
+ )
+
+ bmeta = proc.stdout.split("\n")
+ meta_re = re.compile(r"([A-z\s]+)\s+([\w.]*)")
+ if len(bmeta) != 0:
+ match = meta_re.match(bmeta[0])
+ if match:
+ browser_name = self.config["app"]
+ browser_version = match.group(2)
+ else:
+ LOG.info("Couldn't get browser version and name")
+ else:
+ # On windows we need to use wimc to get the version
+ command = r'wmic datafile where name="{0}"'.format(
+ self.config["binary"].replace("\\", r"\\")
+ )
+ bmeta = subprocess.check_output(command)
+
+ meta_re = re.compile(r"\s+([\d.a-z]+)\s+")
+ match = meta_re.findall(bmeta.decode("utf-8"))
+ if len(match) > 0:
+ browser_name = self.config["app"]
+ browser_version = match[-1]
+ else:
+ LOG.info("Couldn't get browser version and name")
+ except Exception as e:
+ LOG.warning(
+ "Failed to get browser meta data through fallback method: %s-%s"
+ % (e.__class__.__name__, e)
+ )
+
+ if not browser_name:
+ LOG.warning("Could not find a browser name")
+ else:
+ LOG.info("Browser name: %s" % browser_name)
+
+ if not browser_version:
+ LOG.warning("Could not find a browser version")
+ else:
+ LOG.info("Browser version: %s" % browser_version)
+
+ return (browser_name, browser_version)