diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/mozharness/mozharness/mozilla/testing/raptor.py | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/mozharness/mozharness/mozilla/testing/raptor.py')
-rw-r--r-- | testing/mozharness/mozharness/mozilla/testing/raptor.py | 1478 |
1 files changed, 1478 insertions, 0 deletions
diff --git a/testing/mozharness/mozharness/mozilla/testing/raptor.py b/testing/mozharness/mozharness/mozilla/testing/raptor.py new file mode 100644 index 0000000000..ceb97da963 --- /dev/null +++ b/testing/mozharness/mozharness/mozilla/testing/raptor.py @@ -0,0 +1,1478 @@ +#!/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 argparse +import copy +import glob +import multiprocessing +import os +import pathlib +import re +import subprocess +import sys +import tempfile +from shutil import copyfile, rmtree + +from six import string_types + +import mozharness +from mozharness.base.errors import PythonErrorList +from mozharness.base.log import CRITICAL, DEBUG, ERROR, INFO, OutputParser +from mozharness.base.python import Python3Virtualenv +from mozharness.base.vcs.vcsbase import MercurialScript +from mozharness.mozilla.automation import ( + EXIT_STATUS_DICT, + TBPL_RETRY, + TBPL_SUCCESS, + TBPL_WORST_LEVEL_TUPLE, +) +from mozharness.mozilla.testing.android import AndroidMixin +from mozharness.mozilla.testing.codecoverage import ( + CodeCoverageMixin, + code_coverage_config_options, +) +from mozharness.mozilla.testing.errors import HarnessErrorList, TinderBoxPrintRe +from mozharness.mozilla.testing.testbase import TestingMixin, testing_config_options + +scripts_path = os.path.abspath(os.path.dirname(os.path.dirname(mozharness.__file__))) +external_tools_path = os.path.join(scripts_path, "external_tools") +here = os.path.abspath(os.path.dirname(__file__)) + +RaptorErrorList = ( + PythonErrorList + + HarnessErrorList + + [ + {"regex": re.compile(r"""run-as: Package '.*' is unknown"""), "level": DEBUG}, + {"substr": r"""raptorDebug""", "level": DEBUG}, + { + "regex": re.compile(r"""^raptor[a-zA-Z-]*( - )?( )?(?i)error(:)?"""), + "level": ERROR, + }, + { + "regex": re.compile(r"""^raptor[a-zA-Z-]*( - )?( )?(?i)critical(:)?"""), + "level": CRITICAL, + }, + { + "regex": re.compile(r"""No machine_name called '.*' can be found"""), + "level": CRITICAL, + }, + { + "substr": r"""No such file or directory: 'browser_output.txt'""", + "level": CRITICAL, + "explanation": "Most likely the browser failed to launch, or the test otherwise " + "failed to start.", + }, + ] +) + +# When running raptor locally, we can attempt to make use of +# the users locally cached ffmpeg binary from from when the user +# ran `./mach browsertime --setup` +FFMPEG_LOCAL_CACHE = { + "mac": "ffmpeg-macos", + "linux": "ffmpeg-4.4.1-i686-static", + "win": "ffmpeg-4.4.1-full_build", +} + + +class Raptor( + TestingMixin, MercurialScript, CodeCoverageMixin, AndroidMixin, Python3Virtualenv +): + """ + Install and run Raptor tests + """ + + # Options to Browsertime. Paths are expected to be absolute. + browsertime_options = [ + [ + ["--browsertime-node"], + {"dest": "browsertime_node", "default": None, "help": argparse.SUPPRESS}, + ], + [ + ["--browsertime-browsertimejs"], + { + "dest": "browsertime_browsertimejs", + "default": None, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-vismet-script"], + { + "dest": "browsertime_vismet_script", + "default": None, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-chromedriver"], + { + "dest": "browsertime_chromedriver", + "default": None, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-ffmpeg"], + {"dest": "browsertime_ffmpeg", "default": None, "help": argparse.SUPPRESS}, + ], + [ + ["--browsertime-geckodriver"], + { + "dest": "browsertime_geckodriver", + "default": None, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-video"], + { + "dest": "browsertime_video", + "action": "store_true", + "default": False, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-visualmetrics"], + { + "dest": "browsertime_visualmetrics", + "action": "store_true", + "default": False, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-no-ffwindowrecorder"], + { + "dest": "browsertime_no_ffwindowrecorder", + "action": "store_true", + "default": False, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime-arg"], + { + "action": "append", + "metavar": "PREF=VALUE", + "dest": "browsertime_user_args", + "default": [], + "help": argparse.SUPPRESS, + }, + ], + [ + ["--browsertime"], + { + "dest": "browsertime", + "action": "store_true", + "default": True, + "help": argparse.SUPPRESS, + }, + ], + ] + + config_options = ( + [ + [ + ["--test"], + {"action": "store", "dest": "test", "help": "Raptor test to run"}, + ], + [ + ["--app"], + { + "default": "firefox", + "choices": [ + "firefox", + "chrome", + "chrome-m", + "chromium", + "fennec", + "geckoview", + "refbrow", + "fenix", + "safari", + "custom-car", + ], + "dest": "app", + "help": "Name of the application we are testing (default: firefox).", + }, + ], + [ + ["--activity"], + { + "dest": "activity", + "help": "The Android activity used to launch the Android app. " + "e.g.: org.mozilla.fenix.browser.BrowserPerformanceTestActivity", + }, + ], + [ + ["--intent"], + { + "dest": "intent", + "help": "Name of the Android intent action used to launch the Android app", + }, + ], + [ + ["--is-release-build"], + { + "action": "store_true", + "dest": "is_release_build", + "help": "Whether the build is a release build which requires work arounds " + "using MOZ_DISABLE_NONLOCAL_CONNECTIONS to support installing unsigned " + "webextensions. Defaults to False.", + }, + ], + [ + ["--add-option"], + { + "action": "extend", + "dest": "raptor_cmd_line_args", + "default": None, + "help": "Extra options to Raptor.", + }, + ], + [ + ["--device-name"], + { + "dest": "device_name", + "default": None, + "help": "Device name of mobile device.", + }, + ], + [ + ["--geckoProfile"], + { + "dest": "gecko_profile", + "action": "store_true", + "default": False, + "help": argparse.SUPPRESS, + }, + ], + [ + ["--geckoProfileInterval"], + { + "dest": "gecko_profile_interval", + "type": "int", + "help": argparse.SUPPRESS, + }, + ], + [ + ["--geckoProfileEntries"], + { + "dest": "gecko_profile_entries", + "type": "int", + "help": argparse.SUPPRESS, + }, + ], + [ + ["--geckoProfileFeatures"], + { + "dest": "gecko_profile_features", + "type": "str", + "help": argparse.SUPPRESS, + }, + ], + [ + ["--gecko-profile"], + { + "dest": "gecko_profile", + "action": "store_true", + "default": False, + "help": "Whether to profile the test run and save the profile results.", + }, + ], + [ + ["--gecko-profile-interval"], + { + "dest": "gecko_profile_interval", + "type": "int", + "help": "The interval between samples taken by the profiler (ms).", + }, + ], + [ + ["--gecko-profile-entries"], + { + "dest": "gecko_profile_entries", + "type": "int", + "help": "How many samples to take with the profiler.", + }, + ], + [ + ["--gecko-profile-threads"], + { + "dest": "gecko_profile_threads", + "type": "str", + "help": "Comma-separated list of threads to sample.", + }, + ], + [ + ["--gecko-profile-features"], + { + "dest": "gecko_profile_features", + "type": "str", + "help": "Features to enable in the profiler.", + }, + ], + [ + ["--extra-profiler-run"], + { + "dest": "extra_profiler_run", + "action": "store_true", + "default": False, + "help": "Run the tests again with profiler enabled after the main run.", + }, + ], + [ + ["--page-cycles"], + { + "dest": "page_cycles", + "type": "int", + "help": ( + "How many times to repeat loading the test page (for page load " + "tests); for benchmark tests this is how many times the benchmark test " + "will be run." + ), + }, + ], + [ + ["--page-timeout"], + { + "dest": "page_timeout", + "type": "int", + "help": "How long to wait (ms) for one page_cycle to complete, before timing out.", # NOQA: E501 + }, + ], + [ + ["--browser-cycles"], + { + "dest": "browser_cycles", + "type": "int", + "help": ( + "The number of times a cold load test is repeated (for cold load tests " + "only, where the browser is shutdown and restarted between test " + "iterations)." + ), + }, + ], + [ + ["--project"], + { + "action": "store", + "dest": "project", + "default": "mozilla-central", + "type": "str", + "help": "Name of the project (try, mozilla-central, etc.)", + }, + ], + [ + ["--test-url-params"], + { + "action": "store", + "dest": "test_url_params", + "help": "Parameters to add to the test_url query string.", + }, + ], + [ + ["--host"], + { + "dest": "host", + "type": "str", + "default": "127.0.0.1", + "help": "Hostname from which to serve urls (default: 127.0.0.1). " + "The value HOST_IP will cause the value of host to be " + "to be loaded from the environment variable HOST_IP.", + }, + ], + [ + ["--power-test"], + { + "dest": "power_test", + "action": "store_true", + "default": False, + "help": ( + "Use Raptor to measure power usage on Android browsers (Geckoview " + "Example, Fenix, Refbrow, and Fennec) as well as on Intel-based MacOS " + "machines that have Intel Power Gadget installed." + ), + }, + ], + [ + ["--memory-test"], + { + "dest": "memory_test", + "action": "store_true", + "default": False, + "help": "Use Raptor to measure memory usage.", + }, + ], + [ + ["--cpu-test"], + { + "dest": "cpu_test", + "action": "store_true", + "default": False, + "help": "Use Raptor to measure CPU usage.", + }, + ], + [ + ["--disable-perf-tuning"], + { + "action": "store_true", + "dest": "disable_perf_tuning", + "default": False, + "help": "Disable performance tuning on android.", + }, + ], + [ + ["--conditioned-profile"], + { + "dest": "conditioned_profile", + "type": "str", + "default": None, + "help": ( + "Name of conditioned profile to use. Prefix with `artifact:` " + "if we should obtain the profile from CI.", + ), + }, + ], + [ + ["--live-sites"], + { + "dest": "live_sites", + "action": "store_true", + "default": False, + "help": "Run tests using live sites instead of recorded sites.", + }, + ], + [ + ["--test-bytecode-cache"], + { + "dest": "test_bytecode_cache", + "action": "store_true", + "default": False, + "help": ( + "If set, the pageload test will set the preference " + "`dom.script_loader.bytecode_cache.strategy=-1` and wait 20 seconds " + "after the first cold pageload to populate the bytecode cache before " + "running a warm pageload test. Only available if `--chimera` " + "is also provided." + ), + }, + ], + [ + ["--chimera"], + { + "dest": "chimera", + "action": "store_true", + "default": False, + "help": "Run tests in chimera mode. Each browser cycle will run a cold and warm test.", # NOQA: E501 + }, + ], + [ + ["--debug-mode"], + { + "dest": "debug_mode", + "action": "store_true", + "default": False, + "help": "Run Raptor in debug mode (open browser console, limited page-cycles, etc.)", # NOQA: E501 + }, + ], + [ + ["--noinstall"], + { + "dest": "noinstall", + "action": "store_true", + "default": False, + "help": "Do not offer to install Android APK.", + }, + ], + [ + ["--disable-e10s"], + { + "dest": "e10s", + "action": "store_false", + "default": True, + "help": "Run without multiple processes (e10s).", + }, + ], + [ + ["--disable-fission"], + { + "action": "store_false", + "dest": "fission", + "default": True, + "help": "Disable Fission (site isolation) in Gecko.", + }, + ], + [ + ["--setpref"], + { + "action": "append", + "metavar": "PREF=VALUE", + "dest": "extra_prefs", + "default": [], + "help": "Set a browser preference. May be used multiple times.", + }, + ], + [ + ["--setenv"], + { + "action": "append", + "metavar": "NAME=VALUE", + "dest": "environment", + "default": [], + "help": "Set a variable in the test environment. May be used multiple times.", + }, + ], + [ + ["--skip-preflight"], + { + "action": "store_true", + "dest": "skip_preflight", + "default": False, + "help": "skip preflight commands to prepare machine.", + }, + ], + [ + ["--cold"], + { + "action": "store_true", + "dest": "cold", + "default": False, + "help": "Enable cold page-load for browsertime tp6", + }, + ], + [ + ["--verbose"], + { + "action": "store_true", + "dest": "verbose", + "default": False, + "help": "Verbose output", + }, + ], + [ + ["--enable-marionette-trace"], + { + "action": "store_true", + "dest": "enable_marionette_trace", + "default": False, + "help": "Enable marionette tracing", + }, + ], + [ + ["--clean"], + { + "action": "store_true", + "dest": "clean", + "default": False, + "help": ( + "Clean the python virtualenv (remove, and rebuild) for " + "Raptor before running tests." + ), + }, + ], + [ + ["--webext"], + { + "action": "store_true", + "dest": "webext", + "default": False, + "help": ( + "Whether to use webextension to execute pageload tests " + "(WebExtension is being deprecated).", + ), + }, + ], + [ + ["--collect-perfstats"], + { + "action": "store_true", + "dest": "collect_perfstats", + "default": False, + "help": ( + "If set, the test will collect perfstats in addition to " + "the regular metrics it gathers." + ), + }, + ], + [ + ["--extra-summary-methods"], + { + "action": "append", + "metavar": "OPTION", + "dest": "extra_summary_methods", + "default": [], + "help": ( + "Alternative methods for summarizing technical and visual" + "pageload metrics." + "Options: geomean, mean." + ), + }, + ], + [ + ["--benchmark-repository"], + { + "dest": "benchmark_repository", + "type": "str", + "default": None, + "help": ( + "Repository that should be used for a particular benchmark test. " + "e.g. https://github.com/mozilla-mobile/firefox-android" + ), + }, + ], + [ + ["--benchmark-revision"], + { + "dest": "benchmark_revision", + "type": "str", + "default": None, + "help": ( + "Repository revision that should be used for a particular " + "benchmark test." + ), + }, + ], + [ + ["--benchmark-branch"], + { + "dest": "benchmark_branch", + "type": "str", + "default": None, + "help": ( + "Repository branch that should be used for a particular benchmark test." + ), + }, + ], + ] + + testing_config_options + + copy.deepcopy(code_coverage_config_options) + + browsertime_options + ) + + def __init__(self, **kwargs): + kwargs.setdefault("config_options", self.config_options) + kwargs.setdefault( + "all_actions", + [ + "clobber", + "download-and-extract", + "populate-webroot", + "create-virtualenv", + "install-chrome-android", + "install-chromium-distribution", + "install", + "run-tests", + ], + ) + kwargs.setdefault( + "default_actions", + [ + "clobber", + "download-and-extract", + "populate-webroot", + "create-virtualenv", + "install-chromium-distribution", + "install", + "run-tests", + ], + ) + kwargs.setdefault("config", {}) + super(Raptor, self).__init__(**kwargs) + + # Convenience + self.workdir = self.query_abs_dirs()["abs_work_dir"] + + self.run_local = self.config.get("run_local") + + # App (browser testing on) defaults to firefox + self.app = "firefox" + + if self.run_local: + # Get app from command-line args, passed in from mach, inside 'raptor_cmd_line_args' + # Command-line args can be in two formats depending on how the user entered them + # i.e. "--app=geckoview" or separate as "--app", "geckoview" so we have to + # parse carefully. It's simplest to use `argparse` to parse partially. + self.app = "firefox" + if "raptor_cmd_line_args" in self.config: + sub_parser = argparse.ArgumentParser() + # It's not necessary to limit the allowed values: each value + # will be parsed and verifed by raptor/raptor.py. + sub_parser.add_argument("--app", default=None, dest="app") + sub_parser.add_argument("-i", "--intent", default=None, dest="intent") + sub_parser.add_argument( + "-a", "--activity", default=None, dest="activity" + ) + + # We'd prefer to use `parse_known_intermixed_args`, but that's + # new in Python 3.7. + known, unknown = sub_parser.parse_known_args( + self.config["raptor_cmd_line_args"] + ) + + if known.app: + self.app = known.app + if known.intent: + self.intent = known.intent + if known.activity: + self.activity = known.activity + else: + # Raptor initiated in production via mozharness + self.test = self.config["test"] + self.app = self.config.get("app", "firefox") + self.binary_path = self.config.get("binary_path", None) + + if self.app in ("refbrow", "fenix"): + self.app_name = self.binary_path + + self.installer_url = self.config.get("installer_url") + self.raptor_json_url = self.config.get("raptor_json_url") + self.raptor_json = self.config.get("raptor_json") + self.raptor_json_config = self.config.get("raptor_json_config") + self.repo_path = self.config.get("repo_path") + self.obj_path = self.config.get("obj_path") + self.mozbuild_path = self.config.get("mozbuild_path") + self.test = None + self.gecko_profile = self.config.get( + "gecko_profile" + ) or "--geckoProfile" in self.config.get("raptor_cmd_line_args", []) + self.gecko_profile_interval = self.config.get("gecko_profile_interval") + self.gecko_profile_entries = self.config.get("gecko_profile_entries") + self.gecko_profile_threads = self.config.get("gecko_profile_threads") + self.gecko_profile_features = self.config.get("gecko_profile_features") + self.extra_profiler_run = self.config.get("extra_profiler_run") + self.test_packages_url = self.config.get("test_packages_url") + self.test_url_params = self.config.get("test_url_params") + self.host = self.config.get("host") + if self.host == "HOST_IP": + self.host = os.environ["HOST_IP"] + self.power_test = self.config.get("power_test") + self.memory_test = self.config.get("memory_test") + self.cpu_test = self.config.get("cpu_test") + self.live_sites = self.config.get("live_sites") + self.chimera = self.config.get("chimera") + self.disable_perf_tuning = self.config.get("disable_perf_tuning") + self.conditioned_profile = self.config.get("conditioned_profile") + self.extra_prefs = self.config.get("extra_prefs") + self.environment = self.config.get("environment") + self.is_release_build = self.config.get("is_release_build") + self.debug_mode = self.config.get("debug_mode", False) + self.chromium_dist_path = None + self.firefox_android_browsers = ["fennec", "geckoview", "refbrow", "fenix"] + self.android_browsers = self.firefox_android_browsers + ["chrome-m"] + self.browsertime_visualmetrics = self.config.get("browsertime_visualmetrics") + self.browsertime_node = self.config.get("browsertime_node") + self.browsertime_user_args = self.config.get("browsertime_user_args") + self.browsertime_video = False + self.enable_marionette_trace = self.config.get("enable_marionette_trace") + self.browser_cycles = self.config.get("browser_cycles") + self.clean = self.config.get("clean") + + for (arg,), details in Raptor.browsertime_options: + # Allow overriding defaults on the `./mach raptor-test ...` command-line. + value = self.config.get(details["dest"]) + if value and arg not in self.config.get("raptor_cmd_line_args", []): + setattr(self, details["dest"], value) + + # We accept some configuration options from the try commit message in the + # format mozharness: <options>. Example try commit message: mozharness: + # --geckoProfile try: <stuff> + def query_gecko_profile_options(self): + gecko_results = [] + # If gecko_profile is set, we add that to Raptor's options + if self.gecko_profile: + gecko_results.append("--gecko-profile") + if self.gecko_profile_interval: + gecko_results.extend( + ["--gecko-profile-interval", str(self.gecko_profile_interval)] + ) + if self.gecko_profile_entries: + gecko_results.extend( + ["--gecko-profile-entries", str(self.gecko_profile_entries)] + ) + if self.gecko_profile_features: + gecko_results.extend( + ["--gecko-profile-features", self.gecko_profile_features] + ) + if self.gecko_profile_threads: + gecko_results.extend( + ["--gecko-profile-threads", self.gecko_profile_threads] + ) + else: + if self.extra_profiler_run: + gecko_results.append("--extra-profiler-run") + return gecko_results + + def query_abs_dirs(self): + if self.abs_dirs: + return self.abs_dirs + abs_dirs = super(Raptor, self).query_abs_dirs() + abs_dirs["abs_blob_upload_dir"] = os.path.join( + abs_dirs["abs_work_dir"], "blobber_upload_dir" + ) + abs_dirs["abs_test_install_dir"] = os.path.join( + abs_dirs["abs_work_dir"], "tests" + ) + + self.abs_dirs = abs_dirs + return self.abs_dirs + + def install_chrome_android(self): + """Install Google Chrome for Android in production from tooltool""" + if self.app != "chrome-m": + self.info("Google Chrome for Android not required") + return + if self.config.get("run_local"): + self.info( + "Google Chrome for Android will not be installed " + "from tooltool when running locally" + ) + return + self.info("Fetching and installing Google Chrome for Android") + self.device.shell_output("cmd package install-existing com.android.chrome") + self.info("Google Chrome for Android successfully installed") + + def download_chrome_android(self): + # Fetch the APK + tmpdir = tempfile.mkdtemp() + self.tooltool_fetch( + os.path.join( + self.raptor_path, + "raptor", + "tooltool-manifests", + "chrome-android", + "chrome87.manifest", + ), + output_dir=tmpdir, + ) + files = os.listdir(tmpdir) + if len(files) > 1: + raise Exception( + "Found more than one chrome APK file after tooltool download" + ) + chromeapk = os.path.join(tmpdir, files[0]) + + # Disable verification and install the APK + self.device.shell_output("settings put global verifier_verify_adb_installs 0") + self.install_android_app(chromeapk, replace=True) + + # Re-enable verification and delete the temporary directory + self.device.shell_output("settings put global verifier_verify_adb_installs 1") + rmtree(tmpdir) + + def install_chromium_distribution(self): + """Install Google Chromium distribution in production""" + linux, mac, win = "linux", "mac", "win" + chrome, chromium, chromium_release = "chrome", "chromium", "custom-car" + + available_chromium_dists = [chrome, chromium, chromium_release] + binary_location = { + chromium: { + linux: ["chrome-linux", "chrome"], + mac: ["chrome-mac", "Chromium.app", "Contents", "MacOS", "Chromium"], + win: ["chrome-win", "Chrome.exe"], + }, + chromium_release: { + linux: ["chromium", "Default", "chrome"], + win: ["chromium", "Default", "chrome.exe"], + }, + } + + if self.app not in available_chromium_dists: + self.info("Google Chrome or Chromium distributions are not required.") + return + + if self.app == "chrome": + self.info("Chrome should be preinstalled.") + if win in self.platform_name(): + base_path = "C:\\%s\\Google\\Chrome\\Application\\chrome.exe" + self.chromium_dist_path = base_path % "Progra~1" + if not os.path.exists(self.chromium_dist_path): + self.chromium_dist_path = base_path % "Progra~2" + elif linux in self.platform_name(): + self.chromium_dist_path = "/usr/bin/google-chrome" + elif mac in self.platform_name(): + self.chromium_dist_path = ( + "/Applications/Google Chrome.app/" "Contents/MacOS/Google Chrome" + ) + else: + self.error( + "Chrome is not installed on the platform %s yet." + % self.platform_name() + ) + + if os.path.exists(self.chromium_dist_path): + self.info( + "Google Chrome found in expected location %s" + % self.chromium_dist_path + ) + else: + self.error("Cannot find Google Chrome at %s" % self.chromium_dist_path) + + return + + chromium_dist = self.app + + if self.config.get("run_local"): + self.info("Expecting %s to be pre-installed locally" % chromium_dist) + return + + self.info("Getting fetched %s build" % chromium_dist) + self.chromium_dist_dest = os.path.normpath( + os.path.abspath(os.environ["MOZ_FETCHES_DIR"]) + ) + + if mac in self.platform_name(): + self.chromium_dist_path = os.path.join( + self.chromium_dist_dest, *binary_location[chromium_dist][mac] + ) + + elif linux in self.platform_name(): + self.chromium_dist_path = os.path.join( + self.chromium_dist_dest, *binary_location[chromium_dist][linux] + ) + + else: + self.chromium_dist_path = os.path.join( + self.chromium_dist_dest, *binary_location[chromium_dist][win] + ) + + self.info("%s dest is: %s" % (chromium_dist, self.chromium_dist_dest)) + self.info("%s path is: %s" % (chromium_dist, self.chromium_dist_path)) + + # Now ensure Chromium binary exists + if os.path.exists(self.chromium_dist_path): + self.info( + "Successfully installed %s to: %s" + % (chromium_dist, self.chromium_dist_path) + ) + else: + self.info("Abort: failed to install %s" % chromium_dist) + + def raptor_options(self, args=None, **kw): + """Return options to Raptor""" + options = [] + kw_options = {} + + # Get the APK location to be able to get the browser version + # through mozversion + if self.app in self.firefox_android_browsers and not self.run_local: + kw_options["installerpath"] = self.installer_path + + # If testing on Firefox, the binary path already came from mozharness/pro; + # otherwise the binary path is forwarded from command-line arg (raptor_cmd_line_args). + kw_options["app"] = self.app + if self.app == "firefox" or ( + self.app in self.firefox_android_browsers and not self.run_local + ): + binary_path = self.binary_path or self.config.get("binary_path") + if not binary_path: + self.fatal("Raptor requires a path to the binary.") + kw_options["binary"] = binary_path + if self.app in self.firefox_android_browsers: + # In production ensure we have correct app name, + # i.e. fennec_aurora or fennec_release etc. + kw_options["binary"] = self.query_package_name() + self.info( + "Set binary to %s instead of %s" + % (kw_options["binary"], binary_path) + ) + elif self.app == "safari" and not self.run_local: + binary_path = "/Applications/Safari.app/Contents/MacOS/Safari" + kw_options["binary"] = binary_path + else: # Running on Chromium + if not self.run_local: + # When running locally we already set the Chromium binary above, in init. + # In production, we already installed Chromium, so set the binary path + # to our install. + kw_options["binary"] = self.chromium_dist_path or "" + + # Options overwritten from **kw + if "test" in self.config: + kw_options["test"] = self.config["test"] + if "binary" in self.config: + kw_options["binary"] = self.config["binary"] + if self.symbols_path: + kw_options["symbolsPath"] = self.symbols_path + if self.config.get("obj_path", None) is not None: + kw_options["obj-path"] = self.config["obj_path"] + if self.config.get("mozbuild_path", None) is not None: + kw_options["mozbuild-path"] = self.config["mozbuild_path"] + if self.test_url_params: + kw_options["test-url-params"] = self.test_url_params + if self.config.get("device_name") is not None: + kw_options["device-name"] = self.config["device_name"] + if self.config.get("activity") is not None: + kw_options["activity"] = self.config["activity"] + if self.config.get("conditioned_profile") is not None: + kw_options["conditioned-profile"] = self.config["conditioned_profile"] + if self.config.get("benchmark_repository"): + kw_options["benchmark_repository"] = self.config["benchmark_repository"] + if self.config.get("benchmark_revision"): + kw_options["benchmark_revision"] = self.config["benchmark_revision"] + if self.config.get("benchmark_repository"): + kw_options["benchmark_branch"] = self.config["benchmark_branch"] + + kw_options.update(kw) + if self.host: + kw_options["host"] = self.host + # Configure profiling options + options.extend(self.query_gecko_profile_options()) + # Extra arguments + if args is not None: + options += args + if os.getenv("PERF_FLAGS"): + for option in os.getenv("PERF_FLAGS").split(): + if "=" in option: + kw_option, value = option.split("=") + kw_options[kw_option] = value + else: + options.extend(["--" + option]) + + if self.config.get("run_local", False): + options.extend(["--run-local"]) + if "raptor_cmd_line_args" in self.config: + options += self.config["raptor_cmd_line_args"] + if self.config.get("code_coverage", False): + options.extend(["--code-coverage"]) + if self.config.get("is_release_build", False): + options.extend(["--is-release-build"]) + if self.config.get("power_test", False): + options.extend(["--power-test"]) + if self.config.get("memory_test", False): + options.extend(["--memory-test"]) + if self.config.get("cpu_test", False): + options.extend(["--cpu-test"]) + if self.config.get("live_sites", False): + options.extend(["--live-sites"]) + if self.config.get("chimera", False): + options.extend(["--chimera"]) + if self.config.get("disable_perf_tuning", False): + options.extend(["--disable-perf-tuning"]) + if self.config.get("cold", False): + options.extend(["--cold"]) + if not self.config.get("fission", True): + options.extend(["--disable-fission"]) + if self.config.get("verbose", False): + options.extend(["--verbose"]) + if self.config.get("extra_prefs"): + options.extend( + ["--setpref={}".format(i) for i in self.config.get("extra_prefs")] + ) + if self.config.get("environment"): + options.extend( + ["--setenv={}".format(i) for i in self.config.get("environment")] + ) + if self.config.get("enable_marionette_trace", False): + options.extend(["--enable-marionette-trace"]) + if self.config.get("browser_cycles"): + options.extend( + ["--browser-cycles={}".format(self.config.get("browser_cycles"))] + ) + if self.config.get("test_bytecode_cache", False): + options.extend(["--test-bytecode-cache"]) + if self.config.get("collect_perfstats", False): + options.extend(["--collect-perfstats"]) + if self.config.get("extra_summary_methods"): + options.extend( + [ + "--extra-summary-methods={}".format(method) + for method in self.config.get("extra_summary_methods") + ] + ) + if self.config.get("webext", False): + options.extend(["--webext"]) + else: + for (arg,), details in Raptor.browsertime_options: + # Allow overriding defaults on the `./mach raptor-test ...` command-line + value = self.config.get(details["dest"]) + if value is None or value != getattr(self, details["dest"], None): + # Check for modifications done to the instance variables + value = getattr(self, details["dest"], None) + if value and arg not in self.config.get("raptor_cmd_line_args", []): + if isinstance(value, string_types): + options.extend([arg, os.path.expandvars(value)]) + elif isinstance(value, (tuple, list)): + for val in value: + options.extend([arg, val]) + else: + options.extend([arg]) + + for key, value in kw_options.items(): + options.extend(["--%s" % key, value]) + + return options + + def populate_webroot(self): + """Populate the production test machines' webroots""" + self.raptor_path = os.path.join( + self.query_abs_dirs()["abs_test_install_dir"], "raptor" + ) + if self.config.get("run_local"): + self.raptor_path = os.path.join(self.repo_path, "testing", "raptor") + + def clobber(self): + # Recreate the upload directory for storing the logcat collected + # during APK installation. + super(Raptor, self).clobber() + upload_dir = self.query_abs_dirs()["abs_blob_upload_dir"] + if not os.path.isdir(upload_dir): + self.mkdir_p(upload_dir) + + def install_android_app(self, apk, replace=False): + # Override AndroidMixin's install_android_app in order to capture + # logcat during the installation. If the installation fails, + # the logcat file will be left in the upload directory. + self.logcat_start() + try: + super(Raptor, self).install_android_app(apk, replace=replace) + finally: + self.logcat_stop() + + def download_and_extract(self, extract_dirs=None, suite_categories=None): + # Use in-tree wptserve for Python 3.10 compatibility + extract_dirs = [ + "tools/wptserve/*", + "tools/wpt_third_party/pywebsocket3/*", + ] + return super(Raptor, self).download_and_extract( + extract_dirs=extract_dirs, suite_categories=["common", "condprof", "raptor"] + ) + + def create_virtualenv(self, **kwargs): + """VirtualenvMixin.create_virtualenv() assumes we're using + self.config['virtualenv_modules']. Since we're installing + raptor from its source, we have to wrap that method here.""" + # If virtualenv already exists, just add to path and don't re-install. + # We need it in-path to import jsonschema later when validating output for perfherder. + _virtualenv_path = self.config.get("virtualenv_path") + + if self.clean: + rmtree(_virtualenv_path, ignore_errors=True) + + _python_interp = self.query_exe("python") + if "win" in self.platform_name() and os.path.exists(_python_interp): + multiprocessing.set_executable(_python_interp) + + if self.run_local and os.path.exists(_virtualenv_path): + self.info("Virtualenv already exists, skipping creation") + # ffmpeg exists outside of this virtual environment so + # we re-add it to the platform environment on repeated + # local runs of browsertime visual metric tests + self.setup_local_ffmpeg() + + if "win" in self.platform_name(): + _path = os.path.join(_virtualenv_path, "Lib", "site-packages") + else: + _path = os.path.join( + _virtualenv_path, + "lib", + os.path.basename(_python_interp), + "site-packages", + ) + + sys.path.append(_path) + return + + # virtualenv doesn't already exist so create it + # Install mozbase first, so we use in-tree versions + # Additionally, decide where to pull raptor requirements from. + if not self.run_local: + mozbase_requirements = os.path.join( + self.query_abs_dirs()["abs_test_install_dir"], + "config", + "mozbase_requirements.txt", + ) + raptor_requirements = os.path.join(self.raptor_path, "requirements.txt") + else: + mozbase_requirements = os.path.join( + os.path.dirname(self.raptor_path), + "config", + "mozbase_source_requirements.txt", + ) + raptor_requirements = os.path.join( + self.raptor_path, "source_requirements.txt" + ) + self.register_virtualenv_module( + requirements=[mozbase_requirements], + two_pass=True, + editable=True, + ) + + modules = ["pip>=1.5"] + + # Add modules required for visual metrics + py3_minor = sys.version_info.minor + if py3_minor <= 7: + modules.extend( + [ + "numpy==1.16.1", + "Pillow==6.1.0", + "scipy==1.2.3", + "pyssim==0.4", + "opencv-python==4.5.4.60", + ] + ) + else: # python version >= 3.8 + modules.extend( + [ + "numpy==1.22.0", + "Pillow==9.0.0", + "scipy==1.7.3", + "pyssim==0.4", + "opencv-python==4.5.4.60", + ] + ) + + if self.run_local: + self.setup_local_ffmpeg() + + # Require pip >= 1.5 so pip will prefer .whl files to install + super(Raptor, self).create_virtualenv(modules=modules) + + # Install Raptor dependencies + self.install_module(requirements=[raptor_requirements]) + + def setup_local_ffmpeg(self): + """Make use of the users local ffmpeg when running browsertime visual + metrics tests. + """ + + if "ffmpeg" in os.environ["PATH"]: + return + + platform = self.platform_name() + btime_cache = os.path.join(self.config["mozbuild_path"], "browsertime") + if "mac" in platform: + path_to_ffmpeg = os.path.join( + btime_cache, + FFMPEG_LOCAL_CACHE["mac"], + ) + elif "linux" in platform: + path_to_ffmpeg = os.path.join( + btime_cache, + FFMPEG_LOCAL_CACHE["linux"], + ) + elif "win" in platform: + path_to_ffmpeg = os.path.join( + btime_cache, + FFMPEG_LOCAL_CACHE["win"], + "bin", + ) + + if os.path.exists(path_to_ffmpeg): + os.environ["PATH"] += os.pathsep + path_to_ffmpeg + self.browsertime_ffmpeg = path_to_ffmpeg + self.info( + "Added local ffmpeg found at: %s to environment." % path_to_ffmpeg + ) + else: + raise Exception( + "No local ffmpeg binary found. Expected it to be here: %s" + % path_to_ffmpeg + ) + + def install(self): + if not self.config.get("noinstall", False): + if self.app in self.firefox_android_browsers: + self.device.uninstall_app(self.binary_path) + + # Check if the user supplied their own APK, and install + # that instead + installer_path = pathlib.Path( + self.raptor_path, "raptor", "user_upload.apk" + ) + if not installer_path.exists(): + installer_path = self.installer_path + + self.info(f"Installing APK from: {installer_path}") + self.install_android_app(str(installer_path)) + else: + super(Raptor, self).install() + + def _artifact_perf_data(self, src, dest): + if not os.path.isdir(os.path.dirname(dest)): + # create upload dir if it doesn't already exist + self.info("Creating dir: %s" % os.path.dirname(dest)) + os.makedirs(os.path.dirname(dest)) + self.info("Copying raptor results from %s to %s" % (src, dest)) + try: + copyfile(src, dest) + except Exception as e: + self.critical("Error copying results %s to upload dir %s" % (src, dest)) + self.info(str(e)) + + def run_tests(self, args=None, **kw): + """Run raptor tests""" + + # Get Raptor options + options = self.raptor_options(args=args, **kw) + + # Python version check + python = self.query_python_path() + self.run_command([python, "--version"]) + parser = RaptorOutputParser( + config=self.config, log_obj=self.log_obj, error_list=RaptorErrorList + ) + env = {} + env["MOZ_UPLOAD_DIR"] = self.query_abs_dirs()["abs_blob_upload_dir"] + if not self.run_local: + env["MINIDUMP_STACKWALK"] = self.query_minidump_stackwalk() + env["MINIDUMP_SAVE_PATH"] = self.query_abs_dirs()["abs_blob_upload_dir"] + env["RUST_BACKTRACE"] = "full" + if not os.path.isdir(env["MOZ_UPLOAD_DIR"]): + self.mkdir_p(env["MOZ_UPLOAD_DIR"]) + env = self.query_env(partial_env=env, log_level=INFO) + # adjust PYTHONPATH to be able to use raptor as a python package + if "PYTHONPATH" in env: + env["PYTHONPATH"] = self.raptor_path + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = self.raptor_path + + # mitmproxy needs path to mozharness when installing the cert, and tooltool + env["SCRIPTSPATH"] = scripts_path + env["EXTERNALTOOLSPATH"] = external_tools_path + + # Needed to load unsigned Raptor WebExt on release builds + if self.is_release_build: + env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" + + if self.repo_path is not None: + env["MOZ_DEVELOPER_REPO_DIR"] = self.repo_path + if self.obj_path is not None: + env["MOZ_DEVELOPER_OBJ_DIR"] = self.obj_path + if self.mozbuild_path is not None: + env["MOZ_MOZBUILD_DIR"] = self.mozbuild_path + + # Sets a timeout for how long Raptor should run without output + output_timeout = self.config.get("raptor_output_timeout", 3600) + # Run Raptor tests + run_tests = os.path.join(self.raptor_path, "raptor", "raptor.py") + + # Dynamically set the log level based on the raptor config for consistency + # throughout the test + mozlog_opts = [f"--log-tbpl-level={self.config['log_level']}"] + + if not self.run_local and "suite" in self.config: + fname_pattern = "%s_%%s.log" % self.config["test"] + mozlog_opts.append( + "--log-errorsummary=%s" + % os.path.join(env["MOZ_UPLOAD_DIR"], fname_pattern % "errorsummary") + ) + + def launch_in_debug_mode(cmdline): + cmdline = set(cmdline) + debug_opts = {"--debug", "--debugger", "--debugger_args"} + + return bool(debug_opts.intersection(cmdline)) + + if self.app in self.android_browsers: + self.logcat_start() + + command = [python, run_tests] + options + mozlog_opts + if launch_in_debug_mode(command): + raptor_process = subprocess.Popen(command, cwd=self.workdir, env=env) + raptor_process.wait() + else: + self.return_code = self.run_command( + command, + cwd=self.workdir, + output_timeout=output_timeout, + output_parser=parser, + env=env, + ) + + if self.app in self.android_browsers: + self.logcat_stop() + + if parser.minidump_output: + self.info("Looking at the minidump files for debugging purposes...") + for item in parser.minidump_output: + self.run_command(["ls", "-l", item]) + + elif not self.run_local: + # Copy results to upload dir so they are included as an artifact + self.info("Copying Raptor results to upload dir:") + + src = os.path.join(self.query_abs_dirs()["abs_work_dir"], "raptor.json") + dest = os.path.join(env["MOZ_UPLOAD_DIR"], "perfherder-data.json") + self.info(str(dest)) + self._artifact_perf_data(src, dest) + + # Make individual perfherder data JSON's for each supporting data type + for file in glob.glob( + os.path.join(self.query_abs_dirs()["abs_work_dir"], "*") + ): + path, filename = os.path.split(file) + + if not filename.startswith("raptor-"): + continue + + # filename is expected to contain a unique data name + # i.e. raptor-os-baseline-power.json would result in + # the data name os-baseline-power + data_name = "-".join(filename.split("-")[1:]) + data_name = ".".join(data_name.split(".")[:-1]) + + src = file + dest = os.path.join( + env["MOZ_UPLOAD_DIR"], "perfherder-data-%s.json" % data_name + ) + self._artifact_perf_data(src, dest) + + src = os.path.join( + self.query_abs_dirs()["abs_work_dir"], "screenshots.html" + ) + if os.path.exists(src): + dest = os.path.join(env["MOZ_UPLOAD_DIR"], "screenshots.html") + self.info(str(dest)) + self._artifact_perf_data(src, dest) + + # Allow log failures to over-ride successful runs of the test harness and + # give log failures priority, so that, for instance, log failures resulting + # in TBPL_RETRY cause a retry rather than simply reporting an error. + if parser.tbpl_status != TBPL_SUCCESS: + parser_status = EXIT_STATUS_DICT[parser.tbpl_status] + self.info( + "return code %s changed to %s due to log output" + % (str(self.return_code), str(parser_status)) + ) + self.return_code = parser_status + + +class RaptorOutputParser(OutputParser): + minidump_regex = re.compile( + r'''raptorError: "error executing: '(\S+) (\S+) (\S+)'"''' + ) + RE_PERF_DATA = re.compile(r".*PERFHERDER_DATA:\s+(\{.*\})") + + def __init__(self, **kwargs): + super(RaptorOutputParser, self).__init__(**kwargs) + self.minidump_output = None + self.found_perf_data = [] + self.tbpl_status = TBPL_SUCCESS + self.worst_log_level = INFO + self.harness_retry_re = TinderBoxPrintRe["harness_error"]["retry_regex"] + + def parse_single_line(self, line): + m = self.minidump_regex.search(line) + if m: + self.minidump_output = (m.group(1), m.group(2), m.group(3)) + + m = self.RE_PERF_DATA.match(line) + if m: + self.found_perf_data.append(m.group(1)) + + if self.harness_retry_re.search(line): + self.critical(" %s" % line) + self.worst_log_level = self.worst_level(CRITICAL, self.worst_log_level) + self.tbpl_status = self.worst_level( + TBPL_RETRY, self.tbpl_status, levels=TBPL_WORST_LEVEL_TUPLE + ) + return # skip base parse_single_line + super(RaptorOutputParser, self).parse_single_line(line) |