mach_commands.py (16736B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 # Originally taken from /talos/mach_commands.py 6 7 # Integrates raptor mozharness with mach 8 9 import json 10 import logging 11 import os 12 import socket 13 import subprocess 14 import sys 15 16 from mach.decorators import Command 17 from mach.util import get_state_dir 18 from mozbuild.base import BinaryNotFoundException, MozbuildObject 19 from mozbuild.base import MachCommandConditions as Conditions 20 21 HERE = os.path.dirname(os.path.realpath(__file__)) 22 23 ANDROID_BROWSERS = ["geckoview", "refbrow", "fenix", "chrome-m", "cstm-car-m"] 24 25 26 class RaptorRunner(MozbuildObject): 27 def run_test(self, raptor_args, kwargs): 28 """Setup and run mozharness. 29 30 We want to do a few things before running Raptor: 31 32 1. Clone mozharness 33 2. Make the config for Raptor mozharness 34 3. Run mozharness 35 """ 36 # Validate that the user is using a supported python version before doing anything else 37 max_py_major, max_py_minor = 3, 13 38 sys_maj, sys_min = sys.version_info.major, sys.version_info.minor 39 if sys_min > max_py_minor: 40 raise PythonVersionException( 41 print( 42 f"\tPlease downgrade your Python version as Raptor does not yet support Python " 43 f"versions greater than {max_py_major}.{max_py_minor}." 44 f"\n\tYou seem to currently be using Python {sys_maj}.{sys_min}." 45 f"\n\tSee here for a possible solution in debugging your python environment: " 46 f"https://firefox-source-docs.mozilla.org/testing/perfdocs/" 47 f"debugging.html#debugging-local-python-environment" 48 ) 49 ) 50 self.init_variables(raptor_args, kwargs) 51 self.make_config() 52 self.write_config() 53 self.make_args() 54 return self.run_mozharness() 55 56 def init_variables(self, raptor_args, kwargs): 57 self.raptor_args = raptor_args 58 59 if kwargs.get("host") == "HOST_IP": 60 kwargs["host"] = os.environ["HOST_IP"] 61 self.host = kwargs["host"] 62 self.is_release_build = kwargs["is_release_build"] 63 self.live_sites = kwargs["live_sites"] 64 self.disable_perf_tuning = kwargs["disable_perf_tuning"] 65 self.conditioned_profile = kwargs["conditioned_profile"] 66 self.device_name = kwargs["device_name"] 67 self.enable_marionette_trace = kwargs["enable_marionette_trace"] 68 self.browsertime_visualmetrics = kwargs["browsertime_visualmetrics"] 69 self.browsertime_node = kwargs["browsertime_node"] 70 self.clean = kwargs["clean"] 71 self.screenshot_on_failure = kwargs["screenshot_on_failure"] 72 73 if Conditions.is_android(self) or kwargs["app"] in ANDROID_BROWSERS: 74 self.binary_path = None 75 else: 76 self.binary_path = kwargs.get("binary") or self.get_binary_path() 77 78 self.python = sys.executable 79 80 self.raptor_dir = os.path.join(self.topsrcdir, "testing", "raptor") 81 self.mozharness_dir = os.path.join(self.topsrcdir, "testing", "mozharness") 82 self.config_file_path = os.path.join( 83 self._topobjdir, "testing", "raptor-in_tree_conf.json" 84 ) 85 86 self.virtualenv_script = os.path.join( 87 self.topsrcdir, "third_party", "python", "virtualenv", "virtualenv.py" 88 ) 89 self.virtualenv_path = os.path.join(self._topobjdir, "testing", "raptor-venv") 90 91 def make_config(self): 92 default_actions = [ 93 "populate-webroot", 94 "create-virtualenv", 95 "install-chromium-distribution", 96 "run-tests", 97 ] 98 self.config = { 99 "run_local": True, 100 "binary_path": self.binary_path, 101 "repo_path": self.topsrcdir, 102 "raptor_path": self.raptor_dir, 103 "obj_path": self.topobjdir, 104 "log_name": "raptor", 105 "virtualenv_path": self.virtualenv_path, 106 "pypi_url": "http://pypi.org/simple", 107 "base_work_dir": self.mozharness_dir, 108 "exes": { 109 "python": self.python, 110 "virtualenv": [self.python, self.virtualenv_script], 111 }, 112 "title": socket.gethostname(), 113 "default_actions": default_actions, 114 "raptor_cmd_line_args": self.raptor_args, 115 "host": self.host, 116 "live_sites": self.live_sites, 117 "disable_perf_tuning": self.disable_perf_tuning, 118 "conditioned_profile": self.conditioned_profile, 119 "is_release_build": self.is_release_build, 120 "device_name": self.device_name, 121 "enable_marionette_trace": self.enable_marionette_trace, 122 "browsertime_visualmetrics": self.browsertime_visualmetrics, 123 "browsertime_node": self.browsertime_node, 124 "mozbuild_path": get_state_dir(), 125 "clean": self.clean, 126 "screenshot_on_failure": self.screenshot_on_failure, 127 } 128 129 sys.path.insert(0, os.path.join(self.topsrcdir, "tools", "browsertime")) 130 try: 131 import platform 132 133 import mach_commands as browsertime 134 135 # We don't set `browsertime_{chromedriver,geckodriver} -- those will be found by 136 # browsertime in its `node_modules` directory, which is appropriate for local builds. 137 # We don't set `browsertime_ffmpeg` yet: it will need to be on the path. There is code 138 # to configure the environment including the path in 139 # `tools/browsertime/mach_commands.py` but integrating it here will take more effort. 140 self.config.update({ 141 "browsertime_browsertimejs": browsertime.browsertime_path(), 142 "browsertime_vismet_script": browsertime.visualmetrics_path(), 143 }) 144 145 def _get_browsertime_package(): 146 with open( 147 os.path.join( 148 self.topsrcdir, 149 "tools", 150 "browsertime", 151 "node_modules", 152 "browsertime", 153 "package.json", 154 ) 155 ) as package: 156 return json.load(package) 157 158 def _get_browsertime_resolved(): 159 try: 160 with open( 161 os.path.join( 162 self.topsrcdir, 163 "tools", 164 "browsertime", 165 "node_modules", 166 ".package-lock.json", 167 ) 168 ) as package_lock: 169 return json.load(package_lock)["packages"][ 170 "node_modules/browsertime" 171 ]["resolved"] 172 except FileNotFoundError: 173 # Older versions of node/npm add this metadata to package.json 174 return _get_browsertime_package()["_from"] 175 176 def _should_install(): 177 # If ffmpeg doesn't exist in the .mozbuild directory, 178 # then we should install 179 btime_cache = os.path.join(self.config["mozbuild_path"], "browsertime") 180 if not os.path.exists(btime_cache) or not any([ 181 "ffmpeg" in cache_dir for cache_dir in os.listdir(btime_cache) 182 ]): 183 return True 184 185 # If browsertime doesn't exist, install it 186 if not os.path.exists( 187 self.config["browsertime_browsertimejs"] 188 ) or not os.path.exists(self.config["browsertime_vismet_script"]): 189 return True 190 191 # Browsertime exists, check if it's outdated 192 with open( 193 os.path.join(self.topsrcdir, "tools", "browsertime", "package.json") 194 ) as new: 195 new_pkg = json.load(new) 196 197 return not _get_browsertime_resolved().endswith( 198 new_pkg["devDependencies"]["browsertime"] 199 ) 200 201 def _get_browsertime_version(): 202 # Returns the (version number, current commit) used 203 return ( 204 _get_browsertime_package()["version"], 205 _get_browsertime_resolved(), 206 ) 207 208 # Check if browsertime scripts exist and try to install them if 209 # they aren't 210 if _should_install(): 211 # TODO: Make this "integration" nicer in the near future 212 print("Missing browsertime files...attempting to install") 213 subprocess.check_output( 214 [ 215 os.path.join(self.topsrcdir, "mach"), 216 "browsertime", 217 "--setup", 218 "--clobber", 219 ], 220 shell="windows" in platform.system().lower(), 221 ) 222 if _should_install(): 223 raise Exception( 224 "Failed installation attempt. Cannot find browsertime scripts. " 225 "Run `./mach browsertime --setup --clobber` to set it up." 226 ) 227 228 # Bug 1766112 - For the time being, we need to trigger a 229 # clean build to upgrade browsertime. This should be disabled 230 # after some time. 231 print( 232 "Setting --clean to True to rebuild Python " 233 "environment for Browsertime upgrade..." 234 ) 235 self.config["clean"] = True 236 237 print("Using browsertime version %s from %s" % _get_browsertime_version()) 238 239 finally: 240 sys.path = sys.path[1:] 241 242 def make_args(self): 243 self.args = { 244 "config": {}, 245 "initial_config_file": self.config_file_path, 246 } 247 248 def write_config(self): 249 try: 250 config_file = open(self.config_file_path, "w") 251 config_file.write(json.dumps(self.config)) 252 config_file.close() 253 except OSError as e: 254 err_str = "Error writing to Raptor Mozharness config file {0}:{1}" 255 print(err_str.format(self.config_file_path, str(e))) 256 raise e 257 258 def run_mozharness(self): 259 sys.path.insert(0, self.mozharness_dir) 260 from mozharness.mozilla.testing.raptor import Raptor 261 262 raptor_mh = Raptor( 263 config=self.args["config"], 264 initial_config_file=self.args["initial_config_file"], 265 ) 266 return raptor_mh.run() 267 268 269 def setup_node(command_context): 270 """Fetch the latest node-22 binary and install it into the .mozbuild directory.""" 271 import platform 272 273 from mozbuild.artifact_commands import artifact_toolchain 274 from mozbuild.nodeutil import find_node_executable 275 from packaging.version import Version 276 277 print("Setting up node for browsertime...") 278 state_dir = get_state_dir() 279 cache_path = os.path.join(state_dir, "browsertime", "node-22") 280 281 def __check_for_node(): 282 # Check standard locations first 283 node_exe = find_node_executable(min_version=Version("22.0.0")) 284 if node_exe and (node_exe[0] is not None): 285 return node_exe[0] 286 if not os.path.exists(cache_path): 287 return None 288 289 # Check the browsertime-specific node location next 290 node_name = "node" 291 if platform.system() == "Windows": 292 node_name = "node.exe" 293 node_exe_path = os.path.join( 294 state_dir, 295 "browsertime", 296 "node-22", 297 "node", 298 ) 299 else: 300 node_exe_path = os.path.join( 301 state_dir, 302 "browsertime", 303 "node-22", 304 "node", 305 "bin", 306 ) 307 308 node_exe = os.path.join(node_exe_path, node_name) 309 if not os.path.exists(node_exe): 310 return None 311 312 return node_exe 313 314 node_exe = __check_for_node() 315 if node_exe is None: 316 toolchain_job = "{}-node-22" 317 plat = platform.system() 318 if plat == "Windows": 319 toolchain_job = toolchain_job.format("win64") 320 elif plat == "Darwin": 321 if platform.processor() == "arm": 322 toolchain_job = toolchain_job.format("macosx64-aarch64") 323 else: 324 toolchain_job = toolchain_job.format("macosx64") 325 else: 326 toolchain_job = toolchain_job.format("linux64") 327 328 print(f"Downloading Node 22 from Taskcluster toolchain {toolchain_job}...") 329 330 if not os.path.exists(cache_path): 331 os.makedirs(cache_path, exist_ok=True) 332 333 # Change directories to where node should be installed 334 # before installing. Otherwise, it gets installed in the 335 # top level of the repo (or the current working directory). 336 cur_dir = os.getcwd() 337 os.chdir(cache_path) 338 artifact_toolchain( 339 command_context, 340 verbose=False, 341 from_build=[toolchain_job], 342 no_unpack=False, 343 retry=0, 344 cache_dir=cache_path, 345 ) 346 os.chdir(cur_dir) 347 348 node_exe = __check_for_node() 349 if node_exe is None: 350 raise Exception("Could not find Node v22 binary for Raptor-Browsertime") 351 352 print("Finished downloading Node v22 from Taskcluster") 353 354 print("Node v22+ found at: %s" % node_exe) 355 return node_exe 356 357 358 def create_parser(): 359 sys.path.insert(0, HERE) # allow to import the raptor package 360 from raptor.cmdline import create_parser 361 362 return create_parser(mach_interface=True) 363 364 365 @Command( 366 "raptor", 367 category="testing", 368 description="Run Raptor performance tests.", 369 parser=create_parser, 370 ) 371 def run_raptor(command_context, **kwargs): 372 build_obj = command_context 373 374 # Setup node for browsertime 375 kwargs["browsertime_node"] = setup_node(command_context) 376 377 is_android = Conditions.is_android(build_obj) or kwargs["app"] in ANDROID_BROWSERS 378 379 if is_android: 380 from mozrunner.devices.android_device import ( 381 InstallIntent, 382 verify_android_device, 383 ) 384 385 install = ( 386 InstallIntent.NO if kwargs.pop("no_install", False) else InstallIntent.YES 387 ) 388 verbose = False 389 if ( 390 kwargs.get("log_mach_verbose") 391 or kwargs.get("log_tbpl_level") == "debug" 392 or kwargs.get("log_mach_level") == "debug" 393 or kwargs.get("log_raw_level") == "debug" 394 ): 395 verbose = True 396 if not verify_android_device( 397 build_obj, 398 install=install, 399 app=kwargs["binary"], 400 verbose=verbose, 401 xre=True, 402 ): # Equivalent to 'run_local' = True. 403 print( 404 "****************************************************************************" 405 ) 406 print( 407 "Unable to verify device, please check your attached/connected android device" 408 ) 409 print( 410 "****************************************************************************" 411 ) 412 return 1 413 # Disable fission until geckoview supports fission by default. 414 # Need fission on Android? Use '--setpref fission.autostart=true' 415 kwargs["fission"] = False 416 417 # Remove mach global arguments from sys.argv to prevent them 418 # from being consumed by raptor. Treat any item in sys.argv 419 # occuring before "raptor" as a mach global argument. 420 argv = [] 421 in_mach = True 422 for arg in sys.argv: 423 if not in_mach: 424 argv.append(arg) 425 if arg.startswith("raptor"): 426 in_mach = False 427 428 raptor = command_context._spawn(RaptorRunner) 429 430 try: 431 return raptor.run_test(argv, kwargs) 432 except BinaryNotFoundException as e: 433 command_context.log( 434 logging.ERROR, "raptor", {"error": str(e)}, "ERROR: {error}" 435 ) 436 command_context.log(logging.INFO, "raptor", {"help": e.help()}, "{help}") 437 return 1 438 except Exception as e: 439 print(repr(e)) 440 return 1 441 442 443 @Command( 444 "raptor-test", 445 category="testing", 446 description="Run Raptor performance tests.", 447 parser=create_parser, 448 ) 449 def run_raptor_test(command_context, **kwargs): 450 return run_raptor(command_context, **kwargs) 451 452 453 class PythonVersionException(Exception): 454 pass